@deftai/directive-core 0.87.0 → 0.89.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 (193) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/scanner.d.ts +11 -1
  28. package/dist/cache/scanner.js +29 -4
  29. package/dist/cache/task-cache/store.js +11 -7
  30. package/dist/capacity/backfill.js +10 -4
  31. package/dist/category-b-namespace/index.js +10 -4
  32. package/dist/check/gate-lists.js +1 -0
  33. package/dist/codebase/default-extractor.js +3 -0
  34. package/dist/codebase/map.js +11 -4
  35. package/dist/content-contracts/skills/helpers.d.ts +10 -0
  36. package/dist/content-contracts/skills/helpers.js +35 -0
  37. package/dist/deposit/copy-tree.d.ts +19 -1
  38. package/dist/deposit/copy-tree.js +134 -5
  39. package/dist/doctor/doctor-state.js +28 -4
  40. package/dist/doctor/main.d.ts +10 -0
  41. package/dist/doctor/main.js +208 -0
  42. package/dist/doctor/types.d.ts +11 -0
  43. package/dist/escalation/actions.d.ts +63 -0
  44. package/dist/escalation/actions.js +137 -0
  45. package/dist/escalation/index.d.ts +8 -0
  46. package/dist/escalation/index.js +8 -0
  47. package/dist/escalation/paths.d.ts +3 -0
  48. package/dist/escalation/paths.js +10 -0
  49. package/dist/escalation/store.d.ts +17 -0
  50. package/dist/escalation/store.js +172 -0
  51. package/dist/escalation/types.d.ts +73 -0
  52. package/dist/escalation/types.js +43 -0
  53. package/dist/eval/crud-telemetry.js +30 -10
  54. package/dist/eval/health.js +22 -7
  55. package/dist/eval/readback.js +11 -10
  56. package/dist/eval/run.js +32 -16
  57. package/dist/events/attribution-enrichment.js +10 -4
  58. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  59. package/dist/finish-loop/directive-finish-loop.js +239 -0
  60. package/dist/finish-loop/grant-gate.d.ts +31 -0
  61. package/dist/finish-loop/grant-gate.js +169 -0
  62. package/dist/finish-loop/index.d.ts +11 -0
  63. package/dist/finish-loop/index.js +11 -0
  64. package/dist/finish-loop/main.d.ts +35 -0
  65. package/dist/finish-loop/main.js +365 -0
  66. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  67. package/dist/finish-loop/pr-finish-loop.js +217 -0
  68. package/dist/finish-loop/progress.d.ts +17 -0
  69. package/dist/finish-loop/progress.js +45 -0
  70. package/dist/finish-loop/queue.d.ts +16 -0
  71. package/dist/finish-loop/queue.js +65 -0
  72. package/dist/finish-loop/types.d.ts +52 -0
  73. package/dist/finish-loop/types.js +10 -0
  74. package/dist/fs/contained-write.d.ts +101 -0
  75. package/dist/fs/contained-write.js +281 -0
  76. package/dist/fs/projection-containment.d.ts +18 -0
  77. package/dist/fs/projection-containment.js +40 -0
  78. package/dist/hooks/classify/classify.d.ts +10 -0
  79. package/dist/hooks/classify/classify.js +37 -0
  80. package/dist/hooks/classify/index.d.ts +14 -0
  81. package/dist/hooks/classify/index.js +13 -0
  82. package/dist/hooks/classify/paths.d.ts +22 -0
  83. package/dist/hooks/classify/paths.js +72 -0
  84. package/dist/hooks/classify/payload.d.ts +16 -0
  85. package/dist/hooks/classify/payload.js +45 -0
  86. package/dist/hooks/classify/stdin.d.ts +12 -0
  87. package/dist/hooks/classify/stdin.js +63 -0
  88. package/dist/hooks/classify/tool-name.d.ts +18 -0
  89. package/dist/hooks/classify/tool-name.js +85 -0
  90. package/dist/hooks/classify/types.d.ts +41 -0
  91. package/dist/hooks/classify/types.js +7 -0
  92. package/dist/hooks/dispatcher.d.ts +30 -15
  93. package/dist/hooks/dispatcher.js +326 -134
  94. package/dist/hooks/fixtures/cases.d.ts +44 -0
  95. package/dist/hooks/fixtures/cases.js +595 -0
  96. package/dist/hooks/fixtures/index.d.ts +2 -0
  97. package/dist/hooks/fixtures/index.js +2 -0
  98. package/dist/hooks/index.d.ts +2 -0
  99. package/dist/hooks/index.js +2 -0
  100. package/dist/hooks/tools.d.ts +31 -0
  101. package/dist/hooks/tools.js +74 -0
  102. package/dist/index.d.ts +4 -0
  103. package/dist/index.js +5 -0
  104. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  105. package/dist/init-deposit/agent-hooks.js +65 -10
  106. package/dist/init-deposit/gitignore.js +11 -3
  107. package/dist/init-deposit/headless-manifest.js +12 -4
  108. package/dist/init-deposit/hygiene.d.ts +16 -0
  109. package/dist/init-deposit/hygiene.js +26 -0
  110. package/dist/init-deposit/init-dispatch.js +28 -0
  111. package/dist/init-deposit/migrate.d.ts +1 -1
  112. package/dist/init-deposit/migrate.js +13 -3
  113. package/dist/init-deposit/prettierignore.js +12 -5
  114. package/dist/init-deposit/refresh.js +38 -7
  115. package/dist/init-deposit/scaffold.js +42 -29
  116. package/dist/init-deposit/xbrief-projections.js +17 -10
  117. package/dist/intake/candidates-log.js +46 -35
  118. package/dist/intake/github-body-cli.d.ts +6 -0
  119. package/dist/intake/github-body-cli.js +17 -0
  120. package/dist/intake/github-body.d.ts +46 -3
  121. package/dist/intake/github-body.js +191 -20
  122. package/dist/intake/issue-emit.d.ts +1 -0
  123. package/dist/intake/issue-emit.js +41 -23
  124. package/dist/intake/issue-ingest.js +11 -2
  125. package/dist/lifecycle/event-detect.js +12 -4
  126. package/dist/lifecycle/events.js +29 -20
  127. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  128. package/dist/packs/pack-render.d.ts +33 -0
  129. package/dist/packs/pack-render.js +172 -13
  130. package/dist/packs/quarantine-ext.d.ts +10 -0
  131. package/dist/packs/quarantine-ext.js +26 -2
  132. package/dist/plan-sequence/store.js +11 -4
  133. package/dist/policy/hotfix-criteria.d.ts +79 -0
  134. package/dist/policy/hotfix-criteria.js +197 -0
  135. package/dist/policy/index.d.ts +5 -0
  136. package/dist/policy/index.js +32 -1
  137. package/dist/policy/intent-ceiling.d.ts +79 -0
  138. package/dist/policy/intent-ceiling.js +181 -0
  139. package/dist/policy/no-deft-directive.d.ts +59 -0
  140. package/dist/policy/no-deft-directive.js +110 -0
  141. package/dist/policy/org-force-on-migration.d.ts +52 -0
  142. package/dist/policy/org-force-on-migration.js +269 -27
  143. package/dist/policy/require-human-merge.d.ts +75 -0
  144. package/dist/policy/require-human-merge.js +324 -0
  145. package/dist/policy/resolve.js +17 -6
  146. package/dist/policy/runtime-authority.d.ts +56 -2
  147. package/dist/policy/runtime-authority.js +297 -3
  148. package/dist/policy/write-fence.d.ts +78 -0
  149. package/dist/policy/write-fence.js +164 -0
  150. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  151. package/dist/pr-wait-mergeable/cascade.js +28 -0
  152. package/dist/preflight/evaluate.js +10 -0
  153. package/dist/product-signal/consent.js +21 -5
  154. package/dist/product-signal/submit.js +22 -12
  155. package/dist/release/build-dist-runner.js +5 -2
  156. package/dist/release/build-dist.d.ts +19 -1
  157. package/dist/release/build-dist.js +50 -2
  158. package/dist/release/native-steps.js +7 -2
  159. package/dist/release-publish/pipeline.d.ts +13 -0
  160. package/dist/release-publish/pipeline.js +46 -1
  161. package/dist/scope/audit-log.js +19 -24
  162. package/dist/scope/decompose.js +10 -5
  163. package/dist/scope/decomposed-refs.js +18 -3
  164. package/dist/scope/demote.js +9 -2
  165. package/dist/scope/undo.js +9 -2
  166. package/dist/session/release-availability.js +26 -6
  167. package/dist/session/ritual-sentinel.js +19 -12
  168. package/dist/session/session-start-hook.d.ts +3 -0
  169. package/dist/session/session-start-hook.js +15 -0
  170. package/dist/session/session-start.js +37 -0
  171. package/dist/staleness-tickler/state.js +26 -6
  172. package/dist/swarm/launch.js +13 -3
  173. package/dist/swarm/routing.js +9 -3
  174. package/dist/task-surface/index.js +24 -9
  175. package/dist/triage/bootstrap/gitignore.js +53 -10
  176. package/dist/triage/cache-path.js +10 -3
  177. package/dist/triage/welcome/summary.js +11 -4
  178. package/dist/triage/welcome/writers.js +45 -16
  179. package/dist/validate-content/validate-links.js +5 -0
  180. package/dist/value/readback.js +11 -6
  181. package/dist/vbrief-activate/activate.js +12 -4
  182. package/dist/vbrief-build/project-definition-io.js +14 -6
  183. package/dist/verify-source/contained-writes.d.ts +59 -0
  184. package/dist/verify-source/contained-writes.js +272 -0
  185. package/dist/verify-source/cursor-tier1.js +7 -2
  186. package/dist/verify-source/index.d.ts +1 -0
  187. package/dist/verify-source/index.js +1 -0
  188. package/dist/verify-source/openclaw-tier1.js +19 -1
  189. package/dist/verify-source/verify-stubs.js +3 -0
  190. package/dist/xbrief-migrate/migrate-project.js +26 -12
  191. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  192. package/dist/xbrief-migrate/transforms.js +37 -14
  193. package/package.json +16 -3
