kc-beta 0.7.3 → 0.8.1
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/README.md +57 -4
- package/bin/kc-beta.js +20 -6
- package/package.json +3 -2
- package/src/agent/engine.js +493 -132
- package/src/agent/pipelines/_advance-hints.js +92 -0
- package/src/agent/pipelines/_milestone-derive.js +387 -17
- package/src/agent/pipelines/initializer.js +4 -1
- package/src/agent/pipelines/skill-authoring.js +30 -1
- package/src/agent/skill-loader.js +433 -111
- package/src/agent/tools/agent-tool.js +2 -2
- package/src/agent/tools/consult-skill.js +127 -0
- package/src/agent/tools/copy-to-workspace.js +4 -3
- package/src/agent/tools/dashboard-render.js +48 -1
- package/src/agent/tools/document-parse.js +31 -2
- package/src/agent/tools/phase-advance.js +17 -13
- package/src/agent/tools/release.js +378 -8
- package/src/agent/tools/sandbox-exec.js +65 -8
- package/src/agent/tools/worker-llm-call.js +95 -15
- package/src/agent/tools/workspace-file.js +7 -7
- package/src/agent/workspace.js +25 -4
- package/src/cli/components.js +4 -1
- package/src/cli/index.js +97 -1
- package/src/config.js +20 -3
- package/src/marathon/driver.js +217 -0
- package/src/marathon/prompts.js +93 -0
- package/template/.env.template +16 -0
- package/template/AGENT.md +182 -7
- package/template/skills/en/{meta-meta/auto-model-selection → auto-model-selection}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/bootstrap-workspace → bootstrap-workspace}/SKILL.md +15 -0
- package/template/skills/{zh/meta → en}/compliance-judgment/SKILL.md +1 -0
- package/template/skills/en/{meta/confidence-system → confidence-system}/SKILL.md +1 -0
- package/template/skills/en/{meta/corner-case-management → corner-case-management}/SKILL.md +1 -0
- package/template/skills/en/{meta/cross-document-verification → cross-document-verification}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/dashboard-reporting → dashboard-reporting}/SKILL.md +1 -0
- package/template/skills/en/{meta/data-sensibility → data-sensibility}/SKILL.md +1 -0
- package/template/skills/{zh/meta → en}/document-chunking/SKILL.md +1 -0
- package/template/skills/en/{meta/document-parsing → document-parsing}/SKILL.md +1 -0
- package/template/skills/{zh/meta → en}/entity-extraction/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/evolution-loop → evolution-loop}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/quality-control → quality-control}/SKILL.md +10 -0
- package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/rule-graph → rule-graph}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/skill-authoring → skill-authoring}/SKILL.md +40 -0
- package/template/skills/en/skill-creator/SKILL.md +2 -1
- package/template/skills/en/{meta-meta/skill-to-workflow → skill-to-workflow}/SKILL.md +58 -4
- package/template/skills/en/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
- package/template/skills/en/{meta/tree-processing → tree-processing}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/version-control → version-control}/SKILL.md +1 -0
- package/template/skills/en/{meta-meta/work-decomposition → work-decomposition}/SKILL.md +51 -6
- package/template/skills/phase_skills.yaml +112 -0
- package/template/skills/zh/{meta-meta/auto-model-selection → auto-model-selection}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/bootstrap-workspace → bootstrap-workspace}/SKILL.md +15 -0
- package/template/skills/zh/compliance-judgment/SKILL.md +83 -0
- package/template/skills/zh/{meta/confidence-system → confidence-system}/SKILL.md +1 -0
- package/template/skills/zh/{meta/corner-case-management → corner-case-management}/SKILL.md +1 -0
- package/template/skills/zh/{meta/cross-document-verification → cross-document-verification}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/dashboard-reporting → dashboard-reporting}/SKILL.md +1 -0
- package/template/skills/zh/{meta/data-sensibility → data-sensibility}/SKILL.md +1 -0
- package/template/skills/zh/document-chunking/SKILL.md +40 -0
- package/template/skills/zh/document-parsing/SKILL.md +102 -0
- package/template/skills/zh/entity-extraction/SKILL.md +121 -0
- package/template/skills/zh/{meta-meta/evolution-loop → evolution-loop}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/quality-control → quality-control}/SKILL.md +10 -0
- package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/rule-graph → rule-graph}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/skill-authoring → skill-authoring}/SKILL.md +40 -0
- package/template/skills/zh/skill-creator/SKILL.md +205 -200
- package/template/skills/zh/skill-to-workflow/SKILL.md +243 -0
- package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
- package/template/skills/zh/tree-processing/SKILL.md +126 -0
- package/template/skills/zh/{meta-meta/version-control → version-control}/SKILL.md +1 -0
- package/template/skills/zh/{meta-meta/work-decomposition → work-decomposition}/SKILL.md +49 -4
- package/template/workflows/common/llm_client.py +168 -0
- package/template/workflows/common/utils.py +132 -0
- package/template/CLAUDE.md +0 -150
- package/template/skills/en/meta/compliance-judgment/SKILL.md +0 -82
- package/template/skills/en/meta/document-chunking/SKILL.md +0 -32
- package/template/skills/en/meta/entity-extraction/SKILL.md +0 -120
- package/template/skills/zh/meta/document-parsing/SKILL.md +0 -101
- package/template/skills/zh/meta/tree-processing/SKILL.md +0 -121
- package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +0 -188
- /package/template/skills/en/{meta/compliance-judgment → compliance-judgment}/references/output-format.md +0 -0
- /package/template/skills/en/{meta/cross-document-verification → cross-document-verification}/references/contradiction-taxonomy.md +0 -0
- /package/template/skills/en/{meta-meta/dashboard-reporting → dashboard-reporting}/scripts/generate_dashboard.py +0 -0
- /package/template/skills/en/{meta/document-parsing → document-parsing}/references/parser-catalog.md +0 -0
- /package/template/skills/en/{meta-meta/evolution-loop → evolution-loop}/references/convergence-guide.md +0 -0
- /package/template/skills/en/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/scripts/generate_review.js +0 -0
- /package/template/skills/en/{meta-meta/quality-control → quality-control}/references/qa-layers.md +0 -0
- /package/template/skills/en/{meta-meta/quality-control → quality-control}/references/sampling-strategies.md +0 -0
- /package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/references/chunking-strategies.md +0 -0
- /package/template/skills/en/{meta-meta/skill-authoring → skill-authoring}/references/skill-format-spec.md +0 -0
- /package/template/skills/en/{meta-meta/skill-to-workflow → skill-to-workflow}/references/worker-llm-catalog.md +0 -0
- /package/template/skills/en/{meta-meta/task-decomposition → task-decomposition}/references/decision-matrix.md +0 -0
- /package/template/skills/en/{meta-meta/version-control → version-control}/references/trace-id-spec.md +0 -0
- /package/template/skills/zh/{meta/compliance-judgment → compliance-judgment}/references/output-format.md +0 -0
- /package/template/skills/zh/{meta/cross-document-verification → cross-document-verification}/references/contradiction-taxonomy.md +0 -0
- /package/template/skills/zh/{meta-meta/dashboard-reporting → dashboard-reporting}/scripts/generate_dashboard.py +0 -0
- /package/template/skills/zh/{meta/document-parsing → document-parsing}/references/parser-catalog.md +0 -0
- /package/template/skills/zh/{meta-meta/evolution-loop → evolution-loop}/references/convergence-guide.md +0 -0
- /package/template/skills/zh/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/scripts/generate_review.js +0 -0
- /package/template/skills/zh/{meta-meta/quality-control → quality-control}/references/qa-layers.md +0 -0
- /package/template/skills/zh/{meta-meta/quality-control → quality-control}/references/sampling-strategies.md +0 -0
- /package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/references/chunking-strategies.md +0 -0
- /package/template/skills/zh/{meta-meta/skill-authoring → skill-authoring}/references/skill-format-spec.md +0 -0
- /package/template/skills/zh/{meta-meta/skill-to-workflow → skill-to-workflow}/references/worker-llm-catalog.md +0 -0
- /package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/references/decision-matrix.md +0 -0
- /package/template/skills/zh/{meta-meta/version-control → version-control}/references/trace-id-spec.md +0 -0
|
@@ -4,63 +4,142 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const BUNDLED_SKILLS_DIR = path.resolve(__dirname, "../../template/skills");
|
|
7
|
+
const PHASE_SKILLS_REGISTRY_PATH = path.join(BUNDLED_SKILLS_DIR, "phase_skills.yaml");
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// phases save the system-prompt budget. This is a soft filter: the
|
|
12
|
-
// agent can still `workspace_file` read any skill on-demand.
|
|
9
|
+
// v0.7.5: registry is data, not code. The phase × skills mapping lives
|
|
10
|
+
// in template/skills/phase_skills.yaml as the single source of truth.
|
|
11
|
+
// SkillLoader reads it at startup and exposes getPhaseSkillSet(phase).
|
|
13
12
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
13
|
+
// Fallback used when the YAML file is missing or unparseable — preserves
|
|
14
|
+
// prior v0.7.x behavior. Audit-derived from v0.7.4 PHASE_RELEVANT_SKILLS.
|
|
15
|
+
const PHASE_SKILLS_FALLBACK = {
|
|
16
|
+
bootstrap: {
|
|
17
|
+
always_loaded: ["bootstrap-workspace"],
|
|
18
|
+
available: ["auto-model-selection", "data-sensibility", "document-parsing", "document-chunking", "version-control"],
|
|
19
|
+
},
|
|
20
|
+
rule_extraction: {
|
|
21
|
+
always_loaded: ["rule-extraction"],
|
|
22
|
+
available: ["work-decomposition", "rule-graph", "data-sensibility", "document-parsing", "document-chunking", "version-control"],
|
|
23
|
+
},
|
|
24
|
+
skill_authoring: {
|
|
25
|
+
always_loaded: ["skill-authoring", "work-decomposition"],
|
|
26
|
+
available: ["data-sensibility", "entity-extraction", "tree-processing", "compliance-judgment", "rule-graph", "corner-case-management", "evolution-loop", "skill-to-workflow", "skill-creator", "version-control"],
|
|
27
|
+
},
|
|
28
|
+
skill_testing: {
|
|
29
|
+
always_loaded: ["evolution-loop"],
|
|
30
|
+
available: ["skill-authoring", "skill-to-workflow", "tree-processing", "corner-case-management", "compliance-judgment", "data-sensibility", "rule-graph", "version-control"],
|
|
31
|
+
},
|
|
32
|
+
distillation: {
|
|
33
|
+
always_loaded: ["skill-to-workflow", "evolution-loop"],
|
|
34
|
+
available: ["skill-authoring", "task-decomposition", "corner-case-management", "confidence-system", "entity-extraction", "compliance-judgment", "version-control"],
|
|
35
|
+
},
|
|
36
|
+
production_qc: {
|
|
37
|
+
always_loaded: ["quality-control", "evolution-loop"],
|
|
38
|
+
available: ["skill-authoring", "skill-to-workflow", "confidence-system", "cross-document-verification", "corner-case-management", "compliance-judgment", "dashboard-reporting", "version-control"],
|
|
39
|
+
},
|
|
40
|
+
finalization: {
|
|
41
|
+
always_loaded: ["quality-control"],
|
|
42
|
+
available: ["skill-authoring", "skill-to-workflow", "dashboard-reporting", "version-control", "pdf-review-dashboard"],
|
|
43
|
+
},
|
|
45
44
|
};
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Parse the simple phase-skills YAML format.
|
|
48
|
+
*
|
|
49
|
+
* Expected shape:
|
|
50
|
+
* phases:
|
|
51
|
+
* <name>:
|
|
52
|
+
* always_loaded: [<skill>, ...] # or block list with leading " - <skill>"
|
|
53
|
+
* available: [<skill>, ...]
|
|
54
|
+
*
|
|
55
|
+
* This handles only the file's specific shape — block-style nested mappings
|
|
56
|
+
* with single-line lists OR block lists. Anchors, multiline strings, flow
|
|
57
|
+
* mappings are NOT supported (we don't need them). Comments (#) ignored.
|
|
58
|
+
* On any parse weirdness, returns null so caller falls back to defaults.
|
|
59
|
+
*/
|
|
60
|
+
function parsePhaseSkillsYaml(text) {
|
|
61
|
+
if (!text || typeof text !== "string") return null;
|
|
62
|
+
|
|
63
|
+
const lines = text.split("\n");
|
|
64
|
+
const result = { phases: {} };
|
|
65
|
+
|
|
66
|
+
let currentPhase = null;
|
|
67
|
+
let currentList = null; // "always_loaded" | "available" | null
|
|
68
|
+
|
|
69
|
+
for (let raw of lines) {
|
|
70
|
+
// Strip inline comments + trailing whitespace
|
|
71
|
+
const hashIdx = raw.indexOf("#");
|
|
72
|
+
const line = (hashIdx >= 0 ? raw.slice(0, hashIdx) : raw).trimEnd();
|
|
73
|
+
if (!line.trim()) continue;
|
|
74
|
+
|
|
75
|
+
// Match indent level — phases: at column 0, phase name at 2 spaces,
|
|
76
|
+
// list-name at 4 spaces, list-item at 6 spaces
|
|
77
|
+
if (/^phases\s*:\s*$/.test(line)) {
|
|
78
|
+
currentPhase = null;
|
|
79
|
+
currentList = null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Phase name line: " bootstrap:"
|
|
84
|
+
const phaseMatch = /^ {2}(\w+)\s*:\s*$/.exec(line);
|
|
85
|
+
if (phaseMatch) {
|
|
86
|
+
currentPhase = phaseMatch[1];
|
|
87
|
+
result.phases[currentPhase] = { always_loaded: [], available: [] };
|
|
88
|
+
currentList = null;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// List name line: " always_loaded:" or " available:" or with inline list
|
|
93
|
+
const listMatch = /^ {4}(always_loaded|available)\s*:\s*(.*)$/.exec(line);
|
|
94
|
+
if (listMatch && currentPhase) {
|
|
95
|
+
const listName = listMatch[1];
|
|
96
|
+
const inline = listMatch[2].trim();
|
|
97
|
+
currentList = listName;
|
|
98
|
+
// Inline list shape: "[foo, bar]" or "[]"
|
|
99
|
+
if (inline.startsWith("[") && inline.endsWith("]")) {
|
|
100
|
+
const inner = inline.slice(1, -1).trim();
|
|
101
|
+
if (inner) {
|
|
102
|
+
result.phases[currentPhase][listName] = inner
|
|
103
|
+
.split(",")
|
|
104
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ""))
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
currentList = null; // inline list closed
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// List item line: " - foo"
|
|
113
|
+
const itemMatch = /^ {6}-\s+(.+)$/.exec(line);
|
|
114
|
+
if (itemMatch && currentPhase && currentList) {
|
|
115
|
+
const item = itemMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
116
|
+
result.phases[currentPhase][currentList].push(item);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validate we got at least one phase
|
|
122
|
+
if (Object.keys(result.phases).length === 0) return null;
|
|
123
|
+
return result;
|
|
52
124
|
}
|
|
53
125
|
|
|
54
126
|
/**
|
|
55
127
|
* Discover and index meta skills from template/skills/.
|
|
56
|
-
* Follows Claude Code's pattern: skills are NOT dumped into the system prompt.
|
|
57
|
-
* Instead, a brief index (name + description) is injected into context.
|
|
58
|
-
* The agent reads full SKILL.md content on demand via workspace_file or sandbox_exec.
|
|
59
128
|
*
|
|
60
|
-
*
|
|
61
|
-
* template/skills/{lang}
|
|
62
|
-
*
|
|
63
|
-
*
|
|
129
|
+
* v0.7.5 layout (flat):
|
|
130
|
+
* template/skills/{lang}/<name>/SKILL.md
|
|
131
|
+
*
|
|
132
|
+
* Earlier v0.3.0–v0.7.4 layout (deep, deprecated):
|
|
133
|
+
* template/skills/{lang}/{meta-meta|meta|skill-creator}/<name>/SKILL.md
|
|
134
|
+
*
|
|
135
|
+
* SkillLoader supports both layouts during the v0.7.5 reorganization;
|
|
136
|
+
* after Group B completes, only the flat layout exists.
|
|
137
|
+
*
|
|
138
|
+
* Skills are auto-discovered by walking the lang dir for any subdirectory
|
|
139
|
+
* containing a SKILL.md. Frontmatter is parsed for name, description,
|
|
140
|
+
* and tier (meta | meta-meta). The phase × skills registry
|
|
141
|
+
* (template/skills/phase_skills.yaml) declares which skills appear in
|
|
142
|
+
* which phase's always-loaded vs available sets.
|
|
64
143
|
*/
|
|
65
144
|
export class SkillLoader {
|
|
66
145
|
/**
|
|
@@ -71,51 +150,165 @@ export class SkillLoader {
|
|
|
71
150
|
this._lang = language;
|
|
72
151
|
this._skillsDir = skillsDir || BUNDLED_SKILLS_DIR;
|
|
73
152
|
this._index = null;
|
|
153
|
+
this._registry = null;
|
|
154
|
+
this._bodyCache = new Map(); // name → body string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Load + cache the phase × skills registry from YAML.
|
|
159
|
+
* Falls back to PHASE_SKILLS_FALLBACK on parse failure or missing file.
|
|
160
|
+
*/
|
|
161
|
+
_loadRegistry() {
|
|
162
|
+
if (this._registry) return this._registry;
|
|
163
|
+
try {
|
|
164
|
+
if (fs.existsSync(PHASE_SKILLS_REGISTRY_PATH)) {
|
|
165
|
+
const text = fs.readFileSync(PHASE_SKILLS_REGISTRY_PATH, "utf-8");
|
|
166
|
+
const parsed = parsePhaseSkillsYaml(text);
|
|
167
|
+
if (parsed?.phases) {
|
|
168
|
+
this._registry = parsed.phases;
|
|
169
|
+
return this._registry;
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.warn("[skill-loader] phase_skills.yaml parsed empty/invalid; using fallback");
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.warn(`[skill-loader] phase_skills.yaml load error: ${err.message}; using fallback`);
|
|
177
|
+
}
|
|
178
|
+
this._registry = PHASE_SKILLS_FALLBACK;
|
|
179
|
+
return this._registry;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the always_loaded + available skill names for a phase.
|
|
184
|
+
* always_loaded is auto-added to available (set semantics, no duplicates).
|
|
185
|
+
*
|
|
186
|
+
* @param {string} phase
|
|
187
|
+
* @returns {{alwaysLoaded: string[], available: string[]}}
|
|
188
|
+
*/
|
|
189
|
+
getPhaseSkillSet(phase) {
|
|
190
|
+
const reg = this._loadRegistry();
|
|
191
|
+
const entry = reg[phase];
|
|
192
|
+
if (!entry) return { alwaysLoaded: [], available: [] };
|
|
193
|
+
const alwaysLoaded = [...(entry.always_loaded || [])];
|
|
194
|
+
const availableSet = new Set([...alwaysLoaded, ...(entry.available || [])]);
|
|
195
|
+
return {
|
|
196
|
+
alwaysLoaded,
|
|
197
|
+
available: [...availableSet],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Find a skill's directory on disk, supporting both flat (v0.7.5+)
|
|
203
|
+
* and deep (v0.3.0–v0.7.4) layouts. Returns absolute path to the
|
|
204
|
+
* directory containing SKILL.md, or null.
|
|
205
|
+
*/
|
|
206
|
+
_findSkillDir(name) {
|
|
207
|
+
const langDir = path.join(this._skillsDir, this._lang);
|
|
208
|
+
if (!fs.existsSync(langDir)) return null;
|
|
209
|
+
|
|
210
|
+
// v0.7.5 flat layout: <langDir>/<name>/SKILL.md
|
|
211
|
+
const flatPath = path.join(langDir, name);
|
|
212
|
+
if (fs.existsSync(path.join(flatPath, "SKILL.md"))) return flatPath;
|
|
213
|
+
|
|
214
|
+
// Pre-v0.7.5 deep layout: <langDir>/{meta-meta|meta|skill-creator}/<name>/SKILL.md
|
|
215
|
+
for (const category of ["meta-meta", "meta", "skill-creator"]) {
|
|
216
|
+
const deepPath = path.join(langDir, category, name);
|
|
217
|
+
if (fs.existsSync(path.join(deepPath, "SKILL.md"))) return deepPath;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read a skill's body (post-frontmatter content).
|
|
224
|
+
* Cached after first read.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} name
|
|
227
|
+
* @returns {string|null} body text, or null if skill not found
|
|
228
|
+
*/
|
|
229
|
+
loadSkillBody(name) {
|
|
230
|
+
if (this._bodyCache.has(name)) return this._bodyCache.get(name);
|
|
231
|
+
const dir = this._findSkillDir(name);
|
|
232
|
+
if (!dir) {
|
|
233
|
+
this._bodyCache.set(name, null);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const text = fs.readFileSync(path.join(dir, "SKILL.md"), "utf-8");
|
|
238
|
+
// Strip frontmatter: everything between leading "---\n" and the next "---\n"
|
|
239
|
+
const stripped = text.replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
240
|
+
this._bodyCache.set(name, stripped);
|
|
241
|
+
return stripped;
|
|
242
|
+
} catch {
|
|
243
|
+
this._bodyCache.set(name, null);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
74
246
|
}
|
|
75
247
|
|
|
76
248
|
/**
|
|
77
249
|
* Build the skill index by scanning SKILL.md frontmatter.
|
|
78
250
|
* Cached after first call.
|
|
79
|
-
*
|
|
251
|
+
*
|
|
252
|
+
* @returns {Array<{name: string, description: string, category: string, tier: string, path: string}>}
|
|
80
253
|
*/
|
|
81
254
|
getIndex() {
|
|
82
255
|
if (this._index) return this._index;
|
|
83
|
-
|
|
84
256
|
this._index = [];
|
|
257
|
+
|
|
85
258
|
const langDir = path.join(this._skillsDir, this._lang);
|
|
86
259
|
if (!fs.existsSync(langDir)) return this._index;
|
|
87
260
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
261
|
+
// Walk lang dir. For each entry that is a directory:
|
|
262
|
+
// - if it directly contains SKILL.md → flat skill (v0.7.5+ layout)
|
|
263
|
+
// - else if it matches a known category name → walk it for skills
|
|
264
|
+
// (pre-v0.7.5 deep layout backward-compat during Group B reorg)
|
|
265
|
+
const entries = fs.readdirSync(langDir, { withFileTypes: true });
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
if (!entry.isDirectory()) continue;
|
|
268
|
+
const entryPath = path.join(langDir, entry.name);
|
|
269
|
+
const directSkillMd = path.join(entryPath, "SKILL.md");
|
|
270
|
+
|
|
271
|
+
if (fs.existsSync(directSkillMd)) {
|
|
272
|
+
// Flat layout
|
|
273
|
+
const meta = this._parseFrontmatter(directSkillMd);
|
|
274
|
+
if (meta.name) {
|
|
97
275
|
this._index.push({
|
|
98
|
-
name: name
|
|
99
|
-
description: description || "",
|
|
100
|
-
|
|
101
|
-
|
|
276
|
+
name: meta.name,
|
|
277
|
+
description: meta.description || "",
|
|
278
|
+
tier: meta.tier || this._inferTierFromName(meta.name),
|
|
279
|
+
category: meta.tier || "meta", // legacy alias
|
|
280
|
+
path: path.relative(this._skillsDir, entryPath),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
} else if (["meta-meta", "meta", "skill-creator"].includes(entry.name)) {
|
|
284
|
+
// Deep layout (pre-v0.7.5) — recurse one level
|
|
285
|
+
// For skill-creator, the SKILL.md is at this level (not in a subdir)
|
|
286
|
+
const skillCreatorMd = path.join(entryPath, "SKILL.md");
|
|
287
|
+
if (fs.existsSync(skillCreatorMd)) {
|
|
288
|
+
const meta = this._parseFrontmatter(skillCreatorMd);
|
|
289
|
+
if (meta.name) {
|
|
290
|
+
this._index.push({
|
|
291
|
+
name: meta.name,
|
|
292
|
+
description: meta.description || "",
|
|
293
|
+
tier: meta.tier || "meta",
|
|
294
|
+
category: entry.name,
|
|
295
|
+
path: path.relative(this._skillsDir, entryPath),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const subEntry of fs.readdirSync(entryPath, { withFileTypes: true })) {
|
|
300
|
+
if (!subEntry.isDirectory()) continue;
|
|
301
|
+
const subSkillMd = path.join(entryPath, subEntry.name, "SKILL.md");
|
|
302
|
+
if (!fs.existsSync(subSkillMd)) continue;
|
|
303
|
+
const meta = this._parseFrontmatter(subSkillMd);
|
|
304
|
+
this._index.push({
|
|
305
|
+
name: meta.name || subEntry.name,
|
|
306
|
+
description: meta.description || "",
|
|
307
|
+
tier: meta.tier || (entry.name === "meta-meta" ? "meta-meta" : "meta"),
|
|
308
|
+
category: entry.name,
|
|
309
|
+
path: path.relative(this._skillsDir, path.join(entryPath, subEntry.name)),
|
|
102
310
|
});
|
|
103
311
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check subdirectories (meta-meta/bootstrap-workspace/, etc.)
|
|
107
|
-
for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
|
|
108
|
-
if (!entry.isDirectory()) continue;
|
|
109
|
-
const subSkillMd = path.join(catDir, entry.name, "SKILL.md");
|
|
110
|
-
if (!fs.existsSync(subSkillMd)) continue;
|
|
111
|
-
|
|
112
|
-
const { name, description } = this._parseFrontmatter(subSkillMd);
|
|
113
|
-
this._index.push({
|
|
114
|
-
name: name || entry.name,
|
|
115
|
-
description: description || "",
|
|
116
|
-
category,
|
|
117
|
-
path: path.relative(this._skillsDir, path.join(catDir, entry.name)),
|
|
118
|
-
});
|
|
119
312
|
}
|
|
120
313
|
}
|
|
121
314
|
|
|
@@ -123,60 +316,189 @@ export class SkillLoader {
|
|
|
123
316
|
}
|
|
124
317
|
|
|
125
318
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
319
|
+
* Heuristic tier inference for skills lacking explicit `tier:` frontmatter.
|
|
320
|
+
* Used as a fallback during the v0.7.5 reorganization when frontmatter
|
|
321
|
+
* hasn't been backfilled yet. Once Group B completes, every SKILL.md
|
|
322
|
+
* declares its tier explicitly and this inference becomes a no-op fallback.
|
|
323
|
+
*/
|
|
324
|
+
_inferTierFromName(name) {
|
|
325
|
+
const META_META = new Set([
|
|
326
|
+
"bootstrap-workspace", "evolution-loop", "quality-control",
|
|
327
|
+
"rule-graph", "task-decomposition", "work-decomposition",
|
|
328
|
+
"dashboard-reporting",
|
|
329
|
+
]);
|
|
330
|
+
return META_META.has(name) ? "meta-meta" : "meta";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Format the skill index + always-loaded bodies for injection into agent context.
|
|
335
|
+
*
|
|
336
|
+
* v0.7.5: emits TWO sections:
|
|
337
|
+
* - Always loaded: full bodies inline (architecturally-needed for the phase)
|
|
338
|
+
* - Available: name + description tease + reminder to use consult_skill
|
|
128
339
|
*
|
|
129
|
-
*
|
|
130
|
-
* to the phase (per PHASE_RELEVANT_SKILLS). Unknown skills stay visible
|
|
131
|
-
* so new additions to template/skills/ aren't accidentally hidden.
|
|
340
|
+
* Pre-v0.7.5: emitted only descriptions for the phase-relevant subset.
|
|
132
341
|
*
|
|
133
|
-
* @param {string} [phase] - Current engine phase
|
|
342
|
+
* @param {string} [phase] - Current engine phase
|
|
134
343
|
* @returns {string}
|
|
135
344
|
*/
|
|
136
345
|
formatForContext(phase) {
|
|
137
346
|
const index = this.getIndex();
|
|
138
347
|
if (index.length === 0) return "";
|
|
139
348
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
349
|
+
const { alwaysLoaded, available } = this.getPhaseSkillSet(phase);
|
|
350
|
+
const alwaysSet = new Set(alwaysLoaded);
|
|
351
|
+
const availableSet = new Set(available);
|
|
143
352
|
|
|
144
|
-
const
|
|
145
|
-
const meta = visible.filter((s) => s.category === "meta");
|
|
146
|
-
const other = visible.filter((s) => s.category !== "meta-meta" && s.category !== "meta");
|
|
353
|
+
const byName = new Map(index.map(s => [s.name, s]));
|
|
147
354
|
|
|
148
|
-
const lines = [
|
|
149
|
-
"Read full skill content from the skills/ directory when needed.\n"];
|
|
355
|
+
const lines = [];
|
|
150
356
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
357
|
+
// Section 1: Always-loaded skill bodies (inline)
|
|
358
|
+
if (alwaysLoaded.length > 0) {
|
|
359
|
+
lines.push("## Methodology Skills — Loaded Into Your Context");
|
|
360
|
+
lines.push(
|
|
361
|
+
"These are the architecturally-required skills for the current phase. " +
|
|
362
|
+
"Treat their content as authoritative guidance for your work in this phase. " +
|
|
363
|
+
"If meta-meta and meta guidance conflict, meta-meta wins (architect's frame " +
|
|
364
|
+
"bounds the technique).\n",
|
|
365
|
+
);
|
|
366
|
+
for (const name of alwaysLoaded) {
|
|
367
|
+
const skill = byName.get(name);
|
|
368
|
+
const body = this.loadSkillBody(name);
|
|
369
|
+
if (!body) continue;
|
|
370
|
+
const tierLabel = skill?.tier ? ` [tier: ${skill.tier}]` : "";
|
|
371
|
+
lines.push(`### ${name}${tierLabel}\n`);
|
|
372
|
+
lines.push(body.trim());
|
|
373
|
+
lines.push("");
|
|
155
374
|
}
|
|
156
|
-
lines.push("");
|
|
157
375
|
}
|
|
158
376
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
377
|
+
// Section 2: Available skills (description teases only)
|
|
378
|
+
const consultable = [...availableSet].filter(n => !alwaysSet.has(n));
|
|
379
|
+
if (consultable.length > 0) {
|
|
380
|
+
lines.push("## Available Methodology Skills");
|
|
381
|
+
lines.push(
|
|
382
|
+
"Call `consult_skill(name)` to load the full body into your conversation " +
|
|
383
|
+
"history when a description tease isn't enough. Each consult returns the " +
|
|
384
|
+
"skill body once; subsequent turns may need to re-consult if the body has " +
|
|
385
|
+
"aged out of context.\n",
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const visible = consultable
|
|
389
|
+
.map(n => byName.get(n))
|
|
390
|
+
.filter(Boolean);
|
|
391
|
+
|
|
392
|
+
const metaMeta = visible.filter(s => s.tier === "meta-meta");
|
|
393
|
+
const meta = visible.filter(s => s.tier !== "meta-meta");
|
|
394
|
+
|
|
395
|
+
if (metaMeta.length > 0) {
|
|
396
|
+
lines.push("**System Architecture (meta-meta):**");
|
|
397
|
+
for (const s of metaMeta) {
|
|
398
|
+
lines.push(`- **${s.name}**: ${s.description.slice(0, 160)}`);
|
|
399
|
+
}
|
|
400
|
+
lines.push("");
|
|
163
401
|
}
|
|
164
|
-
lines.push("");
|
|
165
|
-
}
|
|
166
402
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
403
|
+
if (meta.length > 0) {
|
|
404
|
+
lines.push("**Procedural Techniques (meta):**");
|
|
405
|
+
for (const s of meta) {
|
|
406
|
+
lines.push(`- **${s.name}**: ${s.description.slice(0, 160)}`);
|
|
407
|
+
}
|
|
408
|
+
lines.push("");
|
|
171
409
|
}
|
|
172
410
|
}
|
|
173
411
|
|
|
174
412
|
return lines.join("\n");
|
|
175
413
|
}
|
|
176
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Populate `<workspace>/skills/` with the available skill set for a phase.
|
|
417
|
+
*
|
|
418
|
+
* Uses symlink-with-copy-fallback: each phase's `available` skills are
|
|
419
|
+
* symlinked from the bundled template/skills/<lang>/<name>/ into
|
|
420
|
+
* <workspace>/skills/<name>. On phase advance/retreat, the workspace
|
|
421
|
+
* `skills/` is wiped + re-populated to match the new phase.
|
|
422
|
+
*
|
|
423
|
+
* The agent's `ls skills/` shows only the phase-relevant set. The
|
|
424
|
+
* `consult_skill` tool reads bodies via SkillLoader (independent of
|
|
425
|
+
* workspace dir state), so consult still works for the available set
|
|
426
|
+
* even if symlinks fail.
|
|
427
|
+
*
|
|
428
|
+
* @param {string} workspaceCwd - absolute path to workspace root
|
|
429
|
+
* @param {string} phase - current phase
|
|
430
|
+
* @returns {{phase: string, populated: string[], failures: Array<{name: string, error: string}>}}
|
|
431
|
+
*/
|
|
432
|
+
populateWorkspaceSkills(workspaceCwd, phase) {
|
|
433
|
+
const targetDir = path.join(workspaceCwd, "skills");
|
|
434
|
+
const result = { phase, populated: [], failures: [] };
|
|
435
|
+
|
|
436
|
+
// Clear existing skills/ contents (preserve dir).
|
|
437
|
+
try {
|
|
438
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
439
|
+
for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
|
|
440
|
+
const entryPath = path.join(targetDir, entry.name);
|
|
441
|
+
try {
|
|
442
|
+
if (entry.isSymbolicLink() || entry.isFile()) {
|
|
443
|
+
fs.unlinkSync(entryPath);
|
|
444
|
+
} else if (entry.isDirectory()) {
|
|
445
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
result.failures.push({ name: entry.name, error: `clear failed: ${err.message}` });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch (err) {
|
|
452
|
+
result.failures.push({ name: "(setup)", error: `mkdir/clear failed: ${err.message}` });
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const { available } = this.getPhaseSkillSet(phase);
|
|
457
|
+
|
|
458
|
+
for (const name of available) {
|
|
459
|
+
const sourceDir = this._findSkillDir(name);
|
|
460
|
+
if (!sourceDir) {
|
|
461
|
+
result.failures.push({ name, error: "source not found in bundled skills" });
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const linkPath = path.join(targetDir, name);
|
|
465
|
+
try {
|
|
466
|
+
// Try symlink first (zero file churn on phase advance/retreat).
|
|
467
|
+
fs.symlinkSync(sourceDir, linkPath, "dir");
|
|
468
|
+
result.populated.push(name);
|
|
469
|
+
} catch (symErr) {
|
|
470
|
+
// Fallback: recursive copy. Slower but works on Windows / restricted FSes.
|
|
471
|
+
try {
|
|
472
|
+
this._recursiveCopy(sourceDir, linkPath);
|
|
473
|
+
result.populated.push(name);
|
|
474
|
+
} catch (copyErr) {
|
|
475
|
+
result.failures.push({ name, error: `symlink: ${symErr.message}; copy: ${copyErr.message}` });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Recursive directory copy (sync). Used as fallback when symlinkSync fails.
|
|
485
|
+
*/
|
|
486
|
+
_recursiveCopy(srcDir, destDir) {
|
|
487
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
488
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
489
|
+
const src = path.join(srcDir, entry.name);
|
|
490
|
+
const dst = path.join(destDir, entry.name);
|
|
491
|
+
if (entry.isDirectory()) {
|
|
492
|
+
this._recursiveCopy(src, dst);
|
|
493
|
+
} else if (entry.isFile()) {
|
|
494
|
+
fs.copyFileSync(src, dst);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
177
499
|
/**
|
|
178
500
|
* Parse YAML frontmatter from a SKILL.md file.
|
|
179
|
-
*
|
|
501
|
+
* Extracts name, description, and tier.
|
|
180
502
|
*/
|
|
181
503
|
_parseFrontmatter(filePath) {
|
|
182
504
|
try {
|
|
@@ -186,12 +508,12 @@ export class SkillLoader {
|
|
|
186
508
|
|
|
187
509
|
const frontmatter = match[1];
|
|
188
510
|
const name = frontmatter.match(/^name:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
511
|
+
const tier = frontmatter.match(/^tier:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
189
512
|
|
|
190
|
-
//
|
|
513
|
+
// Description: single-line OR multi-line (YAML > folded)
|
|
191
514
|
let description = "";
|
|
192
515
|
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
193
516
|
if (descMatch && descMatch[1].trim() === ">") {
|
|
194
|
-
// Multi-line: capture indented lines after "description: >"
|
|
195
517
|
const multiMatch = frontmatter.match(/^description:\s*>\s*\n((?:[ \t]+.+\n?)*)/m);
|
|
196
518
|
if (multiMatch) {
|
|
197
519
|
description = multiMatch[1].replace(/^[ \t]+/gm, "").replace(/\n/g, " ").trim();
|
|
@@ -199,7 +521,7 @@ export class SkillLoader {
|
|
|
199
521
|
} else if (descMatch) {
|
|
200
522
|
description = descMatch[1].trim();
|
|
201
523
|
}
|
|
202
|
-
return { name, description };
|
|
524
|
+
return { name, description, tier };
|
|
203
525
|
} catch {
|
|
204
526
|
return {};
|
|
205
527
|
}
|
|
@@ -382,8 +382,8 @@ export class AgentTool extends BaseTool {
|
|
|
382
382
|
* B8: List currently-running sub-agents. Called by engine's phase-advance
|
|
383
383
|
* path to emit a `stale_subagents` pipeline event — the main agent's next
|
|
384
384
|
* turn sees the list and decides whether to kill each. Soft signal, not
|
|
385
|
-
* an automated kill
|
|
386
|
-
*
|
|
385
|
+
* an automated kill: coupling the subagent lifecycle to phase advance
|
|
386
|
+
* would amplify blast radius if a transition happened unexpectedly.
|
|
387
387
|
*/
|
|
388
388
|
getRunningTaskIds() {
|
|
389
389
|
return Array.from(this._runningTasks.keys());
|