@deftai/directive-core 0.88.0 → 0.90.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 (202) 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/constants.d.ts +1 -1
  33. package/dist/doctor/constants.js +2 -0
  34. package/dist/doctor/doctor-state.js +28 -4
  35. package/dist/doctor/flags.js +21 -1
  36. package/dist/doctor/index.d.ts +1 -0
  37. package/dist/doctor/index.js +1 -0
  38. package/dist/doctor/main.d.ts +10 -0
  39. package/dist/doctor/main.js +182 -0
  40. package/dist/doctor/openclaw-skills.d.ts +109 -0
  41. package/dist/doctor/openclaw-skills.js +463 -0
  42. package/dist/doctor/types.d.ts +22 -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/hooks/classify/classify.d.ts +10 -0
  77. package/dist/hooks/classify/classify.js +37 -0
  78. package/dist/hooks/classify/index.d.ts +14 -0
  79. package/dist/hooks/classify/index.js +13 -0
  80. package/dist/hooks/classify/paths.d.ts +22 -0
  81. package/dist/hooks/classify/paths.js +72 -0
  82. package/dist/hooks/classify/payload.d.ts +16 -0
  83. package/dist/hooks/classify/payload.js +45 -0
  84. package/dist/hooks/classify/stdin.d.ts +12 -0
  85. package/dist/hooks/classify/stdin.js +63 -0
  86. package/dist/hooks/classify/tool-name.d.ts +18 -0
  87. package/dist/hooks/classify/tool-name.js +85 -0
  88. package/dist/hooks/classify/types.d.ts +41 -0
  89. package/dist/hooks/classify/types.js +7 -0
  90. package/dist/hooks/dispatcher.d.ts +21 -18
  91. package/dist/hooks/dispatcher.js +232 -168
  92. package/dist/hooks/fixtures/cases.d.ts +44 -0
  93. package/dist/hooks/fixtures/cases.js +595 -0
  94. package/dist/hooks/fixtures/index.d.ts +2 -0
  95. package/dist/hooks/fixtures/index.js +2 -0
  96. package/dist/hooks/index.d.ts +2 -0
  97. package/dist/hooks/index.js +2 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.js +6 -0
  100. package/dist/init-deposit/agent-hooks.js +27 -8
  101. package/dist/init-deposit/gitignore.js +11 -3
  102. package/dist/init-deposit/headless-manifest.js +12 -4
  103. package/dist/init-deposit/migrate.d.ts +1 -1
  104. package/dist/init-deposit/migrate.js +13 -3
  105. package/dist/init-deposit/prettierignore.js +10 -3
  106. package/dist/init-deposit/scaffold.js +35 -26
  107. package/dist/init-deposit/xbrief-projections.js +11 -4
  108. package/dist/intake/candidates-log.js +46 -35
  109. package/dist/intake/github-body-cli.d.ts +6 -0
  110. package/dist/intake/github-body-cli.js +17 -0
  111. package/dist/intake/github-body.d.ts +46 -3
  112. package/dist/intake/github-body.js +191 -20
  113. package/dist/intake/issue-emit.d.ts +1 -0
  114. package/dist/intake/issue-emit.js +41 -23
  115. package/dist/lifecycle/event-detect.js +12 -4
  116. package/dist/lifecycle/events.js +31 -20
  117. package/dist/lifecycle/index.d.ts +1 -0
  118. package/dist/lifecycle/index.js +1 -0
  119. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  120. package/dist/lifecycle/stats.d.ts +53 -0
  121. package/dist/lifecycle/stats.js +286 -0
  122. package/dist/packs/pack-render.js +19 -6
  123. package/dist/plan-sequence/store.js +11 -4
  124. package/dist/policy/hotfix-criteria.d.ts +79 -0
  125. package/dist/policy/hotfix-criteria.js +197 -0
  126. package/dist/policy/index.d.ts +4 -0
  127. package/dist/policy/index.js +31 -1
  128. package/dist/policy/intent-ceiling.d.ts +79 -0
  129. package/dist/policy/intent-ceiling.js +181 -0
  130. package/dist/policy/no-deft-directive.js +10 -3
  131. package/dist/policy/org-force-on-migration.js +9 -5
  132. package/dist/policy/require-human-merge.d.ts +75 -0
  133. package/dist/policy/require-human-merge.js +324 -0
  134. package/dist/policy/resolve.js +17 -6
  135. package/dist/policy/runtime-authority.d.ts +15 -2
  136. package/dist/policy/runtime-authority.js +23 -3
  137. package/dist/policy/write-fence.d.ts +78 -0
  138. package/dist/policy/write-fence.js +164 -0
  139. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  140. package/dist/pr-wait-mergeable/cascade.js +28 -0
  141. package/dist/preflight/evaluate.js +10 -0
  142. package/dist/product-signal/consent.js +21 -5
  143. package/dist/product-signal/submit.js +22 -12
  144. package/dist/release/build-dist-runner.js +5 -2
  145. package/dist/release/build-dist.d.ts +19 -1
  146. package/dist/release/build-dist.js +50 -2
  147. package/dist/release/native-steps.js +7 -2
  148. package/dist/release/spawn.js +13 -0
  149. package/dist/release-e2e/git-ops.d.ts +7 -0
  150. package/dist/release-e2e/git-ops.js +35 -2
  151. package/dist/release-publish/pipeline.d.ts +13 -0
  152. package/dist/release-publish/pipeline.js +46 -1
  153. package/dist/scope/audit-log.js +19 -24
  154. package/dist/scope/decompose.js +10 -5
  155. package/dist/scope/decomposed-refs.js +18 -3
  156. package/dist/scope/demote.js +9 -2
  157. package/dist/scope/undo.js +9 -2
  158. package/dist/session/index.d.ts +2 -0
  159. package/dist/session/index.js +2 -0
  160. package/dist/session/process-cost-constants.d.ts +9 -0
  161. package/dist/session/process-cost-constants.js +11 -0
  162. package/dist/session/process-cost.d.ts +53 -0
  163. package/dist/session/process-cost.js +82 -0
  164. package/dist/session/release-availability.js +26 -6
  165. package/dist/session/ritual-sentinel.d.ts +5 -0
  166. package/dist/session/ritual-sentinel.js +31 -13
  167. package/dist/session/session-ready.d.ts +67 -0
  168. package/dist/session/session-ready.js +264 -0
  169. package/dist/session/session-start.d.ts +56 -1
  170. package/dist/session/session-start.js +384 -23
  171. package/dist/session/verify-session-ritual.d.ts +8 -0
  172. package/dist/session/verify-session-ritual.js +95 -35
  173. package/dist/staleness-tickler/state.js +26 -6
  174. package/dist/swarm/launch.js +13 -3
  175. package/dist/swarm/routing.js +9 -3
  176. package/dist/task-surface/index.js +24 -9
  177. package/dist/tool-events/classify.d.ts +20 -0
  178. package/dist/tool-events/classify.js +634 -0
  179. package/dist/tool-events/index.d.ts +10 -0
  180. package/dist/tool-events/index.js +10 -0
  181. package/dist/tool-events/summarize.d.ts +34 -0
  182. package/dist/tool-events/summarize.js +93 -0
  183. package/dist/tool-events/types.d.ts +52 -0
  184. package/dist/tool-events/types.js +13 -0
  185. package/dist/triage/bootstrap/gitignore.js +53 -10
  186. package/dist/triage/cache-path.js +10 -3
  187. package/dist/triage/welcome/summary.js +11 -4
  188. package/dist/triage/welcome/writers.js +45 -16
  189. package/dist/validate-content/validate-links.js +5 -0
  190. package/dist/value/readback.js +11 -6
  191. package/dist/vbrief-activate/activate.js +12 -4
  192. package/dist/vbrief-build/project-definition-io.js +14 -6
  193. package/dist/verify-source/contained-writes.d.ts +59 -0
  194. package/dist/verify-source/contained-writes.js +272 -0
  195. package/dist/verify-source/index.d.ts +1 -0
  196. package/dist/verify-source/index.js +1 -0
  197. package/dist/verify-source/openclaw-tier1.js +14 -1
  198. package/dist/verify-source/verify-stubs.js +3 -0
  199. package/dist/xbrief-migrate/migrate-project.js +26 -12
  200. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  201. package/dist/xbrief-migrate/transforms.js +37 -14
  202. package/package.json +19 -3
