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.
Files changed (109) hide show
  1. package/README.md +57 -4
  2. package/bin/kc-beta.js +20 -6
  3. package/package.json +3 -2
  4. package/src/agent/engine.js +493 -132
  5. package/src/agent/pipelines/_advance-hints.js +92 -0
  6. package/src/agent/pipelines/_milestone-derive.js +387 -17
  7. package/src/agent/pipelines/initializer.js +4 -1
  8. package/src/agent/pipelines/skill-authoring.js +30 -1
  9. package/src/agent/skill-loader.js +433 -111
  10. package/src/agent/tools/agent-tool.js +2 -2
  11. package/src/agent/tools/consult-skill.js +127 -0
  12. package/src/agent/tools/copy-to-workspace.js +4 -3
  13. package/src/agent/tools/dashboard-render.js +48 -1
  14. package/src/agent/tools/document-parse.js +31 -2
  15. package/src/agent/tools/phase-advance.js +17 -13
  16. package/src/agent/tools/release.js +378 -8
  17. package/src/agent/tools/sandbox-exec.js +65 -8
  18. package/src/agent/tools/worker-llm-call.js +95 -15
  19. package/src/agent/tools/workspace-file.js +7 -7
  20. package/src/agent/workspace.js +25 -4
  21. package/src/cli/components.js +4 -1
  22. package/src/cli/index.js +97 -1
  23. package/src/config.js +20 -3
  24. package/src/marathon/driver.js +217 -0
  25. package/src/marathon/prompts.js +93 -0
  26. package/template/.env.template +16 -0
  27. package/template/AGENT.md +182 -7
  28. package/template/skills/en/{meta-meta/auto-model-selection → auto-model-selection}/SKILL.md +1 -0
  29. package/template/skills/en/{meta-meta/bootstrap-workspace → bootstrap-workspace}/SKILL.md +15 -0
  30. package/template/skills/{zh/meta → en}/compliance-judgment/SKILL.md +1 -0
  31. package/template/skills/en/{meta/confidence-system → confidence-system}/SKILL.md +1 -0
  32. package/template/skills/en/{meta/corner-case-management → corner-case-management}/SKILL.md +1 -0
  33. package/template/skills/en/{meta/cross-document-verification → cross-document-verification}/SKILL.md +1 -0
  34. package/template/skills/en/{meta-meta/dashboard-reporting → dashboard-reporting}/SKILL.md +1 -0
  35. package/template/skills/en/{meta/data-sensibility → data-sensibility}/SKILL.md +1 -0
  36. package/template/skills/{zh/meta → en}/document-chunking/SKILL.md +1 -0
  37. package/template/skills/en/{meta/document-parsing → document-parsing}/SKILL.md +1 -0
  38. package/template/skills/{zh/meta → en}/entity-extraction/SKILL.md +1 -0
  39. package/template/skills/en/{meta-meta/evolution-loop → evolution-loop}/SKILL.md +1 -0
  40. package/template/skills/en/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/SKILL.md +1 -0
  41. package/template/skills/en/{meta-meta/quality-control → quality-control}/SKILL.md +10 -0
  42. package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +1 -0
  43. package/template/skills/en/{meta-meta/rule-graph → rule-graph}/SKILL.md +1 -0
  44. package/template/skills/en/{meta-meta/skill-authoring → skill-authoring}/SKILL.md +40 -0
  45. package/template/skills/en/skill-creator/SKILL.md +2 -1
  46. package/template/skills/en/{meta-meta/skill-to-workflow → skill-to-workflow}/SKILL.md +58 -4
  47. package/template/skills/en/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
  48. package/template/skills/en/{meta/tree-processing → tree-processing}/SKILL.md +1 -0
  49. package/template/skills/en/{meta-meta/version-control → version-control}/SKILL.md +1 -0
  50. package/template/skills/en/{meta-meta/work-decomposition → work-decomposition}/SKILL.md +51 -6
  51. package/template/skills/phase_skills.yaml +112 -0
  52. package/template/skills/zh/{meta-meta/auto-model-selection → auto-model-selection}/SKILL.md +1 -0
  53. package/template/skills/zh/{meta-meta/bootstrap-workspace → bootstrap-workspace}/SKILL.md +15 -0
  54. package/template/skills/zh/compliance-judgment/SKILL.md +83 -0
  55. package/template/skills/zh/{meta/confidence-system → confidence-system}/SKILL.md +1 -0
  56. package/template/skills/zh/{meta/corner-case-management → corner-case-management}/SKILL.md +1 -0
  57. package/template/skills/zh/{meta/cross-document-verification → cross-document-verification}/SKILL.md +1 -0
  58. package/template/skills/zh/{meta-meta/dashboard-reporting → dashboard-reporting}/SKILL.md +1 -0
  59. package/template/skills/zh/{meta/data-sensibility → data-sensibility}/SKILL.md +1 -0
  60. package/template/skills/zh/document-chunking/SKILL.md +40 -0
  61. package/template/skills/zh/document-parsing/SKILL.md +102 -0
  62. package/template/skills/zh/entity-extraction/SKILL.md +121 -0
  63. package/template/skills/zh/{meta-meta/evolution-loop → evolution-loop}/SKILL.md +1 -0
  64. package/template/skills/zh/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/SKILL.md +1 -0
  65. package/template/skills/zh/{meta-meta/quality-control → quality-control}/SKILL.md +10 -0
  66. package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +1 -0
  67. package/template/skills/zh/{meta-meta/rule-graph → rule-graph}/SKILL.md +1 -0
  68. package/template/skills/zh/{meta-meta/skill-authoring → skill-authoring}/SKILL.md +40 -0
  69. package/template/skills/zh/skill-creator/SKILL.md +205 -200
  70. package/template/skills/zh/skill-to-workflow/SKILL.md +243 -0
  71. package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
  72. package/template/skills/zh/tree-processing/SKILL.md +126 -0
  73. package/template/skills/zh/{meta-meta/version-control → version-control}/SKILL.md +1 -0
  74. package/template/skills/zh/{meta-meta/work-decomposition → work-decomposition}/SKILL.md +49 -4
  75. package/template/workflows/common/llm_client.py +168 -0
  76. package/template/workflows/common/utils.py +132 -0
  77. package/template/CLAUDE.md +0 -150
  78. package/template/skills/en/meta/compliance-judgment/SKILL.md +0 -82
  79. package/template/skills/en/meta/document-chunking/SKILL.md +0 -32
  80. package/template/skills/en/meta/entity-extraction/SKILL.md +0 -120
  81. package/template/skills/zh/meta/document-parsing/SKILL.md +0 -101
  82. package/template/skills/zh/meta/tree-processing/SKILL.md +0 -121
  83. package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +0 -188
  84. /package/template/skills/en/{meta/compliance-judgment → compliance-judgment}/references/output-format.md +0 -0
  85. /package/template/skills/en/{meta/cross-document-verification → cross-document-verification}/references/contradiction-taxonomy.md +0 -0
  86. /package/template/skills/en/{meta-meta/dashboard-reporting → dashboard-reporting}/scripts/generate_dashboard.py +0 -0
  87. /package/template/skills/en/{meta/document-parsing → document-parsing}/references/parser-catalog.md +0 -0
  88. /package/template/skills/en/{meta-meta/evolution-loop → evolution-loop}/references/convergence-guide.md +0 -0
  89. /package/template/skills/en/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/scripts/generate_review.js +0 -0
  90. /package/template/skills/en/{meta-meta/quality-control → quality-control}/references/qa-layers.md +0 -0
  91. /package/template/skills/en/{meta-meta/quality-control → quality-control}/references/sampling-strategies.md +0 -0
  92. /package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/references/chunking-strategies.md +0 -0
  93. /package/template/skills/en/{meta-meta/skill-authoring → skill-authoring}/references/skill-format-spec.md +0 -0
  94. /package/template/skills/en/{meta-meta/skill-to-workflow → skill-to-workflow}/references/worker-llm-catalog.md +0 -0
  95. /package/template/skills/en/{meta-meta/task-decomposition → task-decomposition}/references/decision-matrix.md +0 -0
  96. /package/template/skills/en/{meta-meta/version-control → version-control}/references/trace-id-spec.md +0 -0
  97. /package/template/skills/zh/{meta/compliance-judgment → compliance-judgment}/references/output-format.md +0 -0
  98. /package/template/skills/zh/{meta/cross-document-verification → cross-document-verification}/references/contradiction-taxonomy.md +0 -0
  99. /package/template/skills/zh/{meta-meta/dashboard-reporting → dashboard-reporting}/scripts/generate_dashboard.py +0 -0
  100. /package/template/skills/zh/{meta/document-parsing → document-parsing}/references/parser-catalog.md +0 -0
  101. /package/template/skills/zh/{meta-meta/evolution-loop → evolution-loop}/references/convergence-guide.md +0 -0
  102. /package/template/skills/zh/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/scripts/generate_review.js +0 -0
  103. /package/template/skills/zh/{meta-meta/quality-control → quality-control}/references/qa-layers.md +0 -0
  104. /package/template/skills/zh/{meta-meta/quality-control → quality-control}/references/sampling-strategies.md +0 -0
  105. /package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/references/chunking-strategies.md +0 -0
  106. /package/template/skills/zh/{meta-meta/skill-authoring → skill-authoring}/references/skill-format-spec.md +0 -0
  107. /package/template/skills/zh/{meta-meta/skill-to-workflow → skill-to-workflow}/references/worker-llm-catalog.md +0 -0
  108. /package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/references/decision-matrix.md +0 -0
  109. /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
