@aragon-agent/cli 0.6.44 → 0.6.45

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 (236) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +69 -2
  3. package/dist/agent/controller.d.ts +69 -0
  4. package/dist/agent/controller.js +224 -11
  5. package/dist/agent/governance-view.d.ts +40 -0
  6. package/dist/agent/governance-view.js +40 -0
  7. package/dist/agent/governance-wiring.d.ts +162 -0
  8. package/dist/agent/governance-wiring.js +470 -0
  9. package/dist/agent/plan-prompt.d.ts +7 -0
  10. package/dist/agent/plan-prompt.js +2 -1
  11. package/dist/agent/reducer.d.ts +33 -1
  12. package/dist/agent/reducer.js +45 -9
  13. package/dist/agent/system-prompt.d.ts +19 -0
  14. package/dist/agent/system-prompt.js +9 -1
  15. package/dist/cli.js +45 -1
  16. package/dist/commands/builtins.js +10 -4
  17. package/dist/commands/governance-command.d.ts +35 -0
  18. package/dist/commands/governance-command.js +292 -0
  19. package/dist/commands/governance-runs-table.d.ts +40 -0
  20. package/dist/commands/governance-runs-table.js +130 -0
  21. package/dist/commands/registry.d.ts +12 -0
  22. package/dist/config/app-paths.d.ts +10 -0
  23. package/dist/config/app-paths.js +12 -0
  24. package/dist/config/cli-commands.d.ts +18 -0
  25. package/dist/config/cli-commands.js +47 -1
  26. package/dist/config/env.d.ts +2 -1
  27. package/dist/config/env.js +10 -1
  28. package/dist/config/governance-config.d.ts +125 -0
  29. package/dist/config/governance-config.js +164 -0
  30. package/dist/config/governance-launch.d.ts +71 -0
  31. package/dist/config/governance-launch.js +58 -0
  32. package/dist/config/load.d.ts +15 -0
  33. package/dist/config/load.js +26 -1
  34. package/dist/config/model-profile-store.d.ts +4 -2
  35. package/dist/config/schema.d.ts +22 -0
  36. package/dist/config/schema.js +6 -0
  37. package/dist/config/store.js +14 -1
  38. package/dist/config/ui-state.d.ts +11 -0
  39. package/dist/config/ui-state.js +11 -0
  40. package/dist/diagnostics/doctor.js +20 -0
  41. package/dist/governance/card-normalize.d.ts +24 -0
  42. package/dist/governance/card-normalize.js +76 -0
  43. package/dist/governance/copy.d.ts +109 -0
  44. package/dist/governance/copy.js +223 -0
  45. package/dist/governance/engine/aftermath.d.ts +49 -0
  46. package/dist/governance/engine/aftermath.js +134 -0
  47. package/dist/governance/engine/artifact-persist.d.ts +35 -0
  48. package/dist/governance/engine/artifact-persist.js +86 -0
  49. package/dist/governance/engine/budget.d.ts +52 -0
  50. package/dist/governance/engine/budget.js +112 -0
  51. package/dist/governance/engine/cards.d.ts +18 -0
  52. package/dist/governance/engine/cards.js +186 -0
  53. package/dist/governance/engine/digest.d.ts +32 -0
  54. package/dist/governance/engine/digest.js +63 -0
  55. package/dist/governance/engine/engine-core.d.ts +219 -0
  56. package/dist/governance/engine/engine-core.js +562 -0
  57. package/dist/governance/engine/engine.d.ts +68 -0
  58. package/dist/governance/engine/engine.js +314 -0
  59. package/dist/governance/engine/estimate.d.ts +38 -0
  60. package/dist/governance/engine/estimate.js +46 -0
  61. package/dist/governance/engine/inspector.d.ts +16 -0
  62. package/dist/governance/engine/inspector.js +396 -0
  63. package/dist/governance/engine/integration.d.ts +35 -0
  64. package/dist/governance/engine/integration.js +220 -0
  65. package/dist/governance/engine/lifecycle.d.ts +29 -0
  66. package/dist/governance/engine/lifecycle.js +656 -0
  67. package/dist/governance/engine/pause.d.ts +56 -0
  68. package/dist/governance/engine/pause.js +107 -0
  69. package/dist/governance/engine/phase-members.d.ts +15 -0
  70. package/dist/governance/engine/phase-members.js +34 -0
  71. package/dist/governance/engine/phase-planner.d.ts +53 -0
  72. package/dist/governance/engine/phase-planner.js +282 -0
  73. package/dist/governance/engine/rebuild.d.ts +27 -0
  74. package/dist/governance/engine/rebuild.js +57 -0
  75. package/dist/governance/engine/report.d.ts +66 -0
  76. package/dist/governance/engine/report.js +220 -0
  77. package/dist/governance/engine/run-digest.d.ts +20 -0
  78. package/dist/governance/engine/run-digest.js +42 -0
  79. package/dist/governance/engine/spec-builder.d.ts +117 -0
  80. package/dist/governance/engine/spec-builder.js +321 -0
  81. package/dist/governance/engine/stage-runner.d.ts +22 -0
  82. package/dist/governance/engine/stage-runner.js +664 -0
  83. package/dist/governance/engine/start-input.d.ts +19 -0
  84. package/dist/governance/engine/start-input.js +111 -0
  85. package/dist/governance/engine/state-machine.d.ts +101 -0
  86. package/dist/governance/engine/state-machine.js +586 -0
  87. package/dist/governance/engine/submission-queries.d.ts +42 -0
  88. package/dist/governance/engine/submission-queries.js +97 -0
  89. package/dist/governance/engine/submission-render.d.ts +12 -0
  90. package/dist/governance/engine/submission-render.js +92 -0
  91. package/dist/governance/engine/submissions.d.ts +27 -0
  92. package/dist/governance/engine/submissions.js +455 -0
  93. package/dist/governance/engine/visibility.d.ts +104 -0
  94. package/dist/governance/engine/visibility.js +223 -0
  95. package/dist/governance/governed-dispatch.d.ts +19 -0
  96. package/dist/governance/governed-dispatch.js +17 -0
  97. package/dist/governance/limits.d.ts +107 -0
  98. package/dist/governance/limits.js +131 -0
  99. package/dist/governance/posture-transition.d.ts +38 -0
  100. package/dist/governance/posture-transition.js +44 -0
  101. package/dist/governance/posture.d.ts +35 -0
  102. package/dist/governance/posture.js +59 -0
  103. package/dist/governance/profiles.d.ts +48 -0
  104. package/dist/governance/profiles.js +140 -0
  105. package/dist/governance/prompts/orchestrator-block.d.ts +40 -0
  106. package/dist/governance/prompts/orchestrator-block.js +128 -0
  107. package/dist/governance/prompts/plan-addendum.d.ts +22 -0
  108. package/dist/governance/prompts/plan-addendum.js +60 -0
  109. package/dist/governance/prompts/refusals.d.ts +48 -0
  110. package/dist/governance/prompts/refusals.js +82 -0
  111. package/dist/governance/prompts/role-blocks.d.ts +40 -0
  112. package/dist/governance/prompts/role-blocks.js +255 -0
  113. package/dist/governance/report-reader.d.ts +31 -0
  114. package/dist/governance/report-reader.js +57 -0
  115. package/dist/governance/roles.d.ts +83 -0
  116. package/dist/governance/roles.js +142 -0
  117. package/dist/governance/rules/claims.d.ts +83 -0
  118. package/dist/governance/rules/claims.js +250 -0
  119. package/dist/governance/rules/elimination.d.ts +66 -0
  120. package/dist/governance/rules/elimination.js +105 -0
  121. package/dist/governance/rules/events-table.d.ts +46 -0
  122. package/dist/governance/rules/events-table.js +94 -0
  123. package/dist/governance/rules/failure-analysis.d.ts +28 -0
  124. package/dist/governance/rules/failure-analysis.js +93 -0
  125. package/dist/governance/rules/ledger-replay.d.ts +53 -0
  126. package/dist/governance/rules/ledger-replay.js +53 -0
  127. package/dist/governance/rules/ledger-state.d.ts +85 -0
  128. package/dist/governance/rules/ledger-state.js +175 -0
  129. package/dist/governance/rules/ledger.d.ts +228 -0
  130. package/dist/governance/rules/ledger.js +560 -0
  131. package/dist/governance/rules/rca.d.ts +68 -0
  132. package/dist/governance/rules/rca.js +220 -0
  133. package/dist/governance/rules/settlement.d.ts +129 -0
  134. package/dist/governance/rules/settlement.js +363 -0
  135. package/dist/governance/rules/suspicion.d.ts +66 -0
  136. package/dist/governance/rules/suspicion.js +169 -0
  137. package/dist/governance/store/artifacts.d.ts +81 -0
  138. package/dist/governance/store/artifacts.js +173 -0
  139. package/dist/governance/store/atomic.d.ts +79 -0
  140. package/dist/governance/store/atomic.js +119 -0
  141. package/dist/governance/store/events-log.d.ts +35 -0
  142. package/dist/governance/store/events-log.js +86 -0
  143. package/dist/governance/store/run-lock.d.ts +51 -0
  144. package/dist/governance/store/run-lock.js +122 -0
  145. package/dist/governance/store/run-store.d.ts +229 -0
  146. package/dist/governance/store/run-store.js +644 -0
  147. package/dist/governance/store/scoreboard-export.d.ts +53 -0
  148. package/dist/governance/store/scoreboard-export.js +65 -0
  149. package/dist/governance/tools/child-tools.d.ts +31 -0
  150. package/dist/governance/tools/child-tools.js +65 -0
  151. package/dist/governance/tools/guard.d.ts +71 -0
  152. package/dist/governance/tools/guard.js +194 -0
  153. package/dist/governance/tools/inspector-tools.d.ts +63 -0
  154. package/dist/governance/tools/inspector-tools.js +424 -0
  155. package/dist/governance/tools/lead-tools.d.ts +42 -0
  156. package/dist/governance/tools/lead-tools.js +226 -0
  157. package/dist/governance/tools/normalize-util.d.ts +101 -0
  158. package/dist/governance/tools/normalize-util.js +230 -0
  159. package/dist/governance/tools/scan-incomplete.d.ts +79 -0
  160. package/dist/governance/tools/scan-incomplete.js +486 -0
  161. package/dist/governance/tools/shared-tools.d.ts +44 -0
  162. package/dist/governance/tools/shared-tools.js +125 -0
  163. package/dist/governance/tools/submit-normalizers.d.ts +28 -0
  164. package/dist/governance/tools/submit-normalizers.js +474 -0
  165. package/dist/governance/tools/submit-tools.d.ts +26 -0
  166. package/dist/governance/tools/submit-tools.js +171 -0
  167. package/dist/governance/types.d.ts +661 -0
  168. package/dist/governance/types.js +27 -0
  169. package/dist/session/validate-session.js +32 -1
  170. package/dist/team/runtime.d.ts +8 -0
  171. package/dist/team/runtime.js +9 -0
  172. package/dist/team/subagent.d.ts +23 -0
  173. package/dist/team/subagent.js +14 -3
  174. package/dist/team/types.d.ts +6 -0
  175. package/dist/tools/index.d.ts +30 -7
  176. package/dist/tools/index.js +14 -0
  177. package/dist/tools/plan-tools.d.ts +14 -2
  178. package/dist/tools/plan-tools.js +5 -2
  179. package/dist/ui/App.js +145 -25
  180. package/dist/ui/BottomStatusRow.js +1 -1
  181. package/dist/ui/Composer.d.ts +7 -1
  182. package/dist/ui/Composer.js +9 -6
  183. package/dist/ui/GovernancePanel.d.ts +50 -0
  184. package/dist/ui/GovernancePanel.js +77 -0
  185. package/dist/ui/GovernanceRun.d.ts +18 -0
  186. package/dist/ui/GovernanceRun.js +11 -0
  187. package/dist/ui/StatusBar.d.ts +15 -2
  188. package/dist/ui/StatusBar.js +10 -4
  189. package/dist/ui/Transcript.js +3 -0
  190. package/dist/ui/entries/GovernanceCard.d.ts +30 -0
  191. package/dist/ui/entries/GovernanceCard.js +32 -0
  192. package/dist/ui/governance-copy.d.ts +69 -0
  193. package/dist/ui/governance-copy.js +104 -0
  194. package/dist/ui/governance-status.d.ts +145 -0
  195. package/dist/ui/governance-status.js +326 -0
  196. package/dist/ui/interaction-copy.d.ts +4 -0
  197. package/dist/ui/interaction-copy.js +5 -0
  198. package/dist/ui/interaction-hints.d.ts +17 -0
  199. package/dist/ui/interaction-hints.js +14 -2
  200. package/dist/ui/layout/governance-board.d.ts +121 -0
  201. package/dist/ui/layout/governance-board.js +382 -0
  202. package/dist/ui/layout/governance-card.d.ts +53 -0
  203. package/dist/ui/layout/governance-card.js +134 -0
  204. package/dist/ui/layout/governance-chip.d.ts +38 -0
  205. package/dist/ui/layout/governance-chip.js +64 -0
  206. package/dist/ui/layout/governance-panel.d.ts +85 -0
  207. package/dist/ui/layout/governance-panel.js +358 -0
  208. package/dist/ui/layout/governance-roster.d.ts +65 -0
  209. package/dist/ui/layout/governance-roster.js +275 -0
  210. package/dist/ui/layout/status-detail-layout.js +3 -3
  211. package/dist/ui/layout/status-layout.d.ts +5 -2
  212. package/dist/ui/layout/status-layout.js +10 -6
  213. package/dist/ui/layout/virtual-window.js +11 -0
  214. package/dist/ui/overlays/GovernanceBoardOverlay.d.ts +41 -0
  215. package/dist/ui/overlays/GovernanceBoardOverlay.js +76 -0
  216. package/dist/ui/overlays/GovernanceSettingsPage.d.ts +48 -0
  217. package/dist/ui/overlays/GovernanceSettingsPage.js +93 -0
  218. package/dist/ui/overlays/HelpOverlay.js +4 -3
  219. package/dist/ui/overlays/QuestionOverlay.d.ts +9 -1
  220. package/dist/ui/overlays/QuestionOverlay.js +4 -2
  221. package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
  222. package/dist/ui/overlays/SettingsScreen.js +21 -1
  223. package/dist/ui/overlays/governance-settings.d.ts +79 -0
  224. package/dist/ui/overlays/governance-settings.js +140 -0
  225. package/dist/ui/overlays/model-profile-state.d.ts +6 -1
  226. package/dist/ui/overlays/model-profile-state.js +10 -0
  227. package/dist/ui/palettes.d.ts +17 -0
  228. package/dist/ui/palettes.js +9 -0
  229. package/dist/ui/scrollback/live-preview.js +2 -0
  230. package/dist/ui/scrollback/settled-boundary.js +3 -0
  231. package/dist/ui/shortcuts.js +2 -2
  232. package/dist/ui/theme.d.ts +7 -0
  233. package/dist/ui/theme.js +1 -0
  234. package/dist/ui/transcript-text.js +9 -0
  235. package/dist/ui/use-wheel-routing.js +3 -1
  236. package/package.json +2 -2
