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.
- package/package.json +1 -1
- package/src/agent/confidence-scorer.js +8 -0
- package/src/agent/context.js +25 -0
- package/src/agent/corner-case-registry.js +5 -0
- package/src/agent/engine.js +514 -75
- package/src/agent/event-log.js +15 -2
- package/src/agent/history.js +91 -23
- package/src/agent/pipelines/initializer.js +3 -6
- package/src/agent/retry.js +9 -1
- package/src/agent/scheduler.js +276 -0
- package/src/agent/session-state.js +11 -2
- package/src/agent/task-manager.js +5 -0
- package/src/agent/tools/agent-tool.js +57 -14
- package/src/agent/tools/archive-file.js +94 -0
- package/src/agent/tools/copy-to-workspace.js +140 -0
- package/src/agent/tools/phase-advance.js +60 -0
- package/src/agent/tools/release.js +322 -0
- package/src/agent/tools/schedule-fetch.js +118 -0
- package/src/agent/tools/snapshot.js +101 -0
- package/src/agent/tools/workspace-file.js +10 -7
- package/src/agent/version-manager.js +29 -120
- package/src/agent/workspace.js +127 -4
- package/src/cli/components.js +4 -1
- package/src/cli/index.js +57 -4
- package/src/config.js +10 -1
- package/template/release-runtime/README.md.tmpl +84 -0
- package/template/release-runtime/kc_runtime/__init__.py +2 -0
- package/template/release-runtime/kc_runtime/confidence.py +93 -0
- package/template/release-runtime/kc_runtime/dashboard.py +208 -0
- package/template/release-runtime/render_dashboard.py +49 -0
- package/template/release-runtime/run.py +230 -0
- package/template/release-runtime/serve.sh +15 -0
- package/template/skills/en/meta-meta/bootstrap-workspace/SKILL.md +11 -0
- package/template/skills/en/meta-meta/quality-control/SKILL.md +13 -1
- package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +8 -0
- package/template/skills/en/meta-meta/task-decomposition/SKILL.md +13 -0
- package/template/skills/en/meta-meta/version-control/SKILL.md +13 -0
- package/template/skills/zh/meta-meta/bootstrap-workspace/SKILL.md +11 -0
- package/template/skills/zh/meta-meta/quality-control/SKILL.md +12 -0
- package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +8 -0
- package/template/skills/zh/meta-meta/task-decomposition/SKILL.md +16 -0
- package/template/skills/zh/meta-meta/version-control/SKILL.md +13 -0
- package/template/workspace.gitignore +22 -0
|
@@ -1,130 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Trace ID utility.
|
|
3
|
+
*
|
|
4
|
+
* As of v0.4.0 (Block 11), real version history is kept by git
|
|
5
|
+
* (per-session repo, auto-committed by Workspace.autoCommit). This module
|
|
6
|
+
* is now just a stable place for trace ID generation, used by tools that
|
|
7
|
+
* need to cross-reference a write or a result with the event log.
|
|
8
|
+
*
|
|
9
|
+
* The legacy versions.json manifest in pre-v0.4.0 workspaces is left
|
|
10
|
+
* untouched — nothing reads it any more, but old data is preserved.
|
|
11
|
+
*/
|
|
3
12
|
|
|
4
|
-
|
|
5
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Generate a trace ID like "20260417_114203_R001_workflow_result".
|
|
15
|
+
* @param {string} ruleId
|
|
16
|
+
* @param {string} [label]
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function generateTraceId(ruleId, label = "") {
|
|
20
|
+
const now = new Date().toISOString().replace(/[-:T]/g, (m) =>
|
|
21
|
+
m === "T" ? "_" : ""
|
|
22
|
+
).slice(0, 15);
|
|
23
|
+
const suffix = label ? `_${label}` : "";
|
|
24
|
+
return `${now}_${ruleId}${suffix}`;
|
|
25
|
+
}
|
|
6
26
|
|
|
7
27
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - Manifest at versions.json: tracks lineage, timestamps, change reasons
|
|
11
|
-
* - Trace ID generation: {timestamp}_{rule_id}_{version}
|
|
12
|
-
* Cannot be bypassed — hooks into WorkspaceFileTool.
|
|
28
|
+
* Back-compat shell. The class is retained so existing constructors
|
|
29
|
+
* that take a VersionManager don't break, but it carries no state.
|
|
13
30
|
*/
|
|
14
31
|
export class VersionManager {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
constructor(workspacePath) {
|
|
19
|
-
this._workspace = workspacePath;
|
|
20
|
-
this._manifestPath = path.join(workspacePath, "versions.json");
|
|
21
|
-
this._manifest = this._loadManifest();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
_loadManifest() {
|
|
25
|
-
if (fs.existsSync(this._manifestPath)) {
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(fs.readFileSync(this._manifestPath, "utf-8"));
|
|
28
|
-
} catch {
|
|
29
|
-
// fall through
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return { version: "0.1.0", entries: [] };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
_saveManifest() {
|
|
36
|
-
fs.writeFileSync(
|
|
37
|
-
this._manifestPath,
|
|
38
|
-
JSON.stringify(this._manifest, null, 2),
|
|
39
|
-
"utf-8",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Check if a path falls within versioned directories.
|
|
45
|
-
* @param {string} relPath
|
|
46
|
-
* @returns {boolean}
|
|
47
|
-
*/
|
|
48
|
-
shouldVersion(relPath) {
|
|
49
|
-
const parts = relPath.split(path.sep);
|
|
50
|
-
if (parts.length === 0) return false;
|
|
51
|
-
const topDir = parts[0];
|
|
52
|
-
const ext = path.extname(relPath);
|
|
53
|
-
return VERSIONED_DIRS.has(topDir) && VERSIONED_EXTS.has(ext);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Called on file write. Returns trace ID if versioned, null otherwise.
|
|
58
|
-
* @param {string} relPath
|
|
59
|
-
* @param {string} content
|
|
60
|
-
* @returns {string|null}
|
|
61
|
-
*/
|
|
62
|
-
onWrite(relPath, content) {
|
|
63
|
-
if (!this.shouldVersion(relPath)) return null;
|
|
64
|
-
|
|
65
|
-
const version = this._nextVersion(relPath);
|
|
66
|
-
const now = new Date().toISOString().replace(/[-:T]/g, (m) =>
|
|
67
|
-
m === "T" ? "_" : ""
|
|
68
|
-
).slice(0, 15);
|
|
69
|
-
|
|
70
|
-
// Extract rule_id from path if possible (e.g., rule_skills/R001/SKILL.md → R001)
|
|
71
|
-
const parts = relPath.split(path.sep);
|
|
72
|
-
const ruleId = parts.length > 1 ? parts[1] : "global";
|
|
73
|
-
|
|
74
|
-
const traceId = `${now}_${ruleId}_v${version}`;
|
|
75
|
-
|
|
76
|
-
const entry = {
|
|
77
|
-
file: relPath,
|
|
78
|
-
version,
|
|
79
|
-
trace_id: traceId,
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
rule_id: ruleId,
|
|
82
|
-
size_chars: content.length,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
this._manifest.entries.push(entry);
|
|
86
|
-
this._saveManifest();
|
|
87
|
-
|
|
88
|
-
return traceId;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
_nextVersion(relPath) {
|
|
92
|
-
const existing = this._manifest.entries
|
|
93
|
-
.filter((e) => e.file === relPath)
|
|
94
|
-
.map((e) => e.version);
|
|
95
|
-
return Math.max(0, ...existing) + 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get all version entries for a file.
|
|
100
|
-
* @param {string} relPath
|
|
101
|
-
* @returns {Array<object>}
|
|
102
|
-
*/
|
|
103
|
-
getVersions(relPath) {
|
|
104
|
-
return this._manifest.entries.filter((e) => e.file === relPath);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Get the most recent version entry for a file.
|
|
109
|
-
* @param {string} relPath
|
|
110
|
-
* @returns {object|null}
|
|
111
|
-
*/
|
|
112
|
-
latestVersion(relPath) {
|
|
113
|
-
const versions = this.getVersions(relPath);
|
|
114
|
-
return versions.length > 0 ? versions[versions.length - 1] : null;
|
|
32
|
+
constructor(_workspacePath) {
|
|
33
|
+
// No-op. Workspace path is no longer needed.
|
|
115
34
|
}
|
|
116
35
|
|
|
117
|
-
/**
|
|
118
|
-
* Generate a standalone trace ID for results, QC records, etc.
|
|
119
|
-
* @param {string} ruleId
|
|
120
|
-
* @param {string} [label]
|
|
121
|
-
* @returns {string}
|
|
122
|
-
*/
|
|
123
36
|
generateTraceId(ruleId, label = "") {
|
|
124
|
-
|
|
125
|
-
m === "T" ? "_" : ""
|
|
126
|
-
).slice(0, 15);
|
|
127
|
-
const suffix = label ? `_${label}` : "";
|
|
128
|
-
return `${now}_${ruleId}${suffix}`;
|
|
37
|
+
return generateTraceId(ruleId, label);
|
|
129
38
|
}
|
|
130
39
|
}
|
package/src/agent/workspace.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { generateTraceId } from "./version-manager.js";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const GITIGNORE_TEMPLATE = path.resolve(__dirname, "../../template/workspace.gitignore");
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Per-session workspace directory with path traversal protection.
|
|
7
13
|
* Each agent session gets its own directory under the workspace root.
|
|
8
14
|
* All file operations by tools must go through resolvePath().
|
|
15
|
+
*
|
|
16
|
+
* As of v0.4.0 (Block 11): the workspace is also a git repo. Writes to
|
|
17
|
+
* non-gitignored paths are auto-committed via autoCommit() so KC has a
|
|
18
|
+
* real version history of its outputs.
|
|
9
19
|
*/
|
|
10
20
|
export class Workspace {
|
|
11
21
|
/**
|
|
12
22
|
* @param {string} root - Workspace root directory
|
|
13
23
|
* @param {string} [sessionId] - Session identifier (auto-generated if omitted)
|
|
14
24
|
* @param {string} [projectDir] - User's project directory (CWD at launch)
|
|
25
|
+
* @param {object} [opts]
|
|
26
|
+
* @param {boolean} [opts.gitAutoCommit=true] - If false, skip git init / auto-commit
|
|
15
27
|
*/
|
|
16
|
-
constructor(root, sessionId, projectDir) {
|
|
28
|
+
constructor(root, sessionId, projectDir, opts = {}) {
|
|
17
29
|
this.root = path.resolve(root);
|
|
18
30
|
this.sessionId = sessionId || crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
19
31
|
this.path = path.resolve(this.root, this.sessionId);
|
|
20
32
|
this.projectDir = projectDir ? path.resolve(projectDir) : null;
|
|
33
|
+
this._currentPhase = "bootstrap";
|
|
21
34
|
fs.mkdirSync(this.path, { recursive: true });
|
|
35
|
+
|
|
36
|
+
this._gitAutoCommitEnabled = opts.gitAutoCommit !== false;
|
|
37
|
+
this._gitAvailable = this._gitAutoCommitEnabled && Workspace.isGitInstalled();
|
|
38
|
+
if (this._gitAvailable) this._initGitRepo();
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
/** @returns {string} Current workspace directory */
|
|
@@ -26,6 +43,16 @@ export class Workspace {
|
|
|
26
43
|
return this.path;
|
|
27
44
|
}
|
|
28
45
|
|
|
46
|
+
/** @returns {boolean} Whether auto-commit is wired up for this session */
|
|
47
|
+
get gitAvailable() {
|
|
48
|
+
return this._gitAvailable;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Update the current phase (used in auto-commit messages). */
|
|
52
|
+
setPhase(phase) {
|
|
53
|
+
if (phase) this._currentPhase = phase;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* Resolve a user-supplied relative path against the workspace.
|
|
31
58
|
* Rejects absolute paths and any path that escapes the workspace via .. or symlinks.
|
|
@@ -66,23 +93,62 @@ export class Workspace {
|
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
/**
|
|
69
|
-
*
|
|
96
|
+
* Auto-commit a workspace write. Silently no-ops if the path is gitignored,
|
|
97
|
+
* if there's nothing to commit, or if git isn't available. Returns the trace
|
|
98
|
+
* ID generated for this write (always returned, even if no commit happened,
|
|
99
|
+
* so callers can cross-reference with the event log).
|
|
100
|
+
*
|
|
101
|
+
* @param {string} relPath - workspace-relative path that was just written
|
|
102
|
+
* @param {string} [opLabel="update"] - short verb for the commit message
|
|
103
|
+
* @returns {string} trace id
|
|
104
|
+
*/
|
|
105
|
+
autoCommit(relPath, opLabel = "update") {
|
|
106
|
+
const ruleId = this._extractRuleId(relPath);
|
|
107
|
+
const traceId = generateTraceId(ruleId, opLabel);
|
|
108
|
+
|
|
109
|
+
if (!this._gitAvailable) return traceId;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const r = spawnSync("git", ["add", "--", relPath], {
|
|
113
|
+
cwd: this.path,
|
|
114
|
+
stdio: "ignore",
|
|
115
|
+
});
|
|
116
|
+
if (r.status !== 0) return traceId; // gitignored or other add error — skip commit
|
|
117
|
+
const msg = `[${this._currentPhase}] ${opLabel} ${relPath} [trace:${traceId}]`;
|
|
118
|
+
spawnSync("git", ["commit", "-m", msg, "--allow-empty-message"], {
|
|
119
|
+
cwd: this.path,
|
|
120
|
+
stdio: "ignore",
|
|
121
|
+
});
|
|
122
|
+
// Status doesn't matter — "nothing to commit" is fine.
|
|
123
|
+
} catch {
|
|
124
|
+
// Never let a git failure break a workspace write.
|
|
125
|
+
}
|
|
126
|
+
return traceId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Rename the workspace folder. Returns `{ sessionId, oldCwd, newCwd }` so
|
|
131
|
+
* callers can cascade the new path to subsystems that captured cwd at
|
|
132
|
+
* construction (Bug 3).
|
|
70
133
|
* @param {string} newName
|
|
71
|
-
* @returns {string}
|
|
134
|
+
* @returns {{sessionId: string, oldCwd: string, newCwd: string, changed: boolean}}
|
|
72
135
|
*/
|
|
73
136
|
rename(newName) {
|
|
74
137
|
newName = newName.trim().replace(/ /g, "_").replace(/\//g, "_");
|
|
75
138
|
if (!newName) throw new Error("Name cannot be empty");
|
|
76
139
|
const newPath = path.join(this.root, newName);
|
|
140
|
+
const oldCwd = this.path;
|
|
77
141
|
if (fs.existsSync(newPath) && path.resolve(newPath) !== path.resolve(this.path)) {
|
|
78
142
|
throw new Error(`Session '${newName}' already exists`);
|
|
79
143
|
}
|
|
144
|
+
let changed = false;
|
|
80
145
|
if (path.resolve(newPath) !== path.resolve(this.path)) {
|
|
81
146
|
fs.renameSync(this.path, newPath);
|
|
82
147
|
this.path = path.resolve(newPath);
|
|
83
148
|
this.sessionId = newName;
|
|
149
|
+
changed = true;
|
|
84
150
|
}
|
|
85
|
-
return this.sessionId;
|
|
151
|
+
return { sessionId: this.sessionId, oldCwd, newCwd: this.path, changed };
|
|
86
152
|
}
|
|
87
153
|
|
|
88
154
|
/**
|
|
@@ -102,4 +168,61 @@ export class Workspace {
|
|
|
102
168
|
}
|
|
103
169
|
return sessions;
|
|
104
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Probe whether the `git` executable is on PATH. Cached per process. */
|
|
173
|
+
static isGitInstalled() {
|
|
174
|
+
if (Workspace._gitProbeCache !== undefined) return Workspace._gitProbeCache;
|
|
175
|
+
try {
|
|
176
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
177
|
+
Workspace._gitProbeCache = true;
|
|
178
|
+
} catch {
|
|
179
|
+
Workspace._gitProbeCache = false;
|
|
180
|
+
}
|
|
181
|
+
return Workspace._gitProbeCache;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- private helpers ---
|
|
185
|
+
|
|
186
|
+
_initGitRepo() {
|
|
187
|
+
const gitDir = path.join(this.path, ".git");
|
|
188
|
+
const gitignorePath = path.join(this.path, ".gitignore");
|
|
189
|
+
const isFresh = !fs.existsSync(gitDir);
|
|
190
|
+
|
|
191
|
+
if (isFresh) {
|
|
192
|
+
try {
|
|
193
|
+
spawnSync("git", ["init", "--initial-branch=main"], { cwd: this.path, stdio: "ignore" });
|
|
194
|
+
// --initial-branch isn't supported on older git; fall back silently
|
|
195
|
+
if (!fs.existsSync(gitDir)) {
|
|
196
|
+
spawnSync("git", ["init"], { cwd: this.path, stdio: "ignore" });
|
|
197
|
+
}
|
|
198
|
+
// Local identity so commits don't depend on user's global config
|
|
199
|
+
spawnSync("git", ["config", "user.name", "kc-agent"], { cwd: this.path, stdio: "ignore" });
|
|
200
|
+
spawnSync("git", ["config", "user.email", "agent@kc.local"], { cwd: this.path, stdio: "ignore" });
|
|
201
|
+
} catch {
|
|
202
|
+
this._gitAvailable = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Always ensure .gitignore is present (template may have evolved)
|
|
208
|
+
if (!fs.existsSync(gitignorePath) && fs.existsSync(GITIGNORE_TEMPLATE)) {
|
|
209
|
+
fs.copyFileSync(GITIGNORE_TEMPLATE, gitignorePath);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isFresh) {
|
|
213
|
+
// Initial commit — captures whatever's already in the dir (for migrated workspaces)
|
|
214
|
+
spawnSync("git", ["add", "-A"], { cwd: this.path, stdio: "ignore" });
|
|
215
|
+
const msg = fs.existsSync(path.join(this.path, "AGENT.md"))
|
|
216
|
+
? `Migrated session ${this.sessionId} to git-tracked workspace`
|
|
217
|
+
: `Initialized session ${this.sessionId}`;
|
|
218
|
+
spawnSync("git", ["commit", "--allow-empty", "-m", msg], { cwd: this.path, stdio: "ignore" });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Extract rule ID from a path like rule_skills/R001/SKILL.md → "R001". */
|
|
223
|
+
_extractRuleId(relPath) {
|
|
224
|
+
const parts = relPath.split(path.sep);
|
|
225
|
+
if (parts.length >= 2 && /^R\d+/i.test(parts[1])) return parts[1];
|
|
226
|
+
return "global";
|
|
227
|
+
}
|
|
105
228
|
}
|
package/src/cli/components.js
CHANGED
|
@@ -88,7 +88,7 @@ export function TaskDashboard({ tasks, progress }) {
|
|
|
88
88
|
|
|
89
89
|
// --- Welcome banner ---
|
|
90
90
|
|
|
91
|
-
export function WelcomeBanner({ projectDir } = {}) {
|
|
91
|
+
export function WelcomeBanner({ projectDir, pendingInputCount = 0 } = {}) {
|
|
92
92
|
return h(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1 },
|
|
93
93
|
h(Box, null,
|
|
94
94
|
h(Text, { bold: true }, "KC AGENT CLI"),
|
|
@@ -102,6 +102,9 @@ export function WelcomeBanner({ projectDir } = {}) {
|
|
|
102
102
|
h(Text, { color: "yellow", dimColor: true }, "KC has full read/write access to this directory. We recommend backing up important files."),
|
|
103
103
|
)
|
|
104
104
|
: null,
|
|
105
|
+
pendingInputCount > 0
|
|
106
|
+
? h(Text, { color: "cyan" }, `📥 ${pendingInputCount} new file(s) pending in input/ — run /schedule for details`)
|
|
107
|
+
: null,
|
|
105
108
|
h(Text, null, ""),
|
|
106
109
|
h(Text, { dimColor: true }, "Product of Memium / kitchen-engineer42"),
|
|
107
110
|
);
|
package/src/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { LLMClient } from "../agent/llm-client.js";
|
|
|
5
5
|
import { AgentEngine } from "../agent/engine.js";
|
|
6
6
|
import { Workspace } from "../agent/workspace.js";
|
|
7
7
|
import { ConversationHistory } from "../agent/history.js";
|
|
8
|
+
import { Scheduler } from "../agent/scheduler.js";
|
|
8
9
|
import {
|
|
9
10
|
WelcomeBanner,
|
|
10
11
|
StatusBar,
|
|
@@ -158,6 +159,7 @@ function App({ engine, config }) {
|
|
|
158
159
|
" /help Show this help\n" +
|
|
159
160
|
" /status Show session info, model, phase, workspace\n" +
|
|
160
161
|
" /tasks Show task progress\n" +
|
|
162
|
+
" /schedule Show scheduled ingestion jobs and recent log lines\n" +
|
|
161
163
|
" /clear Clear conversation history (keep workspace)\n" +
|
|
162
164
|
" /compact Summarize older messages to reduce context\n" +
|
|
163
165
|
" /sessions List all sessions\n" +
|
|
@@ -193,6 +195,30 @@ function App({ engine, config }) {
|
|
|
193
195
|
});
|
|
194
196
|
return true;
|
|
195
197
|
|
|
198
|
+
case "/schedule": {
|
|
199
|
+
const sched = new Scheduler(engineRef.current.workspace);
|
|
200
|
+
const jobs = sched.list();
|
|
201
|
+
if (jobs.length === 0) {
|
|
202
|
+
addMessage({ role: "system", content: "No scheduled ingestion jobs. Ask KC to set one up via the schedule_fetch tool." });
|
|
203
|
+
} else {
|
|
204
|
+
const lines = jobs.map((j) => {
|
|
205
|
+
const status = j.enabled ? "✓ enabled" : "· disabled";
|
|
206
|
+
const hint = j.cron_hint ? ` cron: ${j.cron_hint}` : " cron: (not set)";
|
|
207
|
+
return ` ${status} ${j.id}\n${hint}\n cmd: ${j.command}`;
|
|
208
|
+
});
|
|
209
|
+
const tail = sched.tailLog(8);
|
|
210
|
+
const pending = sched.pendingInputCount();
|
|
211
|
+
addMessage({
|
|
212
|
+
role: "system",
|
|
213
|
+
content:
|
|
214
|
+
`Scheduled jobs:\n${lines.join("\n\n")}\n\n` +
|
|
215
|
+
`Pending in input/: ${pending} file(s)` +
|
|
216
|
+
(tail ? `\n\nlogs/ingest.log (last 8):\n${tail}` : ""),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
196
222
|
case "/clear":
|
|
197
223
|
engineRef.current.history = new ConversationHistory(engineRef.current.workspace.cwd);
|
|
198
224
|
setMessages([]);
|
|
@@ -227,9 +253,22 @@ function App({ engine, config }) {
|
|
|
227
253
|
addMessage({ role: "system", content: "Usage: /rename <new_name>" });
|
|
228
254
|
} else {
|
|
229
255
|
try {
|
|
230
|
-
const
|
|
231
|
-
setSessionId(
|
|
232
|
-
|
|
256
|
+
const r = engineRef.current.renameSession(arg);
|
|
257
|
+
setSessionId(r.sessionId);
|
|
258
|
+
const lines = [`Session renamed to: ${r.sessionId}`];
|
|
259
|
+
if (r.scheduleWrappersRegenerated.length > 0) {
|
|
260
|
+
lines.push(
|
|
261
|
+
`${r.scheduleWrappersRegenerated.length} cron wrapper script(s) regenerated.`,
|
|
262
|
+
`If you'd installed crontab lines for the OLD path, re-install via 'schedule_fetch print_crontab'.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (r.scheduleWrappersFailed && r.scheduleWrappersFailed.length > 0) {
|
|
266
|
+
const ids = r.scheduleWrappersFailed.map((f) => f.id).join(", ");
|
|
267
|
+
lines.push(
|
|
268
|
+
`⚠ ${r.scheduleWrappersFailed.length} wrapper script(s) failed to regenerate (${ids}). Check workspace/scripts/ingest/ and disk space.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
addMessage({ role: "system", content: lines.join("\n") });
|
|
233
272
|
} catch (err) {
|
|
234
273
|
addMessage({ role: "system", content: `Rename failed: ${err.message}` });
|
|
235
274
|
}
|
|
@@ -342,7 +381,13 @@ function App({ engine, config }) {
|
|
|
342
381
|
|
|
343
382
|
return h(Box, { flexDirection: "column" },
|
|
344
383
|
// Welcome banner
|
|
345
|
-
showWelcome ? h(WelcomeBanner, {
|
|
384
|
+
showWelcome ? h(WelcomeBanner, {
|
|
385
|
+
projectDir: config.projectDir,
|
|
386
|
+
pendingInputCount: (() => {
|
|
387
|
+
try { return new Scheduler(engineRef.current.workspace).pendingInputCount(); }
|
|
388
|
+
catch { return 0; }
|
|
389
|
+
})(),
|
|
390
|
+
}) : null,
|
|
346
391
|
|
|
347
392
|
// Task dashboard (ralph-loop)
|
|
348
393
|
taskList.length > 0 ? h(TaskDashboard, { tasks: taskList, progress: taskProgress }) : null,
|
|
@@ -436,6 +481,14 @@ export async function main({ languageOverride } = {}) {
|
|
|
436
481
|
console.log(`\x1b[33m${msg}\x1b[0m\n`);
|
|
437
482
|
}
|
|
438
483
|
|
|
484
|
+
// Warn if git is missing — Block 11 file system relies on git for version history.
|
|
485
|
+
if (config.gitAutoCommit !== false && !Workspace.isGitInstalled()) {
|
|
486
|
+
const msg = config.language === "zh"
|
|
487
|
+
? " ⚠ 未检测到 git。本会话将不记录版本历史。安装 git 以启用自动提交。"
|
|
488
|
+
: " ⚠ git not found — version history disabled this session. Install git to enable auto-commit.";
|
|
489
|
+
console.log(`\x1b[33m${msg}\x1b[0m\n`);
|
|
490
|
+
}
|
|
491
|
+
|
|
439
492
|
const client = new LLMClient({
|
|
440
493
|
apiKey: config.llmApiKey,
|
|
441
494
|
baseUrl: config.llmBaseUrl,
|
package/src/config.js
CHANGED
|
@@ -65,7 +65,7 @@ export function loadSettings(workspacePath) {
|
|
|
65
65
|
llmApiKey: env.LLM_API_KEY || env.SILICONFLOW_API_KEY || gc.api_key || "",
|
|
66
66
|
llmBaseUrl: env.LLM_BASE_URL || env.SILICONFLOW_BASE_URL || gc.base_url || "https://api.siliconflow.cn/v1",
|
|
67
67
|
kcModel: gc.conductor_model || "glm-5",
|
|
68
|
-
kcMaxTokens: 65536,
|
|
68
|
+
kcMaxTokens: parseInt(env.KC_MAX_TOKENS || gc.kc_max_tokens?.toString() || "65536", 10),
|
|
69
69
|
|
|
70
70
|
// Tier models (from .env or global config tiers)
|
|
71
71
|
tier1: env.TIER1 || gc.tiers?.tier1 || "",
|
|
@@ -111,6 +111,15 @@ export function loadSettings(workspacePath) {
|
|
|
111
111
|
|
|
112
112
|
// Context management
|
|
113
113
|
kcContextLimit: parseInt(env.KC_CONTEXT_LIMIT || "200000", 10),
|
|
114
|
+
toolOutputOffloadTokens: parseInt(env.TOOL_OUTPUT_OFFLOAD_TOKENS || gc.tool_output_offload_tokens?.toString() || "2000", 10),
|
|
115
|
+
toolOutputOffloadErrorTokens: parseInt(env.TOOL_OUTPUT_OFFLOAD_ERROR_TOKENS || gc.tool_output_offload_error_tokens?.toString() || "500", 10),
|
|
116
|
+
maxMessageTokens: parseInt(env.MAX_MESSAGE_TOKENS || gc.max_message_tokens?.toString() || "60000", 10),
|
|
117
|
+
|
|
118
|
+
// File system (Block 11)
|
|
119
|
+
gitAutoCommit: (env.GIT_AUTO_COMMIT ?? gc.git_auto_commit ?? true) !== false &&
|
|
120
|
+
(env.GIT_AUTO_COMMIT !== "false") &&
|
|
121
|
+
(gc.git_auto_commit !== false),
|
|
122
|
+
largeRefThresholdMB: parseInt(env.LARGE_REF_THRESHOLD_MB || gc.large_ref_threshold_mb?.toString() || "10", 10),
|
|
114
123
|
|
|
115
124
|
// Language
|
|
116
125
|
language: env.LANGUAGE || gc.language || "en",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# {LABEL} — KC Verification Release
|
|
2
|
+
|
|
3
|
+
Generated: {CREATED_AT}
|
|
4
|
+
Snapshot tag: `{SNAPSHOT_TAG}`
|
|
5
|
+
Commit: `{SNAPSHOT_COMMIT}`
|
|
6
|
+
Built by: kc-beta {KC_VERSION}
|
|
7
|
+
|
|
8
|
+
{NOTES_BLOCK}
|
|
9
|
+
|
|
10
|
+
This bundle is self-contained. It runs without `kc-beta` installed — only Python 3 and a worker LLM API key are required.
|
|
11
|
+
|
|
12
|
+
## What's in here
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
manifest.json — release metadata (rules, models, snapshot tag)
|
|
16
|
+
README.md — this file
|
|
17
|
+
run.py — standalone driver, runs all rules
|
|
18
|
+
render_dashboard.py — re-render an HTML dashboard from a result JSON
|
|
19
|
+
serve.sh — optional helper, serves this dir over local HTTP
|
|
20
|
+
kc_runtime/ — bundled Python helpers (confidence scoring, dashboard)
|
|
21
|
+
workflows/ — pinned per-rule Python workflows + prompts
|
|
22
|
+
fixtures/ — sample inputs (if KC selected any)
|
|
23
|
+
glossary.json — project entity vocabulary at release time
|
|
24
|
+
catalog.json — rule catalog at release time
|
|
25
|
+
corner_cases.json — known corner cases (used by confidence scoring)
|
|
26
|
+
confidence_calibration.json — per-rule historical accuracy
|
|
27
|
+
models.json — worker LLM tier→model assignments
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Run a verification
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
export LLM_API_KEY="sk-..."
|
|
34
|
+
export LLM_BASE_URL="https://api.siliconflow.cn/v1" # or your provider
|
|
35
|
+
export TIER1="..." # comma-separated model list
|
|
36
|
+
export TIER2="..."
|
|
37
|
+
|
|
38
|
+
python run.py /path/to/document.pdf > result.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Each rule's workflow runs against the document; results are aggregated into a single JSON.
|
|
42
|
+
|
|
43
|
+
### Useful flags
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
python run.py doc.pdf --rule R001 # run only one rule
|
|
47
|
+
python run.py doc.pdf --output result.json # write to a file
|
|
48
|
+
python run.py doc.pdf --dashboard # also emit an HTML dashboard
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Re-render a dashboard
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
python render_dashboard.py result.json
|
|
55
|
+
# → result.html alongside the JSON
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Browse dashboards in a browser
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
./serve.sh
|
|
62
|
+
# → http://localhost:8080/result.html
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Rules in this release
|
|
66
|
+
|
|
67
|
+
{RULES_LIST}
|
|
68
|
+
|
|
69
|
+
## Reproducibility
|
|
70
|
+
|
|
71
|
+
The release bundle is regenerable from the snapshot tag:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
git checkout {SNAPSHOT_TAG}
|
|
75
|
+
# then run kc-beta and ask it to release({label: "{LABEL}"}) again
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `manifest.json` records the exact commit (`{SNAPSHOT_COMMIT}`) so you can verify what's running.
|
|
79
|
+
|
|
80
|
+
## Caveats
|
|
81
|
+
|
|
82
|
+
- Workflows call worker LLMs. Costs depend on your provider; the bundle does not enforce a budget.
|
|
83
|
+
- Workflow output for each rule is preserved in `result.raw[*]` for audit. If you need full audit history with KC's event log + corner-case registry, work from the source workspace, not this bundle.
|
|
84
|
+
- Bundle does not sandbox `python`. Treat it like any executable you trust.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Confidence scorer — Python port of src/agent/confidence-scorer.js.
|
|
3
|
+
|
|
4
|
+
Composite formula: confidence = method_prior * source_presence
|
|
5
|
+
* historical_accuracy * (1 - corner_proximity)
|
|
6
|
+
|
|
7
|
+
Identical to the JS scorer used inside KC, so release runs produce the same
|
|
8
|
+
confidence values KC produces in-workspace.
|
|
9
|
+
|
|
10
|
+
Note on rounding: JS Math.round() is half-up, Python's round() is half-to-even
|
|
11
|
+
(banker's rounding). We use a half-up implementation here to match JS exactly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _round3_halfup(x):
|
|
18
|
+
"""Round x to 3 decimals, half-up (matches JS Math.round)."""
|
|
19
|
+
return math.floor(x * 1000 + 0.5) / 1000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFAULT_PRIORS = {
|
|
23
|
+
"regex": 0.95,
|
|
24
|
+
"python": 0.90,
|
|
25
|
+
"llm": 0.75,
|
|
26
|
+
"ocr": 0.65,
|
|
27
|
+
"fallback": 0.50,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def score(rule_id, extracted_value, source_text="", method="llm",
|
|
32
|
+
document="", priors=None, historical=None, corner_cases=None):
|
|
33
|
+
"""
|
|
34
|
+
Compute composite confidence score (0.0 - 1.0).
|
|
35
|
+
|
|
36
|
+
rule_id: rule identifier
|
|
37
|
+
extracted_value: the value the workflow extracted (string)
|
|
38
|
+
source_text: optional surrounding text from the document
|
|
39
|
+
method: "regex" | "python" | "llm" | "ocr" | "fallback"
|
|
40
|
+
document: document name / path (used for corner-case proximity)
|
|
41
|
+
priors: dict overriding DEFAULT_PRIORS
|
|
42
|
+
historical: dict of {rule_id: accuracy} from confidence_calibration.json
|
|
43
|
+
corner_cases: list/dict from corner_cases.json registry
|
|
44
|
+
"""
|
|
45
|
+
p = priors or DEFAULT_PRIORS
|
|
46
|
+
method_prior = p.get(method, p.get("fallback", 0.50))
|
|
47
|
+
|
|
48
|
+
source_presence = 1.0
|
|
49
|
+
if source_text and extracted_value:
|
|
50
|
+
source_presence = 1.0 if str(extracted_value) in source_text else 0.7
|
|
51
|
+
|
|
52
|
+
hist = (historical or {}).get(rule_id, 0.8)
|
|
53
|
+
|
|
54
|
+
corner_proximity = _corner_proximity(corner_cases, document, rule_id)
|
|
55
|
+
|
|
56
|
+
confidence = method_prior * source_presence * hist * (1.0 - corner_proximity)
|
|
57
|
+
confidence = max(0.0, min(1.0, confidence))
|
|
58
|
+
return _round3_halfup(confidence)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def band(confidence):
|
|
62
|
+
"""Classify confidence into low/medium/high band — matches JS getBand()."""
|
|
63
|
+
if confidence >= 0.8:
|
|
64
|
+
return "high"
|
|
65
|
+
if confidence >= 0.5:
|
|
66
|
+
return "medium"
|
|
67
|
+
return "low"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _corner_proximity(corner_cases, document, rule_id):
|
|
71
|
+
"""Mirror CornerCaseRegistry.match: count entries matching this doc + rule.
|
|
72
|
+
Each match adds 0.1 (capped at 0.3). Schema is intentionally loose — KC's
|
|
73
|
+
JS registry stores entries with optional `document_pattern` and `rule_id`
|
|
74
|
+
fields; we replicate the same matching semantics here.
|
|
75
|
+
"""
|
|
76
|
+
if not corner_cases or not document:
|
|
77
|
+
return 0.0
|
|
78
|
+
entries = corner_cases if isinstance(corner_cases, list) else corner_cases.get("entries", [])
|
|
79
|
+
if not entries:
|
|
80
|
+
return 0.0
|
|
81
|
+
|
|
82
|
+
matches = 0
|
|
83
|
+
for e in entries:
|
|
84
|
+
if not isinstance(e, dict):
|
|
85
|
+
continue
|
|
86
|
+
if e.get("rule_id") and e.get("rule_id") != rule_id:
|
|
87
|
+
continue
|
|
88
|
+
pattern = e.get("document_pattern") or e.get("document") or ""
|
|
89
|
+
if pattern and pattern not in document:
|
|
90
|
+
continue
|
|
91
|
+
matches += 1
|
|
92
|
+
|
|
93
|
+
return min(0.3, 0.1 * matches)
|