@deftai/directive-core 0.105.0 → 0.106.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 (85) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/named-cause.js +1 -1
  3. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  4. package/dist/forward-coverage/diff-coverage.js +112 -0
  5. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  6. package/dist/forward-coverage/diff-lines.js +173 -0
  7. package/dist/forward-coverage/evaluate.d.ts +24 -9
  8. package/dist/forward-coverage/evaluate.js +125 -19
  9. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  10. package/dist/init-deposit/agent-hooks.js +1 -1
  11. package/dist/intake/clause-derivation.d.ts +9 -0
  12. package/dist/intake/clause-derivation.js +30 -3
  13. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  14. package/dist/lifecycle-visible/evaluate.js +710 -0
  15. package/dist/lifecycle-visible/index.d.ts +2 -0
  16. package/dist/lifecycle-visible/index.js +2 -0
  17. package/dist/literal-acceptance/capture.js +3 -3
  18. package/dist/orchestration/judgment-policy.d.ts +13 -0
  19. package/dist/orchestration/judgment-policy.js +15 -0
  20. package/dist/orchestration/probe-session.js +13 -8
  21. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  22. package/dist/orchestration/verify-judgment-gates.js +43 -11
  23. package/dist/policy/ceremony-dial.js +7 -7
  24. package/dist/policy/host-hooks.d.ts +20 -0
  25. package/dist/policy/host-hooks.js +121 -2
  26. package/dist/policy/org-force-on-migration.js +2 -1
  27. package/dist/policy/plan-extensions.d.ts +1 -1
  28. package/dist/policy/plan-extensions.js +2 -0
  29. package/dist/policy/product-signal.js +6 -7
  30. package/dist/policy/require-human-merge.js +8 -6
  31. package/dist/policy/resolve.d.ts +7 -2
  32. package/dist/policy/resolve.js +25 -8
  33. package/dist/policy/value-feedback.js +7 -8
  34. package/dist/preflight-cache/evaluate.d.ts +12 -0
  35. package/dist/preflight-cache/evaluate.js +15 -1
  36. package/dist/preflight-cache/index.d.ts +1 -1
  37. package/dist/preflight-cache/index.js +1 -1
  38. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  39. package/dist/product-first-done-gate/evaluate.js +46 -5
  40. package/dist/release/closed-verb-gate.d.ts +12 -0
  41. package/dist/release/closed-verb-gate.js +51 -0
  42. package/dist/release/index.d.ts +1 -0
  43. package/dist/release/index.js +1 -0
  44. package/dist/release/pipeline.js +7 -0
  45. package/dist/release/types.d.ts +12 -0
  46. package/dist/render/constants.d.ts +2 -1
  47. package/dist/render/constants.js +2 -1
  48. package/dist/render/framework-commands.js +4 -0
  49. package/dist/render/spec-validate.js +23 -12
  50. package/dist/run-summary/types.d.ts +4 -0
  51. package/dist/scope/main.js +15 -5
  52. package/dist/scope/promote-from-issue.js +5 -5
  53. package/dist/scope-provenance/evaluate.js +3 -3
  54. package/dist/session/ac-pass-banking.d.ts +5 -0
  55. package/dist/session/ac-pass-banking.js +3 -2
  56. package/dist/session/process-cost.d.ts +32 -10
  57. package/dist/session/process-cost.js +189 -1
  58. package/dist/session/product-state-hash.js +41 -24
  59. package/dist/session/ritual-entrypoint.js +16 -1
  60. package/dist/session/session-start.d.ts +6 -0
  61. package/dist/session/session-start.js +32 -2
  62. package/dist/session/verify-session-ritual.d.ts +24 -1
  63. package/dist/session/verify-session-ritual.js +89 -6
  64. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  65. package/dist/swarm/finalize-cohort-cli.js +165 -35
  66. package/dist/swarm/finalize-cohort.d.ts +1 -0
  67. package/dist/swarm/finalize-cohort.js +14 -2
  68. package/dist/swarm/routing-set-cli.js +2 -0
  69. package/dist/swarm/routing-verify.d.ts +1 -1
  70. package/dist/swarm/routing-verify.js +2 -2
  71. package/dist/triage/bootstrap/index.js +6 -6
  72. package/dist/triage/help/registry-data.d.ts +1 -1
  73. package/dist/triage/help/registry-data.js +1 -1
  74. package/dist/triage/welcome/writers.d.ts +1 -1
  75. package/dist/triage/welcome/writers.js +10 -5
  76. package/dist/value/readback.d.ts +4 -1
  77. package/dist/value/readback.js +51 -12
  78. package/dist/vbrief-validate/schema.d.ts +4 -0
  79. package/dist/vbrief-validate/schema.js +1 -1
  80. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  81. package/dist/verify-env/agent-hook-readiness.js +12 -8
  82. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  83. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  84. package/dist/verify-env/agent-hooks.js +3 -4
  85. package/package.json +7 -3
@@ -13,6 +13,16 @@ export const ENV_BYPASS = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
13
13
  export const LEGACY_NARRATIVE_KEY = "Allow direct commits to master";
14
14
  /** Audit log relative path (#746). */
15
15
  export const AUDIT_LOG_REL_PATH = "meta/policy-changes.log";
