@deftai/directive-core 0.88.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/task-cache/store.js +11 -7
  28. package/dist/capacity/backfill.js +10 -4
  29. package/dist/category-b-namespace/index.js +10 -4
  30. package/dist/codebase/default-extractor.js +3 -0
  31. package/dist/codebase/map.js +11 -4
  32. package/dist/doctor/doctor-state.js +28 -4
  33. package/dist/doctor/main.d.ts +10 -0
  34. package/dist/doctor/main.js +160 -0
  35. package/dist/doctor/types.d.ts +11 -0
  36. package/dist/escalation/actions.d.ts +63 -0
  37. package/dist/escalation/actions.js +137 -0
  38. package/dist/escalation/index.d.ts +8 -0
  39. package/dist/escalation/index.js +8 -0
  40. package/dist/escalation/paths.d.ts +3 -0
  41. package/dist/escalation/paths.js +10 -0
  42. package/dist/escalation/store.d.ts +17 -0
  43. package/dist/escalation/store.js +172 -0
  44. package/dist/escalation/types.d.ts +73 -0
  45. package/dist/escalation/types.js +43 -0
  46. package/dist/eval/crud-telemetry.js +30 -10
  47. package/dist/eval/health.js +22 -7
  48. package/dist/eval/readback.js +11 -10
  49. package/dist/eval/run.js +32 -16
  50. package/dist/events/attribution-enrichment.js +10 -4
  51. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  52. package/dist/finish-loop/directive-finish-loop.js +239 -0
  53. package/dist/finish-loop/grant-gate.d.ts +31 -0
  54. package/dist/finish-loop/grant-gate.js +169 -0
  55. package/dist/finish-loop/index.d.ts +11 -0
  56. package/dist/finish-loop/index.js +11 -0
  57. package/dist/finish-loop/main.d.ts +35 -0
  58. package/dist/finish-loop/main.js +365 -0
  59. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  60. package/dist/finish-loop/pr-finish-loop.js +217 -0
  61. package/dist/finish-loop/progress.d.ts +17 -0
  62. package/dist/finish-loop/progress.js +45 -0
  63. package/dist/finish-loop/queue.d.ts +16 -0
  64. package/dist/finish-loop/queue.js +65 -0
  65. package/dist/finish-loop/types.d.ts +52 -0
  66. package/dist/finish-loop/types.js +10 -0
  67. package/dist/fs/contained-write.d.ts +101 -0
  68. package/dist/fs/contained-write.js +281 -0
  69. package/dist/hooks/classify/classify.d.ts +10 -0
  70. package/dist/hooks/classify/classify.js +37 -0
  71. package/dist/hooks/classify/index.d.ts +14 -0
  72. package/dist/hooks/classify/index.js +13 -0
  73. package/dist/hooks/classify/paths.d.ts +22 -0
  74. package/dist/hooks/classify/paths.js +72 -0
  75. package/dist/hooks/classify/payload.d.ts +16 -0
  76. package/dist/hooks/classify/payload.js +45 -0
  77. package/dist/hooks/classify/stdin.d.ts +12 -0
  78. package/dist/hooks/classify/stdin.js +63 -0
  79. package/dist/hooks/classify/tool-name.d.ts +18 -0
  80. package/dist/hooks/classify/tool-name.js +85 -0
  81. package/dist/hooks/classify/types.d.ts +41 -0
  82. package/dist/hooks/classify/types.js +7 -0
  83. package/dist/hooks/dispatcher.d.ts +21 -18
  84. package/dist/hooks/dispatcher.js +212 -163
  85. package/dist/hooks/fixtures/cases.d.ts +44 -0
  86. package/dist/hooks/fixtures/cases.js +595 -0
  87. package/dist/hooks/fixtures/index.d.ts +2 -0
  88. package/dist/hooks/fixtures/index.js +2 -0
  89. package/dist/hooks/index.d.ts +2 -0
  90. package/dist/hooks/index.js +2 -0
  91. package/dist/index.d.ts +4 -0
  92. package/dist/index.js +5 -0
  93. package/dist/init-deposit/agent-hooks.js +27 -8
  94. package/dist/init-deposit/gitignore.js +11 -3
  95. package/dist/init-deposit/headless-manifest.js +12 -4
  96. package/dist/init-deposit/migrate.d.ts +1 -1
  97. package/dist/init-deposit/migrate.js +13 -3
  98. package/dist/init-deposit/prettierignore.js +10 -3
  99. package/dist/init-deposit/scaffold.js +35 -26
  100. package/dist/init-deposit/xbrief-projections.js +11 -4
  101. package/dist/intake/candidates-log.js +46 -35
  102. package/dist/intake/github-body-cli.d.ts +6 -0
  103. package/dist/intake/github-body-cli.js +17 -0
  104. package/dist/intake/github-body.d.ts +46 -3
  105. package/dist/intake/github-body.js +191 -20
  106. package/dist/intake/issue-emit.d.ts +1 -0
  107. package/dist/intake/issue-emit.js +41 -23
  108. package/dist/lifecycle/event-detect.js +12 -4
  109. package/dist/lifecycle/events.js +29 -20
  110. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  111. package/dist/packs/pack-render.js +19 -6
  112. package/dist/plan-sequence/store.js +11 -4
  113. package/dist/policy/hotfix-criteria.d.ts +79 -0
  114. package/dist/policy/hotfix-criteria.js +197 -0
  115. package/dist/policy/index.d.ts +4 -0
  116. package/dist/policy/index.js +31 -1
  117. package/dist/policy/intent-ceiling.d.ts +79 -0
  118. package/dist/policy/intent-ceiling.js +181 -0
  119. package/dist/policy/no-deft-directive.js +10 -3
  120. package/dist/policy/org-force-on-migration.js +9 -5
  121. package/dist/policy/require-human-merge.d.ts +75 -0
  122. package/dist/policy/require-human-merge.js +324 -0
  123. package/dist/policy/resolve.js +17 -6
  124. package/dist/policy/runtime-authority.d.ts +15 -2
  125. package/dist/policy/runtime-authority.js +23 -3
  126. package/dist/policy/write-fence.d.ts +78 -0
  127. package/dist/policy/write-fence.js +164 -0
  128. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  129. package/dist/pr-wait-mergeable/cascade.js +28 -0
  130. package/dist/preflight/evaluate.js +10 -0
  131. package/dist/product-signal/consent.js +21 -5
  132. package/dist/product-signal/submit.js +22 -12
  133. package/dist/release/build-dist-runner.js +5 -2
  134. package/dist/release/build-dist.d.ts +19 -1
  135. package/dist/release/build-dist.js +50 -2
  136. package/dist/release/native-steps.js +7 -2
  137. package/dist/release-publish/pipeline.d.ts +13 -0
  138. package/dist/release-publish/pipeline.js +46 -1
  139. package/dist/scope/audit-log.js +19 -24
  140. package/dist/scope/decompose.js +10 -5
  141. package/dist/scope/decomposed-refs.js +18 -3
  142. package/dist/scope/demote.js +9 -2
  143. package/dist/scope/undo.js +9 -2
  144. package/dist/session/release-availability.js +26 -6
  145. package/dist/session/ritual-sentinel.js +19 -12
  146. package/dist/session/session-start.js +7 -0
  147. package/dist/staleness-tickler/state.js +26 -6
  148. package/dist/swarm/launch.js +13 -3
  149. package/dist/swarm/routing.js +9 -3
  150. package/dist/task-surface/index.js +24 -9
  151. package/dist/triage/bootstrap/gitignore.js +53 -10
  152. package/dist/triage/cache-path.js +10 -3
  153. package/dist/triage/welcome/summary.js +11 -4
  154. package/dist/triage/welcome/writers.js +45 -16
  155. package/dist/validate-content/validate-links.js +5 -0
  156. package/dist/value/readback.js +11 -6
  157. package/dist/vbrief-activate/activate.js +12 -4
  158. package/dist/vbrief-build/project-definition-io.js +14 -6
  159. package/dist/verify-source/contained-writes.d.ts +59 -0
  160. package/dist/verify-source/contained-writes.js +272 -0
  161. package/dist/verify-source/index.d.ts +1 -0
  162. package/dist/verify-source/index.js +1 -0
  163. package/dist/verify-source/openclaw-tier1.js +14 -1
  164. package/dist/verify-source/verify-stubs.js +3 -0
  165. package/dist/xbrief-migrate/migrate-project.js +26 -12
  166. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  167. package/dist/xbrief-migrate/transforms.js +37 -14
  168. package/package.json +15 -3
