@nimiplatform/nimi-coding 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/adapters/README.md +25 -0
- package/adapters/claude/README.md +89 -0
- package/adapters/claude/profile.yaml +70 -0
- package/adapters/codex/README.md +53 -0
- package/adapters/codex/profile.yaml +78 -0
- package/adapters/oh-my-codex/README.md +185 -0
- package/adapters/oh-my-codex/profile.yaml +46 -0
- package/bin/nimicoding.mjs +6 -0
- package/cli/commands/admit-high-risk-decision.mjs +108 -0
- package/cli/commands/audit-sweep.mjs +341 -0
- package/cli/commands/blueprint-audit.mjs +91 -0
- package/cli/commands/clear.mjs +168 -0
- package/cli/commands/closeout.mjs +183 -0
- package/cli/commands/decide-high-risk-execution.mjs +124 -0
- package/cli/commands/doctor.mjs +53 -0
- package/cli/commands/generate-spec-derived-docs.mjs +131 -0
- package/cli/commands/handoff.mjs +123 -0
- package/cli/commands/ingest-high-risk-execution.mjs +95 -0
- package/cli/commands/review-high-risk-execution.mjs +95 -0
- package/cli/commands/start.mjs +717 -0
- package/cli/commands/topic-formatters.mjs +382 -0
- package/cli/commands/topic-goal.mjs +33 -0
- package/cli/commands/topic-options-shared.mjs +27 -0
- package/cli/commands/topic-options-workflow.mjs +767 -0
- package/cli/commands/topic-options.mjs +626 -0
- package/cli/commands/topic-runner.mjs +169 -0
- package/cli/commands/topic.mjs +795 -0
- package/cli/commands/validate-acceptance.mjs +5 -0
- package/cli/commands/validate-ai-governance.mjs +214 -0
- package/cli/commands/validate-execution-packet.mjs +5 -0
- package/cli/commands/validate-orchestration-state.mjs +5 -0
- package/cli/commands/validate-prompt.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +27 -0
- package/cli/commands/validate-spec-governance.mjs +124 -0
- package/cli/commands/validate-spec-tree.mjs +27 -0
- package/cli/commands/validate-worker-output.mjs +5 -0
- package/cli/constants.mjs +489 -0
- package/cli/help.mjs +134 -0
- package/cli/index.mjs +103 -0
- package/cli/lib/adapter-profiles.mjs +403 -0
- package/cli/lib/audit-execution.mjs +52 -0
- package/cli/lib/audit-sweep-runtime/admissions.mjs +381 -0
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +333 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +697 -0
- package/cli/lib/audit-sweep-runtime/closeout.mjs +144 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +639 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +515 -0
- package/cli/lib/audit-sweep-runtime/common.mjs +329 -0
- package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +172 -0
- package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +152 -0
- package/cli/lib/audit-sweep-runtime/format.mjs +57 -0
- package/cli/lib/audit-sweep-runtime/ingest.mjs +486 -0
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +198 -0
- package/cli/lib/audit-sweep-runtime/inventory.mjs +728 -0
- package/cli/lib/audit-sweep-runtime/ledger.mjs +315 -0
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +101 -0
- package/cli/lib/audit-sweep-runtime/remediation.mjs +349 -0
- package/cli/lib/audit-sweep-runtime/rerun.mjs +129 -0
- package/cli/lib/audit-sweep-runtime/risk-budget.mjs +300 -0
- package/cli/lib/audit-sweep-runtime/status.mjs +62 -0
- package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +215 -0
- package/cli/lib/audit-sweep-runtime/validators.mjs +758 -0
- package/cli/lib/audit-sweep.mjs +18 -0
- package/cli/lib/authority-convergence.mjs +309 -0
- package/cli/lib/blueprint-audit.mjs +370 -0
- package/cli/lib/bootstrap.mjs +228 -0
- package/cli/lib/closeout.mjs +623 -0
- package/cli/lib/codex-sdk-runner.mjs +76 -0
- package/cli/lib/contracts.mjs +180 -0
- package/cli/lib/doctor.mjs +18 -0
- package/cli/lib/entrypoints.mjs +274 -0
- package/cli/lib/external-execution.mjs +101 -0
- package/cli/lib/fs-helpers.mjs +33 -0
- package/cli/lib/handoff.mjs +785 -0
- package/cli/lib/high-risk-admission.mjs +442 -0
- package/cli/lib/high-risk-decision.mjs +324 -0
- package/cli/lib/high-risk-ingest.mjs +317 -0
- package/cli/lib/high-risk-review.mjs +263 -0
- package/cli/lib/internal/contracts-loaders.mjs +132 -0
- package/cli/lib/internal/contracts-parse-high-risk.mjs +131 -0
- package/cli/lib/internal/contracts-parse.mjs +457 -0
- package/cli/lib/internal/contracts-validators.mjs +398 -0
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +359 -0
- package/cli/lib/internal/doctor-delegated-surface.mjs +256 -0
- package/cli/lib/internal/doctor-finalize.mjs +385 -0
- package/cli/lib/internal/doctor-format.mjs +286 -0
- package/cli/lib/internal/doctor-inspectors.mjs +294 -0
- package/cli/lib/internal/doctor-state.mjs +205 -0
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +315 -0
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +358 -0
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +155 -0
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +173 -0
- package/cli/lib/internal/governance/config.mjs +150 -0
- package/cli/lib/internal/governance/runner.mjs +35 -0
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +49 -0
- package/cli/lib/internal/validators-artifacts.mjs +515 -0
- package/cli/lib/internal/validators-shared.mjs +28 -0
- package/cli/lib/internal/validators-spec-helpers.mjs +186 -0
- package/cli/lib/internal/validators-spec.mjs +410 -0
- package/cli/lib/shared.mjs +83 -0
- package/cli/lib/topic-draft-packets.mjs +48 -0
- package/cli/lib/topic-goal.mjs +361 -0
- package/cli/lib/topic-runner.mjs +772 -0
- package/cli/lib/topic.mjs +93 -0
- package/cli/lib/ui.mjs +178 -0
- package/cli/lib/validators.mjs +78 -0
- package/cli/lib/value-helpers.mjs +24 -0
- package/cli/lib/yaml-helpers.mjs +133 -0
- package/cli/nimicoding.mjs +1 -0
- package/cli/seeds/bootstrap.mjs +47 -0
- package/config/audit-execution-artifacts.yaml +20 -0
- package/config/bootstrap.yaml +6 -0
- package/config/external-execution-artifacts.yaml +16 -0
- package/config/host-adapter.yaml +30 -0
- package/config/host-profile.yaml +29 -0
- package/config/installer-evidence.yaml +31 -0
- package/config/skill-installer.yaml +23 -0
- package/config/skill-manifest.yaml +46 -0
- package/config/skills.yaml +30 -0
- package/config/spec-generation-inputs.yaml +25 -0
- package/contracts/acceptance.schema.yaml +16 -0
- package/contracts/admission-checklist.schema.yaml +15 -0
- package/contracts/audit-chunk.schema.yaml +110 -0
- package/contracts/audit-closeout.schema.yaml +51 -0
- package/contracts/audit-finding.schema.yaml +61 -0
- package/contracts/audit-ledger.schema.yaml +138 -0
- package/contracts/audit-plan.schema.yaml +123 -0
- package/contracts/audit-remediation-map.schema.yaml +51 -0
- package/contracts/audit-rerun.schema.yaml +31 -0
- package/contracts/audit-sweep-result.yaml +49 -0
- package/contracts/authority-convergence-audit.schema.yaml +19 -0
- package/contracts/closeout.schema.yaml +25 -0
- package/contracts/decision-review.schema.yaml +16 -0
- package/contracts/doc-spec-audit-result.yaml +19 -0
- package/contracts/execution-packet.schema.yaml +49 -0
- package/contracts/external-host-compatibility.yaml +22 -0
- package/contracts/forbidden-shortcuts.catalog.yaml +23 -0
- package/contracts/high-risk-admission.schema.yaml +23 -0
- package/contracts/high-risk-execution-result.yaml +20 -0
- package/contracts/orchestration-state.schema.yaml +41 -0
- package/contracts/overflow-continuation.schema.yaml +12 -0
- package/contracts/packet.schema.yaml +30 -0
- package/contracts/pending-note.schema.yaml +17 -0
- package/contracts/prompt.schema.yaml +12 -0
- package/contracts/remediation.schema.yaml +16 -0
- package/contracts/result.schema.yaml +24 -0
- package/contracts/spec-generation-audit.schema.yaml +31 -0
- package/contracts/spec-generation-inputs.schema.yaml +39 -0
- package/contracts/spec-reconstruction-result.yaml +37 -0
- package/contracts/topic-goal.schema.yaml +78 -0
- package/contracts/topic-run-ledger.schema.yaml +72 -0
- package/contracts/topic-step-decision.schema.yaml +45 -0
- package/contracts/topic.schema.yaml +65 -0
- package/contracts/true-close.schema.yaml +15 -0
- package/contracts/wave.schema.yaml +29 -0
- package/contracts/worker-output.schema.yaml +15 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +45 -0
- package/methodology/authority-convergence-policy.yaml +42 -0
- package/methodology/core.yaml +25 -0
- package/methodology/four-closure-policy.yaml +28 -0
- package/methodology/overflow-continuation-policy.yaml +14 -0
- package/methodology/role-separation-policy.yaml +28 -0
- package/methodology/skill-exchange-projection.yaml +114 -0
- package/methodology/skill-handoff.yaml +34 -0
- package/methodology/skill-installer-result.yaml +27 -0
- package/methodology/skill-installer-summary-projection.yaml +181 -0
- package/methodology/skill-runtime.yaml +23 -0
- package/methodology/spec-reconstruction.yaml +63 -0
- package/methodology/spec-target-truth-profile.yaml +53 -0
- package/methodology/topic-lifecycle-report.yaml +144 -0
- package/methodology/topic-lifecycle.yaml +37 -0
- package/methodology/topic-naming-ontology.yaml +21 -0
- package/methodology/topic-ontology.yaml +38 -0
- package/methodology/topic-validation-policy.yaml +9 -0
- package/methodology/wave-dag-policy.yaml +14 -0
- package/package.json +50 -0
- package/spec/_meta/command-gating-matrix.yaml +110 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +155 -0
- package/spec/_meta/governance-routing-cutover-checklist.yaml +35 -0
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +44 -0
- package/spec/_meta/spec-authority-cutover-readiness.yaml +104 -0
- package/spec/_meta/spec-tree-model.yaml +72 -0
- package/spec/bootstrap-state.yaml +99 -0
- package/spec/product-scope.yaml +56 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { runNativeCodexSdkPrompt } from "./codex-sdk-runner.mjs";
|
|
5
|
+
import {
|
|
6
|
+
admitWaveInTopic,
|
|
7
|
+
buildTopicRunLedger,
|
|
8
|
+
closeoutWaveInTopic,
|
|
9
|
+
decideTopicNextStep,
|
|
10
|
+
dispatchTopicPacket,
|
|
11
|
+
freezePacketForTopic,
|
|
12
|
+
initTopicRunLedger,
|
|
13
|
+
loadTopicReport,
|
|
14
|
+
readTopicRunLedger,
|
|
15
|
+
recordTopicRunEvent,
|
|
16
|
+
} from "./topic.mjs";
|
|
17
|
+
|
|
18
|
+
const ADAPTER_IDS = new Set(["codex", "oh_my_codex", "claude"]);
|
|
19
|
+
|
|
20
|
+
function utcNowNoMillis() {
|
|
21
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toPortablePath(value) {
|
|
25
|
+
return value.split(path.sep).join("/");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function projectRef(projectRoot, absolutePath) {
|
|
29
|
+
return toPortablePath(path.relative(projectRoot, absolutePath));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasPlaceholder(value) {
|
|
33
|
+
return /<[^>]+>/.test(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safeSegment(value) {
|
|
37
|
+
return String(value).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function remapTopicRef(ref, fromTopicRef, toTopicRef) {
|
|
41
|
+
if (!ref || !fromTopicRef || !toTopicRef || fromTopicRef === toTopicRef) {
|
|
42
|
+
return ref;
|
|
43
|
+
}
|
|
44
|
+
return ref.startsWith(`${fromTopicRef}/`)
|
|
45
|
+
? `${toTopicRef}/${ref.slice(fromTopicRef.length + 1)}`
|
|
46
|
+
: ref;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildGate(decision) {
|
|
50
|
+
if (!decision || decision.stop_class === "continue") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
stopClass: decision.stop_class,
|
|
55
|
+
reasonCode: decision.reason_code,
|
|
56
|
+
recommendedAction: decision.recommended_action,
|
|
57
|
+
recommendedDecision: decision.recommended_decision,
|
|
58
|
+
recommendationRationale: decision.recommendation_rationale,
|
|
59
|
+
nextCommandRef: decision.next_command_ref,
|
|
60
|
+
blockingChecks: decision.blocking_checks ?? [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isTerminalWave(wave) {
|
|
65
|
+
return ["closed", "retired", "superseded"].includes(wave?.state);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findDeterministicNextWave(topic) {
|
|
69
|
+
const waves = Array.isArray(topic?.waves) ? topic.waves : [];
|
|
70
|
+
const terminalIds = new Set(waves.filter(isTerminalWave).map((wave) => wave.wave_id));
|
|
71
|
+
const ready = waves.filter((wave) => {
|
|
72
|
+
if (isTerminalWave(wave)) return false;
|
|
73
|
+
if (!["candidate", "preflight_draft", "needs_revision"].includes(wave.state)) return false;
|
|
74
|
+
const deps = Array.isArray(wave.deps) ? wave.deps : [];
|
|
75
|
+
return deps.every((dep) => terminalIds.has(dep));
|
|
76
|
+
});
|
|
77
|
+
return ready.length === 1 ? ready[0] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizePhaseTransitionDecision(decisionReport, topic) {
|
|
81
|
+
const decision = decisionReport?.decision;
|
|
82
|
+
if (!decision || decision.stop_class !== "require_human_confirmation") {
|
|
83
|
+
return decisionReport;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
decision.recommended_action === "closeout_wave" &&
|
|
88
|
+
typeof decision.next_command_ref === "string" &&
|
|
89
|
+
!hasPlaceholder(decision.next_command_ref)
|
|
90
|
+
) {
|
|
91
|
+
return {
|
|
92
|
+
...decisionReport,
|
|
93
|
+
decision: {
|
|
94
|
+
...decision,
|
|
95
|
+
stop_class: "continue",
|
|
96
|
+
requires_human_confirmation: false,
|
|
97
|
+
recommended_decision: "closeout_wave",
|
|
98
|
+
recommendation_rationale: "Wave closeout is a deterministic phase transition once lineage-backed result evidence exists.",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (decision.reason_code === "no_selected_next_target") {
|
|
104
|
+
const nextWave = findDeterministicNextWave(topic);
|
|
105
|
+
if (!nextWave) {
|
|
106
|
+
return decisionReport;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...decisionReport,
|
|
110
|
+
decision: {
|
|
111
|
+
...decision,
|
|
112
|
+
wave_id: nextWave.wave_id,
|
|
113
|
+
stop_class: "continue",
|
|
114
|
+
recommended_action: "admit_wave",
|
|
115
|
+
reason_code: "deterministic_next_wave_ready",
|
|
116
|
+
requires_human_confirmation: false,
|
|
117
|
+
recommended_decision: "admit_wave",
|
|
118
|
+
recommendation_rationale: "Exactly one dependency-ready non-terminal wave exists, so selecting the next phase is mechanical.",
|
|
119
|
+
expected_artifacts: [],
|
|
120
|
+
next_command_ref: `nimicoding topic wave admit ${topic.topic_id} ${nextWave.wave_id}`,
|
|
121
|
+
blocking_checks: [],
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return decisionReport;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function blockedResult(base, error, extra = {}) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
...base,
|
|
133
|
+
runnerStatus: "blocked",
|
|
134
|
+
stopClass: "blocked",
|
|
135
|
+
recommendedAction: "no_action",
|
|
136
|
+
error,
|
|
137
|
+
circuitBreaker: {
|
|
138
|
+
state: "open",
|
|
139
|
+
reason: error,
|
|
140
|
+
},
|
|
141
|
+
...extra,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function ensureLedger(projectRoot, topicInput, runId, startedAt) {
|
|
146
|
+
const existing = await readTopicRunLedger(projectRoot, topicInput, runId);
|
|
147
|
+
if (existing.ok) {
|
|
148
|
+
return existing;
|
|
149
|
+
}
|
|
150
|
+
if (!String(existing.error ?? "").includes("not found")) {
|
|
151
|
+
return existing;
|
|
152
|
+
}
|
|
153
|
+
return initTopicRunLedger(projectRoot, topicInput, runId, startedAt);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function writeDecisionArtifact(projectRoot, loaded, runId, decision, eventIndex) {
|
|
157
|
+
const decisionPath = path.join(
|
|
158
|
+
loaded.topicDir,
|
|
159
|
+
`runner-decision-${safeSegment(runId)}-${String(eventIndex).padStart(4, "0")}.json`,
|
|
160
|
+
);
|
|
161
|
+
await writeFile(decisionPath, `${JSON.stringify(decision, null, 2)}\n`, "utf8");
|
|
162
|
+
return projectRef(projectRoot, decisionPath);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function recordRunnerBlocked(projectRoot, options, fields) {
|
|
166
|
+
return recordTopicRunEvent(projectRoot, options.topicInput, {
|
|
167
|
+
runId: options.runId,
|
|
168
|
+
eventKind: "runner_blocked",
|
|
169
|
+
stopClass: "blocked",
|
|
170
|
+
recommendedAction: "no_action",
|
|
171
|
+
sourceRef: fields.sourceRef,
|
|
172
|
+
summary: fields.summary,
|
|
173
|
+
recordedAt: fields.recordedAt,
|
|
174
|
+
waveId: fields.waveId,
|
|
175
|
+
artifactRefs: fields.artifactRefs ?? {},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function rewriteMovedRunEventRef(projectRoot, eventRef, fromTopicRef, toTopicRef, fromRef, toRef) {
|
|
180
|
+
if (fromRef === toRef) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const movedEventRef = remapTopicRef(eventRef, fromTopicRef, toTopicRef);
|
|
184
|
+
const eventPath = path.join(projectRoot, movedEventRef);
|
|
185
|
+
const eventText = await readFile(eventPath, "utf8");
|
|
186
|
+
await writeFile(eventPath, eventText.split(fromRef).join(toRef), "utf8");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseMechanicalCommandRef(commandRef, topicId) {
|
|
190
|
+
if (typeof commandRef !== "string" || commandRef.trim().length === 0) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: "topic-runner refused: decision.next_command_ref is empty",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (hasPlaceholder(commandRef)) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
error: `topic-runner refused: decision.next_command_ref contains a placeholder: ${commandRef}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const parts = commandRef.trim().split(/\s+/);
|
|
204
|
+
const expectedPrefix = ["nimicoding", "topic"];
|
|
205
|
+
if (parts[0] !== expectedPrefix[0] || parts[1] !== expectedPrefix[1]) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
error: `topic-runner refused: next command is not a package-owned topic command: ${commandRef}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const [domain, action, commandTopicId] = parts.slice(2, 5);
|
|
213
|
+
if (domain === "wave" && action === "admit") {
|
|
214
|
+
const waveId = parts[5] ?? null;
|
|
215
|
+
if (commandTopicId !== topicId) {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: `topic-runner refused: next command topic ${commandTopicId} does not match ${topicId}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (!waveId || waveId.startsWith("--")) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: `topic-runner refused: wave admit command is missing wave id: ${commandRef}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
action: "admit_wave",
|
|
230
|
+
waveId,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (domain === "packet" && action === "freeze") {
|
|
235
|
+
if (commandTopicId !== topicId) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: `topic-runner refused: next command topic ${commandTopicId} does not match ${topicId}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const fromFlagIndex = parts.indexOf("--from");
|
|
242
|
+
const draftPath = fromFlagIndex >= 0 ? parts[fromFlagIndex + 1] : null;
|
|
243
|
+
if (!draftPath || draftPath.startsWith("--")) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: `topic-runner refused: packet freeze command is missing --from: ${commandRef}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
action: "freeze_packet",
|
|
252
|
+
draftPath,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (["worker", "audit"].includes(domain) && action === "dispatch") {
|
|
257
|
+
if (commandTopicId !== topicId) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
error: `topic-runner refused: next command topic ${commandTopicId} does not match ${topicId}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const packetFlagIndex = parts.indexOf("--packet");
|
|
265
|
+
const packetId = packetFlagIndex >= 0 ? parts[packetFlagIndex + 1] : null;
|
|
266
|
+
if (!packetId || packetId.startsWith("--")) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: `topic-runner refused: dispatch command is missing --packet: ${commandRef}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
action: domain === "audit" ? "dispatch_audit" : "dispatch_worker",
|
|
276
|
+
role: domain,
|
|
277
|
+
packetId,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (domain === "closeout" && action === "wave") {
|
|
282
|
+
if (commandTopicId !== topicId) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
error: `topic-runner refused: next command topic ${commandTopicId} does not match ${topicId}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const waveId = parts[5] ?? null;
|
|
289
|
+
if (!waveId || waveId.startsWith("--")) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: `topic-runner refused: closeout wave command is missing wave id: ${commandRef}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const optionValue = (flag) => {
|
|
297
|
+
const index = parts.indexOf(flag);
|
|
298
|
+
return index >= 0 ? parts[index + 1] : null;
|
|
299
|
+
};
|
|
300
|
+
const authorityClosure = optionValue("--authority");
|
|
301
|
+
const semanticClosure = optionValue("--semantic");
|
|
302
|
+
const consumerClosure = optionValue("--consumer");
|
|
303
|
+
const driftResistanceClosure = optionValue("--drift-resistance");
|
|
304
|
+
const disposition = optionValue("--disposition");
|
|
305
|
+
if (
|
|
306
|
+
!authorityClosure || authorityClosure.startsWith("--") ||
|
|
307
|
+
!semanticClosure || semanticClosure.startsWith("--") ||
|
|
308
|
+
!consumerClosure || consumerClosure.startsWith("--") ||
|
|
309
|
+
!driftResistanceClosure || driftResistanceClosure.startsWith("--") ||
|
|
310
|
+
!disposition || disposition.startsWith("--")
|
|
311
|
+
) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: `topic-runner refused: closeout wave command is missing required closure flags: ${commandRef}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
ok: true,
|
|
320
|
+
action: "closeout_wave",
|
|
321
|
+
waveId,
|
|
322
|
+
authorityClosure,
|
|
323
|
+
semanticClosure,
|
|
324
|
+
consumerClosure,
|
|
325
|
+
driftResistanceClosure,
|
|
326
|
+
disposition,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
error: `topic-runner refused: unsupported mechanical next command: ${commandRef}`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function executeMechanicalCommand(projectRoot, options, parsedCommand) {
|
|
337
|
+
if (parsedCommand.action === "admit_wave") {
|
|
338
|
+
const report = await admitWaveInTopic(projectRoot, options.topicInput, parsedCommand.waveId);
|
|
339
|
+
return {
|
|
340
|
+
ok: report.ok,
|
|
341
|
+
action: parsedCommand.action,
|
|
342
|
+
report,
|
|
343
|
+
eventKind: "wave_admitted",
|
|
344
|
+
eventSourceRef: report.ok ? `${report.topicRef}/topic.yaml` : null,
|
|
345
|
+
summary: report.ok ? "wave_admit_completed" : "runner_wave_admit_failed",
|
|
346
|
+
artifactRefs: {},
|
|
347
|
+
waveId: report.ok ? report.waveId : parsedCommand.waveId,
|
|
348
|
+
error: report.ok ? null : report.error,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (parsedCommand.action === "freeze_packet") {
|
|
353
|
+
const report = await freezePacketForTopic(projectRoot, options.topicInput, parsedCommand.draftPath);
|
|
354
|
+
return {
|
|
355
|
+
ok: report.ok,
|
|
356
|
+
action: parsedCommand.action,
|
|
357
|
+
report,
|
|
358
|
+
eventKind: "packet_frozen",
|
|
359
|
+
eventSourceRef: report.ok ? report.packetRef : null,
|
|
360
|
+
summary: report.ok ? "packet_freeze_completed" : "runner_packet_freeze_failed",
|
|
361
|
+
artifactRefs: report.ok ? { packet_ref: report.packetRef } : {},
|
|
362
|
+
waveId: report.ok ? report.waveId : null,
|
|
363
|
+
error: report.ok ? null : report.error,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (parsedCommand.action === "closeout_wave") {
|
|
368
|
+
const report = await closeoutWaveInTopic(projectRoot, options.topicInput, parsedCommand.waveId, {
|
|
369
|
+
authorityClosure: parsedCommand.authorityClosure,
|
|
370
|
+
semanticClosure: parsedCommand.semanticClosure,
|
|
371
|
+
consumerClosure: parsedCommand.consumerClosure,
|
|
372
|
+
driftResistanceClosure: parsedCommand.driftResistanceClosure,
|
|
373
|
+
disposition: parsedCommand.disposition,
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
ok: report.ok,
|
|
377
|
+
action: parsedCommand.action,
|
|
378
|
+
report,
|
|
379
|
+
eventKind: "wave_closed",
|
|
380
|
+
eventSourceRef: report.ok ? report.closeoutRef : null,
|
|
381
|
+
summary: report.ok ? "wave_closeout_completed" : "runner_wave_closeout_failed",
|
|
382
|
+
artifactRefs: report.ok ? { closeout_ref: report.closeoutRef } : {},
|
|
383
|
+
waveId: report.ok ? report.waveId : parsedCommand.waveId,
|
|
384
|
+
error: report.ok ? null : report.error,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
action: parsedCommand.action,
|
|
391
|
+
report: null,
|
|
392
|
+
eventKind: null,
|
|
393
|
+
eventSourceRef: null,
|
|
394
|
+
summary: "runner_unsupported_mechanical_command",
|
|
395
|
+
artifactRefs: {},
|
|
396
|
+
waveId: null,
|
|
397
|
+
error: `topic-runner refused: unsupported mechanical action ${parsedCommand.action}`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function writeHostOutput(projectRoot, options, dispatchReport, hostReport, recordedAt) {
|
|
402
|
+
const outputDir = path.join(projectRoot, ".nimi", "local", "outputs", safeSegment(options.runId));
|
|
403
|
+
await mkdir(outputDir, { recursive: true });
|
|
404
|
+
const outputPath = path.join(
|
|
405
|
+
outputDir,
|
|
406
|
+
`${safeSegment(dispatchReport.role)}-${safeSegment(dispatchReport.packetId)}.codex-output.json`,
|
|
407
|
+
);
|
|
408
|
+
await writeFile(
|
|
409
|
+
outputPath,
|
|
410
|
+
`${JSON.stringify({
|
|
411
|
+
contract: "nimicoding.topic-runner.host-output.v1",
|
|
412
|
+
topic_id: dispatchReport.topicId,
|
|
413
|
+
run_id: options.runId,
|
|
414
|
+
adapter_id: options.adapter,
|
|
415
|
+
role: dispatchReport.role,
|
|
416
|
+
packet_id: dispatchReport.packetId,
|
|
417
|
+
prompt_ref: dispatchReport.promptRef,
|
|
418
|
+
recorded_at: recordedAt,
|
|
419
|
+
host_report: hostReport,
|
|
420
|
+
}, null, 2)}\n`,
|
|
421
|
+
"utf8",
|
|
422
|
+
);
|
|
423
|
+
return projectRef(projectRoot, outputPath);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function maybeExecuteHost(projectRoot, options, dispatchReport, deps) {
|
|
427
|
+
if (!options.executeHost) {
|
|
428
|
+
return {
|
|
429
|
+
ok: true,
|
|
430
|
+
executed: false,
|
|
431
|
+
hostOutputRef: null,
|
|
432
|
+
hostReport: null,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (options.adapter !== "codex") {
|
|
436
|
+
return {
|
|
437
|
+
ok: false,
|
|
438
|
+
error: `topic-runner refused: --execute-host is currently supported only for adapter codex, not ${options.adapter}`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const prompt = await readFile(path.join(projectRoot, dispatchReport.promptRef), "utf8");
|
|
443
|
+
const hostReport = await runNativeCodexSdkPrompt({
|
|
444
|
+
prompt,
|
|
445
|
+
threadId: options.threadId,
|
|
446
|
+
codex: deps.codex,
|
|
447
|
+
});
|
|
448
|
+
if (!hostReport.ok) {
|
|
449
|
+
return hostReport;
|
|
450
|
+
}
|
|
451
|
+
const hostOutputRef = await writeHostOutput(projectRoot, options, dispatchReport, hostReport, options.recordedAt);
|
|
452
|
+
return {
|
|
453
|
+
ok: true,
|
|
454
|
+
executed: true,
|
|
455
|
+
hostOutputRef,
|
|
456
|
+
hostReport,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function runTopicRunnerStep(projectRoot, options, deps = {}) {
|
|
461
|
+
if (!ADAPTER_IDS.has(options.adapter)) {
|
|
462
|
+
return {
|
|
463
|
+
ok: false,
|
|
464
|
+
error: `topic-runner refused: unsupported adapter ${options.adapter}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const recordedAt = options.verifiedAt ?? utcNowNoMillis();
|
|
469
|
+
const ledger = await ensureLedger(projectRoot, options.topicInput, options.runId, recordedAt);
|
|
470
|
+
if (!ledger.ok) {
|
|
471
|
+
return ledger;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const loaded = await loadTopicReport(projectRoot, options.topicInput);
|
|
475
|
+
if (!loaded.ok) {
|
|
476
|
+
return loaded;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const rawDecisionReport = await decideTopicNextStep(projectRoot, options.topicInput);
|
|
480
|
+
if (!rawDecisionReport.ok) {
|
|
481
|
+
return rawDecisionReport;
|
|
482
|
+
}
|
|
483
|
+
const decisionReport = normalizePhaseTransitionDecision(rawDecisionReport, loaded.topic);
|
|
484
|
+
|
|
485
|
+
const decisionRef = await writeDecisionArtifact(
|
|
486
|
+
projectRoot,
|
|
487
|
+
loaded,
|
|
488
|
+
options.runId,
|
|
489
|
+
decisionReport.decision,
|
|
490
|
+
(ledger.eventCount ?? 0) + 1,
|
|
491
|
+
);
|
|
492
|
+
const decisionEvent = await recordTopicRunEvent(projectRoot, options.topicInput, {
|
|
493
|
+
runId: options.runId,
|
|
494
|
+
eventKind: "decision_emitted",
|
|
495
|
+
stopClass: decisionReport.decision.stop_class,
|
|
496
|
+
recommendedAction: decisionReport.decision.recommended_action,
|
|
497
|
+
sourceRef: decisionRef,
|
|
498
|
+
summary: decisionReport.decision.reason_code,
|
|
499
|
+
recordedAt,
|
|
500
|
+
waveId: decisionReport.decision.wave_id,
|
|
501
|
+
artifactRefs: {
|
|
502
|
+
decision_ref: decisionRef,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
if (!decisionEvent.ok) {
|
|
506
|
+
return decisionEvent;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (decisionReport.decision.stop_class !== "continue") {
|
|
510
|
+
return {
|
|
511
|
+
ok: true,
|
|
512
|
+
topicId: decisionReport.topicId,
|
|
513
|
+
topicRef: decisionReport.topicRef,
|
|
514
|
+
runId: options.runId,
|
|
515
|
+
adapter: options.adapter,
|
|
516
|
+
runnerStatus: "stopped",
|
|
517
|
+
executed: false,
|
|
518
|
+
stopClass: decisionReport.decision.stop_class,
|
|
519
|
+
recommendedAction: decisionReport.decision.recommended_action,
|
|
520
|
+
decision: decisionReport.decision,
|
|
521
|
+
gate: buildGate(decisionReport.decision),
|
|
522
|
+
decisionRef,
|
|
523
|
+
ledgerRef: decisionEvent.ledgerRef,
|
|
524
|
+
eventCount: decisionEvent.eventCount,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const parsedCommand = parseMechanicalCommandRef(
|
|
529
|
+
decisionReport.decision.next_command_ref,
|
|
530
|
+
decisionReport.topicId,
|
|
531
|
+
);
|
|
532
|
+
if (!parsedCommand.ok) {
|
|
533
|
+
const blockedEvent = await recordRunnerBlocked(projectRoot, { ...options, verifiedAt: recordedAt }, {
|
|
534
|
+
sourceRef: decisionRef,
|
|
535
|
+
summary: "runner_refused_next_command",
|
|
536
|
+
recordedAt,
|
|
537
|
+
waveId: decisionReport.decision.wave_id,
|
|
538
|
+
artifactRefs: { decision_ref: decisionRef },
|
|
539
|
+
});
|
|
540
|
+
return blockedResult({
|
|
541
|
+
topicId: decisionReport.topicId,
|
|
542
|
+
topicRef: decisionReport.topicRef,
|
|
543
|
+
runId: options.runId,
|
|
544
|
+
adapter: options.adapter,
|
|
545
|
+
decision: decisionReport.decision,
|
|
546
|
+
decisionRef,
|
|
547
|
+
ledgerRef: blockedEvent.ok ? blockedEvent.ledgerRef : decisionEvent.ledgerRef,
|
|
548
|
+
eventCount: blockedEvent.ok ? blockedEvent.eventCount : decisionEvent.eventCount,
|
|
549
|
+
}, parsedCommand.error);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (["admit_wave", "freeze_packet", "closeout_wave"].includes(parsedCommand.action)) {
|
|
553
|
+
const commandExecution = await executeMechanicalCommand(projectRoot, { ...options, recordedAt }, parsedCommand);
|
|
554
|
+
if (!commandExecution.ok) {
|
|
555
|
+
const blockedEvent = await recordRunnerBlocked(projectRoot, { ...options, verifiedAt: recordedAt }, {
|
|
556
|
+
sourceRef: decisionRef,
|
|
557
|
+
summary: commandExecution.summary,
|
|
558
|
+
recordedAt,
|
|
559
|
+
waveId: commandExecution.waveId ?? decisionReport.decision.wave_id,
|
|
560
|
+
artifactRefs: { decision_ref: decisionRef },
|
|
561
|
+
});
|
|
562
|
+
return blockedResult({
|
|
563
|
+
topicId: decisionReport.topicId,
|
|
564
|
+
topicRef: decisionReport.topicRef,
|
|
565
|
+
runId: options.runId,
|
|
566
|
+
adapter: options.adapter,
|
|
567
|
+
decision: decisionReport.decision,
|
|
568
|
+
decisionRef,
|
|
569
|
+
ledgerRef: blockedEvent.ok ? blockedEvent.ledgerRef : decisionEvent.ledgerRef,
|
|
570
|
+
eventCount: blockedEvent.ok ? blockedEvent.eventCount : decisionEvent.eventCount,
|
|
571
|
+
}, commandExecution.error ?? "topic-runner mechanical command failed");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const effectiveDecisionRef = remapTopicRef(
|
|
575
|
+
decisionRef,
|
|
576
|
+
decisionReport.topicRef,
|
|
577
|
+
commandExecution.report.topicRef,
|
|
578
|
+
);
|
|
579
|
+
await rewriteMovedRunEventRef(
|
|
580
|
+
projectRoot,
|
|
581
|
+
decisionEvent.eventRef,
|
|
582
|
+
decisionReport.topicRef,
|
|
583
|
+
commandExecution.report.topicRef,
|
|
584
|
+
decisionRef,
|
|
585
|
+
effectiveDecisionRef,
|
|
586
|
+
);
|
|
587
|
+
const artifactRefs = {
|
|
588
|
+
decision_ref: effectiveDecisionRef,
|
|
589
|
+
...commandExecution.artifactRefs,
|
|
590
|
+
};
|
|
591
|
+
const commandEvent = await recordTopicRunEvent(projectRoot, options.topicInput, {
|
|
592
|
+
runId: options.runId,
|
|
593
|
+
eventKind: commandExecution.eventKind,
|
|
594
|
+
stopClass: "continue",
|
|
595
|
+
recommendedAction: parsedCommand.action,
|
|
596
|
+
sourceRef: commandExecution.eventSourceRef,
|
|
597
|
+
summary: commandExecution.summary,
|
|
598
|
+
recordedAt,
|
|
599
|
+
waveId: commandExecution.waveId,
|
|
600
|
+
artifactRefs,
|
|
601
|
+
});
|
|
602
|
+
if (!commandEvent.ok) {
|
|
603
|
+
return commandEvent;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
ok: true,
|
|
608
|
+
topicId: commandExecution.report.topicId,
|
|
609
|
+
topicRef: commandExecution.report.topicRef,
|
|
610
|
+
runId: options.runId,
|
|
611
|
+
adapter: options.adapter,
|
|
612
|
+
runnerStatus: "continued",
|
|
613
|
+
executed: true,
|
|
614
|
+
stopClass: "continue",
|
|
615
|
+
recommendedAction: decisionReport.decision.recommended_action,
|
|
616
|
+
decision: decisionReport.decision,
|
|
617
|
+
gate: buildGate(decisionReport.decision),
|
|
618
|
+
decisionRef: effectiveDecisionRef,
|
|
619
|
+
command: commandExecution.report,
|
|
620
|
+
ledgerRef: commandEvent.ledgerRef,
|
|
621
|
+
eventCount: commandEvent.eventCount,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const dispatchReport = await dispatchTopicPacket(
|
|
626
|
+
projectRoot,
|
|
627
|
+
options.topicInput,
|
|
628
|
+
parsedCommand.packetId,
|
|
629
|
+
parsedCommand.role,
|
|
630
|
+
);
|
|
631
|
+
if (!dispatchReport.ok) {
|
|
632
|
+
const blockedEvent = await recordRunnerBlocked(projectRoot, { ...options, verifiedAt: recordedAt }, {
|
|
633
|
+
sourceRef: decisionRef,
|
|
634
|
+
summary: "runner_dispatch_failed",
|
|
635
|
+
recordedAt,
|
|
636
|
+
waveId: decisionReport.decision.wave_id,
|
|
637
|
+
artifactRefs: { decision_ref: decisionRef },
|
|
638
|
+
});
|
|
639
|
+
return blockedResult({
|
|
640
|
+
topicId: decisionReport.topicId,
|
|
641
|
+
topicRef: decisionReport.topicRef,
|
|
642
|
+
runId: options.runId,
|
|
643
|
+
adapter: options.adapter,
|
|
644
|
+
decision: decisionReport.decision,
|
|
645
|
+
decisionRef,
|
|
646
|
+
ledgerRef: blockedEvent.ok ? blockedEvent.ledgerRef : decisionEvent.ledgerRef,
|
|
647
|
+
eventCount: blockedEvent.ok ? blockedEvent.eventCount : decisionEvent.eventCount,
|
|
648
|
+
}, dispatchReport.error);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const hostExecution = await maybeExecuteHost(
|
|
652
|
+
projectRoot,
|
|
653
|
+
{ ...options, recordedAt },
|
|
654
|
+
dispatchReport,
|
|
655
|
+
deps,
|
|
656
|
+
);
|
|
657
|
+
if (!hostExecution.ok) {
|
|
658
|
+
const blockedEvent = await recordRunnerBlocked(projectRoot, { ...options, verifiedAt: recordedAt }, {
|
|
659
|
+
sourceRef: dispatchReport.promptRef,
|
|
660
|
+
summary: "runner_host_execution_failed",
|
|
661
|
+
recordedAt,
|
|
662
|
+
waveId: dispatchReport.waveId,
|
|
663
|
+
artifactRefs: {
|
|
664
|
+
decision_ref: decisionRef,
|
|
665
|
+
packet_ref: dispatchReport.packetRef,
|
|
666
|
+
prompt_ref: dispatchReport.promptRef,
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
return blockedResult({
|
|
670
|
+
topicId: decisionReport.topicId,
|
|
671
|
+
topicRef: decisionReport.topicRef,
|
|
672
|
+
runId: options.runId,
|
|
673
|
+
adapter: options.adapter,
|
|
674
|
+
decision: decisionReport.decision,
|
|
675
|
+
decisionRef,
|
|
676
|
+
dispatch: dispatchReport,
|
|
677
|
+
ledgerRef: blockedEvent.ok ? blockedEvent.ledgerRef : decisionEvent.ledgerRef,
|
|
678
|
+
eventCount: blockedEvent.ok ? blockedEvent.eventCount : decisionEvent.eventCount,
|
|
679
|
+
}, hostExecution.error);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const artifactRefs = {
|
|
683
|
+
packet_ref: dispatchReport.packetRef,
|
|
684
|
+
prompt_ref: dispatchReport.promptRef,
|
|
685
|
+
};
|
|
686
|
+
if (hostExecution.hostOutputRef) {
|
|
687
|
+
artifactRefs[dispatchReport.role === "audit" ? "audit_output_ref" : "worker_output_ref"] = hostExecution.hostOutputRef;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const dispatchEvent = await recordTopicRunEvent(projectRoot, options.topicInput, {
|
|
691
|
+
runId: options.runId,
|
|
692
|
+
eventKind: dispatchReport.role === "audit" ? "audit_dispatched" : "worker_dispatched",
|
|
693
|
+
stopClass: "continue",
|
|
694
|
+
recommendedAction: "record_result",
|
|
695
|
+
sourceRef: dispatchReport.promptRef,
|
|
696
|
+
summary: `${dispatchReport.role}_dispatch_completed`,
|
|
697
|
+
recordedAt,
|
|
698
|
+
waveId: dispatchReport.waveId,
|
|
699
|
+
artifactRefs,
|
|
700
|
+
});
|
|
701
|
+
if (!dispatchEvent.ok) {
|
|
702
|
+
return dispatchEvent;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
ok: true,
|
|
707
|
+
topicId: decisionReport.topicId,
|
|
708
|
+
topicRef: decisionReport.topicRef,
|
|
709
|
+
runId: options.runId,
|
|
710
|
+
adapter: options.adapter,
|
|
711
|
+
runnerStatus: "continued",
|
|
712
|
+
executed: true,
|
|
713
|
+
stopClass: "continue",
|
|
714
|
+
recommendedAction: decisionReport.decision.recommended_action,
|
|
715
|
+
decision: decisionReport.decision,
|
|
716
|
+
gate: buildGate(decisionReport.decision),
|
|
717
|
+
decisionRef,
|
|
718
|
+
dispatch: dispatchReport,
|
|
719
|
+
hostExecution,
|
|
720
|
+
ledgerRef: dispatchEvent.ledgerRef,
|
|
721
|
+
eventCount: dispatchEvent.eventCount,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function runTopicRunner(projectRoot, options, deps = {}) {
|
|
726
|
+
const maxSteps = Number.isInteger(options.maxSteps) && options.maxSteps > 0
|
|
727
|
+
? options.maxSteps
|
|
728
|
+
: 20;
|
|
729
|
+
const steps = [];
|
|
730
|
+
for (let index = 0; index < maxSteps; index += 1) {
|
|
731
|
+
const step = await runTopicRunnerStep(projectRoot, options, deps);
|
|
732
|
+
steps.push(step);
|
|
733
|
+
if (!step.ok || step.runnerStatus !== "continued") {
|
|
734
|
+
return {
|
|
735
|
+
...step,
|
|
736
|
+
mode: "run",
|
|
737
|
+
steps,
|
|
738
|
+
stepCount: steps.length,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const recordedAt = options.verifiedAt ?? utcNowNoMillis();
|
|
744
|
+
const latestStep = steps.at(-1) ?? null;
|
|
745
|
+
const blockedEvent = latestStep?.decisionRef
|
|
746
|
+
? await recordRunnerBlocked(projectRoot, { ...options, verifiedAt: recordedAt }, {
|
|
747
|
+
sourceRef: latestStep.decisionRef,
|
|
748
|
+
summary: "runner_max_steps_exhausted",
|
|
749
|
+
recordedAt,
|
|
750
|
+
waveId: latestStep.decision?.wave_id ?? null,
|
|
751
|
+
artifactRefs: { decision_ref: latestStep.decisionRef },
|
|
752
|
+
})
|
|
753
|
+
: null;
|
|
754
|
+
const ledger = await buildTopicRunLedger(projectRoot, options.topicInput, options.runId, recordedAt);
|
|
755
|
+
return blockedResult({
|
|
756
|
+
mode: "run",
|
|
757
|
+
topicId: latestStep?.topicId ?? null,
|
|
758
|
+
topicRef: latestStep?.topicRef ?? null,
|
|
759
|
+
runId: options.runId,
|
|
760
|
+
adapter: options.adapter,
|
|
761
|
+
steps,
|
|
762
|
+
stepCount: steps.length,
|
|
763
|
+
ledgerRef: blockedEvent?.ok ? blockedEvent.ledgerRef : ledger.ok ? ledger.ledgerRef : null,
|
|
764
|
+
eventCount: blockedEvent?.ok ? blockedEvent.eventCount : ledger.ok ? ledger.eventCount : null,
|
|
765
|
+
}, `topic-runner refused: max steps exhausted (${maxSteps})`, {
|
|
766
|
+
circuitBreaker: {
|
|
767
|
+
state: "open",
|
|
768
|
+
reason: "max_steps_exhausted",
|
|
769
|
+
maxSteps,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|