@deftai/directive-core 0.87.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/scanner.d.ts +11 -1
  28. package/dist/cache/scanner.js +29 -4
  29. package/dist/cache/task-cache/store.js +11 -7
  30. package/dist/capacity/backfill.js +10 -4
  31. package/dist/category-b-namespace/index.js +10 -4
  32. package/dist/check/gate-lists.js +1 -0
  33. package/dist/codebase/default-extractor.js +3 -0
  34. package/dist/codebase/map.js +11 -4
  35. package/dist/content-contracts/skills/helpers.d.ts +10 -0
  36. package/dist/content-contracts/skills/helpers.js +35 -0
  37. package/dist/deposit/copy-tree.d.ts +19 -1
  38. package/dist/deposit/copy-tree.js +134 -5
  39. package/dist/doctor/doctor-state.js +28 -4
  40. package/dist/doctor/main.d.ts +10 -0
  41. package/dist/doctor/main.js +208 -0
  42. package/dist/doctor/types.d.ts +11 -0
  43. package/dist/escalation/actions.d.ts +63 -0
  44. package/dist/escalation/actions.js +137 -0
  45. package/dist/escalation/index.d.ts +8 -0
  46. package/dist/escalation/index.js +8 -0
  47. package/dist/escalation/paths.d.ts +3 -0
  48. package/dist/escalation/paths.js +10 -0
  49. package/dist/escalation/store.d.ts +17 -0
  50. package/dist/escalation/store.js +172 -0
  51. package/dist/escalation/types.d.ts +73 -0
  52. package/dist/escalation/types.js +43 -0
  53. package/dist/eval/crud-telemetry.js +30 -10
  54. package/dist/eval/health.js +22 -7
  55. package/dist/eval/readback.js +11 -10
  56. package/dist/eval/run.js +32 -16
  57. package/dist/events/attribution-enrichment.js +10 -4
  58. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  59. package/dist/finish-loop/directive-finish-loop.js +239 -0
  60. package/dist/finish-loop/grant-gate.d.ts +31 -0
  61. package/dist/finish-loop/grant-gate.js +169 -0
  62. package/dist/finish-loop/index.d.ts +11 -0
  63. package/dist/finish-loop/index.js +11 -0
  64. package/dist/finish-loop/main.d.ts +35 -0
  65. package/dist/finish-loop/main.js +365 -0
  66. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  67. package/dist/finish-loop/pr-finish-loop.js +217 -0
  68. package/dist/finish-loop/progress.d.ts +17 -0
  69. package/dist/finish-loop/progress.js +45 -0
  70. package/dist/finish-loop/queue.d.ts +16 -0
  71. package/dist/finish-loop/queue.js +65 -0
  72. package/dist/finish-loop/types.d.ts +52 -0
  73. package/dist/finish-loop/types.js +10 -0
  74. package/dist/fs/contained-write.d.ts +101 -0
  75. package/dist/fs/contained-write.js +281 -0
  76. package/dist/fs/projection-containment.d.ts +18 -0
  77. package/dist/fs/projection-containment.js +40 -0
  78. package/dist/hooks/classify/classify.d.ts +10 -0
  79. package/dist/hooks/classify/classify.js +37 -0
  80. package/dist/hooks/classify/index.d.ts +14 -0
  81. package/dist/hooks/classify/index.js +13 -0
  82. package/dist/hooks/classify/paths.d.ts +22 -0
  83. package/dist/hooks/classify/paths.js +72 -0
  84. package/dist/hooks/classify/payload.d.ts +16 -0
  85. package/dist/hooks/classify/payload.js +45 -0
  86. package/dist/hooks/classify/stdin.d.ts +12 -0
  87. package/dist/hooks/classify/stdin.js +63 -0
  88. package/dist/hooks/classify/tool-name.d.ts +18 -0
  89. package/dist/hooks/classify/tool-name.js +85 -0
  90. package/dist/hooks/classify/types.d.ts +41 -0
  91. package/dist/hooks/classify/types.js +7 -0
  92. package/dist/hooks/dispatcher.d.ts +30 -15
  93. package/dist/hooks/dispatcher.js +326 -134
  94. package/dist/hooks/fixtures/cases.d.ts +44 -0
  95. package/dist/hooks/fixtures/cases.js +595 -0
  96. package/dist/hooks/fixtures/index.d.ts +2 -0
  97. package/dist/hooks/fixtures/index.js +2 -0
  98. package/dist/hooks/index.d.ts +2 -0
  99. package/dist/hooks/index.js +2 -0
  100. package/dist/hooks/tools.d.ts +31 -0
  101. package/dist/hooks/tools.js +74 -0
  102. package/dist/index.d.ts +4 -0
  103. package/dist/index.js +5 -0
  104. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  105. package/dist/init-deposit/agent-hooks.js +65 -10
  106. package/dist/init-deposit/gitignore.js +11 -3
  107. package/dist/init-deposit/headless-manifest.js +12 -4
  108. package/dist/init-deposit/hygiene.d.ts +16 -0
  109. package/dist/init-deposit/hygiene.js +26 -0
  110. package/dist/init-deposit/init-dispatch.js +28 -0
  111. package/dist/init-deposit/migrate.d.ts +1 -1
  112. package/dist/init-deposit/migrate.js +13 -3
  113. package/dist/init-deposit/prettierignore.js +12 -5
  114. package/dist/init-deposit/refresh.js +38 -7
  115. package/dist/init-deposit/scaffold.js +42 -29
  116. package/dist/init-deposit/xbrief-projections.js +17 -10
  117. package/dist/intake/candidates-log.js +46 -35
  118. package/dist/intake/github-body-cli.d.ts +6 -0
  119. package/dist/intake/github-body-cli.js +17 -0
  120. package/dist/intake/github-body.d.ts +46 -3
  121. package/dist/intake/github-body.js +191 -20
  122. package/dist/intake/issue-emit.d.ts +1 -0
  123. package/dist/intake/issue-emit.js +41 -23
  124. package/dist/intake/issue-ingest.js +11 -2
  125. package/dist/lifecycle/event-detect.js +12 -4
  126. package/dist/lifecycle/events.js +29 -20
  127. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  128. package/dist/packs/pack-render.d.ts +33 -0
  129. package/dist/packs/pack-render.js +172 -13
  130. package/dist/packs/quarantine-ext.d.ts +10 -0
  131. package/dist/packs/quarantine-ext.js +26 -2
  132. package/dist/plan-sequence/store.js +11 -4
  133. package/dist/policy/hotfix-criteria.d.ts +79 -0
  134. package/dist/policy/hotfix-criteria.js +197 -0
  135. package/dist/policy/index.d.ts +5 -0
  136. package/dist/policy/index.js +32 -1
  137. package/dist/policy/intent-ceiling.d.ts +79 -0
  138. package/dist/policy/intent-ceiling.js +181 -0
  139. package/dist/policy/no-deft-directive.d.ts +59 -0
  140. package/dist/policy/no-deft-directive.js +110 -0
  141. package/dist/policy/org-force-on-migration.d.ts +52 -0
  142. package/dist/policy/org-force-on-migration.js +269 -27
  143. package/dist/policy/require-human-merge.d.ts +75 -0
  144. package/dist/policy/require-human-merge.js +324 -0
  145. package/dist/policy/resolve.js +17 -6
  146. package/dist/policy/runtime-authority.d.ts +56 -2
  147. package/dist/policy/runtime-authority.js +297 -3
  148. package/dist/policy/write-fence.d.ts +78 -0
  149. package/dist/policy/write-fence.js +164 -0
  150. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  151. package/dist/pr-wait-mergeable/cascade.js +28 -0
  152. package/dist/preflight/evaluate.js +10 -0
  153. package/dist/product-signal/consent.js +21 -5
  154. package/dist/product-signal/submit.js +22 -12
  155. package/dist/release/build-dist-runner.js +5 -2
  156. package/dist/release/build-dist.d.ts +19 -1
  157. package/dist/release/build-dist.js +50 -2
  158. package/dist/release/native-steps.js +7 -2
  159. package/dist/release-publish/pipeline.d.ts +13 -0
  160. package/dist/release-publish/pipeline.js +46 -1
  161. package/dist/scope/audit-log.js +19 -24
  162. package/dist/scope/decompose.js +10 -5
  163. package/dist/scope/decomposed-refs.js +18 -3
  164. package/dist/scope/demote.js +9 -2
  165. package/dist/scope/undo.js +9 -2
  166. package/dist/session/release-availability.js +26 -6
  167. package/dist/session/ritual-sentinel.js +19 -12
  168. package/dist/session/session-start-hook.d.ts +3 -0
  169. package/dist/session/session-start-hook.js +15 -0
  170. package/dist/session/session-start.js +37 -0
  171. package/dist/staleness-tickler/state.js +26 -6
  172. package/dist/swarm/launch.js +13 -3
  173. package/dist/swarm/routing.js +9 -3
  174. package/dist/task-surface/index.js +24 -9
  175. package/dist/triage/bootstrap/gitignore.js +53 -10
  176. package/dist/triage/cache-path.js +10 -3
  177. package/dist/triage/welcome/summary.js +11 -4
  178. package/dist/triage/welcome/writers.js +45 -16
  179. package/dist/validate-content/validate-links.js +5 -0
  180. package/dist/value/readback.js +11 -6
  181. package/dist/vbrief-activate/activate.js +12 -4
  182. package/dist/vbrief-build/project-definition-io.js +14 -6
  183. package/dist/verify-source/contained-writes.d.ts +59 -0
  184. package/dist/verify-source/contained-writes.js +272 -0
  185. package/dist/verify-source/cursor-tier1.js +7 -2
  186. package/dist/verify-source/index.d.ts +1 -0
  187. package/dist/verify-source/index.js +1 -0
  188. package/dist/verify-source/openclaw-tier1.js +19 -1
  189. package/dist/verify-source/verify-stubs.js +3 -0
  190. package/dist/xbrief-migrate/migrate-project.js +26 -12
  191. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  192. package/dist/xbrief-migrate/transforms.js +37 -14
  193. package/package.json +16 -3
