@cleocode/goal 0.0.0-stage → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/cli/ocg.ts +149 -0
  5. package/dist/cli.js +8708 -0
  6. package/dist/tui.d.ts +2 -0
  7. package/dist/tui.js +1920 -0
  8. package/package.json +79 -4
  9. package/rpc.ts +2 -0
  10. package/server.ts +3 -0
  11. package/skills/write-goal/SKILL.md +211 -0
  12. package/skills/write-goal/assets/goal.template.yaml +38 -0
  13. package/skills/write-goal/references/examples.md +283 -0
  14. package/skills/write-goal/references/goal-schema.md +53 -0
  15. package/skills/write-goal/references/interview.md +36 -0
  16. package/src/cli/install.ts +125 -0
  17. package/src/cli/status.ts +116 -0
  18. package/src/contract/parse.ts +454 -0
  19. package/src/contract/render.ts +134 -0
  20. package/src/contract/types.ts +158 -0
  21. package/src/engine/amendment.ts +51 -0
  22. package/src/engine/amendments.ts +72 -0
  23. package/src/engine/cleo-bridge.ts +339 -0
  24. package/src/engine/cleo-link.ts +150 -0
  25. package/src/engine/continue-work.ts +131 -0
  26. package/src/engine/decisions.ts +44 -0
  27. package/src/engine/dogfood-record.ts +278 -0
  28. package/src/engine/handoff.ts +102 -0
  29. package/src/engine/interrupt-policy.ts +45 -0
  30. package/src/engine/inventory.ts +70 -0
  31. package/src/engine/launches.ts +82 -0
  32. package/src/engine/loop-stop.ts +50 -0
  33. package/src/engine/loop.ts +109 -0
  34. package/src/engine/next-work.ts +82 -0
  35. package/src/engine/owner-intent.ts +71 -0
  36. package/src/engine/owner-reviews.ts +178 -0
  37. package/src/engine/peer-announce.ts +67 -0
  38. package/src/engine/peer-transport.ts +78 -0
  39. package/src/engine/policy.ts +42 -0
  40. package/src/engine/registry.ts +119 -0
  41. package/src/engine/similarity.ts +65 -0
  42. package/src/engine/stale-decisions.ts +68 -0
  43. package/src/engine/state.ts +185 -0
  44. package/src/engine/store.ts +263 -0
  45. package/src/engine/tick-record.ts +129 -0
  46. package/src/engine/tick.ts +207 -0
  47. package/src/rpc.ts +182 -0
  48. package/src/server/app.ts +4320 -0
  49. package/src/server/help.ts +50 -0
  50. package/src/server/index.ts +23 -0
  51. package/src/server/intent.ts +22 -0
  52. package/src/server/messages.ts +228 -0
  53. package/src/summary/audit.ts +76 -0
  54. package/src/summary/build.ts +314 -0
  55. package/src/tui/dashboard.ts +322 -0
  56. package/src/tui/decision-form.ts +12 -0
  57. package/src/tui/format.ts +127 -0
  58. package/src/tui/index.tsx +718 -0
  59. package/src/tui/manager.ts +94 -0
  60. package/src/tui/navigation.ts +11 -0
  61. package/src/tui/pointer-select.ts +20 -0
  62. package/src/tui/tracer.ts +51 -0
  63. package/src/util/git.ts +102 -0
  64. package/src/util/ids.ts +20 -0
  65. package/src/util/shell.ts +42 -0
  66. package/src/verify/artifact.ts +95 -0
  67. package/src/verify/pipeline.ts +200 -0
  68. package/tui.tsx +2 -0
