kc-beta 0.7.2 → 0.7.5
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 +21 -8
- package/bin/kc-beta.js +20 -6
- package/package.json +1 -1
- package/src/agent/engine.js +138 -55
- package/src/agent/pipelines/_milestone-derive.js +140 -4
- package/src/agent/pipelines/initializer.js +4 -1
- package/src/agent/skill-loader.js +433 -111
- package/src/agent/tools/consult-skill.js +112 -0
- package/src/agent/tools/copy-to-workspace.js +18 -12
- package/src/agent/tools/release.js +128 -1
- package/src/agent/tools/sandbox-exec.js +4 -1
- package/src/agent/tools/task-board.js +194 -0
- package/src/agent/tools/workspace-file.js +57 -43
- package/src/config.js +6 -4
- 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 +1 -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 +1 -0
- package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +60 -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 +1 -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 +5 -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 +37 -2
- package/template/skills/phase_skills.yaml +107 -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 +1 -0
- package/template/skills/{en/meta → zh}/compliance-judgment/SKILL.md +1 -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/{en/meta → zh}/document-chunking/SKILL.md +1 -0
- package/template/skills/zh/{meta/document-parsing → document-parsing}/SKILL.md +1 -0
- package/template/skills/{en/meta → zh}/entity-extraction/SKILL.md +1 -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 +1 -0
- package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +48 -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 +1 -0
- package/template/skills/zh/skill-creator/SKILL.md +2 -1
- package/template/skills/zh/skill-to-workflow/SKILL.md +190 -0
- package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
- package/template/skills/zh/{meta/tree-processing → tree-processing}/SKILL.md +1 -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 +37 -2
- package/template/CLAUDE.md +0 -137
- 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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { BaseTool, ToolResult } from "./base.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v0.7.5: load a methodology skill's body into the agent's conversation
|
|
5
|
+
* history as a tool result. Pairs with the always-loaded body injection
|
|
6
|
+
* in SkillLoader.formatForContext — that handles the 1-2 architecturally-
|
|
7
|
+
* required skills per phase; consult_skill handles the rest on demand.
|
|
8
|
+
*
|
|
9
|
+
* Validation:
|
|
10
|
+
* - Skill name must be in the current phase's available set (per
|
|
11
|
+
* template/skills/phase_skills.yaml).
|
|
12
|
+
* - Already-always-loaded skills return a hint pointing the agent at the
|
|
13
|
+
* system prompt (don't double-load).
|
|
14
|
+
* - Missing bodies return an error result.
|
|
15
|
+
*
|
|
16
|
+
* Emits `skill_invoked` event with proper skill name on success — replaces
|
|
17
|
+
* the older path-matching regex at engine.js:1297-1313 that produced
|
|
18
|
+
* "(unknown)" spam from rule_skills/<id>/SKILL.md writes.
|
|
19
|
+
*/
|
|
20
|
+
export class ConsultSkillTool extends BaseTool {
|
|
21
|
+
/**
|
|
22
|
+
* @param {import('../workspace.js').Workspace} workspace
|
|
23
|
+
* @param {import('../skill-loader.js').SkillLoader} skillLoader
|
|
24
|
+
* @param {() => string} getCurrentPhase — returns the engine's current phase
|
|
25
|
+
* @param {import('../event-log.js').EventLog} [eventLog] — for skill_invoked emission
|
|
26
|
+
*/
|
|
27
|
+
constructor(workspace, skillLoader, getCurrentPhase, eventLog) {
|
|
28
|
+
super();
|
|
29
|
+
this._workspace = workspace;
|
|
30
|
+
this._skillLoader = skillLoader;
|
|
31
|
+
this._getCurrentPhase = getCurrentPhase;
|
|
32
|
+
this._eventLog = eventLog;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get name() { return "consult_skill"; }
|
|
36
|
+
|
|
37
|
+
get description() {
|
|
38
|
+
return (
|
|
39
|
+
"Load the full body of a methodology skill into your context for the " +
|
|
40
|
+
"current turn. Use when the description tease in the system prompt's " +
|
|
41
|
+
"'Available Methodology Skills' section isn't enough detail to proceed. " +
|
|
42
|
+
"The body lands in your conversation history; subsequent turns can " +
|
|
43
|
+
"reference it via context, or you can re-consult if it ages out. " +
|
|
44
|
+
"Skills already in the 'Loaded Into Your Context' section don't need " +
|
|
45
|
+
"consulting — they're already in your prompt."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get inputSchema() {
|
|
50
|
+
return {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
name: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Skill name as listed in the system prompt (e.g., 'work-decomposition', 'evolution-loop').",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ["name"],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async execute(input) {
|
|
63
|
+
const name = (input?.name || "").trim();
|
|
64
|
+
if (!name) return new ToolResult("name required (e.g. consult_skill({name: 'work-decomposition'}))", true);
|
|
65
|
+
|
|
66
|
+
const phase = this._getCurrentPhase ? this._getCurrentPhase() : null;
|
|
67
|
+
const { alwaysLoaded, available } = this._skillLoader.getPhaseSkillSet(phase);
|
|
68
|
+
|
|
69
|
+
const alwaysSet = new Set(alwaysLoaded);
|
|
70
|
+
const availableSet = new Set(available);
|
|
71
|
+
|
|
72
|
+
if (alwaysSet.has(name)) {
|
|
73
|
+
return new ToolResult(
|
|
74
|
+
`Skill '${name}' is already always-loaded in your system prompt for phase '${phase}'. ` +
|
|
75
|
+
`Re-read the system prompt's 'Methodology Skills — Loaded Into Your Context' section ` +
|
|
76
|
+
`— the body is there. No separate consult needed.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!availableSet.has(name)) {
|
|
81
|
+
const sorted = [...availableSet].sort();
|
|
82
|
+
return new ToolResult(
|
|
83
|
+
`Skill '${name}' is not available in phase '${phase}'. ` +
|
|
84
|
+
`Available for this phase: ${sorted.join(", ")}. ` +
|
|
85
|
+
`If you genuinely need this skill, either advance/retreat to a phase ` +
|
|
86
|
+
`where it's available, or check the spelling.`,
|
|
87
|
+
true,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const body = this._skillLoader.loadSkillBody(name);
|
|
92
|
+
if (!body) {
|
|
93
|
+
return new ToolResult(
|
|
94
|
+
`Skill '${name}' is declared available for phase '${phase}' but its body could not be loaded. ` +
|
|
95
|
+
`This is an engine/template inconsistency — surface to the developer user.`,
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Emit skill_invoked event with the real skill name (replaces the
|
|
101
|
+
// old path-matching regex that produced "(unknown)" spam).
|
|
102
|
+
try {
|
|
103
|
+
this._eventLog?.append?.("skill_invoked", {
|
|
104
|
+
skill: name,
|
|
105
|
+
via_tool: "consult_skill",
|
|
106
|
+
phase,
|
|
107
|
+
});
|
|
108
|
+
} catch { /* event logging is best-effort */ }
|
|
109
|
+
|
|
110
|
+
return new ToolResult(body);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -94,7 +94,7 @@ export class CopyToWorkspaceTool extends BaseTool {
|
|
|
94
94
|
this._appendGitignore(`refs/${targetName}`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
this._appendManifest({
|
|
97
|
+
await this._appendManifest({
|
|
98
98
|
target: targetRel,
|
|
99
99
|
source: sourcePath,
|
|
100
100
|
size: stat.size,
|
|
@@ -113,17 +113,23 @@ export class CopyToWorkspaceTool extends BaseTool {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
_appendManifest(entry) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
async _appendManifest(entry) {
|
|
117
|
+
// v0.7.4 (re-applied from v0.7.3 G1a): refs/manifest.json is a
|
|
118
|
+
// shared coordination path — wrap the whole read-modify-write
|
|
119
|
+
// under the workspace lock so two parallel copy_to_workspace
|
|
120
|
+
// calls (main agent + subagent) don't lose entries.
|
|
121
|
+
return await this._workspace.withSharedLockIfApplicable(MANIFEST_REL, () => {
|
|
122
|
+
const manifestAbs = this._workspace.resolvePath(MANIFEST_REL);
|
|
123
|
+
fs.mkdirSync(path.dirname(manifestAbs), { recursive: true });
|
|
124
|
+
let entries = [];
|
|
125
|
+
if (fs.existsSync(manifestAbs)) {
|
|
126
|
+
try { entries = JSON.parse(fs.readFileSync(manifestAbs, "utf-8")); }
|
|
127
|
+
catch { entries = []; }
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(entries)) entries = [];
|
|
130
|
+
entries.push(entry);
|
|
131
|
+
fs.writeFileSync(manifestAbs, JSON.stringify(entries, null, 2), "utf-8");
|
|
132
|
+
});
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
_appendGitignore(line) {
|
|
@@ -185,8 +185,23 @@ export class ReleaseTool extends BaseTool {
|
|
|
185
185
|
// file through and emitted a stub on miss. We try to populate from
|
|
186
186
|
// known QC artifact shapes here; if nothing matches, fall through
|
|
187
187
|
// to the existing stub fallback.
|
|
188
|
+
// v0.7.5 G-H3: aggregator now runs if calibSrc is MISSING **or** has
|
|
189
|
+
// empty `historical_accuracy`. v0.7.4 audit (both 贷款 + 资管) shipped
|
|
190
|
+
// empty stubs despite QC data on disk — root cause was the v0.7.2
|
|
191
|
+
// gate only checked file existence; a stub written earlier (e.g., on
|
|
192
|
+
// finalization phase entry) kept the aggregator from firing later.
|
|
188
193
|
const calibSrc = path.join(this._workspace.cwd, "confidence_calibration.json");
|
|
189
|
-
|
|
194
|
+
let shouldAggregate = !fs.existsSync(calibSrc);
|
|
195
|
+
if (!shouldAggregate) {
|
|
196
|
+
try {
|
|
197
|
+
const existing = JSON.parse(fs.readFileSync(calibSrc, "utf-8"));
|
|
198
|
+
const ha = existing?.historical_accuracy;
|
|
199
|
+
if (!ha || (typeof ha === "object" && Object.keys(ha).length === 0)) {
|
|
200
|
+
shouldAggregate = true;
|
|
201
|
+
}
|
|
202
|
+
} catch { shouldAggregate = true; } // corrupt → re-aggregate
|
|
203
|
+
}
|
|
204
|
+
if (shouldAggregate) {
|
|
190
205
|
const aggregated = this._aggregateAccuracyFromOutput();
|
|
191
206
|
if (aggregated && Object.keys(aggregated.historical_accuracy).length > 0) {
|
|
192
207
|
fs.writeFileSync(calibSrc, JSON.stringify(aggregated, null, 2) + "\n", "utf-8");
|
|
@@ -247,6 +262,14 @@ export class ReleaseTool extends BaseTool {
|
|
|
247
262
|
.replace(/\{RULES_LIST\}/g, rulesList);
|
|
248
263
|
fs.writeFileSync(path.join(bundleAbs, "README.md"), readme, "utf-8");
|
|
249
264
|
|
|
265
|
+
// v0.7.5 G-H4: sweep any leftover `.tmpl` files from the bundle dir.
|
|
266
|
+
// template/release/v1/ contains manifest.json.tmpl, catalog.json.tmpl,
|
|
267
|
+
// README.md.tmpl. _copyDir's exclude list (line 119) only filters
|
|
268
|
+
// README.md.tmpl; the other two ride along and persist alongside their
|
|
269
|
+
// populated counterparts. Audit (v0.7.4 贷款) confirmed this regression
|
|
270
|
+
// of v0.7.2 G1d which only handled the v1/ scaffold case.
|
|
271
|
+
this._sweepTmplFiles(bundleAbs);
|
|
272
|
+
|
|
250
273
|
// v0.7.2 1d: clean up the template scaffold dir if a customized
|
|
251
274
|
// release was just written alongside it. Both v0.7.1 audit runs
|
|
252
275
|
// shipped with `output/releases/v1/` (template-derived, .tmpl
|
|
@@ -303,6 +326,25 @@ export class ReleaseTool extends BaseTool {
|
|
|
303
326
|
}
|
|
304
327
|
}
|
|
305
328
|
|
|
329
|
+
/**
|
|
330
|
+
* v0.7.5 G-H4: recursively remove any `*.tmpl` files from a directory.
|
|
331
|
+
* Used after populating a release bundle to drop template stubs that
|
|
332
|
+
* weren't filtered by the initial copy's exclude list. Idempotent.
|
|
333
|
+
*/
|
|
334
|
+
_sweepTmplFiles(dir) {
|
|
335
|
+
try {
|
|
336
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return;
|
|
337
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
338
|
+
const entryPath = path.join(dir, entry.name);
|
|
339
|
+
if (entry.isDirectory()) {
|
|
340
|
+
this._sweepTmplFiles(entryPath);
|
|
341
|
+
} else if (entry.isFile() && entry.name.endsWith(".tmpl")) {
|
|
342
|
+
try { fs.unlinkSync(entryPath); } catch { /* best-effort */ }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch { /* best-effort */ }
|
|
346
|
+
}
|
|
347
|
+
|
|
306
348
|
_findLatestWorkflow(ruleId) {
|
|
307
349
|
// Canonical: workflows/<ruleId>/workflow_v#.py (subdirectory layout)
|
|
308
350
|
const wfDir = path.join(this._workspace.cwd, "workflows", ruleId);
|
|
@@ -338,10 +380,95 @@ export class ReleaseTool extends BaseTool {
|
|
|
338
380
|
}
|
|
339
381
|
} catch { /* manifest unreadable; skip */ }
|
|
340
382
|
}
|
|
383
|
+
|
|
384
|
+
// v0.7.5 G-H2: master / grouped workflow pattern. Agent shipped a
|
|
385
|
+
// single workflow folder (e.g., workflows/master/ or workflows/
|
|
386
|
+
// bank_wm_compliance/) declaring `source_rules: [R001, R002, ...]`
|
|
387
|
+
// in its SKILL.md / workflow.md / config.json. The manifest writer
|
|
388
|
+
// should credit this rule_id as covered by that workflow.
|
|
389
|
+
//
|
|
390
|
+
// Walk workflows/ subdirs looking for a source_rules declaration
|
|
391
|
+
// that includes this ruleId. Return the first matching workflow file.
|
|
392
|
+
// Audit (v0.7.4 贷款 session) confirmed manifest under-counted:
|
|
393
|
+
// catalog had 15 rules; manifest only listed R001 because R002-R015
|
|
394
|
+
// weren't found as standalone workflows.
|
|
395
|
+
for (const entry of fs.readdirSync(flatRoot, { withFileTypes: true })) {
|
|
396
|
+
if (!entry.isDirectory()) continue;
|
|
397
|
+
if (entry.name === ruleId) continue; // already checked above
|
|
398
|
+
const subDir = path.join(flatRoot, entry.name);
|
|
399
|
+
const declaredRules = this._readWorkflowSourceRules(subDir);
|
|
400
|
+
if (declaredRules.includes(ruleId)) {
|
|
401
|
+
// Find the workflow entry file in this dir
|
|
402
|
+
const subFiles = fs.readdirSync(subDir);
|
|
403
|
+
const versioned = subFiles.filter((f) => /^workflow_v\d+\.py$/.test(f)).sort();
|
|
404
|
+
if (versioned.length > 0) return path.join(subDir, versioned[versioned.length - 1]);
|
|
405
|
+
const any = subFiles.find((f) => f.endsWith(".py"));
|
|
406
|
+
if (any) return path.join(subDir, any);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
341
409
|
}
|
|
342
410
|
return null;
|
|
343
411
|
}
|
|
344
412
|
|
|
413
|
+
/**
|
|
414
|
+
* v0.7.5 G-H2: read a workflow directory's source_rules declaration.
|
|
415
|
+
* Checks SKILL.md / workflow.md frontmatter (`source_rules: [...]`)
|
|
416
|
+
* and config.json (`source_rules`, `rules`, or `rule_ids` field).
|
|
417
|
+
* Returns array of canonical rule IDs.
|
|
418
|
+
*/
|
|
419
|
+
_readWorkflowSourceRules(workflowDir) {
|
|
420
|
+
const ids = new Set();
|
|
421
|
+
try {
|
|
422
|
+
const files = fs.readdirSync(workflowDir);
|
|
423
|
+
|
|
424
|
+
// Frontmatter sources
|
|
425
|
+
for (const fname of files) {
|
|
426
|
+
if (!/^(skill|workflow)\.md$/i.test(fname)) continue;
|
|
427
|
+
let content;
|
|
428
|
+
try { content = fs.readFileSync(path.join(workflowDir, fname), "utf-8"); } catch { continue; }
|
|
429
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
430
|
+
if (!fmMatch) continue;
|
|
431
|
+
const fm = fmMatch[1];
|
|
432
|
+
// Inline form
|
|
433
|
+
const inlineMatch = fm.match(/^source_rules\s*:\s*\[([^\]]*)\]\s*$/m);
|
|
434
|
+
if (inlineMatch) {
|
|
435
|
+
inlineMatch[1].split(",").map(s => s.trim().replace(/^["']|["']$/g, ""))
|
|
436
|
+
.filter(Boolean).forEach(s => {
|
|
437
|
+
const m = s.match(/^R0*(\d+)$/i);
|
|
438
|
+
if (m) ids.add(`R${String(parseInt(m[1], 10)).padStart(3, "0")}`);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// Block form
|
|
442
|
+
const blockMatch = fm.match(/^source_rules\s*:\s*\n((?:[ \t]+-\s+\S+\s*\n?)+)/m);
|
|
443
|
+
if (blockMatch) {
|
|
444
|
+
blockMatch[1].split("\n").forEach(line => {
|
|
445
|
+
const m = line.match(/^[ \t]+-\s+["']?(R0*\d+)["']?\s*$/i);
|
|
446
|
+
if (m) {
|
|
447
|
+
const n = m[1].match(/R0*(\d+)/i);
|
|
448
|
+
if (n) ids.add(`R${String(parseInt(n[1], 10)).padStart(3, "0")}`);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Config.json sources
|
|
455
|
+
const configPath = path.join(workflowDir, "config.json");
|
|
456
|
+
if (fs.existsSync(configPath)) {
|
|
457
|
+
try {
|
|
458
|
+
const data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
459
|
+
const rules = Array.isArray(data?.source_rules) ? data.source_rules :
|
|
460
|
+
Array.isArray(data?.rules) ? data.rules :
|
|
461
|
+
Array.isArray(data?.rule_ids) ? data.rule_ids : [];
|
|
462
|
+
for (const r of rules) {
|
|
463
|
+
const m = String(r).match(/^R0*(\d+)$/i);
|
|
464
|
+
if (m) ids.add(`R${String(parseInt(m[1], 10)).padStart(3, "0")}`);
|
|
465
|
+
}
|
|
466
|
+
} catch { /* ignore */ }
|
|
467
|
+
}
|
|
468
|
+
} catch { /* dir unreadable */ }
|
|
469
|
+
return [...ids];
|
|
470
|
+
}
|
|
471
|
+
|
|
345
472
|
_resolveFixture(rel) {
|
|
346
473
|
// Try samples/ first (workspace, then project), then plain workspace path
|
|
347
474
|
const candidates = [];
|
|
@@ -44,7 +44,10 @@ export class SandboxExecTool extends BaseTool {
|
|
|
44
44
|
"Execute a shell command. " +
|
|
45
45
|
"cwd='workspace' (default) runs in KC's workspace. " +
|
|
46
46
|
"cwd='project' runs in the user's project directory. " +
|
|
47
|
-
"Pipes, redirects, and chained commands (&&) are supported."
|
|
47
|
+
"Pipes, redirects, and chained commands (&&) are supported. " +
|
|
48
|
+
"stdout + stderr combined are capped at 10,000 chars; longer output is truncated. " +
|
|
49
|
+
"For reading individual files larger than ~10 KB (e.g. regulation documents), " +
|
|
50
|
+
"prefer workspace_file (operation=read) which has a larger 50 KB cap."
|
|
48
51
|
);
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { BaseTool, ToolResult } from "./base.js";
|
|
2
|
+
|
|
3
|
+
const TASKS_REL = "tasks.json";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* v0.7.3 — TaskCreate / TaskUpdate / TaskComplete tools.
|
|
7
|
+
*
|
|
8
|
+
* Completes the v0.7.0 "agent owns TaskBoard" design. The engine no longer
|
|
9
|
+
* auto-populates per-rule tasks on phase entry (PER_RULE_PHASES is empty by
|
|
10
|
+
* default — see task-manager.js); the agent reads the rule list via
|
|
11
|
+
* describeState, picks a decomposition (single / grouped / range / non-rule),
|
|
12
|
+
* and calls these tools to populate tasks.json. The Ralph loop in
|
|
13
|
+
* AgentEngine._runTaskLoopSerial then walks pending tasks one at a time.
|
|
14
|
+
*
|
|
15
|
+
* Skill teaching for these tools lives in
|
|
16
|
+
* template/skills/{en,zh}/meta-meta/work-decomposition/SKILL.md.
|
|
17
|
+
*
|
|
18
|
+
* tasks.json is a shared-coordination path (workspace.js
|
|
19
|
+
* SHARED_COORDINATION_PATHS) — every write goes through
|
|
20
|
+
* withSharedLockIfApplicable so two writers (main + subagent) serialize.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class TaskCreateTool extends BaseTool {
|
|
24
|
+
constructor(workspace, taskManager) {
|
|
25
|
+
super();
|
|
26
|
+
this._workspace = workspace;
|
|
27
|
+
this._taskManager = taskManager;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get name() { return "TaskCreate"; }
|
|
31
|
+
|
|
32
|
+
get description() {
|
|
33
|
+
return (
|
|
34
|
+
"Add a task to the session task board. Tasks gate the Ralph loop — " +
|
|
35
|
+
"after the current turn ends, the engine pulls the next pending task " +
|
|
36
|
+
"and runs it. Use one task per unit of work you want to iterate on " +
|
|
37
|
+
"(per-rule, per-group, per-document — your decomposition). " +
|
|
38
|
+
"Call this on phase entry after reading describeState."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get inputSchema() {
|
|
43
|
+
return {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
id: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Unique task ID within this session (e.g. 'R001-skill_authoring' or 'group-trust-1').",
|
|
49
|
+
},
|
|
50
|
+
title: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Short human-readable title for the task.",
|
|
53
|
+
},
|
|
54
|
+
phase: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Phase this task belongs to (e.g. 'skill_authoring', 'skill_testing', 'distillation').",
|
|
57
|
+
},
|
|
58
|
+
ruleId: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Optional rule_id if this is a per-rule task. Omit for grouped or non-rule tasks.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ["id", "title", "phase"],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async execute(input) {
|
|
68
|
+
const id = input.id || "";
|
|
69
|
+
const title = input.title || "";
|
|
70
|
+
const phase = input.phase || "";
|
|
71
|
+
const ruleId = input.ruleId || null;
|
|
72
|
+
|
|
73
|
+
if (!id) return new ToolResult("id required", true);
|
|
74
|
+
if (!title) return new ToolResult("title required", true);
|
|
75
|
+
if (!phase) return new ToolResult("phase required", true);
|
|
76
|
+
|
|
77
|
+
return await this._workspace.withSharedLockIfApplicable(TASKS_REL, () => {
|
|
78
|
+
const before = this._taskManager.getAllTasks().some((t) => t.id === id);
|
|
79
|
+
this._taskManager.addTask({ id, title, phase, ruleId });
|
|
80
|
+
if (before) {
|
|
81
|
+
return new ToolResult(`Task ${id} already existed (no-op).`);
|
|
82
|
+
}
|
|
83
|
+
const p = this._taskManager.progress;
|
|
84
|
+
return new ToolResult(
|
|
85
|
+
`Task ${id} created. Board: ${p.pending} pending, ${p.inProgress} in_progress, ${p.completed} completed.`,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class TaskUpdateTool extends BaseTool {
|
|
92
|
+
constructor(workspace, taskManager) {
|
|
93
|
+
super();
|
|
94
|
+
this._workspace = workspace;
|
|
95
|
+
this._taskManager = taskManager;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get name() { return "TaskUpdate"; }
|
|
99
|
+
|
|
100
|
+
get description() {
|
|
101
|
+
return (
|
|
102
|
+
"Update a task's status and optional summary. Status: 'pending', " +
|
|
103
|
+
"'in_progress', 'completed', or 'failed'. Use TaskComplete instead " +
|
|
104
|
+
"for the common case of marking a task done with a summary."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get inputSchema() {
|
|
109
|
+
return {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
id: { type: "string", description: "Task ID to update." },
|
|
113
|
+
status: {
|
|
114
|
+
type: "string",
|
|
115
|
+
enum: ["pending", "in_progress", "completed", "failed"],
|
|
116
|
+
description: "New status for the task.",
|
|
117
|
+
},
|
|
118
|
+
summary: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: "Optional short summary (e.g. why the task failed, what was produced).",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ["id"],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async execute(input) {
|
|
128
|
+
const id = input.id || "";
|
|
129
|
+
const status = input.status;
|
|
130
|
+
const summary = input.summary;
|
|
131
|
+
|
|
132
|
+
if (!id) return new ToolResult("id required", true);
|
|
133
|
+
|
|
134
|
+
return await this._workspace.withSharedLockIfApplicable(TASKS_REL, () => {
|
|
135
|
+
const exists = this._taskManager.getAllTasks().some((t) => t.id === id);
|
|
136
|
+
if (!exists) return new ToolResult(`Task ${id} not found.`, true);
|
|
137
|
+
this._taskManager.updateTask(id, { status, summary });
|
|
138
|
+
const p = this._taskManager.progress;
|
|
139
|
+
return new ToolResult(
|
|
140
|
+
`Task ${id} updated${status ? ` to ${status}` : ""}. ` +
|
|
141
|
+
`Board: ${p.pending} pending, ${p.inProgress} in_progress, ${p.completed} completed, ${p.failed} failed.`,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class TaskCompleteTool extends BaseTool {
|
|
148
|
+
constructor(workspace, taskManager) {
|
|
149
|
+
super();
|
|
150
|
+
this._workspace = workspace;
|
|
151
|
+
this._taskManager = taskManager;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get name() { return "TaskComplete"; }
|
|
155
|
+
|
|
156
|
+
get description() {
|
|
157
|
+
return (
|
|
158
|
+
"Mark a task as completed with an optional summary. Sugar for " +
|
|
159
|
+
"TaskUpdate({id, status: 'completed', summary}). The Ralph loop " +
|
|
160
|
+
"advances to the next pending task after this returns."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get inputSchema() {
|
|
165
|
+
return {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
id: { type: "string", description: "Task ID to complete." },
|
|
169
|
+
summary: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Optional short summary of what was produced.",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ["id"],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async execute(input) {
|
|
179
|
+
const id = input.id || "";
|
|
180
|
+
const summary = input.summary;
|
|
181
|
+
|
|
182
|
+
if (!id) return new ToolResult("id required", true);
|
|
183
|
+
|
|
184
|
+
return await this._workspace.withSharedLockIfApplicable(TASKS_REL, () => {
|
|
185
|
+
const exists = this._taskManager.getAllTasks().some((t) => t.id === id);
|
|
186
|
+
if (!exists) return new ToolResult(`Task ${id} not found.`, true);
|
|
187
|
+
this._taskManager.markDone(id, summary);
|
|
188
|
+
const p = this._taskManager.progress;
|
|
189
|
+
return new ToolResult(
|
|
190
|
+
`Task ${id} completed. Board: ${p.pending} pending, ${p.inProgress} in_progress, ${p.completed} completed.`,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -87,7 +87,7 @@ export class WorkspaceFileTool extends BaseTool {
|
|
|
87
87
|
|
|
88
88
|
try {
|
|
89
89
|
if (op === "read") return this._read(filePath, scope);
|
|
90
|
-
if (op === "write") return this._write(filePath, content, scope);
|
|
90
|
+
if (op === "write") return await this._write(filePath, content, scope);
|
|
91
91
|
if (op === "list") return this._list(filePath, scope);
|
|
92
92
|
return new ToolResult(`Unknown operation: ${op}`, true);
|
|
93
93
|
} catch (err) {
|
|
@@ -107,56 +107,70 @@ export class WorkspaceFileTool extends BaseTool {
|
|
|
107
107
|
return new ToolResult(text);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
_write(filePath, content, scope) {
|
|
110
|
+
async _write(filePath, content, scope) {
|
|
111
111
|
if (!filePath || filePath === ".") {
|
|
112
112
|
return new ToolResult("Path required for write operation", true);
|
|
113
113
|
}
|
|
114
114
|
const resolved = this._resolveForScope(filePath, scope);
|
|
115
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
116
|
-
|
|
117
|
-
// v0.7.0 Group M (#84 remainder): on case-insensitive filesystems
|
|
118
|
-
// (macOS/Windows defaults), warn when the target's basename collides
|
|
119
|
-
// with an existing sibling differing only in case. Write proceeds
|
|
120
|
-
// — agents may legitimately overwrite — but the agent gets visible
|
|
121
|
-
// signal so it doesn't end up confused like E2E #5 GLM ("SKILL.md
|
|
122
|
-
// disappeared" when the inode was shared with skill.md). Workspace-
|
|
123
|
-
// scope only; project-dir scope is the user's territory.
|
|
124
|
-
let collisionNote = "";
|
|
125
|
-
if (
|
|
126
|
-
scope === "workspace" &&
|
|
127
|
-
this._workspace.fsCaseSensitive === false
|
|
128
|
-
) {
|
|
129
|
-
try {
|
|
130
|
-
const parent = path.dirname(resolved);
|
|
131
|
-
const targetBase = path.basename(resolved);
|
|
132
|
-
const targetLower = targetBase.toLowerCase();
|
|
133
|
-
const siblings = fs.readdirSync(parent);
|
|
134
|
-
const collision = siblings.find(
|
|
135
|
-
(s) => s !== targetBase && s.toLowerCase() === targetLower,
|
|
136
|
-
);
|
|
137
|
-
if (collision) {
|
|
138
|
-
collisionNote =
|
|
139
|
-
` ⚠ case-collision: case-insensitive filesystem already has '${collision}'` +
|
|
140
|
-
` at this path; both names resolve to the same inode. Pick one canonical case` +
|
|
141
|
-
` (lowercase preferred for skill files) and use it consistently — otherwise` +
|
|
142
|
-
` archive_file / Read on either name affects the other.`;
|
|
143
|
-
}
|
|
144
|
-
} catch { /* readdirSync may fail on a fresh dir; that's fine, no collision possible */ }
|
|
145
|
-
}
|
|
146
115
|
|
|
147
|
-
|
|
116
|
+
const doWrite = () => {
|
|
117
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
118
|
+
|
|
119
|
+
// v0.7.0 Group M (#84 remainder): on case-insensitive filesystems
|
|
120
|
+
// (macOS/Windows defaults), warn when the target's basename collides
|
|
121
|
+
// with an existing sibling differing only in case. Write proceeds
|
|
122
|
+
// — agents may legitimately overwrite — but the agent gets visible
|
|
123
|
+
// signal so it doesn't end up confused like E2E #5 GLM ("SKILL.md
|
|
124
|
+
// disappeared" when the inode was shared with skill.md). Workspace-
|
|
125
|
+
// scope only; project-dir scope is the user's territory.
|
|
126
|
+
let collisionNote = "";
|
|
127
|
+
if (
|
|
128
|
+
scope === "workspace" &&
|
|
129
|
+
this._workspace.fsCaseSensitive === false
|
|
130
|
+
) {
|
|
131
|
+
try {
|
|
132
|
+
const parent = path.dirname(resolved);
|
|
133
|
+
const targetBase = path.basename(resolved);
|
|
134
|
+
const targetLower = targetBase.toLowerCase();
|
|
135
|
+
const siblings = fs.readdirSync(parent);
|
|
136
|
+
const collision = siblings.find(
|
|
137
|
+
(s) => s !== targetBase && s.toLowerCase() === targetLower,
|
|
138
|
+
);
|
|
139
|
+
if (collision) {
|
|
140
|
+
collisionNote =
|
|
141
|
+
` ⚠ case-collision: case-insensitive filesystem already has '${collision}'` +
|
|
142
|
+
` at this path; both names resolve to the same inode. Pick one canonical case` +
|
|
143
|
+
` (lowercase preferred for skill files) and use it consistently — otherwise` +
|
|
144
|
+
` archive_file / Read on either name affects the other.`;
|
|
145
|
+
}
|
|
146
|
+
} catch { /* readdirSync may fail on a fresh dir; that's fine, no collision possible */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.writeFileSync(resolved, content, "utf-8");
|
|
150
|
+
|
|
151
|
+
// Auto-commit to git for workspace writes (silently no-ops if gitignored or git unavailable)
|
|
152
|
+
let traceId = null;
|
|
153
|
+
if (scope === "workspace") {
|
|
154
|
+
traceId = this._workspace.autoCommit(filePath, "update");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const label = scope === "project" ? `[project] ${filePath}` : filePath;
|
|
158
|
+
let msg = `Wrote ${content.length} chars to ${label}`;
|
|
159
|
+
if (traceId) msg += ` [trace: ${traceId}]`;
|
|
160
|
+
if (collisionNote) msg += collisionNote;
|
|
161
|
+
return new ToolResult(msg);
|
|
162
|
+
};
|
|
148
163
|
|
|
149
|
-
//
|
|
150
|
-
|
|
164
|
+
// v0.7.4 (re-applied from v0.7.3 G1a): route writes to shared
|
|
165
|
+
// coordination paths (rules/catalog.json, tasks.json,
|
|
166
|
+
// refs/manifest.json, etc.) through the workspace lock so
|
|
167
|
+
// concurrent writers serialize. No-op for non-shared paths and
|
|
168
|
+
// for project-scope writes (project dir is the user's, not
|
|
169
|
+
// shared engine state).
|
|
151
170
|
if (scope === "workspace") {
|
|
152
|
-
|
|
171
|
+
return await this._workspace.withSharedLockIfApplicable(filePath, doWrite);
|
|
153
172
|
}
|
|
154
|
-
|
|
155
|
-
const label = scope === "project" ? `[project] ${filePath}` : filePath;
|
|
156
|
-
let msg = `Wrote ${content.length} chars to ${label}`;
|
|
157
|
-
if (traceId) msg += ` [trace: ${traceId}]`;
|
|
158
|
-
if (collisionNote) msg += collisionNote;
|
|
159
|
-
return new ToolResult(msg);
|
|
173
|
+
return doWrite();
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
_list(filePath, scope) {
|
package/src/config.js
CHANGED
|
@@ -90,10 +90,12 @@ export function loadSettings(workspacePath) {
|
|
|
90
90
|
tier3: env.TIER3 || gc.tiers?.tier3 || "",
|
|
91
91
|
tier4: env.TIER4 || gc.tiers?.tier4 || "",
|
|
92
92
|
|
|
93
|
-
// VLM tiers (vision/OCR models)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// VLM tiers (vision/OCR models). v0.7.4: accept OCR_MODEL_TIER* as
|
|
94
|
+
// alias since template/.env.template + initializer.js seed that name.
|
|
95
|
+
// VLM_TIER* takes precedence when both are set.
|
|
96
|
+
vlmTier1: env.VLM_TIER1 || env.OCR_MODEL_TIER1 || gc.vlm_tiers?.tier1 || "",
|
|
97
|
+
vlmTier2: env.VLM_TIER2 || env.OCR_MODEL_TIER2 || gc.vlm_tiers?.tier2 || "",
|
|
98
|
+
vlmTier3: env.VLM_TIER3 || env.OCR_MODEL_TIER3 || gc.vlm_tiers?.tier3 || "",
|
|
97
99
|
|
|
98
100
|
// Worker LLM — optional, defaults to conductor config (process.env wins)
|
|
99
101
|
workerProvider: penv.KC_WORKER_PROVIDER || gc.worker_provider || "",
|