@@ -1,6 +1,7 @@
1
- import { 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,13 +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";
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";
5
8
  import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "../policy/plan-extensions.js";
6
9
  import { loadProjectDefinition } from "../policy/resolve.js";
7
10
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
11
+ import { classifyXbriefSchemaDistance } from "../staleness-tickler/probe-xbrief.js";
8
12
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
9
13
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
10
14
  import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
15
+ import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
11
16
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
12
17
  import { runChecks } from "./checks.js";
13
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";
@@ -23,6 +28,8 @@ import { runLocalSignpostChecks } from "./signpost-checks.js";
23
28
  import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
24
29
  import { defaultWhich } from "./which.js";
25
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";
26
33
  /**
27
34
  * Read the `packageManager` field (Corepack) from a project package.json, or
28
35
  * null when absent/unreadable. Lets the doctor detect a pnpm project that has
@@ -62,6 +69,53 @@ export function cmdDoctor(args, seams = {}) {
62
69
  const consumerContext = resolve(projectRoot) !== resolve(frameworkRoot);
63
70
  const whichFn = seams.whichFn ?? defaultWhich;
64
71
  const nowFn = seams.now ?? (() => new Date());
72
+ // #2926: official root opt-out — short-circuit Directive doctor when clean;
73
+ // diagnose flag+deposit inconsistency (warn; exit dirty).
74
+ const optOut = detectNoDeftDirective(projectRoot);
75
+ if (optOut.present) {
76
+ if (optOut.inconsistent) {
77
+ const message = `${NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE} (${NO_DEFT_DIRECTIVE_DISABLED_MESSAGE})`;
78
+ if (jsonMode) {
79
+ const payload = {
80
+ status: "disabled-inconsistent",
81
+ disabled: true,
82
+ disabled_via: NO_DEFT_DIRECTIVE_FLAG_NAME,
83
+ inconsistent: true,
84
+ inconsistent_policy: NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY,
85
+ deposit_present: true,
86
+ message,
87
+ findings: [
88
+ {
89
+ severity: "warning",
90
+ message,
91
+ check: "no-deft-directive",
92
+ policy: NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY,
93
+ },
94
+ ],
95
+ };
96
+ process.stdout.write(`${pythonJsonDump(payload)}\n`);
97
+ }
98
+ else if (!quietMode) {
99
+ process.stderr.write(`${message} [policy=${NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY}]\n`);
100
+ }
101
+ return 1;
102
+ }
103
+ if (jsonMode) {
104
+ const payload = {
105
+ status: "disabled",
106
+ disabled: true,
107
+ disabled_via: NO_DEFT_DIRECTIVE_FLAG_NAME,
108
+ inconsistent: false,
109
+ deposit_present: false,
110
+ message: NO_DEFT_DIRECTIVE_DISABLED_MESSAGE,
111
+ };
112
+ process.stdout.write(`${pythonJsonDump(payload)}\n`);
113
+ }
114
+ else if (!quietMode) {
115
+ process.stdout.write(`${NO_DEFT_DIRECTIVE_DISABLED_MESSAGE}\n`);
116
+ }
117
+ return 0;
118
+ }
65
119
  if (!fullMode) {
66
120
  const state = (seams.readState ?? readState)(projectRoot);
67
121
  const decision = decideThrottle(state, nowFn());
@@ -293,6 +347,11 @@ export function cmdDoctor(args, seams = {}) {
293
347
  }
294
348
  sink.info("Checking gates-surface readiness (Taskfile include for deep-think agent gates)...");
295
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);
296
355
  let resolution = null;
297
356
  if (!runningInsideDeftRepo(projectRoot, seams)) {
298
357
  if (!jsonMode) {
@@ -722,6 +781,155 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
722
781
  suggestion: GATES_SURFACE_DEFT_REMEDIATION,
723
782
  });
724
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
+ }
725
933
  /**
726
934
  * Surface the resolved USER.md path + which search rung matched (#2271) so the
727
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
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Operator-facing escalation actions: file, list, resolve, batch-approve (#518).
3
+ */
4
+ import { randomBytes } from "node:crypto";
5
+ import { listEscalations, listOpenEscalations, loadEscalation, parseEscalation, saveEscalation, utcIso, validateEscalationType, } from "./store.js";
6
+ import { BATCH_APPROVABLE_TYPES, DEFAULT_SLA_HOURS, ESCALATION_SCHEMA_VERSION, isBatchApprovableType, isEscalationDecision, } from "./types.js";
7
+ function newEscalationId(now) {
8
+ const ts = (now ?? new Date())
9
+ .toISOString()
10
+ .replace(/[-:TZ.]/g, "")
11
+ .slice(0, 14);
12
+ const suffix = randomBytes(3).toString("hex");
13
+ return `esc-${ts}-${suffix}`;
14
+ }
15
+ export function fileEscalation(input) {
16
+ const type = validateEscalationType(input.type);
17
+ const title = input.title.trim();
18
+ if (title.length === 0) {
19
+ throw new Error("escalation title must be non-empty");
20
+ }
21
+ const createdAt = utcIso(input.now);
22
+ const event = {
23
+ schemaVersion: ESCALATION_SCHEMA_VERSION,
24
+ id: input.id?.trim() || newEscalationId(input.now),
25
+ agentId: (input.agentId ?? "agent").trim() || "agent",
26
+ type,
27
+ title,
28
+ body: input.body ?? "",
29
+ contextRefs: input.contextRefs ?? [],
30
+ createdAt,
31
+ slaHours: input.slaHours !== undefined && input.slaHours > 0 ? input.slaHours : DEFAULT_SLA_HOURS[type],
32
+ status: "open",
33
+ dangerous: input.dangerous === true,
34
+ resolution: null,
35
+ };
36
+ // Round-trip through parse to guarantee store shape.
37
+ const parsed = parseEscalation(event);
38
+ if (parsed === null) {
39
+ throw new Error("internal: filed escalation failed validation");
40
+ }
41
+ saveEscalation(input.projectRoot, parsed);
42
+ return parsed;
43
+ }
44
+ export function resolveEscalation(input) {
45
+ const decisionRaw = input.decision.trim().toLowerCase();
46
+ if (!isEscalationDecision(decisionRaw)) {
47
+ return {
48
+ ok: false,
49
+ code: "invalid-decision",
50
+ message: `invalid decision '${input.decision}'; expected one of: approved, denied, answered, dismissed`,
51
+ };
52
+ }
53
+ const existing = loadEscalation(input.projectRoot, input.id);
54
+ if (existing === null) {
55
+ return {
56
+ ok: false,
57
+ code: "not-found",
58
+ message: `escalation not found: ${input.id}`,
59
+ };
60
+ }
61
+ if (existing.status !== "open") {
62
+ return {
63
+ ok: false,
64
+ code: "already-resolved",
65
+ message: `escalation already resolved: ${input.id}`,
66
+ };
67
+ }
68
+ const resolved = {
69
+ ...existing,
70
+ status: "resolved",
71
+ resolution: {
72
+ decision: decisionRaw,
73
+ resolvedAt: utcIso(input.now),
74
+ resolvedBy: (input.actor ?? "operator").trim() || "operator",
75
+ note: input.note ?? null,
76
+ answer: input.answer ?? null,
77
+ },
78
+ };
79
+ saveEscalation(input.projectRoot, resolved);
80
+ return { ok: true, event: resolved };
81
+ }
82
+ /**
83
+ * Bulk-approve only `cmd_approval` and `question` open items.
84
+ * design_decision / approval / resource / external must use individual resolve.
85
+ * Dangerous items are skipped unless `includeDangerous` is true.
86
+ */
87
+ export function batchApproveEscalations(input) {
88
+ const open = listOpenEscalations(input.projectRoot);
89
+ const idFilter = input.ids !== undefined && input.ids.length > 0
90
+ ? new Set(input.ids.map((x) => x.trim()).filter((x) => x.length > 0))
91
+ : null;
92
+ const approved = [];
93
+ const skipped = [];
94
+ for (const event of open) {
95
+ if (idFilter !== null && !idFilter.has(event.id)) {
96
+ continue;
97
+ }
98
+ if (!isBatchApprovableType(event.type)) {
99
+ skipped.push({
100
+ id: event.id,
101
+ reason: `type ${event.type} is not batch-approvable (use escalation:resolve); allowed bulk types: ${BATCH_APPROVABLE_TYPES.join(", ")}`,
102
+ });
103
+ continue;
104
+ }
105
+ if (event.dangerous && input.includeDangerous !== true) {
106
+ skipped.push({
107
+ id: event.id,
108
+ reason: "dangerous=true — resolve individually or pass --include-dangerous",
109
+ });
110
+ continue;
111
+ }
112
+ const decision = event.type === "question" ? "answered" : "approved";
113
+ const result = resolveEscalation({
114
+ projectRoot: input.projectRoot,
115
+ id: event.id,
116
+ decision,
117
+ actor: input.actor,
118
+ note: input.note ?? "batch-approve",
119
+ answer: event.type === "question" ? (input.note ?? "acknowledged") : null,
120
+ now: input.now,
121
+ });
122
+ if (result.ok) {
123
+ approved.push(result.event);
124
+ }
125
+ else {
126
+ skipped.push({ id: event.id, reason: result.message });
127
+ }
128
+ }
129
+ return { approved, skipped };
130
+ }
131
+ export function listEscalationsFiltered(projectRoot, opts = {}) {
132
+ const base = opts.openOnly === true ? listOpenEscalations(projectRoot) : listEscalations(projectRoot);
133
+ if (opts.type === undefined)
134
+ return base;
135
+ return base.filter((e) => e.type === opts.type);
136
+ }
137
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Typed escalation channel — schema + local queue (#518 slim / #2948 Wave 5).
3
+ */
4
+ export * from "./actions.js";
5
+ export * from "./paths.js";
6
+ export * from "./store.js";
7
+ export * from "./types.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Typed escalation channel — schema + local queue (#518 slim / #2948 Wave 5).
3
+ */
4
+ export * from "./actions.js";
5
+ export * from "./paths.js";
6
+ export * from "./store.js";
7
+ export * from "./types.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ export declare function escalationsDir(projectRoot: string): string;
2
+ export declare function escalationPath(projectRoot: string, escalationId: string): string;
3
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { join } from "node:path";
2
+ import { ESCALATION_DIR } from "./types.js";
3
+ export function escalationsDir(projectRoot) {
4
+ return join(projectRoot, ...ESCALATION_DIR.split("/"));
5
+ }
6
+ export function escalationPath(projectRoot, escalationId) {
7
+ const safe = escalationId.replace(/[^a-zA-Z0-9._-]/g, "_");
8
+ return join(escalationsDir(projectRoot), `${safe}.json`);
9
+ }
10
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Disk store for typed escalations under `.deft/escalations/` (#518).
3
+ */
4
+ import { type EscalationEvent, type EscalationType } from "./types.js";
5
+ export declare function utcIso(now?: Date): string;
6
+ /**
7
+ * Parse an escalation JSON object.
8
+ * Returns null when type/status/required fields are invalid (tests reject invalid types).
9
+ */
10
+ export declare function parseEscalation(raw: unknown): EscalationEvent | null;
11
+ /** Validate type string alone (unit tests + CLI). */
12
+ export declare function validateEscalationType(type: string): EscalationType;
13
+ export declare function saveEscalation(projectRoot: string, event: EscalationEvent): void;
14
+ export declare function loadEscalation(projectRoot: string, escalationId: string): EscalationEvent | null;
15
+ export declare function listEscalations(projectRoot: string): EscalationEvent[];
16
+ export declare function listOpenEscalations(projectRoot: string): EscalationEvent[];
17
+ //# sourceMappingURL=store.d.ts.map