kc-beta 0.3.2 → 0.5.3

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/agent/confidence-scorer.js +8 -0
  3. package/src/agent/context.js +25 -0
  4. package/src/agent/corner-case-registry.js +5 -0
  5. package/src/agent/engine.js +514 -75
  6. package/src/agent/event-log.js +15 -2
  7. package/src/agent/history.js +91 -23
  8. package/src/agent/pipelines/initializer.js +3 -6
  9. package/src/agent/retry.js +9 -1
  10. package/src/agent/scheduler.js +276 -0
  11. package/src/agent/session-state.js +11 -2
  12. package/src/agent/task-manager.js +5 -0
  13. package/src/agent/tools/agent-tool.js +57 -14
  14. package/src/agent/tools/archive-file.js +94 -0
  15. package/src/agent/tools/copy-to-workspace.js +140 -0
  16. package/src/agent/tools/phase-advance.js +60 -0
  17. package/src/agent/tools/release.js +322 -0
  18. package/src/agent/tools/schedule-fetch.js +118 -0
  19. package/src/agent/tools/snapshot.js +101 -0
  20. package/src/agent/tools/workspace-file.js +10 -7
  21. package/src/agent/version-manager.js +29 -120
  22. package/src/agent/workspace.js +127 -4
  23. package/src/cli/components.js +4 -1
  24. package/src/cli/index.js +57 -4
  25. package/src/config.js +10 -1
  26. package/template/release-runtime/README.md.tmpl +84 -0
  27. package/template/release-runtime/kc_runtime/__init__.py +2 -0
  28. package/template/release-runtime/kc_runtime/confidence.py +93 -0
  29. package/template/release-runtime/kc_runtime/dashboard.py +208 -0
  30. package/template/release-runtime/render_dashboard.py +49 -0
  31. package/template/release-runtime/run.py +230 -0
  32. package/template/release-runtime/serve.sh +15 -0
  33. package/template/skills/en/meta-meta/bootstrap-workspace/SKILL.md +11 -0
  34. package/template/skills/en/meta-meta/quality-control/SKILL.md +13 -1
  35. package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +8 -0
  36. package/template/skills/en/meta-meta/task-decomposition/SKILL.md +13 -0
  37. package/template/skills/en/meta-meta/version-control/SKILL.md +13 -0
  38. package/template/skills/zh/meta-meta/bootstrap-workspace/SKILL.md +11 -0
  39. package/template/skills/zh/meta-meta/quality-control/SKILL.md +12 -0
  40. package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +8 -0
  41. package/template/skills/zh/meta-meta/task-decomposition/SKILL.md +16 -0
  42. package/template/skills/zh/meta-meta/version-control/SKILL.md +13 -0
  43. package/template/workspace.gitignore +22 -0
@@ -3,27 +3,48 @@ import path from "node:path";
3
3
  import crypto from "node:crypto";
4
4
  import { BaseTool, ToolResult } from "./base.js";
5
5
 
6
+ // Mirrors VALID_ID in scheduler.js — alphanumeric + _- only, max 64 chars.
7
+ // Sub-agent ids are used as path components under sub_agents/, so anything
8
+ // permitting `..` or `/` is a path-traversal risk.
9
+ const VALID_TASK_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
10
+ function _newAutoTaskId() {
11
+ return `task_${crypto.randomUUID().slice(0, 8)}`;
12
+ }
13
+
6
14
  /**
7
15
  * Spawn a sub-agent for parallel work.
8
- * Creates a child AgentEngine sharing the workspace filesystem
9
- * but with independent conversation history.
10
- * Results arrive via workspace files.
16
+ * Creates a child AgentEngine that shares workspace files (rules/, rule_skills/,
17
+ * workflows/, etc.) but isolates its own persistence under
18
+ * `sub_agents/<taskId>/` its own conversation history, event log, and
19
+ * session-state. Sub-agents inherit the parent's phase so they get the right
20
+ * tools registered. Results arrive via files written under sub_agents/<taskId>/.
11
21
  */
