@deftai/directive-core 0.105.0 → 0.107.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 (173) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/gate-lists.js +2 -0
  3. package/dist/check/named-cause.js +1 -1
  4. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  5. package/dist/design-critique/exclusive-chip.js +68 -0
  6. package/dist/design-critique/parent-audit.d.ts +59 -0
  7. package/dist/design-critique/parent-audit.js +121 -0
  8. package/dist/doctor/checks.d.ts +6 -0
  9. package/dist/doctor/checks.js +35 -0
  10. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  11. package/dist/forward-coverage/diff-coverage.js +112 -0
  12. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  13. package/dist/forward-coverage/diff-lines.js +173 -0
  14. package/dist/forward-coverage/evaluate.d.ts +24 -9
  15. package/dist/forward-coverage/evaluate.js +125 -19
  16. package/dist/hooks/dest-form.d.ts +46 -0
  17. package/dist/hooks/dest-form.js +575 -0
  18. package/dist/hooks/dispatcher.d.ts +17 -1
  19. package/dist/hooks/dispatcher.js +82 -11
  20. package/dist/hooks/index.d.ts +1 -0
  21. package/dist/hooks/index.js +1 -0
  22. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  23. package/dist/init-deposit/agent-hooks.js +1 -1
  24. package/dist/intake/clause-derivation.d.ts +9 -0
  25. package/dist/intake/clause-derivation.js +30 -3
  26. package/dist/intake/github-auth-modes-cli.js +14 -2
  27. package/dist/intake/github-auth-modes.d.ts +54 -9
  28. package/dist/intake/github-auth-modes.js +326 -123
  29. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  30. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  31. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  32. package/dist/lifecycle/completed-write-guard.js +373 -0
  33. package/dist/lifecycle/index.d.ts +1 -0
  34. package/dist/lifecycle/index.js +1 -0
  35. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  36. package/dist/lifecycle-visible/evaluate.js +710 -0
  37. package/dist/lifecycle-visible/index.d.ts +2 -0
  38. package/dist/lifecycle-visible/index.js +2 -0
  39. package/dist/literal-acceptance/capture.d.ts +16 -0
  40. package/dist/literal-acceptance/capture.js +78 -10
  41. package/dist/literal-acceptance/evaluate.js +25 -3
  42. package/dist/literal-acceptance/index.d.ts +2 -2
  43. package/dist/literal-acceptance/index.js +2 -2
  44. package/dist/literal-acceptance/run.js +9 -3
  45. package/dist/literal-acceptance/types.d.ts +6 -0
  46. package/dist/literal-acceptance/types.js +6 -0
  47. package/dist/orchestration/judgment-policy.d.ts +13 -0
  48. package/dist/orchestration/judgment-policy.js +15 -0
  49. package/dist/orchestration/probe-session.js +13 -8
  50. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  51. package/dist/orchestration/verify-judgment-gates.js +43 -11
  52. package/dist/policy/ceremony-dial.js +7 -7
  53. package/dist/policy/host-hooks.d.ts +20 -0
  54. package/dist/policy/host-hooks.js +121 -2
  55. package/dist/policy/org-force-on-migration.js +2 -1
  56. package/dist/policy/plan-extensions.d.ts +1 -1
  57. package/dist/policy/plan-extensions.js +2 -0
  58. package/dist/policy/product-signal.js +6 -7
  59. package/dist/policy/require-human-merge.js +8 -6
  60. package/dist/policy/resolve.d.ts +7 -2
  61. package/dist/policy/resolve.js +25 -8
  62. package/dist/policy/runtime-authority.d.ts +18 -0
  63. package/dist/policy/runtime-authority.js +11 -0
  64. package/dist/policy/value-feedback.js +7 -8
  65. package/dist/policy/write-fence.js +3 -0
  66. package/dist/preflight-cache/evaluate.d.ts +12 -0
  67. package/dist/preflight-cache/evaluate.js +24 -4
  68. package/dist/preflight-cache/index.d.ts +1 -1
  69. package/dist/preflight-cache/index.js +1 -1
  70. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  71. package/dist/product-first-done-gate/evaluate.js +82 -10
  72. package/dist/release/closed-verb-gate.d.ts +12 -0
  73. package/dist/release/closed-verb-gate.js +51 -0
  74. package/dist/release/index.d.ts +1 -0
  75. package/dist/release/index.js +1 -0
  76. package/dist/release/pipeline.js +7 -0
  77. package/dist/release/types.d.ts +12 -0
  78. package/dist/render/constants.d.ts +2 -1
  79. package/dist/render/constants.js +2 -1
  80. package/dist/render/framework-commands.js +4 -0
  81. package/dist/render/spec-validate.js +23 -12
  82. package/dist/run-summary/types.d.ts +4 -0
  83. package/dist/scm/design-critique-chip.d.ts +37 -0
  84. package/dist/scm/design-critique-chip.js +171 -0
  85. package/dist/scm/index.d.ts +1 -0
  86. package/dist/scm/index.js +1 -0
  87. package/dist/scm/main.d.ts +3 -0
  88. package/dist/scm/main.js +15 -1
  89. package/dist/scm/readiness-cli.d.ts +2 -0
  90. package/dist/scm/readiness-cli.js +42 -1
  91. package/dist/scm/readiness.d.ts +2 -1
  92. package/dist/scm/readiness.js +7 -1
  93. package/dist/scope/decompose.js +2 -1
  94. package/dist/scope/delivery-evidence.d.ts +27 -1
  95. package/dist/scope/index.d.ts +1 -0
  96. package/dist/scope/index.js +1 -0
  97. package/dist/scope/lifecycle-write.d.ts +24 -0
  98. package/dist/scope/lifecycle-write.js +64 -0
  99. package/dist/scope/main.js +18 -6
  100. package/dist/scope/promote-from-issue.js +5 -5
  101. package/dist/scope/transition.js +90 -11
  102. package/dist/scope-provenance/evaluate.js +8 -4
  103. package/dist/session/ac-pass-banking.d.ts +5 -0
  104. package/dist/session/ac-pass-banking.js +3 -2
  105. package/dist/session/git.d.ts +18 -0
  106. package/dist/session/git.js +81 -1
  107. package/dist/session/occupancy.d.ts +2 -1
  108. package/dist/session/occupancy.js +11 -4
  109. package/dist/session/process-cost.d.ts +32 -10
  110. package/dist/session/process-cost.js +189 -1
  111. package/dist/session/product-state-hash.js +41 -24
  112. package/dist/session/ritual-entrypoint.js +16 -1
  113. package/dist/session/session-start.d.ts +18 -0
  114. package/dist/session/session-start.js +49 -2
  115. package/dist/session/verify-session-ritual.d.ts +46 -4
  116. package/dist/session/verify-session-ritual.js +118 -10
  117. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  118. package/dist/swarm/finalize-cohort-cli.js +165 -35
  119. package/dist/swarm/finalize-cohort.d.ts +1 -0
  120. package/dist/swarm/finalize-cohort.js +14 -2
  121. package/dist/swarm/launch.d.ts +69 -0
  122. package/dist/swarm/launch.js +191 -1
  123. package/dist/swarm/routing-set-cli.js +2 -0
  124. package/dist/swarm/routing-verify.d.ts +1 -1
  125. package/dist/swarm/routing-verify.js +2 -2
  126. package/dist/triage/actions/index.d.ts +4 -0
  127. package/dist/triage/actions/index.js +6 -0
  128. package/dist/triage/bootstrap/index.js +6 -6
  129. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  130. package/dist/triage/evaluate/evaluate.js +169 -0
  131. package/dist/triage/evaluate/github.d.ts +16 -0
  132. package/dist/triage/evaluate/github.js +153 -0
  133. package/dist/triage/evaluate/index.d.ts +9 -0
  134. package/dist/triage/evaluate/index.js +8 -0
  135. package/dist/triage/evaluate/paths.d.ts +7 -0
  136. package/dist/triage/evaluate/paths.js +25 -0
  137. package/dist/triage/evaluate/sink.d.ts +5 -0
  138. package/dist/triage/evaluate/sink.js +52 -0
  139. package/dist/triage/evaluate/types.d.ts +93 -0
  140. package/dist/triage/evaluate/types.js +13 -0
  141. package/dist/triage/evaluate/validity.d.ts +8 -0
  142. package/dist/triage/evaluate/validity.js +87 -0
  143. package/dist/triage/evaluate/value.d.ts +12 -0
  144. package/dist/triage/evaluate/value.js +33 -0
  145. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  146. package/dist/triage/evaluate/wip-census.js +39 -0
  147. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  148. package/dist/triage/evaluate/worktrees.js +68 -0
  149. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  150. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  151. package/dist/triage/help/registry-data.d.ts +22 -8
  152. package/dist/triage/help/registry-data.js +54 -6
  153. package/dist/triage/index.d.ts +1 -0
  154. package/dist/triage/index.js +1 -0
  155. package/dist/triage/welcome/writers.d.ts +1 -1
  156. package/dist/triage/welcome/writers.js +10 -5
  157. package/dist/value/feedback-file.d.ts +17 -2
  158. package/dist/value/feedback-file.js +80 -6
  159. package/dist/value/readback.d.ts +4 -1
  160. package/dist/value/readback.js +51 -12
  161. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  162. package/dist/vbrief-reconcile/labels.js +30 -0
  163. package/dist/vbrief-validate/schema.d.ts +4 -0
  164. package/dist/vbrief-validate/schema.js +1 -1
  165. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  166. package/dist/vbrief-validation/story-quality.js +8 -1
  167. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  168. package/dist/verify-env/agent-hook-readiness.js +12 -8
  169. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  170. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  171. package/dist/verify-env/agent-hooks.js +3 -4
  172. package/dist/xbrief-migrate/agents-header.js +69 -7
  173. package/package.json +7 -3
