@sabaiway/agent-workflow-kit 13.1.0 → 13.3.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/CHANGELOG.md +140 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/review-state.md +1 -1
- package/references/modes/upgrade.md +4 -4
- package/references/templates/vehicles.json +8 -0
- package/tools/checkpoint-core.mjs +118 -0
- package/tools/checkpoint-restore.mjs +126 -0
- package/tools/checkpoint.mjs +146 -0
- package/tools/ensure-configs.mjs +8 -7
- package/tools/ensure-ops.mjs +15 -7
- package/tools/ensure-vocabulary.mjs +1 -1
- package/tools/plan-files.mjs +2 -1
- package/tools/review-state.mjs +1 -1
- package/tools/task-brief.mjs +270 -0
- package/tools/vehicle-settings.mjs +127 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// spec:checkpoint — docs/ai/specs/kit/checkpoint/index.md
|
|
2
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { resolveTreeObject } from './dispatch-baseline.mjs';
|
|
5
|
+
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
6
|
+
import { PLANS_REL, isScratchPlanName } from './plan-files.mjs';
|
|
7
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
8
|
+
import { CHECKPOINT_REF_PREFIX, refuse, withRepository, readGit, readCheckpointRefs, writeSnapshot,
|
|
9
|
+
findOpenCheckpointThread } from './checkpoint-core.mjs';
|
|
10
|
+
import { restoreCheckpoint } from './checkpoint-restore.mjs';
|
|
11
|
+
|
|
12
|
+
export { CHECKPOINT_REF_PREFIX } from './checkpoint-core.mjs';
|
|
13
|
+
const ACCEPT = 0;
|
|
14
|
+
const REFUSE = 1;
|
|
15
|
+
const USAGE = 2;
|
|
16
|
+
const FIRST_NUMBER = 0;
|
|
17
|
+
const INCREMENT = 1;
|
|
18
|
+
const LAST = -1;
|
|
19
|
+
const EMPTY = '';
|
|
20
|
+
const NEWLINE = '\n';
|
|
21
|
+
const SLASH = '/';
|
|
22
|
+
const STRING = 'string';
|
|
23
|
+
const MARKDOWN = '.md';
|
|
24
|
+
const NUMBER_PATTERN = /^(0|[1-9][0-9]*)$/;
|
|
25
|
+
const VERBS = { mint: 'mint', newest: 'newest', verify: 'verify', prune: 'prune', restore: 'restore' };
|
|
26
|
+
const NAMES = { stem: 'stem', sequence: 'sequence', ledger: 'ledger', open: 'open-thread', empty: 'no-checkpoint' };
|
|
27
|
+
const CHECKPOINT_KIND = 'checkpoint';
|
|
28
|
+
const LEDGER_ABSENT = 'absent';
|
|
29
|
+
const LEDGER_OK = 'ok';
|
|
30
|
+
const CLEAN = 'CLEAN';
|
|
31
|
+
const DIRTY = 'DIRTY';
|
|
32
|
+
const PROOF = 'proof';
|
|
33
|
+
const PLAN_FLAG = '--plan';
|
|
34
|
+
const PLAN_ARG_COUNT = 3;
|
|
35
|
+
const OID_ARG_COUNT = 2;
|
|
36
|
+
const ARGV_OFFSET = 2;
|
|
37
|
+
const USAGE_TEXT = 'usage: checkpoint mint|newest|prune --plan <plan> | verify|restore <oid>';
|
|
38
|
+
const PLAN_ERROR = 'expected a readable regular plan directly under docs/plans';
|
|
39
|
+
const SEQUENCE_ERROR = 'expected contiguous numbered refs on trees';
|
|
40
|
+
const GIT_ARGS = {
|
|
41
|
+
checkRef: ['check-ref-format'],
|
|
42
|
+
resolve: ['rev-parse', '--verify'],
|
|
43
|
+
update: ['update-ref', '--no-deref'],
|
|
44
|
+
remove: ['update-ref', '--no-deref', '-d'],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const readPlan = (context, planPath) => {
|
|
48
|
+
try {
|
|
49
|
+
if (typeof planPath !== STRING || isAbsolute(planPath)) return refuse(`${NAMES.stem}: ${PLAN_ERROR}`);
|
|
50
|
+
const path = resolve(context.cwd, planPath);
|
|
51
|
+
const parent = relative(context.top, dirname(path)).split(sep).join(SLASH);
|
|
52
|
+
const name = basename(path);
|
|
53
|
+
if (parent !== PLANS_REL || !name.endsWith(MARKDOWN) || isScratchPlanName(name)
|
|
54
|
+
|| !(context.io.lstat ?? lstatSync)(path).isFile()) return refuse(`${NAMES.stem}: ${PLAN_ERROR}`);
|
|
55
|
+
readFileSync(path);
|
|
56
|
+
const stem = name.slice(FIRST_NUMBER, -MARKDOWN.length);
|
|
57
|
+
readGit(context, [...GIT_ARGS.checkRef, `${CHECKPOINT_REF_PREFIX}${stem}/${FIRST_NUMBER}`]);
|
|
58
|
+
return { ok: true, stem };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return refuse(`${NAMES.stem}: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const readSequence = (context, stem) => {
|
|
64
|
+
const prefix = `${CHECKPOINT_REF_PREFIX}${stem}${SLASH}`;
|
|
65
|
+
const refs = readCheckpointRefs(context, prefix).map(({ ref, oid }) => {
|
|
66
|
+
const suffix = ref.slice(prefix.length);
|
|
67
|
+
if (!NUMBER_PATTERN.test(suffix)) {
|
|
68
|
+
throw new Error(`${NAMES.sequence}: ${SEQUENCE_ERROR}`);
|
|
69
|
+
}
|
|
70
|
+
return { ref, oid, n: Number(suffix), stem };
|
|
71
|
+
}).sort((left, right) => left.n - right.n);
|
|
72
|
+
for (const [index, checkpoint] of refs.entries()) {
|
|
73
|
+
if (!Number.isSafeInteger(checkpoint.n) || checkpoint.n !== index) throw new Error(`${NAMES.sequence}: ${SEQUENCE_ERROR}`);
|
|
74
|
+
const tree = resolveTreeObject(context.top, checkpoint.oid, context.io);
|
|
75
|
+
if (!tree.ok) throw new Error(tree.reason);
|
|
76
|
+
}
|
|
77
|
+
return refs;
|
|
78
|
+
};
|
|
79
|
+
const withSequence = (cwd, planPath, io, action) => withRepository(cwd, io, (context) => {
|
|
80
|
+
const plan = readPlan(context, planPath);
|
|
81
|
+
if (!plan.ok) return plan;
|
|
82
|
+
return action(context, plan.stem, readSequence(context, plan.stem));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const mintCheckpoint = (cwd, planPath, io = {}) => withSequence(cwd, planPath, io, (context, stem, refs) => {
|
|
86
|
+
const newest = refs.at(LAST);
|
|
87
|
+
const snapshot = writeSnapshot(context, newest?.oid ?? null);
|
|
88
|
+
if (!snapshot.ok) return snapshot;
|
|
89
|
+
if (snapshot.oid === newest?.oid) return { ok: true, ...newest };
|
|
90
|
+
const n = newest === undefined ? FIRST_NUMBER : newest.n + INCREMENT;
|
|
91
|
+
const ref = `${CHECKPOINT_REF_PREFIX}${stem}/${n}`;
|
|
92
|
+
readGit(context, [...GIT_ARGS.update, ref, snapshot.oid]);
|
|
93
|
+
return { ok: true, stem, n, ref, oid: snapshot.oid };
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const newestCheckpoint = (cwd, planPath, io = {}) => withSequence(cwd, planPath, io, (context, stem, refs) => {
|
|
97
|
+
const newest = refs.at(LAST);
|
|
98
|
+
return newest === undefined ? refuse(`${NAMES.empty}: ${stem}`) : { ok: true, ...newest };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const verifyCheckpoint = (cwd, oid, io = {}) => withRepository(cwd, io, (context) => {
|
|
102
|
+
const tree = resolveTreeObject(context.top, oid, context.io);
|
|
103
|
+
if (!tree.ok) return refuse(tree.reason);
|
|
104
|
+
const target = readGit(context, [...GIT_ARGS.resolve, oid]);
|
|
105
|
+
const snapshot = writeSnapshot(context, target);
|
|
106
|
+
if (!snapshot.ok) return snapshot;
|
|
107
|
+
return { ok: true, clean: target === snapshot.oid, target, oid: snapshot.oid };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const pruneCheckpoints = (cwd, planPath, io = {}) => withSequence(cwd, planPath, io, (context, stem, refs) => {
|
|
111
|
+
const ledger = readDelegationLedger(context.cwd, context.env);
|
|
112
|
+
if (ledger.state !== LEDGER_ABSENT && ledger.state !== LEDGER_OK) return refuse(`${NAMES.ledger}: ${ledger.reason}`);
|
|
113
|
+
const records = ledger.records ?? [];
|
|
114
|
+
const held = new Set(refs.map(({ oid }) => oid));
|
|
115
|
+
const open = findOpenCheckpointThread(records, held);
|
|
116
|
+
if (open !== undefined) return refuse(`${NAMES.open}: ${open.nonce}`);
|
|
117
|
+
for (const { ref } of [...refs].reverse()) readGit(context, [...GIT_ARGS.remove, ref]);
|
|
118
|
+
return { ok: true };
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const PLAN_OPERATIONS = { [VERBS.mint]: mintCheckpoint, [VERBS.newest]: newestCheckpoint, [VERBS.prune]: pruneCheckpoints };
|
|
122
|
+
const formatRef = ({ stem, n, oid }) => `${CHECKPOINT_KIND} ${stem}/${n} ${oid}${NEWLINE}`;
|
|
123
|
+
const formatRefusal = ({ code, reason }) => ({ code, stdout: EMPTY, stderr: `${reason}${NEWLINE}` });
|
|
124
|
+
export const main = (argv = process.argv.slice(ARGV_OFFSET), deps = {}) => {
|
|
125
|
+
const [verb, operand, planPath] = argv;
|
|
126
|
+
const planVerb = Object.hasOwn(PLAN_OPERATIONS, verb);
|
|
127
|
+
const oidVerb = verb === VERBS.verify || verb === VERBS.restore;
|
|
128
|
+
if (!(planVerb && argv.length === PLAN_ARG_COUNT && operand === PLAN_FLAG && planPath)
|
|
129
|
+
&& !(oidVerb && argv.length === OID_ARG_COUNT && operand)) return formatRefusal(refuse(USAGE_TEXT, USAGE));
|
|
130
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
131
|
+
const operation = verb === VERBS.restore ? restoreCheckpoint : verifyCheckpoint;
|
|
132
|
+
const result = planVerb ? PLAN_OPERATIONS[verb](cwd, planPath, deps) : operation(cwd, operand, deps);
|
|
133
|
+
if (!result.ok) return formatRefusal(result);
|
|
134
|
+
if (verb === VERBS.restore) return { code: ACCEPT,
|
|
135
|
+
stdout: `${CHECKPOINT_KIND} ${VERBS.restore} ${result.target} ${PROOF} ${result.proof}${NEWLINE}`, stderr: EMPTY };
|
|
136
|
+
if (verb === VERBS.verify) return { code: result.clean ? ACCEPT : REFUSE,
|
|
137
|
+
stdout: `${result.clean ? CLEAN : DIRTY} ${result.target} ${result.oid}${NEWLINE}`, stderr: EMPTY };
|
|
138
|
+
return { code: ACCEPT, stdout: verb === VERBS.prune ? EMPTY : formatRef(result), stderr: EMPTY };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (isDirectRun(import.meta.url)) {
|
|
142
|
+
const result = main();
|
|
143
|
+
process.stdout.write(result.stdout);
|
|
144
|
+
process.stderr.write(result.stderr);
|
|
145
|
+
process.exitCode = result.code;
|
|
146
|
+
}
|
package/tools/ensure-configs.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// ensure-configs.mjs — ONE runnable command for the
|
|
2
|
+
// ensure-configs.mjs — ONE runnable command for the seven stamp-independent upgrade ensures:
|
|
3
3
|
//
|
|
4
4
|
// orchestration docs/ai/orchestration.json seed, or refresh a still-canonical onboarding note
|
|
5
5
|
// gates docs/ai/gates.json seed-if-missing (an existing declaration is authored content)
|
|
6
6
|
// autonomy docs/ai/autonomy.json seed-if-missing (same)
|
|
7
|
+
// vehicles docs/ai/vehicles.json seed-if-missing (same)
|
|
7
8
|
// scripts scripts/<ADR enforcement> seed-if-missing, ADR-layout detect FIRST
|
|
8
9
|
// specs scripts/<spec layer> reader + checker pairs seeded when absent, refreshed only on
|
|
9
10
|
// + docs/ai/specs/index.md a shipped prior body, store root seeded behind a current checker
|
|
10
11
|
// index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact)
|
|
11
12
|
//
|
|
12
13
|
// Each was prose in references/modes/upgrade.md that an agent performed by hand. One command instead
|
|
13
|
-
// of
|
|
14
|
-
// has a single invocation point whose
|
|
14
|
+
// of seven is deliberate: seven independent runs would be seven chances to skip one, and the mode doc now
|
|
15
|
+
// has a single invocation point whose seven outcome lines it relays.
|
|
15
16
|
//
|
|
16
17
|
// The contract (pinned by this module's tests):
|
|
17
18
|
// • --reconcile is REQUIRED. A bare run is a usage error, so nothing writes by accident.
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
// • The ops run in a FIXED order and one op's failure NEVER skips the rest: every op reports its own
|
|
20
21
|
// token, and the exit is non-zero when any of them failed.
|
|
21
22
|
// • The deployment gate runs ONCE, before any op: an absent/symlinked docs/ai stops the whole run
|
|
22
|
-
// with the gate's own message rather than
|
|
23
|
+
// with the gate's own message rather than seven copies of it.
|
|
23
24
|
//
|
|
24
25
|
// Output is ENGLISH/structured (repo-artifact Hard Constraint); the agent localizes when narrating.
|
|
25
26
|
// Exit codes: 0 every op fine · 1 an op failed, or the deployment gate stopped the run · 2 usage.
|
|
@@ -51,12 +52,12 @@ const EMPTY_ONLY = `--only needs one operation name (${ENSURE_OPS.join(' | ')})
|
|
|
51
52
|
const REPEATED_ONLY = '--only was passed more than once — this selector names exactly ONE operation';
|
|
52
53
|
const unknownOp = (value) => `--only ${value}: no such operation (${ENSURE_OPS.join(' | ')}) — nothing was run`;
|
|
53
54
|
|
|
54
|
-
const HELP = `ensure-configs — the
|
|
55
|
+
const HELP = `ensure-configs — the seven stamp-independent upgrade ensures, as ONE command.
|
|
55
56
|
|
|
56
57
|
Usage:
|
|
57
58
|
node ensure-configs.mjs --reconcile [--dry-run] [--only <op>] [--cwd <project>]
|
|
58
59
|
|
|
59
|
-
--reconcile required — run the
|
|
60
|
+
--reconcile required — run the seven ensures (${ENSURE_OPS.join(', ')})
|
|
60
61
|
--dry-run report what each ensure WOULD do; write nothing
|
|
61
62
|
--only <op> run EXACTLY ONE of them (an unknown, missing or repeated value is a usage error)
|
|
62
63
|
--cwd <dir> the target project (default: the current directory)
|
|
@@ -148,7 +149,7 @@ export const main = (argv = [], ctx = {}) => {
|
|
|
148
149
|
const cwd = resolve(args.cwd ?? ctx.cwd ?? process.cwd());
|
|
149
150
|
const deps = ctx.deps ?? {};
|
|
150
151
|
// ONE deployment gate for the whole run (see the header): with no docs/ai there is nothing to
|
|
151
|
-
// reconcile, and
|
|
152
|
+
// reconcile, and seven identical STOPs would read as seven separate problems.
|
|
152
153
|
assertDocsAiDeployment(cwd, deps, { noun: 'the project configuration', rel: 'under docs/ai' });
|
|
153
154
|
const outcomes = runEnsures({ cwd, kitRoot: ctx.kitRoot ?? KIT_ROOT, dryRun: args.dryRun, deps, only: args.only });
|
|
154
155
|
return {
|
package/tools/ensure-ops.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// ensure-ops.mjs —
|
|
2
|
-
// outcome shape; the
|
|
1
|
+
// ensure-ops.mjs — the upgrade ensure operations this module owns, one function each, behind one shared
|
|
2
|
+
// outcome shape; the spec-layer ensure (ensure-specs.mjs) composes its outcomes through
|
|
3
3
|
// the same door and probes exported below. The CLI that orders and runs them is ensure-configs.mjs —
|
|
4
4
|
// it owns the op table, so the import graph stays acyclic; this module owns what each ensure DOES
|
|
5
5
|
// and, more importantly, what it is allowed to CLAIM.
|
|
@@ -32,6 +32,7 @@ import { seedConfig, writeConfig } from './orchestration-write.mjs';
|
|
|
32
32
|
import { lstatNoFollow, writeDocsAiFileAtomic, writeProjectFileCreateOnly } from './atomic-write.mjs';
|
|
33
33
|
import { GATES_REL } from './gates-declaration.mjs';
|
|
34
34
|
import { AUTONOMY_REL } from './autonomy-config.mjs';
|
|
35
|
+
import { VEHICLES_REL } from './vehicle-settings.mjs';
|
|
35
36
|
import { surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
36
37
|
import { ENSURE_TOKENS, FAILURE_CAUSES, SEED_SCRIPTS } from './ensure-vocabulary.mjs';
|
|
37
38
|
import { NODE_EVIDENCE, describeNodeProbes, probeNodeEvidence } from './node-evidence.mjs';
|
|
@@ -51,6 +52,9 @@ export {
|
|
|
51
52
|
} from './ensure-vocabulary.mjs';
|
|
52
53
|
|
|
53
54
|
const SCRIPTS_DIR = 'scripts';
|
|
55
|
+
const VEHICLES_OP = 'vehicles';
|
|
56
|
+
const VEHICLES_TEMPLATE = 'vehicles.json';
|
|
57
|
+
const VEHICLES_NOUN = 'a vehicle settings declaration';
|
|
54
58
|
|
|
55
59
|
const outcome = (op, token, lines, failed = false) => {
|
|
56
60
|
if (!ENSURE_TOKENS.includes(token)) {
|
|
@@ -98,7 +102,7 @@ export const probeSeedTarget = (abs, lstat) => {
|
|
|
98
102
|
export const tmpNote = (rel, tmpLeftBehind) =>
|
|
99
103
|
(tmpLeftBehind ? [`${rel}: the write stands, but its temp file could not be removed — delete it by hand: ${tmpLeftBehind}`] : []);
|
|
100
104
|
|
|
101
|
-
// ──
|
|
105
|
+
// ── orchestration.json — seed, or refresh ONLY a still-canonical onboarding note ────────────────
|
|
102
106
|
|
|
103
107
|
// Which no-change outcome is it? refreshReadme returns `changed: false` for two very different trees:
|
|
104
108
|
// a note that already IS the current canonical, and a note the user rewrote. Reporting both as
|
|
@@ -173,7 +177,7 @@ export const ensureOrchestration = ({ cwd, dryRun = false, deps = {} }) => {
|
|
|
173
177
|
return { ...refreshed, lines: [...refreshed.lines, ...seedNote] };
|
|
174
178
|
};
|
|
175
179
|
|
|
176
|
-
// ──
|
|
180
|
+
// ── gates.json + autonomy.json + vehicles.json — seed-if-missing, existing file preserved byte-for-byte ───────
|
|
177
181
|
|
|
178
182
|
const seedFromTemplate = ({ op, rel, template, noun, cwd, kitRoot, dryRun, deps }) => {
|
|
179
183
|
const lstat = deps.lstat ?? lstatSync;
|
|
@@ -202,7 +206,10 @@ export const ensureGates = ({ cwd, kitRoot, dryRun = false, deps = {} }) =>
|
|
|
202
206
|
export const ensureAutonomy = ({ cwd, kitRoot, dryRun = false, deps = {} }) =>
|
|
203
207
|
seedFromTemplate({ op: 'autonomy', rel: AUTONOMY_REL, template: 'autonomy.json', noun: 'an autonomy policy', cwd, kitRoot, dryRun, deps });
|
|
204
208
|
|
|
205
|
-
|
|
209
|
+
export const ensureVehicles = ({ cwd, kitRoot, dryRun = false, deps = {} }) =>
|
|
210
|
+
seedFromTemplate({ op: VEHICLES_OP, rel: VEHICLES_REL, template: VEHICLES_TEMPLATE, noun: VEHICLES_NOUN, cwd, kitRoot, dryRun, deps });
|
|
211
|
+
|
|
212
|
+
// ── scripts/ — the ADR-cascade enforcement pairs, detect-first ──────────────────────────────────
|
|
206
213
|
|
|
207
214
|
// The Node-evidence refusal every ensure that places Node scripts shares (contract: kit/node-evidence):
|
|
208
215
|
// null when Node provably runs here, else the ONE outcome the caller returns — a stated skip naming the
|
|
@@ -288,7 +295,7 @@ export const ensureScripts = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
|
288
295
|
return outcome('scripts', anyCreated ? 'seeded' : 'already-present', lines, false);
|
|
289
296
|
};
|
|
290
297
|
|
|
291
|
-
// ──
|
|
298
|
+
// ── docs/ai/index.md — the GENERATED navigator, regenerated when missing or stale ───────────────
|
|
292
299
|
|
|
293
300
|
// The only ensure whose target is generated rather than authored: there is nothing to preserve, and
|
|
294
301
|
// nothing to seed from either — the bundled generator IS the writer, driven through its idempotent
|
|
@@ -363,12 +370,13 @@ export const ensureIndex = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
|
363
370
|
return loud('index', 'index-probe-failed', `${INDEX_REL}: the generator reported a regeneration, but the verifying probe answered neither fresh nor stale — ${MAY_HAVE_WRITTEN}. ${verify.error ? causeOf(verify.error) : verdict}`);
|
|
364
371
|
};
|
|
365
372
|
|
|
366
|
-
// The
|
|
373
|
+
// The ops this module owns, by name. The CLI composes the full ENSURE_OPS table from these plus
|
|
367
374
|
// the spec-layer ensure (ensure-specs.mjs imports THIS module, so the table cannot live here).
|
|
368
375
|
export const OWN_IMPLEMENTATIONS = Object.freeze({
|
|
369
376
|
orchestration: ensureOrchestration,
|
|
370
377
|
gates: ensureGates,
|
|
371
378
|
autonomy: ensureAutonomy,
|
|
379
|
+
vehicles: ensureVehicles,
|
|
372
380
|
scripts: ensureScripts,
|
|
373
381
|
index: ensureIndex,
|
|
374
382
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// The FIXED order the CLI runs them in — the order references/modes/upgrade.md already prescribed.
|
|
11
11
|
// `specs` sits after `scripts` (its reader pair is a scripts/ seed too) and BEFORE `index`: the store
|
|
12
12
|
// root it seeds is a docs/ai file the navigator must count.
|
|
13
|
-
export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', 'scripts', 'specs', 'index']);
|
|
13
|
+
export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', 'vehicles', 'scripts', 'specs', 'index']);
|
|
14
14
|
|
|
15
15
|
// Tokens that assert a WRITE happened. --dry-run may never emit one of these (the CLI's contract test
|
|
16
16
|
// walks this set), and each has exactly one `would-` counterpart below. `refreshed` is the spec-layer
|
package/tools/plan-files.mjs
CHANGED
|
@@ -9,12 +9,13 @@ import { join } from 'node:path';
|
|
|
9
9
|
|
|
10
10
|
export const PLANS_REL = 'docs/plans';
|
|
11
11
|
|
|
12
|
-
// Scratch by the naming convention: EXECUTE-/FEEDBACK- prefixes, or a name carrying PROMPT/prompt/
|
|
12
|
+
// Scratch by the naming convention: EXECUTE-/FEEDBACK-/TASK- prefixes, or a name carrying PROMPT/prompt/
|
|
13
13
|
// handoff. queue.md is the series index, never a plan.
|
|
14
14
|
export const isScratchPlanName = (name) =>
|
|
15
15
|
name === 'queue.md' ||
|
|
16
16
|
name.startsWith('EXECUTE-') ||
|
|
17
17
|
name.startsWith('FEEDBACK-') ||
|
|
18
|
+
name.startsWith('TASK-') ||
|
|
18
19
|
name.includes('PROMPT') ||
|
|
19
20
|
name.includes('prompt') ||
|
|
20
21
|
name.includes('handoff');
|
package/tools/review-state.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// exit 0 when the CONFIGURED plan-execution.review recipe is solo (or the computed default is —
|
|
19
19
|
// absent config with no reviewer backend ready); when no plan is in flight (docs/plans/
|
|
20
20
|
// holds no top-level .md that is not queue.md and not scratch by the naming convention:
|
|
21
|
-
// prefixes EXECUTE- / FEEDBACK-, or a name containing PROMPT / prompt / handoff); when
|
|
21
|
+
// prefixes EXECUTE- / FEEDBACK- / TASK-, or a name containing PROMPT / prompt / handoff); when
|
|
22
22
|
// the tree is clean (nothing to review — under a non-solo review obligation the PASS
|
|
23
23
|
// still NAMES every plan in flight and states that the gate arms as soon as the tree
|
|
24
24
|
// turns dirty, so a latent arm is discoverable before it blocks a pending commit); when
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// spec:checkpoint — docs/ai/specs/kit/checkpoint/task-brief.md
|
|
2
|
+
import { writeFileSync, realpathSync, constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep, posix } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { readFileBytesNoFollow } from './fs-read-nofollow.mjs';
|
|
6
|
+
import { parseLedger, isSweep, PLAN_HEADINGS } from './plan-shape.mjs';
|
|
7
|
+
import { expandSweepPaths } from './plan-shape-facts.mjs';
|
|
8
|
+
import { readStoryLine, PIN_FILE } from './plan-shape-ownership.mjs';
|
|
9
|
+
import { parseDispatchContract } from './dispatch-record.mjs';
|
|
10
|
+
import { newestCheckpoint, verifyCheckpoint } from './checkpoint.mjs';
|
|
11
|
+
import { withRepository, readGit } from './checkpoint-core.mjs';
|
|
12
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
13
|
+
|
|
14
|
+
export const BRIEF_BINDING_INFO_STRING = 'aw-task-binding';
|
|
15
|
+
const ACCEPT = 0;
|
|
16
|
+
const REFUSE = 1;
|
|
17
|
+
const USAGE = 2;
|
|
18
|
+
const START = 0;
|
|
19
|
+
const ONE = 1;
|
|
20
|
+
const EMPTY = '';
|
|
21
|
+
const NL = '\n';
|
|
22
|
+
const CR_END = /\r$/;
|
|
23
|
+
const UTF8 = 'utf8';
|
|
24
|
+
const STRING = 'string';
|
|
25
|
+
const OBJECT = 'object';
|
|
26
|
+
const HASH = 'sha256';
|
|
27
|
+
const HEX = 'hex';
|
|
28
|
+
const SHA_PREFIX = 'sha256:';
|
|
29
|
+
const DIGEST_LINE = 'task-brief digest';
|
|
30
|
+
const FENCE = '```';
|
|
31
|
+
const FENCE_RE = /^(`{3,})(.*)$/;
|
|
32
|
+
const TITLE_RE = /^# Task: (\S.*)$/;
|
|
33
|
+
const HEADING_RE = /^#{1,2} /;
|
|
34
|
+
const STORY_PREFIX = 'Story:';
|
|
35
|
+
const HEADINGS = ['## Slice', '## Reads', '## Acceptance', '## Negative cases', '## Budget'];
|
|
36
|
+
const SLICE_FIELDS = [/^Plan: (docs\/plans\/[^/]+\.md)$/, /^Row: (\S+)$/, /^Grouping: (.*)$/, /^Files:$/];
|
|
37
|
+
const PAIR_RE = /^- (\S(?:.*\S)?) :: (\S(?:.*\S)?)$/;
|
|
38
|
+
const BULLET_RE = /^- (\S.*)$/;
|
|
39
|
+
const INTEGER_RE = /^[1-9]\d*$/;
|
|
40
|
+
const TEST_RE = /(?:^|\/)[^/]+\.test(?:\.mjs$|\/[^/].*$)/;
|
|
41
|
+
const EXTENSION_RE = /\.[^/.]+$/;
|
|
42
|
+
const TEST_SUFFIX = '.test.mjs';
|
|
43
|
+
const TEST_DIRECTORY = '.test/';
|
|
44
|
+
const TEST = 'test';
|
|
45
|
+
const IMPL = 'impl';
|
|
46
|
+
const PIN = 'pin';
|
|
47
|
+
const TAGS = [TEST, IMPL, PIN];
|
|
48
|
+
const GROUP_SEPARATOR = ', ';
|
|
49
|
+
const PARENT = '..';
|
|
50
|
+
const CURRENT = '.';
|
|
51
|
+
const READ_OK = 'ok';
|
|
52
|
+
const SCHEMA = 1;
|
|
53
|
+
const BINDING_KEYS = ['schema', 'plan', 'reads', 'checkpoint', 'head'];
|
|
54
|
+
const FILE_KEYS = ['path', 'sha256'];
|
|
55
|
+
const SHA_RE = /^[0-9a-f]{64}$/;
|
|
56
|
+
const OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
57
|
+
const HEAD_ARGS = ['rev-parse', '--verify', 'HEAD^{commit}'];
|
|
58
|
+
const STAMP = 'stamp';
|
|
59
|
+
const CHECK = 'check';
|
|
60
|
+
const DISPATCH_FLAG = '--dispatch';
|
|
61
|
+
const BASIC_ARGS = 2;
|
|
62
|
+
const DISPATCH_ARGS = 4;
|
|
63
|
+
const ARGV_OFFSET = 2;
|
|
64
|
+
const NAMES = { shape: 'shape', grouping: 'grouping', row: 'row', story: 'story', reads: 'reads', binding: 'binding',
|
|
65
|
+
plan: 'binding-plan', read: 'binding-read', checkpoint: 'binding-checkpoint', stale: 'checkpoint-stale',
|
|
66
|
+
head: 'head', boundHead: 'binding-head', dispatch: 'dispatch-inputs' };
|
|
67
|
+
const USAGE_TEXT = 'usage: task-brief stamp <brief> | check <brief> [--dispatch <file>]';
|
|
68
|
+
const WRITE_FLAGS = fsConstants.O_WRONLY | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW;
|
|
69
|
+
|
|
70
|
+
const refuse = (reason, code = REFUSE) => ({ ok: false, code, reason });
|
|
71
|
+
const reject = (name, detail = EMPTY) => { throw new Error(`${name}: ${detail}`); };
|
|
72
|
+
const computeDigest = (bytes) => createHash(HASH).update(bytes).digest(HEX);
|
|
73
|
+
const matchLine = (pattern, line) => {
|
|
74
|
+
const match = pattern.exec(line ?? EMPTY);
|
|
75
|
+
if (match === null) reject(NAMES.shape, line);
|
|
76
|
+
return match.slice(ONE);
|
|
77
|
+
};
|
|
78
|
+
const splitBindingBlocks = (text) => {
|
|
79
|
+
const raw = text.split(NL);
|
|
80
|
+
const lines = raw.map((line) => line.replace(CR_END, EMPTY));
|
|
81
|
+
const state = lines.reduce((state, line, index) => {
|
|
82
|
+
const fence = FENCE_RE.exec(line);
|
|
83
|
+
if (fence === null) return state;
|
|
84
|
+
const ticks = fence[ONE].length;
|
|
85
|
+
const info = fence[BASIC_ARGS].trim();
|
|
86
|
+
if (state.open === null) return { ...state, open: { ticks, info, start: index } };
|
|
87
|
+
if (info !== EMPTY || ticks < state.open.ticks) return state;
|
|
88
|
+
const blocks = state.open.info === BRIEF_BINDING_INFO_STRING
|
|
89
|
+
? [...state.blocks, { start: state.open.start, end: index, source: lines.slice(state.open.start + ONE, index).join(NL) }]
|
|
90
|
+
: state.blocks;
|
|
91
|
+
return { open: null, blocks };
|
|
92
|
+
}, { open: null, blocks: [] });
|
|
93
|
+
const unclosed = state.open?.info === BRIEF_BINDING_INFO_STRING;
|
|
94
|
+
const blocks = unclosed ? [...state.blocks, { start: state.open.start, end: lines.length, source: null }] : state.blocks;
|
|
95
|
+
const outside = (index) => !blocks.some(({ start, end }) => index >= start && index <= end);
|
|
96
|
+
return { blocks, unclosed, body: raw.filter((line, index) => outside(index)).join(NL),
|
|
97
|
+
lines: lines.filter((line, index) => outside(index) && line.trim() !== EMPTY) };
|
|
98
|
+
};
|
|
99
|
+
export const parseBrief = (text) => {
|
|
100
|
+
try {
|
|
101
|
+
if (typeof text !== STRING) return refuse(NAMES.shape);
|
|
102
|
+
const document = splitBindingBlocks(text);
|
|
103
|
+
const lines = document.lines;
|
|
104
|
+
const [title] = matchLine(TITLE_RE, lines[START]);
|
|
105
|
+
const headings = lines.flatMap((line, index) => HEADING_RE.test(line) ? [{ text: line, index }] : []);
|
|
106
|
+
if (headings.length !== HEADINGS.length + ONE || HEADINGS.some((text, index) => headings[index + ONE]?.text !== text)) reject(NAMES.shape);
|
|
107
|
+
const sections = HEADINGS.map((heading, index) => lines.slice(headings[index + ONE].index + ONE, headings[index + BASIC_ARGS]?.index ?? lines.length));
|
|
108
|
+
const storyLines = lines.slice(ONE, headings[ONE].index);
|
|
109
|
+
if (storyLines.some((line) => !line.startsWith(STORY_PREFIX))) reject(NAMES.shape);
|
|
110
|
+
const [slice, reads, acceptance, negative, budget] = sections;
|
|
111
|
+
const [[planPath], [rowId], [grouping]] = SLICE_FIELDS.map((pattern, index) => matchLine(pattern, slice[index]));
|
|
112
|
+
const files = slice.slice(SLICE_FIELDS.length).map((line) => {
|
|
113
|
+
const [path, tag] = matchLine(PAIR_RE, line);
|
|
114
|
+
return { path, tag };
|
|
115
|
+
});
|
|
116
|
+
const budgets = budget.map((line) => {
|
|
117
|
+
const [path, count] = matchLine(PAIR_RE, line);
|
|
118
|
+
if (!INTEGER_RE.test(count) || !Number.isSafeInteger(Number(count))) reject(NAMES.shape, line);
|
|
119
|
+
return { path, maxLines: Number(count) };
|
|
120
|
+
});
|
|
121
|
+
const filePaths = new Set(files.map(({ path }) => path));
|
|
122
|
+
if (files.length === START || budgets.length !== filePaths.size || new Set(budgets.map(({ path }) => path)).size !== filePaths.size
|
|
123
|
+
|| budgets.some(({ path }) => !filePaths.has(path)) || acceptance.length === START || negative.length === START) reject(NAMES.shape);
|
|
124
|
+
return { ok: true, title, storyLines, planPath, rowId, grouping, files, budgets,
|
|
125
|
+
reads: reads.map((line) => matchLine(BULLET_RE, line)[START]),
|
|
126
|
+
acceptance: acceptance.map((line) => matchLine(PAIR_RE, line)), negative: negative.map((line) => matchLine(BULLET_RE, line)[START]),
|
|
127
|
+
blocks: document.blocks, unclosed: document.unclosed, body: document.body };
|
|
128
|
+
} catch (error) { return refuse(error.message); }
|
|
129
|
+
};
|
|
130
|
+
const isInside = (top, path) => {
|
|
131
|
+
const rel = relative(top, path);
|
|
132
|
+
return !isAbsolute(rel) && rel !== PARENT && !rel.startsWith(`${PARENT}${sep}`);
|
|
133
|
+
};
|
|
134
|
+
const readInside = (context, path, name = NAMES.reads) => {
|
|
135
|
+
try {
|
|
136
|
+
if (typeof path !== STRING || path === EMPTY || isAbsolute(path)) reject(name, path);
|
|
137
|
+
const absolute = resolve(context.cwd, path);
|
|
138
|
+
if (!isInside(context.top, absolute) || !isInside(realpathSync(context.top), realpathSync(dirname(absolute)))) reject(name, path);
|
|
139
|
+
const result = readFileBytesNoFollow(absolute, context.io);
|
|
140
|
+
if (result.outcome !== READ_OK) reject(name, `${path}: ${result.code ?? result.className ?? result.outcome}`);
|
|
141
|
+
return { path, absolute, bytes: result.bytes, sha256: computeDigest(result.bytes) };
|
|
142
|
+
} catch (error) { reject(name, `${path}: ${error.message}`); }
|
|
143
|
+
};
|
|
144
|
+
const checkGrouping = (brief, row) => {
|
|
145
|
+
if (brief.files.some(({ tag }) => !TAGS.includes(tag))
|
|
146
|
+
|| brief.grouping !== TAGS.filter((tag) => brief.files.some((file) => file.tag === tag)).join(GROUP_SEPARATOR)) reject(NAMES.grouping);
|
|
147
|
+
const pins = brief.files.filter(({ tag }) => tag === PIN);
|
|
148
|
+
if (row && posix.basename(row.path) === PIN_FILE) {
|
|
149
|
+
if (pins.length !== ONE || pins[START].path !== row.path) reject(NAMES.grouping);
|
|
150
|
+
} else if (pins.length !== START) reject(NAMES.grouping);
|
|
151
|
+
};
|
|
152
|
+
const checkRow = (context, brief, row) => {
|
|
153
|
+
if (!row?.valid) reject(NAMES.row, brief.rowId);
|
|
154
|
+
const files = brief.files.map((file) => {
|
|
155
|
+
const path = posix.normalize(file.path);
|
|
156
|
+
if (path !== file.path || path === CURRENT || path.startsWith(PARENT) || posix.isAbsolute(path)) reject(NAMES.row, file.path);
|
|
157
|
+
return { ...file, path };
|
|
158
|
+
});
|
|
159
|
+
const implementations = files.filter(({ tag }) => tag === IMPL).map(({ path }) => path);
|
|
160
|
+
const allowed = isSweep(row.path) ? expandSweepPaths(context.top, [row.path])[row.path] : [row.path];
|
|
161
|
+
if (implementations.some((path) => !allowed.includes(path))) reject(NAMES.row, row.id);
|
|
162
|
+
const tests = files.filter(({ tag }) => tag === TEST);
|
|
163
|
+
if (tests.some(({ path }) => TEST_RE.test(row.path) ? path !== row.path : !implementations.some((impl) => {
|
|
164
|
+
const stem = impl.replace(EXTENSION_RE, EMPTY);
|
|
165
|
+
return path === `${stem}${TEST_SUFFIX}` || path.startsWith(`${stem}${TEST_DIRECTORY}`);
|
|
166
|
+
}))) reject(NAMES.row, row.id);
|
|
167
|
+
};
|
|
168
|
+
const selectStory = (lines) => {
|
|
169
|
+
const entries = lines.map((text, index) => ({ text, line: index + ONE }));
|
|
170
|
+
const result = readStoryLine(entries);
|
|
171
|
+
return result.story && result.findings.length === START ? lines[result.story.line - ONE] : null;
|
|
172
|
+
};
|
|
173
|
+
const readGrammar = (context, briefPath) => {
|
|
174
|
+
const file = readInside(context, briefPath);
|
|
175
|
+
const brief = parseBrief(file.bytes.toString(UTF8));
|
|
176
|
+
if (!brief.ok) return brief;
|
|
177
|
+
const plan = readInside(context, brief.planPath);
|
|
178
|
+
const ledger = parseLedger(plan.bytes.toString(UTF8));
|
|
179
|
+
const row = ledger.rows.find(({ id }) => id === brief.rowId);
|
|
180
|
+
checkGrouping(brief, row);
|
|
181
|
+
checkRow(context, brief, row);
|
|
182
|
+
const goal = ledger.document.headings.find(({ text }) => text === PLAN_HEADINGS[START]);
|
|
183
|
+
const end = ledger.document.headings.find(({ index }) => index > goal?.index);
|
|
184
|
+
const story = goal ? selectStory(ledger.document.lines.slice(goal.index + ONE, end?.index)) : null;
|
|
185
|
+
if (story === null || selectStory(brief.storyLines) !== story) reject(NAMES.story);
|
|
186
|
+
return { ok: true, file, brief, plan };
|
|
187
|
+
};
|
|
188
|
+
const checkBlockCount = (brief, required) => {
|
|
189
|
+
if (brief.unclosed || brief.blocks.length > ONE || (required && brief.blocks.length !== ONE)) reject(NAMES.binding);
|
|
190
|
+
};
|
|
191
|
+
const hasClosedKeys = (value, keys) => value !== null && typeof value === OBJECT && !Array.isArray(value)
|
|
192
|
+
&& Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
193
|
+
const isFileBinding = (value) => hasClosedKeys(value, FILE_KEYS) && typeof value.path === STRING && typeof value.sha256 === STRING && SHA_RE.test(value.sha256);
|
|
194
|
+
const readBinding = (brief) => {
|
|
195
|
+
checkBlockCount(brief, true);
|
|
196
|
+
try {
|
|
197
|
+
const value = JSON.parse(brief.blocks[START].source);
|
|
198
|
+
if (!hasClosedKeys(value, BINDING_KEYS) || value.schema !== SCHEMA || !isFileBinding(value.plan)
|
|
199
|
+
|| !Array.isArray(value.reads) || !value.reads.every(isFileBinding) || typeof value.checkpoint !== STRING
|
|
200
|
+
|| !OID_RE.test(value.checkpoint) || typeof value.head !== STRING || !OID_RE.test(value.head)) reject(NAMES.binding);
|
|
201
|
+
return value;
|
|
202
|
+
} catch (error) { reject(NAMES.binding, error.message); }
|
|
203
|
+
};
|
|
204
|
+
const readHead = (context, name) => {
|
|
205
|
+
try {
|
|
206
|
+
const oid = readGit(context, HEAD_ARGS);
|
|
207
|
+
if (!OID_RE.test(oid)) reject(name);
|
|
208
|
+
return oid;
|
|
209
|
+
} catch (error) { reject(name, error.message); }
|
|
210
|
+
};
|
|
211
|
+
const bindFile = ({ path, sha256 }) => ({ path, sha256 });
|
|
212
|
+
const checkDispatch = (context, dispatchPath, briefPath, digest) => {
|
|
213
|
+
const file = readInside(context, dispatchPath, NAMES.dispatch);
|
|
214
|
+
const parsed = parseDispatchContract(file.bytes.toString(UTF8));
|
|
215
|
+
if (!parsed.ok || typeof parsed.contract.inputs !== STRING || !parsed.contract.inputs.includes(briefPath)
|
|
216
|
+
|| !parsed.contract.inputs.includes(`${SHA_PREFIX}${digest}`)) reject(NAMES.dispatch);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const stampBrief = (cwd, briefPath, io = {}) => withRepository(cwd, io, (context) => {
|
|
220
|
+
const grammar = readGrammar(context, briefPath);
|
|
221
|
+
if (!grammar.ok) return grammar;
|
|
222
|
+
const { file, brief, plan } = grammar;
|
|
223
|
+
checkBlockCount(brief, false);
|
|
224
|
+
const reads = brief.reads.map((path) => bindFile(readInside(context, path)));
|
|
225
|
+
const newest = newestCheckpoint(cwd, brief.planPath, context.io);
|
|
226
|
+
if (!newest.ok) return newest;
|
|
227
|
+
const binding = { schema: SCHEMA, plan: bindFile(plan), reads, checkpoint: newest.oid, head: readHead(context, NAMES.head) };
|
|
228
|
+
const bytes = Buffer.from(`${brief.body.trimEnd()}${NL}${NL}${FENCE}${BRIEF_BINDING_INFO_STRING}${NL}${JSON.stringify(binding)}${NL}${FENCE}${NL}`, UTF8);
|
|
229
|
+
writeFileSync(file.absolute, bytes, { flag: WRITE_FLAGS });
|
|
230
|
+
return { ok: true, digest: computeDigest(bytes) };
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export const checkBrief = (cwd, briefPath, { dispatchPath } = {}, io = {}) => withRepository(cwd, io, (context) => {
|
|
234
|
+
const grammar = readGrammar(context, briefPath);
|
|
235
|
+
if (!grammar.ok) return grammar;
|
|
236
|
+
const { file, brief, plan } = grammar;
|
|
237
|
+
const binding = readBinding(brief);
|
|
238
|
+
const newest = newestCheckpoint(cwd, brief.planPath, context.io);
|
|
239
|
+
if (!newest.ok) return newest;
|
|
240
|
+
const reads = brief.reads.map((path) => bindFile(readInside(context, path)));
|
|
241
|
+
if (binding.plan.path !== brief.planPath || binding.plan.sha256 !== plan.sha256) reject(NAMES.plan);
|
|
242
|
+
if (binding.reads.length !== reads.length || reads.some((entry, index) =>
|
|
243
|
+
entry.path !== binding.reads[index].path || entry.sha256 !== binding.reads[index].sha256)) reject(NAMES.read);
|
|
244
|
+
if (binding.checkpoint !== newest.oid) reject(NAMES.checkpoint);
|
|
245
|
+
const verified = verifyCheckpoint(cwd, binding.checkpoint, context.io);
|
|
246
|
+
if (!verified.ok || !verified.clean) reject(NAMES.stale, verified.reason);
|
|
247
|
+
if (binding.head !== readHead(context, NAMES.boundHead)) reject(NAMES.boundHead);
|
|
248
|
+
if (dispatchPath !== undefined) checkDispatch(context, dispatchPath, briefPath, file.sha256);
|
|
249
|
+
return { ok: true, digest: file.sha256 };
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const formatResult = (result) => result.ok
|
|
253
|
+
? { code: ACCEPT, stdout: `${DIGEST_LINE} ${SHA_PREFIX}${result.digest}${NL}`, stderr: EMPTY }
|
|
254
|
+
: { code: result.code, stdout: EMPTY, stderr: `${result.reason}${NL}` };
|
|
255
|
+
export const main = (argv = process.argv.slice(ARGV_OFFSET), deps = {}) => {
|
|
256
|
+
try {
|
|
257
|
+
const [verb, path, flag, dispatchPath] = argv;
|
|
258
|
+
if (!path || !(verb === STAMP || verb === CHECK) || !(argv.length === BASIC_ARGS
|
|
259
|
+
|| (verb === CHECK && argv.length === DISPATCH_ARGS && flag === DISPATCH_FLAG && dispatchPath))) return formatResult(refuse(USAGE_TEXT, USAGE));
|
|
260
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
261
|
+
return formatResult(verb === STAMP ? stampBrief(cwd, path, deps) : checkBrief(cwd, path, { dispatchPath }, deps));
|
|
262
|
+
} catch (error) { return formatResult(refuse(error.message)); }
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (isDirectRun(import.meta.url)) {
|
|
266
|
+
const result = main();
|
|
267
|
+
process.stdout.write(result.stdout);
|
|
268
|
+
process.stderr.write(result.stderr);
|
|
269
|
+
process.exitCode = result.code;
|
|
270
|
+
}
|