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.
Files changed (90) hide show
  1. package/README.md +21 -8
  2. package/bin/kc-beta.js +20 -6
  3. package/package.json +1 -1
  4. package/src/agent/engine.js +138 -55
  5. package/src/agent/pipelines/_milestone-derive.js +140 -4
  6. package/src/agent/pipelines/initializer.js +4 -1
  7. package/src/agent/skill-loader.js +433 -111
  8. package/src/agent/tools/consult-skill.js +112 -0
  9. package/src/agent/tools/copy-to-workspace.js +18 -12
  10. package/src/agent/tools/release.js +128 -1
  11. package/src/agent/tools/sandbox-exec.js +4 -1
  12. package/src/agent/tools/task-board.js +194 -0
  13. package/src/agent/tools/workspace-file.js +57 -43
  14. package/src/config.js +6 -4
  15. package/template/AGENT.md +182 -7
  16. package/template/skills/en/{meta-meta/auto-model-selection → auto-model-selection}/SKILL.md +1 -0
  17. package/template/skills/en/{meta-meta/bootstrap-workspace → bootstrap-workspace}/SKILL.md +1 -0
  18. package/template/skills/{zh/meta → en}/compliance-judgment/SKILL.md +1 -0
  19. package/template/skills/en/{meta/confidence-system → confidence-system}/SKILL.md +1 -0
  20. package/template/skills/en/{meta/corner-case-management → corner-case-management}/SKILL.md +1 -0
  21. package/template/skills/en/{meta/cross-document-verification → cross-document-verification}/SKILL.md +1 -0
  22. package/template/skills/en/{meta-meta/dashboard-reporting → dashboard-reporting}/SKILL.md +1 -0
  23. package/template/skills/en/{meta/data-sensibility → data-sensibility}/SKILL.md +1 -0
  24. package/template/skills/{zh/meta → en}/document-chunking/SKILL.md +1 -0
  25. package/template/skills/en/{meta/document-parsing → document-parsing}/SKILL.md +1 -0
  26. package/template/skills/{zh/meta → en}/entity-extraction/SKILL.md +1 -0
  27. package/template/skills/en/{meta-meta/evolution-loop → evolution-loop}/SKILL.md +1 -0
  28. package/template/skills/en/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/SKILL.md +1 -0
  29. package/template/skills/en/{meta-meta/quality-control → quality-control}/SKILL.md +1 -0
  30. package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +60 -0
  31. package/template/skills/en/{meta-meta/rule-graph → rule-graph}/SKILL.md +1 -0
  32. package/template/skills/en/{meta-meta/skill-authoring → skill-authoring}/SKILL.md +1 -0
  33. package/template/skills/en/skill-creator/SKILL.md +2 -1
  34. package/template/skills/en/{meta-meta/skill-to-workflow → skill-to-workflow}/SKILL.md +5 -4
  35. package/template/skills/en/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
  36. package/template/skills/en/{meta/tree-processing → tree-processing}/SKILL.md +1 -0
  37. package/template/skills/en/{meta-meta/version-control → version-control}/SKILL.md +1 -0
  38. package/template/skills/en/{meta-meta/work-decomposition → work-decomposition}/SKILL.md +37 -2
  39. package/template/skills/phase_skills.yaml +107 -0
  40. package/template/skills/zh/{meta-meta/auto-model-selection → auto-model-selection}/SKILL.md +1 -0
  41. package/template/skills/zh/{meta-meta/bootstrap-workspace → bootstrap-workspace}/SKILL.md +1 -0
  42. package/template/skills/{en/meta → zh}/compliance-judgment/SKILL.md +1 -0
  43. package/template/skills/zh/{meta/confidence-system → confidence-system}/SKILL.md +1 -0
  44. package/template/skills/zh/{meta/corner-case-management → corner-case-management}/SKILL.md +1 -0
  45. package/template/skills/zh/{meta/cross-document-verification → cross-document-verification}/SKILL.md +1 -0
  46. package/template/skills/zh/{meta-meta/dashboard-reporting → dashboard-reporting}/SKILL.md +1 -0
  47. package/template/skills/zh/{meta/data-sensibility → data-sensibility}/SKILL.md +1 -0
  48. package/template/skills/{en/meta → zh}/document-chunking/SKILL.md +1 -0
  49. package/template/skills/zh/{meta/document-parsing → document-parsing}/SKILL.md +1 -0
  50. package/template/skills/{en/meta → zh}/entity-extraction/SKILL.md +1 -0
  51. package/template/skills/zh/{meta-meta/evolution-loop → evolution-loop}/SKILL.md +1 -0
  52. package/template/skills/zh/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/SKILL.md +1 -0
  53. package/template/skills/zh/{meta-meta/quality-control → quality-control}/SKILL.md +1 -0
  54. package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/SKILL.md +48 -0
  55. package/template/skills/zh/{meta-meta/rule-graph → rule-graph}/SKILL.md +1 -0
  56. package/template/skills/zh/{meta-meta/skill-authoring → skill-authoring}/SKILL.md +1 -0
  57. package/template/skills/zh/skill-creator/SKILL.md +2 -1
  58. package/template/skills/zh/skill-to-workflow/SKILL.md +190 -0
  59. package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/SKILL.md +1 -0
  60. package/template/skills/zh/{meta/tree-processing → tree-processing}/SKILL.md +1 -0
  61. package/template/skills/zh/{meta-meta/version-control → version-control}/SKILL.md +1 -0
  62. package/template/skills/zh/{meta-meta/work-decomposition → work-decomposition}/SKILL.md +37 -2
  63. package/template/CLAUDE.md +0 -137
  64. package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +0 -188
  65. /package/template/skills/en/{meta/compliance-judgment → compliance-judgment}/references/output-format.md +0 -0
  66. /package/template/skills/en/{meta/cross-document-verification → cross-document-verification}/references/contradiction-taxonomy.md +0 -0
  67. /package/template/skills/en/{meta-meta/dashboard-reporting → dashboard-reporting}/scripts/generate_dashboard.py +0 -0
  68. /package/template/skills/en/{meta/document-parsing → document-parsing}/references/parser-catalog.md +0 -0
  69. /package/template/skills/en/{meta-meta/evolution-loop → evolution-loop}/references/convergence-guide.md +0 -0
  70. /package/template/skills/en/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/scripts/generate_review.js +0 -0
  71. /package/template/skills/en/{meta-meta/quality-control → quality-control}/references/qa-layers.md +0 -0
  72. /package/template/skills/en/{meta-meta/quality-control → quality-control}/references/sampling-strategies.md +0 -0
  73. /package/template/skills/en/{meta-meta/rule-extraction → rule-extraction}/references/chunking-strategies.md +0 -0
  74. /package/template/skills/en/{meta-meta/skill-authoring → skill-authoring}/references/skill-format-spec.md +0 -0
  75. /package/template/skills/en/{meta-meta/skill-to-workflow → skill-to-workflow}/references/worker-llm-catalog.md +0 -0
  76. /package/template/skills/en/{meta-meta/task-decomposition → task-decomposition}/references/decision-matrix.md +0 -0
  77. /package/template/skills/en/{meta-meta/version-control → version-control}/references/trace-id-spec.md +0 -0
  78. /package/template/skills/zh/{meta/compliance-judgment → compliance-judgment}/references/output-format.md +0 -0
  79. /package/template/skills/zh/{meta/cross-document-verification → cross-document-verification}/references/contradiction-taxonomy.md +0 -0
  80. /package/template/skills/zh/{meta-meta/dashboard-reporting → dashboard-reporting}/scripts/generate_dashboard.py +0 -0
  81. /package/template/skills/zh/{meta/document-parsing → document-parsing}/references/parser-catalog.md +0 -0
  82. /package/template/skills/zh/{meta-meta/evolution-loop → evolution-loop}/references/convergence-guide.md +0 -0
  83. /package/template/skills/zh/{meta-meta/pdf-review-dashboard → pdf-review-dashboard}/scripts/generate_review.js +0 -0
  84. /package/template/skills/zh/{meta-meta/quality-control → quality-control}/references/qa-layers.md +0 -0
  85. /package/template/skills/zh/{meta-meta/quality-control → quality-control}/references/sampling-strategies.md +0 -0
  86. /package/template/skills/zh/{meta-meta/rule-extraction → rule-extraction}/references/chunking-strategies.md +0 -0
  87. /package/template/skills/zh/{meta-meta/skill-authoring → skill-authoring}/references/skill-format-spec.md +0 -0
  88. /package/template/skills/zh/{meta-meta/skill-to-workflow → skill-to-workflow}/references/worker-llm-catalog.md +0 -0
  89. /package/template/skills/zh/{meta-meta/task-decomposition → task-decomposition}/references/decision-matrix.md +0 -0
  90. /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
- const manifestAbs = this._workspace.resolvePath(MANIFEST_REL);
118
- fs.mkdirSync(path.dirname(manifestAbs), { recursive: true });
119
- let entries = [];
120
- if (fs.existsSync(manifestAbs)) {
121
- try { entries = JSON.parse(fs.readFileSync(manifestAbs, "utf-8")); }
122
- catch { entries = []; }
123
- }
124
- if (!Array.isArray(entries)) entries = [];
125
- entries.push(entry);
126
- fs.writeFileSync(manifestAbs, JSON.stringify(entries, null, 2), "utf-8");
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
- if (!fs.existsSync(calibSrc)) {
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
- fs.writeFileSync(resolved, content, "utf-8");
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
- // Auto-commit to git for workspace writes (silently no-ops if gitignored or git unavailable)
150
- let traceId = null;
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
- traceId = this._workspace.autoCommit(filePath, "update");
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
- vlmTier1: env.VLM_TIER1 || gc.vlm_tiers?.tier1 || "",
95
- vlmTier2: env.VLM_TIER2 || gc.vlm_tiers?.tier2 || "",
96
- vlmTier3: env.VLM_TIER3 || gc.vlm_tiers?.tier3 || "",
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 || "",