@@ -1,8 +1,36 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { HOOK_HOSTS } from "../hooks/dispatcher.js";
2
- import { readPlanPolicy } from "./plan-extensions.js";
3
- import { loadProjectDefinition } from "./resolve.js";
3
+ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
4
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
5
+ import { policyColonInvocation } from "./policy-invocation.js";
6
+ import { appendAuditLog, loadProjectDefinition, POLICY_AUDIT_NOOP_STDOUT, projectDefinitionPath, } from "./resolve.js";
4
7
  export const FIELD_HOST_HOOKS = "plan.policy.hostHooks";
5
8
  export const FIELD_HOST_HOOKS_CLI_ALIAS = "hostHooks";
9
+ export const DISABLE_HOST_HOOKS_SUBCOMMAND = "disable-host-hooks";
10
+ /** Confirm-gated disable verb for unused-host recovery copy (#3571). CLI form: no go-task `--`. */
11
+ export function disableHostHooksInvocation(trailing = " --host <host> --confirm") {
12
+ return policyColonInvocation(DISABLE_HOST_HOOKS_SUBCOMMAND, trailing);
13
+ }
14
+ export const HOST_HOOKS_DISABLE_CAPABILITY_COST_DISCLOSURE = "\u26a0 Capability-cost disclosure -- disabling hostHooks for a host removes " +
15
+ "deft-hook pre-execution guardrails for anyone who later opens this repo in that host. " +
16
+ "The result is tracked.\n" +
17
+ " \u2022 Opted-out hosts skip Directive hook deposit; leftover-free files write {}.\n" +
18
+ " \u2022 This is not a timeout or live-probe fix. Retry the gated ritual when load is the cause.\n" +
19
+ " \u2022 Inspect: `" +
20
+ policyColonInvocation("show", " --field=hostHooks") +
21
+ "`.\n" +
22
+ " \u2022 Apply: `" +
23
+ disableHostHooksInvocation() +
24
+ "`.\n" +
25
+ " \u2022 Reversible: set the host back to true and run `deft update`.\n" +
26
+ " \u2022 Hand-edit of plan.policy.hostHooks plus `deft update` still strips " +
27
+ "(human high-trust bypass).\n" +
28
+ " \u2022 Changes are recorded to meta/policy-changes.log for auditability.";
29
+ export const UNUSED_HOST_HOOKS_RECOVERY = "If an affected host is unused, inspect `" +
30
+ policyColonInvocation("show", " --field=hostHooks") +
31
+ "`, then run `" +
32
+ disableHostHooksInvocation() +
33
+ "`. That removes deft-hook pre-execution guardrails for that host and the result is tracked.";
6
34
  export const DEFAULT_HOST_HOOKS_POLICY = {
7
35
  claude: true,
8
36
  cursor: true,
@@ -93,4 +121,95 @@ export function loadHostHooksPolicyFromProject(projectRoot) {
93
121
  }
94
122
  return resolveHostHooksPolicy(policyBlock.hostHooks);
95
123
  }