16
+ /** Operator line when a policy writer matches current state and does not append (#3528). */
17
+ export const POLICY_AUDIT_NOOP_STDOUT = " no-op: value already matched (ledger unchanged).";
18
+ const CHANGED_TOKEN_RE = /(?:^|\s)changed=(?:true|false)(?:\s|$)/;
19
+ /** Stamp a machine-readable `changed=true|false` token (#3528 / UPGRADING.md). */
20
+ export function stampChangedToken(entry, changed) {
21
+ if (CHANGED_TOKEN_RE.test(entry)) {
22
+ return entry;
23
+ }
24
+ return `${entry} changed=${changed ? "true" : "false"}`;
25
+ }
16
26
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
17
27
  /**
18
28
  * Resolve absolute path to PROJECT-DEFINITION. Layout-aware (#2109 part 2a):
@@ -176,10 +186,15 @@ export function resolvePolicy(projectRoot) {
176
186
  function nowIso() {
177
187
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
178
188
  }
179
- /** Append a one-line audit entry to meta/policy-changes.log (#746). */
180
- export function appendAuditLog(projectRoot, entry) {
189
+ /** Append a one-line audit entry to meta/policy-changes.log (#746).
190
+ * No-op (`changed=false`) does not create or modify the tracked file (#3528). */
191
+ export function appendAuditLog(projectRoot, entry, changed = true) {
181
192
  const root = pathResolve(projectRoot);
182
193
  const logPath = join(root, AUDIT_LOG_REL_PATH);
194
+ const stamped = stampChangedToken(entry, changed);
195
+ if (!changed) {
196
+ return logPath;
197
+ }
183
198
  // #2980 wave D: product write sink routes through containedWrite.
184
199
  if (!existsSync(logPath)) {
185
200
  const header = "# meta/policy-changes.log -- audit trail for " +
@@ -194,7 +209,7 @@ export function appendAuditLog(projectRoot, entry) {
194
209
  containedWrite({
195
210
  root,
196
211
  target: AUDIT_LOG_REL_PATH,
197
- data: `${nowIso()} ${entry}\n`,
212
+ data: `${nowIso()} ${stamped}\n`,
198
213
  mode: "append",
199
214
  });
200
215
  return logPath;
@@ -224,7 +239,7 @@ export function setPolicy(projectRoot, options) {
224
239
  }
225
240
  }
226
241
  const plan = data.plan;
227
- migrateLegacyPolicyKey(plan);
242
+ const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
228
243
  const existingPolicy = plan[PLAN_POLICY_KEY];
229
244
  if (typeof existingPolicy !== "object" ||
230
245
  existingPolicy === null ||
@@ -248,8 +263,7 @@ export function setPolicy(projectRoot, options) {
248
263
  delete narratives[LEGACY_NARRATIVE_KEY];
249
264
  legacyDropped = true;
250
265
  }
251
- atomicWriteProjectDefinition(path, data);
252
- const changed = previous !== Boolean(allowDirectCommits) || legacyDropped;
266
+ const changed = previous !== Boolean(allowDirectCommits) || legacyDropped || legacyKeyMigrated;
253
267
  const parts = [
254
268
  `actor=${actor}`,
255
269
  `allowDirectCommitsToMaster=${allowDirectCommits ? "true" : "false"}`,
@@ -261,8 +275,11 @@ export function setPolicy(projectRoot, options) {
261
275
  if (note) {
262
276
  parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
263
277
  }
264
- const auditEntry = parts.join(" ");
265
- appendAuditLog(projectRoot, auditEntry);
278
+ const auditEntry = stampChangedToken(parts.join(" "), changed);
279
+ if (changed) {
280
+ atomicWriteProjectDefinition(path, data);
281
+ }
282
+ appendAuditLog(projectRoot, auditEntry, changed);
266
283
  return { changed, auditEntry };
267
284
  });
268
285
  }
@@ -3,7 +3,7 @@ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "..
3
3
  import { valueFeedbackInstallForceOnSource } from "./org-force-on-migration.js";
4
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
5
5
  import { policyColonInvocation } from "./policy-invocation.js";
6
- import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
6
+ import { appendAuditLog, loadProjectDefinition, POLICY_AUDIT_NOOP_STDOUT, projectDefinitionPath, } from "./resolve.js";
7
7
  import { isTrustedOrgAutoEnable } from "./value-feedback-autoenable.js";
8
8
  /** Canonical registered policy field name (matches other FIELD_* dotted paths). */
9
9
  export const FIELD_VALUE_FEEDBACK = "plan.policy.valueFeedback";
@@ -236,7 +236,7 @@ export function enableValueFeedback(projectRoot, options) {
236
236
  }
237
237
  }
238
238
  const plan = data.plan;
239
- migrateLegacyPolicyKey(plan);
239
+ const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
240
240
  const existingPolicy = plan[PLAN_POLICY_KEY];
241
241
  if (typeof existingPolicy !== "object" ||
242
242
  existingPolicy === null ||
@@ -273,7 +273,8 @@ export function enableValueFeedback(projectRoot, options) {
273
273
  const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
274
274
  previousNormalized.emitEvents !== nextBlock.emitEvents ||
275
275
  previousNormalized.sessionLine !== nextBlock.sessionLine ||
276
- previousNormalized.upstreamPrompt !== nextBlock.upstreamPrompt;
276
+ previousNormalized.upstreamPrompt !== nextBlock.upstreamPrompt ||
277
+ legacyKeyMigrated;
277
278
  policyBlock.valueFeedback = nextBlock;
278
279
  if (changedFlag) {
279
280
  atomicWriteProjectDefinition(path, data);
@@ -291,15 +292,13 @@ export function enableValueFeedback(projectRoot, options) {
291
292
  if (note) {
292
293
  parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
293
294
  }
294
- appendAuditLog(projectRoot, parts.join(" "));
295
+ appendAuditLog(projectRoot, parts.join(" "), changedFlag);
295
296
  return { changed: changedFlag };
296
297
  });
297
298
  const resolved = resolveValueFeedback(projectRoot);
298
299
  const lines = [
299
300
  `\u2713 ${FIELD_VALUE_FEEDBACK}.enabled=true (value-feedback ON).`,
300
- changed
301
- ? " audit: meta/policy-changes.log updated."
302
- : " no-op: value already matched (audit entry still appended for trail).",
301
+ changed ? " audit: meta/policy-changes.log updated." : POLICY_AUDIT_NOOP_STDOUT,
303
302
  formatValueFeedbackStatusLine(resolved),
304
303
  ];
305
304
  return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
@@ -354,7 +353,7 @@ export function clearValueFeedback(projectRoot, options = {}) {
354
353
  if (note) {
355
354
  parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
356
355
  }
357
- appendAuditLog(projectRoot, parts.join(" "));
356
+ appendAuditLog(projectRoot, parts.join(" "), true);
358
357
  return { changed: true };
359
358
  });
360
359
  const resolved = resolveValueFeedback(projectRoot);
@@ -32,6 +32,12 @@ export interface EvaluateOptions {
32
32
  forIssue?: number | null;
33
33
  allowStale?: boolean;
34
34
  allowMissingBootstrap?: boolean;
35
+ /**
36
+ * Skip the live GitHub drift probe only (#3507). Age staleness stays hard.
37
+ * Explicit flag — never inferred from a missing `--for-issue`.
38
+ * `--for-issue` (work selection) wins over this skip.
39
+ */
40
+ skipDriftProbe?: boolean;
35
41
  /** Injectable clock for tests. */
36
42
  nowFn?: () => Date;
37
43
  /** Injectable drift probe for tests. */
@@ -40,6 +46,12 @@ export interface EvaluateOptions {
40
46
  autoPopulateEmpty?: boolean;
41
47
  }
42
48
  export declare function normaliseRepoUrl(url: string): string | null;
49
+ /**
50
+ * Drift skip is an explicit `--skip-drift-probe` request (#3507).
51
+ * Presence of `--for-issue` is work selection and re-arms the probe.
52
+ * Absence of `--for-issue` is not a skip signal.
53
+ */
54
+ export declare function shouldSkipDriftProbe(options: EvaluateOptions): boolean;
43
55
  /**
44
56
  * Evaluate cache freshness for the given project root.
45
57
  *
@@ -310,6 +310,16 @@ function runDriftProbe(resolvedRepo, cacheRoot, source, options) {
310
310
  return null;
311
311
  }
312
312
  }
313
+ /**
314
+ * Drift skip is an explicit `--skip-drift-probe` request (#3507).
315
+ * Presence of `--for-issue` is work selection and re-arms the probe.
316
+ * Absence of `--for-issue` is not a skip signal.
317
+ */
318
+ export function shouldSkipDriftProbe(options) {
319
+ if (options.forIssue !== undefined && options.forIssue !== null)
320
+ return false;
321
+ return options.skipDriftProbe === true;
322
+ }
313
323
  const REMEDIATION_NO_CANDIDATES = [
314
324
  " Recovery: run `deft triage:bootstrap` to initialise the triage log.",
315
325
  ].join("\n");
@@ -471,7 +481,8 @@ function evaluateWithContext(projectRoot, options, ctx) {
471
481
  ].join("\n"),
472
482
  };
473
483
  }
474
- if (resolvedRepo !== null && !allowStale) {
484
+ const skipDrift = shouldSkipDriftProbe(options);
485
+ if (resolvedRepo !== null && !allowStale && !skipDrift) {
475
486
  const drift = runDriftProbe(resolvedRepo, cacheRoot, source, options);
476
487
  if (drift !== null &&
477
488
  (drift.stateDriftNumbers.length > 0 || drift.contentDriftNumbers.length > 0)) {
@@ -506,6 +517,9 @@ function evaluateWithContext(projectRoot, options, ctx) {
506
517
  if (options.forIssue !== undefined && options.forIssue !== null) {
507
518
  msg += ` Issue #${options.forIssue} latest decision = accept; in subscription scope.`;
508
519
  }
520
+ if (skipDrift) {
521
+ msg += " Drift probe skipped (no work selection).";
522
+ }
509
523
  return { code: 0, message: msg };
510
524
  }
511
525
  function evaluateForIssue(repo, issueNumber, candidatesPath, cacheRoot, source, scopeRules, _projectRoot) {
@@ -1,3 +1,3 @@
1
1
  export type { EvaluateOptions, GateResult } from "./evaluate.js";
2
- export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_RELEASE_PREFLIGHT, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, } from "./evaluate.js";
2
+ export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_RELEASE_PREFLIGHT, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, shouldSkipDriftProbe, } from "./evaluate.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,2 @@
1
- export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_RELEASE_PREFLIGHT, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, } from "./evaluate.js";
1
+ export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_RELEASE_PREFLIGHT, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, shouldSkipDriftProbe, } from "./evaluate.js";
2
2
  //# sourceMappingURL=index.js.map
@@ -23,6 +23,10 @@ export interface VerifyAcResult extends LiteralAcceptanceGateResult {
23
23
  readonly clauseWalked?: boolean;
24
24
  /** How the result was obtained (#3387). */
25
25
  readonly servedFrom?: AcServedFrom;
26
+ /** Config-error cause when resolution is config (#3559). */
27
+ readonly cause?: string;
28
+ /** Reuse-gate miss cause when servedFrom is executed (#3558). */
29
+ readonly missReason?: string;
26
30
  }
27
31
  export interface EvaluateVerifyAcOptions extends EvaluateLiteralAcceptanceOptions {
28
32
  /**
@@ -9,11 +9,12 @@
9
9
  */
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { basename, isAbsolute, relative, resolve } from "node:path";
12
- import { emitAcceptanceStampFromPlan } from "../intake/clause-derivation.js";
12
+ import { emitAcceptanceStampFromPlan, MISSING_AMBIGUITY_ATTESTATION_CAUSE, stampedAmbiguityAttestationError, } from "../intake/clause-derivation.js";
13
13
  import { appendLiteralAcceptanceAdvisory, evaluateLiteralAcceptanceFromPlan, isNoopRefusalReason, runLiteralAcceptanceCommands, stripLiteralAcceptanceAdvisory, } from "../literal-acceptance/index.js";
14
14
  import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter, } from "../run-summary/index.js";
15
15
  import { maybeBankOnAcPass } from "../session/ac-pass-banking.js";
16
16
  import { resolveAcReuse, resolveScopeIdForAcReuse, snapshotFromReuseFields, } from "../session/ac-pass-reuse.js";
17
+ import { gitHead } from "../session/git.js";
17
18
  import { hashProductState } from "../session/product-state-hash.js";
18
19
  import { resolveVerifyAcSessionId, writeVerifyAcSessionCache, } from "../session/verify-ac-session-cache.js";
19
20
  import { formatClauseWalkMessage, walkAcceptanceClauses, } from "../verify-ac/clauses.js";
@@ -162,6 +163,24 @@ export function evaluateVerifyAcFromPlan(plan, options = {}) {
162
163
  }, optionsWithScope, plan);
163
164
  }