@@ -1,6 +1,7 @@
1
1
  import { recordBypassSignal, recordGateCatch } from "../events/attribution-ledger.js";
2
2
  import { disclosureLine } from "../policy/disclosure.js";
3
3
  import { policyColonInvocation } from "../policy/policy-invocation.js";
4
+ import { humanMergeBranchNote, resolveHumanMergePolicy } from "../policy/require-human-merge.js";
4
5
  import { ENV_BYPASS, resolvePolicy } from "../policy/resolve.js";
5
6
  import { currentBranch, GitNotFoundError } from "./git.js";
6
7
  export const DEFAULT_BRANCHES = new Set(["master", "main"]);
@@ -93,9 +94,13 @@ export function evaluate(projectRoot, options = {}) {
93
94
  };
94
95
  }
95
96
  if (!isDefaultBranch(branch, defaultBranches)) {
97
+ // Surface (2) human-merge gate: advisory note on feature branches (#1193).
98
+ const hm = resolveHumanMergePolicy(projectRoot);
99
+ const hmNote = humanMergeBranchNote(hm);
100
+ const base = `✓ deft branch-protection: feature branch '${branch}' -- proceeding.`;
96
101
  return {
97
102
  exitCode: 0,
98
- message: `✓ deft branch-protection: feature branch '${branch}' -- proceeding.`,
103
+ message: hmNote !== null ? `${base}\n${hmNote}` : base,
99
104
  };
100
105
  }
