@deftai/directive-core 0.88.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 (168) 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/task-cache/store.js +11 -7
  28. package/dist/capacity/backfill.js +10 -4
  29. package/dist/category-b-namespace/index.js +10 -4
  30. package/dist/codebase/default-extractor.js +3 -0
  31. package/dist/codebase/map.js +11 -4
  32. package/dist/doctor/doctor-state.js +28 -4
  33. package/dist/doctor/main.d.ts +10 -0
  34. package/dist/doctor/main.js +160 -0
  35. package/dist/doctor/types.d.ts +11 -0
  36. package/dist/escalation/actions.d.ts +63 -0
  37. package/dist/escalation/actions.js +137 -0
  38. package/dist/escalation/index.d.ts +8 -0
  39. package/dist/escalation/index.js +8 -0
  40. package/dist/escalation/paths.d.ts +3 -0
  41. package/dist/escalation/paths.js +10 -0
  42. package/dist/escalation/store.d.ts +17 -0
  43. package/dist/escalation/store.js +172 -0
  44. package/dist/escalation/types.d.ts +73 -0
  45. package/dist/escalation/types.js +43 -0
  46. package/dist/eval/crud-telemetry.js +30 -10
  47. package/dist/eval/health.js +22 -7
  48. package/dist/eval/readback.js +11 -10
  49. package/dist/eval/run.js +32 -16
  50. package/dist/events/attribution-enrichment.js +10 -4
  51. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  52. package/dist/finish-loop/directive-finish-loop.js +239 -0
  53. package/dist/finish-loop/grant-gate.d.ts +31 -0
  54. package/dist/finish-loop/grant-gate.js +169 -0
  55. package/dist/finish-loop/index.d.ts +11 -0
  56. package/dist/finish-loop/index.js +11 -0
  57. package/dist/finish-loop/main.d.ts +35 -0
  58. package/dist/finish-loop/main.js +365 -0
  59. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  60. package/dist/finish-loop/pr-finish-loop.js +217 -0
  61. package/dist/finish-loop/progress.d.ts +17 -0
  62. package/dist/finish-loop/progress.js +45 -0
  63. package/dist/finish-loop/queue.d.ts +16 -0
  64. package/dist/finish-loop/queue.js +65 -0
  65. package/dist/finish-loop/types.d.ts +52 -0
  66. package/dist/finish-loop/types.js +10 -0
  67. package/dist/fs/contained-write.d.ts +101 -0
  68. package/dist/fs/contained-write.js +281 -0
  69. package/dist/hooks/classify/classify.d.ts +10 -0
  70. package/dist/hooks/classify/classify.js +37 -0
  71. package/dist/hooks/classify/index.d.ts +14 -0
  72. package/dist/hooks/classify/index.js +13 -0
  73. package/dist/hooks/classify/paths.d.ts +22 -0
  74. package/dist/hooks/classify/paths.js +72 -0
  75. package/dist/hooks/classify/payload.d.ts +16 -0
  76. package/dist/hooks/classify/payload.js +45 -0
  77. package/dist/hooks/classify/stdin.d.ts +12 -0
  78. package/dist/hooks/classify/stdin.js +63 -0
  79. package/dist/hooks/classify/tool-name.d.ts +18 -0
  80. package/dist/hooks/classify/tool-name.js +85 -0
  81. package/dist/hooks/classify/types.d.ts +41 -0
  82. package/dist/hooks/classify/types.js +7 -0
  83. package/dist/hooks/dispatcher.d.ts +21 -18
  84. package/dist/hooks/dispatcher.js +212 -163
  85. package/dist/hooks/fixtures/cases.d.ts +44 -0
  86. package/dist/hooks/fixtures/cases.js +595 -0
  87. package/dist/hooks/fixtures/index.d.ts +2 -0
  88. package/dist/hooks/fixtures/index.js +2 -0
  89. package/dist/hooks/index.d.ts +2 -0
  90. package/dist/hooks/index.js +2 -0
  91. package/dist/index.d.ts +4 -0
  92. package/dist/index.js +5 -0
  93. package/dist/init-deposit/agent-hooks.js +27 -8
  94. package/dist/init-deposit/gitignore.js +11 -3
  95. package/dist/init-deposit/headless-manifest.js +12 -4
  96. package/dist/init-deposit/migrate.d.ts +1 -1
  97. package/dist/init-deposit/migrate.js +13 -3
  98. package/dist/init-deposit/prettierignore.js +10 -3
  99. package/dist/init-deposit/scaffold.js +35 -26
  100. package/dist/init-deposit/xbrief-projections.js +11 -4
  101. package/dist/intake/candidates-log.js +46 -35
  102. package/dist/intake/github-body-cli.d.ts +6 -0
  103. package/dist/intake/github-body-cli.js +17 -0
  104. package/dist/intake/github-body.d.ts +46 -3
  105. package/dist/intake/github-body.js +191 -20
  106. package/dist/intake/issue-emit.d.ts +1 -0
  107. package/dist/intake/issue-emit.js +41 -23
  108. package/dist/lifecycle/event-detect.js +12 -4
  109. package/dist/lifecycle/events.js +29 -20
  110. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  111. package/dist/packs/pack-render.js +19 -6
  112. package/dist/plan-sequence/store.js +11 -4
  113. package/dist/policy/hotfix-criteria.d.ts +79 -0
  114. package/dist/policy/hotfix-criteria.js +197 -0
  115. package/dist/policy/index.d.ts +4 -0
  116. package/dist/policy/index.js +31 -1
  117. package/dist/policy/intent-ceiling.d.ts +79 -0
  118. package/dist/policy/intent-ceiling.js +181 -0
  119. package/dist/policy/no-deft-directive.js +10 -3
  120. package/dist/policy/org-force-on-migration.js +9 -5
  121. package/dist/policy/require-human-merge.d.ts +75 -0
  122. package/dist/policy/require-human-merge.js +324 -0
  123. package/dist/policy/resolve.js +17 -6
  124. package/dist/policy/runtime-authority.d.ts +15 -2
  125. package/dist/policy/runtime-authority.js +23 -3
  126. package/dist/policy/write-fence.d.ts +78 -0
  127. package/dist/policy/write-fence.js +164 -0
  128. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  129. package/dist/pr-wait-mergeable/cascade.js +28 -0
  130. package/dist/preflight/evaluate.js +10 -0
  131. package/dist/product-signal/consent.js +21 -5
  132. package/dist/product-signal/submit.js +22 -12
  133. package/dist/release/build-dist-runner.js +5 -2
  134. package/dist/release/build-dist.d.ts +19 -1
  135. package/dist/release/build-dist.js +50 -2
  136. package/dist/release/native-steps.js +7 -2
  137. package/dist/release-publish/pipeline.d.ts +13 -0
  138. package/dist/release-publish/pipeline.js +46 -1
  139. package/dist/scope/audit-log.js +19 -24
  140. package/dist/scope/decompose.js +10 -5
  141. package/dist/scope/decomposed-refs.js +18 -3
  142. package/dist/scope/demote.js +9 -2
  143. package/dist/scope/undo.js +9 -2
  144. package/dist/session/release-availability.js +26 -6
  145. package/dist/session/ritual-sentinel.js +19 -12
  146. package/dist/session/session-start.js +7 -0
  147. package/dist/staleness-tickler/state.js +26 -6
  148. package/dist/swarm/launch.js +13 -3
  149. package/dist/swarm/routing.js +9 -3
  150. package/dist/task-surface/index.js +24 -9
  151. package/dist/triage/bootstrap/gitignore.js +53 -10
  152. package/dist/triage/cache-path.js +10 -3
  153. package/dist/triage/welcome/summary.js +11 -4
  154. package/dist/triage/welcome/writers.js +45 -16
  155. package/dist/validate-content/validate-links.js +5 -0
  156. package/dist/value/readback.js +11 -6
  157. package/dist/vbrief-activate/activate.js +12 -4
  158. package/dist/vbrief-build/project-definition-io.js +14 -6
  159. package/dist/verify-source/contained-writes.d.ts +59 -0
  160. package/dist/verify-source/contained-writes.js +272 -0
  161. package/dist/verify-source/index.d.ts +1 -0
  162. package/dist/verify-source/index.js +1 -0
  163. package/dist/verify-source/openclaw-tier1.js +14 -1
  164. package/dist/verify-source/verify-stubs.js +3 -0
  165. package/dist/xbrief-migrate/migrate-project.js +26 -12
  166. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  167. package/dist/xbrief-migrate/transforms.js +37 -14
  168. package/package.json +15 -3
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { resolveTriageCachePath } from "../triage/cache-path.js";
5
6
  import { utcNowIso } from "./vbrief-json.js";
