@sabaiway/agent-workflow-kit 5.10.0 → 5.11.1
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 +152 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +13 -2
- package/references/hooks/state-block-guard.mjs +14 -2
- package/references/modes/commit-guard.md +11 -8
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +32 -10
- package/references/modes/worktrees.md +47 -3
- package/references/scripts/archive-changelog.mjs +14 -3
- package/references/scripts/archive-decisions.mjs +14 -3
- package/references/scripts/archive-issues.mjs +14 -3
- package/references/scripts/check-docs-size.mjs +14 -3
- package/references/scripts/migrate-gates.mjs +13 -2
- package/tools/ack-write.mjs +3 -3
- package/tools/advisor-matrix.mjs +165 -0
- package/tools/autonomy-doctor.mjs +2 -3
- package/tools/bridge-settings.mjs +2 -3
- package/tools/cheap-agents.mjs +3 -3
- package/tools/commands.mjs +4 -5
- package/tools/commit-guard.mjs +77 -20
- package/tools/core-evidence.mjs +12 -3
- package/tools/coverage-check.mjs +2 -3
- package/tools/delegation.mjs +2 -3
- package/tools/detect-backends.mjs +2 -3
- package/tools/dispatch-advisor.mjs +323 -0
- package/tools/dispatch.mjs +174 -109
- package/tools/doc-parity.mjs +69 -16
- package/tools/family-registry.mjs +3 -3
- package/tools/flow-adoption-mint.mjs +70 -0
- package/tools/flow-append.mjs +309 -0
- package/tools/flow-chain-state.mjs +91 -0
- package/tools/flow-check-cores.mjs +35 -6
- package/tools/flow-check-rungs.mjs +20 -2
- package/tools/flow-check.mjs +22 -8
- package/tools/flow-delta-proof.mjs +307 -0
- package/tools/flow-record.mjs +1 -1
- package/tools/flow-store-read.mjs +3 -3
- package/tools/flow-store.mjs +35 -812
- package/tools/flow-subset-budget.mjs +81 -0
- package/tools/flow-writer.mjs +3 -3
- package/tools/gate-hook.mjs +3 -3
- package/tools/gates-init.mjs +3 -3
- package/tools/grounding.mjs +2 -3
- package/tools/hide-footprint.mjs +2 -3
- package/tools/inject-methodology.mjs +2 -3
- package/tools/lens-region.mjs +2 -3
- package/tools/manifest/validate.mjs +2 -3
- package/tools/migrate-adr-store.mjs +3 -3
- package/tools/observation-builder.mjs +123 -0
- package/tools/path-inventory.mjs +2 -3
- package/tools/procedures.mjs +3 -3
- package/tools/receipt-deadline.mjs +2 -3
- package/tools/recipes.mjs +2 -3
- package/tools/recommendations.mjs +3 -3
- package/tools/release-scan.mjs +2 -3
- package/tools/repo-search.mjs +2 -3
- package/tools/review-state.mjs +3 -3
- package/tools/run-gates.mjs +2 -3
- package/tools/sandbox-masks.mjs +3 -3
- package/tools/satellite-locator.mjs +179 -0
- package/tools/set-autonomy.mjs +2 -3
- package/tools/set-flow.mjs +3 -3
- package/tools/set-recipe.mjs +2 -3
- package/tools/setup-backends.mjs +3 -3
- package/tools/store-append.mjs +2 -2
- package/tools/uninstall.mjs +2 -3
- package/tools/velocity-profile.mjs +3 -3
- package/tools/worktree-handoff-return.mjs +369 -0
- package/tools/worktree-prompt.mjs +190 -0
- package/tools/worktrees-record.mjs +171 -0
- package/tools/worktrees.mjs +311 -300
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// flow-subset-budget.mjs — the Decision-7/8 counting-context budget (Plan 4): the two caps, the ONE
|
|
2
|
+
// budget walk both consumers share (subsetAttemptState), the exhaustion remedy prose, and the
|
|
3
|
+
// under-lock gate the append lane runs. Split out of flow-store.mjs unchanged (baseline-practices
|
|
4
|
+
// tranche 2); the facade re-exports everything here except subsetAttemptGate, which crosses to
|
|
5
|
+
// flow-append.mjs alone and stays off the store's public surface.
|
|
6
|
+
//
|
|
7
|
+
// PURE over read results, beside flow-chain-state.mjs at the bottom of the store's five leaves: no
|
|
8
|
+
// store IO, no git, no fs, and the record vocabulary is its only tools sibling.
|
|
9
|
+
|
|
10
|
+
import { flowRecordKey, subsetFoldBatchDigest } from './flow-record.mjs';
|
|
11
|
+
|
|
12
|
+
// The waste bound is the CAP, not the prose (Decision 8): a counting context allows at most
|
|
13
|
+
// THREE red attempts (two blind + one diagnosed) on its own; past that, no diagnosis reopens it —
|
|
14
|
+
// only a recorded FRESH-EYES consult verdict does, one further attempt per consult.
|
|
15
|
+
export const SUBSET_ATTEMPT_MAX_REDS = 3;
|
|
16
|
+
|
|
17
|
+
// Past the SECOND red every further attempt at the key rides a recorded diagnosis — the
|
|
18
|
+
// obligation keys on REDS, never on the attempt index (a green history stays blind-legal).
|
|
19
|
+
export const SUBSET_ATTEMPT_DIAGNOSIS_REDS = 2;
|
|
20
|
+
|
|
21
|
+
// subsetAttemptState(records, probe) → { attempts, nextIndex, reds, credits, exhausted } — the
|
|
22
|
+
// ONE Decision-7/8 budget walk both consumers share (the locked gate below re-runs it under the
|
|
23
|
+
// lock; run-gates' pre-gate check reads it lock-free). The exhaustion ladder is PERMIT-based
|
|
24
|
+
// and foldBatch-GLOBAL (round-3 disposition): red counts stay per key, but permits and their
|
|
25
|
+
// consumption span EVERY subsetDigest of the round context — one consult verdict is exactly
|
|
26
|
+
// ONE further attempt across the whole foldBatch, whichever subset spends it. Consult identity
|
|
27
|
+
// {backend, nonce} is tracked STORE-WIDE before any relevance filtering, so a replay from
|
|
28
|
+
// another round (or any seen identity — pre-exhaustion or spent) never credits; a credit is
|
|
29
|
+
// granted only for a NEW identity whose {planId, cycle, stepId, round} digests to this
|
|
30
|
+
// foldBatch while SOME key of the foldBatch is base-exhausted at that point in raw order.
|
|
31
|
+
// EVERY attempt recorded past its own key's base budget consumes one credit, whatever its
|
|
32
|
+
// status; a tampered store that drove credits negative stays exhausted (fail closed).
|
|
33
|
+
export const subsetAttemptState = (records, probe) => {
|
|
34
|
+
const key = flowRecordKey({ kind: 'subset-attempt', ...probe });
|
|
35
|
+
const attempts = [];
|
|
36
|
+
const seenConsults = new Set();
|
|
37
|
+
const redsByKey = new Map();
|
|
38
|
+
let credits = 0;
|
|
39
|
+
const someKeyExhausted = () => [...redsByKey.values()].some((n) => n >= SUBSET_ATTEMPT_MAX_REDS);
|
|
40
|
+
for (const r of records) {
|
|
41
|
+
if (r.kind === 'subset-attempt' && r.foldBatch === probe.foldBatch) {
|
|
42
|
+
const rKey = flowRecordKey(r);
|
|
43
|
+
const priorReds = redsByKey.get(rKey) ?? 0;
|
|
44
|
+
if (priorReds >= SUBSET_ATTEMPT_MAX_REDS) credits -= 1;
|
|
45
|
+
if (r.status === 'red') redsByKey.set(rKey, priorReds + 1);
|
|
46
|
+
if (rKey === key) attempts.push(r);
|
|
47
|
+
} else if (r.kind === 'consult-attestation') {
|
|
48
|
+
const identity = JSON.stringify([r.backend, r.nonce]);
|
|
49
|
+
const relevant = subsetFoldBatchDigest({ planId: r.planId, cycle: r.cycle, stepId: r.stepId, round: r.round }) === probe.foldBatch;
|
|
50
|
+
if (relevant && !seenConsults.has(identity) && someKeyExhausted()) credits += 1;
|
|
51
|
+
seenConsults.add(identity);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const reds = redsByKey.get(key) ?? 0;
|
|
55
|
+
return {
|
|
56
|
+
attempts,
|
|
57
|
+
nextIndex: attempts.reduce((m, r) => Math.max(m, r.attemptIndex), 0) + 1,
|
|
58
|
+
reds,
|
|
59
|
+
credits,
|
|
60
|
+
exhausted: reds >= SUBSET_ATTEMPT_MAX_REDS && credits <= 0,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const subsetExhaustionRemedy = 'the fresh-eyes lane reopens it (Decision 8 — never a human wait-state): dispatch a MANDATORY grounded bridge consult (a different model) carrying the full attempt/diagnosis trail; its recorded consult-attestation at this round context reopens exactly ONE further diagnosed attempt. Otherwise park the stuck work with its trail and switch to independent work; a fresh context opens with the next round (new foldBatch) or a declared pregateExclude change (new subsetDigest)';
|
|
65
|
+
|
|
66
|
+
// The under-lock rules the factory does NOT already enforce itself: the exhaustion ladder and
|
|
67
|
+
// the byte-distinct diagnosis (blind thrashing refuses; a NEW hypothesis proceeds). The
|
|
68
|
+
// monotonic index and the reds-based diagnosis REQUIREMENT live in the factory alone — it is
|
|
69
|
+
// the ONLY entry for this kind (the generic lane refuses it by name, round-9 fold), computes
|
|
70
|
+
// the index from the SAME locked snapshot this gate sees, and throws its own named stops first.
|
|
71
|
+
export const subsetAttemptGate = (records, snapshot) => {
|
|
72
|
+
const { attempts, reds, exhausted } = subsetAttemptState(records, snapshot);
|
|
73
|
+
if (exhausted) {
|
|
74
|
+
return { ok: false, reason: `this counting context already holds ${reds} red attempts — EXHAUSTED (two blind + one diagnosed, Decision 8) and no diagnosis reopens it; ${subsetExhaustionRemedy}` };
|
|
75
|
+
}
|
|
76
|
+
const prior = attempts.find((r) => r.attemptIndex === snapshot.attemptIndex - 1);
|
|
77
|
+
if (typeof snapshot.diagnosis === 'string' && prior != null && prior.diagnosis === snapshot.diagnosis) {
|
|
78
|
+
return { ok: false, reason: "the diagnosis is byte-identical to the prior attempt's — a diagnosed continuation states a NEW hypothesis (Decision 8); blind thrashing refuses" };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true };
|
|
81
|
+
};
|
package/tools/flow-writer.mjs
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
50
50
|
import { join, dirname } from 'node:path';
|
|
51
|
-
import {
|
|
51
|
+
import { fileURLToPath } from 'node:url';
|
|
52
52
|
import { createHash, randomBytes } from 'node:crypto';
|
|
53
53
|
import {
|
|
54
54
|
FLOW_SCHEMA_VERSION, CHAIN_KIND, canonicalFlowDigest, flowTreeIdentity,
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
appendFlowRecord, appendFlowRecordWithPreflight, mintAdoption, readPlanFrontmatterId,
|
|
60
60
|
resolveRecordReference, priorChainTerminal,
|
|
61
61
|
} from './flow-store.mjs';
|
|
62
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
62
63
|
import { readFileBytesNoFollow, gitLine } from './flow-store-read.mjs';
|
|
63
64
|
import {
|
|
64
65
|
resolveBase, computeTreeFingerprint, lexicalRepoRelative,
|
|
@@ -1256,8 +1257,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
1256
1257
|
}
|
|
1257
1258
|
};
|
|
1258
1259
|
|
|
1259
|
-
|
|
1260
|
-
if (isDirectRun) {
|
|
1260
|
+
if (isDirectRun(import.meta.url)) {
|
|
1261
1261
|
const r = main(process.argv.slice(2));
|
|
1262
1262
|
if (r.stdout) console.log(r.stdout);
|
|
1263
1263
|
if (r.stderr) console.error(r.stderr);
|
package/tools/gate-hook.mjs
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
43
43
|
import { dirname, join, resolve } from 'node:path';
|
|
44
|
-
import { fileURLToPath
|
|
44
|
+
import { fileURLToPath } from 'node:url';
|
|
45
45
|
import {
|
|
46
46
|
CLAUDE_DIR,
|
|
47
47
|
EXPECTED_WORKFLOW_VERSION,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
resolveEffectiveMode,
|
|
55
55
|
} from './velocity-profile.mjs';
|
|
56
56
|
import { assertDocsAiDeployment, writeDocsAiFileAtomic } from './atomic-write.mjs';
|
|
57
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
57
58
|
import { shellQuoteArg } from './review-state.mjs';
|
|
58
59
|
|
|
59
60
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -572,5 +573,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
572
573
|
}
|
|
573
574
|
};
|
|
574
575
|
|
|
575
|
-
|
|
576
|
-
if (isDirectRun) process.exit(main(process.argv.slice(2)));
|
|
576
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
package/tools/gates-init.mjs
CHANGED
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
|
|
59
59
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
60
60
|
import { join, resolve, dirname } from 'node:path';
|
|
61
|
-
import { fileURLToPath
|
|
61
|
+
import { fileURLToPath } from 'node:url';
|
|
62
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
62
63
|
import { discoverGateCandidates, EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
|
|
63
64
|
import { GATES_REL, validateDeclaration } from './run-gates.mjs';
|
|
64
65
|
import { canonicalCheckerGates, coverageDeclarationDefects, isKitOwnedCheckerGate } from './gates-declaration.mjs';
|
|
@@ -795,5 +796,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
795
796
|
}
|
|
796
797
|
};
|
|
797
798
|
|
|
798
|
-
|
|
799
|
-
if (isDirectRun) process.exit(main(process.argv.slice(2)));
|
|
799
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
package/tools/grounding.mjs
CHANGED
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
import { readFileSync, writeFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
32
32
|
import { resolve, join, relative, isAbsolute, dirname, basename, sep } from 'node:path';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
34
|
-
import { pathToFileURL } from 'node:url';
|
|
35
34
|
import { spawnSync } from 'node:child_process';
|
|
35
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
36
36
|
import { fail } from './orchestration-config.mjs';
|
|
37
37
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
38
38
|
// (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
|
|
@@ -466,8 +466,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
466
466
|
}
|
|
467
467
|
};
|
|
468
468
|
|
|
469
|
-
|
|
470
|
-
if (isDirectRun) {
|
|
469
|
+
if (isDirectRun(import.meta.url)) {
|
|
471
470
|
const r = main(process.argv.slice(2));
|
|
472
471
|
// Exact writes + a natural exit: console.log would append a stray newline to the
|
|
473
472
|
// byte-exact facts payload, and process.exit() can truncate large piped stdout before Node
|
package/tools/hide-footprint.mjs
CHANGED
|
@@ -24,8 +24,8 @@ import {
|
|
|
24
24
|
} from 'node:fs';
|
|
25
25
|
import { join, resolve, relative, isAbsolute, basename } from 'node:path';
|
|
26
26
|
import { execFileSync } from 'node:child_process';
|
|
27
|
-
import { pathToFileURL } from 'node:url';
|
|
28
27
|
import os from 'node:os';
|
|
28
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
29
29
|
import {
|
|
30
30
|
KIT_OWN_PATHS, KNOWN_FOOTPRINT, FOOTPRINT_STOP, stop,
|
|
31
31
|
normalizeSlashes, isDirPattern, patternToProbe, expandGlob, matchesKnownGlob,
|
|
@@ -584,5 +584,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
584
584
|
}
|
|
585
585
|
};
|
|
586
586
|
|
|
587
|
-
|
|
588
|
-
if (isDirectRun) process.exit(main(process.argv.slice(2)));
|
|
587
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
// ONLY when its content normalize-matches a KNOWN PRIOR canonical fragment (drift-guarded, append-only
|
|
27
27
|
// per descriptor). A customized slot never matches → preserved verbatim + a read-only upgrade advisory.
|
|
28
28
|
|
|
29
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
29
30
|
import { normalizeCanonical } from './orchestration-config.mjs';
|
|
30
31
|
|
|
31
32
|
export const START_MARKER = '<!-- workflow:methodology:start -->';
|
|
@@ -599,9 +600,7 @@ export const runCli = async (argv, deps = {}) => {
|
|
|
599
600
|
}
|
|
600
601
|
};
|
|
601
602
|
|
|
602
|
-
|
|
603
|
-
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
604
|
-
if (isDirectRun) {
|
|
603
|
+
if (isDirectRun(import.meta.url)) {
|
|
605
604
|
const { code, stdout, stderr } = await runCli(process.argv.slice(2));
|
|
606
605
|
if (stdout) process.stdout.write(stdout);
|
|
607
606
|
if (stderr) process.stderr.write(stderr);
|
package/tools/lens-region.mjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
//
|
|
28
28
|
// Pure string functions (fs only in the CLI); dependency-free, Node >= 22.
|
|
29
29
|
|
|
30
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
30
31
|
import { normalizeCanonical } from './orchestration-config.mjs';
|
|
31
32
|
|
|
32
33
|
// The deployed-heading matcher (prefix, like the historical extractLensBlock: robust to a future
|
|
@@ -386,6 +387,4 @@ export const runCli = async (argv, deps = {}) => {
|
|
|
386
387
|
return 0;
|
|
387
388
|
};
|
|
388
389
|
|
|
389
|
-
|
|
390
|
-
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
391
|
-
if (isDirectRun) process.exitCode = await runCli(process.argv.slice(2));
|
|
390
|
+
if (isDirectRun(import.meta.url)) process.exitCode = await runCli(process.argv.slice(2));
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { readFileSync, existsSync } from 'node:fs';
|
|
22
22
|
import { resolve, join, isAbsolute } from 'node:path';
|
|
23
|
-
import {
|
|
23
|
+
import { isDirectRun } from '../direct-run.mjs';
|
|
24
24
|
|
|
25
25
|
export const FAMILY = 'agent-workflow';
|
|
26
26
|
export const SUPPORTED_SCHEMA = 1;
|
|
@@ -765,5 +765,4 @@ const main = (argv) => {
|
|
|
765
765
|
if (strict && notValid > 0) process.exit(1);
|
|
766
766
|
};
|
|
767
767
|
|
|
768
|
-
|
|
769
|
-
if (isDirectRun) main(process.argv.slice(2));
|
|
768
|
+
if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
|
|
35
35
|
import { join, resolve, dirname, relative, isAbsolute } from 'node:path';
|
|
36
|
-
import { fileURLToPath
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
37
|
import { spawnSync } from 'node:child_process';
|
|
38
38
|
import { tmpdir } from 'node:os';
|
|
39
39
|
import { writeContainedFileAtomic, assertDocsAiDeployment } from './atomic-write.mjs';
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
defaultRegenerateIndex,
|
|
48
48
|
runCli as runArchiveDecisions,
|
|
49
49
|
} from '../references/scripts/archive-decisions.mjs';
|
|
50
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
50
51
|
import { surveyAdrLayoutStrict, ADR_LAYOUT_PATHS } from './family-registry.mjs';
|
|
51
52
|
|
|
52
53
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -463,5 +464,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
463
464
|
}
|
|
464
465
|
};
|
|
465
466
|
|
|
466
|
-
|
|
467
|
-
if (isDirectRun) process.exitCode = main();
|
|
467
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// observation-builder.mjs — measure a scope and construct the OBSERVATION record (delegation
|
|
2
|
+
// Plan 3, Phase 3). Extracted from dispatch.mjs so `dispatch observe` and the handoff-return rung
|
|
3
|
+
// build the IDENTICAL record through ONE path: the rung is imported by dispatch.mjs for its verb,
|
|
4
|
+
// so a rung importing dispatch.mjs back would be exactly the cycle test/read-graph-purity.test.mjs
|
|
5
|
+
// pins the tools graph against. Every function here moved byte-for-byte; the only new export is
|
|
6
|
+
// the record construction the observe verb previously inlined.
|
|
7
|
+
//
|
|
8
|
+
// Spawns nothing but git READS (through the shared gitLine). No CLI, no writes, no side effects on
|
|
9
|
+
// import. Dependency-free, Node >= 22.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, openSync, closeSync, realpathSync, constants as fsConstants } from 'node:fs';
|
|
12
|
+
import { resolve, sep } from 'node:path';
|
|
13
|
+
import {
|
|
14
|
+
DELEGATION_SCHEMA_VERSION, computeNumerator, evaluateObservationEligibility,
|
|
15
|
+
} from './dispatch-record.mjs';
|
|
16
|
+
import { lstatNoFollowRead, describeNonRegular } from './fs-read-nofollow.mjs';
|
|
17
|
+
import { gitLine } from './flow-store-read.mjs';
|
|
18
|
+
import { lexicalRepoRelative } from './repo-lex.mjs';
|
|
19
|
+
|
|
20
|
+
// The solo baseline counts each scope object's POST-IMAGE — the bytes on disk after the
|
|
21
|
+
// construction — which is exactly the `new` numerator rule (D6). No exec diff kind enumerates
|
|
22
|
+
// ranges, so a measured observation can never claim a partial object.
|
|
23
|
+
const SOLO_COMPONENT_KIND = 'new';
|
|
24
|
+
|
|
25
|
+
// The scope's anchor is the git TOP-LEVEL, never the caller's cwd: a recorded scope must name the
|
|
26
|
+
// same objects whoever runs the tool from wherever. `null` outside a work tree — a repo-relative
|
|
27
|
+
// domain with no repository has nothing to be relative TO, and falling back to cwd would be a
|
|
28
|
+
// second, incompatible semantics for the same field.
|
|
29
|
+
export const resolveRepoRoot = (cwd) => {
|
|
30
|
+
if (gitLine(['rev-parse', '--is-inside-work-tree'], cwd) !== 'true') return null;
|
|
31
|
+
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
32
|
+
return top === null ? null : realpathSync(top);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// The read is no-follow on the LEAF (a symlinked leaf is already refused by name above; O_NOFOLLOW
|
|
36
|
+
// makes a swap between the classification and the read fail loudly rather than counting another
|
|
37
|
+
// object's bytes). Honest limit: classify-then-read is not race-free, and it is not meant to be —
|
|
38
|
+
// the scope is the orchestrator's OWN work tree and the result is a MAGNITUDE, never a store
|
|
39
|
+
// identity, so a pathname race costs a wrong byte count, not a forged record.
|
|
40
|
+
const readObjectBytes = (path) => {
|
|
41
|
+
const fd = openSync(path, (fsConstants.O_RDONLY ?? 0) | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(fd);
|
|
44
|
+
} finally {
|
|
45
|
+
closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// One scope object → one numerator entry. Refuses by NAME on anything it cannot count honestly: a
|
|
50
|
+
// path escaping the repo LEXICALLY, an absent path, a non-regular path (a symlinked leaf included —
|
|
51
|
+
// following one would count another object's bytes under this name), and a path whose REAL location
|
|
52
|
+
// is outside the repository. The last one is the case the lexical rule alone cannot see: it rejects
|
|
53
|
+
// `../x` while accepting `link/x`, where `link` is an ancestor symlink pointing out of the tree.
|
|
54
|
+
// The identity is the CANONICAL repo-relative path taken from the verified real path — not a content
|
|
55
|
+
// hash. The solo domain has no rename lineage for a content id to protect, and a content id would
|
|
56
|
+
// let one object read between two measurements look like TWO objects instead of refusing as the
|
|
57
|
+
// producer contradiction it is ("one identity, one size"). Two equal-byte files at different paths
|
|
58
|
+
// are two objects and count twice; one path reached twice (a second listing, an in-repo ancestor
|
|
59
|
+
// symlink) is one object and counts once.
|
|
60
|
+
export const measureObject = (root, rel) => {
|
|
61
|
+
const lexical = lexicalRepoRelative(rel);
|
|
62
|
+
if (!lexical.ok) return { ok: false, reason: `scope path "${rel}": ${lexical.reason}` };
|
|
63
|
+
const path = resolve(root, rel);
|
|
64
|
+
const stat = lstatNoFollowRead(path);
|
|
65
|
+
if (stat === null) return { ok: false, reason: `scope path "${rel}" does not exist — an observation counts objects that are actually there (fail closed)` };
|
|
66
|
+
if (!stat.isFile()) return { ok: false, reason: `scope path "${rel}" is a ${describeNonRegular(stat)}, not a regular file — the scope names repository objects (fail closed)` };
|
|
67
|
+
const real = realpathSync(path);
|
|
68
|
+
if (!real.startsWith(`${root}${sep}`)) {
|
|
69
|
+
return { ok: false, reason: `scope path "${rel}" resolves to ${real}, which leaves the repository at ${root} — an ancestor symlink is not a way out of the scope domain (fail closed)` };
|
|
70
|
+
}
|
|
71
|
+
const canonical = real.slice(root.length + 1);
|
|
72
|
+
const bytes = readObjectBytes(path);
|
|
73
|
+
return { ok: true, entry: { kind: SOLO_COMPONENT_KIND, path: canonical, objectId: canonical, postImageBytes: bytes.length } };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// One repo-relative path per scope entry, in the order given. The measured CANONICAL paths become
|
|
77
|
+
// the record's `scope` as a canonical JSON array, so what was measured and what is written down are
|
|
78
|
+
// the same statement — and a path carrying a space says so unambiguously.
|
|
79
|
+
export const measureScope = (root, paths) => {
|
|
80
|
+
const entries = [];
|
|
81
|
+
for (const rel of paths) {
|
|
82
|
+
const measured = measureObject(root, rel);
|
|
83
|
+
if (!measured.ok) return measured;
|
|
84
|
+
entries.push(measured.entry);
|
|
85
|
+
}
|
|
86
|
+
const numerator = computeNumerator(entries);
|
|
87
|
+
return numerator.ok
|
|
88
|
+
? { ...numerator, scope: JSON.stringify(entries.map((e) => e.path)) }
|
|
89
|
+
: { ok: false, reason: numerator.reason };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const ratio = (value) => value.toFixed(3);
|
|
93
|
+
|
|
94
|
+
// L is printed ONLY where the metric is eligible: an ineligible metric has a NAMED reason and no
|
|
95
|
+
// ratio at all, and printing a number beside the name is how a silent zero gets read as a
|
|
96
|
+
// measurement.
|
|
97
|
+
export const formatRatio = (metric) => (metric.eligible
|
|
98
|
+
? `L = ${ratio(metric.numeratorBytes / metric.denominatorBytes)} (${metric.numeratorBytes} B / ${metric.denominatorBytes} B)`
|
|
99
|
+
: `L = n/a — INELIGIBLE (${metric.ineligibleReason})`);
|
|
100
|
+
|
|
101
|
+
// The ONE observation-record construction: the eligibility is evaluated here, over the same two
|
|
102
|
+
// numbers the record carries, so a record and its own eligibility can never disagree at the door.
|
|
103
|
+
export const buildObservationRecord = ({ waveId, stepClass, measured, provenance, denominatorBytes, planId, phase, timestamp }) => {
|
|
104
|
+
const eligibility = evaluateObservationEligibility({ numeratorBytes: measured.numeratorBytes, denominatorBytes });
|
|
105
|
+
return {
|
|
106
|
+
schema: DELEGATION_SCHEMA_VERSION,
|
|
107
|
+
kind: 'observation',
|
|
108
|
+
waveId,
|
|
109
|
+
stepClass,
|
|
110
|
+
scope: measured.scope,
|
|
111
|
+
metric: {
|
|
112
|
+
numeratorBytes: measured.numeratorBytes,
|
|
113
|
+
denominatorBytes,
|
|
114
|
+
components: measured.components,
|
|
115
|
+
provenance,
|
|
116
|
+
eligible: eligibility.eligible,
|
|
117
|
+
ineligibleReason: eligibility.ineligibleReason,
|
|
118
|
+
},
|
|
119
|
+
planId,
|
|
120
|
+
phase,
|
|
121
|
+
timestamp,
|
|
122
|
+
};
|
|
123
|
+
};
|
package/tools/path-inventory.mjs
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
import { openSync, fstatSync, readSync, closeSync, opendirSync, realpathSync, lstatSync, constants } from 'node:fs';
|
|
48
48
|
import { resolve, relative, isAbsolute, sep, dirname } from 'node:path';
|
|
49
|
-
import {
|
|
49
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
50
50
|
|
|
51
51
|
import {
|
|
52
52
|
UsageError,
|
|
@@ -512,5 +512,4 @@ const emitResult = (r) => {
|
|
|
512
512
|
process.exitCode = r.code;
|
|
513
513
|
};
|
|
514
514
|
|
|
515
|
-
|
|
516
|
-
if (isDirectRun) emitResult(main(process.argv.slice(2)));
|
|
515
|
+
if (isDirectRun(import.meta.url)) emitResult(main(process.argv.slice(2)));
|
package/tools/procedures.mjs
CHANGED
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { join, dirname } from 'node:path';
|
|
23
|
-
import {
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
24
|
import { detectBackends, wrapperCmdFor, wrapperContractFor } from './detect-backends.mjs';
|
|
25
25
|
// The host-level bridge-settings registry (manifest-as-source) + its allowed-value labels. READ-ONLY
|
|
26
26
|
// core only — never the writer — so this read-only advisor never imports the atomic-write core.
|
|
27
27
|
import { loadRegistry, allowedLabel } from './bridge-settings-read.mjs';
|
|
28
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
28
29
|
import { ACTIVITIES, resolveActivityRecipe, planRecipe } from './recipes.mjs';
|
|
29
30
|
import { resolveEngineDir, readEngineFragment, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
30
31
|
// The plan-in-flight detector (AD-038) — imported from the plan-files.mjs LEAF (read-only fs by
|
|
@@ -586,8 +587,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
586
587
|
}
|
|
587
588
|
};
|
|
588
589
|
|
|
589
|
-
|
|
590
|
-
if (isDirectRun) {
|
|
590
|
+
if (isDirectRun(import.meta.url)) {
|
|
591
591
|
const r = main(process.argv.slice(2));
|
|
592
592
|
if (r.stdout) console.log(r.stdout);
|
|
593
593
|
if (r.stderr) console.error(r.stderr);
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
// Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
22
22
|
|
|
23
23
|
import { join, dirname } from 'node:path';
|
|
24
|
-
import { pathToFileURL } from 'node:url';
|
|
25
24
|
import { createHash } from 'node:crypto';
|
|
26
25
|
import { resolveReceiptsPath } from './core-evidence.mjs';
|
|
26
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
27
27
|
import { SAFE_NONCE_RE, findingManifestBasename, decodeFindingManifest } from './flow-record.mjs';
|
|
28
28
|
import { readFileBytesNoFollow } from './flow-store-read.mjs';
|
|
29
29
|
|
|
@@ -254,8 +254,7 @@ export const main = async (argv, ctx = {}) => {
|
|
|
254
254
|
}
|
|
255
255
|
};
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
if (isDirectRun) {
|
|
257
|
+
if (isDirectRun(import.meta.url)) {
|
|
259
258
|
main(process.argv.slice(2)).then((r) => {
|
|
260
259
|
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
261
260
|
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
package/tools/recipes.mjs
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
|
-
import { pathToFileURL } from 'node:url';
|
|
20
19
|
// The host-level bridge-settings snapshot (fact-only, best-effort). READ-ONLY core only — never the
|
|
21
20
|
// writer — so this read-only advisor never pulls in the atomic-write core.
|
|
22
21
|
import { settingsSnapshot, DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
22
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
23
23
|
import {
|
|
24
24
|
detectBackends,
|
|
25
25
|
wrapperCmdFor,
|
|
@@ -596,11 +596,10 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
|
|
|
596
596
|
return 0;
|
|
597
597
|
};
|
|
598
598
|
|
|
599
|
-
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
600
599
|
// Natural exit via process.exitCode — never process.exit inside the async main (it would drop buffered
|
|
601
600
|
// stdio writes on piped stderr), and never a TOP-LEVEL await here: orchestration-config.mjs statically
|
|
602
601
|
// imports this module, so awaiting the dynamic import during our own evaluation would deadlock the cycle.
|
|
603
|
-
if (isDirectRun) {
|
|
602
|
+
if (isDirectRun(import.meta.url)) {
|
|
604
603
|
main(process.argv.slice(2)).then(
|
|
605
604
|
(code) => {
|
|
606
605
|
process.exitCode = code ?? 0;
|
|
@@ -35,7 +35,7 @@ import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
|
|
|
35
35
|
import { createHash } from 'node:crypto';
|
|
36
36
|
import { homedir } from 'node:os';
|
|
37
37
|
import { dirname, join, resolve } from 'node:path';
|
|
38
|
-
import { fileURLToPath
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
39
|
import {
|
|
40
40
|
preflightVelocityProfile,
|
|
41
41
|
planVelocityProfile,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
|
|
52
52
|
import { deriveDoctorPlan } from './autonomy-doctor.mjs';
|
|
53
53
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
54
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
54
55
|
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
55
56
|
import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
56
57
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
@@ -1534,8 +1535,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
1534
1535
|
}
|
|
1535
1536
|
};
|
|
1536
1537
|
|
|
1537
|
-
|
|
1538
|
-
if (isDirectRun) {
|
|
1538
|
+
if (isDirectRun(import.meta.url)) {
|
|
1539
1539
|
const r = main(process.argv.slice(2));
|
|
1540
1540
|
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
1541
1541
|
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
package/tools/release-scan.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
14
14
|
import { join, extname, basename, resolve } from 'node:path';
|
|
15
|
-
import {
|
|
15
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
16
16
|
|
|
17
17
|
export const ALLOWLIST = [
|
|
18
18
|
'codex-cli-bridge',
|
|
@@ -274,5 +274,4 @@ export const main = (argv) => {
|
|
|
274
274
|
return reportOnly ? 0 : 1;
|
|
275
275
|
};
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
if (isDirectRun) process.exitCode = main(process.argv.slice(2));
|
|
277
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
package/tools/repo-search.mjs
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
import { openSync, fstatSync, readSync, closeSync, opendirSync, realpathSync, lstatSync, constants } from 'node:fs';
|
|
51
51
|
import { createHash } from 'node:crypto';
|
|
52
52
|
import { join, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
53
|
-
import {
|
|
53
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
54
54
|
|
|
55
55
|
export const EXIT_OK = 0;
|
|
56
56
|
export const EXIT_ERROR = 1;
|
|
@@ -590,5 +590,4 @@ const emitResult = (r) => {
|
|
|
590
590
|
process.exitCode = r.code;
|
|
591
591
|
};
|
|
592
592
|
|
|
593
|
-
|
|
594
|
-
if (isDirectRun) emitResult(main(process.argv.slice(2)));
|
|
593
|
+
if (isDirectRun(import.meta.url)) emitResult(main(process.argv.slice(2)));
|
package/tools/review-state.mjs
CHANGED
|
@@ -90,10 +90,11 @@
|
|
|
90
90
|
|
|
91
91
|
import { lstatSync, readFileSync } from 'node:fs';
|
|
92
92
|
import { join, dirname } from 'node:path';
|
|
93
|
-
import {
|
|
93
|
+
import { fileURLToPath } from 'node:url';
|
|
94
94
|
import { spawnSync } from 'node:child_process';
|
|
95
95
|
import { createHash } from 'node:crypto';
|
|
96
96
|
import { detectBackends, READY } from './detect-backends.mjs';
|
|
97
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
97
98
|
import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe } from './recipes.mjs';
|
|
98
99
|
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
99
100
|
import { resolveFlowStorePath, readFlowStore, deriveFlowOwner, readPlanFrontmatterId } from './flow-store.mjs';
|
|
@@ -819,8 +820,7 @@ const emitResult = (r) => {
|
|
|
819
820
|
process.exitCode = r.code;
|
|
820
821
|
};
|
|
821
822
|
|
|
822
|
-
|
|
823
|
-
if (isDirectRun) {
|
|
823
|
+
if (isDirectRun(import.meta.url)) {
|
|
824
824
|
const argv = process.argv.slice(2);
|
|
825
825
|
if (argv.includes('--await')) mainAwait(argv).then(emitResult);
|
|
826
826
|
else emitResult(main(argv));
|
package/tools/run-gates.mjs
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
import { readFileSync, lstatSync, unlinkSync } from 'node:fs';
|
|
33
33
|
import { join } from 'node:path';
|
|
34
34
|
import { spawnSync } from 'node:child_process';
|
|
35
|
-
import { pathToFileURL } from 'node:url';
|
|
36
35
|
import { createHash, randomUUID } from 'node:crypto';
|
|
36
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
37
37
|
import { computeTreeFingerprint } from './review-state.mjs';
|
|
38
38
|
import { CONFIG_REL, loadConfig } from './orchestration-config.mjs';
|
|
39
39
|
// The D3(a) final receipt rides the core-evidence SOLE WRITER (the sole-writer boundary — this
|
|
@@ -942,5 +942,4 @@ export const runCli = (argv, deps = {}) => {
|
|
|
942
942
|
}
|
|
943
943
|
};
|
|
944
944
|
|
|
945
|
-
|
|
946
|
-
if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
|
|
945
|
+
if (isDirectRun(import.meta.url)) process.exitCode = runCli(process.argv.slice(2));
|
package/tools/sandbox-masks.mjs
CHANGED
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
|
|
37
37
|
import { lstatSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
38
38
|
import { join, resolve, dirname } from 'node:path';
|
|
39
|
-
import { fileURLToPath
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
40
|
import { spawnSync } from 'node:child_process';
|
|
41
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
41
42
|
import { fail } from './orchestration-config.mjs';
|
|
42
43
|
import { isNeverCommittableStat, shellQuoteArg } from './review-state.mjs';
|
|
43
44
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
@@ -366,5 +367,4 @@ const emitResult = (r) => {
|
|
|
366
367
|
process.exitCode = r.code;
|
|
367
368
|
};
|
|
368
369
|
|
|
369
|
-
|
|
370
|
-
if (isDirectRun) emitResult(main(process.argv.slice(2)));
|
|
370
|
+
if (isDirectRun(import.meta.url)) emitResult(main(process.argv.slice(2)));
|