diff --git a/cli/internal/mem/skill_example_test.go b/cli/internal/mem/skill_example_test.go new file mode 100644 index 000000000..f5e30bb51 --- /dev/null +++ b/cli/internal/mem/skill_example_test.go @@ -0,0 +1,51 @@ +package mem + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// quoted matches one single-quoted shell argument. +var quoted = regexp.MustCompile(`'([^']*)'`) + +// exampleBody returns the handoff body a skill pipes into `dotf mem +// handoff-write`: the single-quoted printf arguments, one per line, ahead of the +// pipe into handoff-write. printf rather than a heredoc, because a heredoc's +// terminator breaks when the example sits indented inside a list item. +func exampleBody(t *testing.T, skill string) string { + t.Helper() + raw, err := os.ReadFile(filepath.Join("../../..", "harness", "skills", skill, "SKILL.md")) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(string(raw), "\n") + for i, l := range lines { + if !strings.Contains(l, "printf '%s\\n'") || !strings.HasSuffix(strings.TrimSpace(l), "\\") { + continue + } + var body []string + for _, next := range lines[i+1:] { + for _, m := range quoted.FindAllStringSubmatch(next, -1) { + body = append(body, m[1]) + } + if strings.Contains(next, "dotf mem handoff-write") { + return strings.Join(body, "\n") + "\n" + } + } + } + t.Fatalf("%s SKILL.md has no `printf '%%s\\n' ... | dotf mem handoff-write` example", skill) + return "" +} + +// A skill that tells an agent to pipe a body into handoff-write must show a body +// the writer accepts without warning: an example without `**Next action:**` is +// copied literally and writes a thread with no first step (#3110). +func TestAutopilotHandoffExampleCarriesNextAction(t *testing.T) { + body := exampleBody(t, "autopilot") + if w := ThreadWarnings(body); len(w) != 0 { + t.Errorf("the autopilot example body draws handoff-write warnings: %q", w) + } +} diff --git a/cli/internal/mem/thread.go b/cli/internal/mem/thread.go index 0d5769045..12d844768 100644 --- a/cli/internal/mem/thread.go +++ b/cli/internal/mem/thread.go @@ -111,7 +111,7 @@ func ThreadWarnings(body string) []string { th := ParseThread(body) var out []string if th.All("Next action") == "" { - out = append(out, "no Next action, so the next session has no first step") + out = append(out, "no `**Next action:**` line, so the next session has no first step") } for _, f := range th.Unknown() { out = append(out, fmt.Sprintf("label %q is not one of: %s", f.Label, strings.Join(ThreadLabels, ", "))) diff --git a/cli/internal/mem/thread_test.go b/cli/internal/mem/thread_test.go index 89a21e915..a90a8b338 100644 --- a/cli/internal/mem/thread_test.go +++ b/cli/internal/mem/thread_test.go @@ -117,7 +117,7 @@ func TestThreadWarningsNameOnlyWhatIsMissingOrUnknown(t *testing.T) { t.Errorf("a canonical thread drew warnings: %q", w) } w := ThreadWarnings("**Last task:** x.\n**Next action:**\n**Trap:** y.\n") - if len(w) != 2 || !strings.Contains(w[0], "Next action") || !strings.Contains(w[1], `"Trap"`) { + if len(w) != 2 || !strings.Contains(w[0], "**Next action:**") || !strings.Contains(w[1], `"Trap"`) { t.Errorf("warnings = %q, want the empty Next action and the unknown Trap", w) } } diff --git a/harness/skills/autopilot/SKILL.md b/harness/skills/autopilot/SKILL.md index abcc2d0e4..47193d866 100644 --- a/harness/skills/autopilot/SKILL.md +++ b/harness/skills/autopilot/SKILL.md @@ -1,7 +1,7 @@ --- generated: true generated_from: 00_meta/skills/autopilot/SKILL.md -generated_sha: da95f19a047826e5 +generated_sha: 9c64789731c9bbb6 id: autopilot-skill type: skill status: active @@ -326,8 +326,15 @@ When the operator returns, the time window expires, the backlog empties, or a st ```bash MEMORY_MD="$(dotf env path VAULT_PATH)/10_projects//memory/MEMORY.md" dotf mem thread # which thread am I: wt-, or main@ - printf '%s' "$BODY" | dotf mem handoff-write --memory "$MEMORY_MD" --agent + printf '%s\n' \ + '> Updated:
' \ + '**Last task:**
' \ + '**Next action:**
' \ + '**Awaiting Manu:** ' | + dotf mem handoff-write --memory "$MEMORY_MD" --agent ``` + The `**Next action:**` line is not optional: without it `handoff-write` warns + "no `**Next action:**` line" and still writes the thread, so the next session starts with no first step. The full checklist (journal, board reconciliation, artifact summary, one next action) is `00_meta/skills/handoff/SKILL.md`. `--memory "$REPO_ROOT/MEMORY.md"` — which an earlier draft used — would write the handoff nowhere near where the next session reads it.