@deftai/directive-core 0.99.0 → 0.101.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.
Files changed (166) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +241 -30
  4. package/dist/authz/decompose-apply.d.ts +81 -0
  5. package/dist/authz/decompose-apply.js +377 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/check/cached-orchestrator.d.ts +26 -5
  13. package/dist/check/cached-orchestrator.js +228 -6
  14. package/dist/check/gate-lists.d.ts +10 -4
  15. package/dist/check/gate-lists.js +19 -5
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +2 -1
  18. package/dist/check/named-cause.d.ts +45 -0
  19. package/dist/check/named-cause.js +121 -0
  20. package/dist/check/orchestrator.js +6 -1
  21. package/dist/doctor/checks.d.ts +44 -0
  22. package/dist/doctor/checks.js +428 -2
  23. package/dist/doctor/help.d.ts +5 -0
  24. package/dist/doctor/help.js +31 -0
  25. package/dist/doctor/index.d.ts +2 -0
  26. package/dist/doctor/index.js +2 -0
  27. package/dist/doctor/main.d.ts +6 -7
  28. package/dist/doctor/main.js +130 -79
  29. package/dist/doctor/session-coda.d.ts +82 -0
  30. package/dist/doctor/session-coda.js +151 -0
  31. package/dist/doctor/types.d.ts +16 -0
  32. package/dist/doctor/which.d.ts +20 -1
  33. package/dist/doctor/which.js +67 -1
  34. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  35. package/dist/finish-loop/pr-finish-loop.js +116 -5
  36. package/dist/finish-loop/types.d.ts +1 -1
  37. package/dist/hooks/dispatcher.d.ts +9 -5
  38. package/dist/hooks/dispatcher.js +23 -15
  39. package/dist/hooks/readonly.d.ts +6 -3
  40. package/dist/hooks/readonly.js +44 -25
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +3 -0
  43. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  44. package/dist/init-deposit/agent-hooks.js +51 -31
  45. package/dist/init-deposit/gitignore.js +2 -0
  46. package/dist/intake/issue-ingest.js +36 -0
  47. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  48. package/dist/lifecycle/completed-consistency.js +443 -0
  49. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  50. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  51. package/dist/lifecycle/index.d.ts +2 -0
  52. package/dist/lifecycle/index.js +2 -0
  53. package/dist/literal-acceptance/capture.d.ts +52 -0
  54. package/dist/literal-acceptance/capture.js +658 -0
  55. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  56. package/dist/literal-acceptance/evaluate.js +193 -0
  57. package/dist/literal-acceptance/index.d.ts +12 -0
  58. package/dist/literal-acceptance/index.js +12 -0
  59. package/dist/literal-acceptance/run.d.ts +38 -0
  60. package/dist/literal-acceptance/run.js +216 -0
  61. package/dist/literal-acceptance/safety.d.ts +24 -0
  62. package/dist/literal-acceptance/safety.js +251 -0
  63. package/dist/literal-acceptance/types.d.ts +82 -0
  64. package/dist/literal-acceptance/types.js +26 -0
  65. package/dist/policy/ac-pass-banking.d.ts +58 -0
  66. package/dist/policy/ac-pass-banking.js +150 -0
  67. package/dist/policy/ceremony-dial.d.ts +96 -11
  68. package/dist/policy/ceremony-dial.js +250 -23
  69. package/dist/policy/index.d.ts +2 -0
  70. package/dist/policy/index.js +16 -1
  71. package/dist/policy/merge-approval-head.d.ts +129 -0
  72. package/dist/policy/merge-approval-head.js +461 -0
  73. package/dist/policy/require-human-merge.d.ts +5 -0
  74. package/dist/policy/require-human-merge.js +5 -0
  75. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  76. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  77. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  78. package/dist/pr-merge-readiness/compute.js +99 -14
  79. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  80. package/dist/pr-merge-readiness/gh.js +268 -0
  81. package/dist/pr-merge-readiness/index.d.ts +2 -2
  82. package/dist/pr-merge-readiness/index.js +2 -2
  83. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  84. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  85. package/dist/pr-wait-mergeable/cascade.js +115 -1
  86. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  87. package/dist/pr-wait-mergeable/main.js +4 -0
  88. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  89. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  90. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  91. package/dist/preflight/evaluate.d.ts +15 -1
  92. package/dist/preflight/evaluate.js +33 -2
  93. package/dist/preflight-cache/evaluate.d.ts +3 -0
  94. package/dist/preflight-cache/evaluate.js +3 -0
  95. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  96. package/dist/product-first-done-gate/acceptance.js +247 -0
  97. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  98. package/dist/product-first-done-gate/check-mode.js +148 -0
  99. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  100. package/dist/product-first-done-gate/evaluate.js +255 -0
  101. package/dist/product-first-done-gate/index.d.ts +11 -0
  102. package/dist/product-first-done-gate/index.js +11 -0
  103. package/dist/product-first-done-gate/types.d.ts +64 -0
  104. package/dist/product-first-done-gate/types.js +61 -0
  105. package/dist/render/framework-commands.js +4 -0
  106. package/dist/run-summary/emit.d.ts +56 -0
  107. package/dist/run-summary/emit.js +171 -0
  108. package/dist/run-summary/index.d.ts +4 -0
  109. package/dist/run-summary/index.js +4 -0
  110. package/dist/run-summary/path.d.ts +25 -0
  111. package/dist/run-summary/path.js +78 -0
  112. package/dist/run-summary/types.d.ts +76 -0
  113. package/dist/run-summary/types.js +21 -0
  114. package/dist/scope/acceptance-evidence.d.ts +110 -0
  115. package/dist/scope/acceptance-evidence.js +422 -0
  116. package/dist/scope/coverage-map.d.ts +98 -0
  117. package/dist/scope/coverage-map.js +558 -0
  118. package/dist/scope/decompose.d.ts +5 -0
  119. package/dist/scope/decompose.js +134 -11
  120. package/dist/scope/index.d.ts +3 -0
  121. package/dist/scope/index.js +3 -0
  122. package/dist/scope/parent-lineage.d.ts +125 -0
  123. package/dist/scope/parent-lineage.js +626 -0
  124. package/dist/scope/transition.d.ts +8 -0
  125. package/dist/scope/transition.js +61 -2
  126. package/dist/session/ac-pass-banking.d.ts +221 -0
  127. package/dist/session/ac-pass-banking.js +761 -0
  128. package/dist/session/active-cli.d.ts +79 -0
  129. package/dist/session/active-cli.js +382 -0
  130. package/dist/session/deposit-sha.d.ts +49 -0
  131. package/dist/session/deposit-sha.js +121 -0
  132. package/dist/session/effort-budget.d.ts +136 -0
  133. package/dist/session/effort-budget.js +352 -0
  134. package/dist/session/index.d.ts +7 -0
  135. package/dist/session/index.js +7 -0
  136. package/dist/session/orientation-compression.d.ts +127 -0
  137. package/dist/session/orientation-compression.js +425 -0
  138. package/dist/session/orientation-state.d.ts +29 -0
  139. package/dist/session/orientation-state.js +85 -0
  140. package/dist/session/session-start.d.ts +31 -0
  141. package/dist/session/session-start.js +228 -2
  142. package/dist/session/toolchain-preflight.d.ts +64 -0
  143. package/dist/session/toolchain-preflight.js +151 -0
  144. package/dist/session/verify-session-ritual.d.ts +14 -0
  145. package/dist/session/verify-session-ritual.js +33 -0
  146. package/dist/story-ready/evaluate.d.ts +11 -0
  147. package/dist/story-ready/evaluate.js +41 -3
  148. package/dist/triage/actions/candidates-log.d.ts +1 -2
  149. package/dist/triage/actions/candidates-log.js +37 -6
  150. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  151. package/dist/triage/bootstrap/gitignore.js +8 -2
  152. package/dist/triage/bootstrap/index.js +15 -3
  153. package/dist/triage/classify/index.d.ts +1 -0
  154. package/dist/triage/classify/index.js +2 -0
  155. package/dist/triage/classify/label-mirror.js +16 -0
  156. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  157. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  158. package/dist/triage/reconcile/reconcile.js +28 -6
  159. package/dist/triage/summary/index.js +12 -4
  160. package/dist/triage/welcome/default-mode.js +16 -0
  161. package/dist/ts-check-lane/run-lane.js +21 -4
  162. package/dist/vbrief-validate/conformance.js +7 -0
  163. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  164. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  165. package/dist/xbrief-migrate/migrate-project.js +92 -3
  166. package/package.json +15 -3
