@deftai/directive-core 0.91.0 → 0.93.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 (92) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/index.d.ts +1 -0
  6. package/dist/doctor/index.js +1 -0
  7. package/dist/doctor/main.js +49 -0
  8. package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
  9. package/dist/doctor/openclaw-l2-adapter.js +199 -0
  10. package/dist/hooks/dispatcher.d.ts +6 -1
  11. package/dist/hooks/dispatcher.js +30 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +2 -0
  14. package/dist/init-deposit/gitignore.js +3 -0
  15. package/dist/init-deposit/hygiene.d.ts +43 -1
  16. package/dist/init-deposit/hygiene.js +134 -10
  17. package/dist/init-deposit/index.d.ts +3 -0
  18. package/dist/init-deposit/index.js +3 -0
  19. package/dist/init-deposit/init-deposit.js +10 -0
  20. package/dist/init-deposit/refresh.js +11 -0
  21. package/dist/init-deposit/scaffold.js +6 -134
  22. package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
  23. package/dist/init-deposit/skill-discovery-deposit.js +287 -0
  24. package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
  25. package/dist/init-deposit/skill-discovery-hosts.js +217 -0
  26. package/dist/init-deposit/slash-deposit.d.ts +46 -0
  27. package/dist/init-deposit/slash-deposit.js +174 -0
  28. package/dist/orchestration/probe-session.d.ts +5 -1
  29. package/dist/orchestration/probe-session.js +24 -13
  30. package/dist/platform/agents-md.js +1 -1
  31. package/dist/policy/deft-directive-disable.d.ts +86 -0
  32. package/dist/policy/deft-directive-disable.js +167 -0
  33. package/dist/policy/delivery-branch.d.ts +33 -0
  34. package/dist/policy/delivery-branch.js +124 -0
  35. package/dist/policy/host-slash-commands.d.ts +28 -0
  36. package/dist/policy/host-slash-commands.js +103 -0
  37. package/dist/policy/index.d.ts +3 -0
  38. package/dist/policy/index.js +61 -5
  39. package/dist/scope/brief-io.d.ts +3 -1
  40. package/dist/scope/brief-io.js +5 -3
  41. package/dist/scope/delivery-evidence.d.ts +112 -0
  42. package/dist/scope/delivery-evidence.js +419 -0
  43. package/dist/scope/index.d.ts +1 -0
  44. package/dist/scope/index.js +1 -0
  45. package/dist/scope/main.d.ts +5 -0
  46. package/dist/scope/main.js +98 -3
  47. package/dist/scope/registry-artifact-sync.js +4 -1
  48. package/dist/scope/transition.d.ts +11 -1
  49. package/dist/scope/transition.js +30 -4
  50. package/dist/session/ritual-sentinel.js +21 -12
  51. package/dist/session/session-start-hook.d.ts +3 -0
  52. package/dist/session/session-start-hook.js +21 -0
  53. package/dist/session/session-start.js +31 -0
  54. package/dist/slash/emitters.d.ts +102 -0
  55. package/dist/slash/emitters.js +148 -0
  56. package/dist/slash/generator.d.ts +98 -0
  57. package/dist/slash/generator.js +145 -0
  58. package/dist/slash/index.d.ts +16 -0
  59. package/dist/slash/index.js +16 -0
  60. package/dist/slash/openclaw-adapter.d.ts +64 -0
  61. package/dist/slash/openclaw-adapter.js +198 -0
  62. package/dist/slash/openclaw-deposit.d.ts +73 -0
  63. package/dist/slash/openclaw-deposit.js +279 -0
  64. package/dist/slash/openclaw-slugs.d.ts +52 -0
  65. package/dist/slash/openclaw-slugs.js +126 -0
  66. package/dist/slash/product-set.d.ts +50 -0
  67. package/dist/slash/product-set.js +142 -0
  68. package/dist/swarm/complete-cohort.d.ts +20 -1
  69. package/dist/swarm/complete-cohort.js +56 -9
  70. package/dist/swarm/finalize-cohort-cli.js +9 -0
  71. package/dist/swarm/finalize-cohort.d.ts +8 -0
  72. package/dist/swarm/finalize-cohort.js +217 -21
  73. package/dist/user-config/experimental-rules.d.ts +43 -0
  74. package/dist/user-config/experimental-rules.js +162 -0
  75. package/dist/user-config/index.d.ts +1 -0
  76. package/dist/user-config/index.js +1 -0
  77. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  78. package/dist/vbrief-validate/plan-hooks.js +50 -0
  79. package/dist/verify-source/contained-writes.js +1 -1
  80. package/dist/xbrief/create.d.ts +36 -0
  81. package/dist/xbrief/create.js +285 -0
  82. package/dist/xbrief/index.d.ts +14 -0
  83. package/dist/xbrief/index.js +42 -0
  84. package/dist/xbrief/paths.d.ts +37 -0
  85. package/dist/xbrief/paths.js +123 -0
  86. package/dist/xbrief/styles.d.ts +36 -0
  87. package/dist/xbrief/styles.js +235 -0
  88. package/dist/xbrief/types.d.ts +50 -0
  89. package/dist/xbrief/types.js +17 -0
  90. package/dist/xbrief/verify.d.ts +30 -0
  91. package/dist/xbrief/verify.js +251 -0
  92. package/package.json +11 -3
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Multi-host init/update deposit for native slash command files (#3054 / epic #55).
3
+ *
4
+ * Wires #3053 emitters into the init/update deposit path:
5
+ * - Default enabled set = hosts with real emitters (L6)
6
+ * - Per-host opt-out via `plan.policy.hostSlashCommands`
7
+ * - Idempotent managed rewrite (no duplicate pile-up)
8
+ * - Prefer commit of **managed product paths only** (L8) — exact allowlist, not whole dirs
9
+ *
10
+ * Parallel to agent-hooks deposit; does not touch hook JSON configs.
11
+ *
12
+ * Ownership: only create/update/remove files that are missing or still look like
13
+ * Directive thin wrappers (`isThinWrapperMarkdown`). Consumer-customized content
14
+ * at a product filename is left untouched.
15
+ */
16
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
17
+ import { basename, dirname, join, resolve } from "node:path";
18
+ import { assertDepositContained } from "../deposit/contain.js";
19
+ import { containedWrite } from "../fs/contained-write.js";
20
+ import { isHostSlashCommandDepositEnabled, loadHostSlashCommandsPolicyFromProject, } from "../policy/host-slash-commands.js";
21
+ import { emitHostCommandFiles, getHostCommandLayout, isThinWrapperMarkdown, listSlashEmitterHosts, } from "../slash/index.js";
22
+ import { listProductCommands, logicalIdToFilename } from "../slash/product-set.js";
23
+ /**
24
+ * Write thin-wrapper command/prompt files for every policy-enabled host.
25
+ *
26
+ * Idempotent: skips files whose on-disk bytes already match emission.
27
+ * Ownership-safe: never overwrites non-thin consumer customizations at product paths.
28
+ * Opt-out: removes managed thin wrappers only (leaves user customizations alone).
29
+ */
30
+ export function writeSlashCommandDeposit(projectRoot, io = { printf: () => undefined }, policy = loadHostSlashCommandsPolicyFromProject(projectRoot)) {
31
+ const rootAbs = resolve(projectRoot);
32
+ const writtenPaths = [];
33
+ const removedPaths = [];
34
+ const preservedCustomPaths = [];
35
+ const depositedHosts = [];
36
+ const skippedHosts = [];
37
+ for (const hostId of listSlashEmitterHosts()) {
38
+ if (!isHostSlashCommandDepositEnabled(hostId, policy)) {
39
+ skippedHosts.push(hostId);
40
+ removedPaths.push(...stripManagedHostCommandFiles(rootAbs, hostId));
41
+ continue;
42
+ }
43
+ depositedHosts.push(hostId);
44
+ const files = emitHostCommandFiles(hostId);
45
+ for (const file of files) {
46
+ const outcome = writeHostCommandFileIfChanged(rootAbs, file);
47
+ if (outcome === "written")
48
+ writtenPaths.push(file.relativePath);
49
+ if (outcome === "preserved")
50
+ preservedCustomPaths.push(file.relativePath);
51
+ }
52
+ }
53
+ if (writtenPaths.length > 0) {
54
+ const hostSummary = depositedHosts.join(", ");
55
+ io.printf(`Installed Directive slash commands for hosts [${hostSummary}]: ${writtenPaths.length} file(s)\n`);
56
+ }
57
+ if (preservedCustomPaths.length > 0) {
58
+ io.printf(`Preserved non-managed slash command customizations: ${preservedCustomPaths.join(", ")}\n`);
59
+ }
60
+ if (removedPaths.length > 0) {
61
+ io.printf(`Removed Directive-managed slash commands (plan.policy.hostSlashCommands opt-out): ${removedPaths.join(", ")}\n`);
62
+ }
63
+ if (writtenPaths.length === 0 && removedPaths.length === 0) {
64
+ if (depositedHosts.length === 0) {
65
+ io.printf("Directive slash commands: all hosts opted out via plan.policy.hostSlashCommands.\n");
66
+ }
67
+ else {
68
+ io.printf("Directive slash commands already current.\n");
69
+ }
70
+ }
71
+ return {
72
+ changed: writtenPaths.length + removedPaths.length > 0,
73
+ writtenPaths,
74
+ removedPaths,
75
+ preservedCustomPaths,
76
+ depositedHosts,
77
+ skippedHosts,
78
+ };
79
+ }
80
+ /**
81
+ * Exact repo-relative product command paths (all emitter hosts).
82
+ * Used by installerManagedMatchers for L8 prefer-commit staging without
83
+ * claiming whole host command directories (consumer custom files stay app-owned).
84
+ */
85
+ export function slashCommandManagedExactPaths() {
86
+ const paths = [];
87
+ for (const hostId of listSlashEmitterHosts()) {
88
+ const dir = getHostCommandLayout(hostId).relativeDir;
89
+ for (const cmd of listProductCommands()) {
90
+ paths.push(`${dir}/${logicalIdToFilename(cmd.logicalId)}`);
91
+ }
92
+ }
93
+ return paths;
94
+ }
95
+ function isManagedThinContent(raw, file) {
96
+ return raw === file.contents || isThinWrapperMarkdown(raw, file.dispatchPath);
97
+ }
98
+ function writeHostCommandFileIfChanged(projectRoot, file) {
99
+ const absolute = join(projectRoot, file.relativePath);
100
+ assertDepositContained(projectRoot, absolute);
101
+ if (existsSync(absolute)) {
102
+ let raw;
103
+ try {
104
+ raw = readFileSync(absolute, "utf8");
105
+ }
106
+ catch {
107
+ // Unreadable existing file: do not clobber consumer content.
108
+ return "preserved";
109
+ }
110
+ if (raw === file.contents)
111
+ return "unchanged";
112
+ // Ownership gate: only rewrite managed thin wrappers.
113
+ if (!isManagedThinContent(raw, file)) {
114
+ return "preserved";
115
+ }
116
+ }
117
+ const parent = dirname(absolute);
118
+ mkdirSync(parent, { recursive: true });
119
+ // Atomic replace via temp under project root (#2951).
120
+ const tmpName = `${basename(absolute)}.deft-${process.pid}.tmp`;
121
+ const temporary = join(parent, tmpName);
122
+ try {
123
+ containedWrite({
124
+ root: projectRoot,
125
+ target: temporary,
126
+ data: file.contents,
127
+ mode: "replace",
128
+ });
129
+ renameSync(temporary, absolute);
130
+ }
131
+ catch (err) {
132
+ try {
133
+ rmSync(temporary, { force: true });
134
+ }
135
+ catch {
136
+ /* best-effort cleanup */
137
+ }
138
+ throw err;
139
+ }
140
+ return "written";
141
+ }
142
+ /**
143
+ * On opt-out, remove only product-set managed thin wrappers for that host.
144
+ * User-authored or non-thin files under the host command dir are left untouched.
145
+ */
146
+ function stripManagedHostCommandFiles(projectRoot, hostId) {
147
+ const removed = [];
148
+ const files = emitHostCommandFiles(hostId);
149
+ for (const file of files) {
150
+ const absolute = join(projectRoot, file.relativePath);
151
+ assertDepositContained(projectRoot, absolute);
152
+ if (!existsSync(absolute))
153
+ continue;
154
+ let raw;
155
+ try {
156
+ raw = readFileSync(absolute, "utf8");
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ if (!isManagedThinContent(raw, file)) {
162
+ continue;
163
+ }
164
+ try {
165
+ rmSync(absolute, { force: true });
166
+ removed.push(file.relativePath);
167
+ }
168
+ catch {
169
+ /* best-effort */
170
+ }
171
+ }
172
+ return removed;
173
+ }
174
+ //# sourceMappingURL=slash-deposit.js.map
@@ -36,7 +36,11 @@ export declare function detectGitBranch(projectRoot: string, execGit?: (args: st
36
36
  }): string;
37
37
  /** Read probe session from project root. */
38
38
  export declare function readSession(projectRoot: string): ProbeSession | null;
39
- /** Atomically persist session to .deft/probe-session.json. */
39
+ /**
40
+ * Atomically persist session to .deft/probe-session.json.
41
+ * #3042: contain against projectRoot; refuse escaping `.deft` symlink parents
42
+ * (no bare open/write/rename).
43
+ */
40
44
  export declare function writeSession(projectRoot: string, session: ProbeSession): string;
41
45
  export declare function startSession(projectRoot: string, options: {
42
46
  target: string;
@@ -3,8 +3,10 @@
3
3
  */
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
- import { closeSync, existsSync, fdatasyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync, } from "node:fs";
6
+ import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
7
7
  import { join, resolve } from "node:path";
8
+ import { containedWrite } from "../fs/contained-write.js";
9
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
8
10
  export const SCHEMA_VERSION = 1;
9
11
  export const SESSION_RELPATH = [".deft", "probe-session.json"];
10
12
  export const STATE_INTERROGATE = "interrogate";
@@ -188,11 +190,16 @@ export function readSession(projectRoot) {
188
190
  completed_at: completedAt,
189
191
  };
190
192
  }
191
- /** Atomically persist session to .deft/probe-session.json. */
193
+ /**
194
+ * Atomically persist session to .deft/probe-session.json.
195
+ * #3042: contain against projectRoot; refuse escaping `.deft` symlink parents
196
+ * (no bare open/write/rename).
197
+ */
192
198
  export function writeSession(projectRoot, session) {
193
- const sessionFile = sessionPath(projectRoot);
194
- mkdirSync(join(projectRoot, ".deft"), { recursive: true });
195
- const tmpName = join(projectRoot, ".deft", `.probe-session.${randomBytes(8).toString("hex")}.json.tmp`);
199
+ const root = resolve(projectRoot);
200
+ const sessionFile = sessionPath(root);
201
+ assertWriteTargetSafe(root, sessionFile);
202
+ const tmpName = join(root, ".deft", `.probe-session.${randomBytes(8).toString("hex")}.json.tmp`);
196
203
  const sortedPayload = sessionToDict(session);
197
204
  const sortedKeys = Object.keys(sortedPayload).sort();
198
205
  const sortedObj = {};
@@ -200,20 +207,24 @@ export function writeSession(projectRoot, session) {
200
207
  sortedObj[k] = sortedPayload[k];
201
208
  }
202
209
  const finalContent = `${JSON.stringify(sortedObj, null, 2)}\n`;
203
- const fd = openSync(tmpName, "w");
204
210
  try {
205
- writeSync(fd, finalContent, undefined, "utf8");
211
+ containedWrite({
212
+ root,
213
+ target: tmpName,
214
+ data: finalContent,
215
+ mode: "create",
216
+ });
217
+ renameSync(tmpName, sessionFile);
218
+ }
219
+ catch (err) {
206
220
  try {
207
- fdatasyncSync(fd);
221
+ rmSync(tmpName, { force: true });
208
222
  }
209
223
  catch {
210
- // best effort
224
+ /* best-effort cleanup */
211
225
  }
226
+ throw err;
212
227
  }
213
- finally {
214
- closeSync(fd);
215
- }
216
- renameSync(tmpName, sessionFile);
217
228
  return sessionFile;
218
229
  }
219
230
  export function startSession(projectRoot, options) {
@@ -324,7 +324,7 @@ export function applyAgentsRefresh(projectRoot, options = {}, seams = {}, lockDe
324
324
  const writable = AGENTS_REFRESH_WRITABLE_STATES.has(state) && typeof newContent === "string";
325
325
  return { state, path, wrote: false, writable };
326
326
  }
327
- atomicWriteText(path, newContent);
327
+ atomicWriteText(path, newContent, { projectRoot });
328
328
  return { state, path, wrote: true, writable: true };
329
329
  }, lockDeps);
330
330
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Temporary test/local kill-switch for Directive enforcement (#3039).
3
+ *
4
+ * Presence of root `.deft-directive-disable` means Directive **enforcement** is
5
+ * OFF (hooks, session ritual, automation) while the deposit may remain — **only
6
+ * when the file is not tracked by git**. A committed flag is misconfig: doctor
7
+ * warns and enforcement stays ON (repository-controlled content must not disable
8
+ * hooks for downstream clones).
9
+ *
10
+ * Distinct from `.no-deft-directive` (#2926 permanent opt-out): flag+deposit is
11
+ * **not** inconsistent here.
12
+ *
13
+ * Product choices (v1):
14
+ * - Flag is **root-only** (workspace root the tool opened).
15
+ * - Flag **must be gitignored** (canonical baseline); committed flag → doctor
16
+ * warns and does **not** short-circuit enforcement.
17
+ * - Deposit presence is **OK**.
18
+ * - Full re-enable requires: delete the file **and** start a **new** agent session.
19
+ * - Precedence: active kill-switch first, then `.no-deft-directive`, else normal.
20
+ */
21
+ /** Canonical root-only filename (lowercase). Presence = candidate flag. */
22
+ export declare const DEFT_DIRECTIVE_DISABLE_FLAG_NAME = ".deft-directive-disable";
23
+ /** Gitignore entry the deposit must ensure (same as flag name). */
24
+ export declare const DEFT_DIRECTIVE_DISABLE_GITIGNORE_LINE = ".deft-directive-disable";
25
+ /**
26
+ * Canonical recovery message when Directive is disabled by the test kill-switch.
27
+ * Surfaces (doctor, agent, CLI, hooks) share this wording.
28
+ */
29
+ export declare const DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE: string;
30
+ /** One-line summary for log lines and short CLI output. */
31
+ export declare const DEFT_DIRECTIVE_DISABLE_ONE_LINE = "Directive disabled via `.deft-directive-disable` (test/local kill-switch; deposit OK)";
32
+ /** Doctor/status label when the kill-switch is active (local / untracked). */
33
+ export declare const DEFT_DIRECTIVE_DISABLE_STATUS: "disabled-test-kill-switch";
34
+ /**
35
+ * Warning when the kill-switch file is tracked by git (should be gitignored).
36
+ * Tracked flags do **not** disable enforcement.
37
+ */
38
+ export declare const DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING = "Misconfig: `.deft-directive-disable` is tracked by git. The flag must be gitignored (local kill-switch only). Untrack it and ensure `.gitignore` covers the path. Enforcement is NOT disabled while the flag is tracked.";
39
+ export interface DeftDirectiveDisableSeams {
40
+ readonly isFile?: (path: string) => boolean;
41
+ readonly isDir?: (path: string) => boolean;
42
+ /** Return true when `git ls-files` lists the flag (tracked). */
43
+ readonly isGitTracked?: (projectRoot: string, relPath: string) => boolean;
44
+ /** Skip tracked probe + cache (tests / doctor full re-check). */
45
+ readonly skipTrackedCache?: boolean;
46
+ }
47
+ export interface DeftDirectiveDisableState {
48
+ readonly present: boolean;
49
+ readonly flagPath: string;
50
+ readonly depositPresent: boolean;
51
+ /**
52
+ * True when the flag file is tracked by git (misconfig — should be gitignored).
53
+ * Always false when the flag is absent or when the probe cannot run.
54
+ */
55
+ readonly trackedByGit: boolean;
56
+ /**
57
+ * True when the kill-switch is **active** for enforcement short-circuit:
58
+ * present and **not** tracked by git.
59
+ */
60
+ readonly active: boolean;
61
+ }
62
+ /** Absolute path to the root kill-switch flag file. */
63
+ export declare function deftDirectiveDisableFlagPath(projectRoot: string): string;
64
+ /**
65
+ * Detect root `.deft-directive-disable`. Presence is file-existence only
66
+ * (empty or short comment OK). Deposit presence is reported but never treated
67
+ * as inconsistent (#3039 vs #2926).
68
+ *
69
+ * Enforcement short-circuit uses `state.active` (present && !trackedByGit).
70
+ */
71
+ export declare function detectDeftDirectiveDisable(projectRoot: string, seams?: DeftDirectiveDisableSeams): DeftDirectiveDisableState;
72
+ /**
73
+ * True when the local (untracked) kill-switch is active for enforcement
74
+ * short-circuit. Tracked flags return false so hooks stay enforced.
75
+ */
76
+ export declare function isDeftDirectiveDisableActive(projectRoot: string, seams?: DeftDirectiveDisableSeams): boolean;
77
+ /**
78
+ * Full operator-facing message for the kill-switch, optionally combined with
79
+ * permanent opt-out when both flags are present.
80
+ */
81
+ export declare function formatDeftDirectiveDisableMessage(options?: {
82
+ readonly permanentOptOutAlsoPresent?: boolean;
83
+ readonly trackedByGit?: boolean;
84
+ readonly oneLine?: boolean;
85
+ }): string;
86
+ //# sourceMappingURL=deft-directive-disable.d.ts.map
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Temporary test/local kill-switch for Directive enforcement (#3039).
3
+ *
4
+ * Presence of root `.deft-directive-disable` means Directive **enforcement** is
5
+ * OFF (hooks, session ritual, automation) while the deposit may remain — **only
6
+ * when the file is not tracked by git**. A committed flag is misconfig: doctor
7
+ * warns and enforcement stays ON (repository-controlled content must not disable
8
+ * hooks for downstream clones).
9
+ *
10
+ * Distinct from `.no-deft-directive` (#2926 permanent opt-out): flag+deposit is
11
+ * **not** inconsistent here.
12
+ *
13
+ * Product choices (v1):
14
+ * - Flag is **root-only** (workspace root the tool opened).
15
+ * - Flag **must be gitignored** (canonical baseline); committed flag → doctor
16
+ * warns and does **not** short-circuit enforcement.
17
+ * - Deposit presence is **OK**.
18
+ * - Full re-enable requires: delete the file **and** start a **new** agent session.
19
+ * - Precedence: active kill-switch first, then `.no-deft-directive`, else normal.
20
+ */
21
+ import { execFileSync } from "node:child_process";
22
+ import { statSync } from "node:fs";
23
+ import { join, resolve } from "node:path";
24
+ import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
25
+ /** Canonical root-only filename (lowercase). Presence = candidate flag. */
26
+ export const DEFT_DIRECTIVE_DISABLE_FLAG_NAME = ".deft-directive-disable";
27
+ /** Gitignore entry the deposit must ensure (same as flag name). */
28
+ export const DEFT_DIRECTIVE_DISABLE_GITIGNORE_LINE = DEFT_DIRECTIVE_DISABLE_FLAG_NAME;
29
+ /**
30
+ * Canonical recovery message when Directive is disabled by the test kill-switch.
31
+ * Surfaces (doctor, agent, CLI, hooks) share this wording.
32
+ */
33
+ export const DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE = "Directive is DISABLED for this project via root `.deft-directive-disable` (test/local kill-switch).\n" +
34
+ "Deposit may still be present; enforcement (hooks, session ritual, automation) will not run.\n" +
35
+ "\n" +
36
+ "To fully re-enable Directive:\n" +
37
+ " 1. Delete the file: rm .deft-directive-disable (or equivalent)\n" +
38
+ " 2. Start a NEW agent session (reload AGENTS / host skills / hooks)\n" +
39
+ "Until both are done, Directive is not fully operational.";
40
+ /** One-line summary for log lines and short CLI output. */
41
+ export const DEFT_DIRECTIVE_DISABLE_ONE_LINE = "Directive disabled via `.deft-directive-disable` (test/local kill-switch; deposit OK)";
42
+ /** Doctor/status label when the kill-switch is active (local / untracked). */
43
+ export const DEFT_DIRECTIVE_DISABLE_STATUS = "disabled-test-kill-switch";
44
+ /**
45
+ * Warning when the kill-switch file is tracked by git (should be gitignored).
46
+ * Tracked flags do **not** disable enforcement.
47
+ */
48
+ export const DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING = "Misconfig: `.deft-directive-disable` is tracked by git. The flag must be gitignored (local kill-switch only). Untrack it and ensure `.gitignore` covers the path. Enforcement is NOT disabled while the flag is tracked.";
49
+ /** Cap git probe so hot-path hooks never hang on a slow VCS call. */
50
+ const GIT_TRACKED_PROBE_TIMEOUT_MS = 1500;
51
+ /** Process-local cache: avoid re-spawning git on every PreToolUse while the flag is present. */
52
+ const trackedProbeCache = new Map();
53
+ const TRACKED_PROBE_CACHE_TTL_MS = 30_000;
54
+ function defaultIsFile(path) {
55
+ try {
56
+ return statSync(path).isFile();
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ function defaultIsDir(path) {
63
+ try {
64
+ return statSync(path).isDirectory();
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ function defaultIsGitTracked(projectRoot, relPath) {
71
+ const cacheKey = `${projectRoot}\0${relPath}`;
72
+ const now = Date.now();
73
+ const cached = trackedProbeCache.get(cacheKey);
74
+ if (cached !== undefined && now - cached.atMs < TRACKED_PROBE_CACHE_TTL_MS) {
75
+ return cached.tracked;
76
+ }
77
+ let tracked = false;
78
+ try {
79
+ const out = execFileSync("git", ["ls-files", "--", relPath], {
80
+ cwd: projectRoot,
81
+ encoding: "utf8",
82
+ stdio: ["ignore", "pipe", "ignore"],
83
+ timeout: GIT_TRACKED_PROBE_TIMEOUT_MS,
84
+ });
85
+ tracked = out.trim().length > 0;
86
+ }
87
+ catch {
88
+ tracked = false;
89
+ }
90
+ trackedProbeCache.set(cacheKey, { tracked, atMs: now });
91
+ return tracked;
92
+ }
93
+ /** Absolute path to the root kill-switch flag file. */
94
+ export function deftDirectiveDisableFlagPath(projectRoot) {
95
+ return resolve(projectRoot, DEFT_DIRECTIVE_DISABLE_FLAG_NAME);
96
+ }
97
+ /**
98
+ * Detect root `.deft-directive-disable`. Presence is file-existence only
99
+ * (empty or short comment OK). Deposit presence is reported but never treated
100
+ * as inconsistent (#3039 vs #2926).
101
+ *
102
+ * Enforcement short-circuit uses `state.active` (present && !trackedByGit).
103
+ */
104
+ export function detectDeftDirectiveDisable(projectRoot, seams = {}) {
105
+ const flagPath = deftDirectiveDisableFlagPath(projectRoot);
106
+ const isFile = seams.isFile ?? defaultIsFile;
107
+ const isDir = seams.isDir ?? defaultIsDir;
108
+ const isGitTracked = seams.isGitTracked ?? defaultIsGitTracked;
109
+ const present = isFile(flagPath);
110
+ const depositPresent = isDir(join(projectRoot, CANONICAL_INSTALL_ROOT));
111
+ let trackedByGit = false;
112
+ if (present) {
113
+ if (seams.skipTrackedCache && seams.isGitTracked === undefined) {
114
+ // Fresh probe without reading process cache (doctor / one-shot CLI).
115
+ try {
116
+ const out = execFileSync("git", ["ls-files", "--", DEFT_DIRECTIVE_DISABLE_FLAG_NAME], {
117
+ cwd: projectRoot,
118
+ encoding: "utf8",
119
+ stdio: ["ignore", "pipe", "ignore"],
120
+ timeout: GIT_TRACKED_PROBE_TIMEOUT_MS,
121
+ });
122
+ trackedByGit = out.trim().length > 0;
123
+ }
124
+ catch {
125
+ trackedByGit = false;
126
+ }
127
+ }
128
+ else {
129
+ trackedByGit = isGitTracked(projectRoot, DEFT_DIRECTIVE_DISABLE_FLAG_NAME);
130
+ }
131
+ }
132
+ return {
133
+ present,
134
+ flagPath,
135
+ depositPresent,
136
+ trackedByGit,
137
+ active: present && !trackedByGit,
138
+ };
139
+ }
140
+ /**
141
+ * True when the local (untracked) kill-switch is active for enforcement
142
+ * short-circuit. Tracked flags return false so hooks stay enforced.
143
+ */
144
+ export function isDeftDirectiveDisableActive(projectRoot, seams = {}) {
145
+ return detectDeftDirectiveDisable(projectRoot, seams).active;
146
+ }
147
+ /**
148
+ * Full operator-facing message for the kill-switch, optionally combined with
149
+ * permanent opt-out when both flags are present.
150
+ */
151
+ export function formatDeftDirectiveDisableMessage(options = {}) {
152
+ const parts = [];
153
+ if (options.oneLine) {
154
+ parts.push(DEFT_DIRECTIVE_DISABLE_ONE_LINE);
155
+ }
156
+ else {
157
+ parts.push(DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE);
158
+ }
159
+ if (options.permanentOptOutAlsoPresent) {
160
+ parts.push("Also present: root `.no-deft-directive` (permanent opt-out). Install/update fail-closed semantics still apply for that flag.");
161
+ }
162
+ if (options.trackedByGit) {
163
+ parts.push(DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING);
164
+ }
165
+ return parts.join("\n\n");
166
+ }
167
+ //# sourceMappingURL=deft-directive-disable.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed plan.policy.deliveryBranch (#3041).
3
+ *
4
+ * Distinct from swarm/PR `baseBranch` (integration target for PRs / finalize
5
+ * sweep PRs). deliveryBranch is where shipped work must land for a delivered
6
+ * completion disposition.
7
+ */
8
+ import { type GitRunner } from "../session/git.js";
9
+ export declare const FIELD_DELIVERY_BRANCH = "plan.policy.deliveryBranch";
10
+ export declare const FIELD_DELIVERY_BRANCH_CLI_ALIAS = "deliveryBranch";
11
+ /** Framework fallback when neither policy nor git default can be resolved. */
12
+ export declare const DEFAULT_DELIVERY_BRANCH_FALLBACK = "master";
13
+ export type DeliveryBranchSource = "typed" | "git-default" | "default-fallback" | "default-on-error";
14
+ export interface DeliveryBranchResult {
15
+ readonly branch: string;
16
+ readonly source: DeliveryBranchSource;
17
+ readonly error: string | null;
18
+ }
19
+ /**
20
+ * Resolve the project's delivery branch (#3041).
21
+ *
22
+ * Order: typed plan.policy.deliveryBranch → git remote default → local main/master → "master".
23
+ */
24
+ export declare function resolveDeliveryBranch(projectRoot: string, runGit?: GitRunner): DeliveryBranchResult;
25
+ export interface DeliveryBranchPolicyField {
26
+ readonly name: string;
27
+ readonly current: string;
28
+ readonly default: string;
29
+ readonly source: string;
30
+ }
31
+ /** Inspector row for `task policy:show --field=deliveryBranch` (#3041). */
32
+ export declare function inspectDeliveryBranch(_data: Record<string, unknown> | null, projectRoot?: string): DeliveryBranchPolicyField;
33
+ //# sourceMappingURL=delivery-branch.d.ts.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Typed plan.policy.deliveryBranch (#3041).
3
+ *
4
+ * Distinct from swarm/PR `baseBranch` (integration target for PRs / finalize
5
+ * sweep PRs). deliveryBranch is where shipped work must land for a delivered
6
+ * completion disposition.
7
+ */
8
+ import { defaultGitRunner } from "../session/git.js";
9
+ import { readPlanPolicy } from "./plan-extensions.js";
10
+ import { loadProjectDefinition } from "./resolve.js";
11
+ export const FIELD_DELIVERY_BRANCH = "plan.policy.deliveryBranch";
12
+ export const FIELD_DELIVERY_BRANCH_CLI_ALIAS = "deliveryBranch";
13
+ /** Framework fallback when neither policy nor git default can be resolved. */
14
+ export const DEFAULT_DELIVERY_BRANCH_FALLBACK = "master";
15
+ function defaultBranchCandidates(projectRoot, runGit) {
16
+ const sym = runGit(projectRoot, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
17
+ if (sym.code === 0 && sym.stdout) {
18
+ const trimmed = sym.stdout.trim();
19
+ // origin/main → main
20
+ const parts = trimmed.split("/");
21
+ const name = (parts.slice(1).join("/") || parts[0] || "").trim();
22
+ if (name.length > 0) {
23
+ return [name];
24
+ }
25
+ }
26
+ const candidates = [];
27
+ for (const branch of ["main", "master"]) {
28
+ const check = runGit(projectRoot, [
29
+ "show-ref",
30
+ "--verify",
31
+ "--quiet",
32
+ `refs/remotes/origin/${branch}`,
33
+ ]);
34
+ if (check.code === 0) {
35
+ candidates.push(branch);
36
+ }
37
+ }
38
+ if (candidates.length === 0) {
39
+ for (const branch of ["main", "master"]) {
40
+ const local = runGit(projectRoot, [
41
+ "show-ref",
42
+ "--verify",
43
+ "--quiet",
44
+ `refs/heads/${branch}`,
45
+ ]);
46
+ if (local.code === 0) {
47
+ candidates.push(branch);
48
+ }
49
+ }
50
+ }
51
+ return candidates;
52
+ }
53
+ /**
54
+ * Resolve the project's delivery branch (#3041).
55
+ *
56
+ * Order: typed plan.policy.deliveryBranch → git remote default → local main/master → "master".
57
+ */
58
+ export function resolveDeliveryBranch(projectRoot, runGit = defaultGitRunner) {
59
+ const [data, err] = loadProjectDefinition(projectRoot);
60
+ if (data === null) {
61
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
62
+ if (gitDefault !== undefined) {
63
+ return { branch: gitDefault, source: "git-default", error: err };
64
+ }
65
+ return {
66
+ branch: DEFAULT_DELIVERY_BRANCH_FALLBACK,
67
+ source: "default-fallback",
68
+ error: err,
69
+ };
70
+ }
71
+ const plan = data.plan;
72
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
73
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
74
+ return {
75
+ branch: gitDefault ?? DEFAULT_DELIVERY_BRANCH_FALLBACK,
76
+ source: gitDefault !== undefined ? "git-default" : "default-fallback",
77
+ error: "PROJECT-DEFINITION 'plan' is not an object",
78
+ };
79
+ }
80
+ const policyBlock = readPlanPolicy(plan);
81
+ if (typeof policyBlock === "object" &&
82
+ policyBlock !== null &&
83
+ !Array.isArray(policyBlock) &&
84
+ "deliveryBranch" in policyBlock) {
85
+ const raw = policyBlock.deliveryBranch;
86
+ if (typeof raw !== "string" || raw.trim().length === 0) {
87
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
88
+ return {
89
+ branch: gitDefault ?? DEFAULT_DELIVERY_BRANCH_FALLBACK,
90
+ source: "default-on-error",
91
+ error: `plan.policy.deliveryBranch must be a non-empty string; got ${typeof raw}`,
92
+ };
93
+ }
94
+ return { branch: raw.trim(), source: "typed", error: null };
95
+ }
96
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
97
+ if (gitDefault !== undefined) {
98
+ return { branch: gitDefault, source: "git-default", error: null };
99
+ }
100
+ return {
101
+ branch: DEFAULT_DELIVERY_BRANCH_FALLBACK,
102
+ source: "default-fallback",
103
+ error: null,
104
+ };
105
+ }
106
+ /** Inspector row for `task policy:show --field=deliveryBranch` (#3041). */
107
+ export function inspectDeliveryBranch(_data, projectRoot) {
108
+ if (projectRoot === undefined || projectRoot.length === 0) {
109
+ return {
110
+ name: FIELD_DELIVERY_BRANCH,
111
+ current: DEFAULT_DELIVERY_BRANCH_FALLBACK,
112
+ default: DEFAULT_DELIVERY_BRANCH_FALLBACK,
113
+ source: "default",
114
+ };
115
+ }
116
+ const resolved = resolveDeliveryBranch(projectRoot);
117
+ return {
118
+ name: FIELD_DELIVERY_BRANCH,
119
+ current: resolved.branch,
120
+ default: DEFAULT_DELIVERY_BRANCH_FALLBACK,
121
+ source: resolved.source,
122
+ };
123
+ }
124
+ //# sourceMappingURL=delivery-branch.js.map