@vincemakes/kiso-code 0.47.3 → 0.49.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/dist/apply-patch.d.ts +42 -0
- package/dist/apply-patch.js +381 -0
- package/dist/builtin.js +10 -3
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +23 -8
- package/dist/child-result.d.ts +10 -2
- package/dist/child-result.js +52 -5
- package/dist/coding-prompt.d.ts +1 -1
- package/dist/coding-prompt.js +2 -2
- package/dist/config.d.ts +11 -0
- package/dist/config.js +25 -0
- package/dist/create-coding-agent.js +43 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -2
- package/dist/state.d.ts +29 -0
- package/dist/state.js +43 -3
- package/dist/task-notice.d.ts +5 -0
- package/dist/task-notice.js +5 -0
- package/package.json +14 -14
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.49.0 B6.2 / I4 — `kiso apply-patch <taskDir> [--allow-partial]
|
|
3
|
+
* [--with-markers]`: adopt an implementer's collected patch into the
|
|
4
|
+
* workspace. A writer owns an isolated workspace; adoption is a separate,
|
|
5
|
+
* crash-honest effect.
|
|
6
|
+
*
|
|
7
|
+
* The collection (the subagent extension's collectWriter) left in the task
|
|
8
|
+
* directory: `patch.json` (the files, their modes, whether the patch is
|
|
9
|
+
* complete), `versions/<n>.base` and `versions/<n>.child` (each changed
|
|
10
|
+
* file before and after the child), and the child's `acceptance.json`.
|
|
11
|
+
*
|
|
12
|
+
* PREPARE writes nothing: each file is merged three ways — base (the
|
|
13
|
+
* snapshot the child started from), ours (the workspace now), theirs (the
|
|
14
|
+
* child's) — with `git merge-file`'s algorithm, which never reads or writes
|
|
15
|
+
* the index. Any conflict, or an entry this release does not adopt
|
|
16
|
+
* (symlinks, submodules, a mode change on an existing file, a file and a
|
|
17
|
+
* directory trading places, two names differing only in case), stops here
|
|
18
|
+
* with nothing written.
|
|
19
|
+
*
|
|
20
|
+
* PUBLISH is per file and journaled (`<taskDir>/apply.jsonl`): each target
|
|
21
|
+
* is revalidated against what PREPARE saw; a creation is no-clobber (link
|
|
22
|
+
* or refuse, WR-1A ②'s rule); a modification is revalidated right before
|
|
23
|
+
* its rename (WR-1A ③); a deletion is revalidated, then unlinked — the
|
|
24
|
+
* same narrow check-to-effect window WR-1's conditional write has, never
|
|
25
|
+
* called a compare-and-swap. A filesystem has no multi-file transaction,
|
|
26
|
+
* so the promise is all-or-none PREPARE, never an atomic write: a late
|
|
27
|
+
* revalidation failure ends `partial`, naming what was and was not
|
|
28
|
+
* published, and nothing retries it.
|
|
29
|
+
*
|
|
30
|
+
* VERIFY: after a clean publication the patch's acceptance always runs
|
|
31
|
+
* again in the workspace (the child's ran against the snapshot; the
|
|
32
|
+
* workspace has what the snapshot never held — ignored files, a build
|
|
33
|
+
* cache). It is re-resolved from the configuration by its NAME, never run
|
|
34
|
+
* from the record; it touches no git state. Both verdicts are printed.
|
|
35
|
+
* There is no automatic rollback. An interrupted verification is reported
|
|
36
|
+
* as not completed (before it started) or unknown (while it ran), and never
|
|
37
|
+
* retried by itself.
|
|
38
|
+
*
|
|
39
|
+
* Exit codes: 0 clean (whatever the acceptance said — two facts, printed),
|
|
40
|
+
* 3 conflicted, 4 partial, 5 refused or unsupported, 1 failed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyPatch(args: readonly string[], cwd?: string): Promise<number>;
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.49.0 B6.2 / I4 — `kiso apply-patch <taskDir> [--allow-partial]
|
|
3
|
+
* [--with-markers]`: adopt an implementer's collected patch into the
|
|
4
|
+
* workspace. A writer owns an isolated workspace; adoption is a separate,
|
|
5
|
+
* crash-honest effect.
|
|
6
|
+
*
|
|
7
|
+
* The collection (the subagent extension's collectWriter) left in the task
|
|
8
|
+
* directory: `patch.json` (the files, their modes, whether the patch is
|
|
9
|
+
* complete), `versions/<n>.base` and `versions/<n>.child` (each changed
|
|
10
|
+
* file before and after the child), and the child's `acceptance.json`.
|
|
11
|
+
*
|
|
12
|
+
* PREPARE writes nothing: each file is merged three ways — base (the
|
|
13
|
+
* snapshot the child started from), ours (the workspace now), theirs (the
|
|
14
|
+
* child's) — with `git merge-file`'s algorithm, which never reads or writes
|
|
15
|
+
* the index. Any conflict, or an entry this release does not adopt
|
|
16
|
+
* (symlinks, submodules, a mode change on an existing file, a file and a
|
|
17
|
+
* directory trading places, two names differing only in case), stops here
|
|
18
|
+
* with nothing written.
|
|
19
|
+
*
|
|
20
|
+
* PUBLISH is per file and journaled (`<taskDir>/apply.jsonl`): each target
|
|
21
|
+
* is revalidated against what PREPARE saw; a creation is no-clobber (link
|
|
22
|
+
* or refuse, WR-1A ②'s rule); a modification is revalidated right before
|
|
23
|
+
* its rename (WR-1A ③); a deletion is revalidated, then unlinked — the
|
|
24
|
+
* same narrow check-to-effect window WR-1's conditional write has, never
|
|
25
|
+
* called a compare-and-swap. A filesystem has no multi-file transaction,
|
|
26
|
+
* so the promise is all-or-none PREPARE, never an atomic write: a late
|
|
27
|
+
* revalidation failure ends `partial`, naming what was and was not
|
|
28
|
+
* published, and nothing retries it.
|
|
29
|
+
*
|
|
30
|
+
* VERIFY: after a clean publication the patch's acceptance always runs
|
|
31
|
+
* again in the workspace (the child's ran against the snapshot; the
|
|
32
|
+
* workspace has what the snapshot never held — ignored files, a build
|
|
33
|
+
* cache). It is re-resolved from the configuration by its NAME, never run
|
|
34
|
+
* from the record; it touches no git state. Both verdicts are printed.
|
|
35
|
+
* There is no automatic rollback. An interrupted verification is reported
|
|
36
|
+
* as not completed (before it started) or unknown (while it ran), and never
|
|
37
|
+
* retried by itself.
|
|
38
|
+
*
|
|
39
|
+
* Exit codes: 0 clean (whatever the acceptance said — two facts, printed),
|
|
40
|
+
* 3 conflicted, 4 partial, 5 refused or unsupported, 1 failed.
|
|
41
|
+
*/
|
|
42
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
43
|
+
import { createHash } from "node:crypto";
|
|
44
|
+
import { appendFileSync, chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
45
|
+
import { tmpdir } from "node:os";
|
|
46
|
+
import { basename, dirname, join } from "node:path";
|
|
47
|
+
import { projectArtifacts, trustFor } from "@vincemakes/kiso-runtime";
|
|
48
|
+
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveProjectTrustPolicy } from "./config.js";
|
|
49
|
+
const ABSENT = "absent";
|
|
50
|
+
const sha = (b) => createHash("sha256").update(b).digest("hex");
|
|
51
|
+
export async function applyPatch(args, cwd = process.cwd()) {
|
|
52
|
+
const taskDir = args.find((a) => !a.startsWith("--"));
|
|
53
|
+
const allowPartial = args.includes("--allow-partial");
|
|
54
|
+
const withMarkers = args.includes("--with-markers");
|
|
55
|
+
if (taskDir === undefined) {
|
|
56
|
+
console.error("usage: kiso apply-patch <task directory> [--allow-partial] [--with-markers]");
|
|
57
|
+
return 5;
|
|
58
|
+
}
|
|
59
|
+
let patch;
|
|
60
|
+
try {
|
|
61
|
+
patch = JSON.parse(readFileSync(join(taskDir, "patch.json"), "utf8"));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(`apply-patch: no collected patch in ${taskDir} (${err.message})`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const journal = join(taskDir, "apply.jsonl");
|
|
68
|
+
const prior = readJournal(journal);
|
|
69
|
+
if (prior.length > 0) {
|
|
70
|
+
const ended = prior.find((r) => r.type === "apply_terminal");
|
|
71
|
+
if (ended !== undefined) {
|
|
72
|
+
console.error(`apply-patch: this patch was already adopted (${String(ended.outcome)}); nothing was done`);
|
|
73
|
+
return 5;
|
|
74
|
+
}
|
|
75
|
+
console.error(interruptedReport(prior));
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
if (!patch.applyable && !allowPartial) {
|
|
79
|
+
console.error("apply-patch: the child was stopped, so this patch is partial; adopt it only with --allow-partial");
|
|
80
|
+
return 5;
|
|
81
|
+
}
|
|
82
|
+
let root;
|
|
83
|
+
try {
|
|
84
|
+
root = git(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
console.error("apply-patch: run it inside the workspace's git repository");
|
|
88
|
+
return 5;
|
|
89
|
+
}
|
|
90
|
+
const unsupported = unsupportedOf(patch, root);
|
|
91
|
+
if (unsupported.length > 0) {
|
|
92
|
+
console.error(`apply-patch: not adopted — this release adopts regular files only:\n${unsupported.map((u) => ` ${u}`).join("\n")}\nIntegrate it by hand, or delegate again.`);
|
|
93
|
+
return 5;
|
|
94
|
+
}
|
|
95
|
+
// ── PREPARE: nothing in the workspace changes ──────────────────────────
|
|
96
|
+
const planned = [];
|
|
97
|
+
const conflicts = [];
|
|
98
|
+
const scratch = mkdtempSync(join(tmpdir(), "kiso-apply-"));
|
|
99
|
+
try {
|
|
100
|
+
patch.files.forEach((f, n) => {
|
|
101
|
+
const target = join(root, f.path);
|
|
102
|
+
const ours = readOrNull(target);
|
|
103
|
+
const expected = ours === null ? ABSENT : sha(ours);
|
|
104
|
+
const base = f.status === "A" ? null : readFileSync(join(taskDir, "versions", `${n}.base`));
|
|
105
|
+
const theirs = f.status === "D" ? null : readFileSync(join(taskDir, "versions", `${n}.child`));
|
|
106
|
+
const exec = f.mode === "100755";
|
|
107
|
+
if (f.status === "A") {
|
|
108
|
+
if (ours === null)
|
|
109
|
+
planned.push({ path: f.path, action: "create", expected, content: theirs, exec });
|
|
110
|
+
else if (!ours.equals(theirs))
|
|
111
|
+
conflicts.push(`${f.path}: created by the child, and it exists in the workspace with other content`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (f.status === "D") {
|
|
115
|
+
if (ours === null)
|
|
116
|
+
return; // already gone
|
|
117
|
+
if (ours.equals(base))
|
|
118
|
+
planned.push({ path: f.path, action: "delete", expected });
|
|
119
|
+
else
|
|
120
|
+
conflicts.push(`${f.path}: deleted by the child, and changed in the workspace since`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// a modification
|
|
124
|
+
if (ours === null) {
|
|
125
|
+
conflicts.push(`${f.path}: changed by the child, and deleted in the workspace since`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (ours.equals(base))
|
|
129
|
+
planned.push({ path: f.path, action: "modify", expected, content: theirs });
|
|
130
|
+
else if (ours.equals(theirs))
|
|
131
|
+
return; // the workspace already has it
|
|
132
|
+
else {
|
|
133
|
+
const merged = mergeThree(scratch, ours, base, theirs);
|
|
134
|
+
if (merged.conflicts === 0)
|
|
135
|
+
planned.push({ path: f.path, action: "modify", expected, content: merged.content });
|
|
136
|
+
else if (withMarkers)
|
|
137
|
+
planned.push({ path: f.path, action: "modify", expected, content: merged.content });
|
|
138
|
+
else
|
|
139
|
+
conflicts.push(`${f.path}: ${merged.conflicts} conflicting ${merged.conflicts === 1 ? "hunk" : "hunks"}\n${hunks(merged.content)}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
if (conflicts.length > 0) {
|
|
147
|
+
console.error(`apply-patch: nothing was written — the patch conflicts with the workspace:\n${conflicts.map((c) => ` ${c}`).join("\n")}\nResolve by hand, apply with --with-markers and fix the markers with edit_file, or delegate again on the current tree.`);
|
|
148
|
+
return 3;
|
|
149
|
+
}
|
|
150
|
+
// test-only: KISO_TEST_APPLY_HOOK names a module whose afterPrepare() /
|
|
151
|
+
// afterPublish(path) run between the steps — the races a gate needs on cue
|
|
152
|
+
const hook = process.env.KISO_TEST_APPLY_HOOK === undefined ? undefined : (await import(process.env.KISO_TEST_APPLY_HOOK));
|
|
153
|
+
hook?.afterPrepare?.();
|
|
154
|
+
// ── PUBLISH: per file, revalidated, journaled ───────────────────────────
|
|
155
|
+
const record = (r) => appendFileSync(journal, `${JSON.stringify({ ...r, ts: Date.now() })}\n`);
|
|
156
|
+
record({ type: "apply_planned", patchSha256: sha(readFileSync(join(taskDir, "patch.diff"))), files: planned.map((p) => ({ path: p.path, action: p.action, expected: p.expected })) });
|
|
157
|
+
const published = [];
|
|
158
|
+
for (const p of planned) {
|
|
159
|
+
const target = join(root, p.path);
|
|
160
|
+
const why = publish(target, p);
|
|
161
|
+
if (why !== null) {
|
|
162
|
+
record({ type: "apply_terminal", outcome: "partial", published, stoppedAt: p.path, reason: why });
|
|
163
|
+
const rest = planned.map((q) => q.path).filter((q) => !published.includes(q));
|
|
164
|
+
console.error(`apply-patch: PARTIAL — ${p.path}: ${why}\n published: ${published.length > 0 ? published.join(", ") : "none"}\n not published: ${rest.join(", ")}\nNothing is retried or rolled back; read the files and decide.`);
|
|
165
|
+
return 4;
|
|
166
|
+
}
|
|
167
|
+
published.push(p.path);
|
|
168
|
+
record({ type: "apply_file_published", path: p.path, action: p.action });
|
|
169
|
+
hook?.afterPublish?.(p.path);
|
|
170
|
+
}
|
|
171
|
+
record({ type: "publication_complete", files: published.length });
|
|
172
|
+
console.log(`apply-patch: adopted ${published.length} ${published.length === 1 ? "file" : "files"}: ${published.join(", ") || "(the workspace already had every change)"}`);
|
|
173
|
+
// ── VERIFY: the acceptance again, in the workspace ──────────────────────
|
|
174
|
+
const child = readOrNull(join(taskDir, "acceptance.json"));
|
|
175
|
+
const childVerdict = child === null ? "none" : JSON.parse(child.toString("utf8")).passed === true ? "PASSED" : "FAILED";
|
|
176
|
+
if (patch.acceptance === undefined) {
|
|
177
|
+
record({ type: "apply_terminal", outcome: "clean" });
|
|
178
|
+
console.log(`child acceptance: ${childVerdict} · workspace acceptance: none named — the merge was textual only`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const resolved = await resolveAcceptance(patch.acceptance, root);
|
|
182
|
+
if (typeof resolved === "string") {
|
|
183
|
+
record({ type: "apply_terminal", outcome: "clean", acceptance: "unresolved", reason: resolved });
|
|
184
|
+
console.log(`child acceptance: ${childVerdict} · workspace acceptance: not run — ${resolved}`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
record({ type: "acceptance_started", ...resolved.label });
|
|
188
|
+
const v = await runInWorkspace(resolved, root);
|
|
189
|
+
record({ type: "acceptance_result", passed: v.passed, exitCode: v.exitCode });
|
|
190
|
+
record({ type: "apply_terminal", outcome: "clean" });
|
|
191
|
+
console.log(`child acceptance: ${childVerdict} · workspace acceptance: ${v.passed ? "PASSED" : "FAILED"} (exit ${v.exitCode ?? "killed"})${v.passed ? "" : `\n${v.tail}`}`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
/** What an interrupted adoption left — the journal's facts, nothing guessed. */
|
|
195
|
+
function interruptedReport(records) {
|
|
196
|
+
const files = records.filter((r) => r.type === "apply_file_published").map((r) => String(r.path));
|
|
197
|
+
const planned = records.find((r) => r.type === "apply_planned")?.files?.map((f) => f.path) ?? [];
|
|
198
|
+
if (records.some((r) => r.type === "acceptance_started")) {
|
|
199
|
+
return `apply-patch: an earlier adoption was interrupted — every file was published (${files.join(", ")}); its workspace acceptance started and its outcome is UNKNOWN. Nothing is retried: run the check yourself.`;
|
|
200
|
+
}
|
|
201
|
+
if (records.some((r) => r.type === "publication_complete")) {
|
|
202
|
+
return `apply-patch: an earlier adoption was interrupted — every file was published (${files.join(", ")}); its workspace acceptance was NOT COMPLETED. Nothing is retried: run the check yourself.`;
|
|
203
|
+
}
|
|
204
|
+
const rest = planned.filter((p) => !files.includes(p));
|
|
205
|
+
return `apply-patch: an earlier adoption was interrupted mid-publication — published: ${files.join(", ") || "none"}; not published: ${rest.join(", ") || "none"}. Nothing is retried or rolled back; read the files and decide.`;
|
|
206
|
+
}
|
|
207
|
+
/** B6.2's scope for 0.49: regular files created, modified or deleted. */
|
|
208
|
+
function unsupportedOf(patch, root) {
|
|
209
|
+
const out = [];
|
|
210
|
+
const lower = new Map();
|
|
211
|
+
for (const f of patch.files) {
|
|
212
|
+
const key = f.path.toLowerCase();
|
|
213
|
+
const seen = lower.get(key);
|
|
214
|
+
if (seen !== undefined && seen !== f.path)
|
|
215
|
+
out.push(`${f.path}: differs from ${seen} only in case`);
|
|
216
|
+
lower.set(key, f.path);
|
|
217
|
+
if (!["A", "M", "D"].includes(f.status))
|
|
218
|
+
out.push(`${f.path}: a type change (${f.status})`);
|
|
219
|
+
if ([f.mode, f.baseMode].some((m) => m === "120000"))
|
|
220
|
+
out.push(`${f.path}: a symlink`);
|
|
221
|
+
if ([f.mode, f.baseMode].some((m) => m === "160000"))
|
|
222
|
+
out.push(`${f.path}: a submodule`);
|
|
223
|
+
if (f.status === "M" && f.mode !== f.baseMode)
|
|
224
|
+
out.push(`${f.path}: a mode change (${f.baseMode} → ${f.mode})`);
|
|
225
|
+
const target = join(root, f.path);
|
|
226
|
+
try {
|
|
227
|
+
if (lstatSync(target).isDirectory())
|
|
228
|
+
out.push(`${f.path}: a directory in the workspace`);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// absent — a creation, or already deleted
|
|
232
|
+
}
|
|
233
|
+
if (f.status === "A") {
|
|
234
|
+
// a new name whose case-folded twin already exists (macOS, Windows)
|
|
235
|
+
try {
|
|
236
|
+
const twin = readdirSync(dirname(target)).find((e) => e.toLowerCase() === basename(target).toLowerCase() && e !== basename(target));
|
|
237
|
+
if (twin !== undefined)
|
|
238
|
+
out.push(`${f.path}: the workspace has ${twin}, differing only in case`);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// the directory does not exist yet
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** git merge-file's algorithm on scratch copies — the index never read. */
|
|
248
|
+
function mergeThree(scratch, ours, base, theirs) {
|
|
249
|
+
const [o, b, t] = ["ours", "base", "theirs"].map((n) => join(scratch, n));
|
|
250
|
+
writeFileSync(o, ours);
|
|
251
|
+
writeFileSync(b, base);
|
|
252
|
+
writeFileSync(t, theirs);
|
|
253
|
+
const r = spawnSync("git", ["merge-file", "-p", "-L", "workspace", "-L", "base", "-L", "child", o, b, t], { maxBuffer: 256 * 1024 * 1024 });
|
|
254
|
+
if (r.status === null || r.status < 0)
|
|
255
|
+
throw new Error(`git merge-file failed: ${String(r.stderr)}`);
|
|
256
|
+
return { content: r.stdout, conflicts: r.status };
|
|
257
|
+
}
|
|
258
|
+
/** The conflicting hunks of a merged text, to name them. */
|
|
259
|
+
function hunks(merged) {
|
|
260
|
+
const lines = merged.toString("utf8").split("\n");
|
|
261
|
+
const out = [];
|
|
262
|
+
let inside = false;
|
|
263
|
+
for (const l of lines) {
|
|
264
|
+
if (l.startsWith("<<<<<<< "))
|
|
265
|
+
inside = true;
|
|
266
|
+
if (inside)
|
|
267
|
+
out.push(` ${l}`);
|
|
268
|
+
if (l.startsWith(">>>>>>> "))
|
|
269
|
+
inside = false;
|
|
270
|
+
if (out.length > 60) {
|
|
271
|
+
out.push(" …");
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out.join("\n");
|
|
276
|
+
}
|
|
277
|
+
/** One file, revalidated against PREPARE; null when published. */
|
|
278
|
+
function publish(target, p) {
|
|
279
|
+
const now = () => {
|
|
280
|
+
const b = readOrNull(target);
|
|
281
|
+
return b === null ? ABSENT : sha(b);
|
|
282
|
+
};
|
|
283
|
+
if (now() !== p.expected)
|
|
284
|
+
return "it changed after the merge was prepared";
|
|
285
|
+
if (p.action === "delete") {
|
|
286
|
+
unlinkSync(target);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
290
|
+
const tmp = join(dirname(target), `.${basename(target)}.kiso-apply-${process.pid}`);
|
|
291
|
+
writeFileSync(tmp, p.content);
|
|
292
|
+
if (p.action === "create") {
|
|
293
|
+
chmodSync(tmp, p.exec === true ? 0o755 : 0o644);
|
|
294
|
+
// WR-1A ②: link(2) or refusal — no-clobber, fail closed
|
|
295
|
+
try {
|
|
296
|
+
linkSync(tmp, target);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
return err.code === "EEXIST" ? "it was created in the workspace after the merge was prepared" : `it could not be created atomically (${err.code ?? "link failed"})`;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
rmSync(tmp, { force: true });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
chmodSync(tmp, statSync(target).mode & 0o7777);
|
|
307
|
+
// WR-1A ③: still what the merge was prepared against, right before the rename
|
|
308
|
+
if (now() !== p.expected) {
|
|
309
|
+
rmSync(tmp, { force: true });
|
|
310
|
+
return "it changed while the adoption was writing";
|
|
311
|
+
}
|
|
312
|
+
renameSync(tmp, target);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
/** The acceptance by its NAME, from the configuration (a trusted project's
|
|
316
|
+
* too, never asked here) — or why it cannot run. */
|
|
317
|
+
async function resolveAcceptance(acceptance, root) {
|
|
318
|
+
const user = loadUserConfig();
|
|
319
|
+
let project = null;
|
|
320
|
+
if (resolveProjectTrustPolicy(user ?? {}) !== "never") {
|
|
321
|
+
const artifacts = await projectArtifacts(root);
|
|
322
|
+
if (artifacts !== null && trustFor(artifacts.root, artifacts.digest)?.decision === "granted")
|
|
323
|
+
project = loadProjectConfig(root, true);
|
|
324
|
+
}
|
|
325
|
+
const merged = mergeConfigs(user, project);
|
|
326
|
+
if (acceptance.check !== undefined) {
|
|
327
|
+
const command = merged.checks?.[acceptance.check];
|
|
328
|
+
if (command === undefined)
|
|
329
|
+
return `the check "${acceptance.check}" is no longer configured`;
|
|
330
|
+
return { command: process.platform === "win32" ? "bash" : "/bin/sh", args: ["-c", command], label: { check: acceptance.check } };
|
|
331
|
+
}
|
|
332
|
+
if (acceptance.evaluator !== undefined) {
|
|
333
|
+
if (!(merged.evaluators ?? []).includes(acceptance.evaluator))
|
|
334
|
+
return `the evaluator ${acceptance.evaluator} is no longer listed`;
|
|
335
|
+
return { command: acceptance.evaluator, args: [root], label: { evaluator: acceptance.evaluator } };
|
|
336
|
+
}
|
|
337
|
+
return "the patch names no acceptance";
|
|
338
|
+
}
|
|
339
|
+
/** The check in the workspace — no git state touched, its tail kept. */
|
|
340
|
+
function runInWorkspace(r, root) {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
let out = "";
|
|
343
|
+
const child = spawn(r.command, [...r.args], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
344
|
+
const keep = (d) => {
|
|
345
|
+
out = (out + d.toString("utf8")).slice(-2_048);
|
|
346
|
+
};
|
|
347
|
+
child.stdout.on("data", keep);
|
|
348
|
+
child.stderr.on("data", keep);
|
|
349
|
+
child.on("error", (err) => resolve({ passed: false, exitCode: null, tail: err.message }));
|
|
350
|
+
child.on("exit", (code) => resolve({ passed: code === 0, exitCode: code, tail: out }));
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function readOrNull(path) {
|
|
354
|
+
try {
|
|
355
|
+
return readFileSync(path);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function readJournal(path) {
|
|
362
|
+
if (!existsSync(path))
|
|
363
|
+
return [];
|
|
364
|
+
return readFileSync(path, "utf8")
|
|
365
|
+
.split("\n")
|
|
366
|
+
.filter((l) => l.trim() !== "")
|
|
367
|
+
.flatMap((l) => {
|
|
368
|
+
try {
|
|
369
|
+
return [JSON.parse(l)];
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function git(cwd, args) {
|
|
377
|
+
const r = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
|
|
378
|
+
if (r.status !== 0)
|
|
379
|
+
throw new Error(r.stderr);
|
|
380
|
+
return r.stdout;
|
|
381
|
+
}
|
package/dist/builtin.js
CHANGED
|
@@ -25,7 +25,7 @@ import createSkills from "@vincemakes/kiso-skills-ext";
|
|
|
25
25
|
import createSubagent from "@vincemakes/kiso-subagent-ext";
|
|
26
26
|
import createAsk, {} from "@vincemakes/kiso-ask-ext";
|
|
27
27
|
import { getDontAsk } from "./mode.js";
|
|
28
|
-
import { tasksFor } from "./state.js";
|
|
28
|
+
import { bindingFor, codingToolOptions, tasksFor } from "./state.js";
|
|
29
29
|
/**
|
|
30
30
|
* 0.40.0 (the owner's dogfood): dontAsk never asks, so its tool table never
|
|
31
31
|
* offers ask_user — a model once put four questions to a dontAsk session
|
|
@@ -60,8 +60,9 @@ ask,
|
|
|
60
60
|
secretEnvNames = []) {
|
|
61
61
|
// ADR-0058 3d: the session's task manager makes `delegate` offer
|
|
62
62
|
// background children (explorer/reviewer; the cap and the turn budget
|
|
63
|
-
// are the extension's defaults)
|
|
64
|
-
|
|
63
|
+
// are the extension's defaults). 0.49.0 C1: the session's live binding
|
|
64
|
+
// is the model a child runs on when its task names none.
|
|
65
|
+
const all = await Promise.all([createMcp({ secretEnvNames }), createSkills(), createSubagent({ tasks: tasksFor, currentBinding: bindingFor, ...joinOption() }), ...(ask === undefined ? [] : [createAsk(ask).then(offInDontAsk)])]);
|
|
65
66
|
const shadowed = all.filter((b) => user.some((u) => u.name === b.name));
|
|
66
67
|
for (const s of shadowed) {
|
|
67
68
|
console.error(`[extensions] user extension "${s.name}" shadows the built-in — the built-in is not loaded`);
|
|
@@ -73,3 +74,9 @@ secretEnvNames = []) {
|
|
|
73
74
|
}
|
|
74
75
|
return all.filter((b) => !shadowed.includes(b));
|
|
75
76
|
}
|
|
77
|
+
/** 0.49.0 A: a reader delegation waits as long as a shell command does —
|
|
78
|
+
* one knob for "the foreground wait" (KISO_MIN_FOREGROUND_MS reaches both). */
|
|
79
|
+
function joinOption() {
|
|
80
|
+
const floor = codingToolOptions().limits?.minForegroundMs;
|
|
81
|
+
return floor !== undefined ? { joinMs: floor } : {};
|
|
82
|
+
}
|
package/dist/chat.d.ts
CHANGED
|
@@ -134,8 +134,8 @@ export declare function statusModelLabel(session: {
|
|
|
134
134
|
};
|
|
135
135
|
}): string;
|
|
136
136
|
/** ADR-0055 Amendment 2 (decision 5): with no stated window the status row
|
|
137
|
-
* shows `ctx ?`, yet the compaction tiers still assume the fallback (
|
|
138
|
-
*
|
|
137
|
+
* shows `ctx ?`, yet the compaction tiers still assume the fallback (200K;
|
|
138
|
+
* 128K from CW-1 batch 2 until the owner's 2026-10-09 ruling)
|
|
139
139
|
* — the assumption is said out loud once, at agent build. */
|
|
140
140
|
export declare function unknownWindowNotice(model: string): string;
|
|
141
141
|
/** CW-1 batch 2: said once, when an endpoint's refusal states its cap — the
|
package/dist/chat.js
CHANGED
|
@@ -9,7 +9,7 @@ import { escapeTerminal, cacheHitPct, decodeRate, palette, renderEvent, renderRe
|
|
|
9
9
|
import { askView, coldResumeLine, coldResumeView, deletionRiskHint, editFileHunksDiff, hunksOf, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
10
|
import { canonicalTargetPath, isProtectedPath, protectedIdentity, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
11
|
import { deliveryLines, liveTasks, lostToldOf, mergedConfig, queuedSwitchLines, setExitTasks, tasksFor, taskWhatOf } from "./state.js";
|
|
12
|
-
import { taskCounts, taskNoticeLines, taskNoticeRows, tasksForDisplay } from "./task-notice.js";
|
|
12
|
+
import { budgetSpentLine, taskCounts, taskNoticeLines, taskNoticeRows, tasksForDisplay } from "./task-notice.js";
|
|
13
13
|
import { current as inRunCompaction, takeKept as takeKeptCompaction } from "./in-run-compaction.js";
|
|
14
14
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
15
15
|
import { canonicalizeUsage, RunClosedError } from "@vincemakes/kiso-runtime";
|
|
@@ -27,11 +27,18 @@ import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertain
|
|
|
27
27
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
28
28
|
import { MODE_LABEL, OFFERED_MODES, getDontAsk, getMode, modeDisplay, setMode } from "./mode.js";
|
|
29
29
|
/** B area: default context window for the ~ctx estimate (config overridable).
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
30
|
+
* The figure a model nobody states a window for is assumed to hold.
|
|
31
|
+
* Registered models never reach it.
|
|
32
|
+
*
|
|
33
|
+
* 200,000 again since the owner's ruling of 2026-10-09, which reverses CW-1
|
|
34
|
+
* batch 2's 128,000. The same batch made an endpoint's refusal teach its
|
|
35
|
+
* real cap, kept across sessions and only ever lowered. So a figure too
|
|
36
|
+
* large costs one refused request per endpoint and model, once, while a
|
|
37
|
+
* figure too small compacts every session early and never corrects
|
|
38
|
+
* itself (gpt-6.1-sol, 272K and unregistered in 0.47.3, compacted from
|
|
39
|
+
* 64K). 200K, not more: a 128K model's soft tier (100K) still usually
|
|
40
|
+
* compacts it before its wall. */
|
|
41
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
35
42
|
/** The in-process fake provider's id, and the window we declare for it.
|
|
36
43
|
* Ours to state: faux is not a vendor's model, so "nobody published a
|
|
37
44
|
* window" — the reason a real model's capacity is unknown — cannot apply.
|
|
@@ -236,8 +243,8 @@ export function statusModelLabel(session) {
|
|
|
236
243
|
return effort !== undefined && effort !== "default" ? `${name} · ${effort}` : name;
|
|
237
244
|
}
|
|
238
245
|
/** ADR-0055 Amendment 2 (decision 5): with no stated window the status row
|
|
239
|
-
* shows `ctx ?`, yet the compaction tiers still assume the fallback (
|
|
240
|
-
*
|
|
246
|
+
* shows `ctx ?`, yet the compaction tiers still assume the fallback (200K;
|
|
247
|
+
* 128K from CW-1 batch 2 until the owner's 2026-10-09 ruling)
|
|
241
248
|
* — the assumption is said out loud once, at agent build. */
|
|
242
249
|
export function unknownWindowNotice(model) {
|
|
243
250
|
return `[kiso] context window unknown for ${model} at this endpoint — compaction assumes ${DEFAULT_CONTEXT_WINDOW / 1000}K; set contextWindow on the profile to state it`;
|
|
@@ -1771,6 +1778,11 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1771
1778
|
// the same chain a person's turn takes (taskWake: false keeps it for
|
|
1772
1779
|
// the next message). Released when this chat ends.
|
|
1773
1780
|
const taskManager = tasksFor(session.id);
|
|
1781
|
+
// 0.49.0 B: writers that ended while nobody listened are collected
|
|
1782
|
+
// BEFORE the delivery is built — so they arrive as anything that ended
|
|
1783
|
+
// meanwhile does, as a notify, never a wake
|
|
1784
|
+
if (taskManager !== undefined)
|
|
1785
|
+
await taskManager.collectPending();
|
|
1774
1786
|
// KISO_AUTO_DETACH_MS — the test rigs' knob (like KISO_STREAM_IDLE_MS):
|
|
1775
1787
|
// how old a command must be before a steer detaches it (3e D1). The
|
|
1776
1788
|
// rigs that hold a steer pending behind a long command raise it.
|
|
@@ -1779,6 +1791,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1779
1791
|
? () => { }
|
|
1780
1792
|
: session.useTasks(taskManager, {
|
|
1781
1793
|
wake: mergedConfig.taskWake !== false,
|
|
1794
|
+
...(mergedConfig.maxWakes !== undefined ? { maxWakes: mergedConfig.maxWakes } : {}),
|
|
1782
1795
|
onWake: (w) => queueTurn(w.content, w.via),
|
|
1783
1796
|
// Amendment 8: a loss is said when kiso concludes it — the
|
|
1784
1797
|
// model's notice still rides the next run, and is not said again
|
|
@@ -1789,6 +1802,8 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1789
1802
|
for (const line of taskNoticeLines(ids.map((taskId) => ({ taskId, transition: "unknown" })), taskWhatOf(session.id)))
|
|
1790
1803
|
body.notice(line);
|
|
1791
1804
|
},
|
|
1805
|
+
// 0480-F9: the chain went quiet on its budget — say so, once
|
|
1806
|
+
onBudgetSpent: (info) => body.notice(budgetSpentLine(info)),
|
|
1792
1807
|
...(Number.isFinite(autoDetachFromEnv) && autoDetachFromEnv >= 0 ? { autoDetachMinAgeMs: autoDetachFromEnv } : {}),
|
|
1793
1808
|
});
|
|
1794
1809
|
// 3e: a task's change repaints the row it is counted on
|
package/dist/child-result.d.ts
CHANGED
|
@@ -11,8 +11,16 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The answer is the child's own — the last assistant text in its session's
|
|
13
13
|
* projection, never its printed output — written atomically to `<r>`
|
|
14
|
-
* (result.md)
|
|
15
|
-
*
|
|
14
|
+
* (result.md), then the result record beside it (result.json), before the
|
|
15
|
+
* process exits: the runner records the exit after both exist.
|
|
16
|
+
*
|
|
17
|
+
* 0.49.0 (I5, I6): the record COMMITS the result — result.md is read only
|
|
18
|
+
* beside it — and it says everything the parent shows: how the run ended
|
|
19
|
+
* (`endedBy`, the main run's terminal: a wrap-up is a second run, and the
|
|
20
|
+
* parent never counts runs to learn it), the requests, the tool calls and
|
|
21
|
+
* usage over the committed events, the model it ran on, and a failure's
|
|
22
|
+
* error. Foreground children carry a result file too, so both ways of
|
|
23
|
+
* waiting read the same record.
|
|
16
24
|
*/
|
|
17
25
|
import type { AgentSession } from "@vincemakes/kiso-runtime";
|
|
18
26
|
export interface ChildEnd {
|
package/dist/child-result.js
CHANGED
|
@@ -11,17 +11,27 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The answer is the child's own — the last assistant text in its session's
|
|
13
13
|
* projection, never its printed output — written atomically to `<r>`
|
|
14
|
-
* (result.md)
|
|
15
|
-
*
|
|
14
|
+
* (result.md), then the result record beside it (result.json), before the
|
|
15
|
+
* process exits: the runner records the exit after both exist.
|
|
16
|
+
*
|
|
17
|
+
* 0.49.0 (I5, I6): the record COMMITS the result — result.md is read only
|
|
18
|
+
* beside it — and it says everything the parent shows: how the run ended
|
|
19
|
+
* (`endedBy`, the main run's terminal: a wrap-up is a second run, and the
|
|
20
|
+
* parent never counts runs to learn it), the requests, the tool calls and
|
|
21
|
+
* usage over the committed events, the model it ran on, and a failure's
|
|
22
|
+
* error. Foreground children carry a result file too, so both ways of
|
|
23
|
+
* waiting read the same record.
|
|
16
24
|
*/
|
|
17
25
|
import { renameSync, writeFileSync } from "node:fs";
|
|
18
26
|
import { dirname, join } from "node:path";
|
|
19
27
|
export const wrapUpInput = (budget) => `Your turn budget of ${budget} model requests is spent. Answer now from what you have found, without further tool calls. End with the UNRESOLVED section, naming what you did not get to.`;
|
|
20
28
|
export async function finishChild(session, end) {
|
|
21
|
-
const
|
|
29
|
+
const lastTerminal = () => {
|
|
22
30
|
const t = [...session.log.all].reverse().find((e) => e.type === "terminal");
|
|
23
|
-
return t === undefined ? undefined : t.outcome
|
|
31
|
+
return t === undefined ? undefined : t.outcome;
|
|
24
32
|
};
|
|
33
|
+
const lastOutcome = () => lastTerminal()?.kind;
|
|
34
|
+
const main = lastTerminal();
|
|
25
35
|
let outcome = lastOutcome() === "completed" ? "completed" : "failed";
|
|
26
36
|
if (end.maxTurns !== undefined && lastOutcome() === "max_turns") {
|
|
27
37
|
for await (const _ of session.run(wrapUpInput(end.maxTurns), { source: "system", maxTurns: 1 })) {
|
|
@@ -33,12 +43,49 @@ export async function finishChild(session, end) {
|
|
|
33
43
|
return;
|
|
34
44
|
const answer = lastAnswer(session);
|
|
35
45
|
const requests = session.log.all.filter((e) => e.type === "stop").length;
|
|
46
|
+
const events = committed(session.log.all);
|
|
47
|
+
const usages = events.filter((e) => e.type === "usage");
|
|
48
|
+
const sum = (k) => usages.reduce((n, e) => n + (typeof e[k] === "number" ? e[k] : 0), 0);
|
|
49
|
+
const error = outcome === "failed" ? errorOf(lastTerminal()) : undefined;
|
|
50
|
+
const record = {
|
|
51
|
+
outcome,
|
|
52
|
+
endedBy: main?.kind ?? "none",
|
|
53
|
+
requests,
|
|
54
|
+
...(end.maxTurns !== undefined ? { budget: end.maxTurns } : {}),
|
|
55
|
+
toolCalls: events.filter((e) => e.type === "tool_call_end").length,
|
|
56
|
+
usage: {
|
|
57
|
+
completedResponses: usages.length,
|
|
58
|
+
abandonedAttempts: session.log.all.filter((e) => e.type === "model_output_abandoned").length,
|
|
59
|
+
inputTokens: sum("inputTokens"),
|
|
60
|
+
outputTokens: sum("outputTokens"),
|
|
61
|
+
cacheRead: sum("cacheRead"),
|
|
62
|
+
},
|
|
63
|
+
model: session.model,
|
|
64
|
+
profile: session.profileName,
|
|
65
|
+
...(error !== undefined ? { error } : {}),
|
|
66
|
+
};
|
|
36
67
|
writeAtomic(end.resultFile, answer === "" ? "" : `${answer}\n`);
|
|
37
|
-
|
|
68
|
+
// I5: the record last — it commits the answer written above
|
|
69
|
+
writeAtomic(join(dirname(end.resultFile), "result.json"), `${JSON.stringify(record)}\n`);
|
|
38
70
|
// the runner's terminal is the exit: a failed child must not read as a clean one
|
|
39
71
|
if (outcome === "failed")
|
|
40
72
|
process.exitCode = 1;
|
|
41
73
|
}
|
|
74
|
+
/** The kernel's void scope (core `project.ts`): a `model_output_abandoned`
|
|
75
|
+
* marker voids (voidFromSeq, seq], an abandoned draft's events. A voided
|
|
76
|
+
* tool call never ran and a voided usage was never billed as an answer. */
|
|
77
|
+
function committed(events) {
|
|
78
|
+
const voids = events.filter((e) => e.type === "model_output_abandoned").map((e) => ({ from: e.voidFromSeq ?? -1, to: e.seq ?? -1 }));
|
|
79
|
+
return events.filter((e) => !voids.some((r) => (e.seq ?? -1) > r.from && (e.seq ?? -1) <= r.to));
|
|
80
|
+
}
|
|
81
|
+
/** A failed run's reason, one line: the kernel's error, or how it ended. */
|
|
82
|
+
function errorOf(t) {
|
|
83
|
+
if (t === undefined)
|
|
84
|
+
return "the run left no terminal";
|
|
85
|
+
if (t.kind === "error" && t.error !== undefined)
|
|
86
|
+
return [t.error.code, t.error.message].filter((s) => s !== undefined && s !== "").join(": ") || "error";
|
|
87
|
+
return `the run ended with ${t.kind}`;
|
|
88
|
+
}
|
|
42
89
|
/** The last assistant message's text in the session's projection. */
|
|
43
90
|
function lastAnswer(session) {
|
|
44
91
|
const messages = session.projected();
|
package/dist/coding-prompt.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/** The built-in prompt. Exported for scripts/request-surface.mjs — the
|
|
11
11
|
* model-side token-rent counter measures the REAL bytes, never a copy. */
|
|
12
|
-
export declare const SYSTEM_PROMPT = "You are kiso, a coding agent. You work in a workspace\ndirectory and change code with tools. Be concise: answer in a few lines\nunless the task genuinely needs more. Never claim a file was changed\nunless a tool confirmed it.\n\nWhat you can reach:\n- The workspace: read_file, list_dir, search_text, write_file, edit_file.\n- This machine and the network: shell \u2014 builds, tests, git, package\n managers, curl for HTTP, system queries. A request one command can\n answer is answered by running it.\n- The human: ask_user, for a decision that is theirs.\n\nTool discipline:\n- READ BEFORE YOU EDIT. For any file you are about to change, read it\n first \u2014 never guess its content.\n- Use edit_file for targeted changes and write_file for
|
|
12
|
+
export declare const SYSTEM_PROMPT = "You are kiso, a coding agent. You work in a workspace\ndirectory and change code with tools. Be concise: answer in a few lines\nunless the task genuinely needs more. Never claim a file was changed\nunless a tool confirmed it.\n\nWhat you can reach:\n- The workspace: read_file, list_dir, search_text, write_file, edit_file.\n- This machine and the network: shell \u2014 builds, tests, git, package\n managers, curl for HTTP, system queries. A request one command can\n answer is answered by running it.\n- The human: ask_user, for a decision that is theirs.\n\nTool discipline:\n- READ BEFORE YOU EDIT. For any file you are about to change, read it\n first \u2014 never guess its content.\n- Use edit_file for targeted changes and write_file for new files or\n full rewrites.\n- Be careful \u2014 shell has side effects and may take time.\n- search_text and list_dir are cheap \u2014 locate first, then read ranges\n with read_file offset/limit; never read a whole large file in one call.\n- Do not re-read a file you already read unchanged, or one you changed\n yourself through a confirmed edit \u2014 rely on the earlier result and\n on the change you just made.\n- When a tool fails, read the error and adjust; do not repeat the same\n call blindly.\n\nAn authorization covers what it NAMES: when the scope is not named \u2014\nwhich files, which branches, whether to push \u2014 ask before acting.\nDelivering, sharing or handing over changes means committing locally and\nstopping; pushing, publishing or sending happens only when the human\nnames it.\n\nWorkflow: understand the request, find the relevant code, make the\nsmallest change that works, then verify with a command (tests/build).\nReport what you did in one or two lines per change.";
|
|
13
13
|
/** Hard cap for injected instructions — truncate and say so. */
|
|
14
14
|
/**
|
|
15
15
|
* A area: read the FIRST present instruction file (AGENTS.md preferred) and
|
package/dist/coding-prompt.js
CHANGED
|
@@ -28,8 +28,8 @@ What you can reach:
|
|
|
28
28
|
Tool discipline:
|
|
29
29
|
- READ BEFORE YOU EDIT. For any file you are about to change, read it
|
|
30
30
|
first — never guess its content.
|
|
31
|
-
- Use edit_file for targeted changes and write_file for
|
|
32
|
-
|
|
31
|
+
- Use edit_file for targeted changes and write_file for new files or
|
|
32
|
+
full rewrites.
|
|
33
33
|
- Be careful — shell has side effects and may take time.
|
|
34
34
|
- search_text and list_dir are cheap — locate first, then read ranges
|
|
35
35
|
with read_file offset/limit; never read a whole large file in one call.
|
package/dist/config.d.ts
CHANGED
|
@@ -105,6 +105,9 @@ export interface KisoConfig {
|
|
|
105
105
|
/** ADR-0058 §8.4: false turns every task wake into a notice that waits
|
|
106
106
|
* for your next message. Default on. */
|
|
107
107
|
readonly taskWake?: boolean;
|
|
108
|
+
/** ADR-0058 Amendment 9: autonomous wakes allowed after one message of yours
|
|
109
|
+
* before a task's end only notifies. Default 20. */
|
|
110
|
+
readonly maxWakes?: number;
|
|
108
111
|
readonly projectTrust?: "ask" | "never";
|
|
109
112
|
/** DC-3 §3 rung 1, persisted. The terminal's light/dark, for terminals
|
|
110
113
|
* that answer neither `CSI ? 996 n` nor OSC 11. USER-LEVEL ONLY: a
|
|
@@ -135,6 +138,14 @@ export interface KisoConfig {
|
|
|
135
138
|
* unlisted path could be an interpreter running code the child wrote.
|
|
136
139
|
* Same layers as `checks`. */
|
|
137
140
|
readonly evaluators?: readonly string[];
|
|
141
|
+
/** 0.49.0 C1: which profile a delegated child runs on when its task
|
|
142
|
+
* names none — `{ "model": "<a profile in models>" }`. Unset, a child
|
|
143
|
+
* runs on the conversation's own profile. USER-LEVEL ONLY, like
|
|
144
|
+
* `models`: a project must never choose where your children's
|
|
145
|
+
* requests go. */
|
|
146
|
+
readonly subagents?: {
|
|
147
|
+
readonly model?: string;
|
|
148
|
+
};
|
|
138
149
|
}
|
|
139
150
|
/** The resolved, merged config — project wins over user, both validated. */
|
|
140
151
|
export interface ResolvedConfig {
|
package/dist/config.js
CHANGED
|
@@ -235,11 +235,32 @@ export function parseConfig(text, source) {
|
|
|
235
235
|
fail("taskWake", "expected true or false");
|
|
236
236
|
out.taskWake = obj.taskWake;
|
|
237
237
|
}
|
|
238
|
+
if (obj.maxWakes !== undefined) {
|
|
239
|
+
if (typeof obj.maxWakes !== "number" || !Number.isInteger(obj.maxWakes) || obj.maxWakes < 0)
|
|
240
|
+
fail("maxWakes", "expected a whole number, 0 or more");
|
|
241
|
+
out.maxWakes = obj.maxWakes;
|
|
242
|
+
}
|
|
238
243
|
if (obj.projectTrust !== undefined) {
|
|
239
244
|
if (obj.projectTrust !== "ask" && obj.projectTrust !== "never")
|
|
240
245
|
fail("projectTrust", 'expected "ask" or "never" (there is deliberately no "always")');
|
|
241
246
|
out.projectTrust = obj.projectTrust;
|
|
242
247
|
}
|
|
248
|
+
if (obj.subagents !== undefined) {
|
|
249
|
+
if (source.startsWith("<cwd>"))
|
|
250
|
+
fail("subagents", "belongs in the USER config — a project must never choose which model your delegated children run on");
|
|
251
|
+
if (obj.subagents === null || typeof obj.subagents !== "object" || Array.isArray(obj.subagents))
|
|
252
|
+
fail("subagents", 'expected an object, e.g. { "model": "<profile>" }');
|
|
253
|
+
const sub = obj.subagents;
|
|
254
|
+
if (sub.model !== undefined) {
|
|
255
|
+
if (typeof sub.model !== "string" || sub.model === "")
|
|
256
|
+
fail("subagents.model", "expected the name of a profile in models");
|
|
257
|
+
// both keys are user-level only, so the profile is in this file or nowhere
|
|
258
|
+
const names = Object.keys(out.models ?? {});
|
|
259
|
+
if (!names.includes(sub.model))
|
|
260
|
+
fail("subagents.model", `"${sub.model}" names no profile in models (configured: ${names.length > 0 ? names.join(", ") : "none"})`);
|
|
261
|
+
}
|
|
262
|
+
out.subagents = sub.model !== undefined ? { model: sub.model } : {};
|
|
263
|
+
}
|
|
243
264
|
return out;
|
|
244
265
|
}
|
|
245
266
|
function readConfigFile(path, source) {
|
|
@@ -312,12 +333,16 @@ export function mergeConfigs(user, project) {
|
|
|
312
333
|
...(p.autoCompact !== undefined ? { autoCompact: p.autoCompact } : {}),
|
|
313
334
|
...(u.taskWake !== undefined ? { taskWake: u.taskWake } : {}),
|
|
314
335
|
...(p.taskWake !== undefined ? { taskWake: p.taskWake } : {}),
|
|
336
|
+
...(u.maxWakes !== undefined ? { maxWakes: u.maxWakes } : {}),
|
|
337
|
+
...(p.maxWakes !== undefined ? { maxWakes: p.maxWakes } : {}),
|
|
315
338
|
...(u.projectTrust !== undefined ? { projectTrust: u.projectTrust } : {}),
|
|
316
339
|
...(p.projectTrust !== undefined ? { projectTrust: p.projectTrust } : {}),
|
|
317
340
|
// DT-1a: checks merge per name — a (trusted) project's check wins over the user's
|
|
318
341
|
...(u.checks !== undefined || p.checks !== undefined ? { checks: { ...(u.checks ?? {}), ...(p.checks ?? {}) } } : {}),
|
|
319
342
|
// CS-1: the evaluator lists join — either layer's script may be named
|
|
320
343
|
...(u.evaluators !== undefined || p.evaluators !== undefined ? { evaluators: [...(u.evaluators ?? []), ...(p.evaluators ?? [])] } : {}),
|
|
344
|
+
// 0.49.0 C1: user-level only — parseConfig refuses a project's
|
|
345
|
+
...(u.subagents !== undefined ? { subagents: u.subagents } : {}),
|
|
321
346
|
};
|
|
322
347
|
}
|
|
323
348
|
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
@@ -168,14 +168,11 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
168
168
|
settingsLayers.project = projectCfg;
|
|
169
169
|
settingsLayers.modelFlag = modelFlag;
|
|
170
170
|
const secretEnvNames = secretEnvNamesOf(merged.models ?? {});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// resolution owns them (a config profile only ever NAMES an env var for
|
|
177
|
-
// its key; the key itself never sits in a config file).
|
|
178
|
-
setMergedConfig(merged);
|
|
171
|
+
// SA-F1: set BEFORE the built-in layer loads. The subagent extension reads
|
|
172
|
+
// it once, at load, to build the delegate schema and its executor's
|
|
173
|
+
// snapshot; assigned after, it read an empty config (no `acceptance` or
|
|
174
|
+
// `model` field, a configured check refused, `subagents.model` ignored,
|
|
175
|
+
// project-layout children in the legacy sessions folder).
|
|
179
176
|
// DT-1a: what a delegated task may NAME — the configured checks and the
|
|
180
177
|
// model profiles — handed to the (in-process) subagent extension through
|
|
181
178
|
// the environment. A model never supplies a command; it names a check.
|
|
@@ -184,7 +181,22 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
184
181
|
// channel and not an exported KISO_SESSIONS_DIR: a variable in
|
|
185
182
|
// process.env would reach every shell child, and a kiso started from a
|
|
186
183
|
// shell tool would then write into this project's folder.
|
|
187
|
-
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({
|
|
184
|
+
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({
|
|
185
|
+
checks: merged.checks ?? {},
|
|
186
|
+
evaluators: merged.evaluators ?? [],
|
|
187
|
+
profiles: Object.keys(merged.models ?? {}),
|
|
188
|
+
sessionsDir: sessionsDir(),
|
|
189
|
+
// 0.49.0 C1: the profile a child runs on when its task names none
|
|
190
|
+
...(merged.subagents?.model !== undefined ? { subagentsModel: merged.subagents.model } : {}),
|
|
191
|
+
});
|
|
192
|
+
const builtIn = await builtInLayer(user, proj, input !== undefined && process.stdin.isTTY ? askUi(input) : undefined, secretEnvNames);
|
|
193
|
+
setExtensionLists(builtIn, user, proj, [...builtIn, ...user, ...proj]);
|
|
194
|
+
// merge round B — the config surface: user config + (trusted) project config,
|
|
195
|
+
// resolved with flags > env > project > user > default. The CLI never
|
|
196
|
+
// imports provider SDKs directly — the runtime's lazy provider
|
|
197
|
+
// resolution owns them (a config profile only ever NAMES an env var for
|
|
198
|
+
// its key; the key itself never sits in a config file).
|
|
199
|
+
setMergedConfig(merged);
|
|
188
200
|
setConfigModels(merged.models ?? {});
|
|
189
201
|
// CW-1 batch 2: the windows endpoints stated by refusing — read before the
|
|
190
202
|
// first window is asked for (the unknown-window notice below).
|
|
@@ -277,6 +289,11 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
277
289
|
// picked for it (the picker's fallback applied); the runtime reads it
|
|
278
290
|
// for a new session only — a resumed one keeps its own
|
|
279
291
|
...(() => {
|
|
292
|
+
// 0.49.0 C1: a delegated child on its conversation's profile runs at
|
|
293
|
+
// the conversation's effort, not at the one remembered for the profile
|
|
294
|
+
const inherited = childReasoning();
|
|
295
|
+
if (inherited !== null)
|
|
296
|
+
return { reasoning: inherited };
|
|
280
297
|
if (resolved === null || merged.models?.[resolved.name] !== resolved.profile)
|
|
281
298
|
return {};
|
|
282
299
|
const effort = startingEffort(resolved.profile, preferences().effort?.[resolved.name]);
|
|
@@ -370,3 +387,20 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
370
387
|
};
|
|
371
388
|
return createAgent(definition);
|
|
372
389
|
}
|
|
390
|
+
/** 0.49.0 C1: the effort the subagent extension hands a child running on its
|
|
391
|
+
* conversation's profile (KISO_CHILD_REASONING). Read only in a child — a
|
|
392
|
+
* top-level kiso ignores a stray variable — and only in its known shape. */
|
|
393
|
+
function childReasoning() {
|
|
394
|
+
if ((Number.parseInt(process.env.KISO_SUBAGENT_DEPTH ?? "0", 10) || 0) < 1)
|
|
395
|
+
return null;
|
|
396
|
+
const raw = process.env.KISO_CHILD_REASONING;
|
|
397
|
+
if (raw === undefined || raw === "")
|
|
398
|
+
return null;
|
|
399
|
+
try {
|
|
400
|
+
const r = JSON.parse(raw);
|
|
401
|
+
return typeof r.thinking === "string" && typeof r.effort === "string" ? { thinking: r.thinking, effort: r.effort } : null;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { type SessionRoute } from "./projects.js";
|
|
27
27
|
export { contextPolicyFromEnv } from "./create-coding-agent.js";
|
|
28
|
-
export { SYSTEM_PROMPT, composeSystemPrompt, readProjectInstructions } from "./coding-prompt.js";
|
|
28
|
+
export { SYSTEM_PROMPT, CODING_TOOL_RULES, composeSystemPrompt, readProjectInstructions } from "./coding-prompt.js";
|
|
29
29
|
export { applyProjectMerges } from "./trust-ui.js";
|
|
30
30
|
export { workspaceRoot } from "./state.js";
|
|
31
31
|
/**
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ import { browserCommand, launchCommand } from "./launch.js";
|
|
|
41
41
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
42
42
|
import { isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
|
|
43
43
|
import { OFFERED_MODES, applyModeState, envModeLayer, getDontAsk, modeDisplay, parseMode, resolveModeLayers } from "./mode.js";
|
|
44
|
-
import { activeStoreDir, setActiveStoreDir, agentModel, atFiles, body, bodyLog, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
|
|
44
|
+
import { activeStoreDir, setActiveStoreDir, agentModel, atFiles, body, bodyLog, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setLiveSession, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
|
|
45
45
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
46
46
|
import { barFor, chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
|
|
47
47
|
import { preferences, usePreferences } from "./preferences.js";
|
|
@@ -63,7 +63,7 @@ import { clipboardImage } from "./clipboard.js";
|
|
|
63
63
|
import { cardsFromListings } from "./session-cards.js";
|
|
64
64
|
import { createCodingAgent } from "./create-coding-agent.js";
|
|
65
65
|
export { contextPolicyFromEnv } from "./create-coding-agent.js";
|
|
66
|
-
export { SYSTEM_PROMPT, composeSystemPrompt, readProjectInstructions } from "./coding-prompt.js";
|
|
66
|
+
export { SYSTEM_PROMPT, CODING_TOOL_RULES, composeSystemPrompt, readProjectInstructions } from "./coding-prompt.js";
|
|
67
67
|
// The moved exports stay reachable from this entry — the test imports
|
|
68
68
|
// (project-trust, coding-agent) never change (B4: zero assertion changes).
|
|
69
69
|
export { applyProjectMerges } from "./trust-ui.js";
|
|
@@ -841,6 +841,8 @@ function bindRestoredSession(session) {
|
|
|
841
841
|
bodyLog(`binding changed — now on ${session.model}: ${ack.reasons.join("; ")}; reasoning reset to defaults${prior === "" ? "" : ` (was ${prior})`}`);
|
|
842
842
|
}
|
|
843
843
|
setAgentModel(session.model, session.baseUrl);
|
|
844
|
+
// 0.49.0 C1: the session a delegation reads its conversation's binding from
|
|
845
|
+
setLiveSession(session);
|
|
844
846
|
session.setMicrocompactThreshold(microcompactThresholdFor({
|
|
845
847
|
model: session.model,
|
|
846
848
|
...(session.baseUrl !== undefined ? { baseUrl: session.baseUrl } : {}),
|
|
@@ -1257,6 +1259,13 @@ async function main() {
|
|
|
1257
1259
|
// first createCodingAgent (the tier extensions read `current` live). The flag
|
|
1258
1260
|
// is stripped from the positional args, so it works in any position.
|
|
1259
1261
|
const args = process.argv.slice(2);
|
|
1262
|
+
// 0.49.0 B6.2: `kiso apply-patch <taskDir>` adopts a writer's collected
|
|
1263
|
+
// patch into the workspace — a one-shot effect, no session, no model
|
|
1264
|
+
if (args[0] === "apply-patch") {
|
|
1265
|
+
const { applyPatch } = await import("./apply-patch.js");
|
|
1266
|
+
process.exitCode = await applyPatch(args.slice(1));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1260
1269
|
// XP-1: --accept-drift (a flag, never an env var) authorizes opening a
|
|
1261
1270
|
// session whose recorded profile materially drifted — the
|
|
1262
1271
|
// acknowledgement is recorded as a new revision by the runtime.
|
|
@@ -1847,6 +1856,7 @@ async function main() {
|
|
|
1847
1856
|
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
1848
1857
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
1849
1858
|
" kiso sessions [--all|--current] list durable sessions (a terminal shows this workspace's by default)\n" +
|
|
1859
|
+
" kiso apply-patch <dir> adopt a subagent's collected patch (its handoff prints the command)\n" +
|
|
1850
1860
|
" kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
|
|
1851
1861
|
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
1852
1862
|
" kiso login --endpoint <url> a gateway: store its API key for that URL's origin only\n" +
|
package/dist/state.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SaferAnswer, type SessionCardView } from "@vincemakes/kiso-tui";
|
|
9
9
|
import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
|
|
10
10
|
import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
|
|
11
|
+
import type { ReasoningSetting } from "@vincemakes/kiso-runtime/internal";
|
|
11
12
|
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
12
13
|
import { type ShellTasks } from "@vincemakes/kiso-tools-node";
|
|
13
14
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
@@ -34,6 +35,9 @@ export declare function codingToolOptions(): {
|
|
|
34
35
|
readonly secretEnvNames: readonly string[];
|
|
35
36
|
readonly protectedFiles: readonly string[];
|
|
36
37
|
readonly tasks: (sessionId: string | undefined) => ShellTasks | undefined;
|
|
38
|
+
readonly limits?: {
|
|
39
|
+
readonly minForegroundMs: number;
|
|
40
|
+
};
|
|
37
41
|
};
|
|
38
42
|
/** Amendment 8: what a session's task ran — a lost task's transcript row
|
|
39
43
|
* names it. Undefined when the task or its journal cannot be read. */
|
|
@@ -41,6 +45,30 @@ export declare function taskWhatOf(sessionId: string | undefined): (taskId: stri
|
|
|
41
45
|
export declare function lostToldOf(sessionId: string): Set<string>;
|
|
42
46
|
/** The transcript lines a delivery shows: a loss already told is left out. */
|
|
43
47
|
export declare function deliveryLines(sessionId: string, items: readonly TaskDeliveryItem[]): string[];
|
|
48
|
+
/** 0.49.0 C1: the live sessions by id — what a delegation reads its
|
|
49
|
+
* conversation's binding from. Every entry registers its session
|
|
50
|
+
* (index.ts bindRestoredSession: fresh, resumed, switched, -p). */
|
|
51
|
+
interface LiveSession {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly model: string;
|
|
54
|
+
readonly baseUrl: string | undefined;
|
|
55
|
+
readonly profileName: string | null;
|
|
56
|
+
readonly reasoning: ReasoningSetting;
|
|
57
|
+
}
|
|
58
|
+
export declare function setLiveSession(session: LiveSession): void;
|
|
59
|
+
/** 0.49.0 C1: the conversation's live binding, for a child that names no
|
|
60
|
+
* model — the profile its session is bound to (the session's own record,
|
|
61
|
+
* never the display mark, which /reload and /resume clear), else the one
|
|
62
|
+
* configured profile with the session's (model, baseUrl), else none — and
|
|
63
|
+
* the effort the session runs at. */
|
|
64
|
+
export declare function bindingFor(sessionId: string | undefined): {
|
|
65
|
+
readonly profile: string | null;
|
|
66
|
+
readonly model: string;
|
|
67
|
+
readonly reasoning: ReasoningSetting;
|
|
68
|
+
} | null;
|
|
69
|
+
/** The one configured profile whose model and endpoint are these — null
|
|
70
|
+
* when none matches, or several do (a guess is never a binding). */
|
|
71
|
+
export declare function profileByIdentity(model: string, baseUrl: string | undefined): string | null;
|
|
44
72
|
export declare function tasksFor(sessionId: string | undefined): TaskManager | undefined;
|
|
45
73
|
export declare function setExitTasks(choice: "stop" | "leave"): void;
|
|
46
74
|
/** 3e: the live tasks of every session this process opened — a runner's
|
|
@@ -413,3 +441,4 @@ export declare function setLastBinding(b: LastBinding): void;
|
|
|
413
441
|
export declare function lastBinding(): LastBinding | null;
|
|
414
442
|
export declare function setAcceptDrift(v: boolean): void;
|
|
415
443
|
export declare function acceptDrift(): boolean;
|
|
444
|
+
export {};
|
package/dist/state.js
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { AT_CAP, AT_SKIP, Dock } from "@vincemakes/kiso-tui";
|
|
14
14
|
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
15
15
|
import { processTaskBackend } from "@vincemakes/kiso-tools-node";
|
|
16
|
+
import { collectWriter } from "@vincemakes/kiso-subagent-ext";
|
|
16
17
|
import { canonicalPath, LEGACY_SESSIONS_DIR, projectDirFor, projectLayoutActive } from "./projects.js";
|
|
17
18
|
import { taskNoticeLines, taskWhat } from "./task-notice.js";
|
|
18
19
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
@@ -42,7 +43,18 @@ export function secretEnvNamesOf(models) {
|
|
|
42
43
|
];
|
|
43
44
|
}
|
|
44
45
|
export function codingToolOptions() {
|
|
45
|
-
|
|
46
|
+
// KISO_MIN_FOREGROUND_MS — the test rigs' knob (like KISO_AUTO_DETACH_MS):
|
|
47
|
+
// the least foreground wait (ADR-0058 Amendment 10, default 60 s). The
|
|
48
|
+
// rigs that promote a command by a short wait lower it; nothing else does.
|
|
49
|
+
const minForeground = Number.parseInt(process.env.KISO_MIN_FOREGROUND_MS ?? "", 10);
|
|
50
|
+
return {
|
|
51
|
+
workspaceRoot: process.cwd(),
|
|
52
|
+
excludeRoots: [kisoHome()],
|
|
53
|
+
secretEnvNames: secretEnvNamesOf(configModels),
|
|
54
|
+
protectedFiles: protectedFiles(),
|
|
55
|
+
tasks: tasksFor,
|
|
56
|
+
...(Number.isFinite(minForeground) && minForeground >= 0 ? { limits: { minForegroundMs: minForeground } } : {}),
|
|
57
|
+
};
|
|
46
58
|
}
|
|
47
59
|
/** ADR-0058 (3b): one TaskManager per session, its directory beside the
|
|
48
60
|
* session's log (`<store dir>/<session>.tasks/`). A call without a session
|
|
@@ -77,13 +89,38 @@ export function deliveryLines(sessionId, items) {
|
|
|
77
89
|
const told = lostToldOf(sessionId);
|
|
78
90
|
return taskNoticeLines(items.filter((i) => !(i.transition === "unknown" && told.has(i.taskId))), taskWhatOf(sessionId));
|
|
79
91
|
}
|
|
92
|
+
const liveSessions = new Map();
|
|
93
|
+
export function setLiveSession(session) {
|
|
94
|
+
liveSessions.set(session.id, session);
|
|
95
|
+
}
|
|
96
|
+
/** 0.49.0 C1: the conversation's live binding, for a child that names no
|
|
97
|
+
* model — the profile its session is bound to (the session's own record,
|
|
98
|
+
* never the display mark, which /reload and /resume clear), else the one
|
|
99
|
+
* configured profile with the session's (model, baseUrl), else none — and
|
|
100
|
+
* the effort the session runs at. */
|
|
101
|
+
export function bindingFor(sessionId) {
|
|
102
|
+
const s = sessionId === undefined ? undefined : liveSessions.get(sessionId);
|
|
103
|
+
if (s === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
const profile = s.profileName !== null && configModels[s.profileName] !== undefined ? s.profileName : profileByIdentity(s.model, s.baseUrl);
|
|
106
|
+
return { profile, model: s.model, reasoning: s.reasoning };
|
|
107
|
+
}
|
|
108
|
+
/** The one configured profile whose model and endpoint are these — null
|
|
109
|
+
* when none matches, or several do (a guess is never a binding). */
|
|
110
|
+
export function profileByIdentity(model, baseUrl) {
|
|
111
|
+
const hits = Object.entries(configModels).filter(([, p]) => p.model === model && p.baseUrl === baseUrl);
|
|
112
|
+
return hits.length === 1 ? hits[0][0] : null;
|
|
113
|
+
}
|
|
80
114
|
export function tasksFor(sessionId) {
|
|
81
115
|
if (sessionId === undefined || activeStoreDir === "")
|
|
82
116
|
return undefined;
|
|
83
117
|
let manager = taskManagers.get(sessionId);
|
|
84
118
|
if (manager === undefined) {
|
|
85
119
|
taskBackend ??= processTaskBackend();
|
|
86
|
-
|
|
120
|
+
// 0.49.0 B: a writer's end is its collection — the subagent
|
|
121
|
+
// extension's collector, called once its process has ended
|
|
122
|
+
const created = new TaskManager({ root: join(activeStoreDir, `${sessionId}.tasks`), backend: taskBackend, collect: (info) => collectWriter(info, created) });
|
|
123
|
+
manager = created;
|
|
87
124
|
manager.observe();
|
|
88
125
|
taskManagers.set(sessionId, manager);
|
|
89
126
|
}
|
|
@@ -268,7 +305,10 @@ export function readContextLedger(sessionId, window) {
|
|
|
268
305
|
const turnSegments = manifest.filter((s) => s.role === "turn" || s.role === "current_turn");
|
|
269
306
|
return {
|
|
270
307
|
window,
|
|
271
|
-
|
|
308
|
+
// RG-F2: the generated tool table (system:tools) is system-prompt
|
|
309
|
+
// text the model reads on every request; it had no ledger line, so
|
|
310
|
+
// this row was short by it
|
|
311
|
+
systemPrompt: sum((s) => s === "system:base" || s === "system:tools" || (s.startsWith("system:ext:") && !isSkills(s))),
|
|
272
312
|
systemBase: sum((s) => s === "system:base"),
|
|
273
313
|
appends: count((s) => s.startsWith("system:ext:") && !isSkills(s)),
|
|
274
314
|
toolTable: sum((s) => s.startsWith("tool:")),
|
package/dist/task-notice.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ import type { NoticeMark } from "@vincemakes/kiso-tui";
|
|
|
7
7
|
* (<what>) — ` — the parenthesis only when the command is known — and the
|
|
8
8
|
* LAST ` — ` part opening with "it may still be running". One line each. */
|
|
9
9
|
export declare function taskNoticeLines(items: readonly TaskDeliveryItem[], what?: (taskId: string) => string | undefined): string[];
|
|
10
|
+
/** Finding 0480-F9: the chain budget is spent — said to the person once,
|
|
11
|
+
* so a chain that went quiet on its budget does not look idle. */
|
|
12
|
+
export declare function budgetSpentLine(info: {
|
|
13
|
+
readonly wakes: number;
|
|
14
|
+
}): string;
|
|
10
15
|
/** A task's state in the person's words. A stop that was asked for and is
|
|
11
16
|
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
12
17
|
* says so; a task kiso lost track of says so (Amendment 8). */
|
package/dist/task-notice.js
CHANGED
|
@@ -36,6 +36,11 @@ export function taskNoticeLines(items, what) {
|
|
|
36
36
|
}
|
|
37
37
|
return rows;
|
|
38
38
|
}
|
|
39
|
+
/** Finding 0480-F9: the chain budget is spent — said to the person once,
|
|
40
|
+
* so a chain that went quiet on its budget does not look idle. */
|
|
41
|
+
export function budgetSpentLine(info) {
|
|
42
|
+
return `✦ chain budget spent — ${info.wakes} autonomous wake${info.wakes === 1 ? "" : "s"} since your last message · finished tasks wait for you`;
|
|
43
|
+
}
|
|
39
44
|
/** A task's state in the person's words. A stop that was asked for and is
|
|
40
45
|
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
41
46
|
* says so; a task kiso lost track of says so (Amendment 8). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "kiso-code \u2014 the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,19 +18,19 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-ask-ext": "0.
|
|
22
|
-
"@vincemakes/kiso-core": "0.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.
|
|
31
|
-
"@vincemakes/kiso-tools-node": "0.
|
|
32
|
-
"@vincemakes/kiso-tui": "0.
|
|
33
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.49.0",
|
|
22
|
+
"@vincemakes/kiso-core": "0.49.0",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.49.0",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.49.0",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.49.0",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.49.0",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.49.0",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.49.0",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.49.0",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.49.0",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.49.0",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.49.0",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.49.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.1.2",
|