@@ -4,7 +4,7 @@
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { existsSync, readdirSync, readFileSync, renameSync, rmSync } from "node:fs";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
- import { containedWrite } from "../fs/contained-write.js";
7
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
8
8
  import { assertWriteTargetSafe } from "../fs/projection-containment.js";
9
9
  import { isHumanOrigin } from "./origin.js";
10
10
  import { authzAuditPath, authzGrantPath, authzGrantsDir, authzStatePath } from "./paths.js";
@@ -105,6 +105,13 @@ function parseScope(raw) {
105
105
  storyIds: readStringArray(rec.storyIds ?? rec.story_ids),
106
106
  issueIds: readNumberArray(rec.issueIds ?? rec.issue_ids),
107
107
  cohortId: readString(rec, "cohortId") ?? readString(rec, "cohort_id"),
108
+ // #3239 structural decompose apply bindings (snake_case accepted on load).
109
+ contentDigest: readString(rec, "contentDigest") ??
110
+ readString(rec, "content_digest") ??
111
+ readString(rec, "draftDigest") ??
112
+ readString(rec, "draft_digest"),
113
+ parentPath: readString(rec, "parentPath") ?? readString(rec, "parent_path"),
114
+ targetPath: readString(rec, "targetPath") ?? readString(rec, "target_path"),
108
115
  };
109
116
  }