- // D3b: Phase-relevance map. Skills not listed here are always visible
9
- // (safe default for future additions). Skills listed here are only
10
- // included in the context index for the named phases — unrelated
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
- // Keep this close to the skill set it describes one hardcoded table
15
- // per release, not spread across files. When adding a skill to
16
- // template/skills/, add it here if phase-specific, or leave it out
17
- // to default to always-visible.
18
- const PHASE_RELEVANT_SKILLS = {
19
- "bootstrap-workspace": ["bootstrap"],
20
- "rule-extraction": ["bootstrap", "rule_extraction"],
21
- "rule-graph": ["rule_extraction", "skill_authoring"],
22
- "task-decomposition": ["rule_extraction", "skill_authoring", "distillation"],
23
- // v0.7.0 B1: work-decomposition teaches the system-level decomposition
24
- // discipline (ordering, grouping, difficulty triage, PATTERNS.md memory).
25
- // Distinct from task-decomposition (per-rule sub-tasks). Loaded on
26
- // rule_extraction → skill_authoring transition where the agent owns
27
- // the TaskBoard.
28
- "work-decomposition": ["rule_extraction", "skill_authoring"],
29
- "skill-authoring": ["skill_authoring", "skill_testing"],
30
- "skill-to-workflow": ["distillation"],
31
- "evolution-loop": ["skill_testing", "distillation", "production_qc"],
32
- "version-control": ["bootstrap", "rule_extraction", "skill_authoring", "skill_testing", "distillation", "production_qc", "finalization"],
33
- "quality-control": ["production_qc", "finalization"],
34
- "confidence-system": ["distillation", "production_qc"],
35
- "dashboard-reporting": ["production_qc", "finalization"],
36
- "cross-document-verification": ["production_qc"],
37
- "corner-case-management": ["skill_testing", "distillation", "production_qc"],
38
- "data-sensibility": ["rule_extraction", "skill_authoring"],
39
- "entity-extraction": ["skill_authoring", "distillation"],
40
- "document-parsing": ["bootstrap", "rule_extraction", "skill_authoring"],
41
- "document-chunking": ["bootstrap", "rule_extraction"],
42
- "tree-processing": ["skill_authoring", "skill_testing"],
43
- "compliance-judgment": ["skill_authoring", "skill_testing", "production_qc"],
44
- "skill-creator": ["skill_authoring"],
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
- function isSkillRelevantToPhase(skillName, phase) {
48
- const relevantPhases = PHASE_RELEVANT_SKILLS[skillName];
49
- if (!relevantPhases) return true; // unknown skill → always visible
50
- if (!phase) return true; // caller didn't pass phase → always visible
51
- return relevantPhases.includes(phase);
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
- * Skills are organized as:
61
- * template/skills/{lang}/meta-meta/ — System architecture methodology
62
- * template/skills/{lang}/meta/ — Verification domain methodology
63
- * template/skills/{lang}/skill-creator/ Anthropic's official skill creation toolkit
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
- * @returns {Array<{name: string, description: string, category: string, path: string}>}
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
- for (const category of ["meta-meta", "meta", "skill-creator"]) {
89
- const catDir = path.join(langDir, category);
90
- if (!fs.existsSync(catDir)) continue;
91
-
92
- // skill-creator is a single skill, not a directory of skills
93
- const skillMd = path.join(catDir, "SKILL.md");
94
- if (fs.existsSync(skillMd)) {
95
- const { name, description } = this._parseFrontmatter(skillMd);
96
- if (name) {
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 || category,
99
- description: description || "",
100
- category,
101
- path: path.relative(this._skillsDir, catDir),
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
- * Format the skill index for injection into agent context.
127
- * Brief listing agent reads full content on demand.
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
- * D3b: when `phase` is provided, filter out skills that aren't relevant
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 for filtering
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 visible = phase
141
- ? index.filter((s) => isSkillRelevantToPhase(s.name, phase))
142
- : index;
349
+ const { alwaysLoaded, available } = this.getPhaseSkillSet(phase);
350
+ const alwaysSet = new Set(alwaysLoaded);
351
+ const availableSet = new Set(available);
143
352
 
144
- const metaMeta = visible.filter((s) => s.category === "meta-meta");
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 = ["## Available Methodology Skills",
149
- "Read full skill content from the skills/ directory when needed.\n"];
355
+ const lines = [];
150
356
 
151
- if (metaMeta.length) {
152
- lines.push("**System Architecture (meta-meta):**");
153
- for (const s of metaMeta) {
154
- lines.push(`- **${s.name}**: ${s.description.slice(0, 120)}`);
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
- if (meta.length) {
160
- lines.push("**Verification Methodology (meta):**");
161
- for (const s of meta) {
162
- lines.push(`- **${s.name}**: ${s.description.slice(0, 120)}`);
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
- if (other.length) {
168
- lines.push("**Toolkits:**");
169
- for (const s of other) {
170
- lines.push(`- **${s.name}**: ${s.description.slice(0, 120)}`);
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
- * Only extracts name and description lightweight.
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
- // Handle both single-line and multi-line (YAML >) descriptions
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, because phase_advance can fire from _maybeAutoAdvance
386
- * unexpectedly and coupling the lifecycle would amplify blast radius.
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());