@deftai/directive-core 0.89.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.
- package/dist/doctor/constants.d.ts +1 -1
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/flags.js +21 -1
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +22 -0
- package/dist/doctor/openclaw-skills.d.ts +109 -0
- package/dist/doctor/openclaw-skills.js +463 -0
- package/dist/doctor/types.d.ts +11 -0
- package/dist/hooks/dispatcher.js +20 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lifecycle/events.js +2 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle/stats.d.ts +53 -0
- package/dist/lifecycle/stats.js +286 -0
- package/dist/release/spawn.js +13 -0
- package/dist/release-e2e/git-ops.d.ts +7 -0
- package/dist/release-e2e/git-ops.js +35 -2
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/process-cost-constants.d.ts +9 -0
- package/dist/session/process-cost-constants.js +11 -0
- package/dist/session/process-cost.d.ts +53 -0
- package/dist/session/process-cost.js +82 -0
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +12 -1
- package/dist/session/session-ready.d.ts +67 -0
- package/dist/session/session-ready.js +264 -0
- package/dist/session/session-start.d.ts +56 -1
- package/dist/session/session-start.js +378 -24
- package/dist/session/verify-session-ritual.d.ts +8 -0
- package/dist/session/verify-session-ritual.js +95 -35
- package/dist/tool-events/classify.d.ts +20 -0
- package/dist/tool-events/classify.js +634 -0
- package/dist/tool-events/index.d.ts +10 -0
- package/dist/tool-events/index.js +10 -0
- package/dist/tool-events/summarize.d.ts +34 -0
- package/dist/tool-events/summarize.js +93 -0
- package/dist/tool-events/types.d.ts +52 -0
- package/dist/tool-events/types.js +13 -0
- package/package.json +7 -3
|
@@ -7,7 +7,7 @@ export declare const DEPRECATED_REDIRECT_SENTINEL = "<!-- deft:deprecated-redire
|
|
|
7
7
|
export declare const DEPRECATED_SKILL_REDIRECT_SENTINEL = "<!-- deft:deprecated-skill-redirect -->";
|
|
8
8
|
export declare const REDIRECT_STUB_HEADER_LINES = 8;
|
|
9
9
|
export declare const TASKFILE_INCLUDE_SNIPPET = "version: '3'\n\nincludes:\n deft:\n taskfile: ./.deft/core/Taskfile.yml\n optional: true\n";
|
|
10
|
-
export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--repair", "--repair-taskfile", "--json", "--quiet", "--full", "--network", "--project-root", "-h", "--help"];
|
|
10
|
+
export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--repair", "--repair-taskfile", "--json", "--quiet", "--full", "--network", "--project-root", "--force", "--openclaw-all-agents", "-h", "--help"];
|
|
11
11
|
/** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
|
|
12
12
|
export declare const NPM_PACKAGE_NAME = "@deftai/directive";
|
|
13
13
|
export declare const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
package/dist/doctor/constants.js
CHANGED
package/dist/doctor/flags.js
CHANGED
|
@@ -8,6 +8,8 @@ export function parseDoctorFlags(args) {
|
|
|
8
8
|
let network = false;
|
|
9
9
|
let help = false;
|
|
10
10
|
let projectRoot = null;
|
|
11
|
+
let force = false;
|
|
12
|
+
let openclawAllAgents = false;
|
|
11
13
|
const unknown = [];
|
|
12
14
|
let i = 0;
|
|
13
15
|
while (i < args.length) {
|
|
@@ -30,6 +32,12 @@ export function parseDoctorFlags(args) {
|
|
|
30
32
|
else if (token === "--network") {
|
|
31
33
|
network = true;
|
|
32
34
|
}
|
|
35
|
+
else if (token === "--force") {
|
|
36
|
+
force = true;
|
|
37
|
+
}
|
|
38
|
+
else if (token === "--openclaw-all-agents") {
|
|
39
|
+
openclawAllAgents = true;
|
|
40
|
+
}
|
|
33
41
|
else if (token === "-h" || token === "--help") {
|
|
34
42
|
help = true;
|
|
35
43
|
}
|
|
@@ -56,7 +64,19 @@ export function parseDoctorFlags(args) {
|
|
|
56
64
|
}
|
|
57
65
|
i += 1;
|
|
58
66
|
}
|
|
59
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
session,
|
|
69
|
+
fix,
|
|
70
|
+
json,
|
|
71
|
+
quiet,
|
|
72
|
+
full,
|
|
73
|
+
network,
|
|
74
|
+
help,
|
|
75
|
+
projectRoot,
|
|
76
|
+
force,
|
|
77
|
+
openclawAllAgents,
|
|
78
|
+
unknown,
|
|
79
|
+
};
|
|
60
80
|
}
|
|
61
81
|
export function formatUnknownFlagsError(unknown) {
|
|
62
82
|
return `Unknown flag(s): ${unknown.join(", ")}`;
|
package/dist/doctor/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./json.js";
|
|
|
7
7
|
export * from "./main.js";
|
|
8
8
|
export * from "./manifest.js";
|
|
9
9
|
export * from "./npm-registry.js";
|
|
10
|
+
export * from "./openclaw-skills.js";
|
|
10
11
|
export * from "./paths.js";
|
|
11
12
|
export * from "./payload-staleness.js";
|
|
12
13
|
export * from "./release-availability.js";
|
package/dist/doctor/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./json.js";
|
|
|
7
7
|
export * from "./main.js";
|
|
8
8
|
export * from "./manifest.js";
|
|
9
9
|
export * from "./npm-registry.js";
|
|
10
|
+
export * from "./openclaw-skills.js";
|
|
10
11
|
export * from "./paths.js";
|
|
11
12
|
export * from "./payload-staleness.js";
|
|
12
13
|
export * from "./release-availability.js";
|
package/dist/doctor/main.js
CHANGED
|
@@ -21,6 +21,7 @@ import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } fro
|
|
|
21
21
|
import { pythonJsonDump } from "./json.js";
|
|
22
22
|
import { parseInstallRootFromAgentsMd } from "./manifest.js";
|
|
23
23
|
import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
|
|
24
|
+
import { runOpenClawSkillPinsCheck } from "./openclaw-skills.js";
|
|
24
25
|
import { createPlainSink } from "./output.js";
|
|
25
26
|
import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
|
|
26
27
|
import { runPayloadStalenessCheck } from "./payload-staleness.js";
|
|
@@ -350,6 +351,27 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
350
351
|
if (!jsonMode) {
|
|
351
352
|
sink.blank();
|
|
352
353
|
}
|
|
354
|
+
sink.info("Checking OpenClaw always-pin skills...");
|
|
355
|
+
runOpenClawSkillPinsCheck(sink, addFinding, {
|
|
356
|
+
frameworkRoot,
|
|
357
|
+
fixMode,
|
|
358
|
+
jsonMode,
|
|
359
|
+
force: flags.force,
|
|
360
|
+
allAgents: flags.openclawAllAgents,
|
|
361
|
+
seams: {
|
|
362
|
+
...seams,
|
|
363
|
+
env: seams.openclawEnv,
|
|
364
|
+
homeDir: seams.openclawHomeDir,
|
|
365
|
+
contentRootFor: seams.openclawContentRootFor,
|
|
366
|
+
isDir: seams.isDir,
|
|
367
|
+
isFile: seams.isFile,
|
|
368
|
+
isTty: seams.isTty,
|
|
369
|
+
readYn: seams.readYn,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
if (!jsonMode) {
|
|
373
|
+
sink.blank();
|
|
374
|
+
}
|
|
353
375
|
sink.info("Checking xBRIEF project envelope version...");
|
|
354
376
|
runXbriefEnvelopeVersionCheck(projectRoot, sink, addFinding, seams);
|
|
355
377
|
let resolution = null;
|
|
@@ -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
|