@deftai/directive-core 0.104.0 → 0.105.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 (225) hide show
  1. package/dist/authz/classify.js +526 -42
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/named-cause.d.ts +1 -1
  5. package/dist/check/named-cause.js +33 -3
  6. package/dist/codebase/default-extractor.d.ts +5 -0
  7. package/dist/codebase/default-extractor.js +7 -15
  8. package/dist/deposit/copy-tree.js +60 -0
  9. package/dist/deposit/python-free.js +26 -18
  10. package/dist/doctor/checks.d.ts +8 -0
  11. package/dist/doctor/checks.js +37 -25
  12. package/dist/doctor/doctor-state.d.ts +4 -0
  13. package/dist/doctor/doctor-state.js +7 -1
  14. package/dist/doctor/main.js +9 -19
  15. package/dist/doctor/types.d.ts +9 -0
  16. package/dist/fs/contained-write.d.ts +115 -1
  17. package/dist/fs/contained-write.js +241 -5
  18. package/dist/fs/mutation-ledger.d.ts +54 -0
  19. package/dist/fs/mutation-ledger.js +133 -0
  20. package/dist/fs/non-product-dirs.d.ts +79 -0
  21. package/dist/fs/non-product-dirs.js +96 -0
  22. package/dist/hooks/dispatcher.d.ts +3 -1
  23. package/dist/hooks/dispatcher.js +14 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/init-deposit/agent-hooks.js +17 -15
  27. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  28. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  29. package/dist/init-deposit/gitignore.js +9 -0
  30. package/dist/init-deposit/hygiene.d.ts +47 -11
  31. package/dist/init-deposit/hygiene.js +172 -37
  32. package/dist/init-deposit/legacy-detect.js +6 -2
  33. package/dist/init-deposit/refresh.d.ts +21 -1
  34. package/dist/init-deposit/refresh.js +106 -9
  35. package/dist/init-deposit/scaffold.js +28 -279
  36. package/dist/init-deposit/slash-deposit.js +13 -7
  37. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  38. package/dist/init-deposit/xbrief-projections.js +24 -11
  39. package/dist/intake/clause-derivation.d.ts +38 -1
  40. package/dist/intake/clause-derivation.js +211 -9
  41. package/dist/intake/issue-ingest.d.ts +3 -1
  42. package/dist/intake/issue-ingest.js +35 -6
  43. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  44. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  45. package/dist/literal-acceptance/capture.d.ts +10 -0
  46. package/dist/literal-acceptance/capture.js +224 -75
  47. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  48. package/dist/literal-acceptance/evaluate.js +70 -2
  49. package/dist/literal-acceptance/index.d.ts +3 -3
  50. package/dist/literal-acceptance/index.js +3 -3
  51. package/dist/literal-acceptance/safety.d.ts +41 -0
  52. package/dist/literal-acceptance/safety.js +126 -2
  53. package/dist/literal-acceptance/types.d.ts +8 -0
  54. package/dist/orphan-active/evaluate.d.ts +7 -1
  55. package/dist/orphan-active/evaluate.js +102 -23
  56. package/dist/platform/platform-capabilities.js +2 -3
  57. package/dist/policy/base-branch.d.ts +37 -0
  58. package/dist/policy/base-branch.js +114 -0
  59. package/dist/policy/branch-sync.d.ts +80 -0
  60. package/dist/policy/branch-sync.js +211 -0
  61. package/dist/policy/ceremony-dial-escalation.js +1 -0
  62. package/dist/policy/ceremony-dial.js +4 -14
  63. package/dist/policy/delivery-branch.d.ts +5 -3
  64. package/dist/policy/delivery-branch.js +7 -3
  65. package/dist/policy/file-size-thresholds.d.ts +24 -0
  66. package/dist/policy/file-size-thresholds.js +23 -0
  67. package/dist/policy/forge-outage-retry.d.ts +47 -0
  68. package/dist/policy/forge-outage-retry.js +157 -0
  69. package/dist/policy/index.d.ts +7 -0
  70. package/dist/policy/index.js +66 -7
  71. package/dist/policy/project-invariants.d.ts +50 -0
  72. package/dist/policy/project-invariants.js +205 -0
  73. package/dist/policy/sync-default.d.ts +129 -0
  74. package/dist/policy/sync-default.js +452 -0
  75. package/dist/policy/sync-max-files.d.ts +93 -0
  76. package/dist/policy/sync-max-files.js +192 -0
  77. package/dist/pr-monitor/absent-required.d.ts +11 -0
  78. package/dist/pr-monitor/absent-required.js +61 -0
  79. package/dist/pr-monitor/constants.d.ts +7 -0
  80. package/dist/pr-monitor/constants.js +7 -0
  81. package/dist/pr-monitor/index.d.ts +2 -1
  82. package/dist/pr-monitor/index.js +2 -1
  83. package/dist/pr-monitor/main.js +4 -0
  84. package/dist/pr-monitor/monitor.js +24 -1
  85. package/dist/pr-wait-mergeable/classify.js +6 -0
  86. package/dist/preflight/evaluate.d.ts +4 -0
  87. package/dist/preflight/evaluate.js +35 -1
  88. package/dist/preflight/index.d.ts +4 -0
  89. package/dist/preflight/index.js +2 -0
  90. package/dist/preflight/intended-placement.d.ts +62 -0
  91. package/dist/preflight/intended-placement.js +275 -0
  92. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  93. package/dist/preflight/project-invariants-gate.js +82 -0
  94. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  95. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  96. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  97. package/dist/product-first-done-gate/acceptance.js +79 -8
  98. package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
  99. package/dist/product-first-done-gate/empty-resolution.js +12 -1
  100. package/dist/product-first-done-gate/evaluate.d.ts +12 -0
  101. package/dist/product-first-done-gate/evaluate.js +254 -31
  102. package/dist/product-first-done-gate/index.d.ts +2 -0
  103. package/dist/product-first-done-gate/index.js +1 -0
  104. package/dist/release/build-dist.d.ts +101 -0
  105. package/dist/release/build-dist.js +428 -46
  106. package/dist/run-summary/emit.d.ts +16 -10
  107. package/dist/run-summary/emit.js +26 -15
  108. package/dist/run-summary/index.d.ts +3 -2
  109. package/dist/run-summary/index.js +2 -1
  110. package/dist/run-summary/session-id.d.ts +21 -0
  111. package/dist/run-summary/session-id.js +92 -0
  112. package/dist/run-summary/types.d.ts +29 -4
  113. package/dist/run-summary/types.js +1 -0
  114. package/dist/scope/acceptance-evidence.d.ts +19 -7
  115. package/dist/scope/acceptance-evidence.js +38 -11
  116. package/dist/scope/delivery-evidence.d.ts +3 -3
  117. package/dist/scope/delivery-evidence.js +22 -27
  118. package/dist/scope/index.d.ts +1 -0
  119. package/dist/scope/index.js +1 -0
  120. package/dist/scope/parent-lineage.js +7 -6
  121. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  122. package/dist/scope/project-invariant-coverage.js +322 -0
  123. package/dist/scope/transition.js +1 -1
  124. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  125. package/dist/scope-provenance/compare-intent.js +233 -0
  126. package/dist/scope-provenance/digest.d.ts +18 -1
  127. package/dist/scope-provenance/digest.js +16 -11
  128. package/dist/scope-provenance/evaluate.d.ts +5 -1
  129. package/dist/scope-provenance/evaluate.js +64 -4
  130. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  131. package/dist/scope-provenance/extract-intent.js +262 -0
  132. package/dist/scope-provenance/index.d.ts +7 -1
  133. package/dist/scope-provenance/index.js +7 -1
  134. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  135. package/dist/scope-provenance/intent-digest.js +33 -0
  136. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  137. package/dist/scope-provenance/intent-evaluate.js +149 -0
  138. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  139. package/dist/scope-provenance/json-tokenizer.js +225 -0
  140. package/dist/scope-provenance/known-machine.d.ts +38 -0
  141. package/dist/scope-provenance/known-machine.js +157 -0
  142. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  143. package/dist/scope-provenance/mint-artifacts.js +425 -0
  144. package/dist/session/ac-pass-banking.d.ts +4 -36
  145. package/dist/session/ac-pass-banking.js +30 -77
  146. package/dist/session/ac-pass-reuse.d.ts +53 -0
  147. package/dist/session/ac-pass-reuse.js +95 -0
  148. package/dist/session/git.js +19 -4
  149. package/dist/session/index.d.ts +4 -0
  150. package/dist/session/index.js +4 -0
  151. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  152. package/dist/session/occupancy-stress-worker.js +36 -0
  153. package/dist/session/occupancy.d.ts +97 -0
  154. package/dist/session/occupancy.js +422 -0
  155. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  156. package/dist/session/product-state-hash.d.ts +26 -0
  157. package/dist/session/product-state-hash.js +683 -0
  158. package/dist/session/session-ready.d.ts +2 -0
  159. package/dist/session/session-ready.js +52 -30
  160. package/dist/session/session-start.d.ts +7 -0
  161. package/dist/session/session-start.js +71 -7
  162. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  163. package/dist/session/verify-ac-session-cache.js +126 -0
  164. package/dist/session/verify-session-ritual.d.ts +5 -0
  165. package/dist/session/verify-session-ritual.js +9 -0
  166. package/dist/slash/openclaw-deposit.js +13 -9
  167. package/dist/slice/constants.d.ts +2 -2
  168. package/dist/slice/constants.js +2 -2
  169. package/dist/slice/existing.js +9 -3
  170. package/dist/slice/lock.d.ts +16 -1
  171. package/dist/slice/lock.js +137 -11
  172. package/dist/story-ready/evaluate.d.ts +2 -0
  173. package/dist/story-ready/evaluate.js +13 -0
  174. package/dist/swarm/complete-cohort.d.ts +3 -0
  175. package/dist/swarm/complete-cohort.js +59 -11
  176. package/dist/swarm/finalize-cohort.js +33 -2
  177. package/dist/swarm/launch.d.ts +22 -0
  178. package/dist/swarm/launch.js +155 -0
  179. package/dist/swarm/routing-set-cli.d.ts +11 -1
  180. package/dist/swarm/routing-set-cli.js +13 -3
  181. package/dist/swarm/routing-verify.d.ts +12 -0
  182. package/dist/swarm/routing-verify.js +59 -20
  183. package/dist/swarm/routing.d.ts +14 -0
  184. package/dist/swarm/routing.js +53 -3
  185. package/dist/telemetry-coverage/fake-trial.js +10 -0
  186. package/dist/telemetry-coverage/kinds.js +2 -0
  187. package/dist/triage/actions/index.js +4 -1
  188. package/dist/ts-check-lane/index.d.ts +3 -0
  189. package/dist/ts-check-lane/index.js +2 -0
  190. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  191. package/dist/ts-check-lane/progress-reporter.js +38 -0
  192. package/dist/ts-check-lane/progress.d.ts +32 -0
  193. package/dist/ts-check-lane/progress.js +57 -0
  194. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  195. package/dist/ts-check-lane/run-lane.js +8 -6
  196. package/dist/validate-content/validate-links.d.ts +8 -0
  197. package/dist/validate-content/validate-links.js +9 -15
  198. package/dist/value/feedback-file.js +12 -2
  199. package/dist/vbrief-build/index.d.ts +1 -0
  200. package/dist/vbrief-build/index.js +1 -0
  201. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  202. package/dist/vbrief-build/project-invariants-io.js +41 -0
  203. package/dist/vbrief-reconcile/graph.js +18 -1
  204. package/dist/vbrief-reconcile/index.d.ts +1 -1
  205. package/dist/vbrief-reconcile/index.js +1 -1
  206. package/dist/vbrief-reconcile/types.d.ts +13 -0
  207. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  208. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  209. package/dist/vbrief-validate/index.d.ts +1 -1
  210. package/dist/vbrief-validate/index.js +1 -1
  211. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  212. package/dist/vbrief-validate/plan-hooks.js +68 -0
  213. package/dist/verify-ac/evaluate.d.ts +7 -1
  214. package/dist/verify-ac/evaluate.js +34 -26
  215. package/dist/verify-ac/flag.d.ts +4 -0
  216. package/dist/verify-ac/flag.js +16 -0
  217. package/dist/verify-ac/index.d.ts +2 -2
  218. package/dist/verify-ac/index.js +2 -2
  219. package/dist/verify-source/contained-writes.js +2 -0
  220. package/dist/verify-source/verify-stubs.d.ts +15 -1
  221. package/dist/verify-source/verify-stubs.js +20 -16
  222. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  223. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  224. package/dist/xbrief-migrate/migrate-project.js +2 -5
  225. package/package.json +3 -3