101
106
  const result = resolvePolicy(projectRoot);
@@ -1,6 +1,6 @@
1
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, 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
4
  import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
5
5
  import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
6
6
  import { CacheError, CacheFetchError } from "./errors.js";
@@ -622,8 +622,13 @@ function readSelfHealState(cacheRoot) {
622
622
  }
623
623
  function writeSelfHealState(projectRoot, cacheRoot, when) {
624
624
  const path = join(resolve(cacheRoot), SELF_HEAL_STATE_FILENAME);
625
- assertWriteTargetSafe(projectRoot, path);
626
- writeFileSync(path, `${JSON.stringify({ last_reconcile_at: when.toISOString() })}\n`, "utf8");
625
+ // #2980 wave C: product write sink routes through containedWrite.
626
+ containedWrite({
627
+ root: resolve(projectRoot),
628
+ target: path,
629
+ data: `${JSON.stringify({ last_reconcile_at: when.toISOString() })}\n`,
630
+ mode: "replace",
631
+ });
627
632
  }
628
633
  /** TTL-bounded closed-reconcile for session ritual / triage:welcome self-healing (#1886). */
629
634
  export function maybeSelfHealCache(projectRoot, options = {}) {
@@ -679,7 +684,7 @@ export function maybeSelfHealCache(projectRoot, options = {}) {
679
684
  writeSelfHealState(projectRoot, cacheRoot, now);
680
685
  }
681
686
  catch (err) {
682
- if (err instanceof ProjectionContainmentError) {
687
+ if (err instanceof ContainedWriteError) {
683
688
  return { skipped: true, skipReason: "containment-refused", drift, refresh: null };
684
689
  }
685
690
  throw err;
@@ -1,7 +1,14 @@
1
1
  import { tmpdir } from "node:os";
2
- /** Write text via tempfile + rename (mirrors Python `_atomic_write_text`). */
2
+ /**
3
+ * Write text via tempfile + rename (mirrors Python `_atomic_write_text`).
4
+ * #2951 Phase 2: temp payload write uses containedWrite under the parent dir.
5
+ */
3
6
  export declare function atomicWriteText(path: string, text: string): void;
4
- /** Append one JSON audit record (mirrors `_append_audit`). */
7
+ /**
8
+ * Append one JSON audit record (mirrors `_append_audit`).
9
+ * #2951 Phase 2: product write sink routes through containedWrite.
10
+ * Create cacheRoot first — containedWrite requires the containment root to exist.
11
+ */
5
12
  export declare function appendAudit(record: Record<string, unknown>, cacheRoot: string): void;
6
13
  /** Touch mtime for LRU signal; failures swallowed. */
7
14
  export declare function touchMtime(path: string): void;
package/dist/cache/io.js CHANGED
@@ -1,14 +1,25 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { appendFileSync, mkdirSync, renameSync, rmSync, statSync, utimesSync, writeFileSync, } from "node:fs";
2
+ import { mkdirSync, renameSync, rmSync, statSync, utimesSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { containedWrite } from "../fs/contained-write.js";
5
6
  import { pythonJsonLine } from "./json.js";
6
- /** Write text via tempfile + rename (mirrors Python `_atomic_write_text`). */
7
+ /**
8
+ * Write text via tempfile + rename (mirrors Python `_atomic_write_text`).
9
+ * #2951 Phase 2: temp payload write uses containedWrite under the parent dir.
10
+ */
7
11
  export function atomicWriteText(path, text) {
8
- mkdirSync(dirname(path), { recursive: true });
9
- const tmp = join(dirname(path), `${path.split(/[/\\]/).pop() ?? "file"}.${randomBytes(4).toString("hex")}.tmp`);
12
+ const dir = dirname(path);
13
+ mkdirSync(dir, { recursive: true });
14
+ const tmpName = `${basename(path)}.${randomBytes(4).toString("hex")}.tmp`;
15
+ const tmp = join(dir, tmpName);
10
16
  try {
11
- writeFileSync(tmp, text, { encoding: "utf8" });
17
+ containedWrite({
18
+ root: resolve(dir),
19
+ target: tmpName,
20
+ data: text,
21
+ mode: "create",
22
+ });
12
23
  renameSync(tmp, path);
13
24
  }
14
25
  catch (err) {
@@ -21,11 +32,20 @@ export function atomicWriteText(path, text) {
21
32
  throw err;
22
33
  }
23
34
  }
24
- /** Append one JSON audit record (mirrors `_append_audit`). */
35
+ /**
36
+ * Append one JSON audit record (mirrors `_append_audit`).
37
+ * #2951 Phase 2: product write sink routes through containedWrite.
38
+ * Create cacheRoot first — containedWrite requires the containment root to exist.
39
+ */
25
40
  export function appendAudit(record, cacheRoot) {
26
- const path = join(cacheRoot, "quarantine-audit.jsonl");
27
- mkdirSync(dirname(path), { recursive: true });
28
- appendFileSync(path, `${pythonJsonLine(record)}\n`, { encoding: "utf8" });
41
+ const root = resolve(cacheRoot);
42
+ mkdirSync(root, { recursive: true });
43
+ containedWrite({
44
+ root,
45
+ target: "quarantine-audit.jsonl",
46
+ data: `${pythonJsonLine(record)}\n`,
47
+ mode: "append",
48
+ });
29
49
  }
30
50
  /** Touch mtime for LRU signal; failures swallowed. */
31
51
  export function touchMtime(path) {
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { assertWriteTargetSafe } from "../../fs/projection-containment.js";
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { containedWrite } from "../../fs/contained-write.js";
4
4
  import { DEFAULT_TASK_CACHE_ROOT, TASK_CACHE_MANIFEST } from "./constants.js";
5
5
  function entryDir(projectRoot, cacheKey, cacheRoot = DEFAULT_TASK_CACHE_ROOT) {
6
6
  return join(resolve(projectRoot), cacheRoot, cacheKey.slice(0, 2), cacheKey);
@@ -22,12 +22,16 @@ export function writeCachedTaskRecord(projectRoot, cacheKey, record, cacheRoot =
22
22
  if (record.exitCode !== 0) {
23
23
  return;
24
24
  }
25
+ const root = resolve(projectRoot);
25
26
  const dir = entryDir(projectRoot, cacheKey, cacheRoot);
26
27
  const manifest = join(dir, TASK_CACHE_MANIFEST);
27
- assertWriteTargetSafe(resolve(projectRoot), dir);
28
- mkdirSync(dirname(manifest), { recursive: true });
29
- assertWriteTargetSafe(resolve(projectRoot), manifest);
30
- writeFileSync(manifest, `${JSON.stringify(record, null, 2)}\n`, "utf8");
28
+ // #2951 Phase 2: product write sink routes through containedWrite (mkdir + replace).
29
+ containedWrite({
30
+ root,
31
+ target: manifest,
32
+ data: `${JSON.stringify(record, null, 2)}\n`,
33
+ mode: "replace",
34
+ });
31
35
  }
32
36
  export function clearTaskCache(projectRoot, cacheRoot = DEFAULT_TASK_CACHE_ROOT) {
33
37
  const root = join(resolve(projectRoot), cacheRoot);
@@ -1,9 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { referenceTypeMatches } from "@deftai/directive-types";
5
5
  import { sortedStringifyCompact } from "../codebase/json.js";
6
- import { assertWriteTargetSafe } from "../fs/projection-containment.js";
6
+ import { containedWrite } from "../fs/contained-write.js";
7
7
  import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
8
8
  import { classifyBucket, loadBucketMatchers, resolveCapacityAllocation, SOURCE_MATCH, } from "../policy/capacity.js";
9
9
  export const COMPLETED_FOLDER = "completed";
@@ -139,7 +139,7 @@ function inWindow(completedAt, windowDays, now) {
139
139
  return ageDays >= 0 && ageDays <= windowDays;
140
140
  }
141
141
  function writeMetadata(projectRoot, path, data, plan, metadata, options) {
142
- assertWriteTargetSafe(projectRoot, path);
142
+ // #2980 wave D: product write sink routes through containedWrite.
143
143
  if (typeof plan.metadata !== "object" || plan.metadata === null || Array.isArray(plan.metadata)) {
144
144
  plan.metadata = metadata;
145
145
  }
@@ -152,7 +152,13 @@ function writeMetadata(projectRoot, path, data, plan, metadata, options) {
152
152
  metadata.capacityBucketSource = options.source;
153
153
  }
154
154
  }
155
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { encoding: "utf8" });
155
+ const root = resolve(projectRoot);
156
+ containedWrite({
157
+ root,
158
+ target: path,
159
+ data: `${JSON.stringify(data, null, 2)}\n`,
160
+ mode: "replace",
161
+ });
156
162
  }
157
163
  /** Backfill capacityBucket / completedAt on completed vBRIEFs. */
158
164
  export async function backfill(projectRoot, options = {}) {
@@ -9,9 +9,10 @@
9
9
  * order-preserving (the namespaced key takes the legacy key's slot, keeping
10
10
  * artifact diffs minimal).
11
11
  */
12
- import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
12
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
13
13
  import { join, relative, resolve } from "node:path";
14
- import { assertDirectoryNotSymlink, assertWriteTargetSafe } from "../fs/projection-containment.js";
14
+ import { containedWrite } from "../fs/contained-write.js";
15
+ import { assertDirectoryNotSymlink } from "../fs/projection-containment.js";
15
16
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
16
17
  import { LEGACY_PLAN_COMPLETED_NOTE_KEY, LEGACY_PLAN_POLICY_KEY, PLAN_COMPLETED_NOTE_KEY, PLAN_POLICY_KEY, } from "../policy/plan-extensions.js";
17
18
  /** The locked Category B renames (#1650): directive's own config, not xbrief data. */
@@ -141,7 +142,13 @@ export function migrateCategoryBCorpus(projectRoot) {
141
142
  }
142
143
  if (result.changed) {
143
144
  try {
144
- assertWriteTargetSafe(root, file);
145
+ // #2980 wave D: product write sink routes through containedWrite.
146
+ containedWrite({
147
+ root,
148
+ target: file,
149
+ data: `${JSON.stringify(result.doc, null, 2)}\n`,
150
+ mode: "replace",
151
+ });
145
152
  }
146
153
  catch (err) {
147
154
  conflicts.push({
@@ -150,7 +157,6 @@ export function migrateCategoryBCorpus(projectRoot) {
150
157
  });
151
158
  continue;
152
159
  }
153
- writeFileSync(file, `${JSON.stringify(result.doc, null, 2)}\n`, "utf8");
154
160
  changed.push(relPath);
155
161
  }
156
162
  }
@@ -20,6 +20,9 @@ export const SKIP_DIRS = new Set([
20
20
  "build",
21
21
  "dist",
22
22
  "node_modules",
23
+ // Swarm / agent worktrees are not source modules (#2953, #1656).
24
+ ".deft-scratch",
25
+ // Legacy swarm worktree root name (pre-.deft-scratch layout).
23
26
  "swarm-worktrees",
24
27
  ]);
25
28
  export const LANGUAGE_BY_SUFFIX = {
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { assertProjectionContained } from "../fs/projection-containment.js";
4
5
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
6
  import { extractCodeStructure, loadJsonFile } from "../verify-source/code-structure-validate.js";
@@ -249,8 +250,14 @@ export function writeCodebaseMap(projectRoot, options) {
249
250
  artifactPath: options.artifactPath,
250
251
  });
251
252
  const document = renderCodebaseMap(selection.artifact);
252
- mkdirSync(dirname(resolvedOutput), { recursive: true });
253
- writeFileSync(resolvedOutput, document, { encoding: "utf8" });
253
+ // #2980 wave D: product write sink routes through containedWrite.
254
+ const root = resolve(projectRoot);
255
+ containedWrite({
256
+ root,
257
+ target: resolvedOutput,
258
+ data: document,
259
+ mode: "replace",
260
+ });
254
261
  return resolvedOutput;
255
262
  }
256
263
  export function runCodebaseMapCli(argv) {
@@ -1,6 +1,7 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { resolveTriageCachePath } from "../triage/cache-path.js";
5
6
  import { CLEAN_WINDOW_HOURS, DIRTY_WINDOW_HOURS, ENV_STATE_PATH } from "./constants.js";
6
7
  const STATE_FILENAME = "doctor-state.json";
@@ -57,8 +58,31 @@ export function writeState(projectRoot, payload) {
57
58
  };
58
59
  const path = statePath(projectRoot);
59
60
  try {
60
- mkdirSync(dirname(path), { recursive: true });
61
- writeFileSync(path, `${JSON.stringify(body, null, 2)}\n`, "utf8");
61
+ // #2980 wave C: product write sink routes through containedWrite.
62
+ // Default path is under the project triage cache; ENV_STATE_PATH may point outside.
63
+ const projectAbs = resolve(projectRoot);
64
+ const pathAbs = resolve(path);
65
+ const rel = relative(projectAbs, pathAbs);
66
+ const nested = rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
67
+ const payload = `${JSON.stringify(body, null, 2)}\n`;
68
+ if (nested) {
69
+ containedWrite({
70
+ root: projectAbs,
71
+ target: pathAbs,
72
+ data: payload,
73
+ mode: "replace",
74
+ });
75
+ }
76
+ else {
77
+ const parent = dirname(pathAbs);
78
+ mkdirSync(parent, { recursive: true });
79
+ containedWrite({
80
+ root: resolve(parent),
81
+ target: basename(pathAbs),
82
+ data: payload,
83
+ mode: "replace",
84
+ });
85
+ }
62
86
  return path;
63
87
  }
64
88
  catch {
@@ -4,6 +4,16 @@ import type { DoctorSeams, Finding, ResolutionSummary } from "./types.js";
4
4
  export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams): number;
5
5
  export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): boolean;
6
6
  export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
7
+ /**
8
+ * Fail closed when PROJECT-DEFINITION under an xbrief/ layout still declares
9
+ * envelope 0.6 while the framework schema is 0.8 (#2971). Greenfield (no
10
+ * project definition yet) and current 0.8 envelopes pass. Unreadable paths
11
+ * (test seams / permission) skip rather than false-positive fail. Behind-major
12
+ * records `deft migrate:xbrief` as the next action — layout rename alone is not
13
+ * enough. Distinct from deposited-schema (`stale-xbrief-schema-deposit`) which
14
+ * must NOT route to migrate:xbrief when only framework schema files are stale.
15
+ */
16
+ export declare function runXbriefEnvelopeVersionCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (f: Finding) => void, seams: DoctorSeams): void;
7
17
  /**
8
18
  * Never emit a bare `task ...` remediation in a project without Taskfile wiring
9
19
  * (#2267 / #2893). The `directive`/`deft` surface always works; `task deft:X` is
@@ -1,14 +1,18 @@
1
1
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { VBRIEF_VERSION } from "@deftai/directive-types";
3
4
  import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
4
5
  import { contentRoot } from "../content-root.js";
6
+ import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
7
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
6
8
  import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "../policy/plan-extensions.js";
7
9
  import { loadProjectDefinition } from "../policy/resolve.js";
8
10
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
11
+ import { classifyXbriefSchemaDistance } from "../staleness-tickler/probe-xbrief.js";
9
12
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
10
13
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
11
14
  import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
15
+ import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
12
16
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
13
17
  import { runChecks } from "./checks.js";
14
18
  import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
@@ -24,6 +28,8 @@ import { runLocalSignpostChecks } from "./signpost-checks.js";
24
28
  import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
25
29
  import { defaultWhich } from "./which.js";
26
30
  const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
31
+ /** Next-action command for project-envelope behind-major (#2971). */
32
+ const XBRIEF_ENVELOPE_MIGRATE_COMMAND = "deft migrate:xbrief";
27
33
  /**
28
34
  * Read the `packageManager` field (Corepack) from a project package.json, or
29
35
  * null when absent/unreadable. Lets the doctor detect a pnpm project that has
@@ -341,6 +347,11 @@ export function cmdDoctor(args, seams = {}) {
341
347
  }
342
348
  sink.info("Checking gates-surface readiness (Taskfile include for deep-think agent gates)...");
343
349
  runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFinding, seams);
350
+ if (!jsonMode) {
351
+ sink.blank();
352
+ }
353
+ sink.info("Checking xBRIEF project envelope version...");
354
+ runXbriefEnvelopeVersionCheck(projectRoot, sink, addFinding, seams);
344
355
  let resolution = null;
345
356
  if (!runningInsideDeftRepo(projectRoot, seams)) {
346
357
  if (!jsonMode) {
@@ -770,6 +781,155 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
770
781
  suggestion: GATES_SURFACE_DEFT_REMEDIATION,
771
782
  });
772
783
  }
784
+ /**
785
+ * Fail closed when PROJECT-DEFINITION under an xbrief/ layout still declares
786
+ * envelope 0.6 while the framework schema is 0.8 (#2971). Greenfield (no
787
+ * project definition yet) and current 0.8 envelopes pass. Unreadable paths
788
+ * (test seams / permission) skip rather than false-positive fail. Behind-major
789
+ * records `deft migrate:xbrief` as the next action — layout rename alone is not
790
+ * enough. Distinct from deposited-schema (`stale-xbrief-schema-deposit`) which
791
+ * must NOT route to migrate:xbrief when only framework schema files are stale.
792
+ */
793
+ export function runXbriefEnvelopeVersionCheck(projectRoot, sink, addFinding, seams) {
794
+ const checkName = "xbrief-envelope-version";
795
+ const isFile = seams.isFile ?? ((p) => existsSync(p));
796
+ const readText = seams.readText ?? readTextSafe;
797
+ const targetVersion = VBRIEF_VERSION;
798
+ if (seams.probeXbriefEnvelope) {
799
+ const probe = seams.probeXbriefEnvelope(projectRoot);
800
+ emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding);
801
+ return;
802
+ }
803
+ let definitionPath;
804
+ try {
805
+ definitionPath = resolveProjectDefinitionPath(projectRoot);
806
+ }
807
+ catch {
808
+ // Pure vbrief/-only trees are already covered by layout migrate signposts.
809
+ const skipMessage = `${checkName}: skip -- legacy-only layout (use layout migrate first)`;
810
+ sink.info(skipMessage);
811
+ addFinding({
812
+ severity: "skip",
813
+ message: skipMessage,
814
+ check: checkName,
815
+ status: "skip",
816
+ reason: "legacy-only-layout",
817
+ });
818
+ return;
819
+ }
820
+ if (!isFile(definitionPath)) {
821
+ const skipMessage = `${checkName}: skip -- no PROJECT-DEFINITION yet (greenfield)`;
822
+ sink.info(skipMessage);
823
+ addFinding({
824
+ severity: "skip",
825
+ message: skipMessage,
826
+ check: checkName,
827
+ status: "skip",
828
+ reason: "no-project-definition",
829
+ });
830
+ return;
831
+ }
832
+ const text = readText(definitionPath);
833
+ if (text === null) {
834
+ // Unreadable path (permissions or injectable seams) is not proof of 0.6.
835
+ const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION unreadable`;
836
+ sink.info(skipMessage);
837
+ addFinding({
838
+ severity: "skip",
839
+ message: skipMessage,
840
+ check: checkName,
841
+ status: "skip",
842
+ reason: "unreadable",
843
+ });
844
+ return;
845
+ }
846
+ let declaredVersion = null;
847
+ try {
848
+ const parsed = JSON.parse(text);
849
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
850
+ declaredVersion = readDeclaredArtifactVersion(parsed);
851
+ }
852
+ else {
853
+ const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION is not a JSON object`;
854
+ sink.info(skipMessage);
855
+ addFinding({
856
+ severity: "skip",
857
+ message: skipMessage,
858
+ check: checkName,
859
+ status: "skip",
860
+ reason: "invalid-json-shape",
861
+ });
862
+ return;
863
+ }
864
+ }
865
+ catch {
866
+ const skipMessage = `${checkName}: skip -- PROJECT-DEFINITION JSON parse failed`;
867
+ sink.info(skipMessage);
868
+ addFinding({
869
+ severity: "skip",
870
+ message: skipMessage,
871
+ check: checkName,
872
+ status: "skip",
873
+ reason: "parse-error",
874
+ });
875
+ return;
876
+ }
877
+ const distance = classifyXbriefSchemaDistance(declaredVersion, targetVersion);
878
+ emitXbriefEnvelopeFinding(checkName, {
879
+ declaredVersion,
880
+ targetVersion,
881
+ distance,
882
+ stale: distance !== "current",
883
+ }, sink, addFinding);
884
+ }
885
+ function emitXbriefEnvelopeFinding(checkName, probe, sink, addFinding) {
886
+ if (probe.distance === "current") {
887
+ const okMessage = `${checkName}: current -- xBRIEFInfo@${probe.declaredVersion ?? probe.targetVersion} ` +
888
+ `(framework ${probe.targetVersion})`;
889
+ sink.success(okMessage);
890
+ addFinding({
891
+ severity: "skip",
892
+ message: okMessage,
893
+ check: checkName,
894
+ status: "current",
895
+ declared_version: probe.declaredVersion,
896
+ target_version: probe.targetVersion,
897
+ });
898
+ return;
899
+ }
900
+ if (probe.distance === "behind-minor") {
901
+ const warnMessage = `${checkName}: behind-minor -- declared ${probe.declaredVersion ?? "unknown"}, ` +
902
+ `framework ${probe.targetVersion}. Prefer \`deft migrate:xbrief\` when convenient.`;
903
+ sink.warn(warnMessage);
904
+ addFinding({
905
+ severity: "warning",
906
+ message: warnMessage,
907
+ check: checkName,
908
+ status: "behind-minor",
909
+ suggestion: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
910
+ declared_version: probe.declaredVersion,
911
+ target_version: probe.targetVersion,
912
+ next_command: XBRIEF_ENVELOPE_MIGRATE_COMMAND,
913
+ });
914
+ return;
915
+ }
916
+ // behind-major (declared 0.6 vs framework 0.8, dual half-state, missing version)
917
+ const nextCommand = XBRIEF_ENVELOPE_MIGRATE_COMMAND;
918
+ const message = `${checkName}: behind-major -- declared ${probe.declaredVersion ?? "unknown"}, ` +
919
+ `framework ${probe.targetVersion}. Next action: run \`${nextCommand}\` to bump project ` +
920
+ `JSON envelopes to xBRIEFInfo@${probe.targetVersion} (layout rename alone is not enough).`;
921
+ sink.error(message);
922
+ addFinding({
923
+ severity: "error",
924
+ message,
925
+ check: checkName,
926
+ status: "behind-major",
927
+ suggestion: nextCommand,
928
+ declared_version: probe.declaredVersion,
929
+ target_version: probe.targetVersion,
930
+ next_command: nextCommand,
931
+ });
932
+ }
773
933
  /**
774
934
  * Surface the resolved USER.md path + which search rung matched (#2271) so the
775
935
  * resolution boundary is visible in doctor output. Uses the shared first-hit-
@@ -150,5 +150,16 @@ export interface DoctorSeams {
150
150
  readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
151
151
  /** Live hook spawn probe for doctor --full (#2852). */
152
152
  readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
153
+ /**
154
+ * xBRIEF project-envelope staleness probe (#2971). Injected so doctor can
155
+ * fail closed on 0.6 project JSON under an xbrief/ layout without re-deriving
156
+ * the shared `probeXbriefStaleness` path in tests.
157
+ */
158
+ readonly probeXbriefEnvelope?: (projectRoot: string) => {
159
+ readonly declaredVersion: string | null;
160
+ readonly targetVersion: string;
161
+ readonly distance: "current" | "behind-minor" | "behind-major";
162
+ readonly stale: boolean;
163
+ };
153
164
  }
154
165
  //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Operator-facing escalation actions: file, list, resolve, batch-approve (#518).
3
+ */
4
+ import { type EscalationEvent, type EscalationType } from "./types.js";
5
+ export interface FileEscalationInput {
6
+ readonly projectRoot: string;
7
+ readonly type: string;
8
+ readonly title: string;
9
+ readonly body?: string;
10
+ readonly agentId?: string;
11
+ readonly contextRefs?: readonly string[];
12
+ readonly slaHours?: number;
13
+ readonly dangerous?: boolean;
14
+ readonly id?: string;
15
+ readonly now?: Date;
16
+ }
17
+ export declare function fileEscalation(input: FileEscalationInput): EscalationEvent;
18
+ export interface ResolveEscalationInput {
19
+ readonly projectRoot: string;
20
+ readonly id: string;
21
+ readonly decision: string;
22
+ readonly actor?: string;
23
+ readonly note?: string | null;
24
+ readonly answer?: string | null;
25
+ readonly now?: Date;
26
+ }
27
+ export type ResolveEscalationResult = {
28
+ readonly ok: true;
29
+ readonly event: EscalationEvent;
30
+ } | {
31
+ readonly ok: false;
32
+ readonly code: "not-found" | "already-resolved" | "invalid-decision";
33
+ readonly message: string;
34
+ };
35
+ export declare function resolveEscalation(input: ResolveEscalationInput): ResolveEscalationResult;
36
+ export interface BatchApproveInput {
37
+ readonly projectRoot: string;
38
+ /** Limit to these ids; empty/undefined = all open batch-eligible. */
39
+ readonly ids?: readonly string[];
40
+ readonly actor?: string;
41
+ readonly note?: string | null;
42
+ /** When false (default), skip `dangerous: true` items. */
43
+ readonly includeDangerous?: boolean;
44
+ readonly now?: Date;
45
+ }
46
+ export interface BatchApproveResult {
47
+ readonly approved: EscalationEvent[];
48
+ readonly skipped: Array<{
49
+ id: string;
50
+ reason: string;
51
+ }>;
52
+ }
53
+ /**
54
+ * Bulk-approve only `cmd_approval` and `question` open items.
55
+ * design_decision / approval / resource / external must use individual resolve.
56
+ * Dangerous items are skipped unless `includeDangerous` is true.
57
+ */
58
+ export declare function batchApproveEscalations(input: BatchApproveInput): BatchApproveResult;
59
+ export declare function listEscalationsFiltered(projectRoot: string, opts?: {
60
+ openOnly?: boolean;
61
+ type?: EscalationType;
62
+ }): EscalationEvent[];
63
+ //# sourceMappingURL=actions.d.ts.map