110
117
  function parseSemantics(raw) {
@@ -233,6 +240,217 @@ export function markGrantUsed(projectRoot, grantId, now = new Date()) {
233
240
  saveGrant(projectRoot, used);
234
241
  return used;
235
242
  }
243
+ function isProcessAlive(pid) {
244
+ if (!Number.isFinite(pid) || pid <= 0)
245
+ return false;
246
+ try {
247
+ process.kill(pid, 0);
248
+ return true;
249
+ }
250
+ catch (err) {
251
+ const code = err.code;
252
+ // ESRCH → dead. EPERM → exists but unsignalable — treat as alive (never reclaim).
253
+ if (code === "EPERM")
254
+ return true;
255
+ return false;
256
+ }
257
+ }
258
+ /**
259
+ * Whether a claim lock may be reclaimed after a crashed holder (#3239).
260
+ * Live owner PIDs are never reclaimed (no mtime-only steal of a live critical section).
261
+ * Corrupt / unreadable records are reclaimable; PID-reuse residual needs manual delete.
262
+ */
263
+ export function isGrantClaimLockReclaimable(rec) {
264
+ if (rec === null)
265
+ return true;
266
+ return !isProcessAlive(rec.pid);
267
+ }
268
+ function readGrantClaimLockRecord(lockPath) {
269
+ try {
270
+ const raw = readFileSync(lockPath, "utf8").trim();
271
+ // JSON form (current). Legacy: "pid\niso\n" lines from earlier #3239 revisions.
272
+ if (raw.startsWith("{")) {
273
+ const parsed = JSON.parse(raw);
274
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
275
+ return null;
276
+ const o = parsed;
277
+ const pid = typeof o.pid === "number" ? o.pid : Number(o.pid);
278
+ const startedAt = typeof o.startedAt === "string" ? o.startedAt : "";
279
+ const token = typeof o.token === "string" ? o.token : "";
280
+ if (!Number.isFinite(pid) || token.length === 0)
281
+ return null;
282
+ return { pid, startedAt, token };
283
+ }
284
+ const lines = raw.split(/\r?\n/);
285
+ const pid = Number(lines[0]);
286
+ if (!Number.isFinite(pid))
287
+ return null;
288
+ return {
289
+ pid,
290
+ startedAt: typeof lines[1] === "string" ? lines[1] : "",
291
+ token: "legacy",
292
+ };
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
298
+ /**
299
+ * Claim a single-use grant for structural apply (#3239).
300
+ *
301
+ * Concurrent-safe + failure-safe order when `apply` is provided:
302
+ * exclusive lock → re-load → revalidate → mark single-use usedAt → apply (writes)
303
+ * → on apply throw: rollback usedAt + release lock (grant reusable for retry).
304
+ * Multi-use grants run apply (if any) without mutating usedAt.
305
+ * Without `apply`, single-use is marked under the lock (claim-only / test path).
306
+ *
307
+ * Crash after mark + before rollback: grant stays spent (no double-apply); operator remints.
308
+ * Dead-PID / corrupt locks reclaim via **rename-away** of the old lock path (only one
309
+ * renamer wins) then exclusive create — never blind rmSync of a live winner's lock.
310
+ * Live PIDs are never reclaimed. PID-reuse residual: operator deletes the lock file.
311
+ */
312
+ export function claimSingleUseGrantForApply(projectRoot, grantId, options = {}) {
313
+ // Back-compat: second arg was `now: Date` in the first #3239 revision.
314
+ const opts = options instanceof Date ? { now: options } : options;
315
+ const now = opts.now ?? new Date();
316
+ const usedAtIso = utcIso(now);
317
+ const safe = grantId.replace(/[^a-zA-Z0-9._-]/g, "_");
318
+ const root = resolve(projectRoot);
319
+ const lockRel = join(".deft", "authz", "locks", `${safe}.lock`);
320
+ const lockPath = join(root, lockRel);
321
+ const lockToken = randomBytes(8).toString("hex");
322
+ const lockBody = `${JSON.stringify({
323
+ pid: process.pid,
324
+ startedAt: usedAtIso,
325
+ token: lockToken,
326
+ })}\n`;
327
+ const tryCreateLock = () => {
328
+ try {
329
+ containedWrite({
330
+ root,
331
+ target: lockPath,
332
+ data: lockBody,
333
+ mode: "create",
334
+ });
335
+ return true;
336
+ }
337
+ catch (err) {
338
+ if (err instanceof ContainedWriteError && err.code === "CONTAINED_WRITE_EXISTS") {
339
+ return false;
340
+ }
341
+ // Other containment/IO failures fail closed as reservation denial.
342
+ return false;
343
+ }
344
+ };
345
+ /** True when the on-disk lock still carries our token (not stolen mid-section). */
346
+ const stillOwnLock = () => {
347
+ const rec = readGrantClaimLockRecord(lockPath);
348
+ return rec !== null && rec.token === lockToken && rec.pid === process.pid;
349
+ };
350
+ let locked = tryCreateLock();
351
+ if (!locked) {
352
+ // Dead-PID / corrupt reclaim: rename the old lock aside (atomic contention) then create.
353
+ // Blind rmSync is forbidden — a second reclaimer must not delete a winner's new lock.
354
+ const existing = readGrantClaimLockRecord(lockPath);
355
+ if (isGrantClaimLockReclaimable(existing)) {
356
+ const side = `${lockPath}.reclaim.${randomBytes(6).toString("hex")}`;
357
+ try {
358
+ renameSync(lockPath, side);
359
+ try {
360
+ rmSync(side, { force: true });
361
+ }
362
+ catch {
363
+ /* best-effort side cleanup */
364
+ }
365
+ }
366
+ catch {
367
+ // Lost rename race or lock already gone — fall through to exclusive create.
368
+ }
369
+ locked = tryCreateLock();
370
+ }
371
+ }
372
+ if (!locked) {
373
+ return {
374
+ ok: false,
375
+ reason: `Directive denied scope:decompose apply: grant ${grantId} is already reserved ` +
376
+ "or spent by a concurrent apply. Human action required: remint if the prior apply failed " +
377
+ "(or remove a leftover `.deft/authz/locks/<id>.lock` after a dead-holder crash if reclaim fails).",
378
+ };
379
+ }
380
+ let markedUsedAt = null;
381
+ try {
382
+ const grant = loadGrant(projectRoot, grantId);
383
+ if (grant === null) {
384
+ return {
385
+ ok: false,
386
+ reason: `Directive denied scope:decompose apply: grant ${grantId} missing.`,
387
+ };
388
+ }
389
+ if (opts.revalidate !== undefined) {
390
+ const check = opts.revalidate(grant);
391
+ if (!check.ok) {
392
+ return { ok: false, reason: check.reason };
393
+ }
394
+ }
395
+ if (!grant.semantics.singleUse) {
396
+ if (opts.apply !== undefined) {
397
+ opts.apply(grant);
398
+ }
399
+ return { ok: true, grant };
400
+ }
401
+ if (grant.semantics.usedAt !== null) {
402
+ return {
403
+ ok: false,
404
+ reason: `Directive denied scope:decompose apply: single-use grant ${grantId} already spent at ` +
405
+ `${grant.semantics.usedAt}.`,
406
+ };
407
+ }
408
+ // Spend under lock before protected writes so a post-write mark failure cannot
409
+ // leave completed mutations behind an unspent single-use grant (#3239 Greptile).
410
+ // Apply throw rolls usedAt back while the lock is still held (retry-safe).
411
+ const used = {
412
+ ...grant,
413
+ semantics: {
414
+ ...grant.semantics,
415
+ usedAt: usedAtIso,
416
+ },
417
+ };
418
+ saveGrant(projectRoot, used);
419
+ markedUsedAt = usedAtIso;
420
+ if (opts.apply !== undefined) {
421
+ try {
422
+ opts.apply(used);
423
+ }
424
+ catch (applyErr) {
425
+ // Rollback spend so a failed multi-file apply does not strand the approval.
426
+ const current = loadGrant(projectRoot, grantId);
427
+ if (current?.semantics.singleUse && current.semantics.usedAt === markedUsedAt) {
428
+ const restored = {
429
+ ...current,
430
+ semantics: { ...current.semantics, usedAt: null },
431
+ };
432
+ saveGrant(projectRoot, restored);
433
+ markedUsedAt = null;
434
+ }
435
+ throw applyErr;
436
+ }
437
+ }
438
+ if (!stillOwnLock()) {
439
+ return {
440
+ ok: false,
441
+ reason: `Directive denied scope:decompose apply: grant ${grantId} lock ownership lost mid-claim. ` +
442
+ "Human action required: inspect partial outputs and remint if needed.",
443
+ };
444
+ }
445
+ return { ok: true, grant: used };
446
+ }
447
+ finally {
448
+ // Only remove the lock when we still own it — never delete a successor's claim.
449
+ if (stillOwnLock()) {
450
+ rmSync(lockPath, { force: true });
451
+ }
452
+ }
453
+ }
236
454
  export function saveAuthzState(projectRoot, state) {
237
455
  writeJsonContained(projectRoot, authzStatePath(projectRoot), state);
238
456
  }
@@ -5,8 +5,10 @@
5
5
  * agent authors must not independently satisfy implementation-approval gates.
6
6
  * Local-file forgery by a credential-compromised agent remains #983-class OOS.
7
7
  */
8
- /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs). */
9
- export declare const AUTHZ_OPERATIONS: readonly ["edit", "push", "pr", "merge", "settings", "deployment", "issue_mutation", "release-cut", "release-publish", "release-rollback"];
8
+ /** Structural apply op for scope:decompose (#3239 / epic #3237 Q2). Always gated. */
9
+ export declare const SCOPE_DECOMPOSE_APPLY_STRUCTURAL: "scope.decompose.apply.structural";
10
+ /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs + #3239). */
11
+ export declare const AUTHZ_OPERATIONS: readonly ["edit", "push", "pr", "merge", "settings", "deployment", "issue_mutation", "release-cut", "release-publish", "release-rollback", "scope.decompose.apply.structural"];
10
12
  export type AuthzOperation = (typeof AUTHZ_OPERATIONS)[number];
11
13
  /**
12
14
  * Human-origin kinds accepted as approval provenance.
@@ -41,6 +43,21 @@ export interface GrantScope {
41
43
  * for product mutations; approving one cohort does not clear UAT.
42
44
  */
43
45
  readonly cohortId: string | null;
46
+ /**
47
+ * SHA-256 hex of exact bound artifact bytes (decompose draft for #3239).
48
+ * Unset/null = unbound (cannot satisfy structural decompose apply).
49
+ */
50
+ readonly contentDigest?: string | null;
51
+ /**
52
+ * Project-relative parent artifact path for structural decompose apply (#3239).
53
+ * Unset/null = unbound.
54
+ */
55
+ readonly parentPath?: string | null;
56
+ /**
57
+ * Project-relative draft/target path for structural decompose apply (#3239).
58
+ * Unset/null = unbound.
59
+ */
60
+ readonly targetPath?: string | null;
44
61
  }
45
62
  export interface GrantSemantics {
46
63
  readonly expiresAt: string | null;
@@ -5,7 +5,9 @@
5
5
  * agent authors must not independently satisfy implementation-approval gates.
6
6
  * Local-file forgery by a credential-compromised agent remains #983-class OOS.
7
7
  */
8
- /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs). */
8
+ /** Structural apply op for scope:decompose (#3239 / epic #3237 Q2). Always gated. */
9
+ export const SCOPE_DECOMPOSE_APPLY_STRUCTURAL = "scope.decompose.apply.structural";
10
+ /** Operation classes bound on a grant (Wave 1 structural binding + Wave 4 closed verbs + #3239). */
9
11
  export const AUTHZ_OPERATIONS = [
10
12
  "edit",
11
13
  "push",
@@ -18,6 +20,8 @@ export const AUTHZ_OPERATIONS = [
18
20
  "release-cut",
19
21
  "release-publish",
20
22
  "release-rollback",
23
+ /** Human-origin exact-draft-digest gate for scope:decompose apply (#3239). */
24
+ SCOPE_DECOMPOSE_APPLY_STRUCTURAL,
21
25
  ];
22
26
  /**
23
27
  * Human-origin kinds accepted as approval provenance.
@@ -1,4 +1,5 @@
1
1
  import type { TaskRunResult } from "../cache/task-cache/types.js";
2
+ import { type ToolchainPreflightResult } from "../session/toolchain-preflight.js";
2
3
  import { type CheckOrchestratorSeams } from "./context.js";
3
4
  export interface CachedCheckOptions extends CheckOrchestratorSeams {
4
5
  readonly onGateStart?: (gateId: string) => void;
@@ -6,16 +7,36 @@ export interface CachedCheckOptions extends CheckOrchestratorSeams {
6
7
  readonly gateSpawnFn?: (gateId: string, taskBin: string, taskArgs: string[], opts: {
7
8
  cwd: string;
8
9
  env?: NodeJS.ProcessEnv;
9
- }) => Pick<TaskRunResult, "exitCode" | "stdout" | "stderr">;
10
+ }) => Pick<TaskRunResult, "exitCode" | "stdout" | "stderr"> & {
11
+ spawnError?: string;
12
+ };
13
+ /**
14
+ * #3282: inject preflight (tests). When omitted, runs live toolchain preflight.
15
+ * Pass `null` to skip preflight entirely (legacy test paths).
16
+ */
17
+ readonly preflight?: ToolchainPreflightResult | null;
18
+ /** #3282: session id for run-summary lines (default: new UUID). */
19
+ readonly sessionId?: string;
20
+ /** #3282: disable run-summary emission (tests). */
21
+ readonly emitRunSummary?: boolean;
10
22
  }
11
23
  /**
12
24
  * Run check gates sequentially with content-hash caching (#1713).
13
25
  * Falls back to fail-open execution for undeclared / non-cacheable gates.
14
26
  *
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).
27
+ * Gate order (#3284 / #3188):
28
+ * 1. Product AC (`verify:ac`) first fail-fast; never skippable when commands exist
29
+ * 2. Hygiene preflight (may be advisory under pressure / degraded)
30
+ * 3. Suite last (`ts:check-lane`)
31
+ *
32
+ * Modes (env / ceremony dial / hard budget — see resolveProductFirstCheckMode):
33
+ * - full: AC hard → hygiene hard → suite
34
+ * - pressure: AC hard → hygiene advisory → suite
35
+ * - rapid: AC only (ceremony dial rapid/minimal positive content)
36
+ *
37
+ * #3282: toolchain preflight enables degraded skip report when go-task/pnpm
38
+ * are missing; gate failures print named cause + remedy; run-summary JSONL
39
+ * is appended when DEFT_RUN_SUMMARY_PATH is set (fail-open).
19
40
  */
20
41
  export declare function dispatchCachedTaskCheck(frameworkRoot: string, projectRoot: string, options?: CachedCheckOptions): number;
21
42
  //# sourceMappingURL=cached-orchestrator.d.ts.map
@@ -1,30 +1,60 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { join, resolve } from "node:path";
3
4
  import { lintShippedRegistry, resolveTaskContract, runWithCache, } from "../cache/task-cache/index.js";
4
5
  import { readCorePackageVersion } from "../engine-version.js";
6
+ import { applyProductFirstGateMode, isHygieneGate, isProductAcGate, resolveProductFirstCheckMode, } from "../product-first-done-gate/index.js";
7
+ import { RunSummaryEmitter } from "../run-summary/emit.js";
8
+ import { runToolchainPreflight, SKIP_ALL_GATES, } from "../session/toolchain-preflight.js";
5
9
  import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "./consumer-gate-integrity.js";
6
10
  import { resolveCheckTarget } from "./context.js";
7
11
  import { checkGateId, checkGateSpawnArgs, gatesForCheckTarget, isSuiteCheckGate, } from "./gate-lists.js";
12
+ import { formatDegradedSkipReport, formatNamedCauseFailure, remedyForGate } from "./named-cause.js";
8
13
  function captureSpawn(taskBin, args, opts) {
9
14
  const result = spawnSync(taskBin, args, {
10
15
  cwd: opts.cwd,
11
16
  encoding: "utf8",
12
17
  env: opts.env ?? process.env,
13
18
  });
19
+ if (result.error !== undefined) {
20
+ return {
21
+ exitCode: result.status ?? 1,
22
+ stdout: result.stdout ?? "",
23
+ stderr: result.stderr ?? "",
24
+ spawnError: result.error.message,
25
+ };
26
+ }
14
27
  return {
15
28
  exitCode: result.status ?? 1,
16
29
  stdout: result.stdout ?? "",
17
30
  stderr: result.stderr ?? "",
18
31
  };
19
32
  }
33
+ function writeLines(lines, stream = "stderr") {
34
+ const write = stream === "stdout"
35
+ ? process.stdout.write.bind(process.stdout)
36
+ : process.stderr.write.bind(process.stderr);
37
+ for (const line of lines) {
38
+ write(`${line}\n`);
39
+ }
40
+ }
20
41
  /**
21
42
  * Run check gates sequentially with content-hash caching (#1713).
22
43
  * Falls back to fail-open execution for undeclared / non-cacheable gates.
23
44
  *
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).
45
+ * Gate order (#3284 / #3188):
46
+ * 1. Product AC (`verify:ac`) first fail-fast; never skippable when commands exist
47
+ * 2. Hygiene preflight (may be advisory under pressure / degraded)
48
+ * 3. Suite last (`ts:check-lane`)
49
+ *
50
+ * Modes (env / ceremony dial / hard budget — see resolveProductFirstCheckMode):
51
+ * - full: AC hard → hygiene hard → suite
52
+ * - pressure: AC hard → hygiene advisory → suite
53
+ * - rapid: AC only (ceremony dial rapid/minimal positive content)
54
+ *
55
+ * #3282: toolchain preflight enables degraded skip report when go-task/pnpm
56
+ * are missing; gate failures print named cause + remedy; run-summary JSONL
57
+ * is appended when DEFT_RUN_SUMMARY_PATH is set (fail-open).
28
58
  */
29
59
  export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}) {
30
60
  const resolvedFramework = resolve(frameworkRoot);
@@ -33,8 +63,41 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
33
63
  const taskBin = options.taskBin ?? "task";
34
64
  const target = resolveCheckTarget(resolvedFramework, resolvedProject);
35
65
  const cwd = target === "check:framework-source" ? resolvedFramework : resolvedProject;
36
- const gates = gatesForCheckTarget(target);
66
+ const modeResolution = resolveProductFirstCheckMode({
67
+ environ: options.env ?? process.env,
68
+ projectRoot: resolvedProject,
69
+ });
70
+ const baseGates = gatesForCheckTarget(target);
71
+ const gates = applyProductFirstGateMode(baseGates, modeResolution.mode, checkGateId);
37
72
  const codeVersion = readCorePackageVersion();
73
+ const sessionId = options.sessionId ?? randomUUID();
74
+ const gateOutcomes = [];
75
+ const emitSummary = (exitCode, degraded) => {
76
+ if (options.emitRunSummary === false)
77
+ return;
78
+ try {
79
+ const emitter = new RunSummaryEmitter({
80
+ projectRoot: resolvedProject,
81
+ sessionId,
82
+ frameworkVersion: codeVersion,
83
+ env: options.env,
84
+ });
85
+ emitter.emitCheckInvocation({
86
+ target,
87
+ exit_code: exitCode,
88
+ degraded,
89
+ gates: gateOutcomes,
90
+ });
91
+ }
92
+ catch {
93
+ // fail-open
94
+ }
95
+ };
96
+ if (modeResolution.mode !== "full") {
97
+ process.stderr.write(`check: product-first mode=${modeResolution.mode} ` +
98
+ `(sources=${modeResolution.sources.join(",")}; ` +
99
+ `hygieneAdvisory=${modeResolution.hygieneAdvisory}; acOnly=${modeResolution.acOnly}) (#3284)\n`);
100
+ }
38
101
  // #3070: fail loud with deposit-repair guidance when consumer check-graph
39
102
  // includes (e.g. tasks/verify.yml for verify:orphan-active) are missing,
40
103
  // instead of opaque go-task "Task does not exist" exit 200/201.
@@ -42,6 +105,7 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
42
105
  const integrity = evaluateConsumerGateIntegrity(resolvedFramework);
43
106
  if (!integrity.ok) {
44
107
  process.stderr.write(formatConsumerGateIntegrityFailure(integrity));
108
+ emitSummary(2, false);
45
109
  return 2;
46
110
  }
47
111
  }
