@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,6 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { assertProjectionContained } from "../../fs/projection-containment.js";
3
+ import { containedWrite } from "../../fs/contained-write.js";
4
4
  import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
5
5
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
6
6
  import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
@@ -12,22 +12,51 @@ function utcIso() {
12
12
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
13
13
  }
14
14
  export function appendAuditEntry(projectRoot, entry) {
15
- const logPath = join(resolve(projectRoot), AUDIT_LOG_REL_PATH);
16
- assertProjectionContained(projectRoot, logPath);
17
- mkdirSync(dirname(logPath), { recursive: true });
18
- const line = `${utcIso()} ${entry}\n`;
15
+ const root = resolve(projectRoot);
16
+ const logPath = join(root, AUDIT_LOG_REL_PATH);
17
+ // #2980 wave D: product write sink routes through containedWrite.
18
+ mkdirSync(root, { recursive: true });
19
19
  if (!existsSync(logPath)) {
20
- writeFileSync(logPath, "# meta/policy-changes.log -- audit trail for PROJECT-DEFINITION plan.policy.* mutations (#746 / #1143)\n", "utf8");
20
+ containedWrite({
21
+ root,
22
+ target: AUDIT_LOG_REL_PATH,
23
+ data: "# meta/policy-changes.log -- audit trail for PROJECT-DEFINITION plan.policy.* mutations (#746 / #1143)\n",
24
+ mode: "create",
25
+ });
21
26
  }
22
- appendFileSync(logPath, line, "utf8");
27
+ containedWrite({
28
+ root,
29
+ target: AUDIT_LOG_REL_PATH,
30
+ data: `${utcIso()} ${entry}\n`,
31
+ mode: "append",
32
+ });
23
33
  return logPath;
24
34
  }