@@ -0,0 +1,50 @@
1
+ // T025 — the /goal help surface: one pure builder producing an agent-forward
2
+ // guide (commands with arguments, the worker/owner permission split, storage
3
+ // layout, mid-run rules). Rendered by /goal help and pinned by a unit test so
4
+ // it cannot drift from the engine's real semantics.
5
+
6
+ export type HelpFacts = { version: string }
7
+
8
+ export function goalHelp({ version }: HelpFacts): string {
9
+ return [
10
+ `# opencode-goal ${version} — the goal loop, for agents and owners`,
11
+ "",
12
+ "## Commands (owner)",
13
+ "- `/goal new <what you want done>` — the write-goal interview (skill attached; similar-goal probe runs first)",
14
+ "- `/goal start <slug> [acknowledge-supersede]` — rehearse every command check, then lock and run",
15
+ "- `/goal status` · `/goal pause` · `/goal resume` · `/goal verify` — status costs no model turn",
16
+ "- `/goal abort` — end the run for good (ledger and evidence stay)",
17
+ "- `/goal amend [confirm]` — propose/confirm a contract re-lock after editing goal.yaml out of band",
18
+ "- `/goal archive` — demote a finished goal to .opencode/goals-archive/ (never deleted)",
19
+ "- `/goal approve <C#>` · `/goal reject <C#> <why>` — owner sign-off for human criteria",
20
+ "- `/goal validate <slug>` · `/goal list [all]` — validate a contract; list live (or +archived) goals",
21
+ "- `/goal cleo [install|init|sync]` — probe the CLEO CLI and this project's .cleo workspace (initialized / uninitialized / CLI absent); `install` and `init` run the exact documented command only on your explicit choice, never silently and never with sudo",
22
+ "",
23
+ "## Worker tools (the agent in the loop)",
24
+ "- `goal_progress` — record the step you are on, one line of change, the next action",
25
+ "- `goal_claim` — claim completion with evidence per criterion; the HOST then verifies you",
26
+ "- `goal_block` — stop for an owner-only blocker (missing credential, decision)",
27
+ "- `goal_flag` — flag a criterion that is contradictory, impossible or unsafe as written",
28
+ "- `goal_wait <seconds>` — pause cheaply while an external process runs",
29
+ "- `goal_status` / `goal_validate` — read-only checks",
30
+ "- `goal_verdict` — reserved for the read-only verifier child; never available to the worker",
31
+ "",
32
+ "All `goal_*` tools are called **directly by name** — they are not registered with Code Mode, so `search()`/`tools.*` inside `execute` will never find them (a routing attempt returns an unrelated Unknown-tool error).",
33
+ "",
34
+ "## Storage layout (per project)",
35
+ "- `.opencode/goals/<slug>/goal.yaml` — the locked contract (sha256)",
36
+ "- `.opencode/goals/<slug>/run.json` — live run state; `ledger.jsonl` — every event, append-only",
37
+ "- `.opencode/goals/<slug>/evidence/<runId>/` — locked contract copy, rehearsal baseline, verify results, verifier transcripts, amendment records",
38
+ "- `.opencode/goals-archive/<slug>/` — demoted goals; the machine registry still answers \"was this ever a goal here?\"",
39
+ "",
40
+ "## Mid-run rules (the contract is enforced, not advice)",
41
+ "- The contract is rendered into every request; compaction cannot lose it.",
42
+ "- Never edit `.opencode/goals/**` while a goal exists — the write guard blocks it, running or stopped.",
43
+ "- Completion is proven, not declared: host checks run first, then an independent verifier child whose quotes are re-read. A rejected claim returns a HOST VERDICT turn — read it, fix the work, claim again.",
44
+ "- Questions are deferred during a run: make the reversible choice, note it in goal_progress, or call goal_block.",
45
+ "- Command checks live on ONE line and are rehearsed at start; a check that cannot run refuses the lock.",
46
+ "- Supersession replaces a goal: `supersedes: <slug>@<lock>` in the new contract; live predecessors need your explicit acknowledgment.",
47
+ "- Describe changes in the Composer; goal_propose_amendment stages an exact diff, then goal_resolve_amendment binds genuine owner chat once. No sidebar Accept or manual command is required.",
48
+ "- Every accepted amendment is a generation-bound, audit-trailed re-lock: the prior generation is retained, changed criteria reset and re-verify, and acceptance resumes the run in the same act.",
49
+ ].join("\n")
50
+ }
@@ -0,0 +1,23 @@
1
+ // OpenCode 2 server entry. A plain-object default export with type-only imports,
2
+ // so the plugin loads without @opencode/plugin installed at runtime.
3
+ import type { Plugin } from "@opencode/plugin"
4
+ import { GoalApp } from "./app"
5
+
6
+ // S6: setup can run more than once for a location; retire the old instance first.
7
+ const instances = new Map<string, GoalApp>()
8
+
9
+ export default {
10
+ id: "opencode-goal",
11
+ async setup(ctx: Plugin.Context) {
12
+ const key = ctx.location.directory
13
+ const previous = instances.get(key)
14
+ if (previous) await previous.stop()
15
+ const app = new GoalApp(ctx)
16
+ instances.set(key, app)
17
+ await app.start()
18
+ return async () => {
19
+ await app.stop()
20
+ if (instances.get(key) === app) instances.delete(key)
21
+ }
22
+ },
23
+ }
@@ -0,0 +1,22 @@
1
+ // Recognize only the existing, explicit command shorthand. Everything
2
+ // conversational remains the owner's text for the agent to interpret; this
3
+ // is deliberately NOT a host-side NLP-to-shell parser.
4
+ export function directGoalCommand(text: string, slugs: readonly string[]): boolean {
5
+ const [head = "", ...rest] = text.trim().split(/\s+/)
6
+ const sub = head.toLowerCase()
7
+ const arg = rest.join(" ")
8
+ const named = (s: string) => slugs.includes(s)
9
+ switch (sub) {
10
+ case "new": case "write": return true
11
+ case "status": case "help": return !arg
12
+ case "list": return !arg || arg === "all"
13
+ case "start": return !arg || named(rest[0] ?? "") && rest.slice(1).every((t) => ["fresh", "acknowledge-supersede"].includes(t))
14
+ case "validate": case "pause": case "resume": case "abort": case "verify": case "archive": case "attach": case "revert":
15
+ return !arg || rest.length === 1 && named(rest[0]!)
16
+ case "cleo": return true
17
+ case "amend": return !arg || arg === "confirm" || rest.length === 1 && named(rest[0]!)
18
+ case "approve": return /^C\d+(?:\s+[a-z0-9-]+)?$/i.test(arg)
19
+ case "reject": return /^C\d+(?:\s|$)/i.test(arg)
20
+ default: return false
21
+ }
22
+ }
@@ -0,0 +1,228 @@
1
+ // T054 — actionable message content. Owner directive: "treat users like
2
+ // they need everything spelled out — actionable, option-based." Every
3
+ // engine notice carries the CAUSE plus CONCRETE CHOICES: what to run, what
4
+ // to approve, what to read. This module owns the wording of every notice
5
+ // the engine emits; app.ts renders nothing raw. The choices render inline
6
+ // (a plain toast stays actionable) and ride the rpc payload as structured
7
+ // choices the T050 dialogs wire to rpc.act.
8
+ export type Choice = {
9
+ /** short imperative label for a dialog button, e.g. "Resume goal" */
10
+ label: string
11
+ /** the owner command or worker action to run, e.g. "/goal resume" */
12
+ run: string
13
+ /** rpc.act action for dialog wiring (optional; commands run as commands) */
14
+ act?: string
15
+ arg?: string
16
+ }
17
+
18
+ export type EngineMessage = {
19
+ id: string
20
+ text: string
21
+ choices: Choice[]
22
+ level: "info" | "success" | "warning" | "error"
23
+ attention?: "done" | "question" | "error"
24
+ }
25
+
26
+ const msg = (id: string, level: EngineMessage["level"], text: string, choices: Choice[], attention?: EngineMessage["attention"]): EngineMessage => ({
27
+ id,
28
+ level,
29
+ text,
30
+ choices,
31
+ ...(attention ? { attention } : {}),
32
+ })
33
+
34
+ /** Inline rendering: cause + explicit choices, one line, toast-safe. */
35
+ export function renderMessage(m: EngineMessage): string {
36
+ return `${m.text} — ${m.choices.map((c) => c.run).join(" · ")}`
37
+ }
38
+
39
+ const resume = { label: "Resume goal", run: "/goal resume", act: "resume" }
40
+ const abort = { label: "Abort goal", run: "/goal abort", act: "abort" }
41
+ const status = { label: "Show status", run: "/goal status" }
42
+ const verify = { label: "Verify now", run: "/goal verify", act: "verify" }
43
+ const archive = { label: "Archive goal", run: "/goal archive", act: "archive" }
44
+
45
+ export const M = {
46
+ managerOperation: (action: string, title: string) => msg("goal.manager.operation", "warning", `${action.toUpperCase()} the selected goal "${title}"? This applies only to the reviewed run and contract. History stays intact; no operation executes until you choose.`, [
47
+ { label: `${action[0]!.toUpperCase()}${action.slice(1)} selected goal`, run: `answer with this option to ${action} the exact reviewed goal; preserve its history`, act: action },
48
+ { label: "Cancel operation", run: "keep the selected goal unchanged" },
49
+ { label: "Comment instead", run: "reply with commentary; no operation is authorized" },
50
+ ]),
51
+ ownerOperationFailed: (reason: string) => msg("goal.owner-operation.failed", "warning", `The selected owner operation did not succeed: ${reason}. No successful resolution was recorded.`, [{ label: "Inspect current status", run: "goal_status before requesting the exact review again" }, { label: "Restore pending review", run: "goal_review for a pending unchanged target" }]),
52
+ amendmentExpired: () => msg("goal.amend.expired", "warning", "The amendment review expired; the locked contract and evidence are unchanged.", [{ label: "Stage a fresh diff", run: "goal_propose_amendment with the current target lock" }]),
53
+ amendmentChanged: () => msg("goal.amend.changed", "warning", "The goal/run/contract changed after amendment review; no amendment was applied.", [{ label: "Read and stage again", run: "read the selected goal and stage a new reviewed diff" }]),
54
+ amendmentInterrupted: () => msg("goal.amend.interrupted", "error", "The amendment commit was interrupted after recorded acceptance; no further work is safe until recovery finishes.", [{ label: "Recover the accepted generation", run: "open a reloaded location to recover the host write-ahead receipt" }]),
55
+ launchExpired: () => msg("goal.launch.expired", "warning", "The launch review expired; nothing started. A fresh review is needed before consent can be used.", [{ label: "Review again", run: "start a new native launch review" }]),
56
+ launchChanged: () => msg("goal.launch.changed", "warning", "The contract changed after review; nothing started. The previous choice cannot approve different bytes.", [{ label: "Review the new contract", run: "read the revised contract and start a new native review" }]),
57
+ launchOutcome: (slug: string, outcome: string, result: string) =>
58
+ msg("goal.launch.outcome", outcome === "succeeded" ? "success" : "warning", `Launch ${slug} (${outcome}): ${result}`, [status, { label: "Inspect the exact review", run: "goal_review; no old approval is replayed" }]),
59
+ launchActivity: (slug: string, detail: string) =>
60
+ msg("goal.launch.activity", "info", `${slug}: ${detail}`, [status]),
61
+ launchFailed: (reason: string) => msg("goal.launch.failed", "error", `Launch failed; the decision has an explicit recoverable result: ${reason}`, [{ label: "Inspect and review again", run: "read the failed launch result and start a new native review" }]),
62
+ goalStarted: (title: string) =>
63
+ msg("goal.started", "success", `Goal started: ${title}. The contract is locked and rendered into every request.`, [status, resume]),
64
+ cleanStarted: (targetID: string, slug: string) =>
65
+ msg("goal.clean-start", "success", `Started clean: the kickoff prompt landed in the new session "${targetID}" (goal kickoff · ${slug}). This session stays untouched — nothing was cleared.`, [
66
+ { label: `Open the goal session`, run: `open the session "goal kickoff · ${slug}"` },
67
+ status,
68
+ ]),
69
+ startPicker: (goals: Array<{ slug: string; title: string; valid: boolean }>) =>
70
+ msg("goal.start-picker", "info", `Start which goal? ${goals.length} startable in this project:`, goals.map((g) => ({
71
+ label: `${g.slug} — ${g.title}`,
72
+ run: `/goal start ${g.slug}`,
73
+ act: "start",
74
+ arg: g.slug,
75
+ }))),
76
+ startPickerEmpty: (folders: number) =>
77
+ msg("goal.start-picker.empty", "warning", `No startable goal here (${folders} goal folder${folders === 1 ? "" : "s"} exist${folders ? ", none fresh" : ""}). Write one first.`, [
78
+ { label: "Write a goal", run: "/goal new <what you want done>" },
79
+ ]),
80
+ goalSummary: (headline: string, caveats: number, status2: string) =>
81
+ msg(`goal.summary.${status2}`, status2 === "complete" ? "success" : "warning", `goal.summary — ${headline}${caveats ? ` (${caveats} caveat${caveats > 1 ? "s" : ""}: provenance and unproven scope flagged in the dialog/evidence)` : ""}. The loop has stopped; follow-ups live in this dialog and the goal folder.`, [
82
+ { label: "Show status", run: "/goal status" },
83
+ ...(status2 === "complete" ? [{ label: "Archive", run: "/goal archive", act: "archive" }, { label: "Start the next goal", run: "/goal new <what you want next>" }] : [{ label: "Resolve review", run: "/goal approve <C#>" }]),
84
+ ]),
85
+ policyApplied: (kind: string, classes: string, summary: string, slug: string) =>
86
+ msg("goal.policy.applied", "warning", `Agent applied an in-policy ${kind} (${classes}): ${summary}. It is journalled; the owner can undo it.`, [{ label: "Undo this change", run: `/goal revert ${slug}` }, status]),
87
+ divergenceChanged: () => msg("goal.divergence.changed", "warning", "The divergence changed since the review; nothing applied.", [{ label: "Review again", run: "goal_recover_divergence for a fresh recovery review" }]),
88
+ divergenceAdopted: (slug: string) => msg("goal.divergence.adopted", "success", `Adopted the disk contract of ${slug} as a new journalled generation; the goal stays paused.`, [resume, status]),
89
+ divergenceRestored: (slug: string) => msg("goal.divergence.restored", "success", `Restored the locked contract of ${slug}; the divergent bytes are kept as evidence. The goal stays paused.`, [resume, status]),
90
+ stallReported: (turns: number) =>
91
+ msg("goal.stall.reported", "warning", `No visible change for ${turns} turns, but the agent kept working: the run continues under the owner-set on_stall policy and this stall is recorded.`, [status, resume]),
92
+ runStateRepaired: (slug: string, kept: string) =>
93
+ msg("goal.runstate.repaired", "warning", `run.json for ${slug} was malformed or carried duplicate keys; it was rebuilt from the run's own ledger and evidence. The raw bytes are kept at ${kept}.`, [status, resume]),
94
+ amended: (generation: number, summary: string) =>
95
+ msg("goal.amended", "success", `Goal amended (generation ${generation}): ${summary}. The contract re-locked; the next turn renders the new text.`, [verify, status]),
96
+ pausedAdmissionError: (reason: string) =>
97
+ msg("goal.paused.admission", "error", `Goal paused: the host could not send the next turn (${reason}).`, [resume, abort], "error"),
98
+ awaitingAnswer: (question: string) =>
99
+ msg("goal.awaiting-answer", "warning", `The goal is waiting for your answer: ${question}. The loop is idle until you reply.`, [
100
+ { label: "Answer in the session", run: "reply in the session" },
101
+ abort,
102
+ ], "question"),
103
+ pausedUserMessage: () =>
104
+ msg("goal.paused.user-message", "info", `Goal paused because you sent a message — reply to steer the run, or resume to hand control back.`, [
105
+ { label: "Resume goal", run: "/goal resume", act: "resume" },
106
+ { label: "Keep steering", run: "keep replying in the session" },
107
+ abort,
108
+ ]),
109
+ budgetStopped: (title: string) =>
110
+ msg("goal.budget.stopped", "warning", `Goal stopped at its budget: ${title}. Raise the budget to continue, or wrap up.`, [
111
+ { label: "Raise budget (amend)", run: "reply in the Composer with the budget change; the agent stages a reviewed amendment" },
112
+ abort,
113
+ archive,
114
+ ], "question"),
115
+ blocked: (key: string, reason: string, needs?: string) =>
116
+ msg("goal.blocked", "warning", `Goal blocked on ${key}: ${reason}${needs ? ` — it needs: ${needs}` : " — only the owner can resolve this"}.`, [
117
+ { label: "Resolve, then resume", run: "/goal resume", act: "resume" },
118
+ { label: "Flag the criterion (worker)", run: "goal_flag it as contradictory/impossible/unsafe" },
119
+ abort,
120
+ ], "question"),
121
+ pausedInterrupted: (why: string) =>
122
+ msg("goal.paused.interrupted", "error", `Goal paused — ${why}.`, [
123
+ { label: "Resume goal", run: "/goal resume", act: "resume" },
124
+ { label: "Keep steering", run: "keep replying in the session" },
125
+ abort,
126
+ ], "error"),
127
+ // T111: the stall notice names the owner-set autonomy.on_stall option and the
128
+ // owner's choices, so a stalled run points at /goal resume and at the policy
129
+ // that would have auto-continued a genuinely working run.
130
+ pausedStalled: (why: string, onStall: "pause" | "continue" = "pause") =>
131
+ msg("goal.paused.stalled", "info", `Goal paused — ${why}. Your autonomy.on_stall is "${onStall}". Choices: /goal resume to continue, /goal abort to stop, or set autonomy.on_stall: continue in goal.yaml so a stalled-but-active run keeps going with a recorded report.`, [resume, abort]),
132
+ verifying: () => msg("goal.verifying", "info", `Verifying the goal: host checks run first, then the independent verifier child. Nothing to do until goal.summary lands.`, [{ label: "Show status", run: "/goal status" }]),
133
+ complete: (title: string, caveats: number) =>
134
+ msg("goal.complete", "success", `Goal complete: ${title}.${caveats ? ` ${caveats} caveat${caveats > 1 ? "s" : ""} in the summary (provenance/unproven scope).` : ""} Every required criterion was verified. The loop has stopped — nothing further runs on its own; follow-ups live in the summary dialog and the goal folder.`, [
135
+ { label: "Archive goal", run: "/goal archive", act: "archive" },
136
+ { label: "Start the next goal", run: "/goal new <what you want next>" },
137
+ ], "done"),
138
+ needsSignOff: (ids: string[]) =>
139
+ msg("goal.needs-review.signoff", "warning", `Goal needs your sign-off on ${ids.join(", ")} — a human criterion only you can prove. Approve runs verification of everything else immediately (your approval is final and never re-checked).`, [
140
+ { label: `Approve ${ids[0] ?? "<id>"} (triggers verify)`, run: `/goal approve ${ids[0] ?? "<id>"}`, act: "approve", arg: ids[0] },
141
+ { label: "Reject with a reason", run: `/goal reject ${ids[0] ?? "<id>"} <why>`, act: "reject", arg: ids[0] },
142
+ status,
143
+ ], "question"),
144
+ needsReview: (reason: string, ids: string[] = []) =>
145
+ msg("goal.needs-review.rejected", "warning", `Goal needs review: ${reason}. A criterion failed verification repeatedly — decide it, change the work, or change the check. Approving runs verification of everything else immediately (final, never re-checked).`, [
146
+ ...ids.map((id) => ({ label: `Approve ${id} (final, triggers verify)`, run: `/goal approve ${id}`, act: "approve", arg: id })),
147
+ ...(ids.length ? [] : [{ label: "Inspect the failed criteria", run: "/goal status" }]),
148
+ { label: "Amend the check", run: "start a Composer interview for the check change; no contract changes before reviewed acceptance", act: "amend", arg: "interview" },
149
+ abort,
150
+ ], "question"),
151
+ claimRejected: (firstLine: string) =>
152
+ msg("goal.claim.rejected", "warning", `Claim rejected by the host — the verdict goes back to the agent: ${firstLine}`, [
153
+ { label: "Agent: fix and re-claim", run: "read the HOST VERDICT lines, fix the work, then goal_claim again" },
154
+ { label: "Owner: verify again", run: "/goal verify", act: "verify" },
155
+ ]),
156
+ attached: (title: string, status2: string) =>
157
+ msg("goal.attached", "success", `Goal attached here: ${title} (${status2}). Every /goal command and palette action now acts on it from this session.`, [
158
+ { label: "Resume goal", run: "/goal resume", act: "resume" },
159
+ status,
160
+ ]),
161
+ archived: (title: string, slug: string) =>
162
+ msg("goal.archived", "success", `Goal archived: ${title} (history intact under .opencode/goals-archive/${slug}/; the registry still answers "was this ever a goal here?").`, [
163
+ { label: "Start the next goal", run: "/goal new <what you want next>" },
164
+ { label: "List everything", run: "/goal list all" },
165
+ ]),
166
+ goalWait: (seconds: number, reason: string) =>
167
+ msg("goal.waiting", "info", `Goal waiting ${seconds}s: ${reason}. The loop resumes itself; the tracer counts down.`, [
168
+ { label: "Show status", run: "/goal status" },
169
+ { label: "Owner: keep steering", run: "reply in the session (it pauses the wait)" },
170
+ ]),
171
+ commandDeferred: (sub: string, held: "turn" | "form") =>
172
+ msg("goal.command.deferred", "info", `/goal ${sub} acknowledged — queued behind the active ${held}; it runs the moment it ends. Nothing is lost, nothing runs early.`, [
173
+ { label: "Show status", run: "/goal status" },
174
+ { label: "Act now anyway", run: "/goal pause or /goal abort run immediately" },
175
+ ]),
176
+ amendProposed: (summary: string) =>
177
+ msg("goal.amend.proposed", "warning", `Amendment proposed: ${summary}. Review the diff; confirming re-locks the contract with a new generation.`, [
178
+ { label: "Confirm re-lock", run: "/goal amend confirm", act: "amend", arg: "confirm" },
179
+ { label: "Revise the proposal", run: "reply in the Composer with your changes; the agent stages a revised diff" },
180
+ ], "question"),
181
+ supersedeAck: (slug: string, predecessor: string, status: string) =>
182
+ msg("goal.supersede.ack", "warning", `Refused to start "${slug}": predecessor "${predecessor}" is still ${status}. Superseding a live predecessor forks the audit trail.`, [
183
+ { label: "Acknowledge and supersede", run: `/goal start ${slug} acknowledge-supersede` },
184
+ { label: "Abort predecessor first", run: "/goal abort", act: "abort" },
185
+ ], "question"),
186
+ pausedAfterVerdict: (firstLine: string) =>
187
+ msg("goal.paused.verdict", "warning", `Verification did not pass while the goal is stopped — ${firstLine} Decide with the verdict in hand.`, [
188
+ resume,
189
+ { label: "Amend the check", run: "start a Composer interview for the check change; no contract changes before reviewed acceptance", act: "amend", arg: "interview" },
190
+ abort,
191
+ ], "question"),
192
+ continueWork: (title: string, slug: string, taskId: string) =>
193
+ msg("goal.continue-work", "success", `Goal complete: ${title}. Started the next ready CLEO item ${taskId} as "${slug}" — no owner decision was created.`, [
194
+ { label: "Open the new goal", run: `/goal status ${slug}` },
195
+ { label: "See all goals", run: "/goal list" },
196
+ ], "done"),
197
+ continueStopped: (reason: string) =>
198
+ msg("goal.continue.stopped", "warning", `The loop did not take more work: ${reason}. Nothing was started.`, [
199
+ { label: "Inspect the goal", run: "/goal status" },
200
+ { label: "Start a goal yourself", run: "/goal new <what you want done>" },
201
+ ], "question"),
202
+ peerAnnounced: (taskId?: string) =>
203
+ msg("goal.peer.announced", "info", `Goal complete. Announced availability to peers over AgentMBX${taskId ? ` (last linked task ${taskId})` : ""}; one announcement is outstanding.`, [
204
+ { label: "Inspect the goal", run: "/goal status" },
205
+ ]),
206
+ peerReplied: (from: string, taskId?: string) =>
207
+ msg("goal.peer.replied", "info", `${from} replied to the availability announcement${taskId ? ` about ${taskId}` : ""}; it is recorded as candidate work, not as consent.`, [
208
+ { label: "Inspect the goal", run: "/goal status" },
209
+ ], "question"),
210
+ completeDecision: (title: string, cleoPresent: boolean, unproven: number) =>
211
+ msg("goal.complete.decision", "success", `Goal complete: ${title}.${unproven ? ` ${unproven} discussed-but-unproven item(s) are flagged in the summary.` : ""} The loop has stopped. What next?`, [
212
+ { label: "Start the next goal", run: "start the write-goal interview for the next goal", act: "start-next" },
213
+ // T070: "Decompose with CLEO" was a dead-end guidance row — it is a
214
+ // real act now, dispatching the decomposition prompt into the session.
215
+ ...(cleoPresent
216
+ ? [{ label: "Decompose with CLEO", run: "file the follow-ups as CLEO tasks in this session", act: "decompose" }]
217
+ : []),
218
+ archive,
219
+ // guidance rows carry no act — the TUI renders them as visible rows
220
+ // instead of dropping them (T067: "why was I only offered Archive?");
221
+ // with CLEO linked, Decompose is act-able, so only the install case
222
+ // stays guidance
223
+ ...(cleoPresent ? [] : [{ label: "Install CLEO for tracking", run: "install CLEO — no .cleo workspace in this project yet" }]),
224
+ ], "done"),
225
+ }
226
+
227
+ /** Every engine notice comes from M — the unit suite pins none is actionless. */
228
+ export const MESSAGE_BUILDERS = Object.keys(M)
@@ -0,0 +1,76 @@
1
+ // T046 — the completion scope audit. The owner's 2026-10-04 gap: a contract's
2
+ // criteria can all pass while a plan step that proves nothing (proves:[])
3
+ // never happened and a scope.in line maps to no criterion at all — the run
4
+ // "completed" a smaller goal than the one discussed. Per the council
5
+ // decision this ships as a FLAGGED CAVEAT in the post-goal summary: items
6
+ // are surfaced as discussed-but-unproven, completion is never blocked by
7
+ // them (deliberately loose plans must not deadlock).
8
+ import type { Contract, Criterion } from "../contract/types"
9
+ import type { RunState } from "../engine/state"
10
+ import type { ScopeAuditFlag } from "./build"
11
+
12
+ const STOP = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "when", "must", "shall", "your", "you", "are", "was", "were", "has", "have", "its", "their", "than", "then", "over", "under", "before", "after", "every", "each", "which", "what", "also", "onto", "any", "all", "not", "but", "out", "off", "via", "per", "new", "next", "them", "they", "some", "more", "most", "less", "least", "very", "just", "only", "done", "work", "owner", "agent"])
13
+
14
+ /** Lowercase meaningful tokens (≥4 chars, naive plural/past stripping). */
15
+ export function tokens(text: string): string[] {
16
+ return [
17
+ ...new Set(
18
+ text
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9+#./-]+/g, " ")
21
+ .split(/\s+/)
22
+ .filter((t) => t.length >= 4 && !STOP.has(t))
23
+ .map((t) => (t.length > 4 && t.endsWith("s") ? t.slice(0, -1) : t.length > 4 && t.endsWith("ed") ? t.slice(0, -2) : t)),
24
+ ),
25
+ ]
26
+ }
27
+
28
+ /** The searchable text of a criterion: statement plus its check body. */
29
+ function criterionText(c: Criterion): string {
30
+ const check = c.check as Record<string, unknown>
31
+ return `${c.statement} ${Object.values(check).filter((v) => typeof v === "string").join(" ")}`
32
+ }
33
+
34
+ /** A scope.in line maps to a criterion when one mentions the other, or they
35
+ * share ≥2 meaningful tokens. Deterministic and explainable — the audit
36
+ * says WHY an item was flagged, never just that it was. */
37
+ export function scopeItemMaps(item: string, criteria: Criterion[]): boolean {
38
+ const itemNorm = item.toLowerCase().trim()
39
+ const itemTokens = tokens(item)
40
+ for (const c of criteria) {
41
+ const text = criterionText(c)
42
+ const textNorm = text.toLowerCase()
43
+ // a direct mention either way (guarded against tiny substrings)
44
+ if (itemNorm.length >= 8 && (textNorm.includes(itemNorm) || itemNorm.includes(textNorm))) return true
45
+ if (tokens(text).filter((t) => itemTokens.includes(t)).length >= 2) return true
46
+ }
47
+ return false
48
+ }
49
+
50
+ /**
51
+ * Flag discussed-but-unproven items:
52
+ * - plan steps with proves:[] and no sibling evidence of work (the step was
53
+ * never recorded done or active in the run's progress state), and
54
+ * - scope.in lines mapping to no criterion.
55
+ * Returns the flags; the caller surfaces them as caveats. Never blocks.
56
+ */
57
+ export function completionAudit(contract: Contract, state: RunState): ScopeAuditFlag[] {
58
+ const flags: ScopeAuditFlag[] = []
59
+ const all = [...contract.criteria, ...contract.invariants]
60
+ for (const step of contract.plan) {
61
+ if (step.proves.length) continue // proves criteria — those outcomes speak
62
+ const sibling = state.steps[step.id]
63
+ if (sibling === "done" || sibling === "active") continue // recorded work exists
64
+ flags.push({
65
+ kind: "step",
66
+ id: step.id,
67
+ title: step.title,
68
+ why: `proves no criterion and was never recorded done or active (${sibling ?? "pending"}) — discussed but unproven`,
69
+ })
70
+ }
71
+ for (const item of contract.scope.in) {
72
+ if (scopeItemMaps(item, all)) continue
73
+ flags.push({ kind: "scope", id: item, title: item, why: "no criterion statement or check maps to it — discussed but unproven" })
74
+ }
75
+ return flags
76
+ }