cairn-ai 0.2.2 → 0.2.3
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/.agents/workflows/cairn-work.md +1 -1
- package/.claude/commands/cairn-work.md +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.de.md +10 -10
- package/README.es.md +10 -10
- package/README.fr.md +10 -10
- package/README.ja.md +10 -10
- package/README.ko.md +10 -10
- package/README.md +10 -10
- package/README.pt.md +10 -10
- package/README.zh.md +10 -10
- package/commands/cairn-doctor.md +5 -3
- package/commands/cairn-install.md +5 -4
- package/commands/cairn-uninstall.md +4 -2
- package/commands/cairn-upgrade.md +6 -6
- package/commands/cairn-work.md +2 -2
- package/docs/model-guidance/README.md +1 -1
- package/docs/model-guidance/codex.md +1 -1
- package/package.json +2 -2
- package/scripts/cairn-goal.mjs +149 -45
- package/scripts/cairn-lifecycle.mjs +926 -313
- package/scripts/cairn-safe-fs.mjs +246 -0
- package/scripts/cairn-state.mjs +24 -11
- package/scripts/cairn-toolcheck.mjs +116 -34
- package/scripts/release-integrity-0.2.2.json +68 -0
- package/skills/cairn-work/SKILL.md +2 -0
- package/templates/work-plan.md +2 -0
|
@@ -8,13 +8,13 @@ Run from the published or global package, not from the installed cache copy:
|
|
|
8
8
|
bunx cairn-ai@latest upgrade
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The current custom lifecycle remains in use. The cached plugin root is a replacement target and must not upgrade itself.
|
|
11
|
+
The current custom marketplace lifecycle remains in use. The cached plugin root is a replacement target and must not upgrade itself.
|
|
12
12
|
|
|
13
13
|
Actions:
|
|
14
14
|
|
|
15
|
-
- Replace the
|
|
16
|
-
- Recompute hook trust state
|
|
17
|
-
-
|
|
18
|
-
- Overwrite Antigravity IDE/CLI skills and workflows mirror files with the latest content.
|
|
15
|
+
- Replace only files tracked by the ownership manifest whose current digest still matches the installed digest.
|
|
16
|
+
- Recompute Cairn-owned hook trust state without rewriting public feature or agent settings.
|
|
17
|
+
- Replace owned Claude Code and Antigravity mirror files with the latest content.
|
|
19
18
|
- Refresh runtime locators for every installed surface.
|
|
20
|
-
-
|
|
19
|
+
- Stage and validate all candidates before commit; rollback committed phases in reverse order on failure.
|
|
20
|
+
- Preserve and report a conflict for modified, unmanaged, or invalid legacy artifacts.
|
package/commands/cairn-work.md
CHANGED
|
@@ -19,9 +19,9 @@ Procedure:
|
|
|
19
19
|
9. Give implementation the test contract, failing evidence, exact file ownership, and constraints. Require minimal implementation and no repeated broad discovery. Preserve status, final-report, close-and-review, recursive bounded delegation, and main-agent fallback rules.
|
|
20
20
|
10. Treat tool exit codes and bounded summaries as authoritative. Keep success output concise; expand only the failing test and related code context.
|
|
21
21
|
11. For Heavy Path, run the recorded automated test command and record explicit `Tests:` evidence.
|
|
22
|
-
12. Run focused module acceptance, then one final surface integration check through `goal verify --
|
|
22
|
+
12. Run focused module acceptance, then one final surface integration check through `goal verify -- ...`. Verification defaults to 600,000 ms (10 minutes), may be shortened with `--timeout-ms`, and is capped at 3,600,000 ms (1 hour). The command runs without holding the state lock; evidence is recorded only if the starting goal/task identity and the pre/post watched fingerprint still match.
|
|
23
23
|
13. Before package verification, inspect package lifecycle scripts. Run normal `npm pack --dry-run` by default. Content-producing or unknown lifecycle scripts must never use `--ignore-scripts`; only absent or proven content-neutral scripts may use it when the prior full check remains fresh.
|
|
24
|
-
14. Record evidence bound to goal/task/plan. Missing, failed, skipped, stale, or placeholder evidence never completes work; relevant mutation invalidates affected evidence.
|
|
24
|
+
14. Record evidence at the semantic boundary that proves the claimed behavior, bound to goal/task/plan. Missing, failed, skipped, stale, or placeholder evidence never completes work; relevant mutation invalidates affected evidence.
|
|
25
25
|
15. Complete the task and continue the goal. Use `--quiet` on successful state mutations; workers report only their assigned task and never select the next one.
|
|
26
26
|
16. Update the plan, goal state, and `PLAN.md` only after evidence exists.
|
|
27
27
|
17. Answer side questions briefly and resume unless explicitly asked to pause, stop, or switch.
|
|
@@ -29,7 +29,7 @@ The goal is not to make prompts long by model. The goal is to stabilize the same
|
|
|
29
29
|
- Treat missing, failed, skipped, stale, or placeholder evidence as failure. Task evidence records must be bound to the current goal, task, and plan.
|
|
30
30
|
- Treat the repository Cairn state as the fail-closed transition authority. Advance the matching Codex UI plan step only after the evidence-gated repository task transition succeeds.
|
|
31
31
|
- Spend reasoning on a focused executable test contract before implementation: requirements, invariants, boundaries, and failure modes. Give implementation only that contract, failing evidence, file scope, and constraints; require the minimum passing change.
|
|
32
|
-
- Run verification through `goal verify -- ...`;
|
|
32
|
+
- Run verification through `goal verify -- ...`; its default timeout is 600,000 ms and its maximum is 3,600,000 ms. Treat tool exit codes and bounded machine summaries as authoritative. Record evidence only when the starting goal/task identity and pre/post watched fingerprint still match, and choose the watch set and command at the semantic boundary that proves the claim. Compress success output and expand context only for failing tests and related code.
|
|
33
33
|
- Use a verification ladder: focused contract tests, one full check after the final change, then package dry-run while the full check remains fresh. Inspect package lifecycle scripts first and run normal `npm pack --dry-run` by default. Content-producing or unknown lifecycle scripts must never use `--ignore-scripts`; only absent or proven content-neutral scripts may use it.
|
|
34
34
|
- Every implementation task must pass module acceptance verification and surface integration verification.
|
|
35
35
|
- Before mutating external state, run the closest available dry-run, check, plan, diff, validate, or equivalent command.
|
|
@@ -42,7 +42,7 @@ Codex-family models are strong at small implementation tasks, explicit tool use,
|
|
|
42
42
|
### Implementation
|
|
43
43
|
|
|
44
44
|
- Invest Codex reasoning in executable test design first, then hand the bounded failing contract to implementation and require the minimum passing patch.
|
|
45
|
-
- Run verification through `goal verify -- ...`. On success, retain exact argv, tool exit code, pass count, output digest, and watched-workspace fingerprint. Expand diagnostics only for the failing test and its related file/symbol scope; reject stale evidence after related changes.
|
|
45
|
+
- Run verification through `goal verify -- ...`. Its default timeout is 600,000 ms (10 minutes), capped at 3,600,000 ms (1 hour). On success, retain exact argv, tool exit code, pass count, output digest, and watched-workspace fingerprint; commit evidence only when the starting goal/task identity and pre/post watched fingerprint match. Choose the watch set and command at the semantic boundary that proves the claim. Expand diagnostics only for the failing test and its related file/symbol scope; reject stale evidence after related changes.
|
|
46
46
|
- If acting as the user-called/main agent, orchestrate the work and delegate actual implementation edits to `worker` whenever subagent tools are available, regardless of Light Path or Heavy Path.
|
|
47
47
|
- If acting as the user-called/main agent, immediately relay received subagent status events to the user. If no mid-run reporting channel exists, relay observable events such as assignment, waiting, and final completion.
|
|
48
48
|
- If acting as the user-called/main agent, close or release completed subagents after capturing their final report and evidence, then review the final report and evidence before marking the work complete.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cairn-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Token-efficient agent harness with tool readiness for Codex, Claude Code, and Antigravity",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"README.*.md"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
|
-
"check": "node --check scripts/cairn.mjs && node --check scripts/cairn-paths.mjs && node --check scripts/cairn-goal.mjs && node --check scripts/cairn-state.mjs && node --check scripts/cairn-lifecycle.mjs && node --check scripts/cairn-toolcheck.mjs && node -e \"JSON.parse(require('node:fs').readFileSync('package.json','utf8')); JSON.parse(require('node:fs').readFileSync('.codex-plugin/plugin.json','utf8')); JSON.parse(require('node:fs').readFileSync('hooks/hooks.json','utf8'))\" && npm test",
|
|
42
|
+
"check": "node --check scripts/cairn.mjs && node --check scripts/cairn-paths.mjs && node --check scripts/cairn-safe-fs.mjs && node --check scripts/cairn-goal.mjs && node --check scripts/cairn-state.mjs && node --check scripts/cairn-lifecycle.mjs && node --check scripts/cairn-toolcheck.mjs && node -e \"JSON.parse(require('node:fs').readFileSync('package.json','utf8')); JSON.parse(require('node:fs').readFileSync('.codex-plugin/plugin.json','utf8')); JSON.parse(require('node:fs').readFileSync('hooks/hooks.json','utf8')); JSON.parse(require('node:fs').readFileSync('scripts/release-integrity-0.2.2.json','utf8'))\" && npm test",
|
|
43
43
|
"test": "node --test",
|
|
44
44
|
"prepack": "npm run check"
|
|
45
45
|
},
|
package/scripts/cairn-goal.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { lstatSync, readFileSync, readlinkSync, realpathSync, readdirSync } from "node:fs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { readFile, rename, rm } from "node:fs/promises";
|
|
6
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { assertNoSymlinkComponents, safeMkdir, safeWriteFile, withStateLock } from "./cairn-safe-fs.mjs";
|
|
8
9
|
|
|
9
10
|
export const GOAL_STATE_VERSION = 2;
|
|
10
11
|
export const GOAL_STATUSES = new Set(["active", "paused", "blocked", "cancelled", "completed"]);
|
|
@@ -34,7 +35,9 @@ export function goalStatePath(root = process.cwd()) {
|
|
|
34
35
|
|
|
35
36
|
export async function readGoalState({ root = process.cwd() } = {}) {
|
|
36
37
|
try {
|
|
37
|
-
const
|
|
38
|
+
const path = goalStatePath(root);
|
|
39
|
+
await assertNoSymlinkComponents(root, path);
|
|
40
|
+
const text = await readFile(path, "utf8");
|
|
38
41
|
return validateState(migrateState(JSON.parse(text)));
|
|
39
42
|
} catch (error) {
|
|
40
43
|
if (error?.code === "ENOENT") return null;
|
|
@@ -48,44 +51,51 @@ export async function startGoal({ root = process.cwd(), goal, planId, tasks, com
|
|
|
48
51
|
const normalizedPlanId = requiredText(planId, "planId");
|
|
49
52
|
if (!Array.isArray(tasks) || tasks.length === 0) throw new Error("tasks must contain at least one task");
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
return withStateLock(root, async () => {
|
|
55
|
+
const existing = await readGoalState({ root });
|
|
56
|
+
if (existing && !terminalGoalStatuses.has(existing.goal.status)) {
|
|
57
|
+
throw new Error(`An active Cairn goal already exists (${existing.goal.id}); pause, block, cancel, or complete it first`);
|
|
58
|
+
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
60
|
+
const now = timestamp();
|
|
61
|
+
const goalId = `goal-${randomUUID()}`;
|
|
62
|
+
const normalizedTasks = tasks.map((task, index) => normalizeTask(task, index, index === 0 ? "active" : "pending"));
|
|
63
|
+
const state = {
|
|
64
|
+
schemaVersion: GOAL_STATE_VERSION,
|
|
65
|
+
revision: 1,
|
|
66
|
+
goal: {
|
|
67
|
+
id: goalId,
|
|
68
|
+
title: normalizedGoal,
|
|
69
|
+
planId: normalizedPlanId,
|
|
70
|
+
status: "active",
|
|
71
|
+
completionCriteria: normalizeCriteria(completionCriteria),
|
|
72
|
+
requiredEvidence: normalizeCriteria(requiredEvidence),
|
|
73
|
+
evidencePolicy: validEvidencePolicy(evidencePolicy),
|
|
74
|
+
ownerSessionId: optionalText(ownerSessionId, "ownerSessionId"),
|
|
75
|
+
blocker: null,
|
|
76
|
+
createdAt: now,
|
|
77
|
+
updatedAt: now,
|
|
78
|
+
},
|
|
79
|
+
tasks: normalizedTasks,
|
|
80
|
+
receipts: [],
|
|
81
|
+
};
|
|
82
|
+
await writeGoalStateUnlocked({ root, state });
|
|
83
|
+
return state;
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
export async function writeGoalState({ root = process.cwd(), state } = {}) {
|
|
88
|
+
return withStateLock(root, () => writeGoalStateUnlocked({ root, state }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function writeGoalStateUnlocked({ root = process.cwd(), state } = {}) {
|
|
83
92
|
const validated = validateState(state);
|
|
84
93
|
const path = goalStatePath(root);
|
|
85
|
-
await
|
|
94
|
+
await safeMkdir(root, ".cairn");
|
|
95
|
+
await assertNoSymlinkComponents(root, path);
|
|
86
96
|
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
87
97
|
try {
|
|
88
|
-
await
|
|
98
|
+
await safeWriteFile(root, temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
|
|
89
99
|
await rename(temporaryPath, path);
|
|
90
100
|
} catch (error) {
|
|
91
101
|
await rm(temporaryPath, { force: true }).catch(() => {});
|
|
@@ -175,6 +185,8 @@ async function recordEvidence({
|
|
|
175
185
|
summary,
|
|
176
186
|
workspaceFingerprint: fingerprint,
|
|
177
187
|
watchPaths = [],
|
|
188
|
+
expectedIdentity,
|
|
189
|
+
expectedFingerprint,
|
|
178
190
|
} = {}) {
|
|
179
191
|
const normalizedScope = validReceiptScope(scope);
|
|
180
192
|
const normalizedTaskId = normalizedScope === "task" ? requiredText(taskId, "taskId") : undefined;
|
|
@@ -189,6 +201,16 @@ async function recordEvidence({
|
|
|
189
201
|
return mutateGoal(root, (state) => {
|
|
190
202
|
assertGoalMutable(state);
|
|
191
203
|
const task = normalizedScope === "task" ? taskById(state, normalizedTaskId) : null;
|
|
204
|
+
if (expectedIdentity && (
|
|
205
|
+
state.goal.id !== expectedIdentity.goalId
|
|
206
|
+
|| state.goal.planId !== expectedIdentity.planId
|
|
207
|
+
)) throw new Error("Goal identity changed during verification; evidence was not recorded");
|
|
208
|
+
if (expectedIdentity && normalizedScope === "task" && (
|
|
209
|
+
task?.id !== expectedIdentity.taskId || task?.status !== expectedIdentity.taskStatus
|
|
210
|
+
)) throw new Error("Verification task changed during verification; evidence was not recorded");
|
|
211
|
+
if (expectedFingerprint && workspaceFingerprint(root, watchPaths) !== expectedFingerprint) {
|
|
212
|
+
throw new Error("Watched workspace changed during verification; evidence was not recorded");
|
|
213
|
+
}
|
|
192
214
|
if (goalId !== undefined && goalId !== state.goal.id) throw new Error("evidence goalId does not match the active goal");
|
|
193
215
|
if (planId !== undefined && planId !== state.goal.planId) throw new Error("evidence planId does not match the active goal");
|
|
194
216
|
const receipt = {
|
|
@@ -219,6 +241,7 @@ export async function verifyAndRecord({
|
|
|
219
241
|
argv,
|
|
220
242
|
watchPaths = [],
|
|
221
243
|
runner = spawnSync,
|
|
244
|
+
timeoutMs = 600_000,
|
|
222
245
|
} = {}) {
|
|
223
246
|
const normalizedArgv = validArgv(argv);
|
|
224
247
|
const normalizedWatchPaths = normalizeWatchPaths(root, watchPaths);
|
|
@@ -228,13 +251,23 @@ export async function verifyAndRecord({
|
|
|
228
251
|
if (!initialState) throw new Error("No Cairn goal state exists; start a goal first");
|
|
229
252
|
assertGoalMutable(initialState);
|
|
230
253
|
const normalizedTaskId = normalizedScope === "task" ? requiredText(taskId, "taskId") : undefined;
|
|
231
|
-
|
|
254
|
+
const initialTask = normalizedScope === "task" ? taskById(initialState, normalizedTaskId) : null;
|
|
255
|
+
if (initialTask && initialTask.status !== "active") throw new Error(`Verification task must be active: ${initialTask.id}`);
|
|
256
|
+
const identity = {
|
|
257
|
+
goalId: initialState.goal.id,
|
|
258
|
+
planId: initialState.goal.planId,
|
|
259
|
+
taskId: normalizedTaskId,
|
|
260
|
+
taskStatus: initialTask?.status,
|
|
261
|
+
};
|
|
262
|
+
const initialFingerprint = workspaceFingerprint(root, normalizedWatchPaths);
|
|
232
263
|
const result = runner(normalizedArgv[0], normalizedArgv.slice(1), {
|
|
233
264
|
cwd: resolve(root),
|
|
234
265
|
encoding: "utf8",
|
|
235
266
|
maxBuffer: 16 * 1024 * 1024,
|
|
236
267
|
shell: false,
|
|
268
|
+
timeout: validTimeout(timeoutMs),
|
|
237
269
|
});
|
|
270
|
+
if (result.error?.code === "ETIMEDOUT") throw new Error(`Verification timed out after ${timeoutMs}ms`);
|
|
238
271
|
if (result.error) throw new Error(`Verification could not start: ${result.error.message}`);
|
|
239
272
|
const exitCode = Number.isInteger(result.status) ? result.status : 1;
|
|
240
273
|
const output = combinedOutput(result.stdout, result.stderr);
|
|
@@ -245,6 +278,14 @@ export async function verifyAndRecord({
|
|
|
245
278
|
|
|
246
279
|
const outputDigest = sha256(output);
|
|
247
280
|
const fingerprint = workspaceFingerprint(root, normalizedWatchPaths);
|
|
281
|
+
if (fingerprint !== initialFingerprint) throw new Error("Watched workspace changed during verification; evidence was not recorded");
|
|
282
|
+
const postState = await readGoalState({ root });
|
|
283
|
+
if (!postState || postState.goal.id !== identity.goalId || postState.goal.planId !== identity.planId) {
|
|
284
|
+
throw new Error("Goal identity changed during verification; evidence was not recorded");
|
|
285
|
+
}
|
|
286
|
+
if (normalizedScope === "task" && taskById(postState, normalizedTaskId).status !== identity.taskStatus) {
|
|
287
|
+
throw new Error("Verification task is no longer active; evidence was not recorded");
|
|
288
|
+
}
|
|
248
289
|
const command = normalizedArgv.map((value) => JSON.stringify(value)).join(" ");
|
|
249
290
|
const state = await recordEvidence({
|
|
250
291
|
root,
|
|
@@ -259,6 +300,10 @@ export async function verifyAndRecord({
|
|
|
259
300
|
summary: successSummary(output),
|
|
260
301
|
workspaceFingerprint: fingerprint,
|
|
261
302
|
watchPaths: normalizedWatchPaths,
|
|
303
|
+
goalId: identity.goalId,
|
|
304
|
+
planId: identity.planId,
|
|
305
|
+
expectedIdentity: identity,
|
|
306
|
+
expectedFingerprint: fingerprint,
|
|
262
307
|
});
|
|
263
308
|
return { state, evidence: state.receipts.at(-1) };
|
|
264
309
|
}
|
|
@@ -312,7 +357,7 @@ export function evaluateStop(state, { subagent = false, agentId } = {}) {
|
|
|
312
357
|
export async function handleGoalCli(args = process.argv.slice(2), { stdout = console.log } = {}) {
|
|
313
358
|
const [firstCommand = "help", ...remaining] = args;
|
|
314
359
|
const [command = "help", ...rest] = firstCommand === "goal" ? remaining : [firstCommand, ...remaining];
|
|
315
|
-
const { options, positional, passthrough } = parseArgs(rest);
|
|
360
|
+
const { options, positional, passthrough } = parseArgs(rest, command);
|
|
316
361
|
const root = options.root ?? process.cwd();
|
|
317
362
|
const emit = options.quiet ? () => {} : stdout;
|
|
318
363
|
if (command === "start") {
|
|
@@ -360,7 +405,7 @@ export async function handleGoalCli(args = process.argv.slice(2), { stdout = con
|
|
|
360
405
|
command: options.command,
|
|
361
406
|
exitCode: numberOption(options.exitCode ?? options["exit-code"] ?? options.exit),
|
|
362
407
|
timestamp: options.timestamp,
|
|
363
|
-
goalId: options.goalId,
|
|
408
|
+
goalId: options.goalId ?? options["goal-id"],
|
|
364
409
|
planId: options.plan,
|
|
365
410
|
});
|
|
366
411
|
emit(JSON.stringify(state));
|
|
@@ -374,6 +419,7 @@ export async function handleGoalCli(args = process.argv.slice(2), { stdout = con
|
|
|
374
419
|
scope: options.scope,
|
|
375
420
|
watchPaths: splitValues(options.watch),
|
|
376
421
|
argv: passthrough,
|
|
422
|
+
timeoutMs: options["timeout-ms"] === undefined ? 600_000 : positiveIntegerOption(options["timeout-ms"], "timeout-ms"),
|
|
377
423
|
});
|
|
378
424
|
emit(JSON.stringify({
|
|
379
425
|
id: result.evidence.id,
|
|
@@ -398,12 +444,14 @@ export async function handleGoalCli(args = process.argv.slice(2), { stdout = con
|
|
|
398
444
|
}
|
|
399
445
|
|
|
400
446
|
async function mutateGoal(root, mutate) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
447
|
+
return withStateLock(root, async () => {
|
|
448
|
+
const state = await readGoalState({ root });
|
|
449
|
+
if (!state) throw new Error("No Cairn goal state exists; start a goal first");
|
|
450
|
+
const next = await mutate(structuredClone(state));
|
|
451
|
+
next.revision += 1;
|
|
452
|
+
await writeGoalStateUnlocked({ root, state: next });
|
|
453
|
+
return next;
|
|
454
|
+
});
|
|
407
455
|
}
|
|
408
456
|
|
|
409
457
|
function normalizeTask(task, index, defaultStatus) {
|
|
@@ -786,11 +834,13 @@ function timestamp() {
|
|
|
786
834
|
return new Date().toISOString();
|
|
787
835
|
}
|
|
788
836
|
|
|
789
|
-
function parseArgs(args) {
|
|
837
|
+
function parseArgs(args, command) {
|
|
790
838
|
const options = {};
|
|
791
839
|
const positional = [];
|
|
792
840
|
const passthrough = [];
|
|
793
841
|
const booleanOptions = new Set(["quiet"]);
|
|
842
|
+
const allowed = allowedOptions(command);
|
|
843
|
+
const seenCanonicalOptions = new Set();
|
|
794
844
|
for (let index = 0; index < args.length; index += 1) {
|
|
795
845
|
const value = args[index];
|
|
796
846
|
if (value === "--") {
|
|
@@ -805,13 +855,54 @@ function parseArgs(args) {
|
|
|
805
855
|
const equalsIndex = option.indexOf("=");
|
|
806
856
|
const key = equalsIndex === -1 ? option : option.slice(0, equalsIndex);
|
|
807
857
|
const inline = equalsIndex === -1 ? undefined : option.slice(equalsIndex + 1);
|
|
808
|
-
if (
|
|
858
|
+
if (!allowed.has(key)) throw new Error(`Unknown option for ${command}: --${key}`);
|
|
859
|
+
const canonicalKey = canonicalOptionKey(key);
|
|
860
|
+
if (seenCanonicalOptions.has(canonicalKey)) throw new Error(`--${canonicalKey} may only be provided once`);
|
|
861
|
+
seenCanonicalOptions.add(canonicalKey);
|
|
862
|
+
if (inline !== undefined) {
|
|
863
|
+
if (!booleanOptions.has(key) && inline.length === 0) throw new Error(`--${key} requires a value`);
|
|
864
|
+
if (booleanOptions.has(key) && !["true", "false"].includes(inline)) throw new Error(`--${key} must be true or false`);
|
|
865
|
+
options[key] = booleanOptions.has(key) ? inline !== "false" : inline;
|
|
866
|
+
}
|
|
809
867
|
else if (booleanOptions.has(key)) options[key] = true;
|
|
810
|
-
else
|
|
868
|
+
else {
|
|
869
|
+
const next = args[index + 1];
|
|
870
|
+
if (next === undefined || next === "--" || next.startsWith("--")) throw new Error(`--${key} requires a value`);
|
|
871
|
+
options[key] = next;
|
|
872
|
+
index += 1;
|
|
873
|
+
}
|
|
811
874
|
}
|
|
812
875
|
return { options, positional, passthrough };
|
|
813
876
|
}
|
|
814
877
|
|
|
878
|
+
function canonicalOptionKey(key) {
|
|
879
|
+
return {
|
|
880
|
+
exitCode: "exit-code",
|
|
881
|
+
exit: "exit-code",
|
|
882
|
+
goalId: "goal-id",
|
|
883
|
+
evidencePolicy: "evidence-policy",
|
|
884
|
+
}[key] ?? key;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function allowedOptions(command) {
|
|
888
|
+
const common = ["root", "quiet"];
|
|
889
|
+
const byCommand = {
|
|
890
|
+
start: ["goal", "plan", "tasks", "criteria", "requiredEvidence", "session", "evidencePolicy", "evidence-policy"],
|
|
891
|
+
status: [],
|
|
892
|
+
task: ["task", "status", "reason"],
|
|
893
|
+
assign: ["task", "agent"],
|
|
894
|
+
receipt: ["task", "kind", "scope", "command", "exitCode", "exit-code", "exit", "timestamp", "goalId", "goal-id", "plan"],
|
|
895
|
+
verify: ["task", "kind", "scope", "watch", "timeout-ms"],
|
|
896
|
+
pause: [],
|
|
897
|
+
resume: [],
|
|
898
|
+
block: ["reason"],
|
|
899
|
+
cancel: [],
|
|
900
|
+
complete: [],
|
|
901
|
+
help: [],
|
|
902
|
+
};
|
|
903
|
+
return new Set([...common, ...(byCommand[command] ?? [])]);
|
|
904
|
+
}
|
|
905
|
+
|
|
815
906
|
function parseTasks(value) {
|
|
816
907
|
if (Array.isArray(value)) return value;
|
|
817
908
|
if (typeof value !== "string" || value.trim().length === 0) throw new Error("Provide tasks with --tasks JSON or positional task titles");
|
|
@@ -837,6 +928,19 @@ function numberOption(value) {
|
|
|
837
928
|
return result;
|
|
838
929
|
}
|
|
839
930
|
|
|
931
|
+
function positiveIntegerOption(value, label) {
|
|
932
|
+
const result = Number(value);
|
|
933
|
+
if (!Number.isSafeInteger(result) || result <= 0 || result > 3_600_000) {
|
|
934
|
+
throw new Error(`${label} must be an integer between 1 and 3600000`);
|
|
935
|
+
}
|
|
936
|
+
return result;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function validTimeout(value) {
|
|
940
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error("timeoutMs must be a positive integer");
|
|
941
|
+
return value;
|
|
942
|
+
}
|
|
943
|
+
|
|
840
944
|
function isCliEntry() {
|
|
841
945
|
if (!process.argv[1]) return false;
|
|
842
946
|
try {
|