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
@@ -1,130 +1,39 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
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
- const VERSIONED_DIRS = new Set(["workflows", "rule_skills", "rules"]);
5
- const VERSIONED_EXTS = new Set([".py", ".json", ".md", ".txt"]);
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
- * Structural component: every write to versioned directories gets tracked.
9
- * - Immutable version copies tracked in manifest
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
- * @param {string} workspacePath
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
- const now = new Date().toISOString().replace(/[-:T]/g, (m) =>
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
  }
@@ -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
- * Rename the workspace folder. Returns the new sessionId.
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
  }
@@ -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 newId = engineRef.current.workspace.rename(arg);
231
- setSessionId(newId);
232
- addMessage({ role: "system", content: `Session renamed to: ${newId}` });
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, { projectDir: config.projectDir }) : null,
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,2 @@
1
+ # KC release-runtime support package.
2
+ # Bundled into every release. Self-contained, no external dependencies.
@@ -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)