@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
@@ -0,0 +1,2 @@
1
+ export { type CheckIgnoreMatch, displayIgnoreSource, evaluateLifecycleVisible, formatLifecycleVisibleSessionLines, indexFlagKind, isSelectiveLifecyclePath, LIFECYCLE_ROOT_PREFIXES, LIFECYCLE_STAGE_DIRS, type LifecycleHideFinding, type LifecycleHideKind, type LifecycleVisibleOptions, type LifecycleVisibleResult, type LsFilesVerboseRecord, lifecycleRootRelPaths, parseCheckIgnoreVerboseLine, parseLsFilesVerboseRecord, } from "./evaluate.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { displayIgnoreSource, evaluateLifecycleVisible, formatLifecycleVisibleSessionLines, indexFlagKind, isSelectiveLifecyclePath, LIFECYCLE_ROOT_PREFIXES, LIFECYCLE_STAGE_DIRS, lifecycleRootRelPaths, parseCheckIgnoreVerboseLine, parseLsFilesVerboseRecord, } from "./evaluate.js";
2
+ //# sourceMappingURL=index.js.map
@@ -167,12 +167,12 @@ function matchLabeledCommand(line) {
167
167
  return null;
168
168
  return line.slice(i).trimEnd();
169
169
  }
170
- /** `$ cmd` or `> cmd` prompt lines. */
170
+ /** `$ cmd` prompt lines. Markdown blockquote `>` is not a prompt (#3572). */
171
171
  function matchPromptCommand(line) {
172
172
  let i = skipWs(line, 0);
173
173
  if (i >= line.length)
174
174
  return null;
175
- if (line[i] !== "$" && line[i] !== ">")
175
+ if (line[i] !== "$")
176
176
  return null;
177
177
  i = skipWs(line, i + 1);
178
178
  if (i >= line.length)
@@ -259,7 +259,7 @@ function flushFenceBody(body, fenceLang, fenceStartLine, requireRegion, regionAc
259
259
  return;
260
260
  if (!isShellFenceLang(fenceLang))
261
261
  return;
262
- // Transcript lines are skipped. `$`/`>` prompts inside a transcript fence are
262
+ // Transcript lines are skipped. `$` prompts inside a transcript fence are
263
263
  // suggested-fixes (biome migrate), not stated AC (#3511). Comments do not
264
264
  // mark a fence as transcript, so a genuine command next to `# Error:` still
265
265
  // captures.
@@ -2,6 +2,19 @@ export declare const GATE_CLASSES: Set<string>;
2
2
  export declare const GATE_TIERS: Set<string>;
3
3
  export declare const GATE_MATCH_PREDICATES: Set<string>;
4
4
  export declare const GATE_MATCH_STATES: Set<string>;
5
+ /** ADR-005 / #3434 Story 1 -- author-stamped marker vocabulary. */
6
+ export declare const DESIGN_CRITIQUE_GATE_ID = "design-critique";
7
+ /** List-visible mirror; this is what judgmentGates predicates match. */
8
+ export declare const DESIGN_CRITIQUE_MARKER_LABEL = "design-critique:mechanism-shaped";
9
+ /** Authoritative body-text field in the #1423 triage write-back comment. */
10
+ export declare const DESIGN_CRITIQUE_MARKER_FIELD = "mechanism-shaped: true";
11
+ /**
12
+ * Clearance line shape (ADR-005). Presence and shape only -- a non-empty
13
+ * because-clause is required; its semantic content is never evaluated.
14
+ */
15
+ export declare const DESIGN_CRITIQUE_CLEARANCE_SHAPE: RegExp;
16
+ /** True when `line` has clearance presence+shape. Never scores the reason text. */
17
+ export declare function designCritiqueClearanceShapeOk(line: string): boolean;
5
18
  export interface JudgmentGate {
6
19
  readonly gate_id: string;
7
20
  readonly gate_class: string;
@@ -7,6 +7,21 @@ export const GATE_CLASSES = new Set(["mechanical", "declared"]);
7
7
  export const GATE_TIERS = new Set(["auto", "review", "block"]);
8
8
  export const GATE_MATCH_PREDICATES = new Set(["labels", "body-text", "paths", "state", "age-days"]);
9
9
  export const GATE_MATCH_STATES = new Set(["open", "closed"]);
10
+ /** ADR-005 / #3434 Story 1 -- author-stamped marker vocabulary. */
11
+ export const DESIGN_CRITIQUE_GATE_ID = "design-critique";
12
+ /** List-visible mirror; this is what judgmentGates predicates match. */
13
+ export const DESIGN_CRITIQUE_MARKER_LABEL = "design-critique:mechanism-shaped";
14
+ /** Authoritative body-text field in the #1423 triage write-back comment. */
15
+ export const DESIGN_CRITIQUE_MARKER_FIELD = "mechanism-shaped: true";
16
+ /**
17
+ * Clearance line shape (ADR-005). Presence and shape only -- a non-empty
18
+ * because-clause is required; its semantic content is never evaluated.
19
+ */
20
+ export const DESIGN_CRITIQUE_CLEARANCE_SHAPE = /^design-critique:\s*(warranted|not warranted),\s*because\s+\S/i;
21
+ /** True when `line` has clearance presence+shape. Never scores the reason text. */
22
+ export function designCritiqueClearanceShapeOk(line) {
23
+ return DESIGN_CRITIQUE_CLEARANCE_SHAPE.test(line.trim());
24
+ }
10
25
  function validateStrList(value, prefix, key) {
11
26
  if (!Array.isArray(value) || value.length === 0) {
12
27
  return [`${prefix}.${key} must be a non-empty list of strings`];
@@ -12,6 +12,11 @@ export const SESSION_RELPATH = [".deft", "probe-session.json"];
12
12
  export const STATE_INTERROGATE = "interrogate";
13
13
  export const STATE_COMPLETE = "complete";
14
14
  export const VALID_DECISION_STATUSES = new Set(["locked", "deferred", "risk-accepted"]);
15
+ /** Live CLI teach-string (#3556). The Python helper was deleted. */
16
+ const PROBE_SESSION_START = "`deft probe-session start --target <scope>`";
17
+ const PROBE_SESSION_RECORD = "`deft probe-session record ...`";
18
+ const PROBE_SESSION_COMPLETE = "`deft probe-session complete`";
19
+ const NO_SESSION_START = `No active probe session. Start one with ${PROBE_SESSION_START}.`;
15
20
  export class ProbeHandoffBlockedError extends Error {
16
21
  session;
17
22
  constructor(message, session = null) {
@@ -249,7 +254,7 @@ export function startSession(projectRoot, options) {
249
254
  export function recordDecision(projectRoot, options) {
250
255
  const session = readSession(projectRoot);
251
256
  if (session === null) {
252
- throw new ProbeHandoffBlockedError("No active probe session. Start one with `uv run python scripts/probe_session.py start --target <scope>`.");
257
+ throw new ProbeHandoffBlockedError(NO_SESSION_START);
253
258
  }
254
259
  if (session.state !== STATE_INTERROGATE) {
255
260
  throw new ProbeHandoffBlockedError("Probe session is already complete; decisions cannot be appended.", session);
@@ -275,7 +280,7 @@ export function recordDecision(projectRoot, options) {
275
280
  export function setCurrentBranch(projectRoot, branch) {
276
281
  const session = readSession(projectRoot);
277
282
  if (session === null) {
278
- throw new ProbeHandoffBlockedError("No active probe session. Start one with `uv run python scripts/probe_session.py start --target <scope>`.");
283
+ throw new ProbeHandoffBlockedError(NO_SESSION_START);
279
284
  }
280
285
  if (session.state !== STATE_INTERROGATE) {
281
286
  throw new ProbeHandoffBlockedError("Probe session is already complete; current branch cannot change.", session);
@@ -287,7 +292,7 @@ export function setCurrentBranch(projectRoot, branch) {
287
292
  export function markComplete(projectRoot, now) {
288
293
  const session = readSession(projectRoot);
289
294
  if (session === null) {
290
- throw new ProbeHandoffBlockedError("No active probe session. Start one with `uv run python scripts/probe_session.py start --target <scope>`.");
295
+ throw new ProbeHandoffBlockedError(NO_SESSION_START);
291
296
  }
292
297
  if (session.state === STATE_COMPLETE) {
293
298
  return session;
@@ -307,7 +312,7 @@ function pythonRepr(value) {
307
312
  function blockedMessage(session, action) {
308
313
  if (session === null) {
309
314
  return (`Probe handoff blocked for ${action}: no active probe session. ` +
310
- "Start interrogation with `uv run python scripts/probe_session.py start --target <scope>` " +
315
+ `Start interrogation with ${PROBE_SESSION_START} ` +
311
316
  "and finish with `... complete` only after transition criteria are met.");
312
317
  }
313
318
  return (`Probe handoff blocked for ${action}: session state is ` +
@@ -315,9 +320,9 @@ function blockedMessage(session, action) {
315
320
  `currentBranch=${pythonRepr(session.current_branch)}, ` +
316
321
  `resolvedDecisions=${session.resolved_decisions.length}). ` +
317
322
  "Continue interrogation until transition criteria are met, record decisions " +
318
- "with `uv run python scripts/probe_session.py record ...`, then run " +
319
- "`uv run python scripts/probe_session.py complete` before writing artifacts " +
320
- "or updating completedStrategies.probe in plan.vbrief.json.");
323
+ `with ${PROBE_SESSION_RECORD}, then run ` +
324
+ `${PROBE_SESSION_COMPLETE} before writing artifacts ` +
325
+ "or updating completedStrategies.probe in plan.xbrief.json.");
321
326
  }
322
327
  export function requireHandoffAllowed(projectRoot, action) {
323
328
  const session = readSession(projectRoot);
@@ -330,7 +335,7 @@ export function guardProbeArtifact(projectRoot, artifactPath) {
330
335
  return requireHandoffAllowed(projectRoot, `probe artifact write (${artifactPath})`);
331
336
  }
332
337
  export function guardPlanProbeRegistration(projectRoot) {
333
- return requireHandoffAllowed(projectRoot, "completedStrategies.probe registration in plan.vbrief.json");
338
+ return requireHandoffAllowed(projectRoot, "completedStrategies.probe registration in plan.xbrief.json");
334
339
  }
335
340
  export function sessionSummary(session) {
336
341
  return {
@@ -2,6 +2,10 @@
2
2
  * Risk-tiered judgment-gate engine (#1419 Slice 3). Port of scripts/verify_judgment_gates.py.
3
3
  */
4
4
  import { type JudgmentGatesPolicy } from "./judgment-policy.js";
5
+ /** Thrown when a design-critique clearance reason fails presence/shape (never content). */
6
+ export declare class JudgmentClearanceShapeError extends Error {
7
+ constructor(message: string);
8
+ }
5
9
  /** Display/back-compat constant; resolution flows through resolveAuditDir (#2109). */
6
10
  export declare const AUDIT_DIR_REL = "vbrief/.audit";
7
11
  export declare const CLEARANCE_LOG_NAME = "judgment-gate-clearances.jsonl";
@@ -7,8 +7,15 @@ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, statSyn
7
7
  import { join, resolve } from "node:path";
8
8
  import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
9
9
  import { resolveAuditDir } from "../layout/resolve.js";
10
- import { resolveJudgmentGates } from "./judgment-policy.js";
10
+ import { DESIGN_CRITIQUE_GATE_ID, designCritiqueClearanceShapeOk, resolveJudgmentGates, } from "./judgment-policy.js";
11
11
  import { matchAny } from "./pathspec.js";
12
+ /** Thrown when a design-critique clearance reason fails presence/shape (never content). */
13
+ export class JudgmentClearanceShapeError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "JudgmentClearanceShapeError";
17
+ }
18
+ }
12
19
  /** Display/back-compat constant; resolution flows through resolveAuditDir (#2109). */
13
20
  export const AUDIT_DIR_REL = "vbrief/.audit";
14
21
  export const CLEARANCE_LOG_NAME = "judgment-gate-clearances.jsonl";
@@ -163,6 +170,12 @@ export function readClearances(projectRoot, logPath) {
163
170
  return out;
164
171
  }
165
172
  export function recordClearance(projectRoot, options) {
173
+ if (options.gate_id === DESIGN_CRITIQUE_GATE_ID &&
174
+ !designCritiqueClearanceShapeOk(options.reason ?? "")) {
175
+ throw new JudgmentClearanceShapeError("design-critique clearance reason must match shape " +
176
+ "'design-critique: warranted | not warranted, because ...' " +
177
+ "(presence and shape only; the because-clause is not scored)");
178
+ }
166
179
  const path = options.log_path ?? clearanceLogPath(projectRoot);
167
180
  const auditDir = resolveAuditDir(projectRoot);
168
181
  assertProjectionContained(projectRoot, auditDir);
@@ -202,6 +215,15 @@ export function fingerprintScope(evidence) {
202
215
  const payload = JSON.stringify(sorted);
203
216
  return createHash("sha256").update(payload, "utf8").digest("hex");
204
217
  }
218
+ function acceptClearanceShape(gateId, clearance) {
219
+ if (clearance === null) {
220
+ return null;
221
+ }
222
+ if (gateId !== DESIGN_CRITIQUE_GATE_ID) {
223
+ return clearance;
224
+ }
225
+ return designCritiqueClearanceShapeOk(String(clearance.reason ?? "")) ? clearance : null;
226
+ }
205
227
  function lookupClearance(clearances, gateId, scope) {
206
228
  let valid = null;
207
229
  let stale = null;
@@ -383,7 +405,8 @@ export function buildReport(projectRoot, candidate, options = {}) {
383
405
  continue;
384
406
  }
385
407
  const scope = fingerprintScope(evidence);
386
- const [valid, stale] = lookupClearance(records, String(gate.id), scope);
408
+ const [validRaw, stale] = lookupClearance(records, String(gate.id), scope);
409
+ const valid = acceptClearanceShape(String(gate.id), validRaw);
387
410
  outcomes.push({
388
411
  gate_id: String(gate.id),
389
412
  gate_class: String(gate.class),
@@ -605,15 +628,24 @@ function clearMain(argv) {
605
628
  if (args.updatedAt !== null)
606
629
  evidence["age-days"] = args.updatedAt;
607
630
  const scope = fingerprintScope(evidence);
608
- const entry = recordClearance(projectRoot, {
609
- gate_id: args.gateId,
610
- cleared_scope: scope,
611
- reviewers: args.reviewers,
612
- actor: args.actor,
613
- reason: args.reason,
614
- });
615
- process.stdout.write(`recorded clearance ${String(entry.clearance_id)} for gate ${JSON.stringify(args.gateId)} (cleared_scope=${scope.slice(0, 12)}...)\n`);
616
- return 0;
631
+ try {
632
+ const entry = recordClearance(projectRoot, {
633
+ gate_id: args.gateId,
634
+ cleared_scope: scope,
635
+ reviewers: args.reviewers,
636
+ actor: args.actor,
637
+ reason: args.reason,
638
+ });
639
+ process.stdout.write(`recorded clearance ${String(entry.clearance_id)} for gate ${JSON.stringify(args.gateId)} (cleared_scope=${scope.slice(0, 12)}...)\n`);
640
+ return 0;
641
+ }
642
+ catch (err) {
643
+ if (err instanceof JudgmentClearanceShapeError) {
644
+ process.stderr.write(`${err.message}\n`);
645
+ return 1;
646
+ }
647
+ throw err;
648
+ }
617
649
  }
618
650
  function parseEvalArgs(argv) {
619
651
  const parsed = {
@@ -23,7 +23,7 @@ import { RunSummaryEmitter } from "../run-summary/emit.js";
23
23
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
24
24
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
25
25
  import { policyColonInvocation } from "./policy-invocation.js";
26
- import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
26
+ import { appendAuditLog, loadProjectDefinition, POLICY_AUDIT_NOOP_STDOUT, projectDefinitionPath, } from "./resolve.js";
27
27
  /** Canonical dotted policy field name. */
28
28
  export const FIELD_CEREMONY_DIAL = "plan.policy.ceremonyDial";
29
29
  /** Short alias for `policy:show --field=ceremonyDial`. */
@@ -527,7 +527,7 @@ export function setCeremonyDial(projectRoot, options) {
527
527
  }
528
528
  }
529
529
  const plan = data.plan;
530
- migrateLegacyPolicyKey(plan);
530
+ const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
531
531
  const existingPolicy = plan[PLAN_POLICY_KEY];
532
532
  if (typeof existingPolicy !== "object" ||
533
533
  existingPolicy === null ||
@@ -564,7 +564,9 @@ export function setCeremonyDial(projectRoot, options) {
564
564
  override: nextOverride,
565
565
  };
566
566
  const prevParsed = parseConfig(previous).config;
567
- const changedFlag = prevParsed.enabled !== nextBlock.enabled || prevParsed.override !== nextBlock.override;
567
+ const changedFlag = prevParsed.enabled !== nextBlock.enabled ||
568
+ prevParsed.override !== nextBlock.override ||
569
+ legacyKeyMigrated;
568
570
  policyBlock.ceremonyDial = nextBlock;
569
571
  if (changedFlag) {
570
572
  atomicWriteProjectDefinition(path, data);
@@ -580,15 +582,13 @@ export function setCeremonyDial(projectRoot, options) {
580
582
  if (note) {
581
583
  parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
582
584
  }
583
- appendAuditLog(projectRoot, parts.join(" "));
585
+ appendAuditLog(projectRoot, parts.join(" "), changedFlag);
584
586
  return { changed: changedFlag };
585
587
  });
586
588
  const resolved = resolveCeremonyDial(projectRoot);
587
589
  const lines = [
588
590
  `\u2713 ${FIELD_CEREMONY_DIAL} updated.`,
589
- changed
590
- ? " audit: meta/policy-changes.log updated."
591
- : " no-op: value already matched (audit entry still appended for trail).",
591
+ changed ? " audit: meta/policy-changes.log updated." : POLICY_AUDIT_NOOP_STDOUT,
592
592
  formatCeremonyDialStatusLine(resolved),
593
593
  ];
594
594
  return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
@@ -1,6 +1,12 @@
1
1
  import type { HookHost } from "../hooks/dispatcher.js";
2
+ export type { HookHost };
2
3
  export declare const FIELD_HOST_HOOKS = "plan.policy.hostHooks";
3
4
  export declare const FIELD_HOST_HOOKS_CLI_ALIAS = "hostHooks";
5
+ export declare const DISABLE_HOST_HOOKS_SUBCOMMAND = "disable-host-hooks";
6
+ /** Confirm-gated disable verb for unused-host recovery copy (#3571). CLI form: no go-task `--`. */
7
+ export declare function disableHostHooksInvocation(trailing?: string): string;
8
+ export declare const HOST_HOOKS_DISABLE_CAPABILITY_COST_DISCLOSURE: string;
9
+ export declare const UNUSED_HOST_HOOKS_RECOVERY: string;
4
10
  /** Per-host Directive hook deposit toggles (#2752). */
5
11
  export type HostHooksPolicy = Record<HookHost, boolean>;
6
12
  export declare const DEFAULT_HOST_HOOKS_POLICY: HostHooksPolicy;
@@ -18,4 +24,18 @@ export declare function isHostHookDepositEnabled(host: HookHost, policy?: HostHo
18
24
  export declare function inspectHostHooks(data: Record<string, unknown> | null): HostHooksPolicyField;
19
25
  /** Resolve host hook deposit policy from PROJECT-DEFINITION on disk. */
20
26
  export declare function loadHostHooksPolicyFromProject(projectRoot: string): HostHooksPolicy;
27
+ export declare function parseHookHost(value: string | undefined): HookHost | null;
28
+ export interface DisableHostHooksOptions {
29
+ readonly host: HookHost;
30
+ readonly confirm: boolean;
31
+ readonly actor?: string;
32
+ readonly note?: string;
33
+ }
34
+ export interface DisableHostHooksResult {
35
+ readonly exitCode: 0 | 1 | 2;
36
+ readonly stdout: string;
37
+ readonly changed: boolean;
38
+ }
39
+ /** Persist `hostHooks.<host>=false` after capability-cost disclosure (#3571). */
40
+ export declare function disableHostHooks(projectRoot: string, options: DisableHostHooksOptions): DisableHostHooksResult;
21
41
  //# sourceMappingURL=host-hooks.d.ts.map
@@ -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;