kc-beta 0.6.1 → 0.7.0
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/LICENSE +81 -0
- package/LICENSE-COMMERCIAL.md +125 -0
- package/README.md +21 -3
- package/package.json +14 -5
- package/src/agent/context-window.js +9 -12
- package/src/agent/context.js +14 -1
- package/src/agent/document-parser.js +169 -0
- package/src/agent/engine.js +499 -20
- package/src/agent/history/event-history.js +222 -0
- package/src/agent/llm-client.js +55 -0
- package/src/agent/message-utils.js +63 -0
- package/src/agent/pipelines/_milestone-derive.js +511 -0
- package/src/agent/pipelines/base.js +21 -0
- package/src/agent/pipelines/distillation.js +28 -15
- package/src/agent/pipelines/extraction.js +103 -36
- package/src/agent/pipelines/finalization.js +178 -11
- package/src/agent/pipelines/index.js +6 -1
- package/src/agent/pipelines/initializer.js +74 -8
- package/src/agent/pipelines/production-qc.js +31 -44
- package/src/agent/pipelines/skill-authoring.js +152 -80
- package/src/agent/pipelines/skill-testing.js +67 -23
- package/src/agent/retry.js +10 -2
- package/src/agent/scheduler.js +14 -2
- package/src/agent/session-state.js +35 -2
- package/src/agent/skill-loader.js +13 -7
- package/src/agent/skill-validator.js +163 -0
- package/src/agent/task-manager.js +61 -5
- package/src/agent/tools/_workflow-result-schema.js +249 -0
- package/src/agent/tools/document-chunk.js +21 -9
- package/src/agent/tools/phase-advance.js +52 -6
- package/src/agent/tools/release.js +51 -9
- package/src/agent/tools/rule-catalog.js +11 -1
- package/src/agent/tools/workflow-run.js +9 -4
- package/src/agent/tools/workspace-file.js +32 -0
- package/src/agent/workspace.js +61 -0
- package/src/cli/components.js +64 -14
- package/src/cli/index.js +62 -3
- package/src/cli/meme.js +26 -25
- package/src/config.js +65 -22
- package/src/model-tiers.json +48 -0
- package/src/providers.js +87 -0
- package/template/release/v1/README.md.tmpl +108 -0
- package/template/release/v1/catalog.json.tmpl +4 -0
- package/template/release/v1/kc_runtime/__init__.py +11 -0
- package/template/release/v1/kc_runtime/confidence.py +63 -0
- package/template/release/v1/kc_runtime/doc_parser.py +127 -0
- package/template/release/v1/manifest.json.tmpl +11 -0
- package/template/release/v1/render_dashboard.py +117 -0
- package/template/release/v1/run.py +212 -0
- package/template/release/v1/serve.sh +17 -0
- package/template/skills/en/meta-meta/skill-authoring/SKILL.md +19 -0
- package/template/skills/en/meta-meta/work-decomposition/SKILL.md +266 -0
- package/template/skills/en/skill-creator/SKILL.md +1 -1
- package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +19 -0
- package/template/skills/zh/meta-meta/work-decomposition/SKILL.md +264 -0
- package/template/skills/zh/skill-creator/SKILL.md +1 -1
|
@@ -151,8 +151,23 @@ export class ReleaseTool extends BaseTool {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
if (ruleEntries.length === 0) {
|
|
154
|
+
// v0.7.0 #98: actionable error. The previous "no workflows found
|
|
155
|
+
// for any selected rule" message left agents confused (E2E #5 GLM
|
|
156
|
+
// dismissed it with "不影响系统功能" and shipped anyway, with a
|
|
157
|
+
// broken bundle). Spell out the canonical layout, the accepted
|
|
158
|
+
// alternatives, and where the agent's actual files are.
|
|
154
159
|
return new ToolResult(
|
|
155
|
-
`no workflows
|
|
160
|
+
`Release tool found no workflows for the selected rules.\n\n` +
|
|
161
|
+
`Missing: ${missingWorkflows.slice(0, 10).join(", ")}` +
|
|
162
|
+
(missingWorkflows.length > 10 ? ` (+ ${missingWorkflows.length - 10} more)` : "") + "\n\n" +
|
|
163
|
+
`Accepted layouts (release tool checks all three):\n` +
|
|
164
|
+
` workflows/<ruleId>/workflow_v1.py (canonical)\n` +
|
|
165
|
+
` workflows/<ruleId>_workflow.py (flat)\n` +
|
|
166
|
+
` workflows/<ruleId>.json (regex_skill manifest with .entry path)\n\n` +
|
|
167
|
+
`If your workflow files exist under a different layout, either ` +
|
|
168
|
+
`relocate them or write a workflows/<ruleId>.json manifest pointing ` +
|
|
169
|
+
`at the actual file. Do NOT ship the release without workflows — ` +
|
|
170
|
+
`the run.py harness needs them at runtime.`,
|
|
156
171
|
true,
|
|
157
172
|
);
|
|
158
173
|
}
|
|
@@ -251,15 +266,42 @@ export class ReleaseTool extends BaseTool {
|
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
_findLatestWorkflow(ruleId) {
|
|
269
|
+
// Canonical: workflows/<ruleId>/workflow_v#.py (subdirectory layout)
|
|
254
270
|
const wfDir = path.join(this._workspace.cwd, "workflows", ruleId);
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
if (fs.existsSync(wfDir) && fs.statSync(wfDir).isDirectory()) {
|
|
272
|
+
const entries = fs.readdirSync(wfDir).sort();
|
|
273
|
+
const versioned = entries.filter((f) => /^workflow_v\d+\.py$/.test(f));
|
|
274
|
+
if (versioned.length > 0) return path.join(wfDir, versioned[versioned.length - 1]);
|
|
275
|
+
const any = entries.find((f) => f.endsWith(".py") && f.toLowerCase().includes("workflow"));
|
|
276
|
+
if (any) return path.join(wfDir, any);
|
|
277
|
+
const py = entries.find((f) => f.endsWith(".py"));
|
|
278
|
+
if (py) return path.join(wfDir, py);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// v0.7.0 #98: fall back to flat layouts seen in E2E #5 — both DS
|
|
282
|
+
// and GLM produced workflows that release.js's strict per-dir check
|
|
283
|
+
// missed. Accept these so the release tool actually packages the
|
|
284
|
+
// agent's work instead of returning "no workflows found".
|
|
285
|
+
const flatRoot = path.join(this._workspace.cwd, "workflows");
|
|
286
|
+
if (fs.existsSync(flatRoot)) {
|
|
287
|
+
// GLM-style flat: workflows/R001_workflow.py
|
|
288
|
+
const flat = path.join(flatRoot, `${ruleId}_workflow.py`);
|
|
289
|
+
if (fs.existsSync(flat) && fs.statSync(flat).isFile()) return flat;
|
|
290
|
+
// DS-style manifest: workflows/R001.json (regex_skill pointer)
|
|
291
|
+
const manifest = path.join(flatRoot, `${ruleId}.json`);
|
|
292
|
+
if (fs.existsSync(manifest) && fs.statSync(manifest).isFile()) {
|
|
293
|
+
try {
|
|
294
|
+
const data = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
295
|
+
if (data?.entry) {
|
|
296
|
+
const entryPath = path.isAbsolute(data.entry)
|
|
297
|
+
? data.entry
|
|
298
|
+
: path.join(this._workspace.cwd, data.entry);
|
|
299
|
+
if (fs.existsSync(entryPath)) return entryPath;
|
|
300
|
+
}
|
|
301
|
+
} catch { /* manifest unreadable; skip */ }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
263
305
|
}
|
|
264
306
|
|
|
265
307
|
_resolveFixture(rel) {
|
|
@@ -56,7 +56,17 @@ export class RuleCatalogTool extends BaseTool {
|
|
|
56
56
|
constructor(workspace) {
|
|
57
57
|
super();
|
|
58
58
|
this._workspace = workspace;
|
|
59
|
-
|
|
59
|
+
// v0.6.3: do NOT cache the absolute path here. engine.renameSession()
|
|
60
|
+
// moves the workspace dir via fs.renameSync and updates this._workspace.cwd
|
|
61
|
+
// via Workspace.rename(), but tool instances live in the registry and
|
|
62
|
+
// never see the cascade — a cached _catalogPath would keep writing to the
|
|
63
|
+
// pre-rename absolute path, mkdir-recreating the orphaned directory and
|
|
64
|
+
// stranding all rule_catalog state. Resolve on every call instead.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Always read workspace.cwd at call time so /rename is followed. */
|
|
68
|
+
get _catalogPath() {
|
|
69
|
+
return path.join(this._workspace.cwd, "rules", "catalog.json");
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
get name() { return "rule_catalog"; }
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { BaseTool, ToolResult } from "./base.js";
|
|
5
|
+
import { normalizeWorkflowResult } from "./_workflow-result-schema.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Execute a distilled workflow script against a document.
|
|
@@ -89,14 +90,18 @@ export class WorkflowRunTool extends BaseTool {
|
|
|
89
90
|
return new ToolResult(e.message, true);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// Parse output
|
|
93
|
-
let
|
|
93
|
+
// Parse output (last stdout line as JSON)
|
|
94
|
+
let parsed;
|
|
94
95
|
try {
|
|
95
96
|
const lines = output.trim().split("\n");
|
|
96
|
-
|
|
97
|
+
parsed = JSON.parse(lines[lines.length - 1]);
|
|
97
98
|
} catch {
|
|
98
|
-
|
|
99
|
+
parsed = { raw_output: output.slice(0, 5000) };
|
|
99
100
|
}
|
|
101
|
+
// v0.6.2 I1: normalize to canonical dict shape — strips Python
|
|
102
|
+
// dataclass repr() keys, classifies ERROR results, ensures rule_id
|
|
103
|
+
// and verdict are present.
|
|
104
|
+
const resultData = normalizeWorkflowResult(parsed, ruleId, output);
|
|
100
105
|
|
|
101
106
|
// Attach confidence score
|
|
102
107
|
const extractedValue = String(resultData.extracted_value || resultData.value || "");
|
|
@@ -113,6 +113,37 @@ export class WorkspaceFileTool extends BaseTool {
|
|
|
113
113
|
}
|
|
114
114
|
const resolved = this._resolveForScope(filePath, scope);
|
|
115
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
|
+
|
|
116
147
|
fs.writeFileSync(resolved, content, "utf-8");
|
|
117
148
|
|
|
118
149
|
// Auto-commit to git for workspace writes (silently no-ops if gitignored or git unavailable)
|
|
@@ -124,6 +155,7 @@ export class WorkspaceFileTool extends BaseTool {
|
|
|
124
155
|
const label = scope === "project" ? `[project] ${filePath}` : filePath;
|
|
125
156
|
let msg = `Wrote ${content.length} chars to ${label}`;
|
|
126
157
|
if (traceId) msg += ` [trace: ${traceId}]`;
|
|
158
|
+
if (collisionNote) msg += collisionNote;
|
|
127
159
|
return new ToolResult(msg);
|
|
128
160
|
}
|
|
129
161
|
|
package/src/agent/workspace.js
CHANGED
|
@@ -62,6 +62,38 @@ export class Workspace {
|
|
|
62
62
|
this._gitAutoCommitEnabled = opts.gitAutoCommit !== false;
|
|
63
63
|
this._gitAvailable = this._gitAutoCommitEnabled && Workspace.isGitInstalled();
|
|
64
64
|
if (this._gitAvailable) this._initGitRepo();
|
|
65
|
+
|
|
66
|
+
// v0.7.0 F1: detect filesystem case-sensitivity once at construction.
|
|
67
|
+
// macOS HFS+/APFS default + Windows NTFS default are case-insensitive
|
|
68
|
+
// — `SKILL.md` and `skill.md` resolve to the same inode. E2E #5 GLM
|
|
69
|
+
// hit this exactly: wrote skill.md, then SKILL.md (overwrote it),
|
|
70
|
+
// then archive_file moved skill.md (moved both since same inode),
|
|
71
|
+
// then agent saw "SKILL.md disappeared". Surfacing the FS property
|
|
72
|
+
// here lets tools warn on case-collision attempts.
|
|
73
|
+
this.fsCaseSensitive = Workspace._probeCaseSensitivity(this.path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* v0.7.0 F1: cheap probe — write a known-case marker, stat its
|
|
78
|
+
* lowercased twin. If the stat succeeds, the FS folded the case →
|
|
79
|
+
* case-insensitive. Probe runs once per session at construction.
|
|
80
|
+
* Marker is deleted regardless. Returns true on case-sensitive (Linux
|
|
81
|
+
* default), false on case-insensitive (macOS/Windows defaults).
|
|
82
|
+
*/
|
|
83
|
+
static _probeCaseSensitivity(dir) {
|
|
84
|
+
const probe = path.join(dir, ".kc-case-probe-MIXED");
|
|
85
|
+
const twin = path.join(dir, ".kc-case-probe-mixed");
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(probe, "");
|
|
88
|
+
const sensitive = !fs.existsSync(twin);
|
|
89
|
+
try { fs.unlinkSync(probe); } catch { /* ignore */ }
|
|
90
|
+
return sensitive;
|
|
91
|
+
} catch {
|
|
92
|
+
// Couldn't probe (permission?); assume sensitive (Linux default)
|
|
93
|
+
// since false-positive on macOS doesn't break correctness, just
|
|
94
|
+
// misses the warning opportunity.
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
/** @returns {string} Current workspace directory */
|
|
@@ -240,6 +272,25 @@ export class Workspace {
|
|
|
240
272
|
return traceId;
|
|
241
273
|
}
|
|
242
274
|
|
|
275
|
+
/**
|
|
276
|
+
* v0.6.2 J3: Synchronous lock mirror of `withFileLock`, for callers
|
|
277
|
+
* that can't go async (SessionState.save). Locks a sibling
|
|
278
|
+
* `<relPath>.lock` file via O_CREAT|O_EXCL, with 5s timeout and 30s
|
|
279
|
+
* stale-takeover. On failure to acquire, runs fn anyway — better to
|
|
280
|
+
* lose serialization than deadlock a save call. Use sparingly; prefer
|
|
281
|
+
* `withFileLock` (async) for all paths that allow it.
|
|
282
|
+
*/
|
|
283
|
+
withSyncFileLock(relPath, fn, { timeoutMs = 5_000, staleMs = 30_000 } = {}) {
|
|
284
|
+
// v0.7.0 H3: route through resolvePath() so a caller passing
|
|
285
|
+
// "../../shared.json" can't write a lockfile outside the workspace
|
|
286
|
+
// root (the async sibling withFileLock already does this — sync
|
|
287
|
+
// version was the asymmetric weak spot). Cross-session lock-file
|
|
288
|
+
// contamination shut down.
|
|
289
|
+
const resolved = this.resolvePath(relPath);
|
|
290
|
+
const lockPath = `${resolved}.lock`;
|
|
291
|
+
return this._withSyncLockAtPath(lockPath, fn, { timeoutMs, staleMs });
|
|
292
|
+
}
|
|
293
|
+
|
|
243
294
|
/**
|
|
244
295
|
* B5: Synchronous gitops lock. Mirror of withFileLock but sync to fit
|
|
245
296
|
* autoCommit's existing call signature. Times out and proceeds anyway
|
|
@@ -247,6 +298,16 @@ export class Workspace {
|
|
|
247
298
|
*/
|
|
248
299
|
_withGitSyncLock(fn, { timeoutMs = 5_000, staleMs = 30_000 } = {}) {
|
|
249
300
|
const lockPath = path.join(this.path, ".git", "kc-commit.lock");
|
|
301
|
+
return this._withSyncLockAtPath(lockPath, fn, { timeoutMs, staleMs });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Shared sync-lock implementation. Used by `_withGitSyncLock` (B5) and
|
|
306
|
+
* `withSyncFileLock` (J3 / v0.6.2). Same semantics: O_CREAT|O_EXCL on
|
|
307
|
+
* a sibling `.lock` file, busy-spin retry with stale takeover, run fn
|
|
308
|
+
* anyway on timeout.
|
|
309
|
+
*/
|
|
310
|
+
_withSyncLockAtPath(lockPath, fn, { timeoutMs = 5_000, staleMs = 30_000 } = {}) {
|
|
250
311
|
const start = Date.now();
|
|
251
312
|
let acquired = false;
|
|
252
313
|
while (Date.now() - start < timeoutMs) {
|
package/src/cli/components.js
CHANGED
|
@@ -52,19 +52,64 @@ const LENAT_QUOTE = "Intelligence is ten million rules.";
|
|
|
52
52
|
// display jumpy. Peak stays at the highest seen this session.
|
|
53
53
|
const CTX_SAMPLE_WINDOW = 30;
|
|
54
54
|
|
|
55
|
+
// Visual width of a string with CJK chars counted as 2 cells each.
|
|
56
|
+
// Used to truncate session IDs etc. so the status bar fits in a single
|
|
57
|
+
// terminal row regardless of terminal width.
|
|
58
|
+
function visualWidth(s) {
|
|
59
|
+
let w = 0;
|
|
60
|
+
for (const ch of s || "") {
|
|
61
|
+
const cp = ch.codePointAt(0);
|
|
62
|
+
// CJK + fullwidth + emoji rough heuristic — wide if codepoint ≥ 0x1100.
|
|
63
|
+
w += cp >= 0x1100 ? 2 : 1;
|
|
64
|
+
}
|
|
65
|
+
return w;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function truncateVisual(s, maxCells) {
|
|
69
|
+
if (!s) return s;
|
|
70
|
+
if (visualWidth(s) <= maxCells) return s;
|
|
71
|
+
// Middle-truncate so renamed sessions keep both ends visible
|
|
72
|
+
// (e.g. "资管新规测试062-GLM" → "资管新规…2-GLM" — model suffix preserved).
|
|
73
|
+
const headBudget = Math.floor((maxCells - 1) / 2);
|
|
74
|
+
const tailBudget = maxCells - 1 - headBudget;
|
|
75
|
+
const chars = [...s];
|
|
76
|
+
let w = 0; let head = "";
|
|
77
|
+
for (const ch of chars) {
|
|
78
|
+
const cw = ch.codePointAt(0) >= 0x1100 ? 2 : 1;
|
|
79
|
+
if (w + cw > headBudget) break;
|
|
80
|
+
w += cw; head += ch;
|
|
81
|
+
}
|
|
82
|
+
let tw = 0; let tail = "";
|
|
83
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
84
|
+
const ch = chars[i];
|
|
85
|
+
const cw = ch.codePointAt(0) >= 0x1100 ? 2 : 1;
|
|
86
|
+
if (tw + cw > tailBudget) break;
|
|
87
|
+
tw += cw; tail = ch + tail;
|
|
88
|
+
}
|
|
89
|
+
return head + "…" + tail;
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
|
|
56
93
|
const samplesRef = useRef([]);
|
|
57
94
|
const peakRef = useRef(0);
|
|
58
95
|
|
|
59
|
-
//
|
|
96
|
+
// v0.7.0 C3: ref mutations live inside useEffect, not in the render
|
|
97
|
+
// body. React may invoke renders multiple times (StrictMode, concurrent
|
|
98
|
+
// rendering, suspense replay); mutating refs inline duplicated the
|
|
99
|
+
// pushed sample on every replay and let peak drift higher than the
|
|
100
|
+
// real history. Effect runs once per commit.
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const samples = samplesRef.current;
|
|
103
|
+
samples.push(contextTokens || 0);
|
|
104
|
+
if (samples.length > CTX_SAMPLE_WINDOW) samples.shift();
|
|
105
|
+
if ((contextTokens || 0) > peakRef.current) peakRef.current = contextTokens || 0;
|
|
106
|
+
}, [contextTokens]);
|
|
107
|
+
|
|
60
108
|
const samples = samplesRef.current;
|
|
61
|
-
samples.push(contextTokens || 0);
|
|
62
|
-
if (samples.length > CTX_SAMPLE_WINDOW) samples.shift();
|
|
63
109
|
const smoothed = samples.length > 0
|
|
64
110
|
? Math.round(samples.reduce((a, b) => a + b, 0) / samples.length)
|
|
65
|
-
: 0;
|
|
66
|
-
|
|
67
|
-
const peak = peakRef.current;
|
|
111
|
+
: (contextTokens || 0);
|
|
112
|
+
const peak = Math.max(peakRef.current, contextTokens || 0);
|
|
68
113
|
|
|
69
114
|
const pct = contextLimit ? Math.round((smoothed / contextLimit) * 100) : 0;
|
|
70
115
|
const ctxColor = pct > 80 ? "red" : pct > 60 ? "yellow" : "green";
|
|
@@ -83,15 +128,19 @@ export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
|
|
|
83
128
|
: pct >= 60 ? " · 💾 建议 /compact"
|
|
84
129
|
: "";
|
|
85
130
|
|
|
131
|
+
// Truncate session ID to keep the bar single-row even with CJK names.
|
|
132
|
+
// 14 visual cells covers most short UUIDs and ~6-7 CJK chars.
|
|
133
|
+
const displaySessionId = sessionId ? truncateVisual(sessionId, 14) : "";
|
|
134
|
+
|
|
86
135
|
return h(Box, { marginTop: 0 },
|
|
87
|
-
h(Text, { dimColor: true }, " ⏵⏵ KC
|
|
88
|
-
h(Text, { dimColor: true },
|
|
89
|
-
phase ? h(Text, { color: "cyan" }, ` ${phase.toUpperCase()}`) : null,
|
|
90
|
-
h(Text, { color: "green" }, " ● "),
|
|
91
|
-
h(Text, { color: ctxColor }, `CTX: ${ctxLabel}/${limitLabel} (${pct}%)`),
|
|
92
|
-
showPeak ? h(Text, { dimColor: true }, ` · peak ${fmt(peak)}`) : null,
|
|
93
|
-
compactHint ? h(Text, { color: ctxColor }, compactHint) : null,
|
|
94
|
-
h(Text, { dimColor: true }, ` · ${LENAT_QUOTE}`),
|
|
136
|
+
h(Text, { dimColor: true, wrap: "truncate-end" }, " ⏵⏵ KC "),
|
|
137
|
+
h(Text, { dimColor: true, wrap: "truncate-end" }, displaySessionId ? `[${displaySessionId}]` : ""),
|
|
138
|
+
phase ? h(Text, { color: "cyan", wrap: "truncate-end" }, ` ${phase.toUpperCase()}`) : null,
|
|
139
|
+
h(Text, { color: "green", wrap: "truncate-end" }, " ● "),
|
|
140
|
+
h(Text, { color: ctxColor, wrap: "truncate-end" }, `CTX: ${ctxLabel}/${limitLabel} (${pct}%)`),
|
|
141
|
+
showPeak ? h(Text, { dimColor: true, wrap: "truncate-end" }, ` · peak ${fmt(peak)}`) : null,
|
|
142
|
+
compactHint ? h(Text, { color: ctxColor, wrap: "truncate-end" }, compactHint) : null,
|
|
143
|
+
h(Text, { dimColor: true, wrap: "truncate-end" }, ` · ${LENAT_QUOTE}`),
|
|
95
144
|
);
|
|
96
145
|
}
|
|
97
146
|
|
|
@@ -161,6 +210,7 @@ export function WelcomeBanner({ projectDir, pendingInputCount = 0 } = {}) {
|
|
|
161
210
|
" rules/02 as core; rules/03-10 are supporting context only.\""),
|
|
162
211
|
h(Text, null, ""),
|
|
163
212
|
h(Text, { dimColor: true }, "Product of Memium / kitchen-engineer42"),
|
|
213
|
+
h(Text, { dimColor: true }, "PolyForm Noncommercial 1.0.0 — commercial use requires a separate license. See LICENSE-COMMERCIAL.md."),
|
|
164
214
|
);
|
|
165
215
|
}
|
|
166
216
|
|
package/src/cli/index.js
CHANGED
|
@@ -76,6 +76,13 @@ function App({ engine, config }) {
|
|
|
76
76
|
}, []);
|
|
77
77
|
|
|
78
78
|
const addMessage = useCallback((msg) => {
|
|
79
|
+
// v0.7.0 H6: dismiss welcome banner once any real message lands.
|
|
80
|
+
// The banner state was initialized true and never set false — the
|
|
81
|
+
// banner stayed on every frame for the entire session, eating
|
|
82
|
+
// permanent screen real estate. Conditionally clear on first
|
|
83
|
+
// user/agent/tool-result message; system-only messages don't
|
|
84
|
+
// dismiss (they're often just the banner-side info itself).
|
|
85
|
+
if (msg && msg.role !== "system") setShowWelcome(false);
|
|
79
86
|
setMessages((prev) => {
|
|
80
87
|
if (prev.length < MAX_RETAINED_MESSAGES) return [...prev, msg];
|
|
81
88
|
// Cap hit: drop the oldest non-system entry. If everything is system
|
|
@@ -385,7 +392,16 @@ function App({ engine, config }) {
|
|
|
385
392
|
const sched = new Scheduler(engineRef.current.workspace);
|
|
386
393
|
const jobs = sched.list();
|
|
387
394
|
if (jobs.length === 0) {
|
|
388
|
-
|
|
395
|
+
// v0.6.3.1: also surface pending input files. The welcome banner
|
|
396
|
+
// tells the user "run /schedule for details" when input/ has
|
|
397
|
+
// unseen files, but the no-jobs branch used to ignore those —
|
|
398
|
+
// user got a dead-end "no jobs" reply with the files invisible.
|
|
399
|
+
const pending = sched.pendingInputCount();
|
|
400
|
+
const tail = sched.tailLog(8);
|
|
401
|
+
let body = "No scheduled ingestion jobs. Ask KC to set one up via the schedule_fetch tool.";
|
|
402
|
+
if (pending > 0) body += `\n\nPending in input/: ${pending} file(s) (drop into workspace input/ to be picked up).`;
|
|
403
|
+
if (tail) body += `\n\nlogs/ingest.log (last 8):\n${tail}`;
|
|
404
|
+
addMessage({ role: "system", content: body });
|
|
389
405
|
} else {
|
|
390
406
|
const lines = jobs.map((j) => {
|
|
391
407
|
const status = j.enabled ? "✓ enabled" : "· disabled";
|
|
@@ -423,6 +439,11 @@ function App({ engine, config }) {
|
|
|
423
439
|
streamingRef.current = true;
|
|
424
440
|
setStreaming(true);
|
|
425
441
|
setSpinnerStatus("Compacting...");
|
|
442
|
+
// v0.7.0 H7: top-level .catch on the IIFE — the inner try/catch
|
|
443
|
+
// handles the compact() failure path; this tail .catch silences
|
|
444
|
+
// any secondary rejection from the catch handler or finally
|
|
445
|
+
// block (e.g., addMessage throw). Without it, those would be
|
|
446
|
+
// UnhandledPromiseRejection in strict-mode Node.
|
|
426
447
|
(async () => {
|
|
427
448
|
try {
|
|
428
449
|
const result = await engineRef.current.compact();
|
|
@@ -469,7 +490,7 @@ function App({ engine, config }) {
|
|
|
469
490
|
runTurn(next);
|
|
470
491
|
}
|
|
471
492
|
}
|
|
472
|
-
})();
|
|
493
|
+
})().catch(() => { /* H7 defensive tail */ });
|
|
473
494
|
return true;
|
|
474
495
|
}
|
|
475
496
|
|
|
@@ -528,6 +549,10 @@ function App({ engine, config }) {
|
|
|
528
549
|
}
|
|
529
550
|
} else {
|
|
530
551
|
// Resume a previous session
|
|
552
|
+
// v0.7.0 H8: top-level .catch on the IIFE so a throw inside
|
|
553
|
+
// addMessage()/setMessages() (e.g., during the catch handler
|
|
554
|
+
// itself, or in Ink reconciler) doesn't surface as an
|
|
555
|
+
// UnhandledPromiseRejection that crashes Node strict-mode.
|
|
531
556
|
(async () => {
|
|
532
557
|
try {
|
|
533
558
|
const client = new LLMClient({
|
|
@@ -541,6 +566,17 @@ function App({ engine, config }) {
|
|
|
541
566
|
setSessionId(resumed.workspace.sessionId);
|
|
542
567
|
setPhase(resumed.currentPhase);
|
|
543
568
|
setMessages([]);
|
|
569
|
+
// v0.7.0 F2: re-populate TaskBoard state from the resumed
|
|
570
|
+
// engine's TaskManager. Without this, the TUI showed an
|
|
571
|
+
// empty task list after /resume even when tasks.json on
|
|
572
|
+
// disk had pending work. The setTaskList path mirrors what
|
|
573
|
+
// the per-event tasks_progress handler does for live
|
|
574
|
+
// sessions.
|
|
575
|
+
try {
|
|
576
|
+
const tasks = resumed.taskManager.getAllTasks();
|
|
577
|
+
setTaskList(tasks);
|
|
578
|
+
setTaskProgress(resumed.taskManager.progress);
|
|
579
|
+
} catch { /* taskManager unavailable on very old session-state */ }
|
|
544
580
|
addMessage({
|
|
545
581
|
role: "system",
|
|
546
582
|
content:
|
|
@@ -552,7 +588,8 @@ function App({ engine, config }) {
|
|
|
552
588
|
} catch (err) {
|
|
553
589
|
addMessage({ role: "system", content: `Resume failed: ${err.message}` });
|
|
554
590
|
}
|
|
555
|
-
})();
|
|
591
|
+
})().catch(() => { /* defended above; tail catch silences any
|
|
592
|
+
secondary rejection from the catch handler itself */ });
|
|
556
593
|
}
|
|
557
594
|
return true;
|
|
558
595
|
|
|
@@ -562,6 +599,13 @@ function App({ engine, config }) {
|
|
|
562
599
|
try { engineRef.current.saveState(); } catch { /* ignore */ }
|
|
563
600
|
try { engineRef.current.stop(); } catch { /* ignore */ }
|
|
564
601
|
exit();
|
|
602
|
+
// v0.6.3.1: force-exit after a brief grace window. Ink's exit()
|
|
603
|
+
// unmounts the TUI but in-flight LLM streams / subagent fetches
|
|
604
|
+
// / unflushed appendFileSync handles can keep the Node event loop
|
|
605
|
+
// alive indefinitely on long sessions. The 500ms gives saveState
|
|
606
|
+
// and any synchronous flushes time to complete; after that we
|
|
607
|
+
// hard-exit so the user's terminal returns to the shell promptly.
|
|
608
|
+
setTimeout(() => process.exit(0), 500).unref();
|
|
565
609
|
return true;
|
|
566
610
|
|
|
567
611
|
default:
|
|
@@ -756,9 +800,24 @@ export async function main({ languageOverride } = {}) {
|
|
|
756
800
|
|
|
757
801
|
// Save state on process exit + stop background diagnostics (B0.1 heap
|
|
758
802
|
// sampler). saveState is idempotent; stop() is safe to call twice.
|
|
803
|
+
//
|
|
804
|
+
// v0.6.3.1: handler must terminate. Pre-fix it only saved + returned, which
|
|
805
|
+
// overrides Node's default SIGINT behavior — the process kept running with
|
|
806
|
+
// active LLM streams / subagent fetches keeping the event loop alive, and
|
|
807
|
+
// mashing ^C did nothing visible. Now: first ^C saves and tries clean exit
|
|
808
|
+
// after 500ms; second ^C hard-kills with no further saves.
|
|
809
|
+
let interruptCount = 0;
|
|
759
810
|
const saveOnExit = () => {
|
|
811
|
+
interruptCount++;
|
|
812
|
+
if (interruptCount >= 2) {
|
|
813
|
+
// Second interrupt — user wants out NOW
|
|
814
|
+
process.stderr.write("\nForce-exiting (second interrupt).\n");
|
|
815
|
+
process.exit(130); // 128 + SIGINT
|
|
816
|
+
}
|
|
760
817
|
try { engine.saveState(); } catch { /* ignore */ }
|
|
761
818
|
try { engine.stop(); } catch { /* ignore */ }
|
|
819
|
+
process.stderr.write("\nReceived interrupt — saving state, then exiting in 500ms (press again to force).\n");
|
|
820
|
+
setTimeout(() => process.exit(130), 500).unref();
|
|
762
821
|
};
|
|
763
822
|
process.on("SIGINT", saveOnExit);
|
|
764
823
|
process.on("SIGTERM", saveOnExit);
|
package/src/cli/meme.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
// AUTO-GENERATED by scripts/build-meme.js — DO NOT EDIT BY HAND.
|
|
2
|
+
// Source of truth lives in src/cli/meme.source.js.
|
|
3
|
+
//
|
|
4
|
+
// v0.7.0 Group K2: the textual easter-egg payload (lyrics, team
|
|
5
|
+
// handles, watermark) is XOR-encoded + base64 in BLOB below. The
|
|
6
|
+
// decoder is intentionally simple — the goal is to defeat grep-based
|
|
7
|
+
// plagiarism scraping, not to provide cryptographic protection.
|
|
8
|
+
// Determined reverse-engineering can recover the data; the watermark
|
|
9
|
+
// survives and identifies origin in any copies. Please leave it in.
|
|
10
|
+
|
|
11
|
+
import React from "react";
|
|
2
12
|
import { Box, Text, useInput } from "ink";
|
|
3
13
|
|
|
4
14
|
const h = React.createElement;
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
const _K = Buffer.from([
|
|
17
|
+
0x4b, 0x43, 0x37, 0x31, 0x2a, 0x68, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73, 0x2a, 0x66, 0x61, 0x74,
|
|
18
|
+
]);
|
|
19
|
+
const _BLOB = "MGFbSFgBAgFMXyhRY0ENGGs0VlheSAAcCkUAHEUIQ1hpFFIWWA1BARoXEh1OAwVUJC0XRUINQRALBBAbCEpDPSVjWERYSAUACwQeUQZENhFrM1ZDXkgVHQFFABxFCENYaQFCRQoBD1IBEAFTRg8EB2lvFWVCDRMXSRZTEgoSEwE/KxdFRUgHGwABUV8IMgkRayZZVQoBElIAAARRBkQgVD8sWl5YGg4FThIWU0cTEgBrMVJQSQBBFAEXUV8IMg5UKSYXWU8JExZMOF9RXgMAGWl5bBNqWFA0BxYbUQZEITUmJltYS0pNUC4kHQBPCiobKCZZEwZKITMaFxYGWURNVgsAVkNFBAgcCyYhPwhKQzQPKkFYWQEOHCoMARZJEg4GEzYVHQgoJgAPBhY0XwlDWGkDcENPDQ89HAQdFE9ETVYLC1ZdTCVDXkwlGBpeBQkRJW5SX00BDxcLF0dBCEpDNAcqW0hiHQAcCUdfUWorAAY+KFgTBkohIwcEHR9DCghWZ2F3QkUGBh8PClFfCCYVHC4sWRMGSiEqBwIGEghKQzQTGseupvdDXkwlChtCC0NYaQNNXk8SDhdMSVEzUB8ZACMmQ0NFBA1QM0lRBEsSBAYmIkVaCFJDOS1FscQKNg4YMgVYQ0dILx0ABhweRwMTFyIiWxEbRlFcXkWxxAqkyFQGJlpYXwVBXU4OGgdJDgQaZiZZVkMGBBccUUFT6NFBHD83R0IQR04VBxEbBkhIAhsmbFxYXgsJFwBIFh1NDw8RLjEDAwUDAl8NCRpRVw==";
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"There's a truth to find",
|
|
17
|
-
"The end is new",
|
|
18
|
-
"A tomorrow we must reach for",
|
|
19
|
-
"To be heard",
|
|
20
|
-
];
|
|
21
|
+
function _decode() {
|
|
22
|
+
const buf = Buffer.from(_BLOB, "base64");
|
|
23
|
+
const out = Buffer.alloc(buf.length);
|
|
24
|
+
for (let i = 0; i < buf.length; i++) out[i] = buf[i] ^ _K[i % _K.length];
|
|
25
|
+
return JSON.parse(out.toString("utf-8"));
|
|
26
|
+
}
|
|
21
27
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"@XY🌟", "@HalfM", "@GreenOrange",
|
|
27
|
-
"@LilyHuang", "@Qianlili", "@songmao",
|
|
28
|
-
"@zoezoe", "@yhhm",
|
|
29
|
-
];
|
|
28
|
+
const _payload = _decode();
|
|
29
|
+
const LYRICS = _payload.lyrics;
|
|
30
|
+
const TEAM = _payload.team;
|
|
31
|
+
const __KC_MEME_WATERMARK__ = _payload.watermark;
|
|
30
32
|
|
|
31
33
|
export function MemeOverlay({ onDismiss }) {
|
|
32
34
|
useInput((input, key) => {
|
|
@@ -34,7 +36,6 @@ export function MemeOverlay({ onDismiss }) {
|
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
return h(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, marginTop: 1, marginBottom: 1 },
|
|
37
|
-
// Lyrics block
|
|
38
39
|
h(Box, { flexDirection: "column" },
|
|
39
40
|
...LYRICS.map((line, i) =>
|
|
40
41
|
h(Text, { key: `l-${i}`, color: "cyan", italic: true }, line),
|
|
@@ -43,7 +44,6 @@ export function MemeOverlay({ onDismiss }) {
|
|
|
43
44
|
h(Text, null, ""),
|
|
44
45
|
h(Text, { dimColor: true }, "─".repeat(60)),
|
|
45
46
|
h(Text, null, ""),
|
|
46
|
-
// Team credit
|
|
47
47
|
h(Text, { color: "yellow", bold: true },
|
|
48
48
|
"Here's to all the smart minds that are/were part of our team:"),
|
|
49
49
|
h(Text, null, ""),
|
|
@@ -54,5 +54,6 @@ export function MemeOverlay({ onDismiss }) {
|
|
|
54
54
|
),
|
|
55
55
|
h(Text, null, ""),
|
|
56
56
|
h(Text, { dimColor: true }, "Press ESC or Enter to dismiss."),
|
|
57
|
+
h(Text, { dimColor: true }, __KC_MEME_WATERMARK__),
|
|
57
58
|
);
|
|
58
59
|
}
|