@@ -1,9 +1,12 @@
1
+ import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
1
2
  import { writeSentinel } from "./ritual-sentinel.js";
2
3
  export interface SessionStartHookOptions {
3
4
  readonly resolveVersionFn?: () => string;
4
5
  readonly detectBranchFn?: (projectRoot: string) => string | null;
5
6
  readonly detectLatestActiveVbriefFn?: (projectRoot: string) => string | null;
6
7
  readonly writeSentinelFn?: typeof writeSentinel;
8
+ /** Test seam for #2926 opt-out detection. */
9
+ readonly detectNoDeftDirectiveFn?: typeof detectNoDeftDirective;
7
10
  }
8
11
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
9
12
  export declare function runSessionStartHookWrite(projectRoot: string, options?: SessionStartHookOptions): {
@@ -1,8 +1,23 @@
1
1
  import { resolveVersion } from "../doctor/paths.js";
2
+ import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
2
3
  import { detectBranch } from "./git.js";
3
4
  import { detectLatestActiveVbrief, writeSentinel } from "./ritual-sentinel.js";
4
5
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
5
6
  export function runSessionStartHookWrite(projectRoot, options = {}) {
7
+ const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
8
+ // #2926: root opt-out wins — host SessionStart must not write ritual bookkeeping.
9
+ const optOut = detectOptOut(projectRoot);
10
+ if (optOut.present) {
11
+ const lines = [NO_DEFT_DIRECTIVE_DISABLED_MESSAGE];
12
+ if (optOut.inconsistent) {
13
+ lines.push(NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE);
14
+ }
15
+ return {
16
+ code: optOut.inconsistent ? 1 : 0,
17
+ stdout: `${lines.join("\n")}\n`,
18
+ stderr: optOut.inconsistent ? `${NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE}\n` : "",
19
+ };
20
+ }
6
21
  const detectBranchFn = options.detectBranchFn ?? detectBranch;
7
22
  const detectVbriefFn = options.detectLatestActiveVbriefFn ?? detectLatestActiveVbrief;
8
23
  const resolveVersionFn = options.resolveVersionFn ?? resolveVersion;
@@ -4,6 +4,8 @@ import { emitSessionEvalReadback } from "../eval/readback.js";
4
4
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
6
  import { disclosureLine } from "../policy/disclosure.js";
7
+ import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
8
+ import { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/require-human-merge.js";
7
9
  import { resolvePolicy } from "../policy/resolve.js";
8
10
  import { maybeFormatProductSignalConsentPrompt } from "../product-signal/consent-prompt.js";
9
11
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
@@ -233,6 +235,35 @@ export function runSessionStart(projectRoot, options = {}) {
233
235
  const deferrals = options.deferrals ?? {};
234
236
  const runGit = options.runGit ?? defaultGitRunner;
235
237
  const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
238
+ // #2926: official root opt-out wins locally — skip Directive session ritual.
239
+ // disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
240
+ // automation does not treat opt-out as "session fully initialized for work".
241
+ const optOut = detectNoDeftDirective(projectRoot);
242
+ if (optOut.present) {
243
+ const lines = [NO_DEFT_DIRECTIVE_DISABLED_MESSAGE];
244
+ if (optOut.inconsistent) {
245
+ lines.push(NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE);
246
+ }
247
+ const code = optOut.inconsistent ? 1 : 0;
248
+ return {
249
+ code,
250
+ payload: {
251
+ ready: false,
252
+ exit_code: code,
253
+ disabled: true,
254
+ disabled_via: NO_DEFT_DIRECTIVE_FLAG_NAME,
255
+ inconsistent: optOut.inconsistent,
256
+ inconsistent_policy: optOut.inconsistent
257
+ ? NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY
258
+ : undefined,
259
+ deposit_present: optOut.depositPresent,
260
+ posture,
261
+ environment: environmentContextToDict(environment),
262
+ message: NO_DEFT_DIRECTIVE_DISABLED_MESSAGE,
263
+ },
264
+ lines,
265
+ };
266
+ }
236
267
  if (posture === READ_ONLY_POSTURE) {
237
268
  return runReadOnlySessionStart(projectRoot, options, instant, environment);
238
269
  }
@@ -289,6 +320,12 @@ export function runSessionStart(projectRoot, options = {}) {
289
320
  exitCode: ok ? 0 : 2,
290
321
  });
291
322
  lines.push(message);
323
+ // Human merge gate disclosure (#1193) — surface when ON (or env bypass active).
324
+ const humanMerge = resolveHumanMergePolicy(projectRoot);
325
+ const humanMergeLine = humanMergeDisclosureLine(humanMerge);
326
+ if (humanMergeLine !== null) {
327
+ lines.push(humanMergeLine);
328
+ }
292
329
  const branchSync = defaultBranchSync(projectRoot, runGit);
293
330
  if (branchSync.warning) {
294
331
  lines.push(branchSync.warning);
@@ -1,5 +1,6 @@
1
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { resolveTriageCachePath } from "../triage/cache-path.js";
4
5
  export const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", "staleness-tickler-state.json");
5
6
  const STATE_FILE_NAME = "staleness-tickler-state.json";
@@ -15,10 +16,29 @@ function defaultReadText(path) {
15
16
  }
16
17
  }
17
18
  function defaultWriteText(path, content) {
18
- mkdirSync(dirname(path), { recursive: true });
19
- const temporary = `${path}.${process.pid}.tmp`;
20
- writeFileSync(temporary, content, "utf8");
21
- renameSync(temporary, path);
19
+ const dir = dirname(path);
20
+ mkdirSync(dir, { recursive: true });
21
+ const tmpBase = `${basename(path)}.${process.pid}.tmp`;
22
+ const temporary = join(dir, tmpBase);
23
+ try {
24
+ // #2980 wave D: product write sink routes through containedWrite.
25
+ containedWrite({
26
+ root: resolve(dir),
27
+ target: tmpBase,
28
+ data: content,
29
+ mode: "create",
30
+ });
31
+ renameSync(temporary, path);
32
+ }
33
+ catch (err) {
34
+ try {
35
+ rmSync(temporary, { force: true });
36
+ }
37
+ catch {
38
+ /* best-effort cleanup */
39
+ }
40
+ throw err;
41
+ }
22
42
  }
23
43
  export function parseStalenessTicklerState(text) {
24
44
  if (text === null) {
@@ -1,5 +1,6 @@
1
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, isAbsolute, join, resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { inferGithubAuthMode } from "../intake/github-auth-modes.js";
4
5
  import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
5
6
  import { hasArtifactSuffix, resolveLifecycleFolder, stripArtifactSuffix, } from "../layout/resolve.js";
@@ -581,7 +582,16 @@ export function swarmLaunch(args) {
581
582
  const rendered = `${JSON.stringify(manifest, null, 2)}\n`;
582
583
  if (args.output !== undefined && args.output !== null) {
583
584
  try {
584
- writeFileSync(args.output, rendered, "utf8");
585
+ // #2980 wave D: product write sink routes through containedWrite.
586
+ const abs = resolve(args.output);
587
+ const dir = dirname(abs);
588
+ mkdirSync(dir, { recursive: true });
589
+ containedWrite({
590
+ root: dir,
591
+ target: basename(abs),
592
+ data: rendered,
593
+ mode: "replace",
594
+ });
585
595
  }
586
596
  catch (exc) {
587
597
  return {
@@ -14,8 +14,9 @@
14
14
  * explicit default.
15
15
  */
16
16
  import { execFileSync } from "node:child_process";
17
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { existsSync, readFileSync } from "node:fs";
18
18
  import { dirname, isAbsolute, join, resolve } from "node:path";
19
+ import { containedWrite } from "../fs/contained-write.js";
19
20
  import { assertWriteTargetSafe } from "../fs/projection-containment.js";
20
21
  /**
21
22
  * The fixed worker-role vocabulary (reused from #1531). No separate tier
@@ -234,7 +235,12 @@ export function writeModelDecision(projectRoot, path, provider, role, decision)
234
235
  decidedAt: decision.decidedAt ?? new Date().toISOString(),
235
236
  };
236
237
  file[provider] = block;
237
- mkdirSync(dirname(path), { recursive: true });
238
- writeFileSync(path, `${JSON.stringify(file, null, 2)}\n`, "utf8");
238
+ // #2980 wave D: product write sink routes through containedWrite.
239
+ containedWrite({
240
+ root: resolve(projectRoot),
241
+ target: path,
242
+ data: `${JSON.stringify(file, null, 2)}\n`,
243
+ mode: "replace",
244
+ });
239
245
  }
240
246
  //# sourceMappingURL=routing.js.map
@@ -1,8 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { LEGACY_VBRIEF_VERSION } from "@deftai/directive-types";
5
- import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
5
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
6
+ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
6
7
  const UNRELEASED_RE = /## \[Unreleased\][ \t]*\n([\s\S]*?)(?=\n## \[|$)/;
7
8
  const CHANGE_NAME_RE = /^[\w][\w-]*$/;
8
9
  const COMMIT_TYPES = "feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert";
@@ -14,7 +15,7 @@ function projectionTarget(projectDir, ...relSegments) {
14
15
  return target;
15
16
  }
16
17
  function containmentExitCode(io, err) {
17
- if (err instanceof ProjectionContainmentError) {
18
+ if (err instanceof ProjectionContainmentError || err instanceof ContainedWriteError) {
18
19
  io.writeErr(`FAIL: ${err.message}\n`);
19
20
  return 2;
20
21
  }
@@ -93,10 +94,19 @@ export function runChangeInit(projectRoot, name, io) {
93
94
  mkdirSync(specsDir, { recursive: true });
94
95
  const proposalPath = join(base, "proposal.xbrief.json");
95
96
  const tasksPath = join(base, "tasks.xbrief.json");
96
- assertWriteTargetSafe(projectAbs, proposalPath);
97
- assertWriteTargetSafe(projectAbs, tasksPath);
98
- writeFileSync(proposalPath, `${JSON.stringify(proposalTemplate(trimmed), null, 2)}\n`, "utf8");
99
- writeFileSync(tasksPath, `${JSON.stringify(tasksTemplate(trimmed), null, 2)}\n`, "utf8");
97
+ // #2980 wave D: product write sinks route through containedWrite.
98
+ containedWrite({
99
+ root: projectAbs,
100
+ target: proposalPath,
101
+ data: `${JSON.stringify(proposalTemplate(trimmed), null, 2)}\n`,
102
+ mode: "create",
103
+ });
104
+ containedWrite({
105
+ root: projectAbs,
106
+ target: tasksPath,
107
+ data: `${JSON.stringify(tasksTemplate(trimmed), null, 2)}\n`,
108
+ mode: "create",
109
+ });
100
110
  io.writeOut(`OK: Created change proposal at ${base}/\n`);
101
111
  for (const file of ["proposal.xbrief.json", "tasks.xbrief.json", "specs/"]) {
102
112
  io.writeOut(` - ${file}\n`);
@@ -150,8 +160,13 @@ export function runInstallUninstall(projectRoot, io) {
150
160
  const next = filtered.join("");
151
161
  if (next !== original) {
152
162
  try {
153
- assertWriteTargetSafe(resolve(projectRoot), path);
154
- writeFileSync(path, next, "utf8");
163
+ // #2980 wave D: product write sink routes through containedWrite.
164
+ containedWrite({
165
+ root: resolve(projectRoot),
166
+ target: path,
167
+ data: next,
168
+ mode: "replace",
169
+ });
155
170
  io.writeOut("Removed deft entry from AGENTS.md\n");
156
171
  }
157
172
  catch (err) {
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, relative } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { containedWrite } from "../../fs/contained-write.js";
3
4
  import { assertProjectionContained, ProjectionContainmentError, } from "../../fs/projection-containment.js";
4
5
  import { MIGRATED_ARTIFACT_DIR, resolveLifecycleLayout } from "../../layout/resolve.js";
5
6
  import { resolveCandidatesLogPath, resolveTriageCachePath, TRIAGE_CACHE_DIR_NAME, triageCacheRelPath, } from "../cache-path.js";
@@ -197,7 +198,14 @@ function ensureGitignoreLine(projectRoot, gitignorePath, line, stepName, createI
197
198
  return stepOutcome(stepName, false, `.gitignore not present after the prior gitignore step; ${line} not written -- re-run bootstrap to retry`, { created: false, appended: false, skipped: "no-gitignore" }, "prior gitignore step did not create .gitignore");
198
199
  }
199
200
  try {
200
- writeFileSync(gitignorePath, `${line}\n`, { encoding: "utf8" });
201
+ // #2980 wave D: product write sink routes through containedWrite.
202
+ const root = resolve(projectRoot);
203
+ containedWrite({
204
+ root,
205
+ target: gitignorePath,
206
+ data: `${line}\n`,
207
+ mode: "create",
208
+ });
201
209
  }
202
210
  catch (exc) {
203
211
  return stepOutcome(stepName, false, "could not create .gitignore", {}, String(exc));
@@ -232,7 +240,13 @@ function ensureGitignoreLine(projectRoot, gitignorePath, line, stepName, createI
232
240
  const suffix = existing.endsWith("\n") || existing === "" ? "" : "\n";
233
241
  const newContent = `${existing + suffix + rationaleBlock + line}\n`;
234
242
  try {
235
- writeFileSync(gitignorePath, newContent, { encoding: "utf8" });
243
+ // #2980 wave D: product write sink routes through containedWrite.
244
+ containedWrite({
245
+ root: resolve(projectRoot),
246
+ target: gitignorePath,
247
+ data: newContent,
248
+ mode: "replace",
249
+ });
236
250
  }
237
251
  catch (exc) {
238
252
  return stepOutcome(stepName, false, "could not write .gitignore", {}, String(exc));
@@ -303,7 +317,13 @@ function ensureGitignoreSelectiveEntries(projectRoot, stepName, entries) {
303
317
  : `${EVAL_ENTRIES_RATIONALE}${missing.join("\n")}\n`;
304
318
  const newContent = existing + suffix + appendedBlock;
305
319
  try {
306
- writeFileSync(gitignorePath, newContent, { encoding: "utf8" });
320
+ // #2980 wave D: product write sink routes through containedWrite.
321
+ containedWrite({
322
+ root: resolve(projectRoot),
323
+ target: gitignorePath,
324
+ data: newContent,
325
+ mode: "replace",
326
+ });
307
327
  }
308
328
  catch (exc) {
309
329
  return stepOutcome(stepName, false, "could not write .gitignore", { gitignore_appended_lines: 0 }, String(exc));
@@ -341,7 +361,13 @@ function ensureGitattributesMergeUnion(projectRoot, stepName, glob, ruleLine) {
341
361
  const suffix = existing.endsWith("\n") || existing === "" ? "" : "\n";
342
362
  const newContent = `${existing + suffix + GITATTRIBUTES_EVAL_RATIONALE + ruleLine}\n`;
343
363
  try {
344
- writeFileSync(gitattributesPath, newContent, { encoding: "utf8" });
364
+ // #2980 wave D: product write sink routes through containedWrite.
365
+ containedWrite({
366
+ root: resolve(projectRoot),
367
+ target: gitattributesPath,
368
+ data: newContent,
369
+ mode: "replace",
370
+ });
345
371
  }
346
372
  catch (exc) {
347
373
  return stepOutcome(stepName, false, "could not write .gitattributes", { gitattributes_appended: false }, String(exc));
@@ -353,7 +379,13 @@ function ensureGitattributesMergeUnion(projectRoot, stepName, glob, ruleLine) {
353
379
  }
354
380
  const newContent = `${GITATTRIBUTES_EVAL_RATIONALE + ruleLine}\n`;
355
381
  try {
356
- writeFileSync(gitattributesPath, newContent, { encoding: "utf8" });
382
+ // #2980 wave D: product write sink routes through containedWrite.
383
+ containedWrite({
384
+ root: resolve(projectRoot),
385
+ target: gitattributesPath,
386
+ data: newContent,
387
+ mode: "create",
388
+ });
357
389
  }
358
390
  catch (exc) {
359
391
  return stepOutcome(stepName, false, "could not create .gitattributes", { gitattributes_appended: false }, String(exc));
@@ -382,8 +414,13 @@ function ensureEvalReadme(options) {
382
414
  // create below
383
415
  }
384
416
  try {
385
- mkdirSync(dirname(readmePath), { recursive: true });
386
- writeFileSync(readmePath, generateTriageCacheReadmeBody(projectRoot), { encoding: "utf8" });
417
+ // #2980 wave D: product write sink routes through containedWrite.
418
+ containedWrite({
419
+ root: resolve(projectRoot),
420
+ target: readmePath,
421
+ data: generateTriageCacheReadmeBody(projectRoot),
422
+ mode: "create",
423
+ });
387
424
  }
388
425
  catch (exc) {
389
426
  return stepOutcome(stepName, false, `could not create ${readmePath}`, { readme_created: false }, String(exc));
@@ -474,7 +511,13 @@ export function stepSeedCandidatesLog(projectRoot) {
474
511
  // create below
475
512
  }
476
513
  try {
477
- writeFileSync(auditPath, "", { encoding: "utf8" });
514
+ // #2980 wave D: product write sink routes through containedWrite.
515
+ containedWrite({
516
+ root: resolve(projectRoot),
517
+ target: auditPath,
518
+ data: "",
519
+ mode: "create",
520
+ });
478
521
  }
479
522
  catch (exc) {
480
523
  return stepOutcome("seed_candidates_log", false, `could not seed ${auditPath}`, {}, String(exc));
@@ -4,8 +4,9 @@
4
4
  * Triage append-only logs and scratch dirs live under `.triage-cache/` so the
5
5
  * `.eval/` namespace can be reclaimed for version-eval results (#1703 Tier 2).
6
6
  */
7
- import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
8
- import { join, relative } from "node:path";
7
+ import { existsSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
8
+ import { join, relative, resolve } from "node:path";
9
+ import { containedWrite } from "../fs/contained-write.js";
9
10
  import { assertProjectionContained } from "../fs/projection-containment.js";
10
11
  import { MIGRATED_ARTIFACT_DIR, resolveEvalDir, resolveLifecycleLayout, resolveLifecycleRoot, } from "../layout/resolve.js";
11
12
  import { generateTriageCacheReadmeBody } from "./bootstrap/gitignore.js";
@@ -99,7 +100,13 @@ export function migrateLegacyTriageCacheFromEval(projectRoot) {
99
100
  removedLegacyFiles.push(name);
100
101
  }
101
102
  else {
102
- writeFileSync(targetPath, generateTriageCacheReadmeBody(projectRoot), "utf8");
103
+ // #2980 wave D: product write sink routes through containedWrite.
104
+ containedWrite({
105
+ root: resolve(projectRoot),
106
+ target: targetPath,
107
+ data: generateTriageCacheReadmeBody(projectRoot),
108
+ mode: "create",
109
+ });
103
110
  unlinkSync(legacyPath);
104
111
  regeneratedFiles.push(name);
105
112
  }
@@ -1,5 +1,6 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { containedWrite } from "../../fs/contained-write.js";
3
4
  import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
4
5
  import { readPlanPolicy } from "../../policy/plan-extensions.js";
5
6
  import { countVbriefWip, resolveWipCap } from "../../policy/wip.js";
@@ -296,8 +297,14 @@ export function appendHistory(historyPath, result, line) {
296
297
  reconcilable: result.reconcilable,
297
298
  };
298
299
  try {
299
- mkdirSync(join(historyPath, ".."), { recursive: true });
300
- appendFileSync(historyPath, `${JSON.stringify(record)}\n`, "utf8");
300
+ // #2980 wave D: product write sink routes through containedWrite.
301
+ const dir = dirname(historyPath);
302
+ containedWrite({
303
+ root: resolve(dir),
304
+ target: basename(historyPath),
305
+ data: `${JSON.stringify(record)}\n`,
306
+ mode: "append",
307
+ });
301
308
  }
302
309
  catch {
303
310
  // observability only
@@ -1,6 +1,6 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { assertProjectionContained } from "../../fs/projection-containment.js";
3
+ import { containedWrite } from "../../fs/contained-write.js";
4
4
  import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
5
5
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
6
6
  import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
@@ -12,22 +12,51 @@ function utcIso() {
12
12
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
13
13
  }
14
14
  export function appendAuditEntry(projectRoot, entry) {
15
- const logPath = join(resolve(projectRoot), AUDIT_LOG_REL_PATH);
16
- assertProjectionContained(projectRoot, logPath);
17
- mkdirSync(dirname(logPath), { recursive: true });
18
- const line = `${utcIso()} ${entry}\n`;
15
+ const root = resolve(projectRoot);
16
+ const logPath = join(root, AUDIT_LOG_REL_PATH);
17
+ // #2980 wave D: product write sink routes through containedWrite.
18
+ mkdirSync(root, { recursive: true });
19
19
  if (!existsSync(logPath)) {
20
- writeFileSync(logPath, "# meta/policy-changes.log -- audit trail for PROJECT-DEFINITION plan.policy.* mutations (#746 / #1143)\n", "utf8");
20
+ containedWrite({
21
+ root,
22
+ target: AUDIT_LOG_REL_PATH,
23
+ data: "# meta/policy-changes.log -- audit trail for PROJECT-DEFINITION plan.policy.* mutations (#746 / #1143)\n",
24
+ mode: "create",
25
+ });
21
26
  }
22
- appendFileSync(logPath, line, "utf8");
27
+ containedWrite({
28
+ root,
29
+ target: AUDIT_LOG_REL_PATH,
30
+ data: `${utcIso()} ${entry}\n`,
31
+ mode: "append",
32
+ });
23
33
  return logPath;
24
34
  }
25
- function atomicWrite(path, data) {
26
- mkdirSync(dirname(path), { recursive: true });
35
+ function atomicWrite(_projectRoot, path, data) {
36
+ const dir = dirname(path);
37
+ mkdirSync(dir, { recursive: true });
27
38
  const payload = `${JSON.stringify(data, null, 2)}\n`;
28
- const tmp = join(dirname(path), `.${Date.now()}.tmp`);
29
- writeFileSync(tmp, payload, "utf8");
30
- renameSync(tmp, path);
39
+ const tmpName = `.${Date.now()}.tmp`;
40
+ const tmp = join(dir, tmpName);
41
+ try {
42
+ // #2980 wave D: temp payload write uses containedWrite under the parent dir.
43
+ containedWrite({
44
+ root: resolve(dir),
45
+ target: tmpName,
46
+ data: payload,
47
+ mode: "create",
48
+ });
49
+ renameSync(tmp, path);
50
+ }
51
+ catch (err) {
52
+ try {
53
+ rmSync(tmp, { force: true });
54
+ }
55
+ catch {
56
+ /* best-effort cleanup */
57
+ }
58
+ throw err;
59
+ }
31
60
  }
32
61
  export function writeTriageScope(projectRoot, rules, options = { presetLabel: "custom" }) {
33
62
  // Serialise read-modify-write + audit append under the shared lock (#1260).
@@ -54,7 +83,7 @@ export function writeTriageScope(projectRoot, rules, options = { presetLabel: "c
54
83
  const policyRec = policy;
55
84
  const previous = policyRec.triageScope;
56
85
  policyRec.triageScope = rules;
57
- atomicWrite(path, data);
86
+ atomicWrite(projectRoot, path, data);
58
87
  const changed = JSON.stringify(previous) !== JSON.stringify(rules);
59
88
  const actor = options.actor ?? WELCOME_AUDIT_TAG;
60
89
  const auditEntry = [
@@ -101,14 +130,14 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
101
130
  }
102
131
  if (previous !== undefined && wipCap === DEFAULT_WIP_CAP) {
103
132
  delete policyRec.wipCap;
104
- atomicWrite(path, data);
133
+ atomicWrite(projectRoot, path, data);
105
134
  const auditEntry = `actor=${actor} field=plan.policy.wipCap action=cleared-to-default value=${wipCap} ` +
106
135
  `previous=${JSON.stringify(previous)} changed=true`;
107
136
  appendAuditEntry(projectRoot, auditEntry);
108
137
  return [true, auditEntry];
109
138
  }
110
139
  policyRec.wipCap = wipCap;
111
- atomicWrite(path, data);
140
+ atomicWrite(projectRoot, path, data);
112
141
  const changed = previous !== wipCap;
113
142
  const auditEntry = `actor=${actor} field=plan.policy.wipCap value=${wipCap} previous=${JSON.stringify(previous)} ` +
114
143
  `changed=${changed ? "true" : "false"}`;
@@ -10,6 +10,11 @@ const EXCLUDE_DIRS = new Set([
10
10
  "dist",
11
11
  ".planning",
12
12
  "specs",
13
+ // Swarm / agent worktrees are not framework content; walking them
14
+ // during release Step 5 / task check is pure wall-clock waste (#2953, #1656).
15
+ ".deft-scratch",
16
+ // Legacy swarm worktree root name (pre-.deft-scratch layout).
17
+ "swarm-worktrees",
13
18
  ]);
14
19
  function shouldSkipPath(parts) {
15
20
  if (parts.some((p) => EXCLUDE_DIRS.has(p)))
@@ -1,8 +1,9 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { runningInsideDeftRepo } from "../doctor/paths.js";
4
4
  import { ALL_ATTRIBUTION_EVENT_NAMES } from "../events/attribution-constants.js";
5
- import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
5
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
6
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
6
7
  import { DEFAULT_EVENT_LOG, readEvents } from "../lifecycle/events.js";
7
8
  import { policyColonInvocation } from "../policy/policy-invocation.js";
8
9
  import { isValueFeedbackPathAllowed, resolveValueFeedback, } from "../policy/value-feedback.js";
@@ -230,12 +231,16 @@ function appendReadbackHistory(projectRoot, eventId, line, options = {}) {
230
231
  line,
231
232
  };
232
233
  try {
233
- assertWriteTargetSafe(projectRoot, path);
234
- mkdirSync(join(path, ".."), { recursive: true });
235
- appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
234
+ // #2980 wave D: product write sink routes through containedWrite.
235
+ containedWrite({
236
+ root: resolve(projectRoot),
237
+ target: path,
238
+ data: `${JSON.stringify(record)}\n`,
239
+ mode: "append",
240
+ });
236
241
  }
237
242
  catch (err) {
238
- if (err instanceof ProjectionContainmentError) {
243
+ if (err instanceof ProjectionContainmentError || err instanceof ContainedWriteError) {
239
244
  throw err;
240
245
  }
241
246
  // observability only
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
- import { basename, dirname } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { utcNowIso } from "../scope/vbrief-json.js";
4
5
  import { pythonJsonPretty } from "../vbrief-build/json.js";
5
6
  import { ACTIVE_FOLDER, ELIGIBLE_STATUSES_FOR_FLIP, formatEligibleStatusList, SOURCE_FOLDERS, TARGET_STATUS, } from "./constants.js";
@@ -150,15 +151,22 @@ export function activate(vbriefPath, options = {}) {
150
151
  "collision manually before re-running `task vbrief:activate`.",
151
152
  };
152
153
  }
154
+ const tmpBase = `${fileName}.tmp`;
153
155
  const tmp = `${dest}.tmp`;
154
156
  try {
155
- writeFileSync(tmp, pythonJsonPretty(payload), "utf8");
157
+ // #2980 wave D: product write sink routes through containedWrite.
158
+ containedWrite({
159
+ root: resolve(activeDir),
160
+ target: tmpBase,
161
+ data: pythonJsonPretty(payload),
162
+ mode: "create",
163
+ });
156
164
  renameSync(tmp, dest);
157
165
  }
158
166
  catch (err) {
159
167
  const e = err;
160
168
  try {
161
- unlinkSync(tmp);
169
+ rmSync(tmp, { force: true });
162
170
  }
163
171
  catch {
164
172
  /* best-effort cleanup */
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
6
  import { pythonJsonPretty } from "./json.js";
6
7
  import { ProjectDefinitionIOError } from "./types.js";
@@ -105,12 +106,19 @@ export function loadProjectDefinitionForMutation(projectRoot) {
105
106
  }
106
107
  /** Atomically write ``data`` to ``path`` as pretty-printed JSON. */
107
108
  export function atomicWriteProjectDefinition(path, data) {
108
- mkdirSync(dirname(path), { recursive: true });
109
+ // #2980 wave C: product write sink routes through containedWrite (temp under parent).
110
+ const dir = dirname(path);
111
+ mkdirSync(dir, { recursive: true });
109
112
  const payload = pythonJsonPretty(data).replace(/\n$/, "");
110
- const tmp = join(dirname(path), `${path.split(/[/\\]/).pop()}.${randomBytes(4).toString("hex")}.tmp`);
113
+ const body = payload.endsWith("\n") ? payload : `${payload}\n`;
114
+ const tmpName = `${basename(path)}.${randomBytes(4).toString("hex")}.tmp`;
115
+ const tmp = join(dir, tmpName);
111
116
  try {
112
- writeFileSync(tmp, payload.endsWith("\n") ? payload : `${payload}\n`, {
113
- encoding: "utf8",
117
+ containedWrite({
118
+ root: resolve(dir),
119
+ target: tmpName,
120
+ data: body,
121
+ mode: "create",
114
122
  });
115
123
  renameSync(tmp, path);
116
124
  }