@vincemakes/kiso-code 0.46.0 → 0.46.2
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 +16 -6
- package/dist/config.d.ts +7 -3
- package/dist/config.js +39 -4
- package/dist/create-coding-agent.js +15 -4
- package/dist/dispatch.js +15 -9
- package/dist/floor.d.ts +21 -0
- package/dist/floor.js +35 -35
- package/dist/index.js +8 -7
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.js +34 -3
- package/dist/protected-writes.d.ts +8 -0
- package/dist/protected-writes.js +12 -0
- package/dist/remote-boundary.d.ts +55 -0
- package/dist/remote-boundary.js +217 -0
- package/dist/replay.d.ts +1 -1
- package/dist/replay.js +8 -7
- package/dist/state.d.ts +7 -3
- package/dist/state.js +29 -3
- package/dist/task-notice.d.ts +15 -7
- package/dist/task-notice.js +51 -14
- package/package.json +14 -14
package/dist/chat.js
CHANGED
|
@@ -8,8 +8,8 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
8
8
|
import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, STATUS_GLYPHS, kUnit, namedPickView, } from "@vincemakes/kiso-tui";
|
|
9
9
|
import { askView, coldResumeLine, coldResumeView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
10
|
import { canonicalTargetPath, isProtectedPath, protectedIdentity, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
|
-
import { liveTasks, mergedConfig, queuedSwitchLines,
|
|
12
|
-
import { taskCounts,
|
|
11
|
+
import { deliveryLines, liveTasks, lostToldOf, mergedConfig, queuedSwitchLines, setExitTasks, tasksFor, taskWhatOf } from "./state.js";
|
|
12
|
+
import { taskCounts, taskNoticeLines } from "./task-notice.js";
|
|
13
13
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
14
14
|
import { canonicalizeUsage, RunClosedError } from "@vincemakes/kiso-runtime";
|
|
15
15
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
@@ -953,7 +953,8 @@ onInputLanded) {
|
|
|
953
953
|
// any run that carries one, as replay.ts does for old logs.
|
|
954
954
|
// ADR-0058: a task notice is a row, never the person's chip
|
|
955
955
|
if (ev.via?.kind === "tasks") {
|
|
956
|
-
|
|
956
|
+
for (const row of deliveryLines(session.id, ev.via.items))
|
|
957
|
+
body.notice(row);
|
|
957
958
|
break;
|
|
958
959
|
}
|
|
959
960
|
if (ev.source === "system") {
|
|
@@ -1536,7 +1537,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1536
1537
|
const manager = tasksFor(session.id);
|
|
1537
1538
|
if (manager === undefined)
|
|
1538
1539
|
return undefined;
|
|
1539
|
-
if (fresh || taskRow === null || Date.now() - taskRow.at > 1_000
|
|
1540
|
+
if (fresh || taskRow === null || Date.now() - taskRow.at > 1_000) {
|
|
1540
1541
|
let list = [];
|
|
1541
1542
|
try {
|
|
1542
1543
|
list = manager.list();
|
|
@@ -1544,7 +1545,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1544
1545
|
catch {
|
|
1545
1546
|
// a corrupt journal is reported where it is read on purpose
|
|
1546
1547
|
}
|
|
1547
|
-
taskRow = { at: Date.now(),
|
|
1548
|
+
taskRow = { at: Date.now(), counts: taskCounts(list) };
|
|
1548
1549
|
}
|
|
1549
1550
|
return taskRow.counts;
|
|
1550
1551
|
};
|
|
@@ -1621,7 +1622,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1621
1622
|
spentUsd = (spentUsd ?? 0) + usd;
|
|
1622
1623
|
};
|
|
1623
1624
|
const queueTurn = (line, via) => {
|
|
1624
|
-
const slot = { line: via?.kind === "skill" ? via.line : via?.kind === "tasks" ?
|
|
1625
|
+
const slot = { line: via?.kind === "skill" ? via.line : via?.kind === "tasks" ? (deliveryLines(session.id, via.items).join(" · ") || taskNoticeLines(via.items, taskWhatOf(session.id)).join(" · ")) : line, content: line, ...(via !== undefined ? { via } : {}), cancelled: false };
|
|
1625
1626
|
pendingTurns.push(slot);
|
|
1626
1627
|
queued += 1;
|
|
1627
1628
|
chainRef.current = chainRef.current.then(async () => {
|
|
@@ -1706,6 +1707,15 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1706
1707
|
: session.useTasks(taskManager, {
|
|
1707
1708
|
wake: mergedConfig.taskWake !== false,
|
|
1708
1709
|
onWake: (w) => queueTurn(w.content, w.via),
|
|
1710
|
+
// Amendment 8: a loss is said when kiso concludes it — the
|
|
1711
|
+
// model's notice still rides the next run, and is not said again
|
|
1712
|
+
onLost: (ids) => {
|
|
1713
|
+
const told = lostToldOf(session.id);
|
|
1714
|
+
for (const id of ids)
|
|
1715
|
+
told.add(id);
|
|
1716
|
+
for (const line of taskNoticeLines(ids.map((taskId) => ({ taskId, transition: "unknown" })), taskWhatOf(session.id)))
|
|
1717
|
+
body.notice(line);
|
|
1718
|
+
},
|
|
1709
1719
|
...(Number.isFinite(autoDetachFromEnv) && autoDetachFromEnv >= 0 ? { autoDetachMinAgeMs: autoDetachFromEnv } : {}),
|
|
1710
1720
|
});
|
|
1711
1721
|
// 3e: a task's change repaints the row it is counted on
|
package/dist/config.d.ts
CHANGED
|
@@ -151,7 +151,8 @@ export declare function loadUserConfig(): KisoConfig | null;
|
|
|
151
151
|
/** The project config — read ONLY when the project's .kiso passed the E3
|
|
152
152
|
* trust gate (an untrusted project's config is never even read). */
|
|
153
153
|
export declare function loadProjectConfig(cwd: string, trusted: boolean): KisoConfig | null;
|
|
154
|
-
/** Merge: project
|
|
154
|
+
/** Merge: a project's keys win over the user's (each layer's own keys
|
|
155
|
+
* only) — except that a project may only tighten (assertProjectTightens). */
|
|
155
156
|
export declare function mergeConfigs(user: KisoConfig | null, project: KisoConfig | null): KisoConfig;
|
|
156
157
|
/** What a profile's sign-in actually IS. The two shapes are not
|
|
157
158
|
* interchangeable: an API key is a string an adapter holds, an OAuth
|
|
@@ -210,6 +211,9 @@ export declare function resolveContextWindow(merged: KisoConfig, profile?: Model
|
|
|
210
211
|
/** Auto-compact: env (KISO_AUTO_COMPACT) > config.autoCompact > off. An
|
|
211
212
|
* invalid env value is OFF (it set the env → it wins, and it is invalid). */
|
|
212
213
|
export declare function resolveAutoCompact(merged: KisoConfig): AutoCompactConfig | undefined;
|
|
213
|
-
/** Project-trust policy: config
|
|
214
|
-
*
|
|
214
|
+
/** Project-trust policy: the USER config's projectTrust — "ask" (the E3
|
|
215
|
+
* gate, default) or "never" (the gate auto-refuses). The gate passes
|
|
216
|
+
* loadUserConfig() alone (trust-ui.ts): a project never relaxes its own
|
|
217
|
+
* gate, and it could not anyway, since an untrusted project's config is
|
|
218
|
+
* never read. */
|
|
215
219
|
export declare function resolveProjectTrustPolicy(merged: KisoConfig): "ask" | "never";
|
package/dist/config.js
CHANGED
|
@@ -153,6 +153,10 @@ export function parseConfig(text, source) {
|
|
|
153
153
|
out.model = obj.model;
|
|
154
154
|
}
|
|
155
155
|
if (obj.models !== undefined) {
|
|
156
|
+
// 0.46.2: a profile names where requests go and which of the
|
|
157
|
+
// person's env vars is the key — a repository must never choose that
|
|
158
|
+
if (source.startsWith("<cwd>"))
|
|
159
|
+
fail("models", 'belongs in the USER config — a project may pick one of your profiles with "model", never define one');
|
|
156
160
|
if (obj.models === null || typeof obj.models !== "object" || Array.isArray(obj.models))
|
|
157
161
|
fail("models", "expected an object of profiles");
|
|
158
162
|
const models = {};
|
|
@@ -261,15 +265,43 @@ export function loadProjectConfig(cwd, trusted) {
|
|
|
261
265
|
return null;
|
|
262
266
|
return readConfigFile(join(cwd, ".kiso", "config.json"), "<cwd>/.kiso/config.json");
|
|
263
267
|
}
|
|
264
|
-
/**
|
|
268
|
+
/** 0.46.2 — how much a tier lets run without a person, strictest first;
|
|
269
|
+
* `manual` sits with `default` (both ask). */
|
|
270
|
+
const MODE_LOOSENESS = { plan: 0, manual: 1, default: 1, "accept-edits": 2, "full-access": 3 };
|
|
271
|
+
/**
|
|
272
|
+
* 0.46.2 — a repository's config may only make kiso STRICTER. A project
|
|
273
|
+
* mode looser than the user level (the user config's mode, else
|
|
274
|
+
* `default`) is refused, and a project may turn don't-ask on but never off
|
|
275
|
+
* — including the switch the old `dontAsk` mode spelling carries. Flags
|
|
276
|
+
* and env are the person's own choice and are not judged here.
|
|
277
|
+
*/
|
|
278
|
+
function assertProjectTightens(u, p) {
|
|
279
|
+
const where = "config <cwd>/.kiso/config.json";
|
|
280
|
+
const base = parseMode(u.mode ?? "default");
|
|
281
|
+
const proj = p.mode !== undefined ? parseMode(p.mode) : undefined;
|
|
282
|
+
if (proj !== undefined && base !== undefined && MODE_LOOSENESS[proj.mode] > MODE_LOOSENESS[base.mode]) {
|
|
283
|
+
const than = u.mode !== undefined ? `your config's "${u.mode}"` : "the default";
|
|
284
|
+
throw new ConfigError(`${where}: mode — "${p.mode}" is looser than ${than}: a project can make the mode stricter, never looser`);
|
|
285
|
+
}
|
|
286
|
+
if (p.dontAsk === false) {
|
|
287
|
+
throw new ConfigError(`${where}: dontAsk — a project can turn don't-ask on, never off`);
|
|
288
|
+
}
|
|
289
|
+
if (base?.dontAsk === true && proj !== undefined && proj.dontAsk !== true && p.dontAsk !== true) {
|
|
290
|
+
throw new ConfigError(`${where}: mode — "${p.mode}" would turn off the don't-ask switch your config's "${u.mode}" carries: a project can turn don't-ask on, never off`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** Merge: a project's keys win over the user's (each layer's own keys
|
|
294
|
+
* only) — except that a project may only tighten (assertProjectTightens). */
|
|
265
295
|
export function mergeConfigs(user, project) {
|
|
266
296
|
const u = user ?? {};
|
|
267
297
|
const p = project ?? {};
|
|
298
|
+
assertProjectTightens(u, p);
|
|
268
299
|
return {
|
|
269
300
|
...(u.model !== undefined ? { model: u.model } : {}),
|
|
270
301
|
...(p.model !== undefined ? { model: p.model } : {}),
|
|
302
|
+
// 0.46.2: profiles come from the user config only — parseConfig
|
|
303
|
+
// refuses a project's `models`, so there is nothing here to merge
|
|
271
304
|
...(u.models !== undefined ? { models: u.models } : {}),
|
|
272
|
-
...(p.models !== undefined ? { models: { ...u.models, ...p.models } } : u.models !== undefined ? { models: u.models } : {}),
|
|
273
305
|
...(u.mode !== undefined ? { mode: u.mode } : {}),
|
|
274
306
|
...(p.mode !== undefined ? { mode: p.mode } : {}),
|
|
275
307
|
...(u.dontAsk !== undefined ? { dontAsk: u.dontAsk } : {}),
|
|
@@ -521,8 +553,11 @@ export function resolveAutoCompact(merged) {
|
|
|
521
553
|
}
|
|
522
554
|
return merged.autoCompact;
|
|
523
555
|
}
|
|
524
|
-
/** Project-trust policy: config
|
|
525
|
-
*
|
|
556
|
+
/** Project-trust policy: the USER config's projectTrust — "ask" (the E3
|
|
557
|
+
* gate, default) or "never" (the gate auto-refuses). The gate passes
|
|
558
|
+
* loadUserConfig() alone (trust-ui.ts): a project never relaxes its own
|
|
559
|
+
* gate, and it could not anyway, since an untrusted project's config is
|
|
560
|
+
* never read. */
|
|
526
561
|
export function resolveProjectTrustPolicy(merged) {
|
|
527
562
|
return merged.projectTrust ?? "ask";
|
|
528
563
|
}
|
|
@@ -18,8 +18,8 @@ import { builtInLayer } from "./builtin.js";
|
|
|
18
18
|
import { recordLearnedWindow, useLearnedWindows } from "./learned-windows.js";
|
|
19
19
|
import { compactionDiscardedNotice, contextWindowTokens, knownContextWindow, unknownWindowNotice, windowLearnedNotice } from "./chat.js";
|
|
20
20
|
import {} from "@vincemakes/kiso-core";
|
|
21
|
-
import { guardSavedAllow,
|
|
22
|
-
import { floorExtension
|
|
21
|
+
import { guardSavedAllow, neverInheritedCall } from "./protected-writes.js";
|
|
22
|
+
import { floorExtension } from "./floor.js";
|
|
23
23
|
import { protectedShellExtension } from "./protected-shell.js";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { breakerExtension } from "./breaker.js";
|
|
@@ -34,6 +34,7 @@ import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
|
34
34
|
import { sweepStaleMergeDirs } from "./temp-sweep.js";
|
|
35
35
|
import { CODING_TOOL_RULES, composeSystemPrompt } from "./coding-prompt.js";
|
|
36
36
|
import { createAgent, loadExtensions, loadProjectExtensions, SessionStore } from "@vincemakes/kiso-runtime";
|
|
37
|
+
import { preferences, startingEffort } from "./preferences.js";
|
|
37
38
|
/** LT-1: the stream watchdog's bound from the environment — a non-negative
|
|
38
39
|
* number of milliseconds (0 disables it); anything else is ignored. The
|
|
39
40
|
* PTY rigs use it to trip the watchdog in seconds against a stub that
|
|
@@ -229,9 +230,10 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
229
230
|
// 0.40.0: the read-only shell allow sits after the tiers — an allow from
|
|
230
231
|
// it outranks a tier's ask and names itself in decidedBy.
|
|
231
232
|
// 0.40.0: a saved allow never carries a write into .git/ or .kiso/, nor
|
|
232
|
-
// a destructive shell command.
|
|
233
|
+
// a destructive shell command; 0.46.2: nor a command across the remote
|
|
234
|
+
// boundary (remote-boundary.ts).
|
|
233
235
|
const workspaceRoot = () => codingToolOptions().workspaceRoot;
|
|
234
|
-
const neverInherited = (call) =>
|
|
236
|
+
const neverInherited = (call) => neverInheritedCall(call, workspaceRoot());
|
|
235
237
|
setNeverInherited(neverInherited);
|
|
236
238
|
// 0.40.0: the catastrophe floor, at the chain's HEAD — a deny there
|
|
237
239
|
// names itself in decidedBy and outranks every tier, bypass included.
|
|
@@ -265,6 +267,15 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
265
267
|
// config profile — a direct provider/model or an env key names none.
|
|
266
268
|
workspace: workspaceRoot(),
|
|
267
269
|
...(resolved !== null && merged.models?.[resolved.name] === resolved.profile ? { profileName: resolved.name } : {}),
|
|
270
|
+
// B1: a NEW session of a config profile starts at the effort last
|
|
271
|
+
// picked for it (the picker's fallback applied); the runtime reads it
|
|
272
|
+
// for a new session only — a resumed one keeps its own
|
|
273
|
+
...(() => {
|
|
274
|
+
if (resolved === null || merged.models?.[resolved.name] !== resolved.profile)
|
|
275
|
+
return {};
|
|
276
|
+
const effort = startingEffort(resolved.profile, preferences().effort?.[resolved.name]);
|
|
277
|
+
return effort === null ? {} : { reasoning: { thinking: "default", effort } };
|
|
278
|
+
})(),
|
|
268
279
|
// Area 5: the coding tools are bound to the workspace — every path
|
|
269
280
|
// they touch is canonicalized inside cwd, escapes are refused.
|
|
270
281
|
tools: [...createCodingTools(codingToolOptions())], // DC-49 — the options live in state.ts, shared with the `!` command's runner
|
package/dist/dispatch.js
CHANGED
|
@@ -13,12 +13,12 @@ import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelNa
|
|
|
13
13
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
14
14
|
import { profileProviderLabel, providerLabel } from "./provider-label.js";
|
|
15
15
|
import { installedVersion, versionStatusLine } from "./stale-version.js";
|
|
16
|
-
import { preferences, setPreference } from "./preferences.js";
|
|
16
|
+
import { preferences, rememberEffort, setPreference } from "./preferences.js";
|
|
17
17
|
import { settingsRows } from "./settings.js";
|
|
18
18
|
import { floorOn, settingsLayers } from "./state.js";
|
|
19
19
|
import { currentGround } from "@vincemakes/kiso-tui-cells/render";
|
|
20
|
-
import { queuedSwitchLines,
|
|
21
|
-
import { taskOutput, taskRow, taskStateLabel } from "./task-notice.js";
|
|
20
|
+
import { queuedSwitchLines, tasksFor } from "./state.js";
|
|
21
|
+
import { lostReason, taskOutput, taskRow, taskStateLabel } from "./task-notice.js";
|
|
22
22
|
import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
|
|
23
23
|
import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
|
|
24
24
|
import { shellTool } from "@vincemakes/kiso-tools-node";
|
|
@@ -98,7 +98,7 @@ function effortAxis(p) {
|
|
|
98
98
|
// the sentence the coordination note asked for: said only when the
|
|
99
99
|
// cursor could NOT land where the previous selection asked.
|
|
100
100
|
...(carried !== undefined && carried !== "default" && carriedIdx < 0 && defaultIdx >= 0
|
|
101
|
-
? { levelNote: `effort ${carried} \u2192 ${levels[defaultIdx]}:
|
|
101
|
+
? { levelNote: `effort ${carried} \u2192 ${levels[defaultIdx]}: this model's default (it has no ${carried})` }
|
|
102
102
|
: {}),
|
|
103
103
|
};
|
|
104
104
|
}
|
|
@@ -867,6 +867,11 @@ export function dispatch(line, ctx) {
|
|
|
867
867
|
// §2.5: the ONE source a reload reads for the model — a
|
|
868
868
|
// switch made here must survive the rebuild.
|
|
869
869
|
setModelChoice(arg);
|
|
870
|
+
// B1: an explicit effort (typed, or the panel's level) is
|
|
871
|
+
// remembered for the profile — where its next NEW session
|
|
872
|
+
// starts (preferences.json)
|
|
873
|
+
if (direct === null && effortTok !== undefined && reasoning !== undefined)
|
|
874
|
+
rememberEffort(profName, effortTok);
|
|
870
875
|
// The window a PROFILE states travels with the switch,
|
|
871
876
|
// like the model id and the endpoint. Leaving it behind
|
|
872
877
|
// is CTX-1 one field over: the row would show a
|
|
@@ -1250,8 +1255,8 @@ export function dispatch(line, ctx) {
|
|
|
1250
1255
|
/** ADR-0058 (3e) — `/tasks`, `/tasks show <id>`, `/tasks stop <id>`. On a
|
|
1251
1256
|
* dock the bare command is a pick: a task, then what to do with it. A
|
|
1252
1257
|
* stop is REQUESTED here; the row says `stopping` until the journal says
|
|
1253
|
-
* how it ended.
|
|
1254
|
-
*
|
|
1258
|
+
* how it ended. A task kiso lost track of says why in `show`
|
|
1259
|
+
* (Amendment 8). */
|
|
1255
1260
|
async function tasksCommand(arg, ctx) {
|
|
1256
1261
|
const manager = tasksFor(ctx.session.id);
|
|
1257
1262
|
if (manager === undefined) {
|
|
@@ -1268,6 +1273,10 @@ async function tasksCommand(arg, ctx) {
|
|
|
1268
1273
|
}
|
|
1269
1274
|
const show = (t) => {
|
|
1270
1275
|
const lines = taskOutput(t);
|
|
1276
|
+
// Amendment 8: a lost task says why, before its output
|
|
1277
|
+
const why = lostReason(t);
|
|
1278
|
+
if (why !== undefined)
|
|
1279
|
+
bodyLog(`${t.id} — lost track: ${why}; it may still be running`);
|
|
1271
1280
|
bodyLog(`${t.id} — ${t.agent !== undefined ? "its answer" : "its last output"}${lines.length === 0 ? ": nothing yet" : ""}`);
|
|
1272
1281
|
for (const line of lines)
|
|
1273
1282
|
bodyLog(` ${line}`);
|
|
@@ -1294,9 +1303,6 @@ async function tasksCommand(arg, ctx) {
|
|
|
1294
1303
|
bodyLog("no tasks in this session");
|
|
1295
1304
|
return;
|
|
1296
1305
|
}
|
|
1297
|
-
for (const t of list)
|
|
1298
|
-
if (t.state.kind === "unknown")
|
|
1299
|
-
seenUnknownTasks.add(t.id);
|
|
1300
1306
|
if (!dock.active) {
|
|
1301
1307
|
for (const t of list) {
|
|
1302
1308
|
const r = taskRow(t);
|
package/dist/floor.d.ts
CHANGED
|
@@ -63,6 +63,27 @@ export declare function innerLines(argv: readonly LooseWord[], home: string): {
|
|
|
63
63
|
readonly lines: readonly string[];
|
|
64
64
|
readonly subshell: boolean;
|
|
65
65
|
} | null;
|
|
66
|
+
/** How deep command lines inside command lines are followed — `sh -c`,
|
|
67
|
+
* `eval` — and how many are read in all: eval's two readings would
|
|
68
|
+
* otherwise double at every level. */
|
|
69
|
+
export declare const MAX_INNER_DEPTH = 16;
|
|
70
|
+
export declare const MAX_INNER_LINES = 256;
|
|
71
|
+
/** git's global options, read to the subcommand. Shared by the floor and
|
|
72
|
+
* the remote boundary (remote-boundary.ts), which must find the same
|
|
73
|
+
* subcommand the floor does: an option without a value is one word
|
|
74
|
+
* skipped, whatever it is, so `git --no-pager push` reads as push. */
|
|
75
|
+
export interface GitInvocation {
|
|
76
|
+
readonly sub: string | undefined;
|
|
77
|
+
readonly rest: readonly LooseWord[];
|
|
78
|
+
/** B7: several -C compose, each relative to the one before */
|
|
79
|
+
readonly cds: readonly LooseWord[];
|
|
80
|
+
/** B7: --work-tree X IS the tree a whole-tree command works on */
|
|
81
|
+
readonly workTree: LooseWord | undefined;
|
|
82
|
+
/** B7: --git-dir X is a target in itself (space and = forms both) */
|
|
83
|
+
readonly gitDirs: readonly LooseWord[];
|
|
84
|
+
}
|
|
85
|
+
/** `args` is git's argv without `git`. */
|
|
86
|
+
export declare function gitInvocation(args: readonly LooseWord[]): GitInvocation;
|
|
66
87
|
/**
|
|
67
88
|
* Would the floor refuse this command line? `workspaceRoot` is where the
|
|
68
89
|
* shell runs it.
|
package/dist/floor.js
CHANGED
|
@@ -344,8 +344,8 @@ export function innerLines(argv, home) {
|
|
|
344
344
|
/** How deep command lines inside command lines are followed — `sh -c`,
|
|
345
345
|
* `eval` — and how many are read in all: eval's two readings would
|
|
346
346
|
* otherwise double at every level. */
|
|
347
|
-
const MAX_INNER_DEPTH = 16;
|
|
348
|
-
const MAX_INNER_LINES = 256;
|
|
347
|
+
export const MAX_INNER_DEPTH = 16;
|
|
348
|
+
export const MAX_INNER_LINES = 256;
|
|
349
349
|
const SELECTING = new Set([
|
|
350
350
|
"-name", "-iname", "-path", "-ipath", "-wholename", "-iwholename", "-regex", "-iregex", "-newer", "-mtime", "-mmin", "-atime",
|
|
351
351
|
"-amin", "-ctime", "-cmin", "-size", "-empty", "-user", "-group", "-perm", "-samefile", "-links",
|
|
@@ -355,6 +355,38 @@ const SELECTING = new Set([
|
|
|
355
355
|
const pathShaped = (t) => t === "." || t === "./" || t.startsWith("/") || t.startsWith("~") || t.includes("/") || t.startsWith(":") || (hostDialect().msys && (t.includes("\\") || /^[A-Za-z]:/.test(t)));
|
|
356
356
|
/** B7: pathspec magic from the top of the repository */
|
|
357
357
|
const topMagic = (t) => t.startsWith(":/") || t.startsWith(":(top");
|
|
358
|
+
/** `args` is git's argv without `git`. */
|
|
359
|
+
export function gitInvocation(args) {
|
|
360
|
+
let i = 0;
|
|
361
|
+
const cds = [];
|
|
362
|
+
let workTree;
|
|
363
|
+
const gitDirs = [];
|
|
364
|
+
while (i < args.length && args[i].text.startsWith("-")) {
|
|
365
|
+
const t = args[i].text;
|
|
366
|
+
const eq = /^--(work-tree|git-dir)=(.*)$/.exec(t);
|
|
367
|
+
if (eq !== null) {
|
|
368
|
+
const value = { ...args[i], text: eq[2], unknownAt: -1, tilde: eq[2].startsWith("~") };
|
|
369
|
+
if (eq[1] === "work-tree")
|
|
370
|
+
workTree = value;
|
|
371
|
+
else
|
|
372
|
+
gitDirs.push(value);
|
|
373
|
+
}
|
|
374
|
+
if ((t === "-C" || t === "--work-tree" || t === "--git-dir") && i + 1 < args.length) {
|
|
375
|
+
if (t === "-C")
|
|
376
|
+
cds.push(args[i + 1]);
|
|
377
|
+
else if (t === "--work-tree")
|
|
378
|
+
workTree = args[i + 1];
|
|
379
|
+
else
|
|
380
|
+
gitDirs.push(args[i + 1]);
|
|
381
|
+
i += 2;
|
|
382
|
+
}
|
|
383
|
+
else if (t === "-c" || t === "--namespace" || t === "--exec-path")
|
|
384
|
+
i += 2;
|
|
385
|
+
else
|
|
386
|
+
i += 1;
|
|
387
|
+
}
|
|
388
|
+
return { sub: args[i]?.text, rest: args.slice(i + 1), cds, workTree, gitDirs };
|
|
389
|
+
}
|
|
358
390
|
/** The destructive command's targets, or null when it is not destructive. */
|
|
359
391
|
function destructiveTargets(argv) {
|
|
360
392
|
const name = basename(argv[0]?.text ?? "");
|
|
@@ -398,39 +430,7 @@ function destructiveTargets(argv) {
|
|
|
398
430
|
}
|
|
399
431
|
if (name !== "git")
|
|
400
432
|
return null;
|
|
401
|
-
|
|
402
|
-
// B7: several -C compose, each relative to the one before
|
|
403
|
-
const cds = [];
|
|
404
|
-
// B7: --work-tree X IS the tree a whole-tree command works on; --git-dir
|
|
405
|
-
// X is a target in itself (space and = forms both)
|
|
406
|
-
let workTree;
|
|
407
|
-
const gitDirs = [];
|
|
408
|
-
while (i < args.length && args[i].text.startsWith("-")) {
|
|
409
|
-
const t = args[i].text;
|
|
410
|
-
const eq = /^--(work-tree|git-dir)=(.*)$/.exec(t);
|
|
411
|
-
if (eq !== null) {
|
|
412
|
-
const value = { ...args[i], text: eq[2], unknownAt: -1, tilde: eq[2].startsWith("~") };
|
|
413
|
-
if (eq[1] === "work-tree")
|
|
414
|
-
workTree = value;
|
|
415
|
-
else
|
|
416
|
-
gitDirs.push(value);
|
|
417
|
-
}
|
|
418
|
-
if ((t === "-C" || t === "--work-tree" || t === "--git-dir") && i + 1 < args.length) {
|
|
419
|
-
if (t === "-C")
|
|
420
|
-
cds.push(args[i + 1]);
|
|
421
|
-
else if (t === "--work-tree")
|
|
422
|
-
workTree = args[i + 1];
|
|
423
|
-
else
|
|
424
|
-
gitDirs.push(args[i + 1]);
|
|
425
|
-
i += 2;
|
|
426
|
-
}
|
|
427
|
-
else if (t === "-c" || t === "--namespace" || t === "--exec-path")
|
|
428
|
-
i += 2;
|
|
429
|
-
else
|
|
430
|
-
i += 1;
|
|
431
|
-
}
|
|
432
|
-
const sub = args[i]?.text;
|
|
433
|
-
const rest = args.slice(i + 1);
|
|
433
|
+
const { sub, rest, cds, workTree, gitDirs } = gitInvocation(args);
|
|
434
434
|
const flags = new Set(rest.filter((a) => a.text.startsWith("-")).map((a) => a.text));
|
|
435
435
|
const as = (targets) => ({
|
|
436
436
|
targets: [...targets.map((t) => (t === "repo" && workTree !== undefined ? workTree : t)), ...gitDirs],
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ import { activeStoreDir, setActiveStoreDir, agentModel, atFiles, body, bodyLog,
|
|
|
45
45
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
46
46
|
import { chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
|
|
47
47
|
import { preferences, usePreferences } from "./preferences.js";
|
|
48
|
-
import { settingsLayers, setChildTurnBudget, stopAllTasks } from "./state.js";
|
|
48
|
+
import { settingsLayers, setChildTurnBudget, stopAllTasks, taskWhatOf } from "./state.js";
|
|
49
49
|
import { finishChild } from "./child-result.js";
|
|
50
50
|
import { loadUserConfig, resolveAutoCompact } from "./config.js";
|
|
51
51
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
@@ -1044,7 +1044,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1044
1044
|
// prompt inside a conversation with thousands of events — the
|
|
1045
1045
|
// durable log was right there and none of it was shown. Empty for
|
|
1046
1046
|
// a fresh session, so `kiso chat` is byte-identical.
|
|
1047
|
-
showResumeTail(session.log.all);
|
|
1047
|
+
showResumeTail(session.log.all, session.id);
|
|
1048
1048
|
// R2 (owner, 2026-08-27): the resume list is NOT on the opening
|
|
1049
1049
|
// screen. `/resume` is where you go looking for a session; the
|
|
1050
1050
|
// opening's job is to say what THIS one is.
|
|
@@ -1076,7 +1076,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1076
1076
|
}
|
|
1077
1077
|
else {
|
|
1078
1078
|
bodyLog(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
|
|
1079
|
-
showResumeTail(session.log.all);
|
|
1079
|
+
showResumeTail(session.log.all, session.id);
|
|
1080
1080
|
if (currentFaux)
|
|
1081
1081
|
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
1082
1082
|
}
|
|
@@ -1188,10 +1188,11 @@ async function readSecret(prompt) {
|
|
|
1188
1188
|
* there is no viewer and no cell renderer, so the plain text tail stays
|
|
1189
1189
|
* (DC-51: one call, one cell).
|
|
1190
1190
|
*/
|
|
1191
|
-
function showResumeTail(events) {
|
|
1191
|
+
function showResumeTail(events, sessionId) {
|
|
1192
1192
|
const W = process.stdout.columns ?? 80;
|
|
1193
|
+
// Amendment 8: a lost task's replayed row names what it ran
|
|
1193
1194
|
if (dock.active)
|
|
1194
|
-
replayInto(body, events, W);
|
|
1195
|
+
replayInto(body, events, W, taskWhatOf(sessionId));
|
|
1195
1196
|
else
|
|
1196
1197
|
bodyLog(resumeTail(events, W).join("\n"));
|
|
1197
1198
|
}
|
|
@@ -1575,7 +1576,7 @@ async function main() {
|
|
|
1575
1576
|
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
1576
1577
|
// REL-0152-D5 — the same tail on the explicit-id form. NOT on
|
|
1577
1578
|
// the -p path above: that one's stdout is a machine's input.
|
|
1578
|
-
showResumeTail(session.log.all);
|
|
1579
|
+
showResumeTail(session.log.all, session.id);
|
|
1579
1580
|
await resume(session, prompt, faux, input);
|
|
1580
1581
|
break;
|
|
1581
1582
|
}
|
|
@@ -1857,7 +1858,7 @@ async function main() {
|
|
|
1857
1858
|
// terminal (kill -9 excepted; `reset` saves it).
|
|
1858
1859
|
dock.exit();
|
|
1859
1860
|
for (const id of tasksAtExit.unconfirmed)
|
|
1860
|
-
console.error(`${id}: stop unconfirmed —
|
|
1861
|
+
console.error(`${id}: stop unconfirmed — it may still be running`);
|
|
1861
1862
|
if (tasksAtExit.left.length > 0)
|
|
1862
1863
|
console.error(`left running: ${tasksAtExit.left.join(", ")} — reopen this session to hear how ${tasksAtExit.left.length === 1 ? "it ends" : "they end"}`);
|
|
1863
1864
|
// finding #8 (P1): extension dispose runs on the same exit path — a
|
package/dist/preferences.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { ModelProfile } from "./config.js";
|
|
1
2
|
export interface Preferences {
|
|
2
3
|
/** ctrl+t: the thinking blocks as one italic line each. Absent = shown. */
|
|
3
4
|
readonly thinking?: "shown" | "hidden";
|
|
5
|
+
/** B1: the effort last picked for each profile (`/model <p> <e>`, or the
|
|
6
|
+
* panel with a level) — where a NEW session of that profile starts. */
|
|
7
|
+
readonly effort?: Readonly<Record<string, string>>;
|
|
4
8
|
}
|
|
5
9
|
export declare function preferencesPath(home?: string): string;
|
|
6
10
|
/** Startup: read the file, and keep choices made from here on in it. */
|
|
@@ -10,5 +14,15 @@ export declare function preferences(): Preferences;
|
|
|
10
14
|
/** Remember a choice. Returns false when no file was named. The file is
|
|
11
15
|
* re-read before the write, so another process's choice is kept. */
|
|
12
16
|
export declare function setPreference<K extends keyof Preferences>(key: K, value: NonNullable<Preferences[K]>): boolean;
|
|
17
|
+
/** B1: remember an explicit effort pick for a profile. */
|
|
18
|
+
export declare function rememberEffort(profile: string, effort: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* B1: the effort a NEW session of `profile` starts at, from the one last
|
|
21
|
+
* picked — the picker's fallback: the level itself when the model has it,
|
|
22
|
+
* else the endpoint's registry default, else none (default/default, no
|
|
23
|
+
* reasoning key). Only a level that resolves for the model is returned, so
|
|
24
|
+
* the session never carries a setting its first request would refuse.
|
|
25
|
+
*/
|
|
26
|
+
export declare function startingEffort(profile: Pick<ModelProfile, "model" | "baseUrl">, remembered: string | undefined): string | null;
|
|
13
27
|
/** Tests: back to the state before startup. */
|
|
14
28
|
export declare function resetPreferences(): void;
|
package/dist/preferences.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* `<KISO_HOME>/preferences.json`.
|
|
4
4
|
*
|
|
5
5
|
* `config.json` is the human's file: kiso reads it and never writes it. A
|
|
6
|
-
* choice made with a key — ctrl+t's thinking display,
|
|
7
|
-
* has to survive a restart, so it lands here instead: kiso-owned, private
|
|
6
|
+
* choice made with a key — ctrl+t's thinking display, and (B1) the effort
|
|
7
|
+
* last picked for each profile — has to survive a restart, so it lands here instead: kiso-owned, private
|
|
8
8
|
* (0600, tmp + rename), and read back at start-up. A missing or unreadable
|
|
9
9
|
* file is an empty one; an unknown value is ignored rather than guessed.
|
|
10
10
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
|
+
import { lookupModelMetadata, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
17
18
|
import { kisoHome } from "./state.js";
|
|
18
19
|
let prefs = null;
|
|
19
20
|
let prefsPath = null;
|
|
@@ -23,7 +24,15 @@ export function preferencesPath(home = kisoHome()) {
|
|
|
23
24
|
function read(path) {
|
|
24
25
|
try {
|
|
25
26
|
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
-
|
|
27
|
+
const out = {};
|
|
28
|
+
if (raw.thinking === "shown" || raw.thinking === "hidden")
|
|
29
|
+
out.thinking = raw.thinking;
|
|
30
|
+
if (raw.effort !== null && typeof raw.effort === "object" && !Array.isArray(raw.effort)) {
|
|
31
|
+
const effort = Object.fromEntries(Object.entries(raw.effort).filter((e) => typeof e[1] === "string"));
|
|
32
|
+
if (Object.keys(effort).length > 0)
|
|
33
|
+
out.effort = effort;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
27
36
|
}
|
|
28
37
|
catch {
|
|
29
38
|
return {};
|
|
@@ -57,6 +66,28 @@ export function setPreference(key, value) {
|
|
|
57
66
|
}
|
|
58
67
|
return true;
|
|
59
68
|
}
|
|
69
|
+
/** B1: remember an explicit effort pick for a profile. */
|
|
70
|
+
export function rememberEffort(profile, effort) {
|
|
71
|
+
return setPreference("effort", { ...(preferences().effort ?? {}), [profile]: effort });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* B1: the effort a NEW session of `profile` starts at, from the one last
|
|
75
|
+
* picked — the picker's fallback: the level itself when the model has it,
|
|
76
|
+
* else the endpoint's registry default, else none (default/default, no
|
|
77
|
+
* reasoning key). Only a level that resolves for the model is returned, so
|
|
78
|
+
* the session never carries a setting its first request would refuse.
|
|
79
|
+
*/
|
|
80
|
+
export function startingEffort(profile, remembered) {
|
|
81
|
+
if (remembered === undefined || remembered === "default")
|
|
82
|
+
return null;
|
|
83
|
+
const effort = lookupModelMetadata(profile.model, profile.baseUrl)?.capabilities.reasoning?.effort ?? null;
|
|
84
|
+
if (effort === null)
|
|
85
|
+
return null;
|
|
86
|
+
const legal = (level) => resolveReasoning(profile.model, { thinking: "default", effort: level }, profile.baseUrl).ok;
|
|
87
|
+
if (effort.levels.includes(remembered) && legal(remembered))
|
|
88
|
+
return remembered;
|
|
89
|
+
return effort.default !== null && legal(effort.default) ? effort.default : null;
|
|
90
|
+
}
|
|
60
91
|
/** Tests: back to the state before startup. */
|
|
61
92
|
export function resetPreferences() {
|
|
62
93
|
prefs = null;
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
import type { PolicyCall } from "@vincemakes/kiso-core";
|
|
19
19
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
20
20
|
export declare function isProtectedWrite(call: PolicyCall, workspaceRoot: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The calls a saved allow never carries — they reach a person every time
|
|
23
|
+
* in the asking tiers: a write into .git/ or .kiso/, a destructive
|
|
24
|
+
* command (floor.ts, which also answers yes for a line it cannot read),
|
|
25
|
+
* and a command across the remote boundary (remote-boundary.ts, 0.46.2).
|
|
26
|
+
* create-coding-agent.ts wires it into every guarded saved allow.
|
|
27
|
+
*/
|
|
28
|
+
export declare function neverInheritedCall(call: PolicyCall, workspaceRoot: string): boolean;
|
|
21
29
|
/**
|
|
22
30
|
* A saved allow ("yes, don't ask again for <tool>") allows by TOOL, so on
|
|
23
31
|
* its own it would carry every later call of that tool — including the
|
package/dist/protected-writes.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { relative, sep } from "node:path";
|
|
19
19
|
import { resolveShellPath } from "./shell-words.js";
|
|
20
|
+
import { isDestructiveCall } from "./floor.js";
|
|
21
|
+
import { isRemoteBoundaryCall } from "./remote-boundary.js";
|
|
20
22
|
const PROTECTED = new Set([".git", ".kiso"]);
|
|
21
23
|
const WRITERS = new Set(["write_file", "edit_file"]);
|
|
22
24
|
export function isProtectedWrite(call, workspaceRoot) {
|
|
@@ -34,6 +36,16 @@ export function isProtectedWrite(call, workspaceRoot) {
|
|
|
34
36
|
.some((seg) => PROTECTED.has(seg.toLowerCase()));
|
|
35
37
|
}
|
|
36
38
|
const ABSTAIN = { action: "abstain" };
|
|
39
|
+
/**
|
|
40
|
+
* The calls a saved allow never carries — they reach a person every time
|
|
41
|
+
* in the asking tiers: a write into .git/ or .kiso/, a destructive
|
|
42
|
+
* command (floor.ts, which also answers yes for a line it cannot read),
|
|
43
|
+
* and a command across the remote boundary (remote-boundary.ts, 0.46.2).
|
|
44
|
+
* create-coding-agent.ts wires it into every guarded saved allow.
|
|
45
|
+
*/
|
|
46
|
+
export function neverInheritedCall(call, workspaceRoot) {
|
|
47
|
+
return isProtectedWrite(call, workspaceRoot) || isDestructiveCall(call) || isRemoteBoundaryCall(call);
|
|
48
|
+
}
|
|
37
49
|
/**
|
|
38
50
|
* A saved allow ("yes, don't ask again for <tool>") allows by TOOL, so on
|
|
39
51
|
* its own it would carry every later call of that tool — including the
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.46.2 — the remote boundary (owner-ratified 2026-10-06, rev 2 with the
|
|
3
|
+
* review's amendments).
|
|
4
|
+
*
|
|
5
|
+
* Two families of shell command change state that other people share:
|
|
6
|
+
*
|
|
7
|
+
* A. a destructive git push — history rewritten or refs removed:
|
|
8
|
+
* --force / -f, --force-with-lease, --force-if-includes, a refspec
|
|
9
|
+
* with a leading `+`, --mirror, --delete / -d, a deleting refspec
|
|
10
|
+
* (`:branch`), --prune. A dry run is exempt, read as git reads it: the
|
|
11
|
+
* last of -n / --dry-run / --no-dry-run wins, and git takes any
|
|
12
|
+
* unambiguous prefix of a long option, so `--mirr` is --mirror and
|
|
13
|
+
* `--no-dry` turns the dry run off. A prefix is read generously — a
|
|
14
|
+
* prefix git would reject as ambiguous costs a question, not a pass.
|
|
15
|
+
* B. a registry write — a package published, unpublished or deprecated,
|
|
16
|
+
* or a dist-tag moved: npm or pnpm with `publish`, `unpublish`,
|
|
17
|
+
* `deprecate`, or `dist-tag` / `dist-tags` with add (a, set, s) or rm
|
|
18
|
+
* (r, del, d, remove) — pnpm hands the last three to npm; yarn with
|
|
19
|
+
* `publish`, `npm publish`, or `tag` / `npm tag` with add, rm or
|
|
20
|
+
* remove. Not as the script `npm run publish` names. A dry run is
|
|
21
|
+
* exempt only where the command honours one, read as its parser reads
|
|
22
|
+
* it (`--dry-run false` is a real publish): npm and pnpm publish and
|
|
23
|
+
* unpublish take --dry-run, `yarn npm publish` takes -n / --dry-run;
|
|
24
|
+
* deprecate, dist-tag and yarn classic's publish have none, so the
|
|
25
|
+
* flag exempts nothing there. npm's abbreviations of a command
|
|
26
|
+
* (`npm pub`) are read too, in the command's place — not after an
|
|
27
|
+
* option that takes a value (`npm --registry x pub`). Listing tags
|
|
28
|
+
* and `npm view` stay out.
|
|
29
|
+
*
|
|
30
|
+
* A saved "don't ask again for shell" never carries either
|
|
31
|
+
* (protected-writes.ts neverInheritedCall): permission remembered by TOOL
|
|
32
|
+
* NAME from some earlier call does not cross into a change other people
|
|
33
|
+
* share. Authority given now still does — every tier decides these as it
|
|
34
|
+
* decides any shell call, so full-access runs them, and an explicit user
|
|
35
|
+
* extension that allows them still allows them. That is the extension of
|
|
36
|
+
* launch-weekend plan §4 ("destructive commands never inherit a saved
|
|
37
|
+
* allow in the asking modes") to the remote; "bypass stays bypass" is
|
|
38
|
+
* untouched.
|
|
39
|
+
*
|
|
40
|
+
* The list is closed: a new family needs the owner's word. It reads
|
|
41
|
+
* direct invocations only, words as written — `npm run release`, a script
|
|
42
|
+
* file, a git alias, a `+` refspec in git config and a variable's value
|
|
43
|
+
* are not seen. A guardrail, not a sandbox. A line it cannot read is not
|
|
44
|
+
* its question: isDestructiveCall already answers yes for one, so a saved
|
|
45
|
+
* allow abstains either way, and the two readings never disagree on what
|
|
46
|
+
* "unreadable" means.
|
|
47
|
+
*/
|
|
48
|
+
import type { PolicyCall } from "@vincemakes/kiso-core";
|
|
49
|
+
/** Whether a command line crosses the remote boundary — read as the floor
|
|
50
|
+
* reads one: past wrappers (sudo, env, timeout), into `sh -c` and eval,
|
|
51
|
+
* along every command of a chain. */
|
|
52
|
+
export declare function isRemoteBoundary(commandLine: string): boolean;
|
|
53
|
+
/** The policy-call form: a shell call whose command crosses. A throw is
|
|
54
|
+
* not this reading's to answer — see the head comment. */
|
|
55
|
+
export declare function isRemoteBoundaryCall(call: PolicyCall): boolean;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.46.2 — the remote boundary (owner-ratified 2026-10-06, rev 2 with the
|
|
3
|
+
* review's amendments).
|
|
4
|
+
*
|
|
5
|
+
* Two families of shell command change state that other people share:
|
|
6
|
+
*
|
|
7
|
+
* A. a destructive git push — history rewritten or refs removed:
|
|
8
|
+
* --force / -f, --force-with-lease, --force-if-includes, a refspec
|
|
9
|
+
* with a leading `+`, --mirror, --delete / -d, a deleting refspec
|
|
10
|
+
* (`:branch`), --prune. A dry run is exempt, read as git reads it: the
|
|
11
|
+
* last of -n / --dry-run / --no-dry-run wins, and git takes any
|
|
12
|
+
* unambiguous prefix of a long option, so `--mirr` is --mirror and
|
|
13
|
+
* `--no-dry` turns the dry run off. A prefix is read generously — a
|
|
14
|
+
* prefix git would reject as ambiguous costs a question, not a pass.
|
|
15
|
+
* B. a registry write — a package published, unpublished or deprecated,
|
|
16
|
+
* or a dist-tag moved: npm or pnpm with `publish`, `unpublish`,
|
|
17
|
+
* `deprecate`, or `dist-tag` / `dist-tags` with add (a, set, s) or rm
|
|
18
|
+
* (r, del, d, remove) — pnpm hands the last three to npm; yarn with
|
|
19
|
+
* `publish`, `npm publish`, or `tag` / `npm tag` with add, rm or
|
|
20
|
+
* remove. Not as the script `npm run publish` names. A dry run is
|
|
21
|
+
* exempt only where the command honours one, read as its parser reads
|
|
22
|
+
* it (`--dry-run false` is a real publish): npm and pnpm publish and
|
|
23
|
+
* unpublish take --dry-run, `yarn npm publish` takes -n / --dry-run;
|
|
24
|
+
* deprecate, dist-tag and yarn classic's publish have none, so the
|
|
25
|
+
* flag exempts nothing there. npm's abbreviations of a command
|
|
26
|
+
* (`npm pub`) are read too, in the command's place — not after an
|
|
27
|
+
* option that takes a value (`npm --registry x pub`). Listing tags
|
|
28
|
+
* and `npm view` stay out.
|
|
29
|
+
*
|
|
30
|
+
* A saved "don't ask again for shell" never carries either
|
|
31
|
+
* (protected-writes.ts neverInheritedCall): permission remembered by TOOL
|
|
32
|
+
* NAME from some earlier call does not cross into a change other people
|
|
33
|
+
* share. Authority given now still does — every tier decides these as it
|
|
34
|
+
* decides any shell call, so full-access runs them, and an explicit user
|
|
35
|
+
* extension that allows them still allows them. That is the extension of
|
|
36
|
+
* launch-weekend plan §4 ("destructive commands never inherit a saved
|
|
37
|
+
* allow in the asking modes") to the remote; "bypass stays bypass" is
|
|
38
|
+
* untouched.
|
|
39
|
+
*
|
|
40
|
+
* The list is closed: a new family needs the owner's word. It reads
|
|
41
|
+
* direct invocations only, words as written — `npm run release`, a script
|
|
42
|
+
* file, a git alias, a `+` refspec in git config and a variable's value
|
|
43
|
+
* are not seen. A guardrail, not a sandbox. A line it cannot read is not
|
|
44
|
+
* its question: isDestructiveCall already answers yes for one, so a saved
|
|
45
|
+
* allow abstains either way, and the two readings never disagree on what
|
|
46
|
+
* "unreadable" means.
|
|
47
|
+
*/
|
|
48
|
+
import { homedir } from "node:os";
|
|
49
|
+
import { basename } from "node:path";
|
|
50
|
+
import { MAX_INNER_DEPTH, MAX_INNER_LINES, gitInvocation, innerLines, unwrap } from "./floor.js";
|
|
51
|
+
import { looseCommands, parseShellLooseChecked } from "./shell-words.js";
|
|
52
|
+
/** git push options that take the next word as their value. */
|
|
53
|
+
const PUSH_VALUED = new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec"]);
|
|
54
|
+
/** git push options that rewrite or remove remote refs. */
|
|
55
|
+
const PUSH_REWRITES = new Set(["--force", "--force-with-lease", "--force-if-includes", "--mirror", "--delete", "--prune"]);
|
|
56
|
+
function destructivePush(args) {
|
|
57
|
+
let crosses = false;
|
|
58
|
+
let dryRun = false;
|
|
59
|
+
let options = true;
|
|
60
|
+
const positionals = [];
|
|
61
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
62
|
+
const t = args[i].text;
|
|
63
|
+
if (!options || !t.startsWith("-") || t === "-") {
|
|
64
|
+
positionals.push(t);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (t === "--") {
|
|
68
|
+
options = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (PUSH_VALUED.has(t)) {
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (t.startsWith("--")) {
|
|
76
|
+
const flag = t.split("=")[0];
|
|
77
|
+
if (flag === "--dry-run")
|
|
78
|
+
dryRun = true;
|
|
79
|
+
else if (flag.length >= "--no-d".length && "--no-dry-run".startsWith(flag))
|
|
80
|
+
dryRun = false;
|
|
81
|
+
else if (flag.length > 2 && [...PUSH_REWRITES].some((r) => r.startsWith(flag)))
|
|
82
|
+
crosses = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// a short cluster, `-uf`; `-o` takes the rest of the word, or the next
|
|
86
|
+
for (let k = 1; k < t.length; k += 1) {
|
|
87
|
+
const c = t[k];
|
|
88
|
+
if (c === "o") {
|
|
89
|
+
if (k === t.length - 1)
|
|
90
|
+
i += 1;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (c === "n")
|
|
94
|
+
dryRun = true;
|
|
95
|
+
if (c === "f" || c === "d")
|
|
96
|
+
crosses = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// the first positional is the repository; the rest are refspecs
|
|
100
|
+
if (positionals.slice(1).some((r) => r.startsWith("+") || r.startsWith(":")))
|
|
101
|
+
crosses = true;
|
|
102
|
+
return crosses && !dryRun;
|
|
103
|
+
}
|
|
104
|
+
/** Whether a dry run is on, read as npm's option parser reads it: the last
|
|
105
|
+
* setting wins, `--dry-run false` and `--dry-run=false` turn it off, and
|
|
106
|
+
* only an explicit true counts. `-n` is a dry run where yarn says so. */
|
|
107
|
+
function dryRun(words, short) {
|
|
108
|
+
let on = false;
|
|
109
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
110
|
+
const w = words[i];
|
|
111
|
+
if (w === "--dry-run" || (short && w === "-n")) {
|
|
112
|
+
const next = words[i + 1];
|
|
113
|
+
on = next !== "false";
|
|
114
|
+
if (next === "true" || next === "false")
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
else if (w.startsWith("--dry-run="))
|
|
118
|
+
on = w === "--dry-run=true";
|
|
119
|
+
else if (w === "--no-dry-run")
|
|
120
|
+
on = false;
|
|
121
|
+
}
|
|
122
|
+
return on;
|
|
123
|
+
}
|
|
124
|
+
/** dist-tag (npm) and tag (yarn) write with these subcommands, and list
|
|
125
|
+
* with the rest — a bare `npm dist-tag <pkg>` lists too. */
|
|
126
|
+
const NPM_TAG_WRITES = new Set(["add", "a", "set", "s", "rm", "r", "del", "d", "remove"]);
|
|
127
|
+
const NPM_TAG_READS = new Set(["ls", "l", "sl", "list"]);
|
|
128
|
+
const YARN_TAG_WRITES = new Set(["add", "rm", "remove"]);
|
|
129
|
+
const YARN_TAG_READS = new Set(["ls", "list"]);
|
|
130
|
+
/** A tag command writes when a write subcommand follows it — unless the
|
|
131
|
+
* first word after it names a listing (`npm dist-tag ls d` lists "d"). */
|
|
132
|
+
function tagWrite(words, at, writes, reads) {
|
|
133
|
+
const after = words.slice(at + 1).filter((w) => !w.startsWith("-"));
|
|
134
|
+
if (after[0] !== undefined && reads.has(after[0]))
|
|
135
|
+
return false;
|
|
136
|
+
return after.some((w) => writes.has(w));
|
|
137
|
+
}
|
|
138
|
+
/** npm runs a unique prefix of a command name: `npm pub` is publish,
|
|
139
|
+
* `npm unp` unpublish, `npm dep` deprecate (npm 10's own resolution).
|
|
140
|
+
* Read generously, as git's options are — a prefix npm would reject as
|
|
141
|
+
* ambiguous (`npm de`) costs a question. `un` is npm's alias for
|
|
142
|
+
* uninstall; dist-tag has no abbreviation, every prefix of it being
|
|
143
|
+
* ambiguous with dist-tags. */
|
|
144
|
+
function npmCommand(first) {
|
|
145
|
+
if (first === undefined || first.length < 2 || first === "un")
|
|
146
|
+
return first;
|
|
147
|
+
return ["publish", "unpublish", "deprecate"].find((w) => w.startsWith(first)) ?? first;
|
|
148
|
+
}
|
|
149
|
+
function registryWrite(pm, args) {
|
|
150
|
+
const words = args.map((a) => a.text);
|
|
151
|
+
const first = words.find((w) => !w.startsWith("-"));
|
|
152
|
+
if (first === "run" || first === "run-script")
|
|
153
|
+
return false;
|
|
154
|
+
if (pm === "yarn") {
|
|
155
|
+
// berry spells the registry commands `yarn npm …`; classic has no
|
|
156
|
+
// unpublish or deprecate, and its publish takes no dry run
|
|
157
|
+
const berry = words.includes("npm");
|
|
158
|
+
if (words.includes("publish") && !(berry && dryRun(words, true)))
|
|
159
|
+
return true;
|
|
160
|
+
const tag = words.indexOf("tag");
|
|
161
|
+
return tag !== -1 && tagWrite(words, tag, YARN_TAG_WRITES, YARN_TAG_READS);
|
|
162
|
+
}
|
|
163
|
+
const command = pm === "npm" ? npmCommand(first) : first;
|
|
164
|
+
const publishes = words.includes("publish") || words.includes("unpublish") || command === "publish" || command === "unpublish";
|
|
165
|
+
if (publishes && !dryRun(words, false))
|
|
166
|
+
return true;
|
|
167
|
+
if (words.includes("deprecate") || command === "deprecate")
|
|
168
|
+
return true;
|
|
169
|
+
const tag = words.findIndex((w) => w === "dist-tag" || w === "dist-tags");
|
|
170
|
+
return tag !== -1 && tagWrite(words, tag, NPM_TAG_WRITES, NPM_TAG_READS);
|
|
171
|
+
}
|
|
172
|
+
function crossing(argv) {
|
|
173
|
+
const name = basename(argv[0]?.text ?? "");
|
|
174
|
+
if (name === "git") {
|
|
175
|
+
const inv = gitInvocation(argv.slice(1));
|
|
176
|
+
return inv.sub === "push" && destructivePush(inv.rest);
|
|
177
|
+
}
|
|
178
|
+
if (name === "npm" || name === "pnpm" || name === "yarn")
|
|
179
|
+
return registryWrite(name, argv.slice(1));
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/** Whether a command line crosses the remote boundary — read as the floor
|
|
183
|
+
* reads one: past wrappers (sudo, env, timeout), into `sh -c` and eval,
|
|
184
|
+
* along every command of a chain. */
|
|
185
|
+
export function isRemoteBoundary(commandLine) {
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const visit = (line, depth) => {
|
|
188
|
+
const read = parseShellLooseChecked(line);
|
|
189
|
+
return looseCommands(read.nodes).some(({ argv: raw }) => {
|
|
190
|
+
const argv = unwrap(raw);
|
|
191
|
+
const inner = innerLines(argv, homedir());
|
|
192
|
+
if (inner !== null) {
|
|
193
|
+
return (depth < MAX_INNER_DEPTH &&
|
|
194
|
+
inner.lines.some((l) => {
|
|
195
|
+
if (seen.has(l) || seen.size >= MAX_INNER_LINES)
|
|
196
|
+
return false;
|
|
197
|
+
seen.add(l);
|
|
198
|
+
return visit(l, depth + 1);
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
return crossing(argv);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
return visit(commandLine, 0);
|
|
205
|
+
}
|
|
206
|
+
/** The policy-call form: a shell call whose command crosses. A throw is
|
|
207
|
+
* not this reading's to answer — see the head comment. */
|
|
208
|
+
export function isRemoteBoundaryCall(call) {
|
|
209
|
+
if (call.name !== "shell" || typeof call.input.command !== "string")
|
|
210
|
+
return false;
|
|
211
|
+
try {
|
|
212
|
+
return isRemoteBoundary(call.input.command);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
package/dist/replay.d.ts
CHANGED
|
@@ -49,5 +49,5 @@ export declare function turnsOf(events: readonly Ev[]): Ev[][];
|
|
|
49
49
|
* Replay a session's durable events into the body. Returns the number of
|
|
50
50
|
* turns it found (0: a fresh session — nothing is drawn).
|
|
51
51
|
*/
|
|
52
|
-
export declare function replayInto(body: ReplayBody, events: readonly Ev[], W?: number): number;
|
|
52
|
+
export declare function replayInto(body: ReplayBody, events: readonly Ev[], W?: number, what?: (taskId: string) => string | undefined): number;
|
|
53
53
|
export {};
|
package/dist/replay.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* the viewer — then any uncovered earlier turns folded as above.
|
|
21
21
|
*/
|
|
22
22
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
23
|
-
import {
|
|
23
|
+
import { taskNoticeLines } from "./task-notice.js";
|
|
24
24
|
/** How many turns stay on screen in full. */
|
|
25
25
|
const FULL_TURNS = 2;
|
|
26
26
|
/** A user turn's opening line, or null when this input does not open one
|
|
@@ -48,7 +48,7 @@ export function turnsOf(events) {
|
|
|
48
48
|
}
|
|
49
49
|
/** One turn through the Body — consumeRun's mapping, minus everything that
|
|
50
50
|
* needs the live process (clocks, tailers, usage, the recap). */
|
|
51
|
-
function replayTurn(body, turn) {
|
|
51
|
+
function replayTurn(body, turn, what) {
|
|
52
52
|
let thinking = false;
|
|
53
53
|
let said = false;
|
|
54
54
|
const results = new Set();
|
|
@@ -63,7 +63,8 @@ function replayTurn(body, turn) {
|
|
|
63
63
|
if (ask !== null)
|
|
64
64
|
body.userLine(ask);
|
|
65
65
|
else if (e.via?.kind === "tasks")
|
|
66
|
-
|
|
66
|
+
for (const row of taskNoticeLines(e.via.items, what))
|
|
67
|
+
body.notice(row);
|
|
67
68
|
else if (e.source === "system") {
|
|
68
69
|
body.notice("verification pass");
|
|
69
70
|
body.notice(` ${typeof e.content === "string" ? e.content : ""}`);
|
|
@@ -124,7 +125,7 @@ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
|
124
125
|
* Replay a session's durable events into the body. Returns the number of
|
|
125
126
|
* turns it found (0: a fresh session — nothing is drawn).
|
|
126
127
|
*/
|
|
127
|
-
export function replayInto(body, events, W = 80) {
|
|
128
|
+
export function replayInto(body, events, W = 80, what) {
|
|
128
129
|
// The latest checkpoint: the turns it covers are what the model will
|
|
129
130
|
// read as its summary, not as turns.
|
|
130
131
|
let checkpoint = null;
|
|
@@ -146,16 +147,16 @@ export function replayInto(body, events, W = 80) {
|
|
|
146
147
|
const summary = checkpoint.summary;
|
|
147
148
|
body.fold(`checkpoint · summarizes ${plural(covered.length, "earlier turn")} · ctrl+r to read`, () => {
|
|
148
149
|
for (const t of covered)
|
|
149
|
-
replayTurn(body, t);
|
|
150
|
+
replayTurn(body, t, what);
|
|
150
151
|
}, summary);
|
|
151
152
|
}
|
|
152
153
|
if (earlier.length > 0) {
|
|
153
154
|
body.fold(`${plural(earlier.length, "earlier turn")} · ctrl+r to read`, () => {
|
|
154
155
|
for (const t of earlier)
|
|
155
|
-
replayTurn(body, t);
|
|
156
|
+
replayTurn(body, t, what);
|
|
156
157
|
});
|
|
157
158
|
}
|
|
158
159
|
for (const t of shown)
|
|
159
|
-
replayTurn(body, t);
|
|
160
|
+
replayTurn(body, t, what);
|
|
160
161
|
return total;
|
|
161
162
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* the moved modules read and mutate at call time.
|
|
7
7
|
*/
|
|
8
8
|
import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SaferAnswer, type SessionCardView } from "@vincemakes/kiso-tui";
|
|
9
|
+
import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
|
|
9
10
|
import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
|
|
10
11
|
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
11
12
|
import { type ShellTasks } from "@vincemakes/kiso-tools-node";
|
|
@@ -34,11 +35,14 @@ export declare function codingToolOptions(): {
|
|
|
34
35
|
readonly protectedFiles: readonly string[];
|
|
35
36
|
readonly tasks: (sessionId: string | undefined) => ShellTasks | undefined;
|
|
36
37
|
};
|
|
38
|
+
/** Amendment 8: what a session's task ran — a lost task's transcript row
|
|
39
|
+
* names it. Undefined when the task or its journal cannot be read. */
|
|
40
|
+
export declare function taskWhatOf(sessionId: string | undefined): (taskId: string) => string | undefined;
|
|
41
|
+
export declare function lostToldOf(sessionId: string): Set<string>;
|
|
42
|
+
/** The transcript lines a delivery shows: a loss already told is left out. */
|
|
43
|
+
export declare function deliveryLines(sessionId: string, items: readonly TaskDeliveryItem[]): string[];
|
|
37
44
|
export declare function tasksFor(sessionId: string | undefined): TaskManager | undefined;
|
|
38
45
|
export declare function setExitTasks(choice: "stop" | "leave"): void;
|
|
39
|
-
/** 3e: the unknown tasks the person has looked at in `/tasks` in this
|
|
40
|
-
* process — the status row stops counting them. */
|
|
41
|
-
export declare const seenUnknownTasks: Set<string>;
|
|
42
46
|
/** 3e: the live tasks of every session this process opened — a runner's
|
|
43
47
|
* (`durable`, it survives this process) and moved commands (`moved`). */
|
|
44
48
|
export declare function liveTasks(): {
|
package/dist/state.js
CHANGED
|
@@ -14,6 +14,7 @@ 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
16
|
import { canonicalPath, LEGACY_SESSIONS_DIR, projectDirFor, projectLayoutActive } from "./projects.js";
|
|
17
|
+
import { taskNoticeLines, taskWhat } from "./task-notice.js";
|
|
17
18
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
18
19
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
19
20
|
* env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
|
|
@@ -48,6 +49,34 @@ export function codingToolOptions() {
|
|
|
48
49
|
* — the `!` gesture — has none, and its shell is today's. */
|
|
49
50
|
const taskManagers = new Map();
|
|
50
51
|
let taskBackend;
|
|
52
|
+
/** Amendment 8: what a session's task ran — a lost task's transcript row
|
|
53
|
+
* names it. Undefined when the task or its journal cannot be read. */
|
|
54
|
+
export function taskWhatOf(sessionId) {
|
|
55
|
+
return (taskId) => {
|
|
56
|
+
try {
|
|
57
|
+
const t = tasksFor(sessionId)?.get(taskId);
|
|
58
|
+
return t === undefined ? undefined : taskWhat(t);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined; // a corrupt journal is reported where it is read on purpose
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Amendment 8: the lost tasks this process has told the person of, per
|
|
66
|
+
* session — when the model's notice of the same loss arrives later, its
|
|
67
|
+
* row is not said again. */
|
|
68
|
+
const toldLost = new Map();
|
|
69
|
+
export function lostToldOf(sessionId) {
|
|
70
|
+
let told = toldLost.get(sessionId);
|
|
71
|
+
if (told === undefined)
|
|
72
|
+
toldLost.set(sessionId, (told = new Set()));
|
|
73
|
+
return told;
|
|
74
|
+
}
|
|
75
|
+
/** The transcript lines a delivery shows: a loss already told is left out. */
|
|
76
|
+
export function deliveryLines(sessionId, items) {
|
|
77
|
+
const told = lostToldOf(sessionId);
|
|
78
|
+
return taskNoticeLines(items.filter((i) => !(i.transition === "unknown" && told.has(i.taskId))), taskWhatOf(sessionId));
|
|
79
|
+
}
|
|
51
80
|
export function tasksFor(sessionId) {
|
|
52
81
|
if (sessionId === undefined || activeStoreDir === "")
|
|
53
82
|
return undefined;
|
|
@@ -67,9 +96,6 @@ let exitTasks = "stop";
|
|
|
67
96
|
export function setExitTasks(choice) {
|
|
68
97
|
exitTasks = choice;
|
|
69
98
|
}
|
|
70
|
-
/** 3e: the unknown tasks the person has looked at in `/tasks` in this
|
|
71
|
-
* process — the status row stops counting them. */
|
|
72
|
-
export const seenUnknownTasks = new Set();
|
|
73
99
|
/** 3e: the live tasks of every session this process opened — a runner's
|
|
74
100
|
* (`durable`, it survives this process) and moved commands (`moved`). */
|
|
75
101
|
export function liveTasks() {
|
package/dist/task-notice.d.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
|
|
2
|
-
import type
|
|
3
|
-
|
|
2
|
+
import { type TaskInfo } from "@vincemakes/kiso-runtime/internal";
|
|
3
|
+
/** The lines a delivery shows: today's `✦ task t1 exited · t2 failed` for
|
|
4
|
+
* the ordinary items, and one `✦ lost track of …` row per lost task. The
|
|
5
|
+
* shape is fixed (a design keys on it): the opening `✦ lost track of <id>
|
|
6
|
+
* (<what>) — ` — the parenthesis only when the command is known — and the
|
|
7
|
+
* LAST ` — ` part opening with "it may still be running". One line each. */
|
|
8
|
+
export declare function taskNoticeLines(items: readonly TaskDeliveryItem[], what?: (taskId: string) => string | undefined): string[];
|
|
4
9
|
/** A task's state in the person's words. A stop that was asked for and is
|
|
5
10
|
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
6
|
-
* says so;
|
|
11
|
+
* says so; a task kiso lost track of says so (Amendment 8). */
|
|
7
12
|
export declare function taskStateLabel(task: TaskInfo): string;
|
|
13
|
+
/** Why kiso lost track of a task, from its journal — or undefined when it
|
|
14
|
+
* has not: a stop it could not confirm, a moved command whose kiso ended,
|
|
15
|
+
* or a runner that is gone. */
|
|
16
|
+
export declare function lostReason(task: TaskInfo): string | undefined;
|
|
8
17
|
/** What the task is: the command, or the child's role and task. */
|
|
9
18
|
export declare function taskWhat(task: TaskInfo): string;
|
|
10
19
|
/** One `/tasks` row: `t3 running 1m 20s`, with the command beside it. */
|
|
@@ -14,9 +23,8 @@ export declare function taskRow(task: TaskInfo, now?: number): {
|
|
|
14
23
|
};
|
|
15
24
|
/** A task's last `lines` lines of output — or, for a child, its answer. */
|
|
16
25
|
export declare function taskOutput(task: TaskInfo, lines?: number): string[];
|
|
17
|
-
/** The status row's
|
|
18
|
-
*
|
|
19
|
-
export declare function taskCounts(tasks: readonly TaskInfo[]
|
|
26
|
+
/** The status row's count: the tasks kiso manages (running or starting).
|
|
27
|
+
* A lost task is not counted here (Amendment 8). */
|
|
28
|
+
export declare function taskCounts(tasks: readonly TaskInfo[]): {
|
|
20
29
|
readonly running: number;
|
|
21
|
-
readonly unknown: number;
|
|
22
30
|
};
|
package/dist/task-notice.js
CHANGED
|
@@ -3,19 +3,40 @@
|
|
|
3
3
|
* on resume. The model reads the notice's own lines; a person reads this.
|
|
4
4
|
*
|
|
5
5
|
* 3e: and what the person sees of the tasks themselves — the `/tasks` rows,
|
|
6
|
-
* a task's output on request, and the
|
|
6
|
+
* a task's output on request, and the count on the status row. All of it
|
|
7
7
|
* is drawn on the screen only: no `user_input`, nothing in the session log,
|
|
8
8
|
* nothing the model sees (the model has its notices and `read_file`).
|
|
9
|
+
*
|
|
10
|
+
* Amendment 8: a task kiso lost track of (`unknown`) is said in the
|
|
11
|
+
* person's words — "lost track", and why — as its own transcript row, once;
|
|
12
|
+
* never a second count on the status row. The model's notice keeps the
|
|
13
|
+
* protocol's `unknown`.
|
|
9
14
|
*/
|
|
10
15
|
import { closeSync, fstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
11
16
|
import { dirname, join } from "node:path";
|
|
17
|
+
import { readRecords } from "@vincemakes/kiso-runtime/internal";
|
|
12
18
|
import { elapsedLabel } from "@vincemakes/kiso-tui-cells";
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
/** The lines a delivery shows: today's `✦ task t1 exited · t2 failed` for
|
|
20
|
+
* the ordinary items, and one `✦ lost track of …` row per lost task. The
|
|
21
|
+
* shape is fixed (a design keys on it): the opening `✦ lost track of <id>
|
|
22
|
+
* (<what>) — ` — the parenthesis only when the command is known — and the
|
|
23
|
+
* LAST ` — ` part opening with "it may still be running". One line each. */
|
|
24
|
+
export function taskNoticeLines(items, what) {
|
|
25
|
+
const rows = [];
|
|
26
|
+
const ordinary = items.filter((i) => i.transition !== "unknown");
|
|
27
|
+
if (ordinary.length > 0)
|
|
28
|
+
rows.push(`✦ task ${ordinary.map((i) => `${i.taskId} ${i.transition}`).join(" · ")}`);
|
|
29
|
+
for (const i of items) {
|
|
30
|
+
if (i.transition !== "unknown")
|
|
31
|
+
continue;
|
|
32
|
+
const w = what?.(i.taskId)?.split("\n")[0]?.trim();
|
|
33
|
+
rows.push(`✦ lost track of ${i.taskId}${w ? ` (${w})` : ""} — it may still be running · /tasks shows it`);
|
|
34
|
+
}
|
|
35
|
+
return rows;
|
|
15
36
|
}
|
|
16
37
|
/** A task's state in the person's words. A stop that was asked for and is
|
|
17
38
|
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
18
|
-
* says so;
|
|
39
|
+
* says so; a task kiso lost track of says so (Amendment 8). */
|
|
19
40
|
export function taskStateLabel(task) {
|
|
20
41
|
const s = task.state;
|
|
21
42
|
switch (s.kind) {
|
|
@@ -26,7 +47,7 @@ export function taskStateLabel(task) {
|
|
|
26
47
|
case "running":
|
|
27
48
|
return task.stoppedBy !== undefined ? "stopping" : s.ready ? "ready" : "running";
|
|
28
49
|
case "unknown":
|
|
29
|
-
return "
|
|
50
|
+
return "lost track — may still be running";
|
|
30
51
|
case "ended":
|
|
31
52
|
if (s.stopped)
|
|
32
53
|
return "stopped";
|
|
@@ -35,6 +56,26 @@ export function taskStateLabel(task) {
|
|
|
35
56
|
return s.exitCode !== null ? (s.exitCode === 0 ? "exited 0" : `failed ${s.exitCode}`) : `ended ${s.signal ?? ""}`.trim();
|
|
36
57
|
}
|
|
37
58
|
}
|
|
59
|
+
/** Why kiso lost track of a task, from its journal — or undefined when it
|
|
60
|
+
* has not: a stop it could not confirm, a moved command whose kiso ended,
|
|
61
|
+
* or a runner that is gone. */
|
|
62
|
+
export function lostReason(task) {
|
|
63
|
+
if (task.state.kind !== "unknown")
|
|
64
|
+
return undefined;
|
|
65
|
+
let records = [];
|
|
66
|
+
try {
|
|
67
|
+
records = readRecords(join(dirname(task.outputPath), "journal.jsonl"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// an unreadable journal: the general reason below
|
|
71
|
+
}
|
|
72
|
+
const unconfirmed = records.find((r) => r.type === "stop_unconfirmed");
|
|
73
|
+
if (unconfirmed !== undefined)
|
|
74
|
+
return `its stop could not be confirmed (pids ${unconfirmed.pids.join(", ")})`;
|
|
75
|
+
if (task.backend === "foreground")
|
|
76
|
+
return "the kiso that held it ended without recording its end";
|
|
77
|
+
return "its runner is gone without recording its end";
|
|
78
|
+
}
|
|
38
79
|
/** What the task is: the command, or the child's role and task. */
|
|
39
80
|
export function taskWhat(task) {
|
|
40
81
|
return task.agent !== undefined ? task.command : task.command.split("\n")[0];
|
|
@@ -71,16 +112,12 @@ export function taskOutput(task, lines = 20) {
|
|
|
71
112
|
return [];
|
|
72
113
|
}
|
|
73
114
|
}
|
|
74
|
-
/** The status row's
|
|
75
|
-
*
|
|
76
|
-
export function taskCounts(tasks
|
|
115
|
+
/** The status row's count: the tasks kiso manages (running or starting).
|
|
116
|
+
* A lost task is not counted here (Amendment 8). */
|
|
117
|
+
export function taskCounts(tasks) {
|
|
77
118
|
let running = 0;
|
|
78
|
-
|
|
79
|
-
for (const t of tasks) {
|
|
119
|
+
for (const t of tasks)
|
|
80
120
|
if (t.state.kind === "running" || t.state.kind === "starting")
|
|
81
121
|
running += 1;
|
|
82
|
-
|
|
83
|
-
unknown += 1;
|
|
84
|
-
}
|
|
85
|
-
return { running, unknown };
|
|
122
|
+
return { running };
|
|
86
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
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.46.
|
|
22
|
-
"@vincemakes/kiso-core": "0.46.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.46.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.46.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.46.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.46.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.46.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.46.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.46.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.46.
|
|
31
|
-
"@vincemakes/kiso-tools-node": "0.46.
|
|
32
|
-
"@vincemakes/kiso-tui": "0.46.
|
|
33
|
-
"@vincemakes/kiso-tui-cells": "0.46.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.46.2",
|
|
22
|
+
"@vincemakes/kiso-core": "0.46.2",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.46.2",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.46.2",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.46.2",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.46.2",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.46.2",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.46.2",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.46.2",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.46.2",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.46.2",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.46.2",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.46.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.1.2",
|