164
165
  const projectRoot = resolve(optionsWithScope.projectRoot ?? process.cwd());
166
+ const attestationError = stampedAmbiguityAttestationError(optionsWithScope.observedAcceptance !== undefined
167
+ ? optionsWithScope.observedAcceptance
168
+ : plan.acceptance);
169
+ if (attestationError !== null) {
170
+ return applyOracle({
171
+ ok: false,
172
+ code: 2,
173
+ message: optionsWithScope.quiet === true ? "" : attestationError.message,
174
+ commands: [],
175
+ runs: [],
176
+ sourceRung: acceptance.source_rung,
177
+ noneStated: acceptance.none_stated,
178
+ acceptance,
179
+ resolution: "config",
180
+ resolvedCommandCount: 0,
181
+ cause: attestationError.cause ?? MISSING_AMBIGUITY_ATTESTATION_CAUSE,
182
+ }, optionsWithScope, plan);
183
+ }
165
184
  const reused = tryReuseVerifyAc(plan, acceptance, optionsWithScope, projectRoot);
166
185
  if (reused !== null) {
167
186
  return applyOracle(reused, optionsWithScope, plan);
@@ -372,6 +391,8 @@ function emitAcceptanceOutcome(result, options, projectRoot) {
372
391
  outcome: row.outcome,
373
392
  })),
