@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,6 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { DEFAULT_PRODUCT_SIGNAL_SINK_REPO } from "../policy/product-signal.js";
5
6
  import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
6
7
  export const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
@@ -142,8 +143,15 @@ export function grantProductSignalConsent(options = {}) {
142
143
  sinkRepo,
143
144
  };
144
145
  const path = resolveProductSignalConsentPath(options);
145
- mkdirSync(dirname(path), { recursive: true });
146
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
146
+ // #2980 wave D: product write sink routes through containedWrite.
147
+ const dir = dirname(path);
148
+ mkdirSync(dir, { recursive: true });
149
+ containedWrite({
150
+ root: resolve(dir),
151
+ target: basename(path),
152
+ data: `${JSON.stringify(record, null, 2)}\n`,
153
+ mode: "replace",
154
+ });
147
155
  return record;
148
156
  }
149
157
  /** Revoke consent by setting revokedAt (#2693 D2). */
@@ -167,7 +175,15 @@ export function revokeProductSignalConsent(options = {}) {
167
175
  ...existing,
168
176
  revokedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
169
177
  };
170
- writeFileSync(path, `${JSON.stringify(revoked, null, 2)}\n`, "utf8");
178
+ // #2980 wave D: product write sink routes through containedWrite.
179
+ const dir = dirname(path);
180
+ mkdirSync(dir, { recursive: true });
181
+ containedWrite({
182
+ root: resolve(dir),
183
+ target: basename(path),
184
+ data: `${JSON.stringify(revoked, null, 2)}\n`,
185
+ mode: "replace",
186
+ });
171
187
  return true;
172
188
  }
173
189
  //# sourceMappingURL=consent.js.map
@@ -1,6 +1,7 @@
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
- import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
4
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
4
5
  import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
5
6
  import { resolveProjectRoot } from "../scope/project-context.js";
6
7
  import { resolveActorName } from "./actor-name.js";
@@ -15,17 +16,26 @@ import { bootstrapProductSignalSink } from "./sink-bootstrap.js";
15
16
  export const PRODUCT_SIGNAL_LAST_SUBMIT_REL = join(".deft-cache", "product-signal-last-submit.json");