@@ -0,0 +1,656 @@
1
+ /**
2
+ * Run lifecycle: start, resume, close, abandon, resolve, status (governance-mode 7.2, 8.4).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned).
5
+ *
6
+ * Every function here answers with plain text that begins `OK:` / `NOT ACCEPTED:` /
7
+ * `ESCALATION:` and ends with `NEXT:` (D-16, 14.5). A refusal is a normal tool RESULT, never
8
+ * an error: an error result invites an identical retry, a result that says what to do
9
+ * instead does not.
10
+ */
11
+ import { readFile } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { GOV_LIMITS, GOV_SCHEMA_VERSION } from '../limits.js';
14
+ import { formatDispatchEstimate, formatDispatchPlanForAgent } from '../copy.js';
15
+ import { rosterIds, rosterSizes } from '../roles.js';
16
+ import { createLedger } from '../rules/ledger-state.js';
17
+ import { formatNotAccepted, isRecord, Issues } from '../tools/normalize-util.js';
18
+ import { newRunUsage, shouldStopNewStages } from './budget.js';
19
+ import { charterCard, escalationCard, finalCard } from './cards.js';
20
+ import { composeDigest, refusal } from './digest.js';
21
+ import { estimateDispatches, estimateForRun } from './estimate.js';
22
+ import { planPhases } from './phase-planner.js';
23
+ import { recoverInterruptedStage, replayCheckpoints } from './rebuild.js';
24
+ import { buildBoardMarkdown, buildFinalReport, renderPhases, renderWpTable, } from './report.js';
25
+ import { applyResolveDisposition, canClose, closeEscalation, nextEscalationId, } from './state-machine.js';
26
+ import { normalizeStartInput } from './start-input.js';
27
+ export const BUDGET_TOPIC_PREFIX = 'Team token budget';
28
+ const LIVE_STATUSES = new Set([
29
+ 'executing',
30
+ 'escalated',
31
+ 'paused',
32
+ ]);
33
+ // ---------------------------------------------------------------------------
34
+ // start
35
+ // ---------------------------------------------------------------------------
36
+ /** `package.json` `scripts.test` -> `npm test`, so a run without commands can still verify. */
37
+ async function inferIntegrationCommands(cwd) {
38
+ try {
39
+ const text = await readFile(join(cwd, 'package.json'), 'utf8');
40
+ const parsed = JSON.parse(text);
41
+ const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
42
+ return typeof scripts.test === 'string' && scripts.test.trim() !== '' ? ['npm test'] : [];
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ function composeSpec(plan, addendum) {
49
+ const parts = [];
50
+ if (plan !== null && plan.trim() !== '')
51
+ parts.push(plan.trim());
52
+ if (addendum.trim() !== '') {
53
+ parts.push(plan ? `## Orchestrator addendum\n${addendum.trim()}` : addendum.trim());
54
+ }
55
+ const joined = parts.join('\n\n');
56
+ return joined.length > GOV_LIMITS.specChars
57
+ ? `${joined.slice(0, GOV_LIMITS.specChars - 12)}\n[truncated]`
58
+ : joined;
59
+ }
60
+ function buildCharterInput(input, charter) {
61
+ const { ceiling: teamCeiling, commands: cmds, plan } = charter;
62
+ const wps = input.workPackages;
63
+ const files = [...new Set(wps.flatMap((w) => w.files))];
64
+ const risks = [
65
+ ...(input.contracts ?? []).map((c) => `Contract ${c.id} ${c.name}: ${c.definition}`),
66
+ ...cmds.map((c) => `Integration command (the inspector may run it): ${c}`),
67
+ ];
68
+ return {
69
+ title: input.goal,
70
+ summary: `${wps.length} work package(s), one DEV agent each plus reviewers, testers and one ` +
71
+ `inspector; stage width <= ${teamCeiling}.${plan === '' ? '' : ` ${plan}`}`,
72
+ steps: wps.map((w) => ({
73
+ title: `${w.id}: ${w.title}`,
74
+ detail: `${w.goal}\nFiles: ${w.files.join(', ') || '(none)'}\n` +
75
+ `Acceptance: ${w.acceptance.join('; ')}` +
76
+ (w.dependsOn && w.dependsOn.length > 0 ? `\nDepends on: ${w.dependsOn.join(', ')}` : ''),
77
+ })),
78
+ filesTouched: files,
79
+ risks,
80
+ };
81
+ }
82
+ function makeWorkPackages(input, phaseOf) {
83
+ const out = {};
84
+ input.workPackages.forEach((w, index) => {
85
+ out[w.id] = {
86
+ id: w.id,
87
+ title: w.title,
88
+ goal: w.goal,
89
+ files: w.files,
90
+ acceptance: w.acceptance,
91
+ dependsOn: w.dependsOn ?? [],
92
+ phase: phaseOf[w.id] ?? 0,
93
+ assignee: `dev${index + 1}`,
94
+ reviewer: null,
95
+ tester: null,
96
+ status: 'pending',
97
+ reworkCount: 0,
98
+ rejections: [],
99
+ appealUsed: false,
100
+ submissions: { dev: [], review: [], test: [], settle: [], integrate: [], fix: [] },
101
+ reworkGrants: 0,
102
+ acceptedWithIssues: false,
103
+ };
104
+ });
105
+ return out;
106
+ }
107
+ function makePhase(index, wpIds) {
108
+ return {
109
+ index,
110
+ wpIds,
111
+ settled: false,
112
+ integrated: false,
113
+ integrationAttempts: 0,
114
+ fixCycles: 0,
115
+ extraFixGrants: 0,
116
+ integrationAccepted: true,
117
+ lastIntegrationFailed: false,
118
+ };
119
+ }
120
+ async function refuseStartReasons(core) {
121
+ if (core.readOnly)
122
+ return 'this session cannot write governance records (newer format).';
123
+ const existing = core.run;
124
+ if (existing !== null && LIVE_STATUSES.has(existing.status)) {
125
+ return (`run ${existing.runId} is still ${existing.status}. Finish it with gov_run close, ` +
126
+ 'stop it with gov_run abandon, or continue it (gov_run status / resume).');
127
+ }
128
+ if (!core.deps.hasApiKey()) {
129
+ return 'no model API key is available, so a team cannot run. Configure one and retry.';
130
+ }
131
+ return null;
132
+ }
133
+ function startDigest(core, notes, chunkNote) {
134
+ const run = core.requireRun();
135
+ const agents = Object.keys(core.ledger.agents).length;
136
+ const plan = formatDispatchPlanForAgent(estimateForRun(run, core.deps.getTeamCeiling()));
137
+ const results = [
138
+ renderPhases(run),
139
+ ...(plan === '' ? [] : [plan]),
140
+ ...(chunkNote ? [chunkNote] : []),
141
+ ];
142
+ return composeDigest(core, {
143
+ headline: `OK: run ${run.runId} started. ${run.wpOrder.length} work package(s) in ` +
144
+ `${run.phases.length} phase(s); ${agents} scored agents + 1 inspector ` +
145
+ `(profile ${run.profile}).`,
146
+ results,
147
+ warnings: notes.map((n) => `NOTE: ${n}`),
148
+ standings: true,
149
+ });
150
+ }
151
+ /** `gov_run start`. Creates nothing unless every check passes (7.2). */
152
+ export async function startRun(core, raw, signal) {
153
+ const refused = await refuseStartReasons(core);
154
+ if (refused !== null)
155
+ return refusal(core, refused);
156
+ const normalized = normalizeStartInput(raw);
157
+ if (!normalized.ok)
158
+ return formatNotAccepted('gov_run start', normalized.errors);
159
+ const input = normalized.value;
160
+ const notes = [...normalized.notes];
161
+ const cfg = core.config();
162
+ const ceiling = core.deps.getTeamCeiling();
163
+ const planned = planPhases({
164
+ workPackages: input.workPackages,
165
+ ...(input.contracts ? { contracts: input.contracts } : {}),
166
+ config: {
167
+ maxWorkPackages: cfg.maxWorkPackages,
168
+ maxPhases: cfg.maxPhases,
169
+ teamCeiling: ceiling,
170
+ },
171
+ });
172
+ if (!planned.ok)
173
+ return formatNotAccepted('gov_run start', planned.errors);
174
+ notes.push(...planned.notes);
175
+ // PEEK, never take: a refused start must not lose the approved plan the user is waiting on.
176
+ const peeked = core.deps.peekApprovedPlan?.() ?? null;
177
+ if (input.spec.trim() === '' && (peeked === null || peeked.trim() === '')) {
178
+ return formatNotAccepted('gov_run start', [
179
+ 'spec is required: pass the user requirement (or the approved plan) verbatim.',
180
+ ]);
181
+ }
182
+ let commands = input.integrationCommands ?? [];
183
+ if (commands.length === 0) {
184
+ commands = await inferIntegrationCommands(core.deps.getCwd());
185
+ if (commands.length > 0) {
186
+ notes.push(`integration command inferred from package.json: ${commands[0]}`);
187
+ }
188
+ }
189
+ const plan = formatDispatchEstimate(estimateDispatches({
190
+ phaseWpCounts: planned.phases.map((phase) => phase.wpIds.length),
191
+ teamCeiling: ceiling,
192
+ }));
193
+ const confirmation = await confirmCharter(core, input, { ceiling, commands, plan }, signal);
194
+ if (confirmation !== null)
195
+ return refusal(core, confirmation);
196
+ return createRun(core, {
197
+ input,
198
+ commands,
199
+ planned: planned.phases,
200
+ phaseOf: planned.phaseOf,
201
+ notes,
202
+ });
203
+ }
204
+ async function confirmCharter(core, input, charter, signal) {
205
+ const cfg = core.config();
206
+ const confirm = core.deps.confirmCharter;
207
+ if (!cfg.confirmCharter || confirm === undefined)
208
+ return null;
209
+ const answer = await confirm(buildCharterInput(input, charter), signal);
210
+ if (answer === null || answer.decision === 'dismissed') {
211
+ return 'the user declined the charter; the posture stays GOVERN. Ask what they want changed.';
212
+ }
213
+ if (answer.decision === 'revise') {
214
+ return `the user asked for changes before the run starts: ${answer.feedback ?? '(no details)'}`;
215
+ }
216
+ return null;
217
+ }
218
+ function buildRunState(core, c, runId, plan) {
219
+ const { input } = c;
220
+ const cfg = core.config();
221
+ const now = core.now();
222
+ const sessionId = core.deps.getSessionId();
223
+ return {
224
+ schemaVersion: GOV_SCHEMA_VERSION,
225
+ runId,
226
+ cwd: core.deps.getCwd(),
227
+ ...(sessionId !== undefined ? { sessionId } : {}),
228
+ goal: input.goal,
229
+ profile: (input.profile ?? cfg.profile),
230
+ status: 'executing',
231
+ createdAt: now,
232
+ updatedAt: now,
233
+ phases: c.planned.map((p) => makePhase(p.index, p.wpIds)),
234
+ workPackages: makeWorkPackages(input, c.phaseOf),
235
+ wpOrder: input.workPackages.map((w) => w.id),
236
+ contracts: (input.contracts ?? []).map((x) => ({ ...x })),
237
+ integrationCommands: c.commands,
238
+ claims: [],
239
+ escalations: [],
240
+ stage: null,
241
+ usage: newRunUsage(),
242
+ dispatches: 0,
243
+ seq: 0,
244
+ ...(plan !== null ? { planSnapshot: plan } : {}),
245
+ limits: {
246
+ maxRework: Math.min(cfg.maxRework, GOV_LIMITS.hardMaxRework),
247
+ maxFixCycles: Math.min(cfg.maxFixCycles, GOV_LIMITS.hardMaxFixCycles),
248
+ budgetTokens: cfg.budgetTokens,
249
+ },
250
+ };
251
+ }
252
+ /** Reset every per-run field of the core and wire it to the new run. */
253
+ function adoptRun(core, run) {
254
+ core.run = run;
255
+ core.seq = 0;
256
+ core.pauseRequested = false;
257
+ core.userPaused = false;
258
+ core.ledger = createLedger([]);
259
+ core.submissionCache.clear();
260
+ core.phaseFlags.clear();
261
+ core.viewRequests.clear();
262
+ core.resetStageScratch();
263
+ core.attachTeam();
264
+ }
265
+ async function createRun(core, c) {
266
+ const store = core.deps.store;
267
+ const plan = core.deps.takeApprovedPlan?.() ?? null;
268
+ const spec = composeSpec(plan, c.input.spec);
269
+ const run = buildRunState(core, c, await store.allocateRunId(), plan);
270
+ await store.createRun({ state: run, spec });
271
+ const lock = await store.acquireLock(run.runId);
272
+ if (!lock.ok)
273
+ return refusal(core, lock.message);
274
+ store.startHeartbeat(run.runId);
275
+ adoptRun(core, run);
276
+ seedRoster(core, c.input.workPackages.length, core.deps.getTeamCeiling());
277
+ core.log('run_started', 'engine', {
278
+ goal: run.goal,
279
+ profile: run.profile,
280
+ workPackages: run.wpOrder,
281
+ phases: run.phases.map((p) => p.wpIds),
282
+ });
283
+ core.checkpoint();
284
+ core.exportScoreboard();
285
+ core.emit((snapshot) => ({ type: 'run_started', snapshot, card: charterCard(core) }));
286
+ pruneOnce(core);
287
+ const ceiling = core.deps.getTeamCeiling();
288
+ const chunkNote = run.wpOrder.length > ceiling
289
+ ? `NOTE: stages will be dispatched in chunks of ${ceiling} (team.maxSubagents).`
290
+ : null;
291
+ const flushNote = await core.flush();
292
+ const digest = startDigest(core, c.notes, chunkNote);
293
+ return flushNote === null ? digest : `${digest}
294
+ ${flushNote}`;
295
+ }
296
+ /** DEV per WP in WP order, REV / TEST by `rosterSizes` (5.2). Logged as ledger ops. */
297
+ function seedRoster(core, wpCount, ceiling) {
298
+ const sizes = rosterSizes(wpCount, ceiling);
299
+ for (const id of rosterIds('DEV', sizes.dev)) {
300
+ core.ledgerOp({ op: 'add_agent', spec: { id, role: 'DEV' } });
301
+ }
302
+ for (const id of rosterIds('REV', sizes.rev)) {
303
+ core.ledgerOp({ op: 'add_agent', spec: { id, role: 'REV' } });
304
+ }
305
+ for (const id of rosterIds('TEST', sizes.test)) {
306
+ core.ledgerOp({ op: 'add_agent', spec: { id, role: 'TEST' } });
307
+ }
308
+ }
309
+ /** Retention prune, once per engine instance, never blocking and never throwing. */
310
+ export function pruneOnce(core) {
311
+ if (core.pruned)
312
+ return;
313
+ core.pruned = true;
314
+ void core.deps.store.pruneOld().catch(() => undefined);
315
+ }
316
+ // ---------------------------------------------------------------------------
317
+ // resume
318
+ // ---------------------------------------------------------------------------
319
+ async function cacheSubmissions(core, runId) {
320
+ core.submissionCache.clear();
321
+ const refs = await core.deps.artifacts.list(runId);
322
+ for (const ref of refs) {
323
+ const read = await core.deps.artifacts.read(runId, ref.id);
324
+ if (read === null || !isRecord(read.json) || typeof read.json.kind !== 'string')
325
+ continue;
326
+ core.submissionCache.set(ref.id, read.json);
327
+ }
328
+ }
329
+ async function loadForResume(core, runId) {
330
+ const store = core.deps.store;
331
+ const lock = await store.acquireLock(runId);
332
+ if (!lock.ok)
333
+ return { ok: false, message: lock.message };
334
+ const loaded = await store.load(runId, { replay: replayCheckpoints });
335
+ const release = async (message) => {
336
+ await store.releaseLock(runId);
337
+ return { ok: false, message };
338
+ };
339
+ if (loaded.readOnly) {
340
+ return release(`run ${runId} was written by a newer version of this program and is read-only here. ` +
341
+ 'Update the CLI to resume it.');
342
+ }
343
+ if (loaded.eventsUnreadable || loaded.run === null) {
344
+ return release(`run ${runId} cannot be resumed: its saved state is missing or corrupt ` +
345
+ `(${loaded.problems.join('; ') || 'no readable records'}). It is left on disk for ` +
346
+ 'inspection. Start a new run with gov_run start.');
347
+ }
348
+ if (loaded.run.status === 'closed' || loaded.run.status === 'abandoned') {
349
+ return release(`run ${runId} is already ${loaded.run.status}; start a new run.`);
350
+ }
351
+ core.run = loaded.run;
352
+ core.pauseRequested = false;
353
+ core.userPaused = false;
354
+ core.ledger = loaded.ledger ?? createLedger([]);
355
+ core.seq = Math.max(loaded.tailSeq, loaded.run.seq);
356
+ core.phaseFlags.clear();
357
+ core.viewRequests.clear();
358
+ core.resetStageScratch();
359
+ await cacheSubmissions(core, runId);
360
+ if (loaded.problems.length > 0) {
361
+ core.stageWarnings.push(`Recovered with notes: ${loaded.problems.join('; ')}`);
362
+ }
363
+ store.startHeartbeat(runId);
364
+ return { ok: true };
365
+ }
366
+ /** Put a paused / interrupted in-memory run back to `executing`. */
367
+ function reactivate(core) {
368
+ const run = core.requireRun();
369
+ const reset = recoverInterruptedStage(run);
370
+ core.pauseRequested = false;
371
+ core.userPaused = false;
372
+ if (run.status === 'paused')
373
+ run.status = 'executing';
374
+ core.log('run_resumed', 'engine', { reset });
375
+ core.checkpoint();
376
+ core.attachTeam();
377
+ core.emit((snapshot) => ({ type: 'run_resumed', snapshot }));
378
+ }
379
+ /** `gov_run resume`. Only WPs that lack a submission for their stage will be re-run. */
380
+ export async function resumeRun(core, runId) {
381
+ const current = core.run;
382
+ if (core.isStageRunning())
383
+ return refusal(core, 'a stage is running; wait for it to finish.');
384
+ const sameRun = current !== null && (runId === undefined || runId === current.runId);
385
+ if (sameRun && current !== null && LIVE_STATUSES.has(current.status)) {
386
+ if (current.status === 'escalated') {
387
+ return composeDigest(core, {
388
+ headline: `ESCALATION: run ${current.runId} is waiting for the user's ruling.`,
389
+ standings: true,
390
+ });
391
+ }
392
+ if (current.status === 'paused')
393
+ reactivate(core);
394
+ pruneOnce(core);
395
+ return finishResume(core);
396
+ }
397
+ const target = runId ?? (await core.deps.store.latestResumable(core.deps.getCwd()))?.runId ?? undefined;
398
+ if (target === undefined) {
399
+ return refusal(core, 'there is no unfinished governance run for this directory to resume.');
400
+ }
401
+ const loaded = await loadForResume(core, target);
402
+ if (!loaded.ok)
403
+ return refusal(core, loaded.message);
404
+ const run = core.requireRun();
405
+ run.status = run.status === 'escalated' ? 'escalated' : 'paused';
406
+ if (run.status === 'paused')
407
+ reactivate(core);
408
+ else {
409
+ core.attachTeam();
410
+ core.checkpoint();
411
+ }
412
+ pruneOnce(core);
413
+ return finishResume(core);
414
+ }
415
+ async function finishResume(core) {
416
+ const run = core.requireRun();
417
+ const warnings = core.stageWarnings.splice(0);
418
+ const flushNote = await core.flush();
419
+ const text = composeDigest(core, {
420
+ headline: `OK: run ${run.runId} resumed (${run.status}). Goal: ${run.goal}`,
421
+ results: [renderPhases(run), renderWpTable(run)],
422
+ warnings,
423
+ standings: true,
424
+ });
425
+ return flushNote === null ? text : `${text}\n${flushNote}`;
426
+ }
427
+ // ---------------------------------------------------------------------------
428
+ // status
429
+ // ---------------------------------------------------------------------------
430
+ /** `gov_run status`: the board as text. Rebuildable by a compacted orchestrator (R-11). */
431
+ export function statusText(core) {
432
+ const run = core.run;
433
+ if (run === null) {
434
+ return ('OK: no governance run is active in this session.\n' +
435
+ 'NEXT:\n gov_run {"action":"start","goal":"...","spec":"...","workPackages":[...]}\n' +
436
+ ' gov_run {"action":"resume"} (continues the latest unfinished run for this directory)');
437
+ }
438
+ const recent = core
439
+ .snapshot()
440
+ ?.recent.map((r) => {
441
+ const sign = r.delta > 0 ? '+' : '';
442
+ return `${r.agentId} ${sign}${r.delta}${r.provisional ? 'P' : ''} ${r.code} ${r.reason}`;
443
+ });
444
+ const open = run.escalations.filter((e) => e.status === 'open');
445
+ const results = [renderPhases(run), renderWpTable(run)];
446
+ if (run.claims.some((c) => c.status === 'open')) {
447
+ const openIds = run.claims.filter((c) => c.status === 'open').map((c) => c.id);
448
+ results.push(`Open claims: ${openIds.join(', ')}`);
449
+ }
450
+ if (open.length > 0) {
451
+ results.push(`Open escalations: ${open.map((e) => `${e.id} (${e.topic})`).join(' | ')}`);
452
+ }
453
+ if (recent && recent.length > 0)
454
+ results.push(`Recent ledger: ${recent.join(' | ')}`);
455
+ const statusWord = run.status === 'escalated' ? 'ESCALATION' : 'OK';
456
+ return composeDigest(core, {
457
+ headline: `${statusWord}: run ${run.runId} is ${run.status}. Goal: ${run.goal}`,
458
+ results,
459
+ warnings: [
460
+ 'NOTE: token figures are TEAM tokens (children + supervisor); the orchestrator\'s own ' +
461
+ 'spend is in the status bar.',
462
+ ],
463
+ standings: true,
464
+ });
465
+ }
466
+ // ---------------------------------------------------------------------------
467
+ // close / abandon
468
+ // ---------------------------------------------------------------------------
469
+ function finalize(core, summary, abandoned) {
470
+ const run = core.requireRun();
471
+ core.pauseRequested = false;
472
+ core.userPaused = false;
473
+ const rows = core.standingRows();
474
+ const board = { run, standings: rows, health: core.health(), recent: [] };
475
+ core.writeFile('board.md', buildBoardMarkdown(board));
476
+ const report = buildFinalReport({
477
+ ...board,
478
+ ...(summary !== undefined ? { summary } : {}),
479
+ wallMs: core.now() - run.createdAt,
480
+ });
481
+ core.writeFile('reports/final.md', report);
482
+ core.exportScoreboard();
483
+ core.log('run_closed', 'engine', { abandoned, ...(summary ? { summary } : {}) });
484
+ core.checkpoint();
485
+ core.emit((snapshot) => ({ type: 'run_closed', snapshot, card: finalCard(core) }));
486
+ }
487
+ async function releaseRun(core) {
488
+ const runId = core.run?.runId;
489
+ await core.flush();
490
+ if (runId !== undefined) {
491
+ core.deps.store.stopHeartbeat(runId);
492
+ await core.deps.store.releaseLock(runId);
493
+ }
494
+ }
495
+ function unfinishedList(run) {
496
+ return run.wpOrder
497
+ .map((id) => run.workPackages[id])
498
+ .filter((w) => w !== undefined && !['integrated', 'dropped'].includes(w.status))
499
+ .map((w) => `${w.id} (${w.status})`)
500
+ .join(', ');
501
+ }
502
+ /** `gov_run close`: only when every phase is integrated (or the run was abandoned). */
503
+ export async function closeRun(core, summary) {
504
+ const run = core.run;
505
+ if (run === null)
506
+ return refusal(core, 'there is no governance run to close.');
507
+ if (run.status === 'closed')
508
+ return refusal(core, `run ${run.runId} is already closed.`);
509
+ if (run.status !== 'abandoned' && !canClose(run)) {
510
+ return refusal(core, `run ${run.runId} cannot close yet: not every phase is integrated ` +
511
+ `(unfinished: ${unfinishedList(run) || 'none'}).`);
512
+ }
513
+ if (run.status !== 'abandoned')
514
+ run.status = 'closed';
515
+ run.stage = run.stage !== null && run.stage.status === 'running' ? null : run.stage;
516
+ finalize(core, summary, false);
517
+ await releaseRun(core);
518
+ const report = buildFinalReport({
519
+ run,
520
+ standings: core.standingRows(),
521
+ health: core.health(),
522
+ recent: [],
523
+ ...(summary !== undefined ? { summary } : {}),
524
+ wallMs: core.now() - run.createdAt,
525
+ });
526
+ return composeDigest(core, {
527
+ headline: `OK: run ${run.runId} closed.`,
528
+ results: [report.trimEnd()],
529
+ standings: false,
530
+ });
531
+ }
532
+ /** `gov_run abandon`: keeps every artifact, ends the run, frees the directory for a new one. */
533
+ export async function abandonRun(core, reason) {
534
+ const run = core.run;
535
+ if (run === null)
536
+ return refusal(core, 'there is no governance run to abandon.');
537
+ if (run.status === 'closed' || run.status === 'abandoned') {
538
+ return refusal(core, `run ${run.runId} is already ${run.status}.`);
539
+ }
540
+ if (core.isStageRunning())
541
+ core.abortStage();
542
+ recoverInterruptedStage(run);
543
+ run.status = 'abandoned';
544
+ for (const e of run.escalations) {
545
+ if (e.status === 'open')
546
+ closeEscalation(run, e, `run abandoned: ${reason}`);
547
+ }
548
+ run.status = 'abandoned';
549
+ finalize(core, `abandoned: ${reason}`, true);
550
+ await releaseRun(core);
551
+ return composeDigest(core, {
552
+ headline: `OK: run ${run.runId} abandoned (${reason}). All artifacts are kept on disk.`,
553
+ standings: true,
554
+ });
555
+ }
556
+ // ---------------------------------------------------------------------------
557
+ // resolve (L3)
558
+ // ---------------------------------------------------------------------------
559
+ const DISPOSITIONS = [
560
+ 'retry',
561
+ 'accept_as_is',
562
+ 'drop',
563
+ 'accept_failing',
564
+ ];
565
+ function normalizeResolve(raw) {
566
+ const issues = new Issues();
567
+ const args = isRecord(raw) ? raw : {};
568
+ const escalationId = issues.str(args.escalationId, 'escalationId', 40, true);
569
+ const ruling = issues.str(args.ruling, 'ruling', GOV_LIMITS.rulingChars, true);
570
+ const favor = issues.str(args.favor, 'favor', 80);
571
+ const disposition = issues.oneOf(args.disposition, 'disposition', DISPOSITIONS);
572
+ if (issues.errors.length > 0)
573
+ return { ok: false, errors: issues.errors };
574
+ return {
575
+ ok: true,
576
+ value: {
577
+ escalationId,
578
+ ruling,
579
+ ...(favor !== '' ? { favor } : {}),
580
+ ...(disposition !== undefined ? { disposition } : {}),
581
+ },
582
+ };
583
+ }
584
+ /** Open a budget escalation (called by the stage runner when the cap is hit). */
585
+ export function openBudgetEscalation(core) {
586
+ const run = core.requireRun();
587
+ const existing = run.escalations.find((e) => e.status === 'open' && e.topic.startsWith(BUDGET_TOPIC_PREFIX));
588
+ if (existing !== undefined)
589
+ return existing;
590
+ const escalation = {
591
+ id: nextEscalationId(run),
592
+ level: 'L3',
593
+ kind: 'dispute',
594
+ topic: `${BUDGET_TOPIC_PREFIX} is exhausted (${run.usage.inputTokens + run.usage.outputTokens} ` +
595
+ `of ${core.config().budgetTokens}). Raise the budget (/gov budget <n>) or abandon the run.`,
596
+ positions: [],
597
+ status: 'open',
598
+ };
599
+ run.escalations.push(escalation);
600
+ run.status = 'escalated';
601
+ core.log('escalation', 'engine', { id: escalation.id, kind: 'budget' });
602
+ core.checkpoint();
603
+ core.emit((snapshot) => ({
604
+ type: 'escalation',
605
+ snapshot,
606
+ escalation,
607
+ card: escalationCard(core, escalation),
608
+ }));
609
+ return escalation;
610
+ }
611
+ function budgetStillExhausted(core, escalation) {
612
+ const run = core.requireRun();
613
+ return (escalation.topic.startsWith(BUDGET_TOPIC_PREFIX) &&
614
+ shouldStopNewStages(run.usage, core.config().budgetTokens));
615
+ }
616
+ /** `gov_run resolve`: the user's L3 ruling, optionally with a disposition (7.2). */
617
+ export async function resolveEscalation(core, raw) {
618
+ const run = core.run;
619
+ if (run === null)
620
+ return refusal(core, 'there is no governance run.');
621
+ const parsed = normalizeResolve(raw);
622
+ if (!parsed.ok)
623
+ return formatNotAccepted('gov_run resolve', parsed.errors);
624
+ const input = parsed.value;
625
+ const escalation = run.escalations.find((e) => e.id === input.escalationId);
626
+ if (escalation === undefined) {
627
+ const open = run.escalations.filter((e) => e.status === 'open').map((e) => e.id);
628
+ return refusal(core, `unknown escalation '${input.escalationId}' (open: ${open.join(', ') || 'none'}).`);
629
+ }
630
+ if (budgetStillExhausted(core, escalation) && input.disposition === undefined) {
631
+ return refusal(core, 'the team-token budget is still exhausted. Ask the user to raise it (/gov budget <n>), ' +
632
+ 'then resolve again, or abandon the run.');
633
+ }
634
+ const applied = applyResolveDisposition(run, escalation, input.disposition);
635
+ if (!applied.ok)
636
+ return composeDigest(core, { headline: applied.message });
637
+ closeEscalation(run, escalation, input.ruling);
638
+ core.log('resolution', 'user', {
639
+ id: escalation.id,
640
+ ruling: input.ruling,
641
+ ...(input.favor ? { favor: input.favor } : {}),
642
+ ...(input.disposition ? { disposition: input.disposition } : {}),
643
+ });
644
+ core.checkpoint();
645
+ core.emit((snapshot) => ({ type: 'agent_state', snapshot }));
646
+ const flushNote = await core.flush();
647
+ const warnings = applied.affectedDependents.length > 0
648
+ ? [`WARNING: dependent work package(s) affected: ${applied.affectedDependents.join(', ')}.`]
649
+ : [];
650
+ const text = composeDigest(core, {
651
+ headline: `OK: ${escalation.id} resolved. ${applied.message}`,
652
+ warnings,
653
+ standings: false,
654
+ });
655
+ return flushNote === null ? text : `${text}\n${flushNote}`;
656
+ }