@@ -1,32 +1,43 @@
1
- import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:fs";
1
+ import { spawnSync } from "node:child_process";
2
+ import { createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, statSync, unlinkSync, } from "node:fs";
2
3
  import { createRequire } from "node:module";
3
4
  import { platform } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { NON_PRODUCT_DIRS } from "../fs/non-product-dirs.js";
7
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
5
8
  const { ZipArchive, TarArchive } = createRequire(import.meta.url)("archiver");
6
- export const DEFAULT_EXCLUDES = new Set([
7
- ".git",
8
- "dist",
9
- "backup",
10
- "node_modules",
11
- "__pycache__",
12
- ".venv",
13
- "htmlcov",
14
- ".pytest_cache",
15
- ".mypy_cache",
16
- ".ruff_cache",
17
- ".coverage",
18
- "coverage",
19
- // Scratch worktrees are never shippable framework content (#2953).
20
- ".deft-scratch",
21
- "swarm-worktrees",
22
- ]);
9
+ /**
10
+ * Shared "not product source" core (#3487) plus the coverage artifacts only the
11
+ * release archive names. Scratch and agent worktrees are never shippable
12
+ * framework content (#2953). The archive set is git-tracked files (#3490); this
13
+ * denylist is defence in depth, including for a tracked path under a core name.
14
+ *
15
+ * Callers may still widen this per run via `extraExcludes` / `--exclude-extra`.
16
+ */
17
+ export const DEFAULT_EXCLUDES = new Set([...NON_PRODUCT_DIRS, "htmlcov", ".coverage", "coverage"]);
23
18
  export const DEFAULT_EXCLUDED_PATH_PREFIXES = [
24
19
  "history/archive",
25
20
  "vbrief/completed",
26
21
  "vbrief/cancelled",
27
22
  ];
