@dougharness/cli 1.4.0 → 1.6.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/dist/ablate.js +2 -2
- package/dist/ablate.js.map +1 -1
- package/dist/generate/agents.js +4 -0
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +1 -1
- package/dist/generate/claude-md.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/coder.md +2 -0
- package/node_modules/@dougharness/flow/agents/implementer.md +2 -0
- package/node_modules/@dougharness/flow/agents/planner.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +2 -0
- package/node_modules/@dougharness/flow/agents/tester.md +2 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +9 -9
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +54 -5
- package/node_modules/@dougharness/flow/lib/code-index.mjs +2 -2
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/decisions.mjs +3 -5
- package/node_modules/@dougharness/flow/lib/learn.mjs +105 -57
- package/node_modules/@dougharness/flow/lib/memory.mjs +1418 -57
- package/node_modules/@dougharness/flow/lib/plan.mjs +40 -0
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +178 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +43 -15
- package/node_modules/@dougharness/flow/scripts/learn.mjs +4 -2
- package/node_modules/@dougharness/flow/scripts/memory.mjs +870 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +4 -0
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +9 -8
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -3
- package/node_modules/@dougharness/flow/workflows/card-queen.js +54 -5
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +34 -19
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +9 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +6 -3
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +175 -4
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +33 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +110 -125
- package/node_modules/@dougharness/gates/scripts/verify.mjs +47 -13
- package/package.json +3 -3
|
@@ -91,10 +91,10 @@ import { isAppliedContent } from "../lib/proposals.mjs";
|
|
|
91
91
|
import { gitHead, mergeSnapshot, readDecisions, readPlanSummary, readPlanSummaries, snapshotSection } from "../lib/anchor.mjs";
|
|
92
92
|
import { contextReading, recordAgentContext } from "../lib/agent-context.mjs";
|
|
93
93
|
import { parseTotals, readSummaries, sumTotals, compareTotals, deltaTotals, waiverCovers } from "../lib/test-totals.mjs";
|
|
94
|
-
import { failingTestFilesFrom, nonTestTscPathsFrom, classifyFinalBlock, appendBlockLine, outputTail, redactText } from "../lib/block-ledger.mjs";
|
|
94
|
+
import { failingTestFilesFrom, tscErrorPaths, nonTestTscPathsFrom, classifyFinalBlock, appendBlockLine, outputTail, redactText } from "../lib/block-ledger.mjs";
|
|
95
95
|
import { docsGateDecision } from "../lib/docs-gate.mjs";
|
|
96
|
-
import { gateLockPath, readGateLock, pidIsLive, writeGateLock, takeOverGateLock, resolveHookTimeoutSec, lockStaleByAge, normalizedTimeoutMs, lockWaitBoundMs } from "../lib/gate-lock.mjs";
|
|
97
|
-
import { resolveCommands, computeTree, readRecord, writeRecord as writeTreeRecord, removeRecord as removeTreeRecord, creditable } from "../lib/verified-tree.mjs";
|
|
96
|
+
import { gateLockPath, readGateLock, pidIsLive, writeGateLock, takeOverGateLock, acquireGateLock, resolveHookTimeoutSec, lockStaleByAge, normalizedTimeoutMs, lockWaitBoundMs } from "../lib/gate-lock.mjs";
|
|
97
|
+
import { resolveCommands, computeTree, readRecord, writeRecord as writeTreeRecord, removeRecord as removeTreeRecord, creditable, totalsUsable, exitCreditUsable } from "../lib/verified-tree.mjs";
|
|
98
98
|
|
|
99
99
|
// Only the last this many characters of last_assistant_message are ever scanned for a partial claim
|
|
100
100
|
// (review, scan cost): a structured result lives at the end of a worker's message, and the balanced-span
|
|
@@ -311,14 +311,14 @@ function stripLeadingDotSlash(f) {
|
|
|
311
311
|
// failed command's output names at least one test file, every named file is in the claim, and every
|
|
312
312
|
// claimed file is changed or untracked in the working tree (`changed`, from changedFiles(dir) above);
|
|
313
313
|
// otherwise `{ covered: false, reasonLine }` — one line, no newline, saying why.
|
|
314
|
-
function redByDesignCoverage(claimFiles, commandFailures, changed, dir) {
|
|
314
|
+
function redByDesignCoverage(claimFiles, commandFailures, changed, dir, secrets) {
|
|
315
315
|
const changedSet = new Set(Array.isArray(changed) ? changed : []);
|
|
316
316
|
for (const cf of commandFailures) {
|
|
317
317
|
const bad = nonTestTscPathsFrom(cf.output, dir);
|
|
318
318
|
if (bad.length > 0) {
|
|
319
319
|
return {
|
|
320
320
|
covered: false,
|
|
321
|
-
reasonLine: `The tests_red_by_design claim does not cover this: \`${cf.command}\` reported a type error in ${bad.join(", ")}, which is not a test file.`,
|
|
321
|
+
reasonLine: `The tests_red_by_design claim does not cover this: \`${redactText(cf.command, secrets)}\` reported a type error in ${bad.join(", ")}, which is not a test file.`,
|
|
322
322
|
};
|
|
323
323
|
}
|
|
324
324
|
}
|
|
@@ -327,7 +327,7 @@ function redByDesignCoverage(claimFiles, commandFailures, changed, dir) {
|
|
|
327
327
|
if (noFile.length > 0) {
|
|
328
328
|
return {
|
|
329
329
|
covered: false,
|
|
330
|
-
reasonLine: `The tests_red_by_design claim does not cover this: ${noFile.map((c) => `\`${c.command}\``).join(", ")} named no test file in its output.`,
|
|
330
|
+
reasonLine: `The tests_red_by_design claim does not cover this: ${noFile.map((c) => `\`${redactText(c.command, secrets)}\``).join(", ")} named no test file in its output.`,
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
const allNamed = [...new Set(namedByCommand.flatMap((c) => c.files))];
|
|
@@ -510,7 +510,7 @@ function agentMadeNoChanges(dir, cfg, sessionId, subagent) {
|
|
|
510
510
|
// card stop-gate-worktree-agent-fingerprint: the start record is a fingerprint of the main checkout (the trace
|
|
511
511
|
// hook runs there), so for a worktree verification dir it is compared like with like instead: unchanged only if
|
|
512
512
|
// the record exists, every verification dir has no changed files, and every one's HEAD carries no commit of its
|
|
513
|
-
// own: it equals or is an ancestor of the record's HEAD, or of the
|
|
513
|
+
// own: it equals or is an ancestor of the record's HEAD, or of the origin/HEAD sha the record holds, taken at start, never the live ref (Claude
|
|
514
514
|
// Code branches a fresh isolation worktree from the remote default branch, which local main may be ahead of or
|
|
515
515
|
// behind). Any git failure reads as changed. A verification set that mixes a worktree with `dir` reads as
|
|
516
516
|
// changed. A main-checkout agent keeps the fingerprint comparison.
|
|
@@ -539,12 +539,7 @@ function treeUnchangedSinceStart(dir, cfg, sessionId, subagent, input, currentFi
|
|
|
539
539
|
if (!start || typeof start !== "object" || !start.head) return false;
|
|
540
540
|
const dirs = forced ? [vdir] : verifyDirs(dir, cfg, sessionId, input, subagent);
|
|
541
541
|
if (!dirs.includes(vdir) || dirs.includes(dir)) return false;
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
remoteHead = execFileSync("git", ["rev-parse", "--verify", "-q", "refs/remotes/origin/HEAD^{commit}"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim() || null;
|
|
545
|
-
} catch {
|
|
546
|
-
remoteHead = null;
|
|
547
|
-
}
|
|
542
|
+
const remoteHead = typeof start.originHead === "string" && start.originHead ? start.originHead : null;
|
|
548
543
|
return dirs.every((d) => {
|
|
549
544
|
const h = headAt(d);
|
|
550
545
|
if (!h) return false;
|
|
@@ -565,7 +560,10 @@ function treeUnchangedSinceStart(dir, cfg, sessionId, subagent, input, currentFi
|
|
|
565
560
|
// .claude/worktrees names the main checkout, except <dir>/.doug/.state and anything under it (untracked state,
|
|
566
561
|
// never project code) which names nothing; any other path names nothing. The set is the cwd's worktree plus
|
|
567
562
|
// every tree its trace paths name (a worktree that no longer exists as a directory is skipped). Empty, a main
|
|
568
|
-
// Stop, or any failure resolves to [dir].
|
|
563
|
+
// Stop, or any failure resolves to [dir]. Card queen-seat-stop-gates-worktree: a Bash or Monitor line of the
|
|
564
|
+
// agent (Pre or Post, whatever ok) also names a worktree by any absolute path token in its detail (a seat
|
|
565
|
+
// that edits only through `cd <wt> && ...`); a shell token never names the main checkout.
|
|
566
|
+
// Only the verification target varies per tree: state, trace,
|
|
569
567
|
// gate.lock, the ledger, and evidence stay under `dir`. Never throws.
|
|
570
568
|
function verifyDirs(dir, cfg, sessionId, input, subagent) {
|
|
571
569
|
try {
|
|
@@ -603,7 +601,22 @@ function verifyDirs(dir, cfg, sessionId, input, subagent) {
|
|
|
603
601
|
const file = tracePath(dir, sessionId);
|
|
604
602
|
if (existsSync(file)) {
|
|
605
603
|
for (const l of readTrace(file)) {
|
|
606
|
-
if (l.agent !== subagent.id
|
|
604
|
+
if (l.agent !== subagent.id) continue;
|
|
605
|
+
if (SHELL_TOOLS.has(l.tool) && (l.event === "PreToolUse" || l.event === "PostToolUse")) {
|
|
606
|
+
// card queen-seat-stop-gates-worktree: a shell command names a worktree by an absolute path token;
|
|
607
|
+
// a main-checkout path in it names nothing.
|
|
608
|
+
for (const tok of String(l.detail || "").split(/[\s;&|()<>'"`=]+/)) {
|
|
609
|
+
if (!tok.startsWith("/")) continue;
|
|
610
|
+
// the trace clips a detail at 120 chars with a trailing ellipsis; a clipped token names a worktree
|
|
611
|
+
// only when the name is followed by "/" inside it (else the cut name may be a sibling's prefix)
|
|
612
|
+
const clipped = tok.endsWith("…");
|
|
613
|
+
const t = clipped ? tok.slice(0, -1) : tok;
|
|
614
|
+
const n = wtName(t);
|
|
615
|
+
if (n && (!clipped || t.includes("/" + n + "/"))) addWt(n);
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (l.event !== "PostToolUse" || !CHANGE_TOOLS.has(l.tool) || l.ok === false) continue;
|
|
607
620
|
const name = wtName(l.detail);
|
|
608
621
|
if (name) addWt(name);
|
|
609
622
|
else if (inMain(l.detail)) add(dir);
|
|
@@ -827,22 +840,36 @@ function readLastGreen(dir) {
|
|
|
827
840
|
}
|
|
828
841
|
}
|
|
829
842
|
|
|
843
|
+
// card ratchet-credit-reads-record: one baseline for the count check and both credit paths. In a worktree
|
|
844
|
+
// (vdir !== dir) it is the worktree's own last-green record. On the main checkout (card
|
|
845
|
+
// ratchet-stale-session-baseline) it is the main last-green record when that has a readable entry for the
|
|
846
|
+
// command, else the session-start copy state.testTotals. Returns (command) => baseline object or null. Never throws.
|
|
847
|
+
function countBaselineFor(dir, state, vdir) {
|
|
848
|
+
try {
|
|
849
|
+
const inWorktree = typeof vdir === "string" && vdir !== dir;
|
|
850
|
+
const worktreeBaseline = inWorktree ? readLastGreen(vdir) : null;
|
|
851
|
+
const mainRecord = inWorktree ? null : readLastGreen(dir);
|
|
852
|
+
const fallback = state && state.testTotals && typeof state.testTotals === "object" ? state.testTotals : null;
|
|
853
|
+
return (command) => {
|
|
854
|
+
try {
|
|
855
|
+
if (inWorktree) return worktreeBaseline;
|
|
856
|
+
return mainRecord && readSummaries(mainRecord[command]) ? mainRecord : fallback;
|
|
857
|
+
} catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
} catch {
|
|
862
|
+
return () => null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
830
866
|
// The count check for the commands that ran green by exit code. Returns { problems, parsed }: `problems` are
|
|
831
867
|
// block reasons, `parsed` the per-command summary lists of every run that was counted (for the last-green
|
|
832
868
|
// record). A missing or unparseable summary, or no baseline, is a trace line and no check.
|
|
833
869
|
function countCheck(dir, cfg, state, input, ran, vdir) {
|
|
834
870
|
// card worktree-count-baseline: a worktree run compares against its own earlier green runs, never main's record
|
|
835
871
|
// (a worktree can lack the dist build main has, so its skipped count differs). No record yet means no check.
|
|
836
|
-
const
|
|
837
|
-
// card ratchet-stale-session-baseline: a main-checkout run compares against the main tree's live last-green
|
|
838
|
-
// record too (per command), not the session-start copy; the copy is the fallback when the record has no
|
|
839
|
-
// readable entry for the command, so a deleted record does not remove the check.
|
|
840
|
-
const worktreeBaseline = inWorktree ? readLastGreen(vdir) : null;
|
|
841
|
-
const mainRecord = inWorktree ? null : readLastGreen(dir);
|
|
842
|
-
const baselineFor = (command) => {
|
|
843
|
-
if (inWorktree) return worktreeBaseline;
|
|
844
|
-
return mainRecord && readSummaries(mainRecord[command]) ? mainRecord : state.testTotals;
|
|
845
|
-
};
|
|
872
|
+
const baselineFor = countBaselineFor(dir, state, vdir);
|
|
846
873
|
const problems = [];
|
|
847
874
|
const parsed = {};
|
|
848
875
|
// The note carries its own event name, never the stop event's: a decision-less SubagentStop line would read to
|
|
@@ -856,14 +883,14 @@ function countCheck(dir, cfg, state, input, ran, vdir) {
|
|
|
856
883
|
// logging must never fail the gate
|
|
857
884
|
}
|
|
858
885
|
};
|
|
859
|
-
// Each entry is `{ command, stdout }` (a real run: parsed here) or `{ command, result }` (a credited
|
|
886
|
+
// Each entry is `{ command, stdout }` (a real run: parsed here) or `{ command, result }` (a credited verified-tree
|
|
860
887
|
// run: the parseTotals-shaped result the hook recorded). Both take the identical path below.
|
|
861
888
|
for (const { command, stdout, result } of ran) {
|
|
862
889
|
try {
|
|
863
890
|
const p = result !== undefined ? result : parseTotals(stdout);
|
|
864
891
|
const summaries = p.status === "ok" ? readSummaries(p) : null;
|
|
865
892
|
if (!summaries) {
|
|
866
|
-
note(`count check did not run for \`${command}\`: no test summary in its output`);
|
|
893
|
+
note(`count check did not run for \`${redactText(command, cfg.secrets)}\`: no test summary in its output`);
|
|
867
894
|
continue;
|
|
868
895
|
}
|
|
869
896
|
const baseline = baselineFor(command);
|
|
@@ -871,11 +898,11 @@ function countCheck(dir, cfg, state, input, ran, vdir) {
|
|
|
871
898
|
const baseList = all ? readSummaries(all[command]) : null;
|
|
872
899
|
if (!baseList) {
|
|
873
900
|
parsed[command] = summaries;
|
|
874
|
-
note(`count check did not run for \`${command}\`: no baseline totals for it (${summaries.length} test summar${summaries.length === 1 ? "y" : "ies"} printed: ${summaries.map((s) => `total ${s.total}, skipped ${s.skipped}, todo ${s.todo}`).join("; ")})`);
|
|
901
|
+
note(`count check did not run for \`${redactText(command, cfg.secrets)}\`: no baseline totals for it (${summaries.length} test summar${summaries.length === 1 ? "y" : "ies"} printed: ${summaries.map((s) => `total ${s.total}, skipped ${s.skipped}, todo ${s.todo}`).join("; ")})`);
|
|
875
902
|
continue;
|
|
876
903
|
}
|
|
877
904
|
if (baseList.length !== summaries.length) {
|
|
878
|
-
problems.push(`\`${command}\` printed ${summaries.length} test summar${summaries.length === 1 ? "y" : "ies"}; the baseline has ${baseList.length}. The test count cannot be trusted. A waiver does not cover this; take a new baseline if a package was added or removed.`);
|
|
905
|
+
problems.push(`\`${redactText(command, cfg.secrets)}\` printed ${summaries.length} test summar${summaries.length === 1 ? "y" : "ies"}; the baseline has ${baseList.length}. The test count cannot be trusted. A waiver does not cover this; take a new baseline if a package was added or removed.`);
|
|
879
906
|
continue;
|
|
880
907
|
}
|
|
881
908
|
parsed[command] = summaries;
|
|
@@ -887,10 +914,10 @@ function countCheck(dir, cfg, state, input, ran, vdir) {
|
|
|
887
914
|
if (waiverCovers(waiverSources(dir, state.baselineHead), delta)) continue;
|
|
888
915
|
const lines = found.map((f) => `${f.kind} ${f.kind === "total" ? "dropped" : "rose"} from ${f.baseline} (baseline) to ${f.now} (now)`);
|
|
889
916
|
problems.push(
|
|
890
|
-
`\`${command}\` test count regressed: ${lines.join("; ")}. Deleting or skipping tests does not make a change green. Restore them, or waive it with a line \`test-count-waiver: total=${delta.total} skipped=${delta.skipped} todo=${delta.todo}; <reason>\` in the commit message or ${WAIVER_FILE_RELPATH}.`,
|
|
917
|
+
`\`${redactText(command, cfg.secrets)}\` test count regressed: ${lines.join("; ")}. Deleting or skipping tests does not make a change green. Restore them, or waive it with a line \`test-count-waiver: total=${delta.total} skipped=${delta.skipped} todo=${delta.todo}; <reason>\` in the commit message or ${WAIVER_FILE_RELPATH}.`,
|
|
891
918
|
);
|
|
892
919
|
} catch {
|
|
893
|
-
note(`count check did not run for \`${command}\`: error while checking`);
|
|
920
|
+
note(`count check did not run for \`${redactText(command, cfg.secrets)}\`: error while checking`);
|
|
894
921
|
}
|
|
895
922
|
}
|
|
896
923
|
return { problems, parsed };
|
|
@@ -914,19 +941,6 @@ function writeLastGreen(dir, parsed) {
|
|
|
914
941
|
}
|
|
915
942
|
}
|
|
916
943
|
|
|
917
|
-
// card stop-gate-credits-pre-commit: `git rev-parse HEAD^{tree}`, the tree sha of the current commit — what
|
|
918
|
-
// the pre-commit hook's own record (.doug/.state/pre-commit/last-run.json) compares its `tree` field against.
|
|
919
|
-
// Null on any failure (no git, no HEAD yet, ...): a reader must never throw.
|
|
920
|
-
function headTreeSha(dir) {
|
|
921
|
-
try {
|
|
922
|
-
const out = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
|
|
923
|
-
const sha = out.trim();
|
|
924
|
-
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
925
|
-
} catch {
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
944
|
// card ratchet-pre-commit-credit: a recorded parseTotals result, normalised, or null when it is not readable.
|
|
931
945
|
function readableRecordedTotals(t) {
|
|
932
946
|
try {
|
|
@@ -939,11 +953,11 @@ function readableRecordedTotals(t) {
|
|
|
939
953
|
}
|
|
940
954
|
|
|
941
955
|
// card verified-tree-record: a verified-tree record (lib/verified-tree.mjs) of `tree` (the working tree now), same
|
|
942
|
-
// Node and command set, credits the commands instead of re-running them.
|
|
956
|
+
// Node and command set, credits the commands instead of re-running them. Returns
|
|
943
957
|
// one `{ name, command, ok: true, credited: "verified-tree", recorded? }` per resolveCommands entry in config
|
|
944
958
|
// order. A command with a baseline in state.testTotals and no readable recorded totals returns null (the commands
|
|
945
|
-
// run)
|
|
946
|
-
function treeCredit(dir, cfg, state, tree) {
|
|
959
|
+
// run). Never throws: any failure is null, and null runs the commands.
|
|
960
|
+
function treeCredit(dir, cfg, state, tree, mainDir) {
|
|
947
961
|
try {
|
|
948
962
|
if (!tree) return null;
|
|
949
963
|
const record = readRecord(dir);
|
|
@@ -951,11 +965,14 @@ function treeCredit(dir, cfg, state, tree) {
|
|
|
951
965
|
const configured = resolveCommands(cfg);
|
|
952
966
|
if (configured.length === 0) return null;
|
|
953
967
|
if (!creditable(dir, cfg, record, tree, configured.map((c) => c.command)).ok) return null;
|
|
954
|
-
|
|
968
|
+
if (!exitCreditUsable(record, dir)) return null;
|
|
969
|
+
// dir is the verify dir (a worktree for a worktree agent); mainDir the main checkout, as countCheck's dir.
|
|
970
|
+
const baselineFor = countBaselineFor(mainDir ?? dir, state, dir);
|
|
955
971
|
const out = [];
|
|
956
972
|
for (const { name, command } of configured) {
|
|
957
|
-
const recorded = readableRecordedTotals(record.totals[command]);
|
|
958
|
-
const
|
|
973
|
+
const recorded = totalsUsable(record, dir) ? readableRecordedTotals(record.totals[command]) : null;
|
|
974
|
+
const base = baselineFor(command);
|
|
975
|
+
const hasBaseline = base ? readSummaries(base[command]) !== null : false;
|
|
959
976
|
if (hasBaseline && (!recorded || recorded.status === "none")) return null;
|
|
960
977
|
out.push({ name, command, ok: true, credited: "verified-tree", ...(recorded ? { recorded } : {}) });
|
|
961
978
|
}
|
|
@@ -965,57 +982,6 @@ function treeCredit(dir, cfg, state, tree) {
|
|
|
965
982
|
}
|
|
966
983
|
}
|
|
967
984
|
|
|
968
|
-
// card stop-gate-credits-pre-commit: the pre-commit hook (.githooks/pre-commit) writes a machine-readable
|
|
969
|
-
// record of its own run to .doug/.state/pre-commit/last-run.json (version, tree, ok, at, commands: [{ name,
|
|
970
|
-
// command, exit, durationMs, totals? }], totals being the hook's parseTotals result for that command; a command with a baseline in state.testTotals is credited only if its totals are readable (a list of summaries, or an old ok or conflict shape), else the commands run). When the working tree has no changes beyond HEAD (nothing left in `changed`
|
|
971
|
-
// once stopGate.ignoreChangedPaths paths are removed) and that record is a passing run, against HEAD's own
|
|
972
|
-
// tree, of exactly today's configured commands in order, the Stop gate credits it instead of re-running the
|
|
973
|
-
// same commands a second time — returning `{ name, command, ok: true, credited: "pre-commit" }` per command,
|
|
974
|
-
// same length and order as resolveCommands(cfg). Returns null (run as today) on a missing, unparsable,
|
|
975
|
-
// wrong-shape, failing, stale-tree, or mismatched-commands record, or a dirty tree — fail closed to running,
|
|
976
|
-
// never to passing. A reader must never throw: every path here is wrapped, so a corrupt record never crashes
|
|
977
|
-
// the hook (`runHook` fails open on a throw, which here would mean passing without running — forbidden).
|
|
978
|
-
function preCommitCredit(dir, cfg, changed, state) {
|
|
979
|
-
try {
|
|
980
|
-
if (!Array.isArray(changed)) return null;
|
|
981
|
-
const ignore = cfg.stopGate.ignoreChangedPaths || [];
|
|
982
|
-
const remaining = changed.filter((f) => !matchAny(ignore, f));
|
|
983
|
-
if (remaining.length > 0) return null;
|
|
984
|
-
const raw = readFileSync(join(dir, ".doug/.state/pre-commit/last-run.json"), "utf8");
|
|
985
|
-
const record = JSON.parse(raw);
|
|
986
|
-
if (!record || typeof record !== "object") return null;
|
|
987
|
-
if (record.version !== 1) return null;
|
|
988
|
-
if (record.ok !== true) return null;
|
|
989
|
-
if (typeof record.tree !== "string") return null;
|
|
990
|
-
if (!Array.isArray(record.commands)) return null;
|
|
991
|
-
const head = headTreeSha(dir);
|
|
992
|
-
if (!head || record.tree !== head) return null;
|
|
993
|
-
for (const c of record.commands) {
|
|
994
|
-
if (!c || typeof c !== "object" || c.exit !== 0) return null;
|
|
995
|
-
}
|
|
996
|
-
const configured = resolveCommands(cfg);
|
|
997
|
-
if (record.commands.length !== configured.length) return null;
|
|
998
|
-
for (let i = 0; i < configured.length; i++) {
|
|
999
|
-
if (!record.commands[i] || record.commands[i].command !== configured[i].command) return null;
|
|
1000
|
-
}
|
|
1001
|
-
// card ratchet-pre-commit-credit: a command with a baseline in state.testTotals is credited only when the
|
|
1002
|
-
// record carries readable totals for it (a list of valid summaries, or an old ok or conflict shape);
|
|
1003
|
-
// otherwise null and the commands really run. `recorded` is the parseTotals-shaped result for the count check.
|
|
1004
|
-
const all = state && state.testTotals && typeof state.testTotals === "object" ? state.testTotals : null;
|
|
1005
|
-
const out = [];
|
|
1006
|
-
for (let i = 0; i < configured.length; i++) {
|
|
1007
|
-
const { name, command } = configured[i];
|
|
1008
|
-
const recorded = readableRecordedTotals(record.commands[i].totals);
|
|
1009
|
-
const hasBaseline = all ? readSummaries(all[command]) !== null : false;
|
|
1010
|
-
if (hasBaseline && (!recorded || recorded.status === "none")) return null;
|
|
1011
|
-
out.push({ name, command, ok: true, credited: "pre-commit", ...(recorded ? { recorded } : {}) });
|
|
1012
|
-
}
|
|
1013
|
-
return out;
|
|
1014
|
-
} catch {
|
|
1015
|
-
return null;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
985
|
// card stop-gate-process-storm, case 2 (second tester pass, reviewer-found gap): a verified-unchanged skip
|
|
1020
986
|
// is a green gate in every way that matters, not a bare allow — it resets the same counters an ordinary
|
|
1021
987
|
// green gate resets, and on the main Stop it still surfaces the context-window notice, exactly like the
|
|
@@ -1721,8 +1687,7 @@ runHook("stop-gate", async (input) => {
|
|
|
1721
1687
|
currentFingerprint &&
|
|
1722
1688
|
(sameTree(rec.fingerprint, currentFingerprint) || (rec.scope === scopeNow && (redViaNoTestPaths = onlyNoTestPathsChanged(vdir, cfg, rec.fingerprint, vchanged)))) &&
|
|
1723
1689
|
!(subagent && (redByDesignClaim(input.last_assistant_message) ?? redByDesignClaim(lastHandbackMessage(input.agent_transcript_path)))) &&
|
|
1724
|
-
!
|
|
1725
|
-
!treeCredit(vdir, cfg, state, computeTree(vdir))
|
|
1690
|
+
!treeCredit(vdir, cfg, state, computeTree(vdir), dir)
|
|
1726
1691
|
) {
|
|
1727
1692
|
lastRed = rec;
|
|
1728
1693
|
}
|
|
@@ -1765,6 +1730,7 @@ runHook("stop-gate", async (input) => {
|
|
|
1765
1730
|
failures: Array.isArray(lastRed.failures) ? lastRed.failures : [],
|
|
1766
1731
|
cwd: typeof lastRed.cwd === "string" ? lastRed.cwd : null,
|
|
1767
1732
|
tree: typeof lastRed.tree === "string" ? lastRed.tree : null,
|
|
1733
|
+
files: Array.isArray(lastRed.files) ? lastRed.files : [],
|
|
1768
1734
|
};
|
|
1769
1735
|
logBlock(reason, reuseClass, reuseRecord);
|
|
1770
1736
|
traceMainBlock(reuseRecord);
|
|
@@ -1804,13 +1770,17 @@ runHook("stop-gate", async (input) => {
|
|
|
1804
1770
|
lockOwned = false;
|
|
1805
1771
|
} else {
|
|
1806
1772
|
const lock = readGateLock(dir);
|
|
1807
|
-
|
|
1773
|
+
// Card gate-lock-takeover-exclusive: a stale-on-arrival lock is acquired exclusively; "busy" (a rival just
|
|
1774
|
+
// took it) enters the wait branch below like a live holder, "unwritable" runs unlocked.
|
|
1775
|
+
let arrivalWait = !(!lock || !pidIsLive(lock.pid) || lockStaleByAge(lock, hookTimeoutMs));
|
|
1776
|
+
if (!arrivalWait) {
|
|
1808
1777
|
// Stale (a dead pid, a lock file that fails to parse, or one older than the hook-timeout budget —
|
|
1809
|
-
// card stop-gate-budget-under-hook-timeout: no legitimate holder can still be running that long)
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
}
|
|
1778
|
+
// card stop-gate-budget-under-hook-timeout: no legitimate holder can still be running that long).
|
|
1779
|
+
const got = acquireGateLock(dir, input.session_id, hookTimeoutMs);
|
|
1780
|
+
if (got === "acquired") lockOwned = true;
|
|
1781
|
+
else if (got === "busy") arrivalWait = true;
|
|
1782
|
+
}
|
|
1783
|
+
if (arrivalWait) {
|
|
1814
1784
|
// The bound scales with the number of configured commands (reviewer-found gap): a holder legitimately
|
|
1815
1785
|
// runs each of resolveCommands(cfg) with its own timeoutMs budget, one after another, so a flat
|
|
1816
1786
|
// timeoutMs would give up on — and duplicate — a holder still legitimately partway through its own
|
|
@@ -1825,11 +1795,22 @@ runHook("stop-gate", async (input) => {
|
|
|
1825
1795
|
// taking the lock over, since taking over and running past the platform's own timeout would just get
|
|
1826
1796
|
// this gate killed mid-command too (an ungated stop, its block message discarded).
|
|
1827
1797
|
let refusedLock = null;
|
|
1798
|
+
let acquired = false;
|
|
1828
1799
|
while (true) {
|
|
1829
1800
|
const live = readGateLock(dir);
|
|
1830
|
-
if (!live || !pidIsLive(live.pid) || lockStaleByAge(live, hookTimeoutMs))
|
|
1801
|
+
if (!live || !pidIsLive(live.pid) || lockStaleByAge(live, hookTimeoutMs)) {
|
|
1802
|
+
// Released, died, or stale by age. Card gate-load-timeouts: take it exclusively, so two waiters
|
|
1803
|
+
// leaving this loop together do not both run their suites (ledger blocks 425 and 426). A lost race
|
|
1804
|
+
// goes back to waiting, still bounded by the two deadlines below.
|
|
1805
|
+
const got = acquireGateLock(dir, input.session_id, hookTimeoutMs);
|
|
1806
|
+
if (got !== "busy") {
|
|
1807
|
+
acquired = got === "acquired";
|
|
1808
|
+
lockOwned = acquired;
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1831
1812
|
if (Date.now() >= budgetDeadline) {
|
|
1832
|
-
refusedLock = live;
|
|
1813
|
+
refusedLock = readGateLock(dir) || live || {};
|
|
1833
1814
|
break;
|
|
1834
1815
|
}
|
|
1835
1816
|
if (Date.now() >= waitDeadline) break; // bounded: never hang the gate on someone else's lock
|
|
@@ -1877,8 +1858,10 @@ runHook("stop-gate", async (input) => {
|
|
|
1877
1858
|
return verifiedUnchangedAllow(dir, cfg, state, input, subagent, deferralCapNote);
|
|
1878
1859
|
}
|
|
1879
1860
|
// Still different, or the wait hit its bound with the lock still held: take it over and run.
|
|
1880
|
-
|
|
1881
|
-
|
|
1861
|
+
if (!acquired) {
|
|
1862
|
+
takeOverGateLock(dir, input.session_id);
|
|
1863
|
+
lockOwned = true;
|
|
1864
|
+
}
|
|
1882
1865
|
}
|
|
1883
1866
|
}
|
|
1884
1867
|
}
|
|
@@ -1957,20 +1940,21 @@ runHook("stop-gate", async (input) => {
|
|
|
1957
1940
|
const r = failedRuns[i] || {};
|
|
1958
1941
|
return { command: redactText(cf.command, cfg.secrets), exit: typeof r.status === "number" ? r.status : null, timedOut: !!r.timedOut, tail: outputTail(cf.output, cfg.secrets) };
|
|
1959
1942
|
});
|
|
1960
|
-
|
|
1943
|
+
// card memory-failures-wiring: the sorted, unique, repo-relative paths the failed commands' output names.
|
|
1944
|
+
const files = [...new Set(commandFailures.flatMap((cf) => [...failingTestFilesFrom(cf.output, vdir), ...tscErrorPaths(cf.output, vdir)]))].sort();
|
|
1945
|
+
return { failures, cwd: vdir, tree: treeBefore ?? null, files };
|
|
1961
1946
|
} catch {
|
|
1962
|
-
return { failures: [], cwd: vdir, tree: null }; // the line is written without the failures; the decision is unchanged
|
|
1947
|
+
return { failures: [], cwd: vdir, tree: null, files: [] }; // the line is written without the failures; the decision is unchanged
|
|
1963
1948
|
}
|
|
1964
1949
|
};
|
|
1965
1950
|
const failedRuns = []; // parallel to commandFailures: { status, timedOut } of each failed run (card block-ledger-failing-output)
|
|
1966
|
-
const ranGreen = []; // count check input: { command, stdout } of each command that really ran and exited 0, plus { command, result } of each credited
|
|
1967
|
-
// card stop-gate-credits-pre-commit: a passing pre-commit record for this exact tree and command set
|
|
1968
|
-
// stands in for actually running them — see preCommitCredit above. Every other check below (evidence,
|
|
1969
|
-
// red-by-design, state.verified, the block ledger) runs exactly as it would after a real run.
|
|
1951
|
+
const ranGreen = []; // count check input: { command, stdout } of each command that really ran and exited 0, plus { command, result } of each credited verified-tree command whose recorded totals are readable
|
|
1970
1952
|
// card verified-tree-record: treeBefore is the working tree immediately before the first command; a record of
|
|
1971
|
-
// it (any writer: Stop, verify.mjs, land, pre-commit)
|
|
1953
|
+
// it (any writer: Stop, verify.mjs, land, pre-commit) for this exact tree and command set stands in for
|
|
1954
|
+
// actually running them. Every other check below (evidence, red-by-design, state.verified, the block ledger)
|
|
1955
|
+
// runs exactly as it would after a real run.
|
|
1972
1956
|
const treeBefore = skipVerification ? null : computeTree(vdir);
|
|
1973
|
-
const credited = skipVerification ? null :
|
|
1957
|
+
const credited = skipVerification ? null : treeCredit(vdir, cfg, state, treeBefore, dir);
|
|
1974
1958
|
// card gate-docs-skip-unless-named: a targeted run is never a full run (no record, no ratchet, no state.verified).
|
|
1975
1959
|
const targeted = !credited && targetedFiles ? targetedFiles : null;
|
|
1976
1960
|
if (!skipVerification && credited) {
|
|
@@ -1990,7 +1974,7 @@ runHook("stop-gate", async (input) => {
|
|
|
1990
1974
|
if (remaining <= 0) {
|
|
1991
1975
|
results.push({ name, command, ok: false, skipped: "budget" });
|
|
1992
1976
|
problems.push(
|
|
1993
|
-
`\`${command}\` not run: the stop gate's budget (hook timeout ${hookTimeoutSec} s) was used up before it could start.`,
|
|
1977
|
+
`\`${redactText(command, cfg.secrets)}\` not run: the stop gate's budget (hook timeout ${hookTimeoutSec} s) was used up before it could start.`,
|
|
1994
1978
|
);
|
|
1995
1979
|
continue;
|
|
1996
1980
|
}
|
|
@@ -2008,7 +1992,7 @@ runHook("stop-gate", async (input) => {
|
|
|
2008
1992
|
const output = r.stderr + "\n" + r.stdout;
|
|
2009
1993
|
// card gate-output-names-failures: the block reason leads with the failing test names (R1/R2);
|
|
2010
1994
|
// commandFailures below keeps the untruncated output for the tests_red_by_design coverage check.
|
|
2011
|
-
problems.push(`\`${command}\` failed (${why}):\n${failureSummary(output, 40)}`);
|
|
1995
|
+
problems.push(`\`${redactText(command, cfg.secrets)}\` failed (${why}):\n${redactText(failureSummary(output, 40), cfg.secrets)}`);
|
|
2012
1996
|
commandFailures.push({ command, output });
|
|
2013
1997
|
failedRuns.push({ status: r.status, timedOut: r.timedOut });
|
|
2014
1998
|
}
|
|
@@ -2035,6 +2019,7 @@ runHook("stop-gate", async (input) => {
|
|
|
2035
2019
|
node: process.version,
|
|
2036
2020
|
at: new Date().toISOString(),
|
|
2037
2021
|
by: subagent ? "subagent-stop" : "stop",
|
|
2022
|
+
ranIn: realpathSync(vdir),
|
|
2038
2023
|
commands: results.map((r) => ({ name: r.name, command: r.command, exit: 0, durationMs: typeof r.durationMs === "number" ? r.durationMs : 0 })),
|
|
2039
2024
|
totals,
|
|
2040
2025
|
});
|
|
@@ -2058,7 +2043,7 @@ runHook("stop-gate", async (input) => {
|
|
|
2058
2043
|
let evidence = null;
|
|
2059
2044
|
if (!skipVerification && cfg.stopGate.requireEvidence !== false && sessionMadeChanges) {
|
|
2060
2045
|
evidence = verificationEvidence(state.commands || [], cfg);
|
|
2061
|
-
if (evidence.ran.length === 0) problems.push(describeMissingEvidence(evidence.looked, results.every((r) => r.ok)));
|
|
2046
|
+
if (evidence.ran.length === 0) problems.push(describeMissingEvidence(evidence.looked.map((c) => redactText(c, cfg.secrets)), results.every((r) => r.ok)));
|
|
2062
2047
|
}
|
|
2063
2048
|
const evidenceProblemAdded = problems.length > postCommandProblems;
|
|
2064
2049
|
|
|
@@ -2150,7 +2135,7 @@ runHook("stop-gate", async (input) => {
|
|
|
2150
2135
|
// (none of them a dropped command) closes that gap.
|
|
2151
2136
|
postCommandProblems - preVerificationProblems === commandFailures.length
|
|
2152
2137
|
) {
|
|
2153
|
-
const coverage = redByDesignCoverage(claimFiles, commandFailures, changed, dir);
|
|
2138
|
+
const coverage = redByDesignCoverage(claimFiles, commandFailures, changed, dir, cfg.secrets);
|
|
2154
2139
|
if (coverage.covered) {
|
|
2155
2140
|
// Same as a subagent's green gate: counter resets, no checkpoint, no turn.
|
|
2156
2141
|
state.agentBlocks = { ...(state.agentBlocks || {}), [subagent.id]: 0 };
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
// No count ratchet and no scans here (no session baseline); the Stop gate does those. Takes the Stop gate's gate.lock (under the git common dir, shared by every worktree) around the commands (card verify-takes-gate-lock).
|
|
8
8
|
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
|
-
import { unlinkSync } from "node:fs";
|
|
10
|
+
import { unlinkSync, realpathSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { loadConfig } from "../lib/config.mjs";
|
|
13
|
-
import { gateLockPath, readGateLock, pidIsLive, writeGateLock, takeOverGateLock, resolveHookTimeoutSec, lockStaleByAge, normalizedTimeoutMs, lockWaitBoundMs } from "../lib/gate-lock.mjs";
|
|
13
|
+
import { gateLockPath, readGateLock, pidIsLive, writeGateLock, takeOverGateLock, acquireGateLock, resolveHookTimeoutSec, lockStaleByAge, normalizedTimeoutMs, lockWaitBoundMs } from "../lib/gate-lock.mjs";
|
|
14
14
|
import { runCommand, tail } from "../lib/run.mjs";
|
|
15
15
|
import { parseTotals, readSummaries, sumTotals } from "../lib/test-totals.mjs";
|
|
16
|
-
import { computeTree, readRecord, writeRecord, removeRecord, creditable, resolveCommands } from "../lib/verified-tree.mjs";
|
|
16
|
+
import { computeTree, readRecord, writeRecord, removeRecord, creditable, exitCreditUsable, resolveCommands } from "../lib/verified-tree.mjs";
|
|
17
17
|
|
|
18
18
|
function parseArgs(argv) {
|
|
19
19
|
const o = { noCredit: false, json: false, commands: [], creditDirs: [] };
|
|
@@ -51,6 +51,20 @@ function toplevel() {
|
|
|
51
51
|
return process.cwd();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// True when dir is a main checkout (git dir equals the common dir), or when that cannot be told (fail closed). Never throws.
|
|
55
|
+
function isMainCheckout(dir) {
|
|
56
|
+
try {
|
|
57
|
+
const env = {};
|
|
58
|
+
for (const [k, v] of Object.entries(process.env)) if (!k.startsWith("GIT_")) env[k] = v;
|
|
59
|
+
const r = spawnSync("git", ["rev-parse", "--git-dir", "--git-common-dir"], { cwd: dir, encoding: "utf8", env, timeout: 10000 });
|
|
60
|
+
const [a, b] = r.status === 0 ? r.stdout.trim().split("\n") : [];
|
|
61
|
+
if (!a || !b) return true;
|
|
62
|
+
return realpathSync(resolve(dir, a)) === realpathSync(resolve(dir, b));
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
function passed(totals) {
|
|
55
69
|
const s = totals && totals.status === "ok" ? readSummaries(totals) : null;
|
|
56
70
|
if (!s) return null;
|
|
@@ -86,9 +100,11 @@ function main() {
|
|
|
86
100
|
const wanted = commands.map((c) => c.command);
|
|
87
101
|
let record = null;
|
|
88
102
|
let fromOther = false;
|
|
103
|
+
// Card verify-credit-foreign-skipped: in a main checkout a record from another checkout that skipped tests is not evidence.
|
|
104
|
+
const mainCwd = isMainCheckout(cwd);
|
|
89
105
|
for (const [i, d] of [recordDir, ...o.creditDirs.map((c) => resolve(c))].entries()) {
|
|
90
106
|
const rec = readRecord(d);
|
|
91
|
-
if (rec && creditable(cwd, cfg, rec, treeBefore, wanted).ok) {
|
|
107
|
+
if (rec && creditable(cwd, cfg, rec, treeBefore, wanted).ok && (!mainCwd || exitCreditUsable(rec, cwd))) {
|
|
92
108
|
record = rec;
|
|
93
109
|
fromOther = i > 0;
|
|
94
110
|
break;
|
|
@@ -139,17 +155,33 @@ function main() {
|
|
|
139
155
|
const hookTimeoutMs = resolveHookTimeoutSec(cfg) * 1000;
|
|
140
156
|
const stale = (l) => !l || !pidIsLive(l.pid) || lockStaleByAge(l, hookTimeoutMs);
|
|
141
157
|
const held = readGateLock(dir);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
// Card gate-lock-takeover-exclusive: a lock read as stale on arrival is acquired exclusively; "busy" (a rival
|
|
159
|
+
// just took it) is waited on like a live holder, never overwritten; "unwritable" runs unlocked.
|
|
160
|
+
let needWait = !stale(held);
|
|
161
|
+
if (!needWait) {
|
|
162
|
+
const got = acquireGateLock(dir, o.by || "verify", hookTimeoutMs);
|
|
163
|
+
if (got === "acquired") lockOwned = true;
|
|
164
|
+
else if (got === "busy") needWait = true;
|
|
165
|
+
}
|
|
166
|
+
if (needWait) {
|
|
146
167
|
const waitStart = Date.now();
|
|
147
168
|
const deadline = waitStart + waitBoundMs;
|
|
148
169
|
let live = held;
|
|
170
|
+
let acquired = false;
|
|
149
171
|
const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
150
172
|
while (true) {
|
|
151
173
|
live = readGateLock(dir);
|
|
152
|
-
if (stale(live)
|
|
174
|
+
if (stale(live)) {
|
|
175
|
+
// Card gate-load-timeouts: take a released or stale lock exclusively (two waiters must not both run);
|
|
176
|
+
// a lost race goes back to waiting, bounded by the deadline.
|
|
177
|
+
const got = acquireGateLock(dir, o.by || "verify", hookTimeoutMs);
|
|
178
|
+
if (got !== "busy") {
|
|
179
|
+
acquired = got === "acquired";
|
|
180
|
+
lockOwned = acquired;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (Date.now() >= deadline) break;
|
|
153
185
|
Atomics.wait(nap, 0, 0, 200);
|
|
154
186
|
}
|
|
155
187
|
lockWaitMs = Date.now() - waitStart;
|
|
@@ -157,9 +189,11 @@ function main() {
|
|
|
157
189
|
treeBefore = computeTree(cwd);
|
|
158
190
|
const again = tryCredit();
|
|
159
191
|
if (again !== null) return again;
|
|
160
|
-
if (!
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
if (!acquired) {
|
|
193
|
+
if (!stale(live)) process.stderr.write(`[doug] verify: gate.lock still held by pid ${live.pid} after ${waitBoundMs} ms; running the commands anyway.\n`);
|
|
194
|
+
takeOverGateLock(dir, o.by || "verify");
|
|
195
|
+
lockOwned = true;
|
|
196
|
+
}
|
|
163
197
|
}
|
|
164
198
|
}
|
|
165
199
|
}
|
|
@@ -185,7 +219,7 @@ function main() {
|
|
|
185
219
|
if (ok) {
|
|
186
220
|
const treeAfter = computeTree(cwd);
|
|
187
221
|
if (treeBefore && treeAfter && treeBefore === treeAfter) {
|
|
188
|
-
writeRecord(recordDir, { version: 1, tree: treeAfter, node: process.version, at: new Date().toISOString(), by: o.by || "verify", commands: recCommands, totals });
|
|
222
|
+
writeRecord(recordDir, { version: 1, tree: treeAfter, node: process.version, at: new Date().toISOString(), by: o.by || "verify", ranIn: realpathSync(cwd), commands: recCommands, totals });
|
|
189
223
|
}
|
|
190
224
|
} else {
|
|
191
225
|
removeRecord(recordDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougharness/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "doug: tailored, approval-gated harness install for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"node": ">=22.16"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@dougharness/flow": "1.
|
|
27
|
-
"@dougharness/gates": "1.
|
|
26
|
+
"@dougharness/flow": "1.6.0",
|
|
27
|
+
"@dougharness/gates": "1.6.0"
|
|
28
28
|
},
|
|
29
29
|
"bundleDependencies": [
|
|
30
30
|
"@dougharness/flow",
|