@@ -0,0 +1,109 @@
1
+ /**
2
+ * OpenClaw always-pin skill detect + doctor --fix wire (#3001).
3
+ *
4
+ * When OpenClaw signals are present, doctor checks the main workspace skills
5
+ * root for the four always-pin skills (#2508). Missing pins emit a warning with
6
+ * remediation `deft doctor --fix`. Under fixMode, pins are symlinked (preferred)
7
+ * or copied from the installed content package into the target skills dir.
8
+ *
9
+ * Multi-seat (`workspace-*`) targets only when `--openclaw-all-agents` is set.
10
+ */
11
+ import type { OutputSink } from "./output.js";
12
+ import type { DoctorSeams, Finding } from "./types.js";
13
+ /** Stable doctor check id for JSON findings. */
14
+ export declare const OPENCLAW_SKILL_PINS_CHECK = "openclaw-skill-pins";
15
+ /** Always-pin skill directory names (#2508 / cold-start). */
16
+ export declare const OPENCLAW_ALWAYS_PIN_SKILLS: readonly ["deft-directive-build", "deft-directive-pre-pr", "deft-directive-review-cycle", "deft-directive-swarm"];
17
+ export type OpenClawAlwaysPinSkill = (typeof OPENCLAW_ALWAYS_PIN_SKILLS)[number];
18
+ /** Env keys that count as strong OpenClaw signals (#3001). */
19
+ export declare const OPENCLAW_ENV_SIGNAL_KEYS: readonly ["OPENCLAW", "DEFT_PROBE_OPENCLAW", "DEFT_AGENT_RUNTIME", "OPENCLAW_STATE_DIR"];
20
+ export interface OpenClawSkillPinsSeams {
21
+ readonly env?: NodeJS.ProcessEnv;
22
+ readonly homeDir?: () => string;
23
+ readonly isDir?: (path: string) => boolean;
24
+ readonly isFile?: (path: string) => boolean;
25
+ readonly pathExists?: (path: string) => boolean;
26
+ readonly readDir?: (path: string) => string[];
27
+ readonly lstatKind?: (path: string) => "file" | "dir" | "symlink" | "other" | "missing";
28
+ readonly mkdirp?: (path: string) => void;
29
+ readonly symlinkDir?: (target: string, path: string) => void;
30
+ readonly copyDir?: (src: string, dst: string) => void;
31
+ readonly removePath?: (path: string) => void;
32
+ readonly contentRootFor?: (frameworkRoot: string) => string;
33
+ readonly isTty?: () => boolean;
34
+ readonly readYn?: (prompt: string, defaultYes: boolean) => boolean;
35
+ }
36
+ export interface OpenClawPinAssessment {
37
+ readonly skillsDir: string;
38
+ readonly present: readonly OpenClawAlwaysPinSkill[];
39
+ readonly missing: readonly OpenClawAlwaysPinSkill[];
40
+ readonly divergent: readonly OpenClawAlwaysPinSkill[];
41
+ }
42
+ export interface OpenClawDetectResult {
43
+ readonly detected: boolean;
44
+ readonly reasons: readonly string[];
45
+ readonly stateDir: string;
46
+ readonly mainSkillsDir: string;
47
+ }
48
+ export type PinInstallMethod = "symlink" | "copy" | "skipped" | "already-present";
49
+ export interface PinInstallResult {
50
+ readonly skillId: OpenClawAlwaysPinSkill;
51
+ readonly method: PinInstallMethod;
52
+ readonly target: string;
53
+ readonly source: string;
54
+ readonly detail?: string;
55
+ }
56
+ /**
57
+ * Resolve OpenClaw state directory: OPENCLAW_STATE_DIR or ~/.openclaw.
58
+ */
59
+ export declare function resolveOpenClawStateDir(env?: NodeJS.ProcessEnv, home?: string): string;
60
+ /**
61
+ * Main agent workspace skills root under the state dir.
62
+ */
63
+ export declare function resolveMainSkillsDir(stateDir: string): string;
64
+ /**
65
+ * Detect OpenClaw from env signals and/or presence of the state directory.
66
+ */
67
+ export declare function detectOpenClaw(env?: NodeJS.ProcessEnv, options?: {
68
+ homeDir?: string;
69
+ isDir?: (path: string) => boolean;
70
+ }): OpenClawDetectResult;
71
+ /**
72
+ * List in-scope skills directories. Default: main only.
73
+ * With allAgents: main + every workspace- star /skills under the state dir.
74
+ */
75
+ export declare function listInScopeSkillsDirs(stateDir: string, allAgents: boolean, seams?: Pick<OpenClawSkillPinsSeams, "isDir" | "readDir">): string[];
76
+ /**
77
+ * Resolve content package skills/<id> for a pin.
78
+ */
79
+ export declare function resolvePinSourceDir(contentBase: string, skillId: string): string;
80
+ /**
81
+ * Assess which always-pins are present / missing / divergent in a skills root.
82
+ *
83
+ * - present: directory (or symlink) with SKILL.md
84
+ * - missing: path does not exist
85
+ * - divergent: path exists but is not a usable pin (file, empty dir, broken link)
86
+ */
87
+ export declare function assessOpenClawPins(skillsDir: string, seams?: Pick<OpenClawSkillPinsSeams, "isDir" | "isFile" | "pathExists" | "lstatKind">): OpenClawPinAssessment;
88
+ /**
89
+ * Install one pin into a skills root. Prefer symlink; copy on failure.
90
+ * Never deletes user skills. Divergent targets require force or TTY confirm.
91
+ */
92
+ export declare function installOpenClawPin(skillId: OpenClawAlwaysPinSkill, sourceDir: string, skillsDir: string, options?: {
93
+ force?: boolean;
94
+ allowOverwrite?: boolean;
95
+ }, seams?: OpenClawSkillPinsSeams): PinInstallResult;
96
+ export interface RunOpenClawSkillPinsOptions {
97
+ readonly frameworkRoot: string;
98
+ readonly fixMode: boolean;
99
+ readonly jsonMode: boolean;
100
+ readonly force: boolean;
101
+ readonly allAgents: boolean;
102
+ readonly seams?: DoctorSeams & OpenClawSkillPinsSeams;
103
+ }
104
+ /**
105
+ * Doctor check: detect OpenClaw + missing always-pins; optionally fix.
106
+ * No-ops when OpenClaw signals are absent.
107
+ */
108
+ export declare function runOpenClawSkillPinsCheck(sink: OutputSink, addFinding: (finding: Finding) => void, options: RunOpenClawSkillPinsOptions): void;
109
+ //# sourceMappingURL=openclaw-skills.d.ts.map
@@ -0,0 +1,463 @@
1
+ /**
2
+ * OpenClaw always-pin skill detect + doctor --fix wire (#3001).
3
+ *
4
+ * When OpenClaw signals are present, doctor checks the main workspace skills
5
+ * root for the four always-pin skills (#2508). Missing pins emit a warning with
6
+ * remediation `deft doctor --fix`. Under fixMode, pins are symlinked (preferred)
7
+ * or copied from the installed content package into the target skills dir.
8
+ *
9
+ * Multi-seat (`workspace-*`) targets only when `--openclaw-all-agents` is set.
10
+ */
11
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, statSync, symlinkSync, } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join, resolve } from "node:path";
14
+ import { contentRoot } from "../content-root.js";
15
+ /** Stable doctor check id for JSON findings. */
16
+ export const OPENCLAW_SKILL_PINS_CHECK = "openclaw-skill-pins";
17
+ /** Always-pin skill directory names (#2508 / cold-start). */
18
+ export const OPENCLAW_ALWAYS_PIN_SKILLS = [
19
+ "deft-directive-build",
20
+ "deft-directive-pre-pr",
21
+ "deft-directive-review-cycle",
22
+ "deft-directive-swarm",
23
+ ];
24
+ /** Env keys that count as strong OpenClaw signals (#3001). */
25
+ export const OPENCLAW_ENV_SIGNAL_KEYS = [
26
+ "OPENCLAW",
27
+ "DEFT_PROBE_OPENCLAW",
28
+ "DEFT_AGENT_RUNTIME",
29
+ "OPENCLAW_STATE_DIR",
30
+ ];
31
+ const DOC_OPENCLAW_HOST = "docs/openclaw-agent-host.md";
32
+ const DOC_HOST_LIFECYCLE = "contracts/host-lifecycle-duties.md";
33
+ const DOC_SKILL_PINS = "docs/skill-pin-policy.md";
34
+ const REMEDIATION_FIX = "deft doctor --fix";
35
+ const REMEDIATION_ALL_AGENTS = "deft doctor --fix --openclaw-all-agents";
36
+ function defaultIsDir(path) {
37
+ try {
38
+ return statSync(path).isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ function defaultIsFile(path) {
45
+ try {
46
+ return statSync(path).isFile();
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ function defaultPathExists(path) {
53
+ return existsSync(path);
54
+ }
55
+ function defaultLstatKind(path) {
56
+ try {
57
+ const st = lstatSync(path);
58
+ if (st.isSymbolicLink())
59
+ return "symlink";
60
+ if (st.isDirectory())
61
+ return "dir";
62
+ if (st.isFile())
63
+ return "file";
64
+ return "other";
65
+ }
66
+ catch {
67
+ return "missing";
68
+ }
69
+ }
70
+ function defaultReadDir(path) {
71
+ try {
72
+ return readdirSync(path);
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ function envTruthy(value) {
79
+ if (value === undefined)
80
+ return false;
81
+ const v = value.trim().toLowerCase();
82
+ if (v === "" || v === "0" || v === "false" || v === "no" || v === "off")
83
+ return false;
84
+ return true;
85
+ }
86
+ /**
87
+ * Resolve OpenClaw state directory: OPENCLAW_STATE_DIR or ~/.openclaw.
88
+ */
89
+ export function resolveOpenClawStateDir(env = process.env, home = homedir()) {
90
+ const fromEnv = env.OPENCLAW_STATE_DIR?.trim();
91
+ if (fromEnv)
92
+ return resolve(fromEnv);
93
+ return resolve(home, ".openclaw");
94
+ }
95
+ /**
96
+ * Main agent workspace skills root under the state dir.
97
+ */
98
+ export function resolveMainSkillsDir(stateDir) {
99
+ return join(stateDir, "workspace", "skills");
100
+ }
101
+ /**
102
+ * Detect OpenClaw from env signals and/or presence of the state directory.
103
+ */
104
+ export function detectOpenClaw(env = process.env, options = {}) {
105
+ const home = options.homeDir ?? homedir();
106
+ const isDir = options.isDir ?? defaultIsDir;
107
+ const stateDir = resolveOpenClawStateDir(env, home);
108
+ const reasons = [];
109
+ if (envTruthy(env.OPENCLAW))
110
+ reasons.push("env:OPENCLAW");
111
+ if (envTruthy(env.DEFT_PROBE_OPENCLAW))
112
+ reasons.push("env:DEFT_PROBE_OPENCLAW");
113
+ if ((env.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase() === "openclaw") {
114
+ reasons.push("env:DEFT_AGENT_RUNTIME=openclaw");
115
+ }
116
+ if (env.OPENCLAW_STATE_DIR?.trim())
117
+ reasons.push("env:OPENCLAW_STATE_DIR");
118
+ if (isDir(stateDir))
119
+ reasons.push(`dir:${stateDir}`);
120
+ return {
121
+ detected: reasons.length > 0,
122
+ reasons,
123
+ stateDir,
124
+ mainSkillsDir: resolveMainSkillsDir(stateDir),
125
+ };
126
+ }
127
+ /**
128
+ * List in-scope skills directories. Default: main only.
129
+ * With allAgents: main + every workspace- star /skills under the state dir.
130
+ */
131
+ export function listInScopeSkillsDirs(stateDir, allAgents, seams = {}) {
132
+ const isDir = seams.isDir ?? defaultIsDir;
133
+ const readDir = seams.readDir ?? defaultReadDir;
134
+ const main = resolveMainSkillsDir(stateDir);
135
+ const dirs = [main];
136
+ if (!allAgents)
137
+ return dirs;
138
+ for (const name of readDir(stateDir)) {
139
+ if (!name.startsWith("workspace-"))
140
+ continue;
141
+ const workspaceDir = join(stateDir, name);
142
+ if (!isDir(workspaceDir))
143
+ continue;
144
+ const skillsDir = join(workspaceDir, "skills");
145
+ if (!dirs.includes(skillsDir))
146
+ dirs.push(skillsDir);
147
+ }
148
+ return dirs;
149
+ }
150
+ /**
151
+ * Resolve content package skills/<id> for a pin.
152
+ */
153
+ export function resolvePinSourceDir(contentBase, skillId) {
154
+ return join(contentBase, "skills", skillId);
155
+ }
156
+ function skillHasBody(skillDir, isFile, isDir) {
157
+ if (!isDir(skillDir) && !existsSync(skillDir))
158
+ return false;
159
+ // Accept dir or symlink-to-dir that contains SKILL.md
160
+ return isFile(join(skillDir, "SKILL.md"));
161
+ }
162
+ /**
163
+ * Assess which always-pins are present / missing / divergent in a skills root.
164
+ *
165
+ * - present: directory (or symlink) with SKILL.md
166
+ * - missing: path does not exist
167
+ * - divergent: path exists but is not a usable pin (file, empty dir, broken link)
168
+ */
169
+ export function assessOpenClawPins(skillsDir, seams = {}) {
170
+ const isDir = seams.isDir ?? defaultIsDir;
171
+ const isFile = seams.isFile ?? defaultIsFile;
172
+ const pathExists = seams.pathExists ?? defaultPathExists;
173
+ const lstatKind = seams.lstatKind ?? defaultLstatKind;
174
+ const present = [];
175
+ const missing = [];
176
+ const divergent = [];
177
+ for (const skillId of OPENCLAW_ALWAYS_PIN_SKILLS) {
178
+ const target = join(skillsDir, skillId);
179
+ const kind = lstatKind(target);
180
+ if (kind === "missing") {
181
+ missing.push(skillId);
182
+ continue;
183
+ }
184
+ if (skillHasBody(target, isFile, isDir)) {
185
+ present.push(skillId);
186
+ continue;
187
+ }
188
+ // Path exists but is not a usable pin body (file, empty dir, broken link).
189
+ if (kind === "file" || kind === "other" || kind === "dir" || kind === "symlink") {
190
+ divergent.push(skillId);
191
+ continue;
192
+ }
193
+ if (!pathExists(target)) {
194
+ missing.push(skillId);
195
+ continue;
196
+ }
197
+ divergent.push(skillId);
198
+ }
199
+ return { skillsDir, present, missing, divergent };
200
+ }
201
+ function trySymlinkDir(target, path) {
202
+ try {
203
+ symlinkSync(target, path, "dir");
204
+ return true;
205
+ }
206
+ catch {
207
+ // Windows: junctions do not require elevated privileges for directories.
208
+ try {
209
+ symlinkSync(target, path, "junction");
210
+ return true;
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ }
216
+ }
217
+ function defaultCopyDir(src, dst) {
218
+ cpSync(src, dst, { recursive: true });
219
+ }
220
+ /**
221
+ * Install one pin into a skills root. Prefer symlink; copy on failure.
222
+ * Never deletes user skills. Divergent targets require force or TTY confirm.
223
+ */
224
+ export function installOpenClawPin(skillId, sourceDir, skillsDir, options = {}, seams = {}) {
225
+ const isFile = seams.isFile ?? defaultIsFile;
226
+ const isDir = seams.isDir ?? defaultIsDir;
227
+ const lstatKind = seams.lstatKind ?? defaultLstatKind;
228
+ const mkdirp = seams.mkdirp ?? ((p) => mkdirSync(p, { recursive: true }));
229
+ const symlinkDir = seams.symlinkDir ??
230
+ ((target, path) => {
231
+ if (!trySymlinkDir(target, path)) {
232
+ throw new Error(`symlink failed for ${path}`);
233
+ }
234
+ });
235
+ const copyDir = seams.copyDir ?? defaultCopyDir;
236
+ const removePath = seams.removePath ?? ((p) => rmSync(p, { recursive: true, force: true }));
237
+ const target = join(skillsDir, skillId);
238
+ const force = options.force === true || options.allowOverwrite === true;
239
+ if (!isDir(sourceDir) || !isFile(join(sourceDir, "SKILL.md"))) {
240
+ return {
241
+ skillId,
242
+ method: "skipped",
243
+ target,
244
+ source: sourceDir,
245
+ detail: `source pin missing or incomplete: ${sourceDir}`,
246
+ };
247
+ }
248
+ mkdirp(skillsDir);
249
+ const kind = lstatKind(target);
250
+ if (kind !== "missing") {
251
+ if (skillHasBody(target, isFile, isDir)) {
252
+ return {
253
+ skillId,
254
+ method: "already-present",
255
+ target,
256
+ source: sourceDir,
257
+ };
258
+ }
259
+ if (!force) {
260
+ return {
261
+ skillId,
262
+ method: "skipped",
263
+ target,
264
+ source: sourceDir,
265
+ detail: "divergent target exists; re-run with --force or confirm on TTY to replace",
266
+ };
267
+ }
268
+ removePath(target);
269
+ }
270
+ try {
271
+ symlinkDir(sourceDir, target);
272
+ return { skillId, method: "symlink", target, source: sourceDir };
273
+ }
274
+ catch {
275
+ try {
276
+ copyDir(sourceDir, target);
277
+ return { skillId, method: "copy", target, source: sourceDir };
278
+ }
279
+ catch (err) {
280
+ return {
281
+ skillId,
282
+ method: "skipped",
283
+ target,
284
+ source: sourceDir,
285
+ detail: `install failed: ${err instanceof Error ? err.message : String(err)}`,
286
+ };
287
+ }
288
+ }
289
+ }
290
+ /**
291
+ * Doctor check: detect OpenClaw + missing always-pins; optionally fix.
292
+ * No-ops when OpenClaw signals are absent.
293
+ */
294
+ export function runOpenClawSkillPinsCheck(sink, addFinding, options) {
295
+ const seams = options.seams ?? {};
296
+ const env = seams.env ?? process.env;
297
+ const homeDir = seams.homeDir ?? (() => homedir());
298
+ const isDir = seams.isDir ?? defaultIsDir;
299
+ const isFile = seams.isFile ?? defaultIsFile;
300
+ const detect = detectOpenClaw(env, { homeDir: homeDir(), isDir });
301
+ if (!detect.detected) {
302
+ sink.info(`${OPENCLAW_SKILL_PINS_CHECK}: skip -- OpenClaw not detected`);
303
+ addFinding({
304
+ severity: "skip",
305
+ message: "OpenClaw not detected",
306
+ check: OPENCLAW_SKILL_PINS_CHECK,
307
+ status: "skip",
308
+ reason: "openclaw-not-detected",
309
+ });
310
+ return;
311
+ }
312
+ const contentBase = (seams.contentRootFor ?? contentRoot)(options.frameworkRoot);
313
+ const skillsDirs = listInScopeSkillsDirs(detect.stateDir, options.allAgents, {
314
+ isDir,
315
+ readDir: seams.readDir,
316
+ });
317
+ const missingByDir = [];
318
+ const divergentByDir = [];
319
+ let allPresent = true;
320
+ for (const skillsDir of skillsDirs) {
321
+ const assessment = assessOpenClawPins(skillsDir, { isDir, isFile, lstatKind: seams.lstatKind });
322
+ if (assessment.missing.length > 0) {
323
+ allPresent = false;
324
+ missingByDir.push({ skillsDir, missing: [...assessment.missing] });
325
+ }
326
+ if (assessment.divergent.length > 0) {
327
+ allPresent = false;
328
+ divergentByDir.push({ skillsDir, divergent: [...assessment.divergent] });
329
+ }
330
+ }
331
+ if (allPresent) {
332
+ const scope = options.allAgents ? "main + workspace-* seats" : "main workspace";
333
+ const message = `${OPENCLAW_SKILL_PINS_CHECK}: OpenClaw host always-pins present ` +
334
+ `(${OPENCLAW_ALWAYS_PIN_SKILLS.join(", ")}) in ${scope}`;
335
+ sink.success(message);
336
+ addFinding({
337
+ severity: "skip",
338
+ message,
339
+ check: OPENCLAW_SKILL_PINS_CHECK,
340
+ status: "present",
341
+ skills_dirs: skillsDirs,
342
+ pins: [...OPENCLAW_ALWAYS_PIN_SKILLS],
343
+ detect_reasons: detect.reasons,
344
+ });
345
+ return;
346
+ }
347
+ // Optional fix path
348
+ const installResults = [];
349
+ if (options.fixMode) {
350
+ const isTty = seams.isTty ?? (() => process.stdin.isTTY === true);
351
+ const readYn = seams.readYn ?? (() => false);
352
+ let allowFix = true;
353
+ if (!options.jsonMode && isTty()) {
354
+ allowFix = readYn(`Wire missing OpenClaw always-pin skills into ${skillsDirs.join(", ")} now?`, true);
355
+ if (!allowFix) {
356
+ sink.info("Skipped OpenClaw pin wire -- re-run `deft doctor --fix` when ready.");
357
+ }
358
+ }
359
+ // Non-interactive --fix applies additive installs (safe); divergent needs --force.
360
+ if (allowFix) {
361
+ for (const skillsDir of skillsDirs) {
362
+ const assessment = assessOpenClawPins(skillsDir, {
363
+ isDir,
364
+ isFile,
365
+ lstatKind: seams.lstatKind,
366
+ });
367
+ const toInstall = new Set([
368
+ ...assessment.missing,
369
+ ...(options.force ? assessment.divergent : []),
370
+ ]);
371
+ if (!options.force && assessment.divergent.length > 0 && isTty() && !options.jsonMode) {
372
+ for (const skillId of assessment.divergent) {
373
+ if (readYn(`Replace divergent OpenClaw skill dir ${join(skillsDir, skillId)} with pin from content package?`, false)) {
374
+ toInstall.add(skillId);
375
+ }
376
+ }
377
+ }
378
+ for (const skillId of toInstall) {
379
+ const sourceDir = resolvePinSourceDir(contentBase, skillId);
380
+ const result = installOpenClawPin(skillId, sourceDir, skillsDir, { force: options.force || assessment.divergent.includes(skillId) }, seams);
381
+ installResults.push(result);
382
+ if (result.method === "symlink" || result.method === "copy") {
383
+ sink.success(`OpenClaw pin ${skillId}: ${result.method} → ${result.target}${result.method === "symlink" ? ` (from ${result.source})` : ""}`);
384
+ }
385
+ else if (result.method === "already-present") {
386
+ sink.info(`OpenClaw pin ${skillId}: already present at ${result.target}`);
387
+ }
388
+ else {
389
+ sink.warn(`OpenClaw pin ${skillId}: skipped${result.detail ? ` -- ${result.detail}` : ""}`);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ // Re-assess after fix
395
+ let stillMissing = false;
396
+ const postMissing = [];
397
+ for (const skillsDir of skillsDirs) {
398
+ const post = assessOpenClawPins(skillsDir, { isDir, isFile, lstatKind: seams.lstatKind });
399
+ if (post.missing.length > 0 || post.divergent.length > 0) {
400
+ stillMissing = true;
401
+ postMissing.push(...post.missing.map((id) => `${skillsDir}/${id}`), ...post.divergent.map((id) => `${skillsDir}/${id} (divergent)`));
402
+ }
403
+ }
404
+ if (!stillMissing) {
405
+ const message = `${OPENCLAW_SKILL_PINS_CHECK}: wired OpenClaw always-pins; ` +
406
+ "restart the OpenClaw gateway or start a new session so available_skills refreshes";
407
+ sink.success(message);
408
+ addFinding({
409
+ severity: "skip",
410
+ message,
411
+ check: OPENCLAW_SKILL_PINS_CHECK,
412
+ status: "fixed",
413
+ skills_dirs: skillsDirs,
414
+ installs: installResults,
415
+ detect_reasons: detect.reasons,
416
+ });
417
+ return;
418
+ }
419
+ // Fall through to warning with remaining gaps
420
+ missingByDir.length = 0;
421
+ divergentByDir.length = 0;
422
+ for (const skillsDir of skillsDirs) {
423
+ const post = assessOpenClawPins(skillsDir, { isDir, isFile, lstatKind: seams.lstatKind });
424
+ if (post.missing.length > 0)
425
+ missingByDir.push({ skillsDir, missing: [...post.missing] });
426
+ if (post.divergent.length > 0)
427
+ divergentByDir.push({ skillsDir, divergent: [...post.divergent] });
428
+ }
429
+ }
430
+ const missingIds = [...new Set(missingByDir.flatMap((m) => m.missing))];
431
+ const divergentIds = [...new Set(divergentByDir.flatMap((d) => d.divergent))];
432
+ const targetSummary = skillsDirs.join(", ");
433
+ const sourceHint = join(contentBase, "skills", "<pin-id>");
434
+ const multiHint = options.allAgents ? "" : ` Multi-seat workspaces: ${REMEDIATION_ALL_AGENTS}.`;
435
+ const message = `${OPENCLAW_SKILL_PINS_CHECK}: missing OpenClaw always-pin skill(s) ` +
436
+ `[${missingIds.join(", ") || "(none)"}]` +
437
+ (divergentIds.length > 0 ? `; divergent: [${divergentIds.join(", ")}]` : "") +
438
+ ` under ${targetSummary}. ` +
439
+ `Source: ${sourceHint}. ` +
440
+ `Remediation: ${REMEDIATION_FIX}. ` +
441
+ `See ${DOC_OPENCLAW_HOST}, ${DOC_HOST_LIFECYCLE}, ${DOC_SKILL_PINS}.` +
442
+ multiHint;
443
+ sink.warn(message);
444
+ if (!options.jsonMode) {
445
+ sink.info("After wiring pins, restart the OpenClaw gateway or open a new session so host available_skills refreshes.");
446
+ }
447
+ addFinding({
448
+ severity: "warning",
449
+ message,
450
+ check: OPENCLAW_SKILL_PINS_CHECK,
451
+ status: "missing",
452
+ missing: missingIds,
453
+ divergent: divergentIds,
454
+ skills_dirs: skillsDirs,
455
+ main_skills_dir: detect.mainSkillsDir,
456
+ source_content_root: contentBase,
457
+ suggestion: REMEDIATION_FIX,
458
+ docs: [DOC_OPENCLAW_HOST, DOC_HOST_LIFECYCLE, DOC_SKILL_PINS],
459
+ detect_reasons: detect.reasons,
460
+ ...(installResults.length > 0 ? { installs: installResults } : {}),
461
+ });
462
+ }
463
+ //# sourceMappingURL=openclaw-skills.js.map
@@ -75,6 +75,10 @@ export interface DoctorFlags {
75
75
  readonly network: boolean;
76
76
  readonly help: boolean;
77
77
  readonly projectRoot: string | null;
78
+ /** Replace divergent OpenClaw pin dirs during doctor --fix (#3001). */
79
+ readonly force: boolean;
80
+ /** Wire always-pins into main + every workspace-* seat (#3001). */
81
+ readonly openclawAllAgents: boolean;
78
82
  readonly unknown: readonly string[];
79
83
  }
80
84
  export interface ThrottleDecision {
@@ -150,5 +154,23 @@ export interface DoctorSeams {
150
154
  readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
151
155
  /** Live hook spawn probe for doctor --full (#2852). */
152
156
  readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
157
+ /**
158
+ * xBRIEF project-envelope staleness probe (#2971). Injected so doctor can
159
+ * fail closed on 0.6 project JSON under an xbrief/ layout without re-deriving
160
+ * the shared `probeXbriefStaleness` path in tests.
161
+ */
162
+ readonly probeXbriefEnvelope?: (projectRoot: string) => {
163
+ readonly declaredVersion: string | null;
164
+ readonly targetVersion: string;
165
+ readonly distance: "current" | "behind-minor" | "behind-major";
166
+ readonly stale: boolean;
167
+ };
168
+ /**
169
+ * OpenClaw skill-pin seams (#3001). Injected so detect/fix stays offline +
170
+ * deterministic in tests (fake HOME / env / fs).
171
+ */
172
+ readonly openclawEnv?: NodeJS.ProcessEnv;
173
+ readonly openclawHomeDir?: () => string;
174
+ readonly openclawContentRootFor?: (frameworkRoot: string) => string;
153
175
  }
154
176
  //# 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