12
22
  export class AgentTool extends BaseTool {
13
- constructor(workspace, engineFactory) {
23
+ /**
24
+ * @param {import('../workspace.js').Workspace} workspace
25
+ * @param {(opts: {sessionId: string, subagentScope: string, initialPhase: string}) => import('../engine.js').AgentEngine} engineFactory
26
+ * @param {() => string} getCurrentPhase Callback returning the parent's current phase (so sub-agents get phase-appropriate tools)
27
+ */
28
+ constructor(workspace, engineFactory, getCurrentPhase = () => "bootstrap") {
14
29
  super();
15
30
  this._workspace = workspace;
16
31
  this._engineFactory = engineFactory;
32
+ this._getCurrentPhase = getCurrentPhase;
17
33
  this._runningTasks = new Map();
18
34
  }
19
35
 
20
36
  get name() { return "agent_tool"; }
21
37
  get description() {
22
38
  return (
23
- "Spawn a sub-agent for an independent task. Give it a complete, " +
24
- "self-contained task description. The sub-agent works in the same " +
25
- "workspace and writes results to files. Use this for parallel rule " +
26
- "processing, batch testing, or any work that can run independently."
39
+ "Spawn a sub-agent for an independent task. The sub-agent must own a " +
40
+ "non-overlapping unit of work — typically per-rule or per-document " +
41
+ "so multiple sub-agents don't have to coordinate through shared mutable " +
42
+ "files. Do NOT build a lock mechanism inside the sub-agent's task body; " +
43
+ "concurrent peers + locks bottleneck and fail. The sub-agent's own " +
44
+ "persistence (history, event log, session-state) lives under " +
45
+ "sub_agents/<taskId>/; workspace artifacts (rules/, skills/, workflows/) " +
46
+ "are shared. Give the sub-agent a complete, self-contained task " +
47
+ "description — it has no conversation context."
27
48
  );
28
49
  }
29
50
 
@@ -35,7 +56,10 @@ export class AgentTool extends BaseTool {
35
56
  type: "string",
36
57
  description: "Complete task description for the sub-agent. Be specific — it has no conversation context.",
37
58
  },
38
- task_id: { type: "string", description: "Optional task identifier (auto-generated if omitted)" },
59
+ task_id: {
60
+ type: "string",
61
+ description: "Optional task identifier — alphanumeric + _- only, max 64 chars. Used as a folder name under sub_agents/. If omitted or invalid, an auto-generated id is used.",
62
+ },
39
63
  },
40
64
  required: ["task_description"],
41
65
  };
