@vincemakes/kiso-code 0.39.2 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/attachments.d.ts +1 -1
  3. package/dist/attachments.js +11 -4
  4. package/dist/builtin.d.ts +12 -0
  5. package/dist/builtin.js +22 -1
  6. package/dist/chat.d.ts +31 -3
  7. package/dist/chat.js +169 -30
  8. package/dist/config.d.ts +11 -1
  9. package/dist/config.js +34 -3
  10. package/dist/dispatch.d.ts +12 -2
  11. package/dist/dispatch.js +165 -24
  12. package/dist/floor.d.ts +80 -0
  13. package/dist/floor.js +652 -0
  14. package/dist/index.d.ts +21 -2
  15. package/dist/index.js +463 -46
  16. package/dist/mode.d.ts +12 -4
  17. package/dist/mode.js +32 -10
  18. package/dist/projects.d.ts +106 -0
  19. package/dist/projects.js +212 -0
  20. package/dist/protected-shell.d.ts +88 -0
  21. package/dist/protected-shell.js +406 -0
  22. package/dist/protected-writes.d.ts +29 -0
  23. package/dist/protected-writes.js +54 -0
  24. package/dist/provider-label.d.ts +32 -0
  25. package/dist/provider-label.js +53 -0
  26. package/dist/readonly-shell.d.ts +58 -0
  27. package/dist/readonly-shell.js +590 -0
  28. package/dist/replay.d.ts +53 -0
  29. package/dist/replay.js +158 -0
  30. package/dist/resume-tail.js +4 -1
  31. package/dist/resume.js +3 -3
  32. package/dist/retries.d.ts +9 -0
  33. package/dist/retries.js +16 -0
  34. package/dist/session-cards.d.ts +33 -4
  35. package/dist/session-cards.js +44 -3
  36. package/dist/session-migration.d.ts +104 -0
  37. package/dist/session-migration.js +319 -0
  38. package/dist/shell-words.d.ts +134 -0
  39. package/dist/shell-words.js +628 -0
  40. package/dist/skill-invoke.d.ts +58 -0
  41. package/dist/skill-invoke.js +139 -0
  42. package/dist/state.d.ts +80 -1
  43. package/dist/state.js +113 -2
  44. package/dist/trust-ui.js +29 -5
  45. package/package.json +15 -15
