@vincemakes/kiso-code 0.1.47 → 0.1.49

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/chat.js CHANGED
@@ -443,6 +443,23 @@ submitTurn) {
443
443
  export async function chat(session, faux, input, autoCompact) {
444
444
  let currentRun = null;
445
445
  let cancelled = false;
446
+ // E group (the graceful-exit gate ③, R-G 0.1.48): the terminal can
447
+ // close MID-run — the stream 'end' fires while currentRun is set, so
448
+ // the EOT callback defers. eotSeen remembers it; each run's end
449
+ // re-evaluates the exit condition (the safe point), so the release
450
+ // always runs.
451
+ let eotSeen = false;
452
+ /** The exit condition, shared by the EOT callback and the deferred
453
+ * re-check: no pending ask, an empty line. currentRun is checked by
454
+ * the callers (the callback when 'end' fires; the run-end re-checks
455
+ * run only after currentRun was nulled). */
456
+ const exitAtEmptyPrompt = () => {
457
+ if (pendingAsk !== null || input.line() !== "")
458
+ return;
459
+ cancelled = true;
460
+ console.log("\n[exit requested]");
461
+ input.close();
462
+ };
446
463
  const turn = (text) => new Promise((resolve, reject) => {
447
464
  queued = Math.max(0, queued - 1); // a queued turn starts
448
465
  const run = session.run(text);
@@ -471,6 +488,11 @@ export async function chat(session, faux, input, autoCompact) {
471
488
  // chat to main's finally/catch, never an orphaned
472
489
  // unhandled rejection from the IIFE.
473
490
  failOnFauxExhaustion(last, faux, input);
491
+ // E group (the graceful-exit gate ③): the fd may have closed
492
+ // mid-run — the run's end is the safe point for the deferred
493
+ // exit, so the release always runs.
494
+ if (eotSeen)
495
+ exitAtEmptyPrompt();
474
496
  // round 8: after EVERY turn the prompt is re-armed — the human
475
497
  // never types blind after the first turn.
476
498
  input.prompt();
@@ -517,11 +539,11 @@ export async function chat(session, faux, input, autoCompact) {
517
539
  }
518
540
  });
519
541
  input.onEot(() => {
520
- if (!currentRun && pendingAsk === null && input.line() === "") {
521
- cancelled = true;
522
- console.log("\n[exit requested]");
523
- input.close();
524
- }
542
+ // E group (the graceful-exit gate ③): the 'end' may fire MID-run
543
+ // the exit defers to the run's end (the re-checks below).
544
+ eotSeen = true;
545
+ if (!currentRun)
546
+ exitAtEmptyPrompt();
525
547
  });
526
548
  input.onEscape(() => {
527
549
  if (currentRun) {
@@ -662,6 +684,10 @@ export async function chat(session, faux, input, autoCompact) {
662
684
  const last = await consumeRun(session, recoveryRun, input, turnNo, faux, statusCb, submitTurn);
663
685
  currentRun = null;
664
686
  failOnFauxExhaustion(last, faux, input);
687
+ // E group (the graceful-exit gate ③): the same deferred re-check
688
+ // as the turn path — the recovery run's end is also a safe point.
689
+ if (eotSeen)
690
+ exitAtEmptyPrompt();
665
691
  maybeAutoCompact(); // the ergonomics batch C8: the recovery run ended too — same check (awaited by the exit re-await)
666
692
  }
667
693
  if (cancelled) {
package/dist/index.d.ts CHANGED
@@ -23,6 +23,13 @@
23
23
  * makeAgent, and main.
24
24
  */
25
25
  export { applyProjectMerges } from "./trust-ui.js";
26
+ /**
27
+ * A area: the coding-agent system prompt — ONE constant, byte-stable for the
28
+ * session's lifetime (D area). Kept under ~80 lines; no template engine.
29
+ */
30
+ /** The built-in prompt. Exported for scripts/request-surface.mjs — the
31
+ * model-side token-rent counter measures the REAL bytes, never a copy. */
32
+ 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\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 full rewrites.\n Prefer many small edits over one large write.\n- shell is for commands: builds, tests, git, grep. Be careful \u2014 shell has\n side effects and may take time. Run one command at a time and inspect\n the output before continuing.\n- Batch independent tool calls into one reply \u2014 they run in parallel.\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 \u2014 rely on the earlier\n result.\n- When a tool fails, read the error and adjust; do not repeat the same\n call blindly.\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.";
26
33
  /**
27
34
  * A area: read the FIRST present instruction file (AGENTS.md preferred) and
28
35
  * return it as an injected section, or "" when none exists. Truncated at
package/dist/index.js CHANGED
@@ -128,6 +128,12 @@ function editorInput(editor) {
128
128
  },
129
129
  onEot(cb) {
130
130
  editor.onEot(cb);
131
+ // E group (the graceful-exit gate ③): a closed pty master is an
132
+ // EOF, not a \x04 byte — the editor's raw key loop never sees it.
133
+ // The stream's 'end' fires the same EOT callback; the chat/resume
134
+ // exit condition (no run, no panel, empty line) decides, so the
135
+ // exit sequence — and with it the lock release — runs.
136
+ process.stdin.on("end", () => cb());
131
137
  },
132
138
  onEscape(cb) {
133
139
  editor.onEscape(cb);
@@ -243,7 +249,9 @@ function recentSessions(id, agent) {
243
249
  * A area: the coding-agent system prompt — ONE constant, byte-stable for the
244
250
  * session's lifetime (D area). Kept under ~80 lines; no template engine.
245
251
  */
246
- const SYSTEM_PROMPT = `You are kiso, a coding agent. You work in a workspace
252
+ /** The built-in prompt. Exported for scripts/request-surface.mjs the
253
+ * model-side token-rent counter measures the REAL bytes, never a copy. */
254
+ export const SYSTEM_PROMPT = `You are kiso, a coding agent. You work in a workspace
247
255
  directory and change code with tools. Be concise: answer in a few lines
248
256
  unless the task genuinely needs more. Never claim a file was changed
249
257
  unless a tool confirmed it.
@@ -387,6 +395,15 @@ async function makeAgent(sessionId, input, modelFlag) {
387
395
  return createAgent(definition);
388
396
  }
389
397
  async function main() {
398
+ // E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
399
+ // turns the in-flight stdout/stderr writes into EIO, and node's
400
+ // unhandled 'error' event on the WriteStream kills the process —
401
+ // mid-exit the release never runs (the 60-byte residue the gate
402
+ // caught). The bytes are undeliverable anyway (the terminal is
403
+ // gone): the error must never abort the exit sequence. The listener
404
+ // swallows it — the standard "the stream may die under me" idiom.
405
+ process.stdout.on("error", () => { });
406
+ process.stderr.on("error", () => { });
390
407
  // Modes: --mode <name> wins over KISO_MODE — both applied before the
391
408
  // first makeAgent (the tier extensions read `current` live). The flag
392
409
  // is stripped from the positional args, so it works in any position.
@@ -459,7 +476,7 @@ async function main() {
459
476
  dock.enter();
460
477
  // E area: a resumed session continues the script at its durable
461
478
  // position — never restarts it (fauxSkip).
462
- const agent = await makeAgent(id, input, modelFlag);
479
+ agent = await makeAgent(id, input, modelFlag);
463
480
  applyConfigMode();
464
481
  const session = await agent.session({ id });
465
482
  bodyLog(`session ${id}\n`);
@@ -477,7 +494,7 @@ async function main() {
477
494
  // (argv = [node, script, resume, id, prompt?]).
478
495
  const prompt = process.argv[4];
479
496
  dock.enter();
480
- const agent = await makeAgent(arg, input, modelFlag);
497
+ agent = await makeAgent(arg, input, modelFlag);
481
498
  applyConfigMode();
482
499
  const session = await agent.session({ id: arg });
483
500
  faux = currentFaux;
@@ -485,7 +502,7 @@ async function main() {
485
502
  break;
486
503
  }
487
504
  case "sessions": {
488
- const agent = await makeAgent(undefined, undefined, modelFlag);
505
+ agent = await makeAgent(undefined, undefined, modelFlag);
489
506
  for (const meta of agent.sessions()) {
490
507
  console.log(renderSessionLine(meta));
491
508
  }
@@ -508,7 +525,7 @@ async function main() {
508
525
  // chat — the first argument is the session id.
509
526
  const id = command ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
510
527
  dock.enter();
511
- const agent = await makeAgent(id);
528
+ agent = await makeAgent(id);
512
529
  const session = await agent.session({ id });
513
530
  bodyLog(`session ${id}\n`);
514
531
  extensionsBanner(recentSessions(id, agent));
@@ -520,8 +537,12 @@ async function main() {
520
537
  finally {
521
538
  body.close(); // flush the pending frame, stop the heartbeat
522
539
  input.close();
523
- // E group: every normal and abnormal exit releases the fds and writer
524
- // locks no lock file is left behind.
540
+ // E group: a NORMAL exit releases the fds and writer locks — the
541
+ // empty released marker is left at the lock path (finding #5: the
542
+ // agent used to be shadowed here; the four branches assign the outer
543
+ // variable now). A signal death skips this and leaves the dead-pid
544
+ // residue — the dead-holder takeover recovers it by design
545
+ // (ADR-0050); the lock never outlives a live writer either way.
525
546
  agent?.close();
526
547
  // v2b: the dock tears down on EVERY exit path — CSI r resets the
527
548
  // scroll region, the cursor lands at the input line, no broken
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.1.47",
4
- "description": "kiso CLI the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
3
+ "version": "0.1.49",
4
+ "description": "kiso CLI \u2014 the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -20,13 +20,13 @@
20
20
  "dependencies": {
21
21
  "@vincemakes/kiso-core": "0.1.35",
22
22
  "@vincemakes/kiso-evals": "0.1.36",
23
- "@vincemakes/kiso-mcp-ext": "0.1.47",
23
+ "@vincemakes/kiso-mcp-ext": "0.1.49",
24
24
  "@vincemakes/kiso-provider-anthropic": "0.1.36",
25
25
  "@vincemakes/kiso-provider-openai": "0.1.36",
26
26
  "@vincemakes/kiso-runtime": "0.1.38",
27
- "@vincemakes/kiso-skills-ext": "0.1.47",
28
- "@vincemakes/kiso-subagent-ext": "0.1.47",
29
- "@vincemakes/kiso-task-ext": "0.1.47",
27
+ "@vincemakes/kiso-skills-ext": "0.1.49",
28
+ "@vincemakes/kiso-subagent-ext": "0.1.49",
29
+ "@vincemakes/kiso-task-ext": "0.1.49",
30
30
  "@vincemakes/kiso-tools-node": "0.1.36",
31
31
  "@vincemakes/kiso-tui": "0.1.42",
32
32
  "@vincemakes/kiso-tui-cells": "0.1.42"
package/dist/diff.d.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * v2e — the diff renderer: edit/write changes as inline ± lines, zero
3
- * dependencies, no syntax highlighting (the spec's scope line). Shown at
4
- * the approval moment ONLY — the frozen summary stays one line (v2d's
5
- * anti-leak principle), /last has the full data.
6
- *
7
- * edit_file diffs IN PLACE (the search→replace windows are known — no
8
- * general engine needed); write_file does a row-level LCS over the old
9
- * file (small files are the target). Context: 2 rows each side. The
10
- * RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
11
- * from the full diff.
12
- */
13
- /** The diff block's per-row kind. */
14
- export type DiffLine = {
15
- kind: "-" | "+" | " ";
16
- text: string;
17
- };
18
- export interface DiffResult {
19
- /** The FULL diff (with context, not truncated) — the display truncates. */
20
- lines: DiffLine[];
21
- added: number;
22
- removed: number;
23
- }
24
- /** The RENDERER's truncation: head + "… N lines (/last for full)" + tail. */
25
- export declare function truncateDiff(diff: DiffLine[]): DiffLine[];
26
- /** edit_file: the search→replace windows replace in place — the changed
27
- * region is KNOWN, so the diff is the old window vs the new window,
28
- * context from the surrounding file. */
29
- export declare function editFileDiff(oldContent: string, search: string, replace: string): DiffResult;
30
- /** write_file: a new file is all +; an existing file diffs row-level
31
- * against its old content. */
32
- export declare function writeFileDiff(oldContent: string | null, newContent: string): DiffResult;
package/dist/diff.js DELETED
@@ -1,122 +0,0 @@
1
- /**
2
- * v2e — the diff renderer: edit/write changes as inline ± lines, zero
3
- * dependencies, no syntax highlighting (the spec's scope line). Shown at
4
- * the approval moment ONLY — the frozen summary stays one line (v2d's
5
- * anti-leak principle), /last has the full data.
6
- *
7
- * edit_file diffs IN PLACE (the search→replace windows are known — no
8
- * general engine needed); write_file does a row-level LCS over the old
9
- * file (small files are the target). Context: 2 rows each side. The
10
- * RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
11
- * from the full diff.
12
- */
13
- /** A line-level LCS diff — the classic two-row DP, ~small inputs. */
14
- function lcsDiff(oldLines, newLines) {
15
- const n = oldLines.length;
16
- const m = newLines.length;
17
- const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
18
- for (let i = n - 1; i >= 0; i -= 1) {
19
- for (let j = m - 1; j >= 0; j -= 1) {
20
- dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
21
- }
22
- }
23
- const out = [];
24
- let i = 0;
25
- let j = 0;
26
- while (i < n && j < m) {
27
- if (oldLines[i] === newLines[j]) {
28
- out.push({ kind: " ", text: oldLines[i] });
29
- i += 1;
30
- j += 1;
31
- }
32
- else if (dp[i + 1][j] >= dp[i][j + 1]) {
33
- out.push({ kind: "-", text: oldLines[i] });
34
- i += 1;
35
- }
36
- else {
37
- out.push({ kind: "+", text: newLines[j] });
38
- j += 1;
39
- }
40
- }
41
- while (i < n) {
42
- out.push({ kind: "-", text: oldLines[i] });
43
- i += 1;
44
- }
45
- while (j < m) {
46
- out.push({ kind: "+", text: newLines[j] });
47
- j += 1;
48
- }
49
- return out;
50
- }
51
- /** Keep 2 context rows around each change — the unified-style window. */
52
- function withContext(diff) {
53
- const out = [];
54
- let lastAdded = -10;
55
- for (let k = 0; k < diff.length; k += 1) {
56
- if (diff[k].kind === " ")
57
- continue;
58
- const from = Math.max(0, k - 2);
59
- const to = Math.min(diff.length - 1, k + 2);
60
- for (let c = from; c <= to; c += 1) {
61
- if (c > lastAdded) {
62
- out.push(diff[c]);
63
- lastAdded = c;
64
- }
65
- }
66
- lastAdded = to;
67
- }
68
- return out;
69
- }
70
- const MAX_DIFF_LINES = 40; // the RENDERED cap
71
- const TRUNCATE_KEEP = 18;
72
- /** The RENDERER's truncation: head + "… N lines (/last for full)" + tail. */
73
- export function truncateDiff(diff) {
74
- if (diff.length <= MAX_DIFF_LINES)
75
- return diff;
76
- const omitted = diff.length - 2 * TRUNCATE_KEEP;
77
- return [
78
- ...diff.slice(0, TRUNCATE_KEEP),
79
- { kind: " ", text: `… ${omitted} lines (/last for full)` },
80
- ...diff.slice(diff.length - TRUNCATE_KEEP),
81
- ];
82
- }
83
- function stats(diff) {
84
- let added = 0;
85
- let removed = 0;
86
- for (const d of diff) {
87
- if (d.kind === "+")
88
- added += 1;
89
- else if (d.kind === "-")
90
- removed += 1;
91
- }
92
- return { added, removed };
93
- }
94
- /** edit_file: the search→replace windows replace in place — the changed
95
- * region is KNOWN, so the diff is the old window vs the new window,
96
- * context from the surrounding file. */
97
- export function editFileDiff(oldContent, search, replace) {
98
- const oldLines = oldContent.split("\n");
99
- const searchLines = search.split("\n");
100
- const replaceLines = replace.split("\n");
101
- // Locate the search window (the first occurrence — the edit tool's own
102
- // semantics); no occurrence → the whole file is the old side.
103
- let at = -1;
104
- for (let i = 0; i + searchLines.length <= oldLines.length; i += 1) {
105
- if (oldLines.slice(i, i + searchLines.length).join("\n") === search) {
106
- at = i;
107
- break;
108
- }
109
- }
110
- const lines = at < 0 ? withContext(lcsDiff(oldLines, replaceLines)) : withContext(lcsDiff(oldLines, [...oldLines.slice(0, at), ...replaceLines, ...oldLines.slice(at + searchLines.length)]));
111
- return { lines, ...stats(lines) };
112
- }
113
- /** write_file: a new file is all +; an existing file diffs row-level
114
- * against its old content. */
115
- export function writeFileDiff(oldContent, newContent) {
116
- if (oldContent === null) {
117
- const lines = newContent.split("\n").map((text) => ({ kind: "+", text }));
118
- return { lines, added: lines.length, removed: 0 };
119
- }
120
- const lines = withContext(lcsDiff(oldContent.split("\n"), newContent.split("\n")));
121
- return { lines, ...stats(lines) };
122
- }