@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
|
@@ -258,6 +258,15 @@ export function writeAgentStart(dir, cfg, sessionId, agentId) {
|
|
|
258
258
|
try {
|
|
259
259
|
const fp = treeFingerprint(dir, cfg, changedFiles(dir));
|
|
260
260
|
if (!fp) return;
|
|
261
|
+
// card worktree-skip-origin-at-start: origin/HEAD's sha at start (null when unset), for the worktree skip.
|
|
262
|
+
let originHead = null;
|
|
263
|
+
try {
|
|
264
|
+
const o = execFileSync("git", ["rev-parse", "--verify", "-q", "refs/remotes/origin/HEAD^{commit}"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim();
|
|
265
|
+
if (/^[0-9a-f]{7,64}$/.test(o)) originHead = o;
|
|
266
|
+
} catch {
|
|
267
|
+
originHead = null;
|
|
268
|
+
}
|
|
269
|
+
fp.originHead = originHead;
|
|
261
270
|
const file = agentStartFile(dir, sessionId, agentId);
|
|
262
271
|
mkdirSync(join(file, ".."), { recursive: true });
|
|
263
272
|
writeFileSync(file, JSON.stringify(fp));
|
|
@@ -110,13 +110,15 @@ export function failingTestFilesFrom(output, dir) {
|
|
|
110
110
|
// them is in the standing tests_red_by_design claim (`redByDesignClaimFiles`).
|
|
111
111
|
// 2. 'load-timeout' — at least one command timed out or was dropped for budget, and no failed command's
|
|
112
112
|
// captured output names a failing test file.
|
|
113
|
-
// 3. '
|
|
113
|
+
// 3. 'test-count' — the test-count ratchet is the block's only problem (`countProblems > 0` and it equals
|
|
114
|
+
// `totalProblems`).
|
|
115
|
+
// 4. 'race' — the working tree (`changedFiles(dir)`, as a set) differs now from `changedAtStart`. Either
|
|
114
116
|
// side being unknown (null: no git, or git unavailable) is never a race. Skipped entirely once
|
|
115
117
|
// `budgetDeadline` (the gate's own hook-timeout deadline, gateBudget() in stop-gate.mjs) has already
|
|
116
118
|
// passed (review pass 2, item 4): a second `git status` here is exactly the kind of extra work that
|
|
117
119
|
// could push a slow gate past the platform's own hook timeout, discarding its block message (F1) — a
|
|
118
120
|
// deadline already blown is not worth spending on a label, so this falls through to 'real' instead.
|
|
119
|
-
//
|
|
121
|
+
// 5. 'real' — otherwise.
|
|
120
122
|
//
|
|
121
123
|
// Never throws (review pass 2, item 2 MAJOR): this call sits between the gate's decision to block and the
|
|
122
124
|
// blockStop call that carries it out (stop-gate.mjs), so an unguarded throw here would fail the *whole gate*
|
|
@@ -141,10 +143,10 @@ export function classifyFinalBlock({ countProblems, totalProblems, preVerificati
|
|
|
141
143
|
if (allNamed.every((f) => claimSet.has(f))) return "expected-red";
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
// card block-ledger-failing-output: the test-count ratchet is the block's only problem.
|
|
145
146
|
const timedOutOrDropped = (Array.isArray(results) ? results : []).some((r) => r.timedOut || r.skipped === "budget");
|
|
146
147
|
if (timedOutOrDropped && allNamed.length === 0) return "load-timeout";
|
|
147
148
|
|
|
149
|
+
// card block-ledger-failing-output: the test-count ratchet is the block's only problem.
|
|
148
150
|
if (typeof countProblems === "number" && countProblems > 0 && totalProblems === countProblems) return "test-count";
|
|
149
151
|
|
|
150
152
|
if (Array.isArray(changedAtStart) && !(typeof budgetDeadline === "number" && Date.now() >= budgetDeadline)) {
|
|
@@ -213,6 +215,7 @@ export function appendBlockLine(dir, { hook, agent, reason, class: cls, session,
|
|
|
213
215
|
class: cls,
|
|
214
216
|
session: session ?? null,
|
|
215
217
|
...plain,
|
|
218
|
+
files: Array.isArray(plain.files) ? plain.files : [],
|
|
216
219
|
}) + "\n";
|
|
217
220
|
mkdirSync(join(dir, ".doug/.state"), { recursive: true });
|
|
218
221
|
appendFileSync(join(dir, BLOCKS_RELPATH), line);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// The gate.lock helpers shared by the Stop gate (scripts/stop-gate.mjs) and verify.mjs (card
|
|
2
2
|
// verify-takes-gate-lock). Moved verbatim from stop-gate.mjs; the Stop gate's behaviour is unchanged.
|
|
3
3
|
|
|
4
|
-
import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { loadConfig } from "./config.mjs";
|
|
7
9
|
|
|
8
10
|
// Single-runner lock (card stop-gate-process-storm, goal 3): only one gate runs the verification commands
|
|
9
11
|
// at a time per project, so concurrent SubagentStops share one suite instead of each starting its own (the
|
|
@@ -72,10 +74,10 @@ export function pidIsLive(pid) {
|
|
|
72
74
|
|
|
73
75
|
// Creates gate.lock exclusively (throws EEXIST when one is already there, or any other fs error). The
|
|
74
76
|
// caller decides what a failure means.
|
|
75
|
-
export function writeGateLock(dir, sessionId) {
|
|
77
|
+
export function writeGateLock(dir, sessionId, pid = process.pid) {
|
|
76
78
|
const p = gateLockPath(dir);
|
|
77
79
|
mkdirSync(dirname(p), { recursive: true });
|
|
78
|
-
writeFileSync(p, JSON.stringify({ pid
|
|
80
|
+
writeFileSync(p, JSON.stringify({ pid, session: sessionId, at: Date.now() }), { flag: "wx" });
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
// Overwrites gate.lock with this gate's own pid/session, whether it was stale (a dead pid) or its holder
|
|
@@ -92,6 +94,48 @@ export function takeOverGateLock(dir, sessionId) {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// Card gate-load-timeouts: exclusive acquisition for a waiter that saw the lock released, dead or stale. The
|
|
98
|
+
// old path overwrote it, so two waiters leaving the wait loop together both ran their suites (ledger blocks 425
|
|
99
|
+
// and 426). Creates with wx; a stale lock is removed only while the content judged stale is still in place, and
|
|
100
|
+
// EEXIST reports "busy" so the caller keeps waiting. Returns "acquired", "busy" or "unwritable" (the lock cannot
|
|
101
|
+
// be written at all: run unlocked). Never throws.
|
|
102
|
+
export function acquireGateLock(dir, sessionId, hookTimeoutMs, pid = process.pid) {
|
|
103
|
+
try {
|
|
104
|
+
const p = gateLockPath(dir);
|
|
105
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
writeGateLock(dir, sessionId, pid);
|
|
108
|
+
return "acquired";
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (!err || err.code !== "EEXIST") return "unwritable";
|
|
111
|
+
}
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = readFileSync(p, "utf8");
|
|
115
|
+
} catch {
|
|
116
|
+
continue; // released between the create and the read: try the create again
|
|
117
|
+
}
|
|
118
|
+
let lock = null;
|
|
119
|
+
try {
|
|
120
|
+
const o = JSON.parse(raw);
|
|
121
|
+
lock = o && typeof o === "object" && Number.isInteger(o.pid) && o.pid > 0 ? o : null;
|
|
122
|
+
} catch {
|
|
123
|
+
lock = null;
|
|
124
|
+
}
|
|
125
|
+
if (lock && pidIsLive(lock.pid) && !lockStaleByAge(lock, hookTimeoutMs)) return "busy";
|
|
126
|
+
try {
|
|
127
|
+
if (readFileSync(p, "utf8") !== raw) return "busy"; // someone else replaced it meanwhile
|
|
128
|
+
unlinkSync(p);
|
|
129
|
+
} catch {
|
|
130
|
+
// already gone, or not removable: the next create decides
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return "busy";
|
|
134
|
+
} catch {
|
|
135
|
+
return "unwritable";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
// Card stop-gate-budget-under-hook-timeout: cfg.stopGate.hookTimeoutSec must equal the platform's own
|
|
96
140
|
// Stop/SubagentStop hook `timeout` (hooks.json/settings.json), but a hook reader never throws (F6), so a
|
|
97
141
|
// missing, non-number, or non-positive value reads as the platform's own default, 600.
|
|
@@ -124,3 +168,130 @@ export function normalizedTimeoutMs(cfg) {
|
|
|
124
168
|
export function lockWaitBoundMs(perCommandMs, commandCount) {
|
|
125
169
|
return perCommandMs * Math.max(1, commandCount);
|
|
126
170
|
}
|
|
171
|
+
|
|
172
|
+
// Card gate-load-timeouts: CLI for .githooks/pre-commit, so a commit's suites queue on the same gate.lock as the
|
|
173
|
+
// Stop gate and verify.mjs (ledger blocks 422, 432, 435 had a lock-free pre-commit suite next to a gate's).
|
|
174
|
+
// `node gate-lock.mjs run -- <command> [args...]`, run from the repo top level: waits on a live lock up to
|
|
175
|
+
// stopGate.timeoutMs (default 300000), then runs anyway; a stale lock is taken at once; a lock that cannot be
|
|
176
|
+
// written runs unlocked. Leading NAME=VALUE arguments set the command's env; one remaining argument runs via `sh -c`.
|
|
177
|
+
// Holds the lock while the command runs, releases it, exits with the command's status.
|
|
178
|
+
function isMain() {
|
|
179
|
+
try {
|
|
180
|
+
return !!process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (isMain()) {
|
|
186
|
+
const args = process.argv.slice(2);
|
|
187
|
+
const dir = process.cwd();
|
|
188
|
+
const sub = args[0];
|
|
189
|
+
if (sub === "acquire" || sub === "release") {
|
|
190
|
+
// Card gate-lock-takeover-exclusive: one lock for a whole shell hook, held under the hook shell's own pid
|
|
191
|
+
// (`--pid $$`, alive for the whole hook). `acquire` waits like `run` and exits 0 whether it owns the lock,
|
|
192
|
+
// gave up waiting, or the lock is unwritable (the hook then runs unlocked); a non-zero exit is a runner
|
|
193
|
+
// failure. `release` removes the lock only when it names that pid, and never fails.
|
|
194
|
+
const pidArg = args.indexOf("--pid") === -1 ? NaN : Number(args[args.indexOf("--pid") + 1]);
|
|
195
|
+
if (!Number.isInteger(pidArg) || pidArg <= 0) {
|
|
196
|
+
process.stderr.write(`usage: gate-lock.mjs ${sub} --pid <pid>\n`);
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
if (sub === "release") {
|
|
200
|
+
try {
|
|
201
|
+
const lock = readGateLock(dir);
|
|
202
|
+
if (lock && lock.pid === pidArg) unlinkSync(gateLockPath(dir));
|
|
203
|
+
} catch {
|
|
204
|
+
// never fail a release
|
|
205
|
+
}
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
let waitMs = 300000;
|
|
209
|
+
let hookTimeoutMs = 600000;
|
|
210
|
+
try {
|
|
211
|
+
const cfg = loadConfig(dir);
|
|
212
|
+
waitMs = normalizedTimeoutMs(cfg) ?? waitMs;
|
|
213
|
+
hookTimeoutMs = resolveHookTimeoutSec(cfg) * 1000;
|
|
214
|
+
} catch {
|
|
215
|
+
// defaults
|
|
216
|
+
}
|
|
217
|
+
const deadline = Date.now() + waitMs;
|
|
218
|
+
const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
219
|
+
while (true) {
|
|
220
|
+
const r = acquireGateLock(dir, "pre-commit", hookTimeoutMs, pidArg);
|
|
221
|
+
if (r !== "busy") break;
|
|
222
|
+
if (Date.now() >= deadline) {
|
|
223
|
+
process.stderr.write(`[doug] pre-commit: gate.lock still held after ${waitMs} ms; running anyway.\n`);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
Atomics.wait(nap, 0, 0, 200);
|
|
227
|
+
}
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
const sep = args.indexOf("--");
|
|
231
|
+
const cmd = sep === -1 ? [] : args.slice(sep + 1);
|
|
232
|
+
if (args[0] !== "run" || cmd.length === 0) {
|
|
233
|
+
process.stderr.write("usage: gate-lock.mjs run -- <command> [args...]\n");
|
|
234
|
+
process.exit(2);
|
|
235
|
+
}
|
|
236
|
+
let owned = false;
|
|
237
|
+
const release = () => {
|
|
238
|
+
try {
|
|
239
|
+
if (!owned) return;
|
|
240
|
+
owned = false;
|
|
241
|
+
const lock = readGateLock(dir);
|
|
242
|
+
if (lock && lock.pid === process.pid) unlinkSync(gateLockPath(dir));
|
|
243
|
+
} catch {
|
|
244
|
+
// never throw from a release
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
process.on("exit", release);
|
|
248
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(128));
|
|
249
|
+
try {
|
|
250
|
+
let waitMs = 300000;
|
|
251
|
+
let hookTimeoutMs = 600000;
|
|
252
|
+
try {
|
|
253
|
+
const cfg = loadConfig(dir);
|
|
254
|
+
waitMs = normalizedTimeoutMs(cfg) ?? waitMs;
|
|
255
|
+
hookTimeoutMs = resolveHookTimeoutSec(cfg) * 1000;
|
|
256
|
+
} catch {
|
|
257
|
+
// defaults
|
|
258
|
+
}
|
|
259
|
+
const deadline = Date.now() + waitMs;
|
|
260
|
+
const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
261
|
+
while (true) {
|
|
262
|
+
const r = acquireGateLock(dir, "pre-commit", hookTimeoutMs);
|
|
263
|
+
if (r === "acquired") {
|
|
264
|
+
owned = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
if (r === "unwritable") break;
|
|
268
|
+
if (Date.now() >= deadline) {
|
|
269
|
+
process.stderr.write(`[doug] pre-commit: gate.lock still held after ${waitMs} ms; running ${cmd[0]} anyway.\n`);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
Atomics.wait(nap, 0, 0, 200);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
// run unlocked rather than block a commit on a lock fault
|
|
276
|
+
}
|
|
277
|
+
// Card gate-lock-run-shell-forms: leading NAME=VALUE arguments go into the child's environment, a single
|
|
278
|
+
// remaining argument runs through `sh -c` (a quoted compound command), and several arguments spawn as argv
|
|
279
|
+
// unchanged. A command that cannot be spawned says so on stderr and exits 127 instead of a silent 1.
|
|
280
|
+
const env = { ...process.env };
|
|
281
|
+
let argv = cmd;
|
|
282
|
+
while (argv.length > 1 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(argv[0])) {
|
|
283
|
+
const eq = argv[0].indexOf("=");
|
|
284
|
+
env[argv[0].slice(0, eq)] = argv[0].slice(eq + 1);
|
|
285
|
+
argv = argv.slice(1);
|
|
286
|
+
}
|
|
287
|
+
const res =
|
|
288
|
+
argv.length === 1
|
|
289
|
+
? spawnSync("sh", ["-c", argv[0]], { stdio: "inherit", env })
|
|
290
|
+
: spawnSync(argv[0], argv.slice(1), { stdio: "inherit", env });
|
|
291
|
+
release();
|
|
292
|
+
if (res.error) {
|
|
293
|
+
process.stderr.write(`[doug] gate-lock run: cannot run ${argv[0]}: ${res.error.message}\n`);
|
|
294
|
+
process.exit(127);
|
|
295
|
+
}
|
|
296
|
+
process.exit(res.status === null ? 1 : res.status);
|
|
297
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// - Counts cannot see delete-one-add-one: a session that removes a test and adds an unrelated one keeps
|
|
6
6
|
// every number level and passes.
|
|
7
7
|
// - The verified-unchanged skip runs no test command, so there is no output to count there; no count check
|
|
8
|
-
// happens on it. (The
|
|
8
|
+
// happens on it. (The credited path is counted from the totals the verified-tree record carries.)
|
|
9
9
|
// - The baseline is the number of `Tests` summaries a command prints plus their sum (ADR 0013): a session
|
|
10
10
|
// that adds or removes a package changes the number of summaries and blocks with no waiver until a new
|
|
11
11
|
// baseline is taken.
|
|
@@ -17,6 +17,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rename
|
|
|
17
17
|
import { join, resolve } from "node:path";
|
|
18
18
|
import { randomBytes } from "node:crypto";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { readSummaries } from "./test-totals.mjs";
|
|
20
21
|
|
|
21
22
|
export const RECORD_RELPATH = ".doug/.state/verified-tree.json";
|
|
22
23
|
export const EXCLUDED = [".doug/.state", ".claude/worktrees", ".doug/anchor.md"];
|
|
@@ -108,6 +109,37 @@ export function validRecord(r) {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// Test totals depend on the environment (a worktree without a built dist skips tests), not only the tree. A record
|
|
113
|
+
// stamps `ranIn` (realpath of the directory its commands ran in); its totals count as evidence only for that directory.
|
|
114
|
+
// Never throws: any failure, or a record with no ranIn (legacy), is false (the commands run).
|
|
115
|
+
export function totalsUsable(record, dir) {
|
|
116
|
+
try {
|
|
117
|
+
return !!record && typeof record.ranIn === "string" && realpathSync(record.ranIn) === realpathSync(dir);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A record from another checkout (or a legacy one with no ranIn) credits exit codes only when none of its recorded
|
|
124
|
+
// totals reports skipped tests: a dist-less worktree skips tests that could fail here. Absent or unreadable entries
|
|
125
|
+
// do not count. Never throws: any failure is false.
|
|
126
|
+
export function exitCreditUsable(record, dir) {
|
|
127
|
+
try {
|
|
128
|
+
if (!record || typeof record !== "object") return false;
|
|
129
|
+
if (totalsUsable(record, dir)) return true;
|
|
130
|
+
const totals = record.totals;
|
|
131
|
+
if (!totals || typeof totals !== "object") return true;
|
|
132
|
+
for (const entry of Object.values(totals)) {
|
|
133
|
+
const sums = readSummaries(entry);
|
|
134
|
+
if (!sums) continue;
|
|
135
|
+
if (sums.reduce((n, s) => n + (s.skipped || 0), 0) > 0) return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
export function readRecord(dir) {
|
|
112
144
|
try {
|
|
113
145
|
const r = JSON.parse(readFileSync(join(dir, RECORD_RELPATH), "utf8"));
|
|
@@ -214,6 +246,7 @@ if (isMain()) {
|
|
|
214
246
|
node: process.version,
|
|
215
247
|
at: new Date().toISOString(),
|
|
216
248
|
by: "pre-commit",
|
|
249
|
+
ranIn: realpathSync(dir),
|
|
217
250
|
commands: commands.map((cm) => ({ name: cm.name, command: cm.command, exit: cm.exit, durationMs: cm.durationMs })),
|
|
218
251
|
totals,
|
|
219
252
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougharness/gates",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Deterministic Claude Code hooks: protected paths, bash guard, secret scan on edits and commands, format on edit, edit-loop detection, stop gate, re-anchor after compaction.",
|
|
6
6
|
"type": "module",
|