@nimiplatform/nimi-coding 0.1.0 → 0.2.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/README.md +19 -20
- package/adapters/oh-my-codex/README.md +8 -9
- package/cli/commands/audit-sweep.mjs +10 -10
- package/cli/commands/classify-spec-tree.mjs +5 -0
- package/cli/commands/closeout.mjs +3 -0
- package/cli/commands/generate-spec-derived-docs.mjs +20 -0
- package/cli/commands/generate-spec-migration-plan.mjs +30 -0
- package/cli/commands/start.mjs +5 -1
- package/cli/commands/surface-validator-command.mjs +49 -0
- package/cli/commands/sweep-design.mjs +295 -0
- package/cli/commands/sweep.mjs +22 -0
- package/cli/commands/sync.mjs +132 -0
- package/cli/commands/topic-formatters.mjs +8 -8
- package/cli/commands/validate-ai-governance.mjs +167 -46
- package/cli/commands/validate-domain-admission.mjs +5 -0
- package/cli/commands/validate-guidance-bodies.mjs +5 -0
- package/cli/commands/validate-placement.mjs +5 -0
- package/cli/commands/validate-projection-edges.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +5 -1
- package/cli/commands/validate-table-family.mjs +5 -0
- package/cli/commands/validate-tracked-output-admission.mjs +5 -0
- package/cli/constants.mjs +5 -49
- package/cli/help.mjs +33 -11
- package/cli/index.mjs +20 -2
- package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
- package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
- package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
- package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
- package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
- package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
- package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
- package/cli/lib/authority-convergence.mjs +397 -2
- package/cli/lib/blueprint-audit.mjs +5 -5
- package/cli/lib/closeout.mjs +126 -3
- package/cli/lib/contracts.mjs +21 -17
- package/cli/lib/handoff.mjs +29 -11
- package/cli/lib/high-risk-admission.mjs +60 -11
- package/cli/lib/high-risk-decision.mjs +31 -2
- package/cli/lib/high-risk-ingest.mjs +5 -1
- package/cli/lib/high-risk-review.mjs +5 -1
- package/cli/lib/internal/contracts-parse.mjs +195 -24
- package/cli/lib/internal/contracts-validators.mjs +3 -2
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
- package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
- package/cli/lib/internal/doctor-finalize.mjs +12 -8
- package/cli/lib/internal/doctor-inspectors.mjs +34 -1
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
- package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
- package/cli/lib/internal/validators-spec.mjs +229 -20
- package/cli/lib/sweep-design-runtime/common.mjs +246 -0
- package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
- package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
- package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
- package/cli/lib/sweep-design-runtime/results.mjs +324 -0
- package/cli/lib/sweep-design.mjs +8 -0
- package/cli/lib/sync.mjs +143 -0
- package/cli/lib/topic-artifacts.mjs +186 -0
- package/cli/lib/topic-authority-coverage.mjs +73 -0
- package/cli/lib/topic-closeout.mjs +560 -0
- package/cli/lib/topic-common.mjs +404 -0
- package/cli/lib/topic-decisions.mjs +332 -0
- package/cli/lib/topic-draft-packets.mjs +126 -7
- package/cli/lib/topic-execution.mjs +515 -0
- package/cli/lib/topic-goal.mjs +112 -33
- package/cli/lib/topic-ledger.mjs +281 -0
- package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
- package/cli/lib/topic-root-validation.mjs +288 -0
- package/cli/lib/topic-runner-commands.mjs +174 -0
- package/cli/lib/topic-runner-deferral.mjs +532 -0
- package/cli/lib/topic-runner-stale-gates.mjs +114 -0
- package/cli/lib/topic-runner-validation.mjs +138 -0
- package/cli/lib/topic-runner.mjs +109 -154
- package/cli/lib/topic-scaffold.mjs +252 -0
- package/cli/lib/topic-waves.mjs +403 -0
- package/cli/lib/topic.mjs +81 -93
- package/cli/lib/value-helpers.mjs +6 -1
- package/cli/seeds/bootstrap.mjs +96 -20
- package/cli/seeds/seed-policy.yaml +67 -0
- package/config/bootstrap.yaml +1 -1
- package/config/skill-manifest.yaml +4 -2
- package/config/spec-generation-inputs.yaml +41 -19
- package/contracts/audit-remediation-map.schema.yaml +1 -0
- package/contracts/audit-sweep-result.yaml +4 -0
- package/contracts/domain-admission.schema.yaml +56 -0
- package/contracts/migration-inventory.schema.yaml +80 -0
- package/contracts/negative-fixtures.yaml +91 -0
- package/contracts/placement-contract.schema.yaml +163 -0
- package/contracts/projection-edge.schema.yaml +130 -0
- package/contracts/shared-enums.yaml +68 -0
- package/contracts/spec-generation-audit.schema.yaml +19 -4
- package/contracts/spec-generation-inputs.schema.yaml +130 -29
- package/contracts/spec-reconstruction-result.yaml +9 -5
- package/contracts/surface-taxonomy.schema.yaml +201 -0
- package/contracts/sweep-design-result.yaml +349 -0
- package/contracts/table-family.schema.yaml +114 -0
- package/contracts/topic-goal.schema.yaml +10 -1
- package/contracts/tracked-output-admission.schema.yaml +70 -0
- package/contracts/workflow-consumer.schema.yaml +112 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
- package/methodology/spec-reconstruction.yaml +53 -30
- package/package.json +5 -4
- package/spec/_meta/command-gating-matrix.yaml +33 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
- package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
- package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
- package/spec/_meta/spec-tree-model.yaml +104 -36
- package/spec/bootstrap-state.yaml +36 -36
- package/spec/product-scope.yaml +13 -10
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { loadTopicReport } from "./topic.mjs";
|
|
6
|
+
|
|
7
|
+
function utcNowNoMillis() {
|
|
8
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toPortablePath(value) {
|
|
12
|
+
return value.split(path.sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function projectRef(projectRoot, absolutePath) {
|
|
16
|
+
return toPortablePath(path.relative(projectRoot, absolutePath));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function safeSegment(value) {
|
|
20
|
+
return String(value).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasPlaceholder(value) {
|
|
24
|
+
return /<[^>]+>/.test(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isFilteredPnpmCommand(command) {
|
|
28
|
+
return /(?:^|\s)pnpm\s+--filter\s+\S+\s+\S+/u.test(command);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function classifyValidationCommandResult(command, exitCode, stdout = "", stderr = "") {
|
|
32
|
+
const combinedOutput = `${stdout}\n${stderr}`;
|
|
33
|
+
if (
|
|
34
|
+
exitCode === 0
|
|
35
|
+
&& isFilteredPnpmCommand(command)
|
|
36
|
+
&& /No projects matched the filters/u.test(combinedOutput)
|
|
37
|
+
) {
|
|
38
|
+
return {
|
|
39
|
+
status: "validation_drift",
|
|
40
|
+
passed: false,
|
|
41
|
+
summary: "Filtered package command matched no projects; replace with a concrete validation command or admit an explicit no-package evidence rule.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (exitCode === 0) {
|
|
45
|
+
return { status: "pass", passed: true, summary: "validation command passed" };
|
|
46
|
+
}
|
|
47
|
+
const failureLine = combinedOutput
|
|
48
|
+
.split(/\r?\n/u)
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.find((line) => line.length > 0) ?? "validation command failed";
|
|
51
|
+
return {
|
|
52
|
+
status: "fail",
|
|
53
|
+
passed: false,
|
|
54
|
+
summary: failureLine.slice(0, 300),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runShellCommand(command, cwd) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const child = spawn(command, {
|
|
61
|
+
cwd,
|
|
62
|
+
shell: true,
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
});
|
|
65
|
+
const stdout = [];
|
|
66
|
+
const stderr = [];
|
|
67
|
+
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
68
|
+
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
69
|
+
child.on("error", (error) => {
|
|
70
|
+
resolve({
|
|
71
|
+
exitCode: 1,
|
|
72
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
73
|
+
stderr: `${Buffer.concat(stderr).toString("utf8")}${error.message}\n`,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
child.on("close", (code) => {
|
|
77
|
+
resolve({
|
|
78
|
+
exitCode: typeof code === "number" ? code : 1,
|
|
79
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
80
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function runValidationCommandEvidence(projectRoot, options) {
|
|
87
|
+
const loaded = await loadTopicReport(projectRoot, options.topicInput);
|
|
88
|
+
if (!loaded.ok) {
|
|
89
|
+
return loaded;
|
|
90
|
+
}
|
|
91
|
+
const command = String(options.command ?? "").trim();
|
|
92
|
+
if (!command || hasPlaceholder(command)) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: "topic-runner validation refused: command must be concrete.",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const cwd = path.resolve(projectRoot, options.cwd ?? ".");
|
|
99
|
+
const startedAt = options.startedAt ?? utcNowNoMillis();
|
|
100
|
+
const runResult = await runShellCommand(command, cwd);
|
|
101
|
+
const completedAt = options.completedAt ?? utcNowNoMillis();
|
|
102
|
+
const classification = classifyValidationCommandResult(
|
|
103
|
+
command,
|
|
104
|
+
runResult.exitCode,
|
|
105
|
+
runResult.stdout,
|
|
106
|
+
runResult.stderr,
|
|
107
|
+
);
|
|
108
|
+
const validationId = safeSegment(options.validationId ?? command).slice(0, 96) || "validation";
|
|
109
|
+
const evidencePath = path.join(loaded.topicDir, `evidence-validation-${validationId}.json`);
|
|
110
|
+
const evidence = {
|
|
111
|
+
contract: "nimicoding.topic-runner.validation-evidence.v1",
|
|
112
|
+
topic_id: loaded.topicId,
|
|
113
|
+
run_id: options.runId ?? null,
|
|
114
|
+
command,
|
|
115
|
+
cwd: projectRef(projectRoot, cwd),
|
|
116
|
+
started_at: startedAt,
|
|
117
|
+
completed_at: completedAt,
|
|
118
|
+
exit_code: runResult.exitCode,
|
|
119
|
+
status: classification.status,
|
|
120
|
+
stdout: runResult.stdout,
|
|
121
|
+
stderr: runResult.stderr,
|
|
122
|
+
remediation: classification.status === "validation_drift"
|
|
123
|
+
? "replace with concrete validation command or admit an explicit no-package evidence rule"
|
|
124
|
+
: null,
|
|
125
|
+
};
|
|
126
|
+
await writeFile(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
|
|
127
|
+
return {
|
|
128
|
+
ok: classification.passed,
|
|
129
|
+
topicId: loaded.topicId,
|
|
130
|
+
topicRef: projectRef(projectRoot, loaded.topicDir),
|
|
131
|
+
command,
|
|
132
|
+
exitCode: runResult.exitCode,
|
|
133
|
+
status: classification.status,
|
|
134
|
+
passed: classification.passed,
|
|
135
|
+
evidenceRef: projectRef(projectRoot, evidencePath),
|
|
136
|
+
summary: classification.summary,
|
|
137
|
+
};
|
|
138
|
+
}
|
package/cli/lib/topic-runner.mjs
CHANGED
|
@@ -2,6 +2,14 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { runNativeCodexSdkPrompt } from "./codex-sdk-runner.mjs";
|
|
5
|
+
import { parseMechanicalCommandRef } from "./topic-runner-commands.mjs";
|
|
6
|
+
export { parseMechanicalCommandRef } from "./topic-runner-commands.mjs";
|
|
7
|
+
import { deferLocalWaveBlocker } from "./topic-runner-deferral.mjs";
|
|
8
|
+
import { maybeResolveStaleHumanGate } from "./topic-runner-stale-gates.mjs";
|
|
9
|
+
export {
|
|
10
|
+
classifyValidationCommandResult,
|
|
11
|
+
runValidationCommandEvidence,
|
|
12
|
+
} from "./topic-runner-validation.mjs";
|
|
5
13
|
import {
|
|
6
14
|
admitWaveInTopic,
|
|
7
15
|
buildTopicRunLedger,
|
|
@@ -12,6 +20,7 @@ import {
|
|
|
12
20
|
initTopicRunLedger,
|
|
13
21
|
loadTopicReport,
|
|
14
22
|
readTopicRunLedger,
|
|
23
|
+
recordTopicResult,
|
|
15
24
|
recordTopicRunEvent,
|
|
16
25
|
} from "./topic.mjs";
|
|
17
26
|
|
|
@@ -65,8 +74,12 @@ function isTerminalWave(wave) {
|
|
|
65
74
|
return ["closed", "retired", "superseded"].includes(wave?.state);
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
function getTopicWaves(topic) {
|
|
78
|
+
return Array.isArray(topic?.waves) ? topic.waves : [];
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
function findDeterministicNextWave(topic) {
|
|
69
|
-
const waves =
|
|
82
|
+
const waves = getTopicWaves(topic);
|
|
70
83
|
const terminalIds = new Set(waves.filter(isTerminalWave).map((wave) => wave.wave_id));
|
|
71
84
|
const ready = waves.filter((wave) => {
|
|
72
85
|
if (isTerminalWave(wave)) return false;
|
|
@@ -74,7 +87,7 @@ function findDeterministicNextWave(topic) {
|
|
|
74
87
|
const deps = Array.isArray(wave.deps) ? wave.deps : [];
|
|
75
88
|
return deps.every((dep) => terminalIds.has(dep));
|
|
76
89
|
});
|
|
77
|
-
return ready.length
|
|
90
|
+
return ready.length > 0 ? ready[0] : null;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
function normalizePhaseTransitionDecision(decisionReport, topic) {
|
|
@@ -115,7 +128,7 @@ function normalizePhaseTransitionDecision(decisionReport, topic) {
|
|
|
115
128
|
reason_code: "deterministic_next_wave_ready",
|
|
116
129
|
requires_human_confirmation: false,
|
|
117
130
|
recommended_decision: "admit_wave",
|
|
118
|
-
recommendation_rationale: "
|
|
131
|
+
recommendation_rationale: "The first dependency-ready non-terminal wave in topic.yaml waves[] order is selected mechanically.",
|
|
119
132
|
expected_artifacts: [],
|
|
120
133
|
next_command_ref: `nimicoding topic wave admit ${topic.topic_id} ${nextWave.wave_id}`,
|
|
121
134
|
blocking_checks: [],
|
|
@@ -186,153 +199,6 @@ async function rewriteMovedRunEventRef(projectRoot, eventRef, fromTopicRef, toTo
|
|
|
186
199
|
await writeFile(eventPath, eventText.split(fromRef).join(toRef), "utf8");
|
|
187
200
|
}
|
|
188
201
|
|
|
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
202
|
async function executeMechanicalCommand(projectRoot, options, parsedCommand) {
|
|
337
203
|
if (parsedCommand.action === "admit_wave") {
|
|
338
204
|
const report = await admitWaveInTopic(projectRoot, options.topicInput, parsedCommand.waveId);
|
|
@@ -385,6 +251,29 @@ async function executeMechanicalCommand(projectRoot, options, parsedCommand) {
|
|
|
385
251
|
};
|
|
386
252
|
}
|
|
387
253
|
|
|
254
|
+
if (parsedCommand.action === "record_result") {
|
|
255
|
+
const report = await recordTopicResult(
|
|
256
|
+
projectRoot,
|
|
257
|
+
options.topicInput,
|
|
258
|
+
parsedCommand.resultKind,
|
|
259
|
+
parsedCommand.verdict,
|
|
260
|
+
parsedCommand.fromPath,
|
|
261
|
+
parsedCommand.verifiedAt,
|
|
262
|
+
);
|
|
263
|
+
return {
|
|
264
|
+
ok: report.ok,
|
|
265
|
+
action: parsedCommand.action,
|
|
266
|
+
report,
|
|
267
|
+
eventKind: "result_recorded",
|
|
268
|
+
eventSourceRef: report.ok ? report.resultRef : null,
|
|
269
|
+
summary: report.ok ? `${parsedCommand.resultKind}_result_recorded` : "runner_result_record_failed",
|
|
270
|
+
artifactRefs: report.ok ? { result_ref: report.resultRef } : {},
|
|
271
|
+
waveId: report.ok ? report.waveId : null,
|
|
272
|
+
recordedAt: parsedCommand.verifiedAt,
|
|
273
|
+
error: report.ok ? null : report.error,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
388
277
|
return {
|
|
389
278
|
ok: false,
|
|
390
279
|
action: parsedCommand.action,
|
|
@@ -466,7 +355,7 @@ export async function runTopicRunnerStep(projectRoot, options, deps = {}) {
|
|
|
466
355
|
}
|
|
467
356
|
|
|
468
357
|
const recordedAt = options.verifiedAt ?? utcNowNoMillis();
|
|
469
|
-
|
|
358
|
+
let ledger = await ensureLedger(projectRoot, options.topicInput, options.runId, recordedAt);
|
|
470
359
|
if (!ledger.ok) {
|
|
471
360
|
return ledger;
|
|
472
361
|
}
|
|
@@ -482,6 +371,19 @@ export async function runTopicRunnerStep(projectRoot, options, deps = {}) {
|
|
|
482
371
|
}
|
|
483
372
|
const decisionReport = normalizePhaseTransitionDecision(rawDecisionReport, loaded.topic);
|
|
484
373
|
|
|
374
|
+
const staleGateSync = await maybeResolveStaleHumanGate(
|
|
375
|
+
projectRoot,
|
|
376
|
+
options,
|
|
377
|
+
loaded,
|
|
378
|
+
ledger,
|
|
379
|
+
decisionReport.decision,
|
|
380
|
+
recordedAt,
|
|
381
|
+
);
|
|
382
|
+
if (!staleGateSync.ok) {
|
|
383
|
+
return staleGateSync;
|
|
384
|
+
}
|
|
385
|
+
ledger = staleGateSync.ledger;
|
|
386
|
+
|
|
485
387
|
const decisionRef = await writeDecisionArtifact(
|
|
486
388
|
projectRoot,
|
|
487
389
|
loaded,
|
|
@@ -507,6 +409,56 @@ export async function runTopicRunnerStep(projectRoot, options, deps = {}) {
|
|
|
507
409
|
}
|
|
508
410
|
|
|
509
411
|
if (decisionReport.decision.stop_class !== "continue") {
|
|
412
|
+
if (options.allowDeferredLocalBlockers === true) {
|
|
413
|
+
const deferred = await deferLocalWaveBlocker(
|
|
414
|
+
projectRoot,
|
|
415
|
+
loaded,
|
|
416
|
+
decisionReport.decision,
|
|
417
|
+
decisionRef,
|
|
418
|
+
recordedAt,
|
|
419
|
+
);
|
|
420
|
+
if (deferred.ok) {
|
|
421
|
+
const deferredEvent = await recordTopicRunEvent(projectRoot, options.topicInput, {
|
|
422
|
+
runId: options.runId,
|
|
423
|
+
eventKind: "runner_blocked",
|
|
424
|
+
stopClass: "continue",
|
|
425
|
+
recommendedAction: "admit_wave",
|
|
426
|
+
sourceRef: deferred.blockerRef,
|
|
427
|
+
summary: "deferred_local_wave_blocker",
|
|
428
|
+
recordedAt,
|
|
429
|
+
waveId: deferred.wave.wave_id,
|
|
430
|
+
artifactRefs: {
|
|
431
|
+
decision_ref: decisionRef,
|
|
432
|
+
evidence_ref: deferred.blockerRef,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
if (!deferredEvent.ok) {
|
|
436
|
+
return deferredEvent;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
ok: true,
|
|
440
|
+
topicId: decisionReport.topicId,
|
|
441
|
+
topicRef: decisionReport.topicRef,
|
|
442
|
+
runId: options.runId,
|
|
443
|
+
adapter: options.adapter,
|
|
444
|
+
runnerStatus: "continued",
|
|
445
|
+
executed: true,
|
|
446
|
+
stopClass: "continue",
|
|
447
|
+
recommendedAction: "defer_local_wave_blocker",
|
|
448
|
+
decision: decisionReport.decision,
|
|
449
|
+
gate: buildGate(decisionReport.decision),
|
|
450
|
+
decisionRef,
|
|
451
|
+
deferredBlocker: {
|
|
452
|
+
waveId: deferred.wave.wave_id,
|
|
453
|
+
reasonCode: decisionReport.decision.reason_code,
|
|
454
|
+
blockerRef: deferred.blockerRef,
|
|
455
|
+
nextWaveId: deferred.nextWave.wave_id,
|
|
456
|
+
},
|
|
457
|
+
ledgerRef: deferredEvent.ledgerRef,
|
|
458
|
+
eventCount: deferredEvent.eventCount,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
510
462
|
return {
|
|
511
463
|
ok: true,
|
|
512
464
|
topicId: decisionReport.topicId,
|
|
@@ -549,7 +501,7 @@ export async function runTopicRunnerStep(projectRoot, options, deps = {}) {
|
|
|
549
501
|
}, parsedCommand.error);
|
|
550
502
|
}
|
|
551
503
|
|
|
552
|
-
if (["admit_wave", "freeze_packet", "closeout_wave"].includes(parsedCommand.action)) {
|
|
504
|
+
if (["admit_wave", "freeze_packet", "closeout_wave", "record_result"].includes(parsedCommand.action)) {
|
|
553
505
|
const commandExecution = await executeMechanicalCommand(projectRoot, { ...options, recordedAt }, parsedCommand);
|
|
554
506
|
if (!commandExecution.ok) {
|
|
555
507
|
const blockedEvent = await recordRunnerBlocked(projectRoot, { ...options, verifiedAt: recordedAt }, {
|
|
@@ -595,7 +547,7 @@ export async function runTopicRunnerStep(projectRoot, options, deps = {}) {
|
|
|
595
547
|
recommendedAction: parsedCommand.action,
|
|
596
548
|
sourceRef: commandExecution.eventSourceRef,
|
|
597
549
|
summary: commandExecution.summary,
|
|
598
|
-
recordedAt,
|
|
550
|
+
recordedAt: commandExecution.recordedAt ?? recordedAt,
|
|
599
551
|
waveId: commandExecution.waveId,
|
|
600
552
|
artifactRefs,
|
|
601
553
|
});
|
|
@@ -728,7 +680,10 @@ export async function runTopicRunner(projectRoot, options, deps = {}) {
|
|
|
728
680
|
: 20;
|
|
729
681
|
const steps = [];
|
|
730
682
|
for (let index = 0; index < maxSteps; index += 1) {
|
|
731
|
-
const step = await runTopicRunnerStep(projectRoot,
|
|
683
|
+
const step = await runTopicRunnerStep(projectRoot, {
|
|
684
|
+
...options,
|
|
685
|
+
allowDeferredLocalBlockers: true,
|
|
686
|
+
}, deps);
|
|
732
687
|
steps.push(step);
|
|
733
688
|
if (!step.ok || step.runnerStatus !== "continued") {
|
|
734
689
|
return {
|