124
+ export function parseHookHost(value) {
125
+ if (value === undefined)
126
+ return null;
127
+ return HOOK_HOSTS.includes(value) ? value : null;
128
+ }
129
+ /** Persist `hostHooks.<host>=false` after capability-cost disclosure (#3571). */
130
+ export function disableHostHooks(projectRoot, options) {
131
+ if (!HOOK_HOSTS.includes(options.host)) {
132
+ return {
133
+ exitCode: 2,
134
+ stdout: `\u274c ${FIELD_HOST_HOOKS}.${String(options.host)} is not a deposited host (${HOOK_HOSTS.join(", ")}).\n`,
135
+ changed: false,
136
+ };
137
+ }
138
+ if (!options.confirm) {
139
+ return {
140
+ exitCode: 1,
141
+ stdout: `${HOST_HOOKS_DISABLE_CAPABILITY_COST_DISCLOSURE}\n\n` +
142
+ `Re-run with --confirm to apply: ${disableHostHooksInvocation(` --host ${options.host} --confirm`)}\n`,
143
+ changed: false,
144
+ };
145
+ }
146
+ const path = projectDefinitionPath(projectRoot);
147
+ try {
148
+ const { changed } = projectDefinitionMutationLock(projectRoot, () => {
149
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
150
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
151
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
152
+ }
153
+ const data = parsed;
154
+ if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
155
+ if (data.plan === undefined) {
156
+ data.plan = {};
157
+ }
158
+ else {
159
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
160
+ }
161
+ }
162
+ const plan = data.plan;
163
+ const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
164
+ const existingPolicy = plan[PLAN_POLICY_KEY];
165
+ if (typeof existingPolicy !== "object" ||
166
+ existingPolicy === null ||
167
+ Array.isArray(existingPolicy)) {
168
+ if (existingPolicy === undefined) {
169
+ plan[PLAN_POLICY_KEY] = {};
170
+ }
171
+ else {
172
+ throw new Error("plan.policy is not an object");
173
+ }
174
+ }
175
+ const policyBlock = plan[PLAN_POLICY_KEY];
176
+ const previous = resolveHostHooksPolicy(policyBlock.hostHooks);
177
+ const next = { ...previous, [options.host]: false };
178
+ const changedFlag = previous[options.host] !== false || legacyKeyMigrated;
179
+ policyBlock.hostHooks = next;
180
+ if (changedFlag) {
181
+ atomicWriteProjectDefinition(path, data);
182
+ }
183
+ const actor = options.actor ?? disableHostHooksInvocation();
184
+ const note = options.note ?? "";
185
+ const parts = [
186
+ `actor=${actor}`,
187
+ `${FIELD_HOST_HOOKS}.${options.host}=false`,
188
+ `previous=${JSON.stringify(previous)}`,
189
+ ];
190
+ if (note) {
191
+ parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
192
+ }
193
+ appendAuditLog(projectRoot, parts.join(" "), changedFlag);
194
+ return { changed: changedFlag };
195
+ });
196
+ const lines = [
197
+ `\u2713 ${FIELD_HOST_HOOKS}.${options.host}=false (deft-hook guardrails removed for ${options.host}; result is tracked).`,
198
+ changed ? " audit: meta/policy-changes.log updated." : POLICY_AUDIT_NOOP_STDOUT,
199
+ " Run `deft update` to strip Directive-managed hook entries for that host.",
200
+ ];
201
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
202
+ }
203
+ catch (err) {
204
+ const message = err instanceof Error ? err.message : String(err);
205
+ if (message.includes("ENOENT") || message.includes("PROJECT-DEFINITION not found")) {
206
+ return {
207
+ exitCode: 2,
208
+ stdout: `\u274c PROJECT-DEFINITION not found at ${path}\n`,
209
+ changed: false,
210
+ };
211
+ }
212
+ return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
213
+ }
214
+ }
96
215
  //# sourceMappingURL=host-hooks.js.map