@@ -43,19 +67,35 @@ export class AgentTool extends BaseTool {
43
67
 
44
68
  async execute(input) {
45
69
  const taskDesc = input.task_description || "";
46
- const taskId = input.task_id || `task_${crypto.randomUUID().slice(0, 8)}`;
70
+ const requestedId = (input.task_id || "").trim();
71
+ // Sanitize: anything not matching VALID_TASK_ID is silently replaced with
72
+ // an auto-generated id. The label survives in result metadata so KC can
73
+ // still cross-reference, but the path component is always safe.
74
+ const taskId = requestedId && VALID_TASK_ID.test(requestedId)
75
+ ? requestedId
76
+ : _newAutoTaskId();
77
+ const labelOverridden = requestedId && taskId !== requestedId;
47
78
 
48
79
  if (!taskDesc) return new ToolResult("No task_description provided", true);
49
80
 
50
- // Create sub-agent output directory
81
+ // Create sub-agent output directory (taskId is now sanitized)
51
82
  const taskDir = path.join(this._workspace.cwd, "sub_agents", taskId);
52
83
  fs.mkdirSync(taskDir, { recursive: true });
53
84
  fs.writeFileSync(path.join(taskDir, "task.md"), taskDesc, "utf-8");
85
+ if (labelOverridden) {
86
+ fs.writeFileSync(path.join(taskDir, "requested_id.txt"), requestedId, "utf-8");
87
+ }
54
88
 
55
- // Create child engine sharing the same workspace
89
+ // Create child engine. Critical: pass subagentScope + initialPhase so the
90
+ // child's persistence is isolated to sub_agents/<taskId>/ AND it has the
91
+ // same tools registered as the parent (Bug 2 fix).
56
92
  let childEngine;
57
93
  try {
58
- childEngine = this._engineFactory(this._workspace.sessionId);
94
+ childEngine = this._engineFactory({
95
+ sessionId: this._workspace.sessionId,
96
+ subagentScope: taskId,
97
+ initialPhase: this._getCurrentPhase(),
98
+ });
59
99
  } catch (e) {
60
100
  return new ToolResult(`Failed to create sub-agent: ${e.message}`, true);
61
101
  }
@@ -92,9 +132,12 @@ export class AgentTool extends BaseTool {
92
132
 
93
133
  return new ToolResult(JSON.stringify({
94
134
  task_id: taskId,
135
+ requested_id: labelOverridden ? requestedId : undefined,
95
136
  status: "started",
96
137
  output_dir: `sub_agents/${taskId}/`,
97
- message: `Sub-agent started. Check sub_agents/${taskId}/status.txt for completion, output.md for text.`,
138
+ message: labelOverridden
139
+ ? `Sub-agent started under sanitized id '${taskId}' (your '${requestedId}' wasn't a valid path component). Check sub_agents/${taskId}/status.txt.`
140
+ : `Sub-agent started. Check sub_agents/${taskId}/status.txt for completion, output.md for text.`,
98
141
  }, null, 2));
99
142
  }
100
143
  }
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { BaseTool, ToolResult } from "./base.js";
5
+
6
+ /**
7
+ * Move a workspace file into an archived/ subdirectory next to it.
8
+ * Use after a workflow consumes an input doc, or when an old result is no
9
+ * longer the primary view. If the file is git-tracked, uses `git mv` so
10
+ * history is preserved.
11
+ *
12
+ * Reverse moves (un-archive) are intentionally NOT exposed as a tool —
13
+ * KC can use sandbox_exec with `mv` for the rare reverse case.
14
+ */
15
+ export class ArchiveFileTool extends BaseTool {
16
+ constructor(workspace) {
17
+ super();
18
+ this._workspace = workspace;
19
+ }
20
+
21
+ get name() { return "archive_file"; }
22
+
23
+ get description() {
24
+ return (
25
+ "Move a workspace file to an archived/ subdirectory next to it. " +
26
+ "Use after a workflow consumes an input doc, or when an old result is no longer primary. " +
27
+ "Preserves git history if the file is tracked."
28
+ );
29
+ }
30
+
31
+ get inputSchema() {
32
+ return {
33
+ type: "object",
34
+ properties: {
35
+ path: {
36
+ type: "string",
37
+ description: "Workspace-relative path of the file to archive (e.g. 'input/doc.pdf').",
38
+ },
39
+ target_subdir: {
40
+ type: "string",
41
+ description: "Subdirectory name (default: 'archived'). Created next to the file's parent.",
42
+ },
43
+ },
44
+ required: ["path"],
45
+ };
46
+ }
47
+
48
+ async execute(input) {
49
+ const relPath = input.path || "";
50
+ const subdir = (input.target_subdir || "archived").replace(/[/\\]/g, "_");
51
+ if (!relPath) return new ToolResult("path required", true);
52
+
53
+ let resolved;
54
+ try { resolved = this._workspace.resolvePath(relPath); }
55
+ catch (e) { return new ToolResult(e.message, true); }
56
+
57
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
58
+ return new ToolResult(`File not found: ${relPath}`, true);
59
+ }
60
+
61
+ const parentRel = path.dirname(relPath);
62
+ const baseName = path.basename(relPath);
63
+ const targetRel = path.join(parentRel, subdir, baseName);
64
+ const targetAbs = this._workspace.resolvePath(targetRel);
65
+
66
+ if (fs.existsSync(targetAbs)) {
67
+ return new ToolResult(`Target already exists: ${targetRel}`, true);
68
+ }
69
+
70
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
71
+
72
+ // Try git mv first (preserves history). If it fails (file untracked or
73
+ // git unavailable), fall back to plain rename.
74
+ let usedGitMv = false;
75
+ if (this._workspace.gitAvailable) {
76
+ const r = spawnSync("git", ["mv", relPath, targetRel], {
77
+ cwd: this._workspace.cwd, stdio: "ignore",
78
+ });
79
+ usedGitMv = r.status === 0;
80
+ }
81
+ if (!usedGitMv) {
82
+ fs.renameSync(resolved, targetAbs);
83
+ }
84
+
85
+ // Auto-commit the move (no-op if both source and target are gitignored)
86
+ const traceId = this._workspace.autoCommit(targetRel, "archive");
87
+
88
+ return new ToolResult(
89
+ `Archived ${relPath} → ${targetRel}` +
90
+ (usedGitMv ? " (git history preserved)" : "") +
91
+ (traceId && this._workspace.gitAvailable ? ` [trace:${traceId}]` : "")
92
+ );
93
+ }
94
+ }
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { BaseTool, ToolResult } from "./base.js";
4
+
5
+ const MANIFEST_REL = "refs/manifest.json";
6
+ const GITIGNORE_REL = ".gitignore";
7
+
8
+ /**
9
+ * Copy a specific file from the user's project directory into the workspace
10
+ * (refs/) for KC to work on as a local copy. Default behavior remains:
11
+ * read project files in place via scope="project". Use this only when KC
12
+ * genuinely needs a working copy with provenance recorded.
13
+ *
14
+ * Files larger than `largeRefThresholdMB` (default 10 MB) are written but
15
+ * added to .gitignore so they don't bloat git history.
16
+ */
17
+ export class CopyToWorkspaceTool extends BaseTool {
18
+ /**
19
+ * @param {import('../workspace.js').Workspace} workspace
20
+ * @param {object} [opts]
21
+ * @param {number} [opts.largeRefThresholdMB=10]
22
+ */
23
+ constructor(workspace, { largeRefThresholdMB = 10 } = {}) {
24
+ super();
25
+ this._workspace = workspace;
26
+ this._largeMB = largeRefThresholdMB;
27
+ }
28
+
29
+ get name() { return "copy_to_workspace"; }
30
+
31
+ get description() {
32
+ return (
33
+ "Copy a file from the user's project directory into the workspace (refs/) " +
34
+ "as a local working copy with provenance recorded. " +
35
+ "Default behavior is to read project files in place via scope='project'; " +
36
+ "only use this tool when you genuinely need a workspace-local copy " +
37
+ "(e.g. to modify it, or to feed a workflow that requires the file inside the workspace). " +
38
+ "Files larger than the configured threshold are written but excluded from git history."
39
+ );
40
+ }
41
+
42
+ get inputSchema() {
43
+ return {
44
+ type: "object",
45
+ properties: {
46
+ source_path: {
47
+ type: "string",
48
+ description: "Relative path within the project directory (e.g. 'samples/foo.pdf').",
49
+ },
50
+ target_name: {
51
+ type: "string",
52
+ description: "Optional file name under refs/. Defaults to the source basename.",
53
+ },
54
+ reason: {
55
+ type: "string",
56
+ description: "Optional reason for the copy, recorded in refs/manifest.json for provenance.",
57
+ },
58
+ },
59
+ required: ["source_path"],
60
+ };
61
+ }
62
+
63
+ async execute(input) {
64
+ const sourcePath = input.source_path || "";
65
+ const reason = input.reason || "";
66
+ if (!sourcePath) return new ToolResult("source_path required", true);
67
+ if (!this._workspace.projectDir) {
68
+ return new ToolResult("No project directory available — KC was launched without a project context.", true);
69
+ }
70
+
71
+ let resolvedSource;
72
+ try {
73
+ resolvedSource = this._workspace.resolveProjectPath(sourcePath);
74
+ } catch (e) {
75
+ return new ToolResult(e.message, true);
76
+ }
77
+
78
+ if (!fs.existsSync(resolvedSource) || !fs.statSync(resolvedSource).isFile()) {
79
+ return new ToolResult(`Source file not found: ${sourcePath}`, true);
80
+ }
81
+
82
+ const targetName = (input.target_name || path.basename(resolvedSource)).replace(/[/\\]/g, "_");
83
+ const targetRel = path.join("refs", targetName);
84
+ const targetAbs = this._workspace.resolvePath(targetRel);
85
+
86
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
87
+ fs.copyFileSync(resolvedSource, targetAbs);
88
+
89
+ const stat = fs.statSync(targetAbs);
90
+ const sizeMB = stat.size / (1024 * 1024);
91
+ const isLarge = sizeMB > this._largeMB;
92
+
93
+ if (isLarge) {
94
+ this._appendGitignore(`refs/${targetName}`);
95
+ }
96
+
97
+ this._appendManifest({
98
+ target: targetRel,
99
+ source: sourcePath,
100
+ size: stat.size,
101
+ copied_at: new Date().toISOString(),
102
+ large_excluded_from_git: isLarge,
103
+ reason: reason || null,
104
+ });
105
+
106
+ // Auto-commit refs/manifest.json (and the file itself, if small enough to track)
107
+ const traceId = this._workspace.autoCommit(MANIFEST_REL, "manifest");
108
+ if (!isLarge) this._workspace.autoCommit(targetRel, "copy");
109
+
110
+ return new ToolResult(
111
+ `Copied ${sourcePath} → ${targetRel} (${stat.size} bytes${isLarge ? ", excluded from git (large)" : ""}). ` +
112
+ `Provenance recorded${traceId ? ` [trace:${traceId}]` : ""}.`
113
+ );
114
+ }
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");
127
+ }
128
+
129
+ _appendGitignore(line) {
130
+ const giPath = this._workspace.resolvePath(GITIGNORE_REL);
131
+ let body = "";
132
+ if (fs.existsSync(giPath)) body = fs.readFileSync(giPath, "utf-8");
133
+ const lines = body.split("\n").map((l) => l.trim());
134
+ if (lines.includes(line.trim())) return;
135
+ if (body.length > 0 && !body.endsWith("\n")) body += "\n";
136
+ body += line + "\n";
137
+ fs.writeFileSync(giPath, body, "utf-8");
138
+ this._workspace.autoCommit(GITIGNORE_REL, "gitignore");
139
+ }
140
+ }
@@ -0,0 +1,60 @@
1
+ import { BaseTool, ToolResult } from "./base.js";
2
+ import { Phase } from "../pipelines/index.js";
3
+
4
+ const VALID_PHASES = new Set(Object.values(Phase));
5
+
6
+ /**
7
+ * Advance the current phase to a target phase. Used when the user instructs
8
+ * KC to skip ahead, or when KC judges criteria are met but auto-detect
9
+ * doesn't see them. Most transitions happen automatically (exit criteria,
10
+ * task completion); this tool is the explicit-user-request path.
11
+ *
12
+ * Linear order is enforced by default — only forward-by-one is allowed.
13
+ * Pass force=true to skip phases or regress (e.g., when the user explicitly
14
+ * asks). Description kept short to minimize system-prompt budget cost.
15
+ */
16
+ export class PhaseAdvanceTool extends BaseTool {
17
+ constructor(advanceFn) {
18
+ super();
19
+ this._advance = advanceFn;
20
+ }
21
+
22
+ get name() { return "phase_advance"; }
23
+
24
+ get description() {
25
+ return "Advance phase. Forward-by-one only unless force=true (use sparingly, e.g. when user asks to skip).";
26
+ }
27
+
28
+ get inputSchema() {
29
+ return {
30
+ type: "object",
31
+ properties: {
32
+ to: {
33
+ type: "string",
34
+ enum: Array.from(VALID_PHASES),
35
+ description: "Target phase",
36
+ },
37
+ reason: { type: "string", description: "Why" },
38
+ force: {
39
+ type: "boolean",
40
+ description: "Allow non-adjacent or backward transitions. Default false.",
41
+ },
42
+ },
43
+ required: ["to"],
44
+ };
45
+ }
46
+
47
+ async execute(input) {
48
+ const to = input.to;
49
+ if (!VALID_PHASES.has(to)) return new ToolResult(`Unknown phase: ${to}`, true);
50
+ const advanced = this._advance(to, input.reason || "agent request", { force: !!input.force });
51
+ if (!advanced) {
52
+ // Either already in target phase, or non-adjacent without force
53
+ return new ToolResult(
54
+ `Did not advance to ${to}. Either you're already there, or the transition is non-adjacent (set force:true to override).`,
55
+ false,
56
+ );
57
+ }
58
+ return new ToolResult(`Advanced to ${to}${input.force ? " (forced)" : ""}`);
59
+ }
60
+ }