@@ -50,22 +114,86 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
50
114
  for (const finding of registryLint.findings.filter((f) => f.kind === "under-declared-input")) {
51
115
  process.stderr.write(`check: task registry lint failed for ${finding.taskId}: ${finding.detail}\n`);
52
116
  }
117
+ emitSummary(2, false);
53
118
  return 2;
54
119
  }
55
120
  if (gates.length === 0) {
56
121
  process.stderr.write(`check: no gate list for target ${target}\n`);
122
+ emitSummary(2, false);
57
123
  return 2;
58
124
  }
125
+ // #3282: toolchain preflight — degraded skip when framework tools missing.
126
+ const preflight = options.preflight === undefined
127
+ ? runToolchainPreflight({
128
+ projectRoot: resolvedProject,
129
+ frameworkRoot: resolvedFramework,
130
+ })
131
+ : options.preflight;
132
+ // Expand SKIP_ALL_GATES sentinel to the live composition (avoids hardcode drift).
133
+ const skipSet = new Set();
134
+ for (const id of preflight?.skipGateIds ?? []) {
135
+ if (id === SKIP_ALL_GATES) {
136
+ for (const g of gates) {
137
+ skipSet.add(checkGateId(g));
138
+ }
139
+ }
140
+ else {
141
+ skipSet.add(id);
142
+ }
143
+ }
144
+ const degraded = preflight?.degraded === true;
145
+ if (preflight?.degraded) {
146
+ for (const line of preflight.lines) {
147
+ process.stderr.write(`${line}\n`);
148
+ }
149
+ }
150
+ // When task/node are missing, every gate is skipped — report named skips and
151
+ // exit 2 (config/environment), never exit 0 green (#3282 Greptile P1).
152
+ // Missing framework tooling must not look like a clean product pass.
153
+ if (degraded && skipSet.size > 0) {
154
+ const allSkipped = gates.every((g) => skipSet.has(checkGateId(g)));
155
+ if (allSkipped) {
156
+ const skipped = gates.map((g) => {
157
+ const id = checkGateId(g);
158
+ const cause = preflight?.findings.find((f) => !f.present)?.cause ?? "toolchain preflight degraded";
159
+ const remedy = preflight?.findings.find((f) => !f.present)?.remedy ?? remedyForGate(id, cause);
160
+ gateOutcomes.push({ id, status: "skipped", cause, remedy });
161
+ return { id, cause, remedy };
162
+ });
163
+ writeLines(formatDegradedSkipReport({
164
+ reason: "done-gate toolchain incomplete at check start",
165
+ skipped,
166
+ exitCode: 2,
167
+ }));
168
+ emitSummary(2, true);
169
+ return 2;
170
+ }
171
+ }
59
172
  for (const gateSpec of gates) {
60
173
  const gateId = checkGateId(gateSpec);
174
+ // #3282: skip gates that require missing tools (e.g. pnpm-only suite).
175
+ if (skipSet.has(gateId)) {
176
+ const missing = preflight?.findings.find((f) => !f.present);
177
+ const cause = missing?.cause ?? "toolchain preflight marked gate skippable";
178
+ const remedy = missing?.remedy ?? remedyForGate(gateId, cause);
179
+ process.stderr.write(`check: skipping gate ${gateId} (degraded) — cause: ${cause}; remedy: ${remedy}\n`);
180
+ gateOutcomes.push({ id: gateId, status: "skipped", cause, remedy });
181
+ options.onGateStart?.(gateId);
182
+ options.onGateComplete?.(gateId, 0, false);
183
+ continue;
184
+ }
61
185
  // #3188: log suite entry so operators/tests can prove fast failures never
62
186
  // reach vitest+coverage (suite gates are ordered last in gate-lists).
63
187
  if (isSuiteCheckGate(gateSpec)) {
64
188
  process.stderr.write(`check: starting suite gate ${gateId} after fast preflight (#3188)\n`);
65
189
  }
190
+ if (isProductAcGate(gateId)) {
191
+ process.stderr.write(`check: product AC gate ${gateId} first (#3284)\n`);
192
+ }
66
193
  options.onGateStart?.(gateId);
67
194
  const contract = resolveTaskContract(gateId);
68
195
  const taskArgs = checkGateSpawnArgs(gateSpec, taskfilePath);
196
+ let lastSpawn = { exitCode: 0, stdout: "", stderr: "" };
69
197
  const result = runWithCache({
70
198
  projectRoot: cwd,
71
199
  contract,
@@ -78,6 +206,12 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
78
206
  env: options.env,
79
207
  })
80
208
  : captureSpawn(taskBin, taskArgs, { cwd, env: options.env });
209
+ lastSpawn = {
210
+ exitCode: spawned.exitCode,
211
+ stdout: spawned.stdout,
212
+ stderr: spawned.stderr,
213
+ spawnError: "spawnError" in spawned ? spawned.spawnError : undefined,
214
+ };
81
215
  if (spawned.stdout.length > 0) {
82
216
  process.stdout.write(spawned.stdout);
83
217
  }
@@ -88,17 +222,105 @@ export function dispatchCachedTaskCheck(frameworkRoot, projectRoot, options = {}
88
222
  },
89
223
  });