6
7
  export class ScopeAuditLogError extends Error {
@@ -28,6 +29,12 @@ const DEMOTE_META_REQUIRED = [
28
29
  "demoted_from",
29
30
  ];
30
31
  let threadLock = false;
32
+ /** Ensure parent exists and return it as the containment root for log writes. */
33
+ function containmentRootForLog(logPath) {
34
+ const parent = dirname(logPath);
35
+ mkdirSync(parent, { recursive: true });
36
+ return resolve(parent);
37
+ }
31
38
  function replacerSortKeys(_key, value) {
32
39
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
33
40
  const sorted = {};
@@ -97,34 +104,16 @@ export function canonicalLogPath(projectRoot) {
97
104
  // Layout-aware (#1703): triage working-set under `.triage-cache/`.
98
105
  return resolveTriageCachePath(resolve(projectRoot), "scope-lifecycle.jsonl");
99
106
  }
100
- function withAppendLock(logPath, fn) {
101
- const lockPath = `${logPath}.lock`;
102
- mkdirSync(dirname(logPath), { recursive: true });
107
+ function withAppendLock(fn) {
103
108
  while (threadLock) {
104
109
  /* spin-wait for in-process serialization */
105
110
  }
106
111
  threadLock = true;
107
- let fd = null;
108
112
  try {
109
- fd = openSync(lockPath, "a+");
110
- if (readFileSync(lockPath).length === 0) {
111
- writeSync(fd, "\0");
112
- }
113
113
  fn();
114
114
  }
115
115
  finally {
116
116
  threadLock = false;
117
- if (fd !== null) {
118
- closeSync(fd);
119
- }
120
- try {
121
- if (existsSync(lockPath)) {
122
- unlinkSync(lockPath);
123
- }
124
- }
125
- catch {
126
- /* best-effort lock cleanup */
127
- }
128
117
  }
129
118
  }
130
119
  /** Validate and append an audit entry; returns decision_id. */
@@ -134,10 +123,16 @@ export function append(entry, logPath) {
134
123
  }
135
124
  validateEntry(entry);
136
125
  const logFile = resolve(logPath);
137
- mkdirSync(join(logFile, ".."), { recursive: true });
126
+ const root = containmentRootForLog(logFile);
138
127
  const line = JSON.stringify(entry, replacerSortKeys);
139
- withAppendLock(logFile, () => {
140
- writeFileSync(logFile, `${line}\n`, { encoding: "utf8", flag: "a" });
128
+ // #2980 wave B: product write sink routes through containedWrite.
129
+ withAppendLock(() => {
130
+ containedWrite({
131
+ root,
132
+ target: logFile,
133
+ data: `${line}\n`,
134
+ mode: "append",
135
+ });
141
136
  });
142
137
  return String(entry.decision_id);
143
138
  }
@@ -6,10 +6,11 @@
6
6
  * A caller supplies a draft with child story definitions; this module validates
7
7
  * that draft, writes the child story vBRIEFs, and updates the parent scope references.
8
8
  */
9
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
10
10
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
11
11
  import { referenceTypeMatches } from "@deftai/directive-types";
12
- import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
12
+ import { containedWrite } from "../fs/contained-write.js";
13
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
13
14
  import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
14
15
  import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
15
16
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
@@ -54,9 +55,13 @@ function loadJson(path) {
54
55
  return data;
55
56
  }
56
57
  function writeJson(projectRoot, path, data) {
57
- assertWriteTargetSafe(projectRoot, path);
58
- mkdirSync(dirname(path), { recursive: true });
59
- writeFileSync(path, formatBriefJson(data), "utf8");
58
+ // #2980 wave D: product write sink routes through containedWrite.
59
+ containedWrite({
60
+ root: resolve(projectRoot),
61
+ target: path,
62
+ data: formatBriefJson(data),
63
+ mode: "replace",
64
+ });
60
65
  }
61
66
  // ---------------------------------------------------------------------------
62
67
  // Story quality helpers (ported from _vbrief_story_quality.py)
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, relative, resolve } from "node:path";
3
3
  import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { formatBriefJson } from "./vbrief-json.js";
5
6
  import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "./vbrief-ref.js";
6
7
  function rewriteOnePlanRef(value, oldParentResolved, newParentRel, vbriefDir) {
@@ -53,7 +54,14 @@ function rewriteParentChildReference(parentPath, oldChildResolved, newChildRel,
53
54
  }
54
55
  if (changed) {
55
56
  try {
56
- writeFileSync(parentPath, formatBriefJson(parentData), "utf8");
57
+ // #2980 wave D: product write sink routes through containedWrite.
58
+ const dir = dirname(parentPath);
59
+ containedWrite({
60
+ root: resolve(dir),
61
+ target: parentPath,
62
+ data: formatBriefJson(parentData),
63
+ mode: "replace",
64
+ });
57
65
  }
58
66
  catch {
59
67
  return false;
@@ -127,7 +135,14 @@ function rewriteChildParentReference(childPath, oldParentResolved, newParentRel,
127
135
  }
128
136
  if (changed) {
129
137
  try {
130
- writeFileSync(childPath, formatBriefJson(childData), "utf8");
138
+ // #2980 wave D: product write sink routes through containedWrite.
139
+ const dir = dirname(childPath);
140
+ containedWrite({
141
+ root: resolve(dir),
142
+ target: childPath,
143
+ data: formatBriefJson(childData),
144
+ mode: "replace",
145
+ });
131
146
  }
132
147
  catch {
133
148
  return false;
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
5
6
  import { stripTrailingPathSeparators } from "../text/redos-safe.js";
@@ -85,7 +86,13 @@ export function demoteOne(filePath, projectRoot, reason, options = {}) {
85
86
  const timestamp = utcNowIso(now);
86
87
  planObj.status = TARGET_STATUS;
87
88
  planObj.updated = timestamp;
88
- writeFileSync(resolved, formatBriefJson(data), "utf8");
89
+ // #2980 wave D: product write sink routes through containedWrite.
90
+ containedWrite({
91
+ root: resolve(projectRoot),
92
+ target: resolved,
93
+ data: formatBriefJson(data),
94
+ mode: "replace",
95
+ });
89
96
  const vbriefRoot = dirname(dirname(resolved));
90
97
  const targetDir = join(vbriefRoot, TARGET_FOLDER);
91
98
  mkdirSync(targetDir, { recursive: true });
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
2
2
  import { basename, join, relative, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
4
5
  import { resolveLifecycleRoot } from "../layout/resolve.js";
5
6
  import { append, canonicalLogPath, newDecisionId, readAll } from "./audit-log.js";
@@ -185,7 +186,13 @@ function moveAndFlip(srcFile, destFolder, newStatus, timestamp, projectRoot) {
185
186
  const planObj = plan;
186
187
  planObj.status = newStatus;
187
188
  planObj.updated = timestamp;
188
- writeFileSync(srcFile, formatBriefJson(data), "utf8");
189
+ // #2980 wave D: product write sink routes through containedWrite.
190
+ containedWrite({
191
+ root: resolve(projectRoot),
192
+ target: srcFile,
193
+ data: formatBriefJson(data),
194
+ mode: "replace",
195
+ });
189
196
  mkdirSync(destFolder, { recursive: true });
190
197
  const destPath = join(destFolder, basename(srcFile));
191
198
  renameSync(srcFile, destPath);
@@ -1,9 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
5
5
  import { runningInsideDeftRepo } from "../doctor/paths.js";
6
6
  import { evaluateReleaseAvailability } from "../doctor/release-availability.js";
7
+ import { containedWrite } from "../fs/contained-write.js";
7
8
  import { resolveTriageCachePath } from "../triage/cache-path.js";
8
9
  const THROTTLE_MS = 24 * 60 * 60 * 1000;
9
10
  const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -56,10 +57,29 @@ function isThrottled(state, latestVersion, now) {
56
57
  return Number.isFinite(notifiedAt) && now.getTime() - notifiedAt < THROTTLE_MS;
57
58
  }
58
59
  function defaultWriteState(path, content) {
59
- mkdirSync(dirname(path), { recursive: true });
60
- const temporary = `${path}.${process.pid}.tmp`;
61
- writeFileSync(temporary, content, "utf8");
62
- renameSync(temporary, path);
60
+ const dir = dirname(path);
61
+ mkdirSync(dir, { recursive: true });
62
+ const tmpBase = `${basename(path)}.${process.pid}.tmp`;
63
+ const temporary = join(dir, tmpBase);
64
+ try {
65
+ // #2980 wave D: product write sink routes through containedWrite.
66
+ containedWrite({
67
+ root: resolve(dir),
68
+ target: tmpBase,
69
+ data: content,
70
+ mode: "create",
71
+ });
72
+ renameSync(temporary, path);
73
+ }
74
+ catch (err) {
75
+ try {
76
+ rmSync(temporary, { force: true });
77
+ }
78
+ catch {
79
+ /* best-effort cleanup */
80
+ }
81
+ throw err;
82
+ }
63
83
  }
64
84
  /**
65
85
  * Probe the public npm registry separately from doctor. Doctor stays offline
@@ -1,5 +1,6 @@
1
- import { closeSync, existsSync, fdatasyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, writeSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
4
5
  import { stableJson } from "./json.js";
5
6
  import { RITUAL_STATE_CONTRACT } from "./posture.js";
@@ -92,24 +93,30 @@ function validateSteps(raw, key) {
92
93
  return [steps, null];
93
94
  }
94
95
  function atomicWriteJson(targetPath, payload, prefix) {
95
- mkdirSync(join(targetPath, ".."), { recursive: true });
96
96
  const dir = join(targetPath, "..");
97
- const tmpName = join(dir, `${prefix}${process.pid}.json.tmp`);
98
- const fd = openSync(tmpName, "w");
97
+ mkdirSync(dir, { recursive: true });
98
+ const tmpBase = `${prefix}${process.pid}.json.tmp`;
99
+ const tmpName = join(dir, tmpBase);
100
+ const text = `${stableJson(payload, 2)}\n`;
99
101
  try {
100
- const text = `${stableJson(payload, 2)}\n`;
101
- writeSync(fd, text, undefined, "utf8");
102
+ // #2980 wave D: ritual-state product write routes through containedWrite.
103
+ containedWrite({
104
+ root: resolve(dir),
105
+ target: tmpBase,
106
+ data: text,
107
+ mode: "create",
108
+ });
109
+ renameSync(tmpName, targetPath);
110
+ }
111
+ catch (err) {
102
112
  try {
103
- fdatasyncSync(fd);
113
+ rmSync(tmpName, { force: true });
104
114
  }
105
115
  catch {
106
- // best-effort
116
+ /* best-effort cleanup */
107
117
  }
118
+ throw err;
108
119
  }
109
- finally {
110
- closeSync(fd);
111
- }
112
- renameSync(tmpName, targetPath);
113
120
  }
114
121
  export function readRitualState(projectRoot) {
115
122
  const stateFile = ritualStatePath(projectRoot);
@@ -5,6 +5,7 @@ import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposi
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
6
  import { disclosureLine } from "../policy/disclosure.js";
7
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";
8
9
  import { resolvePolicy } from "../policy/resolve.js";
9
10
  import { maybeFormatProductSignalConsentPrompt } from "../product-signal/consent-prompt.js";
10
11
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
@@ -319,6 +320,12 @@ export function runSessionStart(projectRoot, options = {}) {
319
320
  exitCode: ok ? 0 : 2,
320
321
  });
321
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
+ }
322
329
  const branchSync = defaultBranchSync(projectRoot, runGit);
323
330
  if (branchSync.warning) {
324
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