374
393
  served_from: result.servedFrom ?? "executed",
394
+ ...(result.cause !== undefined ? { cause: result.cause } : {}),
395
+ miss_reason: (result.servedFrom ?? "executed") === "executed" ? result.missReason : undefined,
375
396
  });
376
397
  }
377
398
  catch {
@@ -524,10 +545,27 @@ function applyOracle(result, options, plan = {}) {
524
545
  next = { ...next, resolution: "fail" };
525
546
  }
526
547
  }
548
+ const servedFrom = next.servedFrom ?? "executed";
549
+ let missReason = next.missReason;
550
+ if (servedFrom === "executed" && (missReason === undefined || missReason.length === 0)) {
551
+ const reuse = resolveAcReuse({
552
+ projectRoot,
553
+ plan,
554
+ scopeId: options.bankScopeId,
555
+ sessionId: options.sessionId,
556
+ env: options.env,
557
+ productPaths: options.productPaths,
558
+ allowCache: (options.reuseMode ?? "auto") === "auto",
559
+ allowBank: true,
560
+ });
561
+ if (reuse.kind === "miss")
562
+ missReason = reuse.reason;
563
+ }
527
564
  const stamped = {
528
565
  ...next,
529
566
  message: options.quiet === true ? next.message : labelVerdict(next),
530
- servedFrom: next.servedFrom ?? "executed",
567
+ servedFrom,
568
+ missReason: servedFrom === "executed" ? missReason : undefined,
531
569
  };