90
224
  options.onGateComplete?.(gateId, result.exitCode, result.fromCache);
91
- // Fail-fast: do not start later gates (including suite) after a failure.
225
+ // Fail-fast: do not start later gates (including suite) after a failure
226
+ // unless this is a hygiene gate under pressure mode (advisory only, #3284).
92
227
  if (result.exitCode !== 0) {
228
+ if (modeResolution.hygieneAdvisory && isHygieneGate(gateId) && !isProductAcGate(gateId)) {
229
+ process.stderr.write(`check: hygiene gate ${gateId} failed (exit ${result.exitCode}) but is ADVISORY ` +
230
+ `under ${modeResolution.mode} mode — continuing (#3284)\n`);
231
+ const named = formatNamedCauseFailure({
232
+ gateId,
233
+ exitCode: result.exitCode,
234
+ stdout: lastSpawn.stdout,
235
+ stderr: lastSpawn.stderr,
236
+ spawnError: lastSpawn.spawnError,
237
+ });
238
+ writeLines(named.lines);
239
+ gateOutcomes.push({
240
+ id: gateId,
241
+ status: "run",
242
+ exit_code: result.exitCode,
243
+ cause: `advisory hygiene failure: ${named.cause}`,
244
+ remedy: named.remedy,
245
+ from_cache: result.fromCache,
246
+ });
247
+ continue;
248
+ }
249
+ const named = formatNamedCauseFailure({
250
+ gateId,
251
+ exitCode: result.exitCode,
252
+ stdout: lastSpawn.stdout,
253
+ stderr: lastSpawn.stderr,
254
+ spawnError: lastSpawn.spawnError,
255
+ });
256
+ writeLines(named.lines);
257
+ gateOutcomes.push({
258
+ id: gateId,
259
+ status: "failed",
260
+ exit_code: result.exitCode,
261
+ cause: named.cause,
262
+ remedy: named.remedy,
263
+ from_cache: result.fromCache,
264
+ });
265
+ // Mark remaining as skipped for the summary (not run).
266
+ let sawCurrent = false;
267
+ for (const later of gates) {
268
+ const id = checkGateId(later);
269
+ if (!sawCurrent) {
270
+ if (id === gateId)
271
+ sawCurrent = true;
272
+ continue;
273
+ }
274
+ if (!gateOutcomes.some((o) => o.id === id)) {
275
+ gateOutcomes.push({
276
+ id,
277
+ status: "skipped",
278
+ cause: `skipped after ${gateId} failed`,
279
+ remedy: "Fix the failed gate above, then re-run task check",
280
+ });
281
+ }
282
+ }
283
+ if (isProductAcGate(gateId)) {
284
+ process.stderr.write(`check: product AC gate ${gateId} failed (exit ${result.exitCode}); ` +
285
+ `failing closed before hygiene (#3284)\n`);
286
+ emitSummary(result.exitCode, degraded);
287
+ return result.exitCode;
288
+ }
93
289
  if (!isSuiteCheckGate(gateSpec)) {
94
290
  const remaining = gates.some(isSuiteCheckGate)
95
291
  ? "skipping remaining gates including suite"
96
292
  : "skipping remaining gates";
97
293
  process.stderr.write(`check: fast gate ${gateId} failed (exit ${result.exitCode}); ${remaining} (#3188)\n`);
98
294
  }
295
+ emitSummary(result.exitCode, degraded);
99
296
  return result.exitCode;
100
297
  }