package/dist/replay.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 4c — a resumed session's history, replayed into cells.
3
+ *
4
+ * REL-0152-D5's tail printed the last two turns as text lines — a raw
5
+ * block, not cells — so nothing earlier could be read, and a fold line
6
+ * naming a key would have promised a read the product could not deliver.
7
+ * This drives the Body's own mutations over the durable events — the same
8
+ * calls a live run makes — so the turns render through the committed-cell
9
+ * renderer, route B's reprint prints them again on a resize, and the
10
+ * earlier turns sit in ONE fold row the ctrl+r viewer reads.
11
+ *
12
+ * Still a projection: it reads the log the session already loaded, writes
13
+ * nothing durable, and holds no state of its own.
14
+ *
15
+ * The shape (owner-ruled, option A):
16
+ * - the last two turns in full;
17
+ * - every earlier turn in one fold row, "N earlier turns · ctrl+r to read";
18
+ * - with a compaction, the latest checkpoint in place of the fold — its
19
+ * summary (what the model sees next) and the turns it covers, read in
20
+ * the viewer — then any uncovered earlier turns folded as above.
21
+ */
22
+ import { echoText } from "@vincemakes/kiso-tui-cells/render";
23
+ /** How many turns stay on screen in full. */
24
+ const FULL_TURNS = 2;
25
+ /** A user turn's opening line, or null when this input does not open one
26
+ * (a system-sourced input is machinery inside a turn; an empty ask is
27
+ * nothing). The words are the chip's words: a skill turn is the line the
28
+ * person typed, an image turn keeps its mark. */
29
+ function askOf(e) {
30
+ if (e.type !== "user_input" || e.source === "system")
31
+ return null;
32
+ const via = e.via;
33
+ const text = typeof via?.line === "string" ? via.line : echoText(e.content);
34
+ return text.trim() === "" ? null : text;
35
+ }
36
+ /** Split events into turns: each starts at a user turn's input; anything
37
+ * before the first one belongs to no turn and is dropped. */
38
+ export function turnsOf(events) {
39
+ const turns = [];
40
+ for (const e of events) {
41
+ if (askOf(e) !== null)
42
+ turns.push([e]);
43
+ else if (turns.length > 0)
44
+ turns[turns.length - 1].push(e);
45
+ }
46
+ return turns;
47
+ }
48
+ /** One turn through the Body — consumeRun's mapping, minus everything that
49
+ * needs the live process (clocks, tailers, usage, the recap). */
50
+ function replayTurn(body, turn) {
51
+ let thinking = false;
52
+ let said = false;
53
+ const results = new Set();
54
+ for (const e of turn) {
55
+ if (thinking && e.type !== "thinking") {
56
+ body.thinkingEnd();
57
+ thinking = false;
58
+ }
59
+ switch (e.type) {
60
+ case "user_input": {
61
+ const ask = askOf(e);
62
+ if (ask !== null)
63
+ body.userLine(ask);
64
+ else if (e.source === "system") {
65
+ body.notice("verification pass");
66
+ body.notice(` ${typeof e.content === "string" ? e.content : ""}`);
67
+ }
68
+ break;
69
+ }
70
+ case "thinking":
71
+ if (typeof e.text === "string") {
72
+ body.thinkingAppend(e.text);
73
+ thinking = true;
74
+ }
75
+ break;
76
+ case "tool_call_end":
77
+ body.toolStart(String(e.name), String(e.callId), e.input ?? {});
78
+ said = true;
79
+ break;
80
+ case "tool_result": {
81
+ const text = typeof e.content === "string" ? e.content : "";
82
+ let reason = null;
83
+ if ((e.tags ?? []).includes("denied")) {
84
+ const m = /^\[Permission denied\] (.*)$/.exec(text);
85
+ if (m !== null)
86
+ reason = m[1];
87
+ }
88
+ body.toolResult(String(e.callId), { content: text, isError: e.isError === true, reason, untimed: true });
89
+ results.add(String(e.callId));
90
+ break;
91
+ }
92
+ case "text_delta":
93
+ if (typeof e.text === "string") {
94
+ body.textAppend(e.text);
95
+ if (e.text.trim() !== "")
96
+ said = true;
97
+ }
98
+ break;
99
+ case "text_end":
100
+ body.textEnd();
101
+ break;
102
+ case "model_output_abandoned":
103
+ body.textEnd();
104
+ body.notice("stream interrupted — the draft above is abandoned");
105
+ break;
106
+ default:
107
+ break;
108
+ }
109
+ }
110
+ if (thinking)
111
+ body.thinkingEnd();
112
+ body.textEnd();
113
+ // A turn with nothing recorded after the ask is the turn that was
114
+ // INTERRUPTED — the case resume exists for; saying so beats a blank.
115
+ if (!said)
116
+ body.notice("no reply recorded — this is where it stopped");
117
+ body.endTurn(0);
118
+ }
119
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
120
+ /**
121
+ * Replay a session's durable events into the body. Returns the number of
122
+ * turns it found (0: a fresh session — nothing is drawn).
123
+ */
124
+ export function replayInto(body, events, W = 80) {
125
+ // The latest checkpoint: the turns it covers are what the model will
126
+ // read as its summary, not as turns.
127
+ let checkpoint = null;
128
+ for (const e of events) {
129
+ if (e.type === "summarized" && typeof e.coversToSeq === "number" && typeof e.summary === "string")
130
+ checkpoint = { coversToSeq: e.coversToSeq, summary: e.summary };
131
+ }
132
+ const covered = checkpoint === null ? [] : turnsOf(events.filter((e) => typeof e.seq === "number" && e.seq <= checkpoint.coversToSeq));
133
+ const rest = turnsOf(checkpoint === null ? events : events.filter((e) => typeof e.seq !== "number" || e.seq > checkpoint.coversToSeq));
134
+ const total = covered.length + rest.length;
135
+ if (total === 0)
136
+ return 0;
137
+ const shown = rest.slice(-FULL_TURNS);
138
+ const earlier = rest.slice(0, rest.length - shown.length);
139
+ const label = total > shown.length ? `resuming · ${plural(total, "turn")}, showing the last ${shown.length}` : `resuming · ${plural(total, "turn")}`;
140
+ const head = `─── ${label} `;
141
+ body.raw([`${head}${"─".repeat(Math.max(1, W - head.length))}`.slice(0, Math.max(1, W))]);
142
+ if (checkpoint !== null) {
143
+ const summary = checkpoint.summary;
144
+ body.fold(`checkpoint · summarizes ${plural(covered.length, "earlier turn")} · ctrl+r to read`, () => {
145
+ for (const t of covered)
146
+ replayTurn(body, t);
147
+ }, summary);
148
+ }
149
+ if (earlier.length > 0) {
150
+ body.fold(`${plural(earlier.length, "earlier turn")} · ctrl+r to read`, () => {
151
+ for (const t of earlier)
152
+ replayTurn(body, t);
153
+ });
154
+ }
155
+ for (const t of shown)
156
+ replayTurn(body, t);
157
+ return total;
158
+ }
@@ -47,7 +47,10 @@ export function resumeTail(events, W = 80) {
47
47
  // RUNTIME keeps its own `contentText` (R7) because it cannot
48
48
  // depend on tui-cells — two implementations, one per direction,
49
49
  // agreeing on "(image)".
50
- const text = echoText(e.content).trim();
50
+ // 0.40.0: a skill turn is shown as the line the person typed —
51
+ // its content is a SKILL.md body they never wrote.
52
+ const via = e.via;
53
+ const text = (typeof via?.line === "string" ? via.line : echoText(e.content)).trim();
51
54
  if (text === "")
52
55
  continue;
53
56
  if (ask !== null)
package/dist/resume.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { idleStatus, runningStatus } from "@vincemakes/kiso-tui";
6
6
  import { getMode } from "./mode.js";
7
- import { agentModel, dock } from "./state.js";
7
+ import { agentModel, dock, retryOnRow, floorOn } from "./state.js";
8
8
  import { pendingAsk, resolveUncertains } from "./trust-ui.js";
9
9
  import { failOnFauxExhaustion } from "./faux-glue.js";
10
10
  import { consumeRun, estimateCtxRatio, startStatusSpinner } from "./chat.js";
@@ -28,7 +28,7 @@ export async function resume(session, prompt, faux, input) {
28
28
  const statusCb = (u, ctx) => {
29
29
  runUsage = u;
30
30
  if (dock.active)
31
- dock.setStatus(runningStatus(runGlyph, runStart, u.out, ctx));
31
+ dock.setStatus(runningStatus(runGlyph, runStart, u.out, ctx, null, process.stdout.columns > 0 ? process.stdout.columns : 80, retryOnRow()));
32
32
  };
33
33
  // the recovery flow prints the BARE mode (chat spells plan's posture) —
34
34
  // the extraction keeps that difference, it was not asked to settle it.
@@ -37,7 +37,7 @@ export async function resume(session, prompt, faux, input) {
37
37
  // same budget. It has no meter, so nothing here can be dropped that
38
38
  // was not already at risk of being cut.
39
39
  if (dock.active)
40
- dock.setStatus(idleStatus(getMode(), agentModel, estimateCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
40
+ dock.setStatus(idleStatus(getMode(), agentModel, estimateCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80, !floorOn));
41
41
  };
42
42
  const withRun = async (run) => {
43
43
  currentRun = run;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ADR-0005 Amendment 2: KISO_MAX_RETRIES overrides the kernel's default
3
+ * budget, CLAMPED to 15 — a typo must not turn one failing request into an
4
+ * hour of retries at 32 s apiece. The kernel reads no environment; this is
5
+ * the front door's knob, passed in as `maxRetries`. Absent or unreadable
6
+ * means the kernel's own default.
7
+ */
8
+ export declare const MAX_RETRIES_CLAMP = 15;
9
+ export declare function maxRetriesFromEnv(raw?: string | undefined): number | undefined;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ADR-0005 Amendment 2: KISO_MAX_RETRIES overrides the kernel's default
3
+ * budget, CLAMPED to 15 — a typo must not turn one failing request into an
4
+ * hour of retries at 32 s apiece. The kernel reads no environment; this is
5
+ * the front door's knob, passed in as `maxRetries`. Absent or unreadable
6
+ * means the kernel's own default.
7
+ */
8
+ export const MAX_RETRIES_CLAMP = 15;
9
+ export function maxRetriesFromEnv(raw = process.env.KISO_MAX_RETRIES) {
10
+ if (raw === undefined || raw.trim() === "")
11
+ return undefined;
12
+ const n = Number(raw);
13
+ if (!Number.isInteger(n) || n < 0)
14
+ return undefined;
15
+ return Math.min(n, MAX_RETRIES_CLAMP);
16
+ }
@@ -28,9 +28,9 @@
28
28
  * terminal layer (sessionNote — the KC3 §1 split); this file owns the
29
29
  * FACTS.
30
30
  */
31
- import { type StoreRecord } from "@vincemakes/kiso-runtime/internal";
31
+ import { type SessionListing, type StoreRecord } from "@vincemakes/kiso-runtime/internal";
32
32
  /** The five durable states, in the order the projection resolves them. */
33
- export type SessionBadge = "uncertain" | "ask" | "interrupted" | "completed" | "failed";
33
+ export type SessionBadge = "uncertain" | "ask" | "interrupted" | "completed" | "failed" | "unknown";
34
34
  export interface SessionCard {
35
35
  readonly id: string;
36
36
  /** REL-0152-D6b: what the conversation was ABOUT. An id is unique and
@@ -43,7 +43,7 @@ export interface SessionCard {
43
43
  * counts runIds, which a resume increments without the human having
44
44
  * said anything — that is a different number and not the one a
45
45
  * picker row means.) */
46
- readonly turns: number;
46
+ readonly turns: number | null;
47
47
  /** The last record's own stamp — the store's `updatedAt`, never a
48
48
  * file mtime (a copied home would lie about every age). */
49
49
  readonly updatedAt: number;
@@ -55,6 +55,14 @@ export interface SessionCard {
55
55
  * open. The ✗ note names it rather than inventing one word for six
56
56
  * different endings. */
57
57
  readonly outcome: string | null;
58
+ /** 0.40.0: where the session started (its profile's `workspace`);
59
+ * null when unknown. */
60
+ readonly workspace: string | null;
61
+ /** 0.40.0: the config profile its latest revision names. */
62
+ readonly profileName: string | null;
63
+ /** 0.40.0: in a project folder by the migration's inference, never by
64
+ * a recorded workspace — the listing marks it. */
65
+ readonly inferred?: boolean;
58
66
  }
59
67
  /**
60
68
  * The projection. `asks` arrives as DATA because its accessor lives on
@@ -78,6 +86,8 @@ export declare function projectSessionCard(input: {
78
86
  readonly updatedAt: number;
79
87
  readonly records: readonly StoreRecord[];
80
88
  readonly asks: number;
89
+ readonly workspace?: string | null;
90
+ readonly profileName?: string | null;
81
91
  }): SessionCard;
82
92
  /** The shapes this module needs from the agent — structural, so the
83
93
  * projection is testable without standing up a provider. */
@@ -101,5 +111,24 @@ interface CardSource {
101
111
  * loads its records and builds an event log, and writes nothing (no
102
112
  * lock, no file creation) — the read-only listing rule holds.
103
113
  */
104
- export declare function collectSessionCards(agent: CardSource, load: (id: string) => readonly StoreRecord[]): Promise<SessionCard[]>;
114
+ export declare function collectSessionCards(agent: CardSource, load: (id: string) => readonly StoreRecord[], profileOf?: (id: string) => {
115
+ readonly workspace: string | null;
116
+ readonly profileName: string | null;
117
+ }): Promise<SessionCard[]>;
118
+ /**
119
+ * The 0.40.0 dogfood (item 2) — a card from the SIDECAR alone: the summary
120
+ * tenant the runtime writes at each run's start and end (or the one-time
121
+ * migration wrote), plus the profile's workspace and name. No log is read.
122
+ * A session with no summary says so ("no summary"); one whose log could not
123
+ * be read when it was summarised says that ("log unreadable") — never a
124
+ * guess, and never a read of the log to find out.
125
+ */
126
+ export declare function cardFromListing(l: SessionListing & {
127
+ readonly inferred?: boolean;
128
+ }): SessionCard;
129
+ /** Every card from the sidecars, newest first (the order the picker and
130
+ * `kiso sessions` both want). */
131
+ export declare function cardsFromListings(listings: readonly (SessionListing & {
132
+ readonly inferred?: boolean;
133
+ })[]): SessionCard[];
105
134
  export {};
@@ -74,7 +74,18 @@ export function projectSessionCard(input) {
74
74
  : outcome === "completed"
75
75
  ? "completed"
76
76
  : "failed";
77
- return { id: input.id, title: sessionTitle(input.records), badge, turns, updatedAt: input.updatedAt, uncertain, asks: input.asks, outcome };
77
+ return {
78
+ id: input.id,
79
+ title: sessionTitle(input.records),
80
+ badge,
81
+ turns,
82
+ updatedAt: input.updatedAt,
83
+ uncertain,
84
+ asks: input.asks,
85
+ outcome,
86
+ workspace: input.workspace ?? null,
87
+ profileName: input.profileName ?? null,
88
+ };
78
89
  }
79
90
  /**
80
91
  * The listing's cards, newest first — the order both the picker and
@@ -85,7 +96,7 @@ export function projectSessionCard(input) {
85
96
  * loads its records and builds an event log, and writes nothing (no
86
97
  * lock, no file creation) — the read-only listing rule holds.
87
98
  */
88
- export async function collectSessionCards(agent, load) {
99
+ export async function collectSessionCards(agent, load, profileOf = () => ({ workspace: null, profileName: null })) {
89
100
  const cards = [];
90
101
  for (const meta of [...agent.sessions()].sort((a, b) => b.updatedAt - a.updatedAt)) {
91
102
  // XP-1: the LISTING never enforces the profile contract — a session
@@ -101,7 +112,37 @@ export async function collectSessionCards(agent, load) {
101
112
  catch {
102
113
  // blocked by the profile contract — the card carries no ask badge
103
114
  }
104
- cards.push(projectSessionCard({ id: meta.id, updatedAt: meta.updatedAt, records: load(meta.id), asks }));
115
+ cards.push(projectSessionCard({ id: meta.id, updatedAt: meta.updatedAt, records: load(meta.id), asks, ...profileOf(meta.id) }));
105
116
  }
106
117
  return cards;
107
118
  }
119
+ /**
120
+ * The 0.40.0 dogfood (item 2) — a card from the SIDECAR alone: the summary
121
+ * tenant the runtime writes at each run's start and end (or the one-time
122
+ * migration wrote), plus the profile's workspace and name. No log is read.
123
+ * A session with no summary says so ("no summary"); one whose log could not
124
+ * be read when it was summarised says that ("log unreadable") — never a
125
+ * guess, and never a read of the log to find out.
126
+ */
127
+ export function cardFromListing(l) {
128
+ const s = l.summary;
129
+ const base = { id: l.id, workspace: l.workspace, profileName: l.profileName, ...(l.inferred === true ? { inferred: true } : {}) };
130
+ if (s === null)
131
+ return { ...base, title: l.id, badge: "unknown", turns: null, updatedAt: l.mtime, uncertain: 0, asks: 0, outcome: "no summary" };
132
+ const badge = s.uncertain > 0 ? "uncertain" : s.asks > 0 ? "ask" : s.state === "open" ? "interrupted" : s.state === "completed" ? "completed" : s.state === null ? "unknown" : "failed";
133
+ return {
134
+ ...base,
135
+ title: s.title ?? l.id,
136
+ badge,
137
+ turns: s.turns,
138
+ updatedAt: s.updatedAt,
139
+ uncertain: s.uncertain,
140
+ asks: s.asks,
141
+ outcome: badge === "unknown" ? (s.turns === null ? "log unreadable" : "no run recorded") : s.state === "open" ? null : s.state,
142
+ };
143
+ }
144
+ /** Every card from the sidecars, newest first (the order the picker and
145
+ * `kiso sessions` both want). */
146
+ export function cardsFromListings(listings) {
147
+ return listings.map(cardFromListing).sort((a, b) => b.updatedAt - a.updatedAt);
148
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 0.40.0 — the one-time move of the legacy `KISO_HOME/sessions` into the
3
+ * per-project folders (see projects.ts). The lead's conditions, in order:
4
+ *
5
+ * 1. the manifest is written FIRST: every session's old folder, new
6
+ * folder, reason and evidence, before any file moves;
7
+ * 2. a session whose lock a live process holds is skipped, and moved on a
8
+ * later start;
9
+ * 3. a recorded workspace places a session; otherwise the paths its tools
10
+ * touched, mapped to their repository on disk, place it when one root
11
+ * has at least 3 mentions and 80% of them; anything else goes to
12
+ * `_unknown`. A delegated child follows its parent;
13
+ * 4. the move is announced once, naming the reverse command.
14
+ *
15
+ * Each session moves trace → sidecar → lock → log (projects.ts), so a crash
16
+ * leaves the log in the legacy folder, where the next start finds it again.
17
+ * Nothing is deleted: files the move does not own (orphan sidecars, the
18
+ * delegation artifacts, whose paths the parents' logs record) stay where
19
+ * they are.
20
+ */
21
+ export type PlacementReason = "recorded" | "inferred" | "unknown" | "skipped-live";
22
+ export interface Placement {
23
+ readonly id: string;
24
+ readonly reason: PlacementReason;
25
+ /** the project the session goes to; null → `_unknown` (or, when
26
+ * skipped, nowhere yet) */
27
+ readonly workspace: string | null;
28
+ readonly from: string;
29
+ readonly to: string | null;
30
+ /** for `inferred`, and for the near misses that went to `_unknown` */
31
+ readonly evidence?: {
32
+ readonly root: string;
33
+ readonly mentions: number;
34
+ readonly total: number;
35
+ };
36
+ /** a delegated child follows this session */
37
+ readonly parent?: string;
38
+ }
39
+ /** The inference's two thresholds (the lead's ruling). */
40
+ export declare const INFER_MIN_MENTIONS = 3;
41
+ export declare const INFER_MIN_SHARE = 0.8;
42
+ /** The legacy sessions still to move: non-empty logs, sorted. */
43
+ export declare function pendingLegacyIds(home: string): string[];
44
+ /**
45
+ * The repository a path belongs to: the nearest ancestor holding `.git`.
46
+ * A linked worktree (`.git` is a file naming `<repo>/.git/worktrees/x`)
47
+ * belongs to its main repository. Walks stop at the home directory, and
48
+ * paths inside KISO_HOME belong to no project.
49
+ */
50
+ export declare function repoRootOf(path: string, cache: Map<string, string | null>, stops: {
51
+ readonly home: string;
52
+ readonly kisoHome: string;
53
+ }): string | null;
54
+ /** The absolute paths one log's tool calls and tool results name. */
55
+ export declare function toolPaths(logText: string): string[];
56
+ /** The inference over one log: the dominant repository and its counts. */
57
+ export declare function inferWorkspace(logText: string, cache: Map<string, string | null>, stops: {
58
+ readonly home: string;
59
+ readonly kisoHome: string;
60
+ }): {
61
+ readonly root: string;
62
+ readonly mentions: number;
63
+ readonly total: number;
64
+ readonly placed: boolean;
65
+ } | null;
66
+ /**
67
+ * The plan: where every pending legacy session goes, and why. A pure read
68
+ * of the legacy folder and the repositories on disk — nothing is written,
69
+ * so the owner's dry run is this function over a copy.
70
+ */
71
+ export declare function planMigration(home: string, opts?: {
72
+ readonly userHome?: string;
73
+ }): Placement[];
74
+ export interface MigrationCounts {
75
+ readonly recorded: number;
76
+ readonly inferred: number;
77
+ readonly unknown: number;
78
+ readonly skippedLive: number;
79
+ }
80
+ export declare function countPlan(plan: readonly Placement[]): MigrationCounts;
81
+ export interface MigrationResult {
82
+ readonly manifest: string;
83
+ readonly counts: MigrationCounts;
84
+ readonly moved: number;
85
+ }
86
+ /**
87
+ * Run the plan. The manifest lands before the first move; after the moves
88
+ * it is rewritten with each entry's outcome. Returns null when there was
89
+ * nothing to move (all skipped, or nothing pending).
90
+ */
91
+ export declare function runMigration(home: string, plan: readonly Placement[], now?: Date): MigrationResult | null;
92
+ /** The announcement, once per migration that moved something. */
93
+ export declare function migrationNotice(result: MigrationResult): string;
94
+ /**
95
+ * `kiso sessions --reverse-migration <manifest>`: every entry the manifest
96
+ * moved goes back to the legacy folder, the log FIRST (so a crash leaves it
97
+ * listed from its old place), and the marker switches the per-project
98
+ * layout off. Sessions created in project folders after the migration are
99
+ * not in the manifest and stay; the count says so.
100
+ */
101
+ export declare function reverseMigration(home: string, manifestPath: string): {
102
+ readonly restored: number;
103
+ readonly left: number;
104
+ };