16
17
  function recordLastSubmit(projectRoot, outcome, url) {
17
18
  const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
18
- assertWriteTargetSafe(projectRoot, path);
19
19
  try {
20
- mkdirSync(join(path, ".."), { recursive: true });
21
- appendFileSync(path, `${JSON.stringify({
22
- outcome,
23
- issueUrl: url,
24
- at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
25
- })}\n`, "utf8");
20
+ // #2980 wave D: product write sink routes through containedWrite.
21
+ const root = resolve(projectRoot);
22
+ containedWrite({
23
+ root,
24
+ target: path,
25
+ data: `${JSON.stringify({
26
+ outcome,
27
+ issueUrl: url,
28
+ at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
29
+ })}\n`,
30
+ mode: "append",
31
+ });
26
32
  }
27
- catch {
28
- // observability only
33
+ catch (err) {
34
+ // Containment refusals must fail closed (tests + product path) (#2807 / #2980).
35
+ if (err instanceof ContainedWriteError || err instanceof ProjectionContainmentError) {
36
+ throw err;
37
+ }
38
+ // other observability I/O failures only
29
39
  }
30
40
  }
31
41
  function readLastSubmitSummary(projectRoot) {
@@ -134,7 +144,7 @@ export async function submitProductSignal(options) {
134
144
  recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
135
145
  }
136
146
  catch (err) {
137
- if (err instanceof ProjectionContainmentError) {
147
+ if (err instanceof ProjectionContainmentError || err instanceof ContainedWriteError) {
138
148
  return {
139
149
  outcome: "error-config",
140
150
  exitCode: 2,
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Subprocess entry for sync callers (release pipeline, task build).
3
3
  * Top-level await keeps the process alive until the archive is written.
4
+ * Progress ticks go to stderr so pipeline callers can still capture the
5
+ * archive path on stdout (#2953).
4
6
  */
5
- import { buildArchive, selectFormat } from "./build-dist.js";
7
+ import { buildArchive, emitBuildProgress, selectFormat } from "./build-dist.js";
6
8
  const version = process.argv[2];
7
9
  const root = process.argv[3];
8
10
  if (!version || !root) {
@@ -11,7 +13,8 @@ if (!version || !root) {
11
13
  }
12
14
  const fmt = selectFormat(process.env.DEFT_BUILD_FORMAT);
13
15
  try {
14
- const out = await buildArchive(root, version, fmt);
16
+ process.stderr.write(`build-dist-runner: start version=${version} format=${fmt}\n`);
17
+ const out = await buildArchive(root, version, fmt, { onProgress: emitBuildProgress });
15
18
  process.stdout.write(`${out}\n`);
16
19
  }
17
20
  catch (err) {
@@ -8,7 +8,25 @@ export declare function iterSourceFiles(root: string, excludes?: ReadonlySet<str
8
8
  }>;
9
9
  export declare function selectFormat(arg: string | null | undefined): "tar" | "zip";
10
10
  export declare function outputPath(root: string, version: string, fmt: "tar" | "zip"): string;
11
- export declare function buildArchive(root: string, version: string, fmt: "tar" | "zip", extraExcludes?: readonly string[]): Promise<string>;
11
+ export type BuildProgress = {
12
+ readonly stage: string;
13
+ readonly detail?: string;
14
+ readonly current?: number;
15
+ readonly total?: number;
16
+ };
17
+ export type BuildArchiveOptions = {
18
+ readonly extraExcludes?: readonly string[];
19
+ /** Progress sink; default silent so unit tests stay quiet (#2953). */
20
+ readonly onProgress?: (p: BuildProgress) => void;
21
+ };
22
+ /**
23
+ * Emit human-readable stage progress for long release packaging runs (#2953).
24
+ * Writes to stderr so stdout stays the archive path for pipeline capture.
25
+ */
26
+ export declare function emitBuildProgress(progress: BuildProgress, stream?: {
27
+ write: (chunk: string) => unknown;
28
+ }): void;
29
+ export declare function buildArchive(root: string, version: string, fmt: "tar" | "zip", options?: readonly string[] | BuildArchiveOptions): Promise<string>;
12
30
  export declare function parseExtraExcludes(raw: string): string[];
13
31
  export declare function main(argv: readonly string[]): Promise<number>;
14
32
  //# sourceMappingURL=build-dist.d.ts.map
@@ -16,6 +16,9 @@ export const DEFAULT_EXCLUDES = new Set([
16
16
  ".ruff_cache",
17
17
  ".coverage",
18
18
  "coverage",
19
+ // Scratch worktrees are never shippable framework content (#2953).
20
+ ".deft-scratch",
21
+ "swarm-worktrees",
19
22
  ]);
20
23
  export const DEFAULT_EXCLUDED_PATH_PREFIXES = [
21
24
  "history/archive",
@@ -97,14 +100,46 @@ export function outputPath(root, version, fmt) {
97
100
  const suffix = fmt === "zip" ? "zip" : "tar.gz";
98
101
  return join(root, "dist", `deft-${version}.${suffix}`);
99
102
  }
100
- export async function buildArchive(root, version, fmt, extraExcludes = []) {
103
+ /**
104
+ * Emit human-readable stage progress for long release packaging runs (#2953).
105
+ * Writes to stderr so stdout stays the archive path for pipeline capture.
106
+ */
107
+ export function emitBuildProgress(progress, stream = process.stderr) {
108
+ const { stage, detail, current, total } = progress;
109
+ let line = `build-dist: ${stage}`;
110
+ if (typeof current === "number" && typeof total === "number" && total > 0) {
111
+ const pct = Math.min(100, Math.floor((current / total) * 100));
112
+ line += ` ${current}/${total} (${pct}%)`;
113
+ }
114
+ if (detail)
115
+ line += ` - ${detail}`;
116
+ stream.write(`${line}\n`);
117
+ }
118
+ function isExtraExcludesList(options) {
119
+ return Array.isArray(options);
120
+ }
121
+ function resolveBuildOptions(options = {}) {
122
+ // Backward-compatible: 4th arg may be a bare extra-excludes array (pre-#2953).
123
+ if (isExtraExcludesList(options)) {
124
+ return { extraExcludes: options, onProgress: () => { } };
125
+ }
126
+ return {
127
+ extraExcludes: options.extraExcludes ?? [],
128
+ onProgress: options.onProgress ?? (() => { }),
129
+ };
130
+ }
131
+ export async function buildArchive(root, version, fmt, options = {}) {
132
+ const { extraExcludes, onProgress } = resolveBuildOptions(options);
101
133
  const excludes = new Set([...DEFAULT_EXCLUDES, ...extraExcludes]);
102
134
  const output = outputPath(root, version, fmt);
103
135
  mkdirSync(dirname(output), { recursive: true });
104
136
  if (existsSync(output)) {
105
137
  unlinkSync(output);
106
138
  }
139
+ onProgress({ stage: "scan", detail: "enumerating source files" });
107
140
  const entries = iterSourceFiles(root, excludes);
141
+ const total = entries.length;
142
+ onProgress({ stage: "scan", detail: `found ${total} files`, current: total, total });
108
143
  await new Promise((resolvePromise, reject) => {
109
144
  const out = createWriteStream(output);
110
145
  const archive = fmt === "zip"
@@ -114,11 +149,21 @@ export async function buildArchive(root, version, fmt, extraExcludes = []) {
114
149
  out.on("error", (err) => reject(err));
115
150
  archive.on("error", (err) => reject(err));
116
151
  archive.pipe(out);
152
+ onProgress({ stage: "pack", detail: `format=${fmt}`, current: 0, total });
153
+ // Progress ticks every ~5% (or every 250 files) so long packs do not look hung.
154
+ const tickEvery = Math.max(1, Math.min(250, Math.ceil(total / 20)));
155
+ let i = 0;
117
156
  for (const { absPath, archiveRel } of entries) {
118
157
  archive.file(absPath, { name: `${ARCHIVE_ROOT}/${archiveRel}` });
158
+ i += 1;
159
+ if (i === total || i % tickEvery === 0) {
160
+ onProgress({ stage: "pack", current: i, total });
161
+ }
119
162
  }
163
+ onProgress({ stage: "finalize", detail: output });
120
164
  void archive.finalize();
121
165
  });
166
+ onProgress({ stage: "done", detail: output, current: total, total });
122
167
  return output;
123
168
  }
124
169
  export function parseExtraExcludes(raw) {
@@ -159,7 +204,10 @@ export async function main(argv) {
159
204
  }
160
205
  const fmt = selectFormat(fmtArg);
161
206
  try {
162
- const out = await buildArchive(projectRoot, version, fmt, parseExtraExcludes(extra));
207
+ const out = await buildArchive(projectRoot, version, fmt, {
208
+ extraExcludes: parseExtraExcludes(extra),
209
+ onProgress: emitBuildProgress,
210
+ });
163
211
  const printable = relative(projectRoot, out) || out;
164
212
  process.stdout.write(`Created ${printable}\n`);
165
213
  return 0;
@@ -79,16 +79,21 @@ export function runBuildNative(projectRoot, version, _seams = {}) {
79
79
  return [false, "build requires a release version"];
80
80
  }
81
81
  const env = { ...process.env, DEFT_RELEASE_VERSION: version };
82
+ // Inherit stderr so build-dist-runner progress ticks stream live during
83
+ // release Step 8; capture stdout only for the archive path (#2953).
82
84
  const result = spawnSync(process.execPath, [BUILD_DIST_RUNNER, version, projectRoot], {
83
85
  cwd: projectRoot,
84
86
  encoding: "utf8",
85
87
  env,
88
+ stdio: ["ignore", "pipe", "inherit"],
86
89
  });
87
90
  if (result.status !== 0) {
88
- const msg = (result.stderr ?? result.stdout ?? "").trim() || `exit ${result.status ?? 1}`;
91
+ const captured = (typeof result.stderr === "string" ? result.stderr : "") ||
92
+ (typeof result.stdout === "string" ? result.stdout : "");
93
+ const msg = captured.trim() || `exit ${result.status ?? 1}`;
89
94
  return [false, `build failed: ${msg}`];
90
95
  }
91
- const out = (result.stdout ?? "").trim();
96
+ const out = (typeof result.stdout === "string" ? result.stdout : "").trim();
92
97
  return [true, `build ran clean (DEFT_RELEASE_VERSION=${version}) -> ${out}`];
93
98
  }
94
99
  //# sourceMappingURL=native-steps.js.map
@@ -1,4 +1,17 @@
1
+ import { type EnvMap } from "../authz/closed-verb.js";
2
+ import type { ClosedVerbDecision, HumanOriginGrant } from "../authz/types.js";
1
3
  import type { PublishConfig, ReleasePublishSeams } from "./types.js";
2
4
  export declare function emit(label: string, status: string): void;
5
+ /**
6
+ * Fail-closed gate before draft→public (#1095 Wave 4).
7
+ * Allow only DEFT_ALLOW_RELEASE_PUBLISH=1 or a matching human-origin grant.
8
+ * Loads conventions/verb-classification.json from projectRoot when present.
9
+ * Test seam: pass grants / env via optional overrides on seams when present.
10
+ */
11
+ export declare function evaluateReleasePublishGate(version: string, projectRoot: string, options?: {
12
+ readonly grants?: readonly HumanOriginGrant[];
13
+ readonly env?: EnvMap;
14
+ readonly repo?: string | null;
15
+ }): ClosedVerbDecision;
3
16
  export declare function runPublish(config: PublishConfig, seams?: ReleasePublishSeams): number;
4
17
  //# sourceMappingURL=pipeline.d.ts.map
@@ -1,10 +1,34 @@
1
+ import { closedVerbEnvBypassKey, evaluateClosedVerb } from "../authz/closed-verb.js";
2
+ import { listActiveHumanGrants, loadAuthzState, markGrantUsed } from "../authz/store.js";
3
+ import { loadVerbClassification } from "../authz/verb-classification.js";
1
4
  import { EXIT_OK, EXIT_VIOLATION } from "../release/constants.js";
2
5
  import { editReleasePublish, viewRelease } from "./gh-api.js";
3
6
  export function emit(label, status) {
4
7
  process.stderr.write(`[publish] ${label}... ${status}\n`);
5
8
  }
9
+ /**
10
+ * Fail-closed gate before draft→public (#1095 Wave 4).
11
+ * Allow only DEFT_ALLOW_RELEASE_PUBLISH=1 or a matching human-origin grant.
12
+ * Loads conventions/verb-classification.json from projectRoot when present.
13
+ * Test seam: pass grants / env via optional overrides on seams when present.
14
+ */
15
+ export function evaluateReleasePublishGate(version, projectRoot, options = {}) {
16
+ // Production load of classification SoT (conventions/verb-classification.json).
17
+ const classification = loadVerbClassification(projectRoot);
18
+ const state = loadAuthzState(projectRoot);
19
+ const grants = options.grants ?? listActiveHumanGrants(projectRoot, state);
20
+ return evaluateClosedVerb({
21
+ verb: "release-publish",
22
+ target: version,
23
+ grants,
24
+ env: options.env ?? process.env,
25
+ projectRoot,
26
+ repo: options.repo ?? null,
27
+ classification,
28
+ });
29
+ }
6
30
  export function runPublish(config, seams = {}) {
7
- const { version, repo, dryRun } = config;
31
+ const { version, repo, dryRun, projectRoot } = config;
8
32
  const tag = `v${version}`;
9
33
  const viewLabel = `View ${tag} on ${repo}`;
10
34
  if (dryRun) {
@@ -26,6 +50,23 @@ export function runPublish(config, seams = {}) {
26
50
  return EXIT_OK;
27
51
  }
28
52
  emit(viewLabel, `OK (draft found at ${payload?.url ?? "<no url>"})`);
53
+ // Closed-verb gate: refuse draft→public without grant or env bypass (#1095).
54
+ const gateSeams = seams;
55
+ const gate = evaluateReleasePublishGate(version, projectRoot, {
56
+ grants: gateSeams.closedVerbGrants,
57
+ env: gateSeams.closedVerbEnv,
58
+ repo,
59
+ });
60
+ if (!gate.allowed) {
61
+ const bypassKey = gate.envBypassKey ??
62
+ closedVerbEnvBypassKey("release-publish", projectRoot) ??
63
+ "DEFT_ALLOW_RELEASE_PUBLISH";
64
+ emit(`Closed-verb gate release-publish ${tag}`, `FAIL (${gate.code}: ${gate.reason})`);
65
+ process.stderr.write(`[publish] denied code=${gate.code} — set ${bypassKey}=1 or ` +
66
+ `mint \`deft authz:grant -- --template release-publish --target ${version}\`\n`);
67
+ return EXIT_VIOLATION;
68
+ }
69
+ emit(`Closed-verb gate release-publish ${tag}`, `OK (${gate.code}${gate.humanApprovalRef !== null ? ` grant=${gate.humanApprovalRef}` : ""})`);
29
70
  const editLabel = `Edit ${tag} (--draft=false)`;
30
71
  const [ok, editReason] = editReleasePublish(version, repo, payload?.id ?? undefined, seams);
31
72
  if (!ok) {
@@ -40,6 +81,10 @@ export function runPublish(config, seams = {}) {
40
81
  return EXIT_VIOLATION;
41
82
  }
42
83
  emit(verifyLabel, `OK (${tag} is now public)`);
84
+ // Consume single-use grant after successful draft→public (#1095).
85
+ if (gate.humanApprovalRef !== null && gate.code === "closed-verb-allow") {
86
+ markGrantUsed(projectRoot, gate.humanApprovalRef);
87
+ }
43
88
  process.stderr.write(`Release ${tag} published successfully on ${repo}.\n`);
44
89
  return EXIT_OK;
45
90
  }
@@ -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);