@deftai/directive-core 0.96.0 → 0.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/authz/classify.js +291 -0
  2. package/dist/cache/archive.js +10 -4
  3. package/dist/check/cached-orchestrator.d.ts +5 -0
  4. package/dist/check/cached-orchestrator.js +18 -1
  5. package/dist/check/gate-lists.d.ts +20 -0
  6. package/dist/check/gate-lists.js +54 -9
  7. package/dist/check/index.d.ts +1 -1
  8. package/dist/check/index.js +1 -1
  9. package/dist/check/orchestrator.d.ts +4 -0
  10. package/dist/check/orchestrator.js +4 -0
  11. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  12. package/dist/consumer-check-contract/evaluate.js +699 -0
  13. package/dist/consumer-check-contract/index.d.ts +5 -0
  14. package/dist/consumer-check-contract/index.js +5 -0
  15. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  16. package/dist/delivery-attempt/disk-begin.js +68 -0
  17. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  18. package/dist/delivery-attempt/evaluate.js +443 -0
  19. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  20. package/dist/delivery-attempt/fingerprint.js +100 -0
  21. package/dist/delivery-attempt/handoff.d.ts +25 -0
  22. package/dist/delivery-attempt/handoff.js +102 -0
  23. package/dist/delivery-attempt/index.d.ts +17 -0
  24. package/dist/delivery-attempt/index.js +17 -0
  25. package/dist/delivery-attempt/ledger.d.ts +169 -0
  26. package/dist/delivery-attempt/ledger.js +758 -0
  27. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  28. package/dist/delivery-attempt/material-delta.js +126 -0
  29. package/dist/delivery-attempt/types.d.ts +210 -0
  30. package/dist/delivery-attempt/types.js +77 -0
  31. package/dist/doctor/checks.d.ts +7 -0
  32. package/dist/doctor/checks.js +83 -0
  33. package/dist/doctor/index.d.ts +1 -0
  34. package/dist/doctor/index.js +1 -0
  35. package/dist/doctor/main.js +12 -0
  36. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  37. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  38. package/dist/hooks/dispatcher.d.ts +7 -1
  39. package/dist/hooks/dispatcher.js +117 -13
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.js +4 -0
  42. package/dist/init-deposit/gitignore.js +7 -0
  43. package/dist/init-deposit/hygiene.d.ts +70 -1
  44. package/dist/init-deposit/hygiene.js +582 -8
  45. package/dist/init-deposit/init-deposit.js +5 -0
  46. package/dist/init-deposit/refresh.js +3 -0
  47. package/dist/init-deposit/scaffold.js +277 -4
  48. package/dist/init-deposit/skill-discovery-deposit.js +15 -0
  49. package/dist/policy/check-resume.d.ts +72 -0
  50. package/dist/policy/check-resume.js +253 -0
  51. package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
  52. package/dist/policy/coverage-check-resume-presets.js +228 -0
  53. package/dist/policy/coverage-debt.d.ts +76 -0
  54. package/dist/policy/coverage-debt.js +262 -0
  55. package/dist/policy/index.d.ts +3 -0
  56. package/dist/policy/index.js +50 -21
  57. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  58. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  59. package/dist/pr-merge-readiness/compute.js +10 -1
  60. package/dist/pr-merge-readiness/index.d.ts +2 -1
  61. package/dist/pr-merge-readiness/index.js +2 -1
  62. package/dist/pr-merge-readiness/output.js +14 -0
  63. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  64. package/dist/pr-merge-readiness/platform-status.js +49 -0
  65. package/dist/pr-watch/constants.d.ts +10 -0
  66. package/dist/pr-watch/constants.js +12 -1
  67. package/dist/pr-watch/main.js +16 -1
  68. package/dist/pr-watch/probe.js +13 -1
  69. package/dist/pr-watch/types.d.ts +3 -2
  70. package/dist/pr-watch/watch.js +14 -7
  71. package/dist/release/auto-hatch.d.ts +114 -0
  72. package/dist/release/auto-hatch.js +301 -0
  73. package/dist/release/coverage-debt-ledger.d.ts +22 -0
  74. package/dist/release/coverage-debt-ledger.js +157 -0
  75. package/dist/release/index.d.ts +3 -0
  76. package/dist/release/index.js +3 -0
  77. package/dist/release/pipeline.js +164 -12
  78. package/dist/release/suite-stamp.d.ts +44 -0
  79. package/dist/release/suite-stamp.js +133 -0
  80. package/dist/release/types.d.ts +19 -0
  81. package/dist/scope-provenance/digest.d.ts +67 -0
  82. package/dist/scope-provenance/digest.js +188 -0
  83. package/dist/scope-provenance/evaluate.d.ts +82 -0
  84. package/dist/scope-provenance/evaluate.js +528 -0
  85. package/dist/scope-provenance/index.d.ts +6 -0
  86. package/dist/scope-provenance/index.js +6 -0
  87. package/dist/session/compact-ritual.d.ts +96 -0
  88. package/dist/session/compact-ritual.js +237 -0
  89. package/dist/session/compact-ritual.spec.d.ts +2 -0
  90. package/dist/session/compact-ritual.spec.js +21 -0
  91. package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
  92. package/dist/session/coverage-check-resume-nudge.js +66 -0
  93. package/dist/session/index.d.ts +3 -0
  94. package/dist/session/index.js +3 -0
  95. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  96. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  97. package/dist/session/session-start.js +21 -0
  98. package/dist/test-boundary/evaluate.d.ts +54 -0
  99. package/dist/test-boundary/evaluate.js +368 -0
  100. package/dist/test-boundary/index.d.ts +6 -0
  101. package/dist/test-boundary/index.js +6 -0
  102. package/dist/test-boundary/policy.d.ts +52 -0
  103. package/dist/test-boundary/policy.js +182 -0
  104. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  105. package/dist/triage/bootstrap/gitignore.js +15 -1
  106. package/dist/triage/classify/label-mirror.d.ts +31 -1
  107. package/dist/triage/classify/label-mirror.js +78 -6
  108. package/dist/triage/help/registry-data.d.ts +6 -6
  109. package/dist/triage/help/registry-data.js +12 -3
  110. package/dist/vbrief-activate/activate.js +22 -6
  111. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  112. package/dist/vbrief-validate/plan-hooks.js +54 -0
  113. package/dist/xbrief/styles.js +33 -17
  114. package/package.json +7 -3
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Doctor check + --fix wire for OpenClaw soft AGENTS re-bind skill (#3171).
3
+ *
4
+ * Complements always-pins (#3001/#3008) and L2 product commands (#3064). This
5
+ * skill is the required durable soft post-amnesia surface for OpenClaw — not
6
+ * claimed via file-host PreCompact hooks alone.
7
+ */
8
+ import { existsSync, readFileSync, statSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { formatOpenClawSoftRebindSkillMarkdown, isManagedOpenClawSoftRebindSkill, OPENCLAW_SOFT_REBIND_SKILL_ID, } from "../session/compact-ritual.js";
12
+ import { assessOpenClawSoftRebindSkill, depositOpenClawSoftRebindSkill, } from "../session/openclaw-soft-rebind-deposit.js";
13
+ import { detectOpenClaw, listInScopeSkillsDirs } from "./openclaw-skills.js";
14
+ /** Stable doctor check id for JSON findings. */
15
+ export const OPENCLAW_SOFT_REBIND_CHECK = "openclaw-soft-agents-rebind";
16
+ const DOC_OPENCLAW_HOST = "docs/openclaw-agent-host.md";
17
+ const REMEDIATION_FIX = "deft doctor --fix";
18
+ function defaultIsDir(path) {
19
+ try {
20
+ return statSync(path).isDirectory();
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function assessGaps(skillsDirs) {
27
+ const expected = formatOpenClawSoftRebindSkillMarkdown();
28
+ const missing = [];
29
+ const stale = [];
30
+ let present = 0;
31
+ for (const skillsDir of skillsDirs) {
32
+ const path = join(skillsDir, OPENCLAW_SOFT_REBIND_SKILL_ID, "SKILL.md");
33
+ if (!existsSync(path)) {
34
+ missing.push(`${skillsDir}/${OPENCLAW_SOFT_REBIND_SKILL_ID}`);
35
+ continue;
36
+ }
37
+ let raw;
38
+ try {
39
+ raw = readFileSync(path, "utf8");
40
+ }
41
+ catch {
42
+ missing.push(`${skillsDir}/${OPENCLAW_SOFT_REBIND_SKILL_ID}`);
43
+ continue;
44
+ }
45
+ // Unmanaged content at the required slug is incomplete health, not pass
46
+ // (Greptile P1 #3171). Doctor --fix overwrites via deposit.
47
+ if (!isManagedOpenClawSoftRebindSkill(raw) || raw !== expected) {
48
+ stale.push(`${skillsDir}/${OPENCLAW_SOFT_REBIND_SKILL_ID}`);
49
+ continue;
50
+ }
51
+ present += 1;
52
+ }
53
+ return { missing, stale, present };
54
+ }
55
+ /**
56
+ * Doctor check: OpenClaw soft AGENTS re-bind skill present and current.
57
+ * No-ops when OpenClaw is not detected. Fix mode deposits managed skill.
58
+ */
59
+ export function runOpenClawSoftRebindCheck(sink, addFinding, options) {
60
+ const seams = options.seams ?? {};
61
+ const env = seams.openclawEnv ?? process.env;
62
+ const homeDirFn = seams.openclawHomeDir ?? (() => homedir());
63
+ const isDir = seams.isDir ?? defaultIsDir;
64
+ const detect = detectOpenClaw(env, { homeDir: homeDirFn(), isDir });
65
+ if (!detect.detected) {
66
+ sink.info(`${OPENCLAW_SOFT_REBIND_CHECK}: skip -- OpenClaw not detected`);
67
+ addFinding({
68
+ severity: "skip",
69
+ message: "OpenClaw not detected",
70
+ check: OPENCLAW_SOFT_REBIND_CHECK,
71
+ status: "skip",
72
+ reason: "openclaw-not-detected",
73
+ });
74
+ return null;
75
+ }
76
+ const skillsDirs = listInScopeSkillsDirs(detect.stateDir, options.allAgents, {
77
+ isDir,
78
+ });
79
+ const gaps = assessGaps(skillsDirs);
80
+ if (gaps.missing.length === 0 && gaps.stale.length === 0) {
81
+ const scope = options.allAgents ? "main + workspace-* seats" : "main workspace";
82
+ const message = `${OPENCLAW_SOFT_REBIND_CHECK}: soft AGENTS re-bind skill present ` +
83
+ `(${OPENCLAW_SOFT_REBIND_SKILL_ID}) in ${scope}`;
84
+ sink.success(message);
85
+ addFinding({
86
+ severity: "skip",
87
+ message,
88
+ check: OPENCLAW_SOFT_REBIND_CHECK,
89
+ status: "present",
90
+ skill: OPENCLAW_SOFT_REBIND_SKILL_ID,
91
+ skills_dirs: skillsDirs,
92
+ });
93
+ return null;
94
+ }
95
+ if (options.fixMode) {
96
+ const depositResult = depositOpenClawSoftRebindSkill({
97
+ env,
98
+ homeDir: homeDirFn(),
99
+ allAgents: options.allAgents,
100
+ isDir,
101
+ skillsDirs,
102
+ });
103
+ const after = assessGaps(skillsDirs);
104
+ if (after.missing.length === 0 && after.stale.length === 0) {
105
+ const message = `${OPENCLAW_SOFT_REBIND_CHECK}: deposited soft AGENTS re-bind skill; ` +
106
+ `restart OpenClaw gateway or start a new session (${DOC_OPENCLAW_HOST})`;
107
+ sink.success(message);
108
+ addFinding({
109
+ severity: "skip",
110
+ message,
111
+ check: OPENCLAW_SOFT_REBIND_CHECK,
112
+ status: "fixed",
113
+ written: depositResult.writtenPaths,
114
+ });
115
+ return depositResult;
116
+ }
117
+ // Partial fix: report remaining gaps from post-deposit assessment (SLizard P2).
118
+ const partsAfter = [];
119
+ if (after.missing.length > 0) {
120
+ partsAfter.push(`missing=${after.missing.join(", ")}`);
121
+ }
122
+ if (after.stale.length > 0) {
123
+ partsAfter.push(`stale=${after.stale.join(", ")}`);
124
+ }
125
+ const messageAfter = `${OPENCLAW_SOFT_REBIND_CHECK}: OpenClaw soft AGENTS re-bind skill incomplete after fix ` +
126
+ `(${partsAfter.join("; ")}). Remediation: ${REMEDIATION_FIX} — see ${DOC_OPENCLAW_HOST}`;
127
+ sink.warn(messageAfter);
128
+ addFinding({
129
+ severity: "warning",
130
+ message: messageAfter,
131
+ check: OPENCLAW_SOFT_REBIND_CHECK,
132
+ status: "incomplete",
133
+ missing: after.missing,
134
+ stale: after.stale,
135
+ written: depositResult.writtenPaths,
136
+ suggestion: REMEDIATION_FIX,
137
+ docs: [DOC_OPENCLAW_HOST],
138
+ });
139
+ return depositResult;
140
+ }
141
+ const parts = [];
142
+ if (gaps.missing.length > 0) {
143
+ parts.push(`missing=${gaps.missing.join(", ")}`);
144
+ }
145
+ if (gaps.stale.length > 0) {
146
+ parts.push(`stale=${gaps.stale.join(", ")}`);
147
+ }
148
+ const message = `${OPENCLAW_SOFT_REBIND_CHECK}: OpenClaw soft AGENTS re-bind skill incomplete ` +
149
+ `(${parts.join("; ")}). Remediation: ${REMEDIATION_FIX} — see ${DOC_OPENCLAW_HOST}`;
150
+ sink.warn(message);
151
+ addFinding({
152
+ severity: "warning",
153
+ message,
154
+ check: OPENCLAW_SOFT_REBIND_CHECK,
155
+ status: "incomplete",
156
+ missing: gaps.missing,
157
+ stale: gaps.stale,
158
+ suggestion: REMEDIATION_FIX,
159
+ docs: [DOC_OPENCLAW_HOST],
160
+ });
161
+ return null;
162
+ }
163
+ export { assessOpenClawSoftRebindSkill, depositOpenClawSoftRebindSkill };
164
+ //# sourceMappingURL=openclaw-soft-rebind.js.map
@@ -119,6 +119,11 @@ export declare const ASSIST_SCRATCH_ROOT_PREFIXES: readonly [".deft-scratch/", "
119
119
  * True when the write target is under an allowlisted disposable scratch root (#1802).
120
120
  * Fail closed on null/empty/unparseable targets and on path escape (`..`).
121
121
  * Does not authorize tracked product paths even under assist posture.
122
+ *
123
+ * #3186: after lexical allowlist match, realpath the scratch root and require
124
+ * {@link assertProjectionContained}; refuse symlink scratch roots and any
125
+ * realpath outside the project (host Write would follow the link).
126
+ * When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
122
127
  */
123
128
  export declare function isAllowlistedAssistScratchPath(projectRoot: string, targetPath: string | null): boolean;
124
129
  /**
@@ -140,7 +145,8 @@ export declare function decideHook(input: HookDispatchInput, seams?: HookPolicyS
140
145
  * Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
141
146
  * hook failure and blocks the tool — so Cursor allows must emit explicit
142
147
  * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
143
- * flow is unchanged.
148
+ * flow is unchanged — except session.start / session.compact soft re-bind
149
+ * injection (#3171), which surfaces checklist text without a write tool.
144
150
  *
145
151
  * Cursor stdout always includes `code` (stable machine-readable decision code)
146
152
  * so agents can distinguish policy denials from host-integration failures
@@ -1,12 +1,14 @@
1
- import { realpathSync } from "node:fs";
1
+ import { lstatSync, realpathSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import { appendAuthzAudit, classifyHookAuthzOps, evaluateAuthzMutation, evidenceSatisfiesImplementationApproval, listActiveHumanGrants, loadAuthzStateResult, markGrantUsed, shouldConsumeSingleUseGrant, utcIso, } from "../authz/index.js";
4
+ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix } from "../layout/resolve.js";
5
6
  import { detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
6
7
  import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
7
8
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
8
9
  import { classifyMcpTool, DEFAULT_RUNTIME_AUTHORITY_POLICY, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
9
10
  import { loadStoryWriteFenceFromPath, resolveWriteFence } from "../policy/write-fence.js";
11
+ import { appendSoftAgentsRebindToMessage, decisionCarriesSoftAgentsRebind, formatSoftAgentsRebindChecklist, } from "../session/compact-ritual.js";
10
12
  import { detectBranch } from "../session/git.js";
11
13
  import { emitSessionRitualBlockedProcessCost } from "../session/process-cost.js";
12
14
  import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
@@ -109,6 +111,11 @@ export const ASSIST_SCRATCH_ROOT_PREFIXES = [".deft-scratch/", "temp/"];
109
111
  * True when the write target is under an allowlisted disposable scratch root (#1802).
110
112
  * Fail closed on null/empty/unparseable targets and on path escape (`..`).
111
113
  * Does not authorize tracked product paths even under assist posture.
114
+ *
115
+ * #3186: after lexical allowlist match, realpath the scratch root and require
116
+ * {@link assertProjectionContained}; refuse symlink scratch roots and any
117
+ * realpath outside the project (host Write would follow the link).
118
+ * When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
112
119
  */
113
120
  export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
114
121
  if (targetPath === null || targetPath.trim().length === 0)
@@ -119,11 +126,55 @@ export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
119
126
  return false;
120
127
  if (isLexicalOutsideProjectRoot(posix))
121
128
  return false;
129
+ let matchedPrefix = null;
122
130
  for (const prefix of ASSIST_SCRATCH_ROOT_PREFIXES) {
123
- if (posix === prefix.slice(0, -1) || posix.startsWith(prefix))
124
- return true;
131
+ if (posix === prefix.slice(0, -1) || posix.startsWith(prefix)) {
132
+ matchedPrefix = prefix;
133
+ break;
134
+ }
125
135
  }
126
- return false;
136
+ if (matchedPrefix === null)
137
+ return false;
138
+ // #3186 containment: realpath scratch root + projection fence (symlink escape).
139
+ const projectAbs = resolve(projectRoot);
140
+ try {
141
+ realpathSync(projectAbs);
142
+ }
143
+ catch {
144
+ // Unit fixtures / missing project dir — lexical allowlist only.
145
+ return true;
146
+ }
147
+ const scratchRootName = matchedPrefix.slice(0, -1); // ".deft-scratch" | "temp"
148
+ const scratchRootAbs = resolve(projectAbs, scratchRootName);
149
+ try {
150
+ // Refuse when the scratch root (or any parent on the path) escapes via symlink.
151
+ assertProjectionContained(projectAbs, scratchRootAbs);
152
+ }
153
+ catch (err) {
154
+ if (err instanceof ProjectionContainmentError)
155
+ return false;
156
+ return false;
157
+ }
158
+ // Refuse symlink scratch roots entirely (#3186) — even in-tree links can divert Write.
159
+ try {
160
+ const st = lstatSync(scratchRootAbs);
161
+ if (st.isSymbolicLink())
162
+ return false;
163
+ }
164
+ catch {
165
+ // Scratch root does not exist yet — mkdir will create a real directory; allow.
166
+ }
167
+ // Also fence the concrete write target when it already exists on disk.
168
+ const targetAbs = resolve(projectAbs, targetPath.replace(/\\/g, "/"));
169
+ try {
170
+ assertProjectionContained(projectAbs, targetAbs);
171
+ }
172
+ catch (err) {
173
+ if (err instanceof ProjectionContainmentError)
174
+ return false;
175
+ return false;
176
+ }
177
+ return true;
127
178
  }
128
179
  /**
129
180
  * Assist/ephemeral classification for scratch-write carve-out (#1802).
@@ -716,7 +767,8 @@ export function decideHook(input, seams = {}) {
716
767
  host: input.host,
717
768
  toolName: null,
718
769
  projectRoot,
719
- message: result.message,
770
+ // Soft checklist still surfaces after compact even when ritual was absent (#3171).
771
+ message: appendSoftAgentsRebindToMessage(result.message),
720
772
  scopePath: null,
721
773
  };
722
774
  }
@@ -727,7 +779,7 @@ export function decideHook(input, seams = {}) {
727
779
  host: input.host,
728
780
  toolName: null,
729
781
  projectRoot,
730
- message: result.message,
782
+ message: appendSoftAgentsRebindToMessage(result.message),
731
783
  scopePath: null,
732
784
  };
733
785
  }
@@ -739,8 +791,8 @@ export function decideHook(input, seams = {}) {
739
791
  host: input.host,
740
792
  toolName: null,
741
793
  projectRoot,
742
- message: "Directive compact re-arm bookkeeping failed on its non-blocking path: " +
743
- `${String(cause)}`,
794
+ message: appendSoftAgentsRebindToMessage("Directive compact re-arm bookkeeping failed on its non-blocking path: " +
795
+ `${String(cause)}`),
744
796
  scopePath: null,
745
797
  };
746
798
  }
@@ -775,8 +827,9 @@ export function decideHook(input, seams = {}) {
775
827
  host: input.host,
776
828
  toolName: null,
777
829
  projectRoot,
778
- message: `Directive SessionStart bookkeeping reported exit ${result.code} on its non-blocking path` +
779
- `${detail.length > 0 ? `: ${detail}` : "."}`,
830
+ // Best-effort soft cue on SessionStart (Codex gap + resume/amnesia) (#3171).
831
+ message: appendSoftAgentsRebindToMessage(`Directive SessionStart bookkeeping reported exit ${result.code} on its non-blocking path` +
832
+ `${detail.length > 0 ? `: ${detail}` : "."}`),
780
833
  scopePath: null,
781
834
  };
782
835
  }
@@ -789,7 +842,7 @@ export function decideHook(input, seams = {}) {
789
842
  host: input.host,
790
843
  toolName: null,
791
844
  projectRoot,
792
- message: `Directive SessionStart bookkeeping failed on its non-blocking path: ${String(cause)}`,
845
+ message: appendSoftAgentsRebindToMessage(`Directive SessionStart bookkeeping failed on its non-blocking path: ${String(cause)}`),
793
846
  scopePath: null,
794
847
  };
795
848
  }
@@ -800,7 +853,8 @@ export function decideHook(input, seams = {}) {
800
853
  host: input.host,
801
854
  toolName: null,
802
855
  projectRoot,
803
- message: "SessionStart bookkeeping completed on a non-blocking path.",
856
+ // Soft AGENTS re-bind on SessionStart without requiring a write tool (#3171 / #2769).
857
+ message: appendSoftAgentsRebindToMessage("SessionStart bookkeeping completed on a non-blocking path."),
804
858
  scopePath: null,
805
859
  };
806
860
  }
@@ -881,13 +935,27 @@ export function decideHook(input, seams = {}) {
881
935
  }
882
936
  return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: true });
883
937
  }
938
+ /**
939
+ * Soft AGENTS re-bind text for host injection when decision carries soft path (#3171).
940
+ * Prefer the checklist portion of the decision message; fall back to SoT formatter.
941
+ */
942
+ function softAgentsRebindWireText(decision) {
943
+ if (!decisionCarriesSoftAgentsRebind({ event: decision.event, code: decision.code })) {
944
+ return null;
945
+ }
946
+ if (decision.message.includes("Soft AGENTS re-bind checklist:")) {
947
+ return decision.message;
948
+ }
949
+ return formatSoftAgentsRebindChecklist();
950
+ }
884
951
  /**
885
952
  * Render host-facing hook output.
886
953
  *
887
954
  * Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
888
955
  * hook failure and blocks the tool — so Cursor allows must emit explicit
889
956
  * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
890
- * flow is unchanged.
957
+ * flow is unchanged — except session.start / session.compact soft re-bind
958
+ * injection (#3171), which surfaces checklist text without a write tool.
891
959
  *
892
960
  * Cursor stdout always includes `code` (stable machine-readable decision code)
893
961
  * so agents can distinguish policy denials from host-integration failures
@@ -896,9 +964,45 @@ export function decideHook(input, seams = {}) {
896
964
  */
897
965
  export function renderHostDecision(host, decision) {
898
966
  if (decision.verdict === "allow") {
967
+ const soft = softAgentsRebindWireText(decision);
899
968
  if (host === "cursor") {
969
+ if (decision.event === "session.start" && soft !== null) {
970
+ // Cursor sessionStart injects additional_context into the conversation.
971
+ return JSON.stringify({
972
+ permission: "allow",
973
+ code: decision.code,
974
+ additional_context: soft,
975
+ });
976
+ }
977
+ if (decision.event === "session.compact" && soft !== null) {
978
+ // Cursor preCompact is observational; user_message surfaces the soft cue.
979
+ // Hard re-arm still ran via bookkeeping; agent path also gets SessionStart on resume.
980
+ return JSON.stringify({
981
+ user_message: soft,
982
+ code: decision.code,
983
+ });
984
+ }
900
985
  return JSON.stringify({ permission: "allow", code: decision.code });
901
986
  }
987
+ if (soft !== null &&
988
+ (decision.event === "session.start" || decision.event === "session.compact")) {
989
+ if (host === "claude" || host === "codex") {
990
+ const hookEventName = decision.event === "session.start" ? "SessionStart" : "PostCompact";
991
+ return JSON.stringify({
992
+ hookSpecificOutput: {
993
+ hookEventName,
994
+ additionalContext: soft,
995
+ },
996
+ });
997
+ }
998
+ if (host === "grok") {
999
+ return JSON.stringify({
1000
+ decision: "allow",
1001
+ reason: soft,
1002
+ additional_context: soft,
1003
+ });
1004
+ }
1005
+ }
902
1006
  return "";
903
1007
  }
904
1008
  switch (host) {
package/dist/index.d.ts CHANGED
@@ -14,7 +14,9 @@ export * as branch from "./branch/index.js";
14
14
  export * as cache from "./cache/index.js";
15
15
  export * as capacity from "./capacity/index.js";
16
16
  export * as codebase from "./codebase/index.js";
17
+ export * as consumerCheckContract from "./consumer-check-contract/index.js";
17
18
  export * from "./coverage-hotspots/index.js";
19
+ export * as deliveryAttempt from "./delivery-attempt/index.js";
18
20
  export * as doctor from "./doctor/index.js";
19
21
  export * from "./encoding/index.js";
20
22
  export * as escalation from "./escalation/index.js";
@@ -52,11 +54,13 @@ export * as render from "./render/index.js";
52
54
  export * as resolution from "./resolution/index.js";
53
55
  export * as scm from "./scm/index.js";
54
56
  export * as scope from "./scope/index.js";
57
+ export * as scopeProvenance from "./scope-provenance/index.js";
55
58
  export * as session from "./session/index.js";
56
59
  export * as slash from "./slash/index.js";
57
60
  export * as slice from "./slice/index.js";
58
61
  export * as storyReady from "./story-ready/index.js";
59
62
  export * as swarm from "./swarm/index.js";
63
+ export * as testBoundary from "./test-boundary/index.js";
60
64
  export * as toolEvents from "./tool-events/index.js";
61
65
  export * as triage from "./triage/index.js";
62
66
  export * as userConfig from "./user-config/index.js";
package/dist/index.js CHANGED
@@ -14,7 +14,9 @@ export * as branch from "./branch/index.js";
14
14
  export * as cache from "./cache/index.js";
15
15
  export * as capacity from "./capacity/index.js";
16
16
  export * as codebase from "./codebase/index.js";
17
+ export * as consumerCheckContract from "./consumer-check-contract/index.js";
17
18
  export * from "./coverage-hotspots/index.js";
19
+ export * as deliveryAttempt from "./delivery-attempt/index.js";
18
20
  export * as doctor from "./doctor/index.js";
19
21
  export * from "./encoding/index.js";
20
22
  export * as escalation from "./escalation/index.js";
@@ -53,11 +55,13 @@ export * as render from "./render/index.js";
53
55
  export * as resolution from "./resolution/index.js";
54
56
  export * as scm from "./scm/index.js";
55
57
  export * as scope from "./scope/index.js";
58
+ export * as scopeProvenance from "./scope-provenance/index.js";
56
59
  export * as session from "./session/index.js";
57
60
  export * as slash from "./slash/index.js";
58
61
  export * as slice from "./slice/index.js";
59
62
  export * as storyReady from "./story-ready/index.js";
60
63
  export * as swarm from "./swarm/index.js";
64
+ export * as testBoundary from "./test-boundary/index.js";
61
65
  export * as toolEvents from "./tool-events/index.js";
62
66
  export * as triage from "./triage/index.js";
63
67
  export * as userConfig from "./user-config/index.js";
@@ -37,6 +37,11 @@ export const CANONICAL_GITIGNORE_BASELINE = [
37
37
  "vbrief/.triage-cache/scope-lifecycle.jsonl",
38
38
  "vbrief/.triage-cache/decompositions/",
39
39
  "vbrief/.triage-cache/doctor-state.json",
40
+ // Per-clone session state (#3146): throttle / release-availability JSON
41
+ // written under the lifecycle .triage-cache; selective ignore only (hybrid
42
+ // #1144 — never blanket-ignore the whole .triage-cache directory).
43
+ "vbrief/.triage-cache/staleness-tickler-state.json",
44
+ "vbrief/.triage-cache/release-availability-state.json",
40
45
  // Symmetric `xbrief/` layout entries (#2348). On the migrated `xbrief/` tree
41
46
  // the engine writes operator-private triage-cache files to
42
47
  // `xbrief/.triage-cache/`; without these the paths are trackable, violating
@@ -47,6 +52,8 @@ export const CANONICAL_GITIGNORE_BASELINE = [
47
52
  "xbrief/.triage-cache/scope-lifecycle.jsonl",
48
53
  "xbrief/.triage-cache/decompositions/",
49
54
  "xbrief/.triage-cache/doctor-state.json",
55
+ "xbrief/.triage-cache/staleness-tickler-state.json",
56
+ "xbrief/.triage-cache/release-availability-state.json",
50
57
  "vbrief/*.lock",
51
58
  ".deft/core.bak-*/",
52
59
  ".deft/*.bak-*",
@@ -8,7 +8,7 @@
8
8
  * and scope briefs are never installer-managed; if they reappear in
9
9
  * `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
10
10
  *
11
- * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
11
+ * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117, #3193.
12
12
  */
13
13
  import { type InitDepositIo } from "./constants.js";
14
14
  export declare const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
@@ -51,13 +51,82 @@ export interface MixedCoreAndAppClassification {
51
51
  readonly app: string[];
52
52
  /** True when both core and app are non-empty — the deposited guard fails. */
53
53
  readonly wouldFail: boolean;
54
+ /**
55
+ * Pin/lock paths that were path-allowlisted but failed content-aware checks
56
+ * when co-travelling with core (#3193). Empty when path-only classification.
57
+ */
58
+ readonly pinContentRejected?: string[];
59
+ }
60
+ /** Paths that path-allowlist for upgrade co-travel but need content checks with core (#3193). */
61
+ export declare const UPGRADE_PIN_CONTENT_PATHS: readonly ["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
62
+ export type UpgradePinContentPath = (typeof UPGRADE_PIN_CONTENT_PATHS)[number];
63
+ /** package.json dependency map fields that may hold @deftai/directive* pins. */
64
+ export declare const PACKAGE_JSON_DEP_FIELDS: readonly ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
65
+ /**
66
+ * True when `name` is an `@deftai/directive*` dependency **key** (#3193).
67
+ * Substring hits in scripts/settings values do not qualify.
68
+ */
69
+ export declare function isDirectiveDependencyKey(name: string): boolean;
70
+ /**
71
+ * package.json co-travel is allowed only when the sole differences are
72
+ * `@deftai/directive*` dependency-key pins under the standard dep maps (#3193).
73
+ * Scripts/settings/metadata that merely contain the substring still fail.
74
+ */
75
+ export declare function isPackageJsonDirectivePinOnlyDiff(baseRaw: string, headRaw: string): boolean;
76
+ /**
77
+ * package-lock.json follow-through: non-@deftai/directive* root direct dependency
78
+ * identities (and their `node_modules/<name>` package entries when present) must
79
+ * be unchanged. Directive pin identity + transitive/resolution churn may change.
80
+ */
81
+ export declare function isPackageLockDirectivePinFollowThrough(baseRaw: string, headRaw: string): boolean;
82
+ /**
83
+ * Minimal pnpm-lock.yaml (v6/v9) root importer (`.`) direct-dep extractor.
84
+ * Avoids a YAML dependency; sufficient for pin follow-through identity checks.
85
+ */
86
+ export declare function pnpmLockRootDirectDeps(raw: string): Record<string, string>;
87
+ /**
88
+ * Extract pnpm `packages:` section records keyed by the package name (not
89
+ * name@version). Used to freeze product package resolution blocks.
90
+ */
91
+ export declare function pnpmPackagesByName(raw: string): Map<string, string>;
92
+ export declare function isPnpmLockDirectivePinFollowThrough(baseRaw: string, headRaw: string): boolean;
93
+ /**
94
+ * yarn.lock (v1) package identity blocks: every non-@deftai/directive* package
95
+ * must keep identical block text (or fail if added/removed). Directive package
96
+ * blocks may change freely as pin follow-through (#3193).
97
+ */
98
+ export declare function isYarnLockDirectivePinFollowThrough(baseRaw: string, headRaw: string): boolean;
99
+ /**
100
+ * Content-aware check for a single upgrade pin path (#3193).
101
+ * Returns true when the path may co-travel with `.deft/core/**`.
102
+ */
103
+ export declare function isUpgradePinPathContentAllowed(path: string, baseRaw: string, headRaw: string): boolean;
104
+ export interface PinContentFilePair {
105
+ readonly base: string;
106
+ readonly head: string;
54
107
  }
55
108
  /**
56
109
  * TS twin of Go `classifyChangedPaths` / deposited shell guard (#1430).
57
110
  * Core = `.deft/core/**`; installer-managed = allowlist; app = everything else.
58
111
  * Guard fails iff both core and app are non-empty.
112
+ *
113
+ * Path-only: does not inspect package/lock contents. Prefer
114
+ * {@link classifyMixedCoreAndAppContentAware} when base/head blobs are available
115
+ * (deposited guard + unit tests for #3193).
59
116
  */
60
117
  export declare function classifyMixedCoreAndApp(changedPaths: readonly string[], matchers?: readonly InstallerManagedMatcher[]): MixedCoreAndAppClassification;
118
+ /**
119
+ * Content-aware upgrade co-travel classifier (#3193).
120
+ *
121
+ * Starts from path classification (#3127 allowlist), then when `.deft/core/**`
122
+ * is present reclassifies package.json / lockfile paths as **app** unless their
123
+ * base→head content is the Directive pin unit (or lock follow-through).
124
+ * `.deft/GENERATION.json` remains path-allowlisted with no content constraint.
125
+ *
126
+ * Missing content for a pin path co-travelling with core fails closed (treated
127
+ * as app) so partial fixtures cannot silently re-open the path-only hole.
128
+ */
129
+ export declare function classifyMixedCoreAndAppContentAware(changedPaths: readonly string[], fileContents: Readonly<Partial<Record<string, PinContentFilePair>>>, matchers?: readonly InstallerManagedMatcher[]): MixedCoreAndAppClassification;
61
130
  export interface FrameworkStagePathsOptions {
62
131
  /**
63
132
  * Include the vendored `.deft/core` payload in the stage set. Defaults to