23
+ /**
24
+ * Generated outputs that may be packed even when untracked. Empty by default:
25
+ * the release archive is a source distribution, so extra outputs must be named
26
+ * explicitly (#3490).
27
+ */
28
+ export const DEFAULT_GENERATED_ALLOWLIST = [];
28
29
  export const ARCHIVE_ROOT = "deft";
29
30
  export const CONTENT_PREFIX = "content/";
31
+ /** Raised when the resolved archive set contains an untracked surprise (#3490). */
32
+ export class UntrackedArchiveEntryError extends Error {
33
+ paths;
34
+ constructor(paths) {
35
+ const listed = paths.join(", ");
36
+ super(`build-dist: refusing to pack untracked files not on the generated-output allowlist: ${listed}`);
37
+ this.name = "UntrackedArchiveEntryError";
38
+ this.paths = paths;
39
+ }
40
+ }
30
41
  const VENDORED_TS_TEST_RE = /\.(test|spec)\.(c|m)?[jt]sx?$/i;
31
42
  function flattenContentPrefix(relPosix) {
32
43
  if (relPosix === "content")
@@ -46,6 +57,362 @@ function isVendoredTsTest(relPosix) {
46
57
  const basename = relPosix.split("/").pop() ?? "";
47
58
  return VENDORED_TS_TEST_RE.test(basename);
48
59
  }
60
+ function pathHasExcludedComponent(relPosix, excludes) {
61
+ return relPosix.split("/").some((part) => excludes.has(part));
62
+ }
63
+ function shouldSkipRel(relPosix, excludes, excludedPrefixes) {
64
+ return (pathHasExcludedComponent(relPosix, excludes) ||
65
+ matchesExcludedPrefix(relPosix, excludedPrefixes) ||
66
+ isVendoredTsTest(relPosix));
67
+ }
68
+ /** Private-use mark so archiver's string `name` can carry raw member bytes. */
69
+ export const ARCHIVE_BINARY_NAME_MARK = "\uF000";
70
+ export function flattenContentPrefixBytes(relBytes) {
71
+ const contentDir = Buffer.from("content", "utf8");
72
+ if (relBytes.equals(contentDir))
73
+ return Buffer.from(relBytes);
74
+ const prefix = Buffer.from(CONTENT_PREFIX, "utf8");
75
+ if (relBytes.length >= prefix.length && relBytes.subarray(0, prefix.length).equals(prefix)) {
76
+ return Buffer.from(relBytes.subarray(prefix.length));
77
+ }
78
+ return Buffer.from(relBytes);
79
+ }
80
+ export function isValidUtf8Buffer(buf) {
81
+ return Buffer.from(buf.toString("utf8"), "utf8").equals(buf);
82
+ }
83
+ export function archiveMemberBytes(archiveRelBytes) {
84
+ return Buffer.concat([Buffer.from(`${ARCHIVE_ROOT}/`, "utf8"), archiveRelBytes]);
85
+ }
86
+ /** Archiver `file()`/`append` name: UTF-8 text, or marked latin1 of raw bytes. */
87
+ export function archiverEntryName(memberBytes) {
88
+ if (isValidUtf8Buffer(memberBytes))
89
+ return memberBytes.toString("utf8");
90
+ return `${ARCHIVE_BINARY_NAME_MARK}${memberBytes.toString("latin1")}`;
91
+ }
92
+ function paxRawRecord(key, value) {
93
+ const keyword = Buffer.from(` ${key}=`, "utf8");
94
+ const nl = Buffer.from("\n", "utf8");
95
+ let digits = 1;
96
+ for (;;) {
97
+ const total = digits + keyword.length + value.length + nl.length;
98
+ const lenStr = String(total);
99
+ if (lenStr.length === digits) {
100
+ return Buffer.concat([Buffer.from(lenStr, "utf8"), keyword, value, nl]);
101
+ }
102
+ digits = lenStr.length;
103
+ }
104
+ }
105
+ function loadZipArchiveEntry() {
106
+ const archiverReq = createRequire(import.meta.url);
107
+ const zipReq = createRequire(archiverReq.resolve("archiver"));
108
+ const commons = createRequire(zipReq.resolve("zip-stream"))("compress-commons");
109
+ const cls = commons.ZipArchiveEntry ?? commons.default?.ZipArchiveEntry;
110
+ if (cls?.prototype?.setName)
111
+ return cls;
112
+ throw new Error("build-dist: cannot load ZipArchiveEntry for byte-preserving member names");
113
+ }
114
+ function loadTarHeaders() {
115
+ const archiverReq = createRequire(import.meta.url);
116
+ const zipReq = createRequire(archiverReq.resolve("archiver"));
117
+ const tarMain = zipReq.resolve("tar-stream");
118
+ const loaded = createRequire(tarMain)("./headers.js");
119
+ if (typeof loaded.encodePax !== "function") {
120
+ throw new Error("build-dist: cannot load tar-stream headers.encodePax");
121
+ }
122
+ return loaded;
123
+ }
124
+ /**
125
+ * Archiver only accepts a string `name` and UTF-8-encodes it. For invalid
126
+ * git path bytes we mark a latin1 string and write those bytes where
127
+ * zip-stream / tar-stream already copy a Buffer (`setName` / PAX path).
128
+ */
129
+ function installBytePreservingMemberNames() {
130
+ const ZipArchiveEntry = loadZipArchiveEntry();
131
+ const origSetName = ZipArchiveEntry.prototype.setName;
132
+ ZipArchiveEntry.prototype.setName = function (name, prependSlash = false) {
133
+ if (typeof name === "string" && name.charCodeAt(0) === ARCHIVE_BINARY_NAME_MARK.charCodeAt(0)) {
134
+ // Raw bytes, not UTF-8: leave the ZIP language-encoding flag off so
135
+ // extractors do not decode 0xFF-class names as UTF-8.
136
+ this.name = Buffer.from(name.slice(ARCHIVE_BINARY_NAME_MARK.length), "latin1");
137
+ return;
138
+ }
139
+ return origSetName.call(this, name, prependSlash);
140
+ };
141
+ const headers = loadTarHeaders();
142
+ const origEncodePax = headers.encodePax.bind(headers);
143
+ headers.encodePax = (opts) => {
144
+ if (typeof opts.name === "string" &&
145
+ opts.name.charCodeAt(0) === ARCHIVE_BINARY_NAME_MARK.charCodeAt(0)) {
146
+ const pathRec = paxRawRecord("path", Buffer.from(opts.name.slice(ARCHIVE_BINARY_NAME_MARK.length), "latin1"));
147
+ const rest = origEncodePax({ ...opts, name: undefined });
148
+ return Buffer.concat([pathRec, rest]);
149
+ }
150
+ return origEncodePax(opts);
151
+ };
152
+ return () => {
153
+ ZipArchiveEntry.prototype.setName = origSetName;
154
+ headers.encodePax = origEncodePax;
155
+ };
156
+ }
157
+ function addArchiveFile(archive, absPath, name) {
158
+ if (Buffer.isBuffer(absPath)) {
159
+ archive.append(createReadStream(absPath), { name });
160
+ return;
161
+ }
162
+ archive.file(absPath, { name });
163
+ }
164
+ const defaultArchiveFs = {
165
+ realpathSync: (path, options) => {
166
+ if (options?.encoding === "buffer") {
167
+ return realpathSync(path, { encoding: "buffer" });
168
+ }
169
+ return realpathSync(path);
170
+ },
171
+ lstatSync: (path) => lstatSync(path),
172
+ };
173
+ /** `-z` stdout is a NUL-separated byte stream; never UTF-8-decode it whole. */
174
+ export const GIT_LS_FILES_Z_ENCODING = null;
175
+ /** Decode one `ls-files -z` segment; keep non-UTF-8 bytes as latin1. */
176
+ export function decodeGitLsFilesPath(segment) {
177
+ const utf8 = segment.toString("utf8");
178
+ if (Buffer.from(utf8, "utf8").equals(segment))
179
+ return utf8;
180
+ return segment.toString("latin1");
181
+ }
182
+ /** Split a raw `git ls-files -z` Buffer on 0x00, keeping original path bytes. */
183
+ export function splitGitLsFilesZRecords(stdout) {
184
+ const paths = [];
185
+ let start = 0;
186
+ for (let i = 0; i < stdout.length; i += 1) {
187
+ if (stdout[i] === 0) {
188
+ if (i > start) {
189
+ const bytes = Buffer.from(stdout.subarray(start, i));
190
+ paths.push({ relPosix: decodeGitLsFilesPath(bytes), bytes });
191
+ }
192
+ start = i + 1;
193
+ }
194
+ }
195
+ if (start < stdout.length) {
196
+ const bytes = Buffer.from(stdout.subarray(start));
197
+ paths.push({ relPosix: decodeGitLsFilesPath(bytes), bytes });
198
+ }
199
+ return paths;
200
+ }
201
+ /** Split a raw `git ls-files -z` Buffer on 0x00, then decode each path. */
202
+ export function splitGitLsFilesZ(stdout) {
203
+ return splitGitLsFilesZRecords(stdout).map((p) => p.relPosix);
204
+ }
205
+ function spawnTextFromMaybeBuffer(value) {
206
+ if (Buffer.isBuffer(value))
207
+ return value.toString("utf8");
208
+ return typeof value === "string" ? value : "";
209
+ }
210
+ function bufferFromSpawnStdout(value) {
211
+ if (Buffer.isBuffer(value))
212
+ return value;
213
+ if (typeof value === "string")
214
+ return Buffer.from(value, "utf8");
215
+ return Buffer.alloc(0);
216
+ }
217
+ /**
218
+ * Return `git ls-files -z` paths with original bytes. Fail closed when git
219
+ * cannot enumerate tracked files -- a walk would pack host-local untracked
220
+ * state (#3490).
221
+ */
222
+ export function listGitTrackedPathRecords(root, spawn = spawnSync) {
223
+ // Pin to root/.git so a nested --root cannot walk up and pack a parent repo.
224
+ const result = spawn("git", ["-C", root, "--git-dir=.git", "--work-tree=.", "ls-files", "-z"], {
225
+ encoding: GIT_LS_FILES_Z_ENCODING,
226
+ timeout: 30_000,
227
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
228
+ stdio: ["ignore", "pipe", "pipe"],
229
+ });
230
+ if (result.status !== 0 || result.error) {
231
+ const detail = spawnTextFromMaybeBuffer(result.stderr).trim() ||
232
+ spawnTextFromMaybeBuffer(result.stdout).trim() ||
233
+ result.error?.message ||
234
+ "unknown error";
235
+ throw new Error(`git ls-files failed in ${root}: ${detail}`);
236
+ }
237
+ return splitGitLsFilesZRecords(bufferFromSpawnStdout(result.stdout));
238
+ }
239
+ /** Return `git ls-files -z` paths in POSIX form. */
240
+ export function listGitTrackedFiles(root, spawn = spawnSync) {
241
+ return listGitTrackedPathRecords(root, spawn).map((p) => p.relPosix);
242
+ }
243
+ /**
244
+ * Fail closed when any resolved archive path is untracked and not on the
245
+ * generated-output allowlist (#3490).
246
+ */
247
+ export function assertArchiveEntriesTrackedOrGenerated(root, rels, generatedAllowlist = DEFAULT_GENERATED_ALLOWLIST, spawn = spawnSync) {
248
+ const tracked = new Set(listGitTrackedFiles(root, spawn));
249
+ const allowed = new Set(generatedAllowlist);
250
+ const surprise = rels.filter((rel) => !tracked.has(rel) && !allowed.has(rel));
251
+ if (surprise.length > 0) {
252
+ throw new UntrackedArchiveEntryError(surprise);
253
+ }
254
+ }
255
+ function isInsideRoot(root, absPath) {
256
+ const rel = relative(resolve(root), absPath);
257
+ return !rel.startsWith("..") && !isAbsolute(rel);
258
+ }
259
+ /** Canonicalize the supplied archive root once per resolve (#3490 review). */
260
+ function canonicalArchiveRoot(root, fs) {
261
+ try {
262
+ const real = fs.realpathSync(root, { encoding: "buffer" });
263
+ if (Buffer.isBuffer(real)) {
264
+ return { text: real.toString("utf8"), bytes: real };
265
+ }
266
+ return { text: real, bytes: Buffer.from(real, "utf8") };
267
+ }
268
+ catch {
269
+ throw new Error(`build-dist: cannot resolve archive root: ${root}`);
270
+ }
271
+ }
272
+ /** Join canonical root bytes with original git path bytes; convert POSIX `/` to OS sep. */
273
+ export function joinRootAndRelBytes(rootBytes, relBytes) {
274
+ const sepByte = sep.charCodeAt(0);
275
+ const converted = Buffer.from(relBytes);
276
+ if (sep !== "/") {
277
+ for (let i = 0; i < converted.length; i += 1) {
278
+ if (converted[i] === 0x2f)
279
+ converted[i] = sepByte;
280
+ }
281
+ }
282
+ if (converted.length === 0)
283
+ return rootBytes;
284
+ const rootEndsSep = rootBytes.length > 0 && rootBytes[rootBytes.length - 1] === sepByte;
285
+ const relStartsSep = converted[0] === sepByte;
286
+ if (rootEndsSep && relStartsSep)
287
+ return Buffer.concat([rootBytes, converted.subarray(1)]);
288
+ if (rootEndsSep || relStartsSep)
289
+ return Buffer.concat([rootBytes, converted]);
290
+ return Buffer.concat([rootBytes, Buffer.from([sepByte]), converted]);
291
+ }
292
+ /**
293
+ * String path when the `-z` segment is valid UTF-8; otherwise a Buffer that
294
+ * keeps the original filename bytes for fs lookup (#3490 residual).
295
+ */
296
+ export function fsLookupPath(root, relPosix, bytes, rootBytes) {
297
+ const contained = containedAbsPath(root, relPosix);
298
+ if (contained === null)
299
+ return null;
300
+ if (Buffer.from(relPosix, "utf8").equals(bytes))
301
+ return contained;
302
+ return joinRootAndRelBytes(rootBytes ?? Buffer.from(resolve(root), "utf8"), bytes);
303
+ }
304
+ function isInsideResolved(canonicalRoot, canonicalBytes, real) {
305
+ if (typeof real === "string")
306
+ return isInsideRoot(canonicalRoot, real);
307
+ const sepByte = sep.charCodeAt(0);
308
+ if (real.equals(canonicalBytes))
309
+ return true;
310
+ const prefix = canonicalBytes.length > 0 && canonicalBytes[canonicalBytes.length - 1] === sepByte
311
+ ? canonicalBytes
312
+ : Buffer.concat([canonicalBytes, Buffer.from([sepByte])]);
313
+ if (real.length < prefix.length)
314
+ return false;
315
+ if (real.subarray(0, prefix.length).equals(prefix))
316
+ return true;
317
+ if (process.platform === "win32") {
318
+ return (real.subarray(0, prefix.length).toString("utf8").toLowerCase() ===
319
+ prefix.toString("utf8").toLowerCase());
320
+ }
321
+ return false;
322
+ }
323
+ /** Resolve a POSIX rel path under root, or null if it escapes (#3490 review). */
324
+ export function containedAbsPath(root, relPosix) {
325
+ if (relPosix.includes("\0"))
326
+ return null;
327
+ // Git index paths are POSIX (`/` only). A literal `\` is a filename byte
328
+ // (gitglossary "path"); splitting on `\` would pack `foo\bar` as nested
329
+ // path segments and could escape the root. Node resolve still fail-closes
330
+ // OS-separator `..\` via isInsideRoot.
331
+ const parts = relPosix.split("/");
332
+ if (parts.some((part) => part === ".."))
333
+ return null;
334
+ const absPath = resolve(root, ...parts.filter((part) => part.length > 0 && part !== "."));
335
+ if (!isInsideRoot(root, absPath))
336
+ return null;
337
+ return absPath;
338
+ }
339
+ function toArchiveEntry(root, canonicalRoot, relPosix, bytes, fs) {
340
+ const lookup = fsLookupPath(root, relPosix, bytes, canonicalRoot.bytes);
341
+ if (lookup === null)
342
+ return null;
343
+ let real;
344
+ try {
345
+ // realpath resolves leaf and ancestor symlinks before the file-type check.
346
+ // Buffer lookup keeps invalid filename bytes; a string would re-encode UTF-8.
347
+ real = Buffer.isBuffer(lookup)
348
+ ? fs.realpathSync(lookup, { encoding: "buffer" })
349
+ : fs.realpathSync(lookup);
350
+ }
351
+ catch {
352
+ return null;
353
+ }
354
+ // Compare against the canonical root so a symlink *to* the checkout is
355
+ // inside, while a symlink *out of* the tree still throws.
356
+ if (!isInsideResolved(canonicalRoot.text, canonicalRoot.bytes, real)) {
357
+ throw new Error(`build-dist: path resolves outside the archive root: ${relPosix}`);
358
+ }
359
+ let st;
360
+ try {
361
+ st = fs.lstatSync(real);
362
+ }
363
+ catch {
364
+ return null;
365
+ }
366
+ if (st == null || !st.isFile())
367
+ return null;
368
+ return {
369
+ absPath: real,
370
+ archiveRel: flattenContentPrefix(relPosix),
371
+ archiveRelBytes: flattenContentPrefixBytes(bytes),
372
+ };
373
+ }
374
+ /**
375
+ * Derive the archive file set from git-tracked paths plus an explicit
376
+ * generated-output allowlist. The basename denylist remains defence in depth.
377
+ */
378
+ export function resolveArchiveEntries(root, options = {}) {
379
+ const excludes = new Set([...DEFAULT_EXCLUDES, ...(options.extraExcludes ?? [])]);
380
+ const excludedPrefixes = options.excludedPrefixes ?? DEFAULT_EXCLUDED_PATH_PREFIXES;
381
+ const generatedAllowlist = options.generatedAllowlist ?? DEFAULT_GENERATED_ALLOWLIST;
382
+ const spawn = options.spawn ?? spawnSync;
383
+ const fs = options.fs ?? defaultArchiveFs;
384
+ const canonicalRoot = canonicalArchiveRoot(root, fs);
385
+ const tracked = listGitTrackedPathRecords(root, spawn);
386
+ const entries = [];
387
+ const sourceRels = [];
388
+ const seen = new Set();
389
+ for (const { relPosix, bytes } of tracked) {
390
+ if (shouldSkipRel(relPosix, excludes, excludedPrefixes))
391
+ continue;
392
+ const entry = toArchiveEntry(root, canonicalRoot, relPosix, bytes, fs);
393
+ if (entry === null)
394
+ continue;
395
+ entries.push(entry);
396
+ sourceRels.push(relPosix);
397
+ seen.add(relPosix);
398
+ }
399
+ for (const relPosix of generatedAllowlist) {
400
+ if (seen.has(relPosix))
401
+ continue;
402
+ if (containedAbsPath(root, relPosix) === null) {
403
+ throw new Error(`build-dist: generated allowlist path escapes the archive root: ${relPosix}`);
404
+ }
405
+ const entry = toArchiveEntry(root, canonicalRoot, relPosix, Buffer.from(relPosix, "utf8"), fs);
406
+ if (entry === null)
407
+ continue;
408
+ entries.push(entry);
409
+ sourceRels.push(relPosix);
410
+ seen.add(relPosix);
411
+ }
412
+ assertArchiveEntriesTrackedOrGenerated(root, sourceRels, generatedAllowlist, spawn);
413
+ entries.sort((a, b) => a.archiveRel.localeCompare(b.archiveRel));
414
+ return entries;
415
+ }
49
416
  export function iterSourceFiles(root, excludes = DEFAULT_EXCLUDES, excludedPrefixes = DEFAULT_EXCLUDED_PATH_PREFIXES) {
50
417
  const entries = [];
51
418
  const walk = (dir) => {
@@ -121,48 +488,63 @@ function isExtraExcludesList(options) {
121
488
  function resolveBuildOptions(options = {}) {
122
489
  // Backward-compatible: 4th arg may be a bare extra-excludes array (pre-#2953).
123
490
  if (isExtraExcludesList(options)) {
124
- return { extraExcludes: options, onProgress: () => { } };
491
+ return {
492
+ extraExcludes: options,
493
+ generatedAllowlist: DEFAULT_GENERATED_ALLOWLIST,
494
+ onProgress: () => { },
495
+ spawn: undefined,
496
+ fs: undefined,
497
+ };
125
498
  }
126
499
  return {
127
500
  extraExcludes: options.extraExcludes ?? [],
501
+ generatedAllowlist: options.generatedAllowlist ?? DEFAULT_GENERATED_ALLOWLIST,
128
502
  onProgress: options.onProgress ?? (() => { }),
503
+ spawn: options.spawn,
504
+ fs: options.fs,
129
505
  };
130
506
  }
131
507
  export async function buildArchive(root, version, fmt, options = {}) {
132
- const { extraExcludes, onProgress } = resolveBuildOptions(options);
133
- const excludes = new Set([...DEFAULT_EXCLUDES, ...extraExcludes]);
508
+ const { extraExcludes, generatedAllowlist, onProgress, spawn, fs } = resolveBuildOptions(options);
134
509
  const output = outputPath(root, version, fmt);
135
510
  mkdirSync(dirname(output), { recursive: true });
136
511
  if (existsSync(output)) {
137
512
  unlinkSync(output);
138
513
  }
139
- onProgress({ stage: "scan", detail: "enumerating source files" });
140
- const entries = iterSourceFiles(root, excludes);
514
+ onProgress({ stage: "scan", detail: "enumerating tracked source files" });
515
+ const entries = resolveArchiveEntries(root, { extraExcludes, generatedAllowlist, spawn, fs });
141
516
  const total = entries.length;
142
517
  onProgress({ stage: "scan", detail: `found ${total} files`, current: total, total });
143
- await new Promise((resolvePromise, reject) => {
144
- const out = createWriteStream(output);
145
- const archive = fmt === "zip"
146
- ? new ZipArchive({ zlib: { level: 9 } })
147
- : new TarArchive({ gzip: true, gzipOptions: { level: 9 } });
148
- out.on("close", () => resolvePromise());
149
- out.on("error", (err) => reject(err));
150
- archive.on("error", (err) => reject(err));
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;
156
- for (const { absPath, archiveRel } of entries) {
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 });
518
+ const uninstallNameHook = installBytePreservingMemberNames();
519
+ try {
520
+ await new Promise((resolvePromise, reject) => {
521
+ const out = createWriteStream(output);
522
+ const archive = fmt === "zip"
523
+ ? new ZipArchive({ zlib: { level: 9 } })
524
+ : new TarArchive({ gzip: true, gzipOptions: { level: 9 } });
525
+ out.on("close", () => resolvePromise());
526
+ out.on("error", (err) => reject(err));
527
+ archive.on("error", (err) => reject(err));
528
+ archive.pipe(out);
529
+ onProgress({ stage: "pack", detail: `format=${fmt}`, current: 0, total });
530
+ // Progress ticks every ~5% (or every 250 files) so long packs do not look hung.
531
+ const tickEvery = Math.max(1, Math.min(250, Math.ceil(total / 20)));
532
+ let i = 0;
533
+ for (const { absPath, archiveRel, archiveRelBytes } of entries) {
534
+ const memberBytes = archiveMemberBytes(archiveRelBytes ?? Buffer.from(archiveRel, "utf8"));
535
+ addArchiveFile(archive, absPath, archiverEntryName(memberBytes));
536
+ i += 1;
537
+ if (i === total || i % tickEvery === 0) {
538
+ onProgress({ stage: "pack", current: i, total });
539
+ }
161
540
  }
162
- }
163
- onProgress({ stage: "finalize", detail: output });
164
- void archive.finalize();
165
- });
541
+ onProgress({ stage: "finalize", detail: output });
542
+ void archive.finalize();
543
+ });
544
+ }
545
+ finally {
546
+ uninstallNameHook();
547
+ }
166
548
  onProgress({ stage: "done", detail: output, current: total, total });
167
549
  return output;
168
550
  }
@@ -6,10 +6,12 @@
6
6
  * Symlink destinations are refused (no-follow / containment) — fail-open.
7
7
  */
8
8
  import { type ResolveRunSummaryDestinationOptions } from "./path.js";
9
- import { type AcceptanceRunSummaryPayload, type AcceptanceStampRunSummaryPayload, type CheckInvocationRunSummaryPayload, type DialEscalationEvaluationRunSummaryPayload, type DialTransitionRunSummaryPayload, type RunSummaryDestination, type RunSummaryEventKind, type RunSummaryLine, type RunSummaryPayload, type SessionStartRunSummaryPayload, type ToolTurnDenominatorRunSummaryPayload, type VerificationRunSummaryPayload } from "./types.js";
9
+ import { type AcceptanceRunSummaryPayload, type AcceptanceStampRunSummaryPayload, type AcPassBankRunSummaryPayload, type CheckInvocationRunSummaryPayload, type DialEscalationEvaluationRunSummaryPayload, type DialTransitionRunSummaryPayload, type RunSummaryDestination, type RunSummaryEventKind, type RunSummaryLine, type RunSummaryPayload, type SessionStartRunSummaryPayload, type ToolTurnDenominatorRunSummaryPayload, type ToolTurnDenominatorSource, type VerificationRunSummaryPayload } from "./types.js";
10
10
  export interface RunSummaryEmitterOptions extends ResolveRunSummaryDestinationOptions {
11
11
  readonly projectRoot: string;
12
- readonly sessionId: string;
12
+ readonly sessionId?: string | null;
13
+ /** Optional envelope label; not a closed union (#3399). */
14
+ readonly component?: string;
13
15
  readonly frameworkVersion?: string;
14
16
  /** Clock seam for tests. */
15
17
  readonly now?: () => Date;
@@ -52,6 +54,7 @@ export declare class RunSummaryEmitter {
52
54
  private warned;
53
55
  private readonly projectRoot;
54
56
  private readonly sessionId;
57
+ private readonly component?;
55
58
  private readonly frameworkVersion;
56
59
  private readonly now;
57
60
  private readonly destination;
@@ -70,12 +73,13 @@ export declare class RunSummaryEmitter {
70
73
  emitVerification(payload: VerificationRunSummaryPayload): EmitRunSummaryResult;
71
74
  emitAcceptance(payload: AcceptanceRunSummaryPayload): EmitRunSummaryResult;
72
75
  emitAcceptanceStamp(payload: AcceptanceStampRunSummaryPayload): EmitRunSummaryResult;
76
+ emitAcPassBank(payload: AcPassBankRunSummaryPayload): EmitRunSummaryResult;
73
77
  /** Emit the harness-supplied denominator when DEFT_TOTAL_TOOL_TURNS is set. */
74
78
  emitKnownToolTurnDenominator(): EmitRunSummaryResult;
75
79
  /**
76
- * Always emit a session denominator when the destination is live (#3356).
80
+ * Emit a session denominator only when a host/harness value is known (#3399).
77
81
  * `emitKnownToolTurnDenominator` stays silent unless DEFT_TOTAL_TOOL_TURNS is
78
- * set; this caller records host planned turns or the session:start CLI floor.
82
+ * set; this caller also records DEFT_MAX_TURNS / host maxTurns.
79
83
  */
80
84
  emitSessionToolTurnDenominator(hostMaxTurns?: number | null): EmitRunSummaryResult;
81
85
  }
@@ -83,16 +87,18 @@ export declare class RunSummaryEmitter {
83
87
  export declare function readEnvToolTurnDenominator(env: NodeJS.ProcessEnv): number | undefined;
84
88
  /** Canonical host planned-turn budget recorded at session:start (#3356). */
85
89
  export declare const ENV_MAX_TURNS_DENOMINATOR = "DEFT_MAX_TURNS";
86
- /** session:start is one CLI invocation when no host/harness count is known (#3356). */
87
- export declare const SESSION_START_CLI_INVOCATION_DENOMINATOR: 1;
90
+ export interface ResolvedSessionToolTurnDenominator {
91
+ readonly total_tool_turns: number;
92
+ readonly denominator_source: ToolTurnDenominatorSource;
93
+ }
88
94
  /**
89
- * Resolve the session tool/turn denominator (#3356).
95
+ * Resolve the session tool/turn denominator (#3399).
90
96
  *
91
97
  * Prefer harness actuals (`DEFT_TOTAL_TOOL_TURNS`), then a host planned-turn
92
- * budget (`DEFT_MAX_TURNS` or `hostMaxTurns` from the session:start descriptor),
93
- * else this CLI invocation (1). An emitted proxy beats a perfect unused kind.
98
+ * budget (`DEFT_MAX_TURNS` or `hostMaxTurns`). Unset undefined (no event).
99
+ * Share is unevaluable without a host/harness value.
94
100
  */
95
- export declare function resolveSessionToolTurnDenominator(env: NodeJS.ProcessEnv, hostMaxTurns?: number | null): number;
101
+ export declare function resolveSessionToolTurnDenominator(env: NodeJS.ProcessEnv, hostMaxTurns?: number | null): ResolvedSessionToolTurnDenominator | undefined;
96
102
  /**
97
103
  * One-shot helper for call sites that do not hold an emitter (e.g. dial escalate).
98
104
  * Still fail-open. File destinations seed seq from the existing JSONL line