298
+ gateOutcomes.push({
299
+ id: gateId,
300
+ status: "run",
301
+ exit_code: 0,
302
+ from_cache: result.fromCache,
303
+ });
304
+ }
305
+ // Partial degraded (some gates skipped for pnpm, others ran): never report a
306
+ // green pass when required suite/toolchain gates were skipped (#3282 Greptile P1).
307
+ const skippedOutcomes = gateOutcomes.filter((o) => o.status === "skipped");
308
+ if (degraded && skippedOutcomes.length > 0) {
309
+ const skipped = skippedOutcomes.map((o) => ({
310
+ id: o.id,
311
+ cause: o.cause ?? "degraded",
312
+ remedy: o.remedy ?? remedyForGate(o.id, o.cause ?? "degraded"),
313
+ }));
314
+ writeLines(formatDegradedSkipReport({
315
+ reason: "partial toolchain; skipped gates did not run",
316
+ skipped,
317
+ ran: gateOutcomes.filter((o) => o.status === "run").map((o) => o.id),
318
+ exitCode: 2,
319
+ }));
320
+ emitSummary(2, true);
321
+ return 2;
101
322
  }
323
+ emitSummary(0, degraded);
102
324
  return 0;
103
325
  }
104
326
  //# sourceMappingURL=cached-orchestrator.js.map