532
570
  persistVerifyAcSessionCache(stamped, options, projectRoot, plan);
533
571
  if (options.skipAcceptanceEmit !== true) {
@@ -634,14 +672,14 @@ export function resolveOracleScopeKey(plan, xbriefPath, projectRoot) {
634
672
  return rel;
635
673
  }
636
674
  /**
637
- * After executable AC pass, FINALIZE the banking checkpoint (#3285).
638
- * Soft/advisory passes with zero runs do not bank. Fail-open on ledger errors.
675
+ * After verified-pass, FINALIZE the banking checkpoint (#3285 / #3558).
676
+ * Soft/advisory empty-pass still skips. Ledger I/O failures fail closed.
639
677
  */
640
678
  function maybeAttachAcPassBank(result, plan, xbriefPath, options) {
641
679
  if (options.bankOnPass === false) {
642
680
  return result;
643
681
  }
644
- if (!result.ok || result.runs.length === 0) {
682
+ if (!result.ok || result.resolution !== "verified-pass") {
645
683
  return result;
646
684
  }
647
685
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
@@ -661,8 +699,11 @@ function maybeAttachAcPassBank(result, plan, xbriefPath, options) {
661
699
  projectRoot,
662
700
  scopeId,
663
701
  executableRuns: result.runs.length,
702
+ verifiedPass: true,
664
703
  quiet: options.quiet,
665
704
  productStateHash: hashed.complete ? hashed.digest : null,
705
+ environ: options.env,
706
+ headSha: gitHead(projectRoot).head,
666
707
  });
667
708
  if (options.quiet || banked.notes.length === 0) {
668
709
  return result;
@@ -0,0 +1,12 @@
1
+ import type { ClosedVerbDecision } from "../authz/types.js";
2
+ import type { ReleaseConfig, ReleaseSeams } from "./types.js";
3
+ export declare const TAG_PUSH_CLOSED_VERB: "release-publish";
4
+ export declare function evaluateReleaseTagPushGate(version: string, projectRoot: string, seams: ReleaseSeams, repo: string): ClosedVerbDecision;
5
+ /**
6
+ * Fail closed before git tag + atomic tag push when skipTag is false.
7
+ * Dry-run and `--skip-tag` do not require a grant (no npm distribution).
8
+ * Returns EXIT_OK to continue, EXIT_VIOLATION to halt. Never spends the grant
9
+ * — draft-flip `release:publish` still owns markGrantUsed (#1095).
10
+ */
11
+ export declare function assertTagPushClosedVerb(config: ReleaseConfig, seams: ReleaseSeams): number;
12
+ //# sourceMappingURL=closed-verb-gate.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Closed-verb gate on the tag-push / npm-publish boundary (#3527).
3
+ *
4
+ * Reuses the draft-flip evaluator (`evaluateReleasePublishGate`) so the
5
+ * distributing step carries the same authz tier. Does not delete or weaken
6
+ * the later `release:publish` draft-flip check.
7
+ */
8
+ import { closedVerbEnvBypassKey } from "../authz/closed-verb.js";
9
+ import { evaluateReleasePublishGate } from "../release-publish/pipeline.js";
10
+ import { EXIT_OK, EXIT_VIOLATION, TOTAL_STEPS } from "./constants.js";
11
+ function emitGate(label, status) {
12
+ process.stderr.write(`[10/${TOTAL_STEPS}] ${label}... ${status}\n`);
13
+ }
14
+ export const TAG_PUSH_CLOSED_VERB = "release-publish";
15
+ export function evaluateReleaseTagPushGate(version, projectRoot, seams, repo) {
16
+ return evaluateReleasePublishGate(version, projectRoot, {
17
+ grants: seams.closedVerbGrants,
18
+ env: seams.closedVerbEnv,
19
+ repo,
20
+ });
21
+ }
22
+ /**
23
+ * Fail closed before git tag + atomic tag push when skipTag is false.
24
+ * Dry-run and `--skip-tag` do not require a grant (no npm distribution).
25
+ * Returns EXIT_OK to continue, EXIT_VIOLATION to halt. Never spends the grant
26
+ * — draft-flip `release:publish` still owns markGrantUsed (#1095).
27
+ */
28
+ export function assertTagPushClosedVerb(config, seams) {
29
+ if (config.skipTag) {
30
+ return EXIT_OK;
31
+ }
32
+ const tag = `v${config.version}`;
33
+ const label = `Closed-verb gate ${TAG_PUSH_CLOSED_VERB} ${tag} (tag-push / npm-publish)`;
34
+ if (config.dryRun) {
35
+ emitGate(label, "DRYRUN (would require human-origin grant or DEFT_ALLOW_RELEASE_PUBLISH=1 before tag push)");
36
+ return EXIT_OK;
37
+ }
38
+ const gate = evaluateReleaseTagPushGate(config.version, config.projectRoot, seams, config.repo);
39
+ if (!gate.allowed) {
40
+ const bypassKey = gate.envBypassKey ??
41
+ closedVerbEnvBypassKey(TAG_PUSH_CLOSED_VERB, config.projectRoot) ??
42
+ "DEFT_ALLOW_RELEASE_PUBLISH";
43
+ emitGate(label, `FAIL (${gate.code}: ${gate.reason})`);
44
+ process.stderr.write(`[release] denied code=${gate.code} — set ${bypassKey}=1 or ` +
45
+ `mint \`deft authz:grant -- --template ${TAG_PUSH_CLOSED_VERB} --target ${config.version}\`\n`);
46
+ return EXIT_VIOLATION;
47
+ }
48
+ emitGate(label, `OK (${gate.code}${gate.humanApprovalRef !== null ? ` grant=${gate.humanApprovalRef}` : ""})`);
49
+ return EXIT_OK;
50
+ }
51
+ //# sourceMappingURL=closed-verb-gate.js.map
@@ -1,5 +1,6 @@
1
1
  export * from "./auto-hatch.js";
2
2
  export * from "./build-dist.js";
3
+ export * from "./closed-verb-gate.js";
3
4
  export * from "./constants.js";
4
5
  export * from "./coverage-debt-ledger.js";
5
6
  export * from "./flags.js";
@@ -1,6 +1,7 @@
1
1
  /* v8 ignore file -- re-export barrel; covered via main.ts */
2
2
  export * from "./auto-hatch.js";
3
3
  export * from "./build-dist.js";
4
+ export * from "./closed-verb-gate.js";
4
5
  export * from "./constants.js";
5
6
  export * from "./coverage-debt-ledger.js";
6
7
  export * from "./flags.js";
@@ -4,6 +4,7 @@ import { assertProjectionContained } from "../fs/projection-containment.js";
4
4
  import { readCoverageTotalsFromReport } from "../vitest-runner/coverage-debt.js";
5
5
  import { buildCoverageDebtIssueDraft, classifyStep5FailureWithFreshness, evaluateAutoHatch, formatAutoHatchBanner, parseExitCodeFromReason, reasonLooksLikeTimeout, } from "./auto-hatch.js";
6
6
  import { prependUpgradeBanner, promoteChangelog, sectionForVersion } from "./changelog.js";
7
+ import { assertTagPushClosedVerb } from "./closed-verb-gate.js";
7
8
  import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, RELEASE_CHECK_TIMEOUT_MINUTES, TOTAL_STEPS, VERIFY_DRAFT_INTERVAL_SECONDS, VERIFY_DRAFT_MAX_ATTEMPTS, } from "./constants.js";
8
9
  import { createCoverageDebtIssue, probeOpenCoverageDebtLedger } from "./coverage-debt-ledger.js";
9
10
  import { checkTagAvailable, createGithubRelease, readTextFile, verifyReleaseDraft } from "./gh.js";
@@ -373,6 +374,12 @@ export function runPipeline(config, seams = {}) {
373
374
  return EXIT_VIOLATION;
374
375
  }
375
376
  }
377
+ // Step 10–11 boundary: closed-verb gate before tag + atomic push (#3527).
378
+ // npm publish fires on tag push; draft-flip `release:publish` stays (#1095).
379
+ const tagPushGate = assertTagPushClosedVerb(config, seams);
380
+ if (tagPushGate !== EXIT_OK) {
381
+ return tagPushGate;
382
+ }
376
383
  // Step 10: git tag.
377
384
  label = `Tag v${version}`;
378
385
  if (config.skipTag) {
@@ -1,3 +1,5 @@
1
+ import type { EnvMap } from "../authz/closed-verb.js";
2
+ import type { HumanOriginGrant } from "../authz/types.js";
1
3
  export interface ReleaseConfig {
2
4
  readonly version: string;
3
5
  readonly repo: string;
@@ -77,5 +79,15 @@ export interface ReleaseSeams {
77
79
  readonly headSha?: (projectRoot: string) => string | null;
78
80
  /** #3187 — CI detector; when true suite stamp is never trusted. */
79
81
  readonly isCi?: () => boolean;
82
+ /**
83
+ * #3527 — closed-verb grants for the tag-push / npm-publish gate.
84
+ * When omitted, production loads active human-origin grants from disk.
85
+ */
86
+ readonly closedVerbGrants?: readonly HumanOriginGrant[];
87
+ /**
88
+ * #3527 — env map for DEFT_ALLOW_RELEASE_PUBLISH. When omitted, process.env.
89
+ * Tests inject `{}` so a host env bypass cannot leak into fail-closed cases.
90
+ */
91
+ readonly closedVerbEnv?: EnvMap;
80
92
  }
81
93
  //# sourceMappingURL=types.d.ts.map
@@ -1,6 +1,7 @@
1
1
  /** Canonical v0.6 status enum (matches `scripts/spec_validate.py`). */
2
2
  export declare const VALID_STATUSES: Set<string>;
3
- export declare const VALID_VBRIEF_VERSIONS: Set<string>;
3
+ export { VALID_VBRIEF_VERSIONS } from "../vbrief-validate/constants.js";
4
+ export { resolveInfoBlock } from "../vbrief-validate/schema.js";
4
5
  export declare const SPECIFICATION_NARRATIVE_KEY_ORDER: readonly ["Overview", "ProblemStatement", "Goals", "UserStories", "Requirements", "SuccessMetrics", "EdgeCases", "Architecture", "TechDecisions", "ImplementationPhases", "PreImplementationGates"];
5
6
  /** Migration capture narrative; omitted from default compact SPECIFICATION.md (#1566). */
6
7
  export declare const LEGACY_ARTIFACTS_NARRATIVE_KEY = "LegacyArtifacts";
@@ -10,7 +10,8 @@ export const VALID_STATUSES = new Set([
10
10
  "failed",
11
11
  "cancelled",
12
12
  ]);
13
- export const VALID_VBRIEF_VERSIONS = new Set(["0.6"]);
13
+ export { VALID_VBRIEF_VERSIONS } from "../vbrief-validate/constants.js";
14
+ export { resolveInfoBlock } from "../vbrief-validate/schema.js";
14
15
  export const SPECIFICATION_NARRATIVE_KEY_ORDER = [
15
16
  "Overview",
16
17
  "ProblemStatement",
@@ -85,6 +85,9 @@ export const COMMANDS = {
85
85
  "verify:orphan-active": spec("verify:orphan-active", "verify_orphan_active:main", {
86
86
  projectRootArg: "--project-root",
87
87
  }),
88
+ "verify:lifecycle-visible": spec("verify:lifecycle-visible", "verify_lifecycle_visible:main", {
89
+ projectRootArg: "--project-root",
90
+ }),
88
91
  "verify:completed-tracked": spec("verify:completed-tracked", "verify_completed_tracked:main", {
89
92
  projectRootArg: "--project-root",
90
93
  }),
@@ -302,6 +305,7 @@ const ENTRYPOINT_VERB = {
302
305
  "preflight_cache:main": "preflight-cache",
303
306
  "preflight_wip_cap:main": "verify-wip-cap",
304
307
  "verify_orphan_active:main": "verify-orphan-active",
308
+ "verify_lifecycle_visible:main": "verify-lifecycle-visible",
305
309
  "verify_completed_tracked:main": "verify-completed-tracked",
306
310
  "pack_render:main": "pack-render",
307
311
  "validate_strategy_output:main": "validate-strategy-output",
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
- import { VALID_STATUSES, VALID_VBRIEF_VERSIONS } from "./constants.js";
3
+ import { VALID_INFO_ROOT_KEYS, VALID_VBRIEF_VERSIONS } from "../vbrief-validate/constants.js";
4
+ import { resolveInfoBlock } from "../vbrief-validate/schema.js";
5
+ import { VALID_STATUSES } from "./constants.js";
4
6
  function validateNarratives(narratives, path, errors) {
5
7
  if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives)) {
6
8
  errors.push(`${path} must be an object`);
@@ -46,19 +48,28 @@ function validatePlanItem(item, path, errors) {
46
48
  }
47
49
  function validateSchema(data) {
48
50
  const errors = [];
49
- if (!("vBRIEFInfo" in data)) {
50
- errors.push("missing required top-level key 'vBRIEFInfo'");
51
+ const resolved = resolveInfoBlock(data);
52
+ if (resolved === null) {
53
+ let infoShapeError = false;
54
+ for (const key of VALID_INFO_ROOT_KEYS) {
55
+ if (!(key in data))
56
+ continue;
57
+ const info = data[key];
58
+ if (info === null || Array.isArray(info) || typeof info !== "object") {
59
+ errors.push(`'${key}' must be an object`);
60
+ infoShapeError = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!infoShapeError) {
65
+ errors.push("missing required top-level key 'vBRIEFInfo' or 'xBRIEFInfo'");
66
+ }
51
67
  }
52
68
  else {
53
- const info = data.vBRIEFInfo;
54
- if (typeof info !== "object" || info === null || Array.isArray(info)) {
55
- errors.push("'vBRIEFInfo' must be an object");
56
- }
57
- else {
58
- const version = info.version;
59
- if (!VALID_VBRIEF_VERSIONS.has(String(version))) {
60
- errors.push(`'vBRIEFInfo.version' must be '0.6' (canonical v0.6 schema, #533), got ${JSON.stringify(version)}. Run \`task migrate:vbrief\` to upgrade pre-existing v0.5 vBRIEFs in-place.`);
61
- }
69
+ const version = resolved.info.version;
70
+ if (!VALID_VBRIEF_VERSIONS.has(String(version))) {
71
+ const allowed = [...VALID_VBRIEF_VERSIONS].map((v) => `'${v}'`).join(", ");
72
+ errors.push(`'${resolved.key}.version' must be one of ${allowed} (canonical v0.6/v0.8 schema, #2107), got ${JSON.stringify(version)}. Run \`task migrate:xbrief\` to upgrade pre-existing v0.5 vBRIEFs in-place.`);
62
73
  }
63
74
  }
64
75
  if (!("plan" in data)) {
@@ -128,6 +128,10 @@ export interface AcceptanceRunSummaryPayload {
128
128
  readonly clause_outcomes?: readonly AcceptanceClauseOutcomeRow[];
129
129
  /** bank | cache | executed — how verify:ac obtained this result (#3387). */
130
130
  readonly served_from?: "bank" | "cache" | "executed";
131
+ /** Config-error cause when outcome is config-error (#3559). */
132
+ readonly cause?: string;
133
+ /** Reuse-gate miss cause when served_from is executed (#3558). */
134
+ readonly miss_reason?: string;
131
135
  }
132
136
  /** Intake-time stamp: which rung locked, whether commands were stated, counts (#3323). */
133
137
  export interface AcceptanceStampRunSummaryPayload {
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
4
+ import { interceptHelp } from "../triage/help/index.js";
4
5
  import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
5
6
  import { canonicalLogPath, readAll } from "./audit-log.js";
6
7
  import { batchPromote } from "./batch-promote.js";
@@ -13,12 +14,11 @@ import { promoteFromIssue } from "./promote-from-issue.js";
13
14
  import { promotePath } from "./promote-path.js";
14
15
  import { runTransition } from "./transition.js";
15
16
  import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoOne, } from "./undo.js";
16
- const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force] [--batch]\n" +
17
- " [--from-issue N] [--repo OWNER/NAME] [--strict] [--force-no-cache]\n" +
17
+ const LIFECYCLE_USAGE_STDERR = "usage: deft scope:<action> [file] [--batch] [--from-issue N] [--repo OWNER/NAME]\n" +
18
+ " [--force] [--project-root PATH] [--strict] [--force-no-cache]\n" +
18
19
  " [--path PATH]\n" +
19
- " {activate,block,cancel,complete,fail,promote,restore,unblock}\n" +
20
- " [file ...]\n" +
21
- "scope_lifecycle.py: error: the following arguments are required: action, file\n" +
20
+ " actions: activate, block, cancel, complete, fail, promote, restore, unblock\n" +
21
+ "The verb already encodes the action (e.g. deft scope:promote <file>). Do not pass the action again.\n" +
22
22
  "(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n" +
23
23
  "(promote --from-issue=N may omit file; #1136)\n";
24
24
  function parseLifecycleArgv(argv) {
@@ -50,6 +50,8 @@ function parseLifecycleArgv(argv) {
50
50
  if (arg === undefined) {
51
51
  return { args: null, error: "usage" };
52
52
  }
53
+ if (arg === "--")
54
+ continue;
53
55
  if (arg === "--force") {
54
56
  force = true;
55
57
  }
@@ -236,6 +238,14 @@ function parseLifecycleArgv(argv) {
236
238
  }
237
239
  /** Main entry for scope_lifecycle.py parity. */
238
240
  export function lifecycleMain(argv) {
241
+ if (argv.includes("--help") || argv.includes("-h")) {
242
+ const helpRc = interceptHelp("scope_lifecycle", argv);
243
+ if (helpRc !== null) {
244
+ return helpRc;
245
+ }
246
+ process.stdout.write(LIFECYCLE_USAGE_STDERR);
247
+ return 0;
248
+ }
239
249
  const parsed = parseLifecycleArgv(argv);
240
250
  if (parsed.args === null) {
241
251
  if (parsed.error === "usage") {
@@ -195,7 +195,7 @@ export function promoteFromIssue(options) {
195
195
  return {
196
196
  ok: false,
197
197
  message: `No triage-cache decision for #${n} (${repoSafe}). ` +
198
- `Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
198
+ `Accept first: deft triage:accept --issue ${n} --repo ${repoSafe} ` +
199
199
  `(or omit --strict to soft-warn and proceed; --force-no-cache skips the gate).`,
200
200
  exitCode: 1,
201
201
  warnings,
@@ -212,7 +212,7 @@ export function promoteFromIssue(options) {
212
212
  ok: false,
213
213
  message: `Refusing promote for #${n} (${repoSafe}): latest triage decision is '${sanitizeMsg(latest.decision)}' ` +
214
214
  `(decision_id=${latest.decision_id}). ` +
215
- `Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
215
+ `Accept first: deft triage:accept --issue ${n} --repo ${repoSafe} ` +
216
216
  `or override with --force-no-cache.`,
217
217
  exitCode: 1,
218
218
  warnings,
@@ -315,8 +315,8 @@ export function promoteFromIssue(options) {
315
315
  return {
316
316
  ok: false,
317
317
  message: `No proposed/ scope artifact found for issue #${n} (${repoSafe}). ` +
318
- `Ingest first (task triage:accept -- --issue ${n} --repo ${repoSafe}) ` +
319
- `or pass an explicit path: task scope:promote -- <path>.`,
318
+ `Ingest first (deft triage:accept --issue ${n} --repo ${repoSafe}) ` +
319
+ `or pass an explicit path: deft scope:promote <path>.`,
320
320
  exitCode: 1,
321
321
  warnings,
322
322
  repo,
@@ -330,7 +330,7 @@ export function promoteFromIssue(options) {
330
330
  return {
331
331
  ok: false,
332
332
  message: `Multiple proposed/ artifacts match issue #${n}; refuse to guess.\n${list}\n` +
333
- `Promote one path explicitly: task scope:promote -- <path> ` +
333
+ `Promote one path explicitly: deft scope:promote <path> ` +
334
334
  `or re-run with --path <one-of-the-above>.`,
335
335
  exitCode: 1,
336
336
  warnings,