@@ -421,6 +421,7 @@ function applyForceOn(projectRoot, options, intent) {
421
421
  ? existing.previousProductSignal
422
422
  : ORG_FORCE_ON_NO_BASELINE;
423
423
  const actor = options.actor ?? "directive-update";
424
+ const changed = valueFeedbackChanged || productSignalChanged;
424
425
  appendAuditLog(projectRoot, [
425
426
  `actor=${actor}`,
426
427
  "org-force-on-v2822",
@@ -428,7 +429,7 @@ function applyForceOn(projectRoot, options, intent) {
428
429
  `productSignal=${needsPs ? "forced-on" : "unchanged"}`,
429
430
  `previousValueFeedback=${JSON.stringify(normalizePolicySnapshot(markerPreviousVf))}`,
430
431
  `previousProductSignal=${JSON.stringify(normalizePolicySnapshot(markerPreviousPs))}`,
431
- ].join(" "));
432
+ ].join(" "), changed);
432
433
  writeMarker(projectRoot, options, {
433
434
  // OR with existing so partial re-apply does not clear install-force-on source.
434
435
  valueFeedback: needsVf || (existing?.valueFeedback ?? false),
@@ -41,7 +41,7 @@ export declare function readPlanOnboarding(plan: unknown): unknown;
41
41
  * policy key is present. Used by writers so a mutation never strands a legacy
42
42
  * bare block alongside the namespaced one.
43
43
  */
44
- export declare function migrateLegacyPolicyKey(plan: Record<string, unknown>): void;
44
+ export declare function migrateLegacyPolicyKey(plan: Record<string, unknown>): boolean;
45
45
  /** The namespaced/legacy key pairs a bare block can silently shadow (#2301). */
46
46
  export declare const SHADOWABLE_PLAN_EXTENSIONS: ReadonlyArray<{
47
47
  readonly namespacedKey: string;
@@ -68,7 +68,9 @@ export function migrateLegacyPolicyKey(plan) {
68
68
  if (plan[PLAN_POLICY_KEY] === undefined && plan[LEGACY_PLAN_POLICY_KEY] !== undefined) {
69
69
  plan[PLAN_POLICY_KEY] = plan[LEGACY_PLAN_POLICY_KEY];
70
70
  delete plan[LEGACY_PLAN_POLICY_KEY];
71
+ return true;
71
72
  }
73
+ return false;
72
74
  }
73
75
  /** The namespaced/legacy key pairs a bare block can silently shadow (#2301). */
74
76
  export const SHADOWABLE_PLAN_EXTENSIONS = [
@@ -3,7 +3,7 @@ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "..
3
3
  import { productSignalInstallForceOnSource } 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
  /** Canonical registered policy field name (#2693). */
8
8
  export const FIELD_PRODUCT_SIGNAL = "plan.policy.productSignal";
9
9
  /** Short alias for `policy:show --field=productSignal`. */
@@ -157,7 +157,7 @@ export function enableProductSignal(projectRoot, options) {
157
157
  }
158
158
  }
159
159
  const plan = data.plan;
160
- migrateLegacyPolicyKey(plan);
160
+ const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
161
161
  const existingPolicy = plan[PLAN_POLICY_KEY];
162
162
  if (typeof existingPolicy !== "object" ||
163
163
  existingPolicy === null ||
@@ -181,7 +181,8 @@ export function enableProductSignal(projectRoot, options) {
181
181
  const nextBlock = { enabled: true, sinkRepo };
182
182
  const previousNormalized = resolveProductSignalFromTypedBlock(previous);
183
183
  const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
184
- previousNormalized.sinkRepo !== nextBlock.sinkRepo;
184
+ previousNormalized.sinkRepo !== nextBlock.sinkRepo ||
185
+ legacyKeyMigrated;
185
186
  policyBlock.productSignal = nextBlock;
186
187
  if (changedFlag) {
187
188
  atomicWriteProjectDefinition(path, data);
@@ -197,15 +198,13 @@ export function enableProductSignal(projectRoot, options) {
197
198
  if (note) {
198
199
  parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
199
200
  }
200
- appendAuditLog(projectRoot, parts.join(" "));
201
+ appendAuditLog(projectRoot, parts.join(" "), changedFlag);
201
202
  return { changed: changedFlag };
202
203
  });
203
204
  const resolved = resolveProductSignal(projectRoot);
204
205
  const lines = [
205
206
  `\u2713 ${FIELD_PRODUCT_SIGNAL}.enabled=true (product-signal ON).`,
206
- changed
207
- ? " audit: meta/policy-changes.log updated."
208
- : " no-op: value already matched (audit entry still appended for trail).",
207
+ changed ? " audit: meta/policy-changes.log updated." : POLICY_AUDIT_NOOP_STDOUT,
209
208
  formatProductSignalStatusLine(resolved),
210
209
  ];
211
210
  return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
@@ -18,7 +18,7 @@ import { existsSync, readFileSync } from "node:fs";
18
18
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
19
19
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
20
20
  import { policyColonInvocation } from "./policy-invocation.js";
21
- import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
21
+ import { appendAuditLog, loadProjectDefinition, projectDefinitionPath, stampChangedToken, } from "./resolve.js";
22
22
  export const FIELD_REQUIRE_HUMAN_MERGE = "plan.policy.requireHumanMerge";
23
23
  export const FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS = "requireHumanMerge";
24
24
  export const FIELD_AUTO_DEPLOY_ON_MERGE = "plan.policy.autoDeployOnMerge";
@@ -285,7 +285,7 @@ export function setRequireHumanMerge(projectRoot, options) {
285
285
  }
286
286
  }
287
287
  const plan = data.plan;
288
- migrateLegacyPolicyKey(plan);
288
+ const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
289
289
  const existingPolicy = plan[PLAN_POLICY_KEY];
290
290
  if (typeof existingPolicy !== "object" ||
291
291
  existingPolicy === null ||
@@ -309,8 +309,7 @@ export function setRequireHumanMerge(projectRoot, options) {
309
309
  delete narratives[LEGACY_REQUIRE_HUMAN_MERGE_KEY];
310
310
  legacyDropped = true;
311
311
  }
312
- atomicWriteProjectDefinition(path, data);
313
- const changed = previous !== Boolean(requireHumanMerge) || legacyDropped;
312
+ const changed = previous !== Boolean(requireHumanMerge) || legacyDropped || legacyKeyMigrated;
314
313
  const parts = [
315
314
  `actor=${actor}`,
316
315
  `requireHumanMerge=${requireHumanMerge ? "true" : "false"}`,
@@ -321,8 +320,11 @@ export function setRequireHumanMerge(projectRoot, options) {
321
320
  if (note) {
322
321
  parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
323
322
  }
324
- const auditEntry = parts.join(" ");
325
- appendAuditLog(projectRoot, auditEntry);
323
+ const auditEntry = stampChangedToken(parts.join(" "), changed);
324
+ if (changed) {
325
+ atomicWriteProjectDefinition(path, data);
326
+ }
327
+ appendAuditLog(projectRoot, auditEntry, changed);
326
328
  return { changed, auditEntry };
327
329
  });
328
330
  }
@@ -6,6 +6,10 @@ export declare const ENV_BYPASS = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
6
6
  export declare const LEGACY_NARRATIVE_KEY = "Allow direct commits to master";
7
7
  /** Audit log relative path (#746). */
8
8
  export declare const AUDIT_LOG_REL_PATH = "meta/policy-changes.log";
9
+ /** Operator line when a policy writer matches current state and does not append (#3528). */
10
+ export declare const POLICY_AUDIT_NOOP_STDOUT = " no-op: value already matched (ledger unchanged).";
11
+ /** Stamp a machine-readable `changed=true|false` token (#3528 / UPGRADING.md). */
12
+ export declare function stampChangedToken(entry: string, changed: boolean): string;
9
13
  export type PolicySource = "typed" | "legacy-narrative" | "env-bypass" | "default-fail-closed";
10
14
  export interface PolicyResult {
11
15
  readonly allowDirectCommits: boolean;
@@ -28,8 +32,9 @@ export declare function coerceLegacyNarrative(value: unknown): {
28
32
  export declare function loadProjectDefinition(projectRoot: string): [Record<string, unknown> | null, string | null];
29
33
  /** Resolve the effective branch-commit policy (#746 / #747). */
30
34
  export declare function resolvePolicy(projectRoot: string): PolicyResult;
31
- /** Append a one-line audit entry to meta/policy-changes.log (#746). */
32
- export declare function appendAuditLog(projectRoot: string, entry: string): string;
35
+ /** Append a one-line audit entry to meta/policy-changes.log (#746).
36
+ * No-op (`changed=false`) does not create or modify the tracked file (#3528). */
37
+ export declare function appendAuditLog(projectRoot: string, entry: string, changed?: boolean): string;
33
38
  /** Write the typed policy flag back to PROJECT-DEFINITION (#746). */
34
39
  export declare function setPolicy(projectRoot: string, options: {
35
40
  allowDirectCommits: boolean;
@@ -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
  }
@@ -11,6 +11,22 @@ export interface RuntimeAuthorityScopes {
11
11
  }
12
12
  /** Layers that contribute to a resolved write fence (#516 / #2443 / #2948 Wave 3). */
13
13
  export type WriteFenceSource = "project" | "story";
14
+ /**
15
+ * Whether recognized Shell dest-forms are enforced (#3438 / #3594).
16
+ *
17
+ * `off` (default) leaves Shell mutations exactly as they were before #3438:
18
+ * unrecognized and fail-open. `enforce` routes recognized dest-forms through
19
+ * `inspectMutationGates` and fail-closes targets it cannot prove.
20
+ *
21
+ * ! Default is `off` on purpose. Before #3438 Bash mutations were not gated at
22
+ * all, so defaulting to `enforce` would land new denials on every consumer with
23
+ * no opt-out. Opting IN via tracked project policy is a narrowing, which this
24
+ * fence model permits; a tracked switch that DISABLED enforcement would not be
25
+ * (see policy/deft-directive-disable.ts — repository-controlled content must
26
+ * not disable hooks for downstream clones).
27
+ */
28
+ export type ShellDestFormsMode = "off" | "enforce";
29
+ export declare const SHELL_DEST_FORMS_MODES: readonly ShellDestFormsMode[];
14
30
  export interface RuntimeAuthorityPolicy {
15
31
  readonly enabled: boolean;
16
32
  /** When non-empty, write targets must match at least one pattern. Empty = allow all paths. */
@@ -18,6 +34,8 @@ export interface RuntimeAuthorityPolicy {
18
34
  /** Deny wins over allow. */
19
35
  readonly denyPaths: readonly string[];
20
36
  readonly scopes: RuntimeAuthorityScopes;
37
+ /** Shell dest-form enforcement (#3438 / #3594). Default `off`. */
38
+ readonly shellDestForms: ShellDestFormsMode;
21
39
  /**
22
40
  * Optional story-layer allow globs from `plan.metadata.swarm.file_scope`
23
41
  * (populated by `resolveWriteFence` only). When non-empty, path must match
@@ -3,6 +3,7 @@ import { readPlanPolicy } from "./plan-extensions.js";
3
3
  import { loadProjectDefinition } from "./resolve.js";
4
4
  export const FIELD_RUNTIME_AUTHORITY = "plan.policy.runtimeAuthority";
5
5
  export const FIELD_RUNTIME_AUTHORITY_CLI_ALIAS = "runtimeAuthority";
6
+ export const SHELL_DEST_FORMS_MODES = ["off", "enforce"];
6
7
  export const DEFAULT_RUNTIME_AUTHORITY_SCOPES = {
7
8
  edits: true,
8
9
  push: false,
@@ -14,6 +15,7 @@ export const DEFAULT_RUNTIME_AUTHORITY_POLICY = {
14
15
  allowPaths: [],
15
16
  denyPaths: [],
16
17
  scopes: DEFAULT_RUNTIME_AUTHORITY_SCOPES,
18
+ shellDestForms: "off",
17
19
  };
18
20
  function readBoolean(rec, key, fallback) {
19
21
  if (key in rec && typeof rec[key] === "boolean") {
@@ -37,6 +39,11 @@ function readScopes(raw) {
37
39
  merge: readBoolean(rec, "merge", DEFAULT_RUNTIME_AUTHORITY_SCOPES.merge),
38
40
  };
39
41
  }
42
+ function readShellDestForms(raw) {
43
+ // Unknown / malformed resolves to the safe default rather than throwing, but
44
+ // validateRuntimeAuthority reports it so a typo is not silent (#3594).
45
+ return raw === "enforce" ? "enforce" : DEFAULT_RUNTIME_AUTHORITY_POLICY.shellDestForms;
46
+ }
40
47
  export function resolveRuntimeAuthorityPolicy(raw) {
41
48
  if (raw === null || raw === undefined) {
42
49
  return DEFAULT_RUNTIME_AUTHORITY_POLICY;
@@ -50,6 +57,7 @@ export function resolveRuntimeAuthorityPolicy(raw) {
50
57
  allowPaths: readStringArray(rec.allowPaths),
51
58
  denyPaths: readStringArray(rec.denyPaths),
52
59
  scopes: readScopes(rec.scopes),
60
+ shellDestForms: readShellDestForms(rec.shellDestForms),
53
61
  };
54
62
  }
55
63
  export function validateRuntimeAuthority(value) {
@@ -69,6 +77,9 @@ export function validateRuntimeAuthority(value) {
69
77
  errors.push(`${FIELD_RUNTIME_AUTHORITY}.${key} must be an array of path globs`);
70
78
  }
71
79
  }
80
+ if ("shellDestForms" in rec && !SHELL_DEST_FORMS_MODES.includes(rec.shellDestForms)) {
81
+ errors.push(`${FIELD_RUNTIME_AUTHORITY}.shellDestForms must be one of ${SHELL_DEST_FORMS_MODES.join(" | ")}`);
82
+ }
72
83
  if ("scopes" in rec) {
73
84
  const scopes = rec.scopes;
74
85
  if (typeof scopes !== "object" || scopes === null || Array.isArray(scopes)) {
@@ -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);
@@ -140,6 +140,9 @@ export function resolveWriteFence(projectPolicy, storyFileScope, options) {
140
140
  allowPaths,
141
141
  denyPaths,
142
142
  scopes,
143
+ // Carry the project setting through: resolving a write fence must not
144
+ // silently enable Shell dest-form enforcement for a story (#3438 / #3594).
145
+ shellDestForms: projectPolicy.shellDestForms,
143
146
  storyAllowPaths: storyAllow,
144
147
  fenceSources: sources,
145
148
  };
@@ -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
  *
@@ -300,16 +300,32 @@ function formatDriftFailure(repo, drift) {
300
300
  };
301
301
  }
302
302
  function runDriftProbe(resolvedRepo, cacheRoot, source, options) {
303
- if (options.probeDriftFn) {
304
- return options.probeDriftFn(resolvedRepo, cacheRoot, source);
305
- }
306
303
  try {
304
+ if (options.probeDriftFn) {
305
+ return options.probeDriftFn(resolvedRepo, cacheRoot, source);
306
+ }
307
307
  return probeCacheDrift({ repo: resolvedRepo, source, cacheRoot });
308
308
  }
309
309
  catch {
310
+ // Deliberate fail-open (#3738 / #3422): a thrown probe (429 after retry,
311
+ // 502/503, DNS, auth) returns null; evaluate treats null as pass.
312
+ // Work-selection / session:start still inherit this. The write path no
313
+ // longer executes cache_fresh, so this is not a write-authorization bypass.
314
+ // Do not silently convert to fail-closed without revisiting #3422 (local
315
+ // work MAY continue when it does not need the forge).
310
316
  return null;
311
317
  }
312
318
  }
319
+ /**
320
+ * Drift skip is an explicit `--skip-drift-probe` request (#3507).
321
+ * Presence of `--for-issue` is work selection and re-arms the probe.
322
+ * Absence of `--for-issue` is not a skip signal.
323
+ */
324
+ export function shouldSkipDriftProbe(options) {
325
+ if (options.forIssue !== undefined && options.forIssue !== null)
326
+ return false;
327
+ return options.skipDriftProbe === true;
328
+ }
313
329
  const REMEDIATION_NO_CANDIDATES = [
314
330
  " Recovery: run `deft triage:bootstrap` to initialise the triage log.",
315
331
  ].join("\n");
@@ -471,7 +487,8 @@ function evaluateWithContext(projectRoot, options, ctx) {
471
487
  ].join("\n"),
472
488
  };
473
489
  }
474
- if (resolvedRepo !== null && !allowStale) {
490
+ const skipDrift = shouldSkipDriftProbe(options);
491
+ if (resolvedRepo !== null && !allowStale && !skipDrift) {
475
492
  const drift = runDriftProbe(resolvedRepo, cacheRoot, source, options);
476
493
  if (drift !== null &&
477
494
  (drift.stateDriftNumbers.length > 0 || drift.contentDriftNumbers.length > 0)) {
@@ -506,6 +523,9 @@ function evaluateWithContext(projectRoot, options, ctx) {
506
523
  if (options.forIssue !== undefined && options.forIssue !== null) {
507
524
  msg += ` Issue #${options.forIssue} latest decision = accept; in subscription scope.`;
508
525
  }
526
+ if (skipDrift) {
527
+ msg += " Drift probe skipped (no work selection).";
528
+ }
509
529
  return { code: 0, message: msg };
510
530
  }
511
531
  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
  /**