25
- function atomicWrite(path, data) {
26
- mkdirSync(dirname(path), { recursive: true });
35
+ function atomicWrite(_projectRoot, path, data) {
36
+ const dir = dirname(path);
37
+ mkdirSync(dir, { recursive: true });
27
38
  const payload = `${JSON.stringify(data, null, 2)}\n`;
28
- const tmp = join(dirname(path), `.${Date.now()}.tmp`);
29
- writeFileSync(tmp, payload, "utf8");
30
- renameSync(tmp, path);
39
+ const tmpName = `.${Date.now()}.tmp`;
40
+ const tmp = join(dir, tmpName);
41
+ try {
42
+ // #2980 wave D: temp payload write uses containedWrite under the parent dir.
43
+ containedWrite({
44
+ root: resolve(dir),
45
+ target: tmpName,
46
+ data: payload,
47
+ mode: "create",
48
+ });
49
+ renameSync(tmp, path);
50
+ }
51
+ catch (err) {
52
+ try {
53
+ rmSync(tmp, { force: true });
54
+ }
55
+ catch {
56
+ /* best-effort cleanup */
57
+ }
58
+ throw err;
59
+ }
31
60
  }
32
61
  export function writeTriageScope(projectRoot, rules, options = { presetLabel: "custom" }) {
33
62
  // Serialise read-modify-write + audit append under the shared lock (#1260).
@@ -54,7 +83,7 @@ export function writeTriageScope(projectRoot, rules, options = { presetLabel: "c
54
83
  const policyRec = policy;
55
84
  const previous = policyRec.triageScope;
56
85
  policyRec.triageScope = rules;
57
- atomicWrite(path, data);
86
+ atomicWrite(projectRoot, path, data);
58
87
  const changed = JSON.stringify(previous) !== JSON.stringify(rules);
59
88
  const actor = options.actor ?? WELCOME_AUDIT_TAG;
60
89
  const auditEntry = [
@@ -101,14 +130,14 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
101
130
  }
102
131
  if (previous !== undefined && wipCap === DEFAULT_WIP_CAP) {
103
132
  delete policyRec.wipCap;
104
- atomicWrite(path, data);
133
+ atomicWrite(projectRoot, path, data);
105
134
  const auditEntry = `actor=${actor} field=plan.policy.wipCap action=cleared-to-default value=${wipCap} ` +
106
135
  `previous=${JSON.stringify(previous)} changed=true`;
107
136
  appendAuditEntry(projectRoot, auditEntry);
108
137
  return [true, auditEntry];
109
138
  }
110
139
  policyRec.wipCap = wipCap;
111
- atomicWrite(path, data);
140
+ atomicWrite(projectRoot, path, data);
112
141
  const changed = previous !== wipCap;
113
142
  const auditEntry = `actor=${actor} field=plan.policy.wipCap value=${wipCap} previous=${JSON.stringify(previous)} ` +
114
143
  `changed=${changed ? "true" : "false"}`;
@@ -10,6 +10,11 @@ const EXCLUDE_DIRS = new Set([
10
10
  "dist",
11
11
  ".planning",
12
12
  "specs",
13
+ // Swarm / agent worktrees are not framework content; walking them
14
+ // during release Step 5 / task check is pure wall-clock waste (#2953, #1656).
15
+ ".deft-scratch",
16
+ // Legacy swarm worktree root name (pre-.deft-scratch layout).
17
+ "swarm-worktrees",
13
18
  ]);
14
19
  function shouldSkipPath(parts) {
15
20
  if (parts.some((p) => EXCLUDE_DIRS.has(p)))
@@ -1,8 +1,9 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { runningInsideDeftRepo } from "../doctor/paths.js";
4
4
  import { ALL_ATTRIBUTION_EVENT_NAMES } from "../events/attribution-constants.js";
5
- import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
5
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
6
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
6
7
  import { DEFAULT_EVENT_LOG, readEvents } from "../lifecycle/events.js";
7
8
  import { policyColonInvocation } from "../policy/policy-invocation.js";
8
9
  import { isValueFeedbackPathAllowed, resolveValueFeedback, } from "../policy/value-feedback.js";
@@ -230,12 +231,16 @@ function appendReadbackHistory(projectRoot, eventId, line, options = {}) {
230
231
  line,
231
232
  };
232
233
  try {
233
- assertWriteTargetSafe(projectRoot, path);
234
- mkdirSync(join(path, ".."), { recursive: true });
235
- appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
234
+ // #2980 wave D: product write sink routes through containedWrite.
235
+ containedWrite({
236
+ root: resolve(projectRoot),
237
+ target: path,
238
+ data: `${JSON.stringify(record)}\n`,
239
+ mode: "append",
240
+ });
236
241
  }
237
242
  catch (err) {
238
- if (err instanceof ProjectionContainmentError) {
243
+ if (err instanceof ProjectionContainmentError || err instanceof ContainedWriteError) {
239
244
  throw err;
240
245
  }
241
246
  // observability only
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
- import { basename, dirname } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { utcNowIso } from "../scope/vbrief-json.js";
4
5
  import { pythonJsonPretty } from "../vbrief-build/json.js";
5
6
  import { ACTIVE_FOLDER, ELIGIBLE_STATUSES_FOR_FLIP, formatEligibleStatusList, SOURCE_FOLDERS, TARGET_STATUS, } from "./constants.js";
@@ -150,15 +151,22 @@ export function activate(vbriefPath, options = {}) {
150
151
  "collision manually before re-running `task vbrief:activate`.",
151
152
  };
152
153
  }
154
+ const tmpBase = `${fileName}.tmp`;
153
155
  const tmp = `${dest}.tmp`;
154
156
  try {
155
- writeFileSync(tmp, pythonJsonPretty(payload), "utf8");
157
+ // #2980 wave D: product write sink routes through containedWrite.
158
+ containedWrite({
159
+ root: resolve(activeDir),
160
+ target: tmpBase,
161
+ data: pythonJsonPretty(payload),
162
+ mode: "create",
163
+ });
156
164
  renameSync(tmp, dest);
157
165
  }
158
166
  catch (err) {
159
167
  const e = err;
160
168
  try {
161
- unlinkSync(tmp);
169
+ rmSync(tmp, { force: true });
162
170
  }
163
171
  catch {
164
172
  /* best-effort cleanup */
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
6
  import { pythonJsonPretty } from "./json.js";
6
7
  import { ProjectDefinitionIOError } from "./types.js";
@@ -105,12 +106,19 @@ export function loadProjectDefinitionForMutation(projectRoot) {
105
106
  }
106
107
  /** Atomically write ``data`` to ``path`` as pretty-printed JSON. */
107
108
  export function atomicWriteProjectDefinition(path, data) {
108
- mkdirSync(dirname(path), { recursive: true });
109
+ // #2980 wave C: product write sink routes through containedWrite (temp under parent).
110
+ const dir = dirname(path);
111
+ mkdirSync(dir, { recursive: true });
109
112
  const payload = pythonJsonPretty(data).replace(/\n$/, "");
110
- const tmp = join(dirname(path), `${path.split(/[/\\]/).pop()}.${randomBytes(4).toString("hex")}.tmp`);
113
+ const body = payload.endsWith("\n") ? payload : `${payload}\n`;
114
+ const tmpName = `${basename(path)}.${randomBytes(4).toString("hex")}.tmp`;
115
+ const tmp = join(dir, tmpName);
111
116
  try {
112
- writeFileSync(tmp, payload.endsWith("\n") ? payload : `${payload}\n`, {
113
- encoding: "utf8",
117
+ containedWrite({
118
+ root: resolve(dir),
119
+ target: tmpName,
120
+ data: body,
121
+ mode: "create",
114
122
  });
115
123
  renameSync(tmp, path);
116
124
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * contained-writes.ts — inventory gate for raw product write sinks (#2951).
3
+ *
4
+ * Scans TypeScript under packages/core/src (and optional roots) for raw write
5
+ * call patterns (`writeFileSync`, `appendFileSync`, `fs.writeFile`, etc.)
6
+ * outside an allowlist. Default remains **fail-open** (advisory exit 0) while
7
+ * mass migration continues; pass `--enforce` to fail closed (Phase 2+).
8
+ *
9
+ * Allowlist is the residual set of modules not yet migrated onto containedWrite.
10
+ * Phase 2 removed cache/io + lifecycle/events after sink migration.
11
+ *
12
+ * Exit codes:
13
+ * 0 — clean OR fail-open with findings (default)
14
+ * 1 — findings under `--enforce` (fail-closed)
15
+ * 2 — config error (project root missing)
16
+ *
17
+ * Refs #2951, #2980 (e2e/parity harness exclusions).
18
+ */
19
+ /** Patterns that indicate a raw filesystem write sink in product code. */
20
+ export declare const RAW_WRITE_PATTERNS: readonly RegExp[];
21
+ /**
22
+ * Relative path prefixes (posix-style) allowed to use raw write sinks.
23
+ * Shrink as sinks migrate onto containedWrite (#2951 Phase 2+).
24
+ */
25
+ export declare const CONTAINED_WRITES_ALLOWLIST: readonly string[];
26
+ /**
27
+ * Non-product harness path markers excluded from the product inventory (#2980).
28
+ * Prefer exclusion over permanent CONTAINED_WRITES_ALLOWLIST entries for e2e/parity noise.
29
+ * Paths are matched as posix substrings after `toPosix` (always `/` separators).
30
+ */
31
+ export declare const NON_PRODUCT_HARNESS_PATH_MARKERS: readonly string[];
32
+ export interface ContainedWriteFinding {
33
+ readonly path: string;
34
+ readonly line: number;
35
+ readonly match: string;
36
+ }
37
+ export interface ContainedWritesOptions {
38
+ readonly projectRoot: string;
39
+ /** When true, exit 1 if any non-allowlisted raw write is found. Default false (fail-open). */
40
+ readonly enforce?: boolean;
41
+ /** Extra relative path prefixes to allow. */
42
+ readonly extraAllowlist?: readonly string[];
43
+ /** Roots to scan relative to projectRoot. Default packages/core/src. */
44
+ readonly scanRoots?: readonly string[];
45
+ }
46
+ export interface ContainedWritesResult {
47
+ readonly code: 0 | 1 | 2;
48
+ readonly message: string;
49
+ readonly stream: "stdout" | "stderr";
50
+ readonly findings: readonly ContainedWriteFinding[];
51
+ readonly enforce: boolean;
52
+ readonly failOpen: boolean;
53
+ }
54
+ /**
55
+ * Inventory raw write sinks under scan roots; fail-open by default (#2951).
56
+ * Pass `enforce: true` / `--enforce` to fail closed on findings (Phase 2 path).
57
+ */
58
+ export declare function evaluateContainedWrites(options: ContainedWritesOptions): ContainedWritesResult;
59
+ //# sourceMappingURL=contained-writes.d.ts.map
@@ -0,0 +1,272 @@
1
+ /**
2
+ * contained-writes.ts — inventory gate for raw product write sinks (#2951).
3
+ *
4
+ * Scans TypeScript under packages/core/src (and optional roots) for raw write
5
+ * call patterns (`writeFileSync`, `appendFileSync`, `fs.writeFile`, etc.)
6
+ * outside an allowlist. Default remains **fail-open** (advisory exit 0) while
7
+ * mass migration continues; pass `--enforce` to fail closed (Phase 2+).
8
+ *
9
+ * Allowlist is the residual set of modules not yet migrated onto containedWrite.
10
+ * Phase 2 removed cache/io + lifecycle/events after sink migration.
11
+ *
12
+ * Exit codes:
13
+ * 0 — clean OR fail-open with findings (default)
14
+ * 1 — findings under `--enforce` (fail-closed)
15
+ * 2 — config error (project root missing)
16
+ *
17
+ * Refs #2951, #2980 (e2e/parity harness exclusions).
18
+ */
19
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
20
+ import { join, relative, resolve, sep } from "node:path";
21
+ const EXIT_OK = 0;
22
+ const EXIT_ENFORCE_FINDINGS = 1;
23
+ const EXIT_CONFIG = 2;
24
+ /** Patterns that indicate a raw filesystem write sink in product code. */
25
+ export const RAW_WRITE_PATTERNS = [
26
+ /\bwriteFileSync\s*\(/,
27
+ /\bappendFileSync\s*\(/,
28
+ /\bfs\.writeFile\s*\(/,
29
+ /\bfs\.promises\.writeFile\s*\(/,
30
+ /\bcreateWriteStream\s*\(/,
31
+ /\bwriteFile\s*\(\s*[^)]+,\s*[^)]+,\s*['"]utf8['"]/,
32
+ // Low-level / async forms that also bypass containedWrite (#2951 Greptile P1).
33
+ /\bopenSync\s*\(/,
34
+ /\bwriteSync\s*\(/,
35
+ /\bappendFile\s*\(/,
36
+ /\.writeFile\s*\(/,
37
+ /\bpromises\.writeFile\s*\(/,
38
+ /\bfs\.open\s*\(/,
39
+ /\bfs\.write\s*\(/,
40
+ ];
41
+ /**
42
+ * Relative path prefixes (posix-style) allowed to use raw write sinks.
43
+ * Shrink as sinks migrate onto containedWrite (#2951 Phase 2+).
44
+ */
45
+ export const CONTAINED_WRITES_ALLOWLIST = [
46
+ // Implementation modules for containment + contained write.
47
+ "packages/core/src/fs/contained-write.ts",
48
+ "packages/core/src/fs/projection-containment.ts",
49
+ // Deposit tree copy / contain primitives (low-level install sinks).
50
+ "packages/core/src/deposit/copy-tree.ts",
51
+ "packages/core/src/deposit/contain.ts",
52
+ // Lock / stream primitives (not product payload writers) — #2980 residual.
53
+ "packages/core/src/slice/lock.ts",
54
+ "packages/core/src/vbrief-build/project-definition-io.ts", // openSync mutation lock only
55
+ "packages/core/src/release/build-dist.ts", // createWriteStream archive packer
56
+ // Temporary residual product sinks (#2980 wave D). Shrink as modules migrate.
57
+ "packages/core/src/doctor/main.ts",
58
+ "packages/core/src/eval-health-relocation/evaluate.ts",
59
+ "packages/core/src/intake/issue-ingest.ts",
60
+ "packages/core/src/intake/reconcile-issues.ts",
61
+ "packages/core/src/issue-sync/sync-from-xbrief.ts",
62
+ "packages/core/src/orchestration/probe-session.ts",
63
+ "packages/core/src/orchestration/verify-judgment-gates.ts",
64
+ "packages/core/src/platform/changelog-cli.ts",
65
+ "packages/core/src/release/gh.ts",
66
+ "packages/core/src/release/pipeline-fixture.ts",
67
+ "packages/core/src/release/pipeline.ts",
68
+ "packages/core/src/render/export-spec.ts",
69
+ "packages/core/src/render/prd-render.ts",
70
+ "packages/core/src/render/roadmap-render.ts",
71
+ "packages/core/src/render/rule-map.ts",
72
+ "packages/core/src/render/spec-render.ts",
73
+ "packages/core/src/scm/gh-rest.ts",
74
+ "packages/core/src/triage/actions/candidates-log.ts",
75
+ "packages/core/src/triage/bootstrap/index.ts",
76
+ "packages/core/src/triage/reconcile/reconcile.ts",
77
+ "packages/core/src/triage/scope-drift/add-ignore.ts",
78
+ "packages/core/src/triage/scope/coverage.ts",
79
+ "packages/core/src/triage/scope/mutations-core.ts",
80
+ "packages/core/src/triage/smoketest/index.ts",
81
+ "packages/core/src/triage/subscribe/index.ts",
82
+ "packages/core/src/triage/summary/index.ts",
83
+ "packages/core/src/vbrief-build/speckit.ts",
84
+ "packages/core/src/vbrief-reconcile/reconciliation.ts",
85
+ "packages/core/src/vbrief-validation/safety.ts",
86
+ "packages/core/src/xbrief-migrate/agents-header.ts",
87
+ // Phase 2 removed: cache/io.ts, lifecycle/events.ts (migrated to containedWrite).
88
+ ];
89
+ /** Path segments that mark a file as test or fixture (always allowlisted). */
90
+ const TEST_PATH_MARKERS = [
91
+ ".test.ts",
92
+ ".test.tsx",
93
+ "/test-helpers.ts",
94
+ "/fixtures/",
95
+ "\\fixtures\\",
96
+ ];
97
+ /**
98
+ * Non-product harness path markers excluded from the product inventory (#2980).
99
+ * Prefer exclusion over permanent CONTAINED_WRITES_ALLOWLIST entries for e2e/parity noise.
100
+ * Paths are matched as posix substrings after `toPosix` (always `/` separators).
101
+ */
102
+ export const NON_PRODUCT_HARNESS_PATH_MARKERS = [
103
+ "/release-e2e/",
104
+ "/integration-e2e/",
105
+ "parity-scenarios.ts",
106
+ "parity-scenarios.tsx",
107
+ ];
108
+ function toPosix(p) {
109
+ return p.split(sep).join("/");
110
+ }
111
+ function isTestPath(relPosix) {
112
+ return TEST_PATH_MARKERS.some((m) => relPosix.includes(m.replace(/\\/g, "/")));
113
+ }
114
+ /** E2E / parity harness under src/ — not product sinks (#2980 scanner hygiene). */
115
+ function isNonProductHarnessPath(relPosix) {
116
+ return NON_PRODUCT_HARNESS_PATH_MARKERS.some((m) => relPosix.includes(m));
117
+ }
118
+ /** Strip trailing `/` without regex (CodeQL js/polynomial-redos safe). */
119
+ function stripTrailingSlashes(path) {
120
+ let end = path.length;
121
+ while (end > 0 && path.charCodeAt(end - 1) === 47 /* / */) {
122
+ end -= 1;
123
+ }
124
+ return end === path.length ? path : path.slice(0, end);
125
+ }
126
+ function isAllowlisted(relPosix, allow) {
127
+ if (isTestPath(relPosix) || isNonProductHarnessPath(relPosix)) {
128
+ return true;
129
+ }
130
+ return allow.some((entry) => {
131
+ // Avoid regex on path input (CodeQL js/polynomial-redos on /\/+$/).
132
+ const e = stripTrailingSlashes(entry.split("\\").join("/"));
133
+ // Exact file match, or directory-prefix only (never bare endsWith — that
134
+ // lets `evil/.../packages/core/src/fs/contained-write.ts` slip through).
135
+ return relPosix === e || relPosix.startsWith(`${e}/`);
136
+ });
137
+ }
138
+ function walkTsFiles(dir, out) {
139
+ let entries;
140
+ try {
141
+ entries = readdirSync(dir);
142
+ }
143
+ catch {
144
+ return;
145
+ }
146
+ for (const name of entries) {
147
+ if (name === "node_modules" || name === "dist" || name === ".git") {
148
+ continue;
149
+ }
150
+ const full = join(dir, name);
151
+ let st;
152
+ try {
153
+ st = statSync(full);
154
+ }
155
+ catch {
156
+ continue;
157
+ }
158
+ if (st.isDirectory()) {
159
+ walkTsFiles(full, out);
160
+ }
161
+ else if (st.isFile() && (name.endsWith(".ts") || name.endsWith(".tsx"))) {
162
+ out.push(full);
163
+ }
164
+ }
165
+ }
166
+ function scanFile(absPath, relPosix) {
167
+ let text;
168
+ try {
169
+ text = readFileSync(absPath, "utf8");
170
+ }
171
+ catch {
172
+ return [];
173
+ }
174
+ const findings = [];
175
+ const lines = text.split(/\r?\n/);
176
+ for (let i = 0; i < lines.length; i += 1) {
177
+ const line = lines[i] ?? "";
178
+ // Skip import lines and comments-only lines for noise reduction.
179
+ const trimmed = line.trim();
180
+ if (trimmed.startsWith("import ") ||
181
+ trimmed.startsWith("//") ||
182
+ trimmed.startsWith("*") ||
183
+ trimmed.startsWith("/*")) {
184
+ continue;
185
+ }
186
+ for (const re of RAW_WRITE_PATTERNS) {
187
+ if (re.test(line)) {
188
+ findings.push({
189
+ path: relPosix,
190
+ line: i + 1,
191
+ match: trimmed.length > 100 ? `${trimmed.slice(0, 97)}...` : trimmed,
192
+ });
193
+ break;
194
+ }
195
+ }
196
+ }
197
+ return findings;
198
+ }
199
+ /**
200
+ * Inventory raw write sinks under scan roots; fail-open by default (#2951).
201
+ * Pass `enforce: true` / `--enforce` to fail closed on findings (Phase 2 path).
202
+ */
203
+ export function evaluateContainedWrites(options) {
204
+ const root = resolve(options.projectRoot);
205
+ if (!existsSync(root)) {
206
+ return {
207
+ code: EXIT_CONFIG,
208
+ message: `verify:contained-writes: project root not found: ${root}`,
209
+ stream: "stderr",
210
+ findings: [],
211
+ enforce: Boolean(options.enforce),
212
+ failOpen: !options.enforce,
213
+ };
214
+ }
215
+ const enforce = options.enforce === true;
216
+ const allow = [...CONTAINED_WRITES_ALLOWLIST, ...(options.extraAllowlist ?? [])];
217
+ const scanRoots = options.scanRoots ?? ["packages/core/src"];
218
+ const findings = [];
219
+ for (const scanRel of scanRoots) {
220
+ const scanAbs = resolve(root, scanRel);
221
+ if (!existsSync(scanAbs)) {
222
+ continue;
223
+ }
224
+ const files = [];
225
+ walkTsFiles(scanAbs, files);
226
+ for (const abs of files) {
227
+ const relPosix = toPosix(relative(root, abs));
228
+ if (isAllowlisted(relPosix, allow)) {
229
+ continue;
230
+ }
231
+ findings.push(...scanFile(abs, relPosix));
232
+ }
233
+ }
234
+ if (findings.length === 0) {
235
+ return {
236
+ code: EXIT_OK,
237
+ message: `OK: verify:contained-writes — no non-allowlisted raw write sinks under ${scanRoots.join(", ")} (enforce=${enforce}).`,
238
+ stream: "stdout",
239
+ findings: [],
240
+ enforce,
241
+ failOpen: !enforce,
242
+ };
243
+ }
244
+ const lines = [
245
+ `verify:contained-writes: found ${findings.length} raw write sink(s) outside allowlist:`,
246
+ ...findings.slice(0, 50).map((f) => ` ${f.path}:${f.line}: ${f.match}`),
247
+ ];
248
+ if (findings.length > 50) {
249
+ lines.push(` ... and ${findings.length - 50} more`);
250
+ }
251
+ if (enforce) {
252
+ lines.push("FAIL: --enforce is set; migrate sinks to packages/core/src/fs/contained-write.ts or add a justified allowlist entry (#2951).");
253
+ return {
254
+ code: EXIT_ENFORCE_FINDINGS,
255
+ message: lines.join("\n"),
256
+ stream: "stderr",
257
+ findings,
258
+ enforce: true,
259
+ failOpen: false,
260
+ };
261
+ }
262
+ lines.push("ADVISORY (fail-open): exit 0. Prefer containedWrite() for new sinks; pass --enforce to fail closed. See docs/reference/contained-write.md (#2951 Phase 2).");
263
+ return {
264
+ code: EXIT_OK,
265
+ message: lines.join("\n"),
266
+ stream: "stdout",
267
+ findings,
268
+ enforce: false,
269
+ failOpen: true,
270
+ };
271
+ }
272
+ //# sourceMappingURL=contained-writes.js.map
@@ -1,5 +1,6 @@
1
1
  export * from "./biome-config.js";
2
2
  export * from "./code-structure-validate.js";
3
+ export * from "./contained-writes.js";
3
4
  export * from "./content-manifest.js";
4
5
  export { CANONICAL_SCHEMA_REL, type ContractDriftOptions, type ContractDriftResult, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
5
6
  export * from "./cursor-tier1.js";
@@ -1,5 +1,6 @@
1
1
  export * from "./biome-config.js";
2
2
  export * from "./code-structure-validate.js";
3
+ export * from "./contained-writes.js";
3
4
  export * from "./content-manifest.js";
4
5
  export { CANONICAL_SCHEMA_REL, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
5
6
  export * from "./cursor-tier1.js";
@@ -14,7 +14,20 @@ export const OPENCLAW_TIER1_TARGETS = [
14
14
  {
15
15
  path: "content/skills/deft-directive-swarm/references/host-openclaw.md",
16
16
  label: "swarm OpenClaw host adapter",
17
- markers: ["Step 2f: OpenClaw Launch", "sessions_spawn", "openclaw"],
17
+ // Cold-start (#2968 A7): project root + Skills Index before freestyle; keep Tier-1 launch markers.
18
+ markers: [
19
+ "Step 2f: OpenClaw Launch",
20
+ "sessions_spawn",
21
+ "openclaw",
22
+ "Cold-start",
23
+ "Skills Index",
24
+ "project root",
25
+ ],
26
+ },
27
+ {
28
+ path: "content/contracts/host-lifecycle-duties.md",
29
+ label: "host lifecycle duty list contract (#2968 A3)",
30
+ markers: ["Session start", "Deft-shaped user intent", "Skills Index", "project root"],
18
31
  },
19
32
  {
20
33
  path: "packages/core/src/swarm/routing.ts",
@@ -11,6 +11,9 @@ export const EXCLUDE_DIRS = new Set([
11
11
  "__pycache__",
12
12
  "dist",
13
13
  "scripts",
14
+ // Swarm / agent worktrees are not product source (#2953, #1656).
15
+ ".deft-scratch",
16
+ "swarm-worktrees",
14
17
  ]);
15
18
  export const EXTENSIONS = new Set([".py", ".go", ".sh"]);
16
19
  function isWordChar(ch) {