@deftai/directive-core 0.97.0 → 0.98.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/authz/classify.js +291 -0
- package/dist/check/cached-orchestrator.d.ts +5 -0
- package/dist/check/cached-orchestrator.js +18 -1
- package/dist/check/gate-lists.d.ts +20 -0
- package/dist/check/gate-lists.js +46 -9
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +4 -0
- package/dist/doctor/checks.d.ts +7 -0
- package/dist/doctor/checks.js +83 -0
- package/dist/hooks/dispatcher.d.ts +5 -0
- package/dist/hooks/dispatcher.js +54 -4
- package/dist/init-deposit/hygiene.d.ts +70 -1
- package/dist/init-deposit/hygiene.js +582 -8
- package/dist/init-deposit/scaffold.js +277 -4
- package/dist/init-deposit/skill-discovery-deposit.js +15 -0
- package/dist/policy/check-resume.d.ts +72 -0
- package/dist/policy/check-resume.js +253 -0
- package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
- package/dist/policy/coverage-check-resume-presets.js +228 -0
- package/dist/policy/coverage-debt.d.ts +76 -0
- package/dist/policy/coverage-debt.js +262 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +50 -21
- package/dist/release/auto-hatch.d.ts +114 -0
- package/dist/release/auto-hatch.js +301 -0
- package/dist/release/coverage-debt-ledger.d.ts +22 -0
- package/dist/release/coverage-debt-ledger.js +157 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/pipeline.js +164 -12
- package/dist/release/suite-stamp.d.ts +44 -0
- package/dist/release/suite-stamp.js +133 -0
- package/dist/release/types.d.ts +19 -0
- package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
- package/dist/session/coverage-check-resume-nudge.js +66 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/session-start.js +21 -0
- package/dist/triage/classify/label-mirror.d.ts +31 -1
- package/dist/triage/classify/label-mirror.js +78 -6
- package/dist/triage/help/registry-data.d.ts +6 -6
- package/dist/triage/help/registry-data.js +12 -3
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +54 -0
- package/package.json +3 -3
package/dist/authz/classify.js
CHANGED
|
@@ -150,6 +150,18 @@ function hasGhApiPath(tokens, needle) {
|
|
|
150
150
|
* active UAT they deny without a prior human grant — never empty → shell-op-unclassifiable fail-open.
|
|
151
151
|
*/
|
|
152
152
|
const AUTHZ_MUTATING_SUBCOMMANDS = new Set(["grant", "uat-start", "uat-suspend", "revoke"]);
|
|
153
|
+
/**
|
|
154
|
+
* Policy authority mutators that weaken merge / branch / directive gates (#3186).
|
|
155
|
+
* Classified as **settings** — agents must not self-serve these under active UAT.
|
|
156
|
+
*/
|
|
157
|
+
const POLICY_AUTHORITY_MUTATORS = new Set([
|
|
158
|
+
"allow-bot-merge",
|
|
159
|
+
"allow-direct-commits",
|
|
160
|
+
"disable-directive",
|
|
161
|
+
"enable-directive",
|
|
162
|
+
]);
|
|
163
|
+
/** Kill-switch / permanent opt-out basenames agents must not plant under UAT (#3186 / #3039). */
|
|
164
|
+
const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
|
|
153
165
|
function authzSubcommandFromToken(token) {
|
|
154
166
|
const t = normalizeToken(token);
|
|
155
167
|
if (t.startsWith("authz:")) {
|
|
@@ -158,6 +170,14 @@ function authzSubcommandFromToken(token) {
|
|
|
158
170
|
}
|
|
159
171
|
return AUTHZ_MUTATING_SUBCOMMANDS.has(t) ? t : null;
|
|
160
172
|
}
|
|
173
|
+
function policyMutatorFromToken(token) {
|
|
174
|
+
const t = normalizeToken(token);
|
|
175
|
+
if (t.startsWith("policy:")) {
|
|
176
|
+
const sub = t.slice("policy:".length);
|
|
177
|
+
return POLICY_AUTHORITY_MUTATORS.has(sub) ? sub : null;
|
|
178
|
+
}
|
|
179
|
+
return POLICY_AUTHORITY_MUTATORS.has(t) ? t : null;
|
|
180
|
+
}
|
|
161
181
|
/**
|
|
162
182
|
* Detect `deft|task|directive authz:grant` / `authz grant` (and wrappers) in shell tokens.
|
|
163
183
|
* O(n) token walk — no nested-quantifier regex on untrusted input.
|
|
@@ -189,6 +209,266 @@ function hasAuthzMutatingCli(tokens) {
|
|
|
189
209
|
}
|
|
190
210
|
return false;
|
|
191
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Detect `policy:allow-bot-merge` / `policy allow-direct-commits` / peers (#3186).
|
|
214
|
+
* O(n) token walk — no nested-quantifier regex on untrusted input.
|
|
215
|
+
*/
|
|
216
|
+
function hasPolicyAuthorityMutator(tokens) {
|
|
217
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
218
|
+
const raw = tokens[i];
|
|
219
|
+
if (raw === undefined)
|
|
220
|
+
break;
|
|
221
|
+
const t = normalizeToken(raw);
|
|
222
|
+
if (policyMutatorFromToken(t) !== null && t.startsWith("policy:")) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
const isPolicyBin = t === "policy" ||
|
|
226
|
+
t.endsWith("/policy") ||
|
|
227
|
+
t.endsWith("\\policy") ||
|
|
228
|
+
t.endsWith("/policy.js") ||
|
|
229
|
+
t.endsWith("\\policy.js") ||
|
|
230
|
+
t.endsWith("/policy.ts") ||
|
|
231
|
+
t.endsWith("\\policy.ts");
|
|
232
|
+
if (!isPolicyBin)
|
|
233
|
+
continue;
|
|
234
|
+
const next = tokens[i + 1] !== undefined ? normalizeToken(tokens[i + 1]) : "";
|
|
235
|
+
if (policyMutatorFromToken(next) !== null)
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039).
|
|
242
|
+
* Planting the kill-switch under UAT would full-bypass subsequent gates without operator recovery.
|
|
243
|
+
*/
|
|
244
|
+
function hasKillSwitchShellWrite(command, tokens) {
|
|
245
|
+
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
246
|
+
let mentionsKill = false;
|
|
247
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
248
|
+
if (lower.includes(name)) {
|
|
249
|
+
mentionsKill = true;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!mentionsKill)
|
|
254
|
+
return false;
|
|
255
|
+
// Redirect dest region after each `>` / `>>` (O(n)).
|
|
256
|
+
for (let i = 0; i < lower.length; i++) {
|
|
257
|
+
if (lower[i] !== ">")
|
|
258
|
+
continue;
|
|
259
|
+
let j = i + 1;
|
|
260
|
+
if (j < lower.length && lower[j] === ">")
|
|
261
|
+
j++;
|
|
262
|
+
let end = j;
|
|
263
|
+
while (end < lower.length &&
|
|
264
|
+
lower[end] !== "|" &&
|
|
265
|
+
lower[end] !== ";" &&
|
|
266
|
+
lower[end] !== "&" &&
|
|
267
|
+
lower[end] !== "\n") {
|
|
268
|
+
end++;
|
|
269
|
+
}
|
|
270
|
+
const dest = lower.slice(j, end);
|
|
271
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
272
|
+
if (dest.includes(name))
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Write/destructive bins with a kill-switch path argument (touch, New-Item, …).
|
|
277
|
+
const killWriteBins = new Set([
|
|
278
|
+
...INDIRECT_WRITE_BINS,
|
|
279
|
+
"touch",
|
|
280
|
+
"new-item",
|
|
281
|
+
"ni",
|
|
282
|
+
"echo",
|
|
283
|
+
"printf",
|
|
284
|
+
"type",
|
|
285
|
+
]);
|
|
286
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
287
|
+
if (!killWriteBins.has(normalizeToken(tokens[ti])))
|
|
288
|
+
continue;
|
|
289
|
+
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
290
|
+
const p = pathishToken(tokens[tj]);
|
|
291
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
292
|
+
if (p.includes(name))
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Bare `touch .deft-directive-disable` — first token may be touch, path later.
|
|
298
|
+
for (const t of tokens) {
|
|
299
|
+
const p = pathishToken(t);
|
|
300
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
301
|
+
// Exact basename or ends with /basename
|
|
302
|
+
if (p === name || p.endsWith(`/${name}`)) {
|
|
303
|
+
// Require some write shape (redirect already handled; touch/ni/echo/…)
|
|
304
|
+
if (hasWriteShape(command, tokens) ||
|
|
305
|
+
lower.includes("touch") ||
|
|
306
|
+
lower.includes("new-item")) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* True when a token is a write-capable language/runtime interpreter (#3186).
|
|
316
|
+
* Matches bare names, versioned bins (`python3.11`), and path-qualified forms
|
|
317
|
+
* (`/usr/bin/python3`, `C:\Python311\python.exe`) — exact Set membership alone
|
|
318
|
+
* would fail-open on absolute/versioned paths (Greptile P1).
|
|
319
|
+
*/
|
|
320
|
+
function isProgrammaticWriteBinToken(token) {
|
|
321
|
+
const t = normalizeToken(token);
|
|
322
|
+
if (t.length === 0)
|
|
323
|
+
return false;
|
|
324
|
+
// Path-qualified: keep final path segment after / or \ (normalizeToken strips quotes only).
|
|
325
|
+
const pathish = token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
326
|
+
const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : t;
|
|
327
|
+
const bare = base.endsWith(".exe") ? base.slice(0, -4) : base;
|
|
328
|
+
if (bare === "python" ||
|
|
329
|
+
bare === "python3" ||
|
|
330
|
+
bare === "node" ||
|
|
331
|
+
bare === "nodejs" ||
|
|
332
|
+
bare === "perl" ||
|
|
333
|
+
bare === "ruby" ||
|
|
334
|
+
bare === "pwsh" ||
|
|
335
|
+
bare === "powershell") {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
// Versioned: python3.11, python3.12, node18, …
|
|
339
|
+
if (bare.startsWith("python3.") || bare.startsWith("python2."))
|
|
340
|
+
return true;
|
|
341
|
+
if (/^python\d+(\.\d+)*$/.test(bare))
|
|
342
|
+
return true;
|
|
343
|
+
if (/^node\d+$/.test(bare))
|
|
344
|
+
return true;
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* True when `needle` occurs in `haystack` outside single/double-quoted regions (O(n)).
|
|
349
|
+
* Escaped quotes (`\'` / `\"`) stay inside the string. Used so `print('.write(')` is not
|
|
350
|
+
* a write API while `open(p,'w').write('x')` still matches `.write(` (Greptile conf residual).
|
|
351
|
+
*/
|
|
352
|
+
function includesOutsideQuotes(haystack, needle) {
|
|
353
|
+
if (needle.length === 0 || haystack.length < needle.length)
|
|
354
|
+
return false;
|
|
355
|
+
let inSingle = false;
|
|
356
|
+
let inDouble = false;
|
|
357
|
+
for (let i = 0; i < haystack.length; i++) {
|
|
358
|
+
const c = haystack[i];
|
|
359
|
+
if (c === "\\" && i + 1 < haystack.length && (inSingle || inDouble)) {
|
|
360
|
+
i++; // skip escaped char inside quotes
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (c === "'" && !inDouble) {
|
|
364
|
+
inSingle = !inSingle;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (c === '"' && !inSingle) {
|
|
368
|
+
inDouble = !inDouble;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (inSingle || inDouble)
|
|
372
|
+
continue;
|
|
373
|
+
if (haystack.startsWith(needle, i))
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* True when command uses a programmatic bin with a **write** API, optionally with
|
|
380
|
+
* path-obfuscation markers (base64/bytes/chr). Pure reads / print-only stay unclassifiable.
|
|
381
|
+
*
|
|
382
|
+
* Chosen rule (#3186): classify write-capable programmatic Shell as **settings** so
|
|
383
|
+
* active UAT fails closed (not shell-op-unclassifiable allow) even when the path is
|
|
384
|
+
* built at runtime without a literal `.deft/authz` substring. Outside UAT, evaluate
|
|
385
|
+
* still returns authz-inactive allow — classification alone is not a hard deny.
|
|
386
|
+
*
|
|
387
|
+
* Read-only `open(...).read()` does **not** count as writeish (Greptile P1).
|
|
388
|
+
* Quoted data containing `.write(` does **not** count (Greptile conf residual).
|
|
389
|
+
* Obfuscation alone without a write API does **not** classify (avoid deny on decode-only).
|
|
390
|
+
*/
|
|
391
|
+
function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
392
|
+
let hasProg = false;
|
|
393
|
+
for (const t of tokens) {
|
|
394
|
+
if (isProgrammaticWriteBinToken(t)) {
|
|
395
|
+
hasProg = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!hasProg)
|
|
400
|
+
return false;
|
|
401
|
+
const lower = command.toLowerCase();
|
|
402
|
+
// Shell wrappers put whole scripts in "…" after -c/-e, so long API names (writeFileSync)
|
|
403
|
+
// use full-string includes. Short ambiguous `.write(` uses quote-aware match so
|
|
404
|
+
// `print('.write(')` is not a write API (Greptile conf residual).
|
|
405
|
+
const writeApi = lower.includes("writefilesync") ||
|
|
406
|
+
lower.includes("writefile") ||
|
|
407
|
+
lower.includes("writetext") ||
|
|
408
|
+
lower.includes("set-content") ||
|
|
409
|
+
lower.includes("out-file") ||
|
|
410
|
+
lower.includes("fs.write") ||
|
|
411
|
+
lower.includes("createwritestream") ||
|
|
412
|
+
lower.includes("path.write(") ||
|
|
413
|
+
lower.includes("file.write(") ||
|
|
414
|
+
lower.includes("spurt") ||
|
|
415
|
+
lower.includes(">>") ||
|
|
416
|
+
lower.includes("unlink(") ||
|
|
417
|
+
lower.includes("rmsync") ||
|
|
418
|
+
lower.includes("rm_rf") ||
|
|
419
|
+
lower.includes("shutil.rmtree") ||
|
|
420
|
+
lower.includes("os.remove(") ||
|
|
421
|
+
lower.includes("os.unlink(") ||
|
|
422
|
+
lower.includes("fs.unlink") ||
|
|
423
|
+
lower.includes("fs.rm(") ||
|
|
424
|
+
lower.includes("fs.rmsync") ||
|
|
425
|
+
// Short `.write(` only counts outside quotes (avoids print('.write(') false positive).
|
|
426
|
+
// Also match when it appears after a shell -c/-e opening quote as code (common PoC shape):
|
|
427
|
+
// if the command has write mode open or other write API, those already hit above.
|
|
428
|
+
includesOutsideQuotes(lower, ".write(") ||
|
|
429
|
+
// Script body after -c/-e often sits in one double-quoted region: still detect `.write(`
|
|
430
|
+
// as code when paired with an assignment/call shape (f.write / .write(x)).
|
|
431
|
+
(lower.includes(".write(") &&
|
|
432
|
+
(lower.includes("open(") || lower.includes("=open") || lower.includes("fs.")));
|
|
433
|
+
// open(..., 'w' / "w" / 'a' / '>') — mode tokens are quoted; match on full command.
|
|
434
|
+
const openWriteMode = lower.includes("open(") &&
|
|
435
|
+
(lower.includes(",'w") ||
|
|
436
|
+
lower.includes(',"w') ||
|
|
437
|
+
lower.includes(", 'w") ||
|
|
438
|
+
lower.includes(', "w') ||
|
|
439
|
+
lower.includes(",'a") ||
|
|
440
|
+
lower.includes(',"a') ||
|
|
441
|
+
lower.includes(", 'a") ||
|
|
442
|
+
lower.includes(', "a') ||
|
|
443
|
+
lower.includes(",'>") ||
|
|
444
|
+
lower.includes(',">') ||
|
|
445
|
+
lower.includes(", '>") ||
|
|
446
|
+
lower.includes(', ">') ||
|
|
447
|
+
lower.includes("mode='w") ||
|
|
448
|
+
lower.includes('mode="w') ||
|
|
449
|
+
lower.includes("mode='a") ||
|
|
450
|
+
lower.includes('mode="a') ||
|
|
451
|
+
lower.includes("mode=w"));
|
|
452
|
+
const writeish = writeApi || openWriteMode;
|
|
453
|
+
// Path construction that hides the destination from literal classifiers.
|
|
454
|
+
const obfuscatedPath = lower.includes("base64") ||
|
|
455
|
+
lower.includes("fromcharcode") ||
|
|
456
|
+
lower.includes("bytes([") ||
|
|
457
|
+
lower.includes("bytearray") ||
|
|
458
|
+
lower.includes("buffer.from") ||
|
|
459
|
+
lower.includes("codecs.decode") ||
|
|
460
|
+
lower.includes("unhexlify") ||
|
|
461
|
+
lower.includes("fromhex") ||
|
|
462
|
+
lower.includes("string.fromcharcode") ||
|
|
463
|
+
lower.includes("atob(") ||
|
|
464
|
+
lower.includes("btoa(") ||
|
|
465
|
+
lower.includes("chr(");
|
|
466
|
+
// Fail-closed residual: write-ish programmatic shell (obfuscation alone not enough).
|
|
467
|
+
if (writeish)
|
|
468
|
+
return true;
|
|
469
|
+
void obfuscatedPath;
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
192
472
|
/**
|
|
193
473
|
* Path-ish normalize: keep separators (do not strip `\` like normalizeToken).
|
|
194
474
|
*/
|
|
@@ -621,6 +901,11 @@ export function classifyShellAuthzOps(command) {
|
|
|
621
901
|
found.add("settings");
|
|
622
902
|
if (hasAuthzDirShellWrite(cmd, tokens))
|
|
623
903
|
found.add("settings");
|
|
904
|
+
// #3186: kill-switch plant + policy authority mutators → settings (UAT fail-closed).
|
|
905
|
+
if (hasKillSwitchShellWrite(cmd, tokens))
|
|
906
|
+
found.add("settings");
|
|
907
|
+
if (hasPolicyAuthorityMutator(tokens))
|
|
908
|
+
found.add("settings");
|
|
624
909
|
// Split path write: `cd .deft && echo x > authz/state.json` OR `cd .deft/authz && echo x > state.json`
|
|
625
910
|
// OR `cd .deft/authz && cp … grants/x` (write bin without redirect).
|
|
626
911
|
// When the command cds into an authz path, any write shape is settings (relative dest has no "authz" text).
|
|
@@ -665,6 +950,12 @@ export function classifyShellAuthzOps(command) {
|
|
|
665
950
|
}
|
|
666
951
|
if (hasIndirectAuthzStoreWrite(cmd, tokens))
|
|
667
952
|
found.add("settings");
|
|
953
|
+
// #3186: programmatic write/obfuscated-path shells fail closed as settings (not unclassifiable allow).
|
|
954
|
+
// Always merge settings even when other ops already matched (e.g. `pytest && python -c '…write…'`)
|
|
955
|
+
// so a compound safe prefix cannot hide a write-capable residual (SLizard residual).
|
|
956
|
+
if (hasWriteCapableProgrammaticShell(cmd, tokens)) {
|
|
957
|
+
found.add("settings");
|
|
958
|
+
}
|
|
668
959
|
return [...found];
|
|
669
960
|
}
|
|
670
961
|
/** Map a PreToolUse tool name + optional shell command to authz ops. */
|
|
@@ -11,6 +11,11 @@ export interface CachedCheckOptions extends CheckOrchestratorSeams {
|
|
|
11
11
|
/**
|
|
12
12
|
* Run check gates sequentially with content-hash caching (#1713).
|
|
13
13
|
* Falls back to fail-open execution for undeclared / non-cacheable gates.
|
|
14
|
+
*
|
|
15
|
+
* Gate order is fast-before-slow (#3188): non-suite gates complete (or fail)
|
|
16
|
+
* before any suite gate (`ts:check-lane` / vitest+coverage) is started. A
|
|
17
|
+
* non-zero exit aborts the loop immediately — the suite never starts after a
|
|
18
|
+
* fast-gate failure (observable via `onGateStart` / suite start log).
|
|
14
19
|
*/
|
|
15
20
|
export declare function dispatchCachedTaskCheck(frameworkRoot: string, projectRoot: string, options?: CachedCheckOptions): number;
|
|
16
21
|
//# sourceMappingURL=cached-orchestrator.d.ts.map
|
|
@@ -4,7 +4,7 @@ import { lintShippedRegistry, resolveTaskContract, runWithCache, } from "../cach
|
|
|
4
4
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
5
5
|
import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "./consumer-gate-integrity.js";
|
|
6
6
|
import { resolveCheckTarget } from "./context.js";
|
|
7
|
-
import { checkGateId, checkGateSpawnArgs, gatesForCheckTarget } from "./gate-lists.js";
|
|
7
|
+
import { checkGateId, checkGateSpawnArgs, gatesForCheckTarget, isSuiteCheckGate, } from "./gate-lists.js";
|
|
8
8
|
function captureSpawn(taskBin, args, opts) {
|
|
9
9
|
const result = spawnSync(taskBin, args, {
|
|
10
10
|
cwd: opts.cwd,
|
|
@@ -20,6 +20,11 @@ function captureSpawn(taskBin, args, opts) {
|
|
|
20
20
|
/**
|
|
21
21
|
* Run check gates sequentially with content-hash caching (#1713).
|
|
22
22
|
* Falls back to fail-open execution for undeclared / non-cacheable gates.
|
|
23
|
+
*
|
|
24
|
+
* Gate order is fast-before-slow (#3188): non-suite gates complete (or fail)
|
|
25
|
+
* before any suite gate (`ts:check-lane` / vitest+coverage) is started. A
|
|
26
|
+
* non-zero exit aborts the loop immediately — the suite never starts after a
|
|
27
|
+
* fast-gate failure (observable via `onGateStart` / suite start log).
|
|
23
28
|
*/
|
|
24
29
|
export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}) {
|
|
25
30
|
const resolvedFramework = resolve(frameworkRoot);
|
|
@@ -53,6 +58,11 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
|
|
|
53
58
|
}
|
|
54
59
|
for (const gateSpec of gates) {
|
|
55
60
|
const gateId = checkGateId(gateSpec);
|
|
61
|
+
// #3188: log suite entry so operators/tests can prove fast failures never
|
|
62
|
+
// reach vitest+coverage (suite gates are ordered last in gate-lists).
|
|
63
|
+
if (isSuiteCheckGate(gateSpec)) {
|
|
64
|
+
process.stderr.write(`check: starting suite gate ${gateId} after fast preflight (#3188)\n`);
|
|
65
|
+
}
|
|
56
66
|
options.onGateStart?.(gateId);
|
|
57
67
|
const contract = resolveTaskContract(gateId);
|
|
58
68
|
const taskArgs = checkGateSpawnArgs(gateSpec, taskfilePath);
|
|
@@ -78,7 +88,14 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
|
|
|
78
88
|
},
|
|
79
89
|
});
|
|
80
90
|
options.onGateComplete?.(gateId, result.exitCode, result.fromCache);
|
|
91
|
+
// Fail-fast: do not start later gates (including suite) after a failure.
|
|
81
92
|
if (result.exitCode !== 0) {
|
|
93
|
+
if (!isSuiteCheckGate(gateSpec)) {
|
|
94
|
+
const remaining = gates.some(isSuiteCheckGate)
|
|
95
|
+
? "skipping remaining gates including suite"
|
|
96
|
+
: "skipping remaining gates";
|
|
97
|
+
process.stderr.write(`check: fast gate ${gateId} failed (exit ${result.exitCode}); ${remaining} (#3188)\n`);
|
|
98
|
+
}
|
|
82
99
|
return result.exitCode;
|
|
83
100
|
}
|
|
84
101
|
}
|
|
@@ -12,6 +12,26 @@ export type CheckGateSpec = string | {
|
|
|
12
12
|
export declare function checkGateId(spec: CheckGateSpec): string;
|
|
13
13
|
/** Args for `task … --taskfile <path>` (optional `--` + public CLI flags). */
|
|
14
14
|
export declare function checkGateSpawnArgs(spec: CheckGateSpec, taskfilePath: string): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Gates that own the long vitest+coverage (or equivalent) suite path.
|
|
17
|
+
* Shared `task check` composition runs every non-suite gate first so cheap
|
|
18
|
+
* failures never pay suite wall-clock (#3188). Release suite stamp/resume
|
|
19
|
+
* remains #3187 / release-scoped.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SUITE_CHECK_GATE_IDS: readonly string[];
|
|
22
|
+
export declare function isSuiteCheckGate(spec: CheckGateSpec | string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* True when every suite gate appears after all non-suite gates (#3188).
|
|
25
|
+
* Empty lists and suite-only lists are valid; a non-suite after a suite is not.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isFastBeforeSlowOrder(gates: readonly CheckGateSpec[]): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Framework-source check composition (#1713 / #3188).
|
|
30
|
+
*
|
|
31
|
+
* Order contract: cheap preflight gates first; `ts:check-lane` (lint+build+
|
|
32
|
+
* vitest coverage suite) last so a stale cache / orphan-active / branch miss
|
|
33
|
+
* fails in seconds without starting the suite.
|
|
34
|
+
*/
|
|
15
35
|
export declare const FRAMEWORK_CHECK_GATES: readonly CheckGateSpec[];
|
|
16
36
|
export declare const CONSUMER_CHECK_GATES: readonly CheckGateSpec[];
|
|
17
37
|
export declare function gatesForCheckTarget(target: string): readonly CheckGateSpec[];
|
package/dist/check/gate-lists.js
CHANGED
|
@@ -11,23 +11,59 @@ export function checkGateSpawnArgs(spec, taskfilePath) {
|
|
|
11
11
|
}
|
|
12
12
|
return [task, "--taskfile", taskfilePath];
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Gates that own the long vitest+coverage (or equivalent) suite path.
|
|
16
|
+
* Shared `task check` composition runs every non-suite gate first so cheap
|
|
17
|
+
* failures never pay suite wall-clock (#3188). Release suite stamp/resume
|
|
18
|
+
* remains #3187 / release-scoped.
|
|
19
|
+
*/
|
|
20
|
+
export const SUITE_CHECK_GATE_IDS = ["ts:check-lane"];
|
|
21
|
+
export function isSuiteCheckGate(spec) {
|
|
22
|
+
const id = typeof spec === "string" ? spec : checkGateId(spec);
|
|
23
|
+
return SUITE_CHECK_GATE_IDS.includes(id);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True when every suite gate appears after all non-suite gates (#3188).
|
|
27
|
+
* Empty lists and suite-only lists are valid; a non-suite after a suite is not.
|
|
28
|
+
*/
|
|
29
|
+
export function isFastBeforeSlowOrder(gates) {
|
|
30
|
+
let sawSuite = false;
|
|
31
|
+
for (const gate of gates) {
|
|
32
|
+
if (isSuiteCheckGate(gate)) {
|
|
33
|
+
sawSuite = true;
|
|
34
|
+
}
|
|
35
|
+
else if (sawSuite) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Framework-source check composition (#1713 / #3188).
|
|
43
|
+
*
|
|
44
|
+
* Order contract: cheap preflight gates first; `ts:check-lane` (lint+build+
|
|
45
|
+
* vitest coverage suite) last so a stale cache / orphan-active / branch miss
|
|
46
|
+
* fails in seconds without starting the suite.
|
|
47
|
+
*/
|
|
14
48
|
export const FRAMEWORK_CHECK_GATES = [
|
|
15
|
-
|
|
49
|
+
// --- Fast preflight (seconds–few min) — #3188 ---
|
|
50
|
+
"verify:branch",
|
|
51
|
+
"verify:encoding",
|
|
52
|
+
"verify:cache-fresh",
|
|
53
|
+
"verify:orphan-active",
|
|
54
|
+
"verify:license-sync",
|
|
55
|
+
"verify:contract-drift",
|
|
16
56
|
"toolchain:check",
|
|
17
57
|
"verify:stubs",
|
|
18
58
|
"verify:links",
|
|
19
59
|
"verify:rule-ownership",
|
|
20
60
|
"verify:biome-config",
|
|
21
61
|
"verify:content-manifest",
|
|
22
|
-
"verify:license-sync",
|
|
23
62
|
"verify:skill-external-fetch-gate",
|
|
24
|
-
"verify:contract-drift",
|
|
25
63
|
"verify:cursor-tier1",
|
|
26
64
|
"verify:openclaw-tier1",
|
|
27
65
|
"verify:go-freeze",
|
|
28
66
|
"verify:bridge-drift",
|
|
29
|
-
"verify:branch",
|
|
30
|
-
"verify:encoding",
|
|
31
67
|
"verify:forward-coverage",
|
|
32
68
|
// #3145: test/source boundary + approved-scope provenance + consumer gate composition
|
|
33
69
|
"verify:test-boundary",
|
|
@@ -38,11 +74,9 @@ export const FRAMEWORK_CHECK_GATES = [
|
|
|
38
74
|
"verify:scm-boundary",
|
|
39
75
|
"verify:xbrief-drift",
|
|
40
76
|
"verify:no-task-runtime",
|
|
41
|
-
"verify:cache-fresh",
|
|
42
77
|
"verify:pack-drift",
|
|
43
78
|
// Public surface for Taskfile verify-wip-cap-framework-self-check (#1124 / #2791)
|
|
44
79
|
{ task: "verify:wip-cap", args: ["--allow-over-cap"] },
|
|
45
|
-
"verify:orphan-active",
|
|
46
80
|
"verify:agents-md-budget",
|
|
47
81
|
// Public surfaces for internal eval-relocation framework shims (#2791)
|
|
48
82
|
{ task: "verify:eval-health-relocation", args: ["--base-ref", "origin/master"] },
|
|
@@ -51,14 +85,17 @@ export const FRAMEWORK_CHECK_GATES = [
|
|
|
51
85
|
"codebase:validate-structure",
|
|
52
86
|
"verify:codebase-map-fresh",
|
|
53
87
|
"verify-strategy-output",
|
|
88
|
+
// --- Suite last: vitest + coverage via ts:check-lane (#3188) ---
|
|
89
|
+
"ts:check-lane",
|
|
54
90
|
];
|
|
55
91
|
export const CONSUMER_CHECK_GATES = [
|
|
56
|
-
|
|
57
|
-
"toolchain:check-consumer",
|
|
92
|
+
// Cheap lifecycle / policy first (no suite co-list today; keep fail-fast order)
|
|
58
93
|
"verify:branch",
|
|
59
94
|
"verify:cache-fresh",
|
|
60
95
|
"verify:wip-cap",
|
|
61
96
|
"verify:orphan-active",
|
|
97
|
+
"doctor",
|
|
98
|
+
"toolchain:check-consumer",
|
|
62
99
|
// #3145 enforcement trio (test placement, scope provenance, gate composition)
|
|
63
100
|
"verify:test-boundary",
|
|
64
101
|
"verify:scope-provenance",
|
package/dist/check/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
2
2
|
export { CHECK_GRAPH_REQUIRED_NAMESPACES, CONSUMER_GATE_INTEGRITY_RECOVERY, type ConsumerGateIntegrityFinding, type ConsumerGateIntegrityResult, type ConsumerGateIntegritySeams, checkGraphOptionalIncludeViolations, evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, gateLocalName, gateNamespace, includeTaskfileRel, parseTaskfileIncludes, requiredNamespacesForGates, taskDefinedInTaskfileYaml, } from "./consumer-gate-integrity.js";
|
|
3
|
-
export { type CheckGateSpec, CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, } from "./gate-lists.js";
|
|
3
|
+
export { type CheckGateSpec, CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, isFastBeforeSlowOrder, isSuiteCheckGate, SUITE_CHECK_GATE_IDS, } from "./gate-lists.js";
|
|
4
4
|
export type { CheckOrchestratorOptions, CheckOrchestratorSeams } from "./orchestrator.js";
|
|
5
5
|
export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
|
|
6
6
|
export { detectTestRunner, type RunnerDetectResult, runnerDetectionTable, type TestRunnerKind, } from "./runner-detect.js";
|
package/dist/check/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
2
2
|
export { CHECK_GRAPH_REQUIRED_NAMESPACES, CONSUMER_GATE_INTEGRITY_RECOVERY, checkGraphOptionalIncludeViolations, evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, gateLocalName, gateNamespace, includeTaskfileRel, parseTaskfileIncludes, requiredNamespacesForGates, taskDefinedInTaskfileYaml, } from "./consumer-gate-integrity.js";
|
|
3
|
-
export { CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, } from "./gate-lists.js";
|
|
3
|
+
export { CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, isFastBeforeSlowOrder, isSuiteCheckGate, SUITE_CHECK_GATE_IDS, } from "./gate-lists.js";
|
|
4
4
|
export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
|
|
5
5
|
export { detectTestRunner, runnerDetectionTable, } from "./runner-detect.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* vendored-consumer context (#1519) and dispatches to the appropriate
|
|
7
7
|
* aggregate Taskfile target.
|
|
8
8
|
*
|
|
9
|
+
* Default path uses the cached sequential gate runner (#1713) with
|
|
10
|
+
* fast-before-slow ordering (#3188): cheap gates run before `ts:check-lane`
|
|
11
|
+
* (vitest+coverage). A fast-gate failure aborts before the suite starts.
|
|
12
|
+
*
|
|
9
13
|
* Exit codes (three-state, mirrors _project_context.py):
|
|
10
14
|
* 0 -- all gates passed
|
|
11
15
|
* 1 -- one or more gates failed
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* vendored-consumer context (#1519) and dispatches to the appropriate
|
|
7
7
|
* aggregate Taskfile target.
|
|
8
8
|
*
|
|
9
|
+
* Default path uses the cached sequential gate runner (#1713) with
|
|
10
|
+
* fast-before-slow ordering (#3188): cheap gates run before `ts:check-lane`
|
|
11
|
+
* (vitest+coverage). A fast-gate failure aborts before the suite starts.
|
|
12
|
+
*
|
|
9
13
|
* Exit codes (three-state, mirrors _project_context.py):
|
|
10
14
|
* 0 -- all gates passed
|
|
11
15
|
* 1 -- one or more gates failed
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ export declare function checkTypescript7SideBySide(projectRoot: string, seams?:
|
|
|
54
54
|
* `directive init` or `directive update`).
|
|
55
55
|
*/
|
|
56
56
|
export declare function checkGitignoreCoverage(projectRoot: string, seams?: CheckSeams): CheckResult;
|
|
57
|
+
/**
|
|
58
|
+
* Surface undecided / invalid coverageDebt + checkResume policy (#3189).
|
|
59
|
+
* Advisory skip when undecided; never hard-fails doctor / check:consumer.
|
|
60
|
+
* Decided-off is quiet; dismiss-with-reason is pass with reason in detail.
|
|
61
|
+
* Invalid typed blocks resolve fail-closed and surface via source=default-on-error.
|
|
62
|
+
*/
|
|
63
|
+
export declare function checkCoverageCheckResumePolicy(projectRoot: string): CheckResult;
|
|
57
64
|
export declare function deriveExitCode(checks: readonly CheckResult[], errors: readonly string[]): number;
|
|
58
65
|
export declare function runChecksImpl(projectRoot: string, seams?: CheckSeams & {
|
|
59
66
|
isDir?: (p: string) => boolean;
|