indusagi-coding-agent 0.1.61 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/entry.js +11589 -14719
  3. package/dist/types/boot/contract.d.ts +2 -0
  4. package/dist/types/boot/runners/addon-wiring.d.ts +103 -0
  5. package/dist/types/boot/runners/addon-wiring.test.d.ts +19 -0
  6. package/dist/types/boot/runners/checkpoint.d.ts +133 -0
  7. package/dist/types/boot/runners/checkpoint.test.d.ts +12 -0
  8. package/dist/types/boot/runners/delegate-runner.d.ts +83 -0
  9. package/dist/types/boot/runners/delegate-runner.test.d.ts +13 -0
  10. package/dist/types/boot/runners/memdir.d.ts +103 -0
  11. package/dist/types/boot/runners/memdir.test.d.ts +12 -0
  12. package/dist/types/boot/runners/read-state.d.ts +82 -0
  13. package/dist/types/boot/runners/read-state.test.d.ts +10 -0
  14. package/dist/types/boot/runners/session.d.ts +37 -2
  15. package/dist/types/boot/runners/session.test.d.ts +10 -0
  16. package/dist/types/briefing/context-docs.d.ts +38 -0
  17. package/dist/types/briefing/context-docs.test.d.ts +18 -0
  18. package/dist/types/briefing/index.d.ts +2 -0
  19. package/dist/types/capability-deck/cards/index.d.ts +6 -0
  20. package/dist/types/capability-deck/cards/memory-card.d.ts +9 -10
  21. package/dist/types/capability-deck/cards/plan-file.d.ts +56 -0
  22. package/dist/types/capability-deck/cards/plan-tools.d.ts +97 -0
  23. package/dist/types/capability-deck/cards/plan-tools.test.d.ts +9 -0
  24. package/dist/types/capability-deck/checkpoint.int.test.d.ts +25 -0
  25. package/dist/types/capability-deck/index.d.ts +1 -1
  26. package/dist/types/capability-deck/read-edit-gate.int.test.d.ts +21 -0
  27. package/dist/types/conductor/bash-guard.d.ts +106 -0
  28. package/dist/types/conductor/bash-guard.test.d.ts +17 -0
  29. package/dist/types/conductor/conductor.d.ts +38 -6
  30. package/dist/types/conductor/contract.d.ts +221 -2
  31. package/dist/types/conductor/diagnostics.d.ts +183 -0
  32. package/dist/types/conductor/diagnostics.test.d.ts +10 -0
  33. package/dist/types/conductor/index.d.ts +4 -1
  34. package/dist/types/conductor/permission-gate.integration.test.d.ts +22 -0
  35. package/dist/types/conductor/permission-wiring.test.d.ts +14 -0
  36. package/dist/types/conductor/permissions.d.ts +217 -0
  37. package/dist/types/conductor/permissions.test.d.ts +12 -0
  38. package/dist/types/conductor/plan-mode.integration.test.d.ts +23 -0
  39. package/dist/types/conductor/post-edit-diagnostics.test.d.ts +13 -0
  40. package/dist/types/conductor/transcript-store/serialize.test.d.ts +10 -0
  41. package/dist/types/conductor/transcript-store/store.d.ts +18 -0
  42. package/dist/types/console/components/Banner.d.ts +28 -6
  43. package/dist/types/console/components/Emblem.d.ts +49 -0
  44. package/dist/types/console/components/StatusBar.d.ts +14 -3
  45. package/dist/types/console/components/WorkingIndicator.d.ts +44 -0
  46. package/dist/types/console/components/WorkingIndicator.test.d.ts +9 -0
  47. package/dist/types/console/components/banner-sweep.d.ts +55 -0
  48. package/dist/types/console/components/banner.test.d.ts +9 -0
  49. package/dist/types/console/contract.d.ts +41 -7
  50. package/dist/types/console/input/keymap.d.ts +10 -1
  51. package/dist/types/console/overlays/approval-queue.d.ts +71 -0
  52. package/dist/types/console/overlays/approval.d.ts +104 -0
  53. package/dist/types/console/overlays/approval.test.d.ts +17 -0
  54. package/dist/types/console/overlays/host.d.ts +4 -3
  55. package/dist/types/console/overlays/index.d.ts +2 -0
  56. package/dist/types/console/theme/adapter.d.ts +19 -0
  57. package/dist/types/console/theme/index.d.ts +1 -1
  58. package/dist/types/console/theme/palette.d.ts +25 -0
  59. package/dist/types/console/theme/tokens.d.ts +23 -1
  60. package/dist/types/index.d.ts +1 -1
  61. package/dist/types/launch/contract.d.ts +2 -0
  62. package/dist/types/launch/index.d.ts +1 -1
  63. package/dist/types/launch/oauth.d.ts +13 -0
  64. package/dist/types/settings/contract.d.ts +59 -0
  65. package/dist/types/settings/index.d.ts +2 -2
  66. package/dist/types/window-budget/condenser.d.ts +15 -1
  67. package/dist/types/window-budget/index.d.ts +3 -1
  68. package/dist/types/window-budget/microcompact.d.ts +68 -0
  69. package/dist/types/window-budget/microcompact.test.d.ts +16 -0
  70. package/dist/types/window-budget/rehydrate.d.ts +56 -0
  71. package/dist/types/workspace/brand.d.ts +6 -0
  72. package/dist/types/workspace/index.d.ts +1 -1
  73. package/package.json +2 -2
@@ -123,6 +123,8 @@ export interface Invocation {
123
123
  readonly prompt?: string;
124
124
  /** Explicit model selector from the command line, if any (`--model` / `-m`). */
125
125
  readonly modelId?: string;
126
+ /** Model to fall back to when the bound model is overloaded mid-turn (`--fallback-model`). */
127
+ readonly fallbackModelId?: string;
126
128
  /** Working directory the run is scoped to; absent means the process cwd (`--cwd`). */
127
129
  readonly cwd?: string;
128
130
  /** Named credential account to authenticate with (`--account`). */
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Boot helper: activate the addon host for a session and fold its tool
3
+ * interceptors around the deck.
4
+ *
5
+ * The {@link createAddonHost addon host} is the product's extension mechanism —
6
+ * locally-authored modules under `<cwd>/.indus/addons` that graft tools, slash
7
+ * commands, lifecycle observers, and tool-boundary interceptors onto a running
8
+ * session. The host itself is fully built and tested, but until this module it
9
+ * was never *instantiated* outside its own unit tests: nothing in the boot path
10
+ * discovered, loaded, or wired addons.
11
+ *
12
+ * This module closes that gap with two narrow seams the runners call once at
13
+ * session-assembly time:
14
+ *
15
+ * 1. {@link buildAddonHost} — construct a host, swallow its fault stream (a
16
+ * broken addon must never sink the session), and {@link AddonHost.loadAll
17
+ * load} every addon discovered under the run's `.indus/addons` directory.
18
+ * With no such directory the returned {@link AddonSurfaceBundle} is empty —
19
+ * no interceptors, no contributed tools — so wiring it is a perfect no-op.
20
+ * 2. {@link wrapToolsWithAddons} — fold the bundle's
21
+ * {@link InterceptorChain} around every tool whose name a stage matches,
22
+ * leaving the rest identity-equal. A wrapped tool runs the chain's
23
+ * `enter` → real `execute` → `exit` reduce; an `enter` block short-circuits
24
+ * to an `isError` result without ever invoking the real tool.
25
+ *
26
+ * Scope (v1): only the per-tool interceptor boundary is wired. The richer
27
+ * lifecycle-event fan-out (`session:start`, `turn:end`, …) needs a conductor
28
+ * seam that does not exist yet and is deferred. The tool interceptor boundary is
29
+ * the load-bearing one — it is where the Wave 4 permission gate later registers
30
+ * as a built-in interceptor rather than a parallel mechanism.
31
+ */
32
+ import { type AddonSurfaceBundle, type AgentTool, type InterceptorChain } from "../../addons";
33
+ import type { BootContext } from "../contract";
34
+ /**
35
+ * Build and populate the addon host for a run, returning its wired
36
+ * {@link AddonSurfaceBundle}.
37
+ *
38
+ * Constructs a host with an empty {@link FrameworkHandles} bag (v1 supplies no
39
+ * `exec` handle — the runner has no shell-exec callback to hand an addon — and
40
+ * the rest are interactive-only), installs a swallow-everything fault sink so a
41
+ * broken addon degrades silently instead of crashing the boot, then discovers
42
+ * and loads every addon under `<cwd>/.indus/addons` (the contract's default
43
+ * {@link ADDONS_DIR}). With no addons directory the bundle is empty and wiring it
44
+ * downstream is a no-op.
45
+ *
46
+ * @param ctx the boot context whose invocation carries the run cwd
47
+ * @returns the wired bundle (dispatch, interceptors, contributed commands/tools)
48
+ */
49
+ export declare function buildAddonHost(ctx: BootContext): Promise<AddonSurfaceBundle>;
50
+ /**
51
+ * Fold a bundle's {@link InterceptorChain} around every tool a stage matches.
52
+ *
53
+ * Each tool whose `name` the chain matches is replaced with a wrapper that runs
54
+ * the chain around its real `execute`; every other tool is returned *identical*
55
+ * (same object reference) so an empty bundle leaves the deck untouched. The
56
+ * contributed `bundle.tools` are NOT appended here — the caller concatenates
57
+ * them (de-duped against the existing deck) before wrapping, so addon tools are
58
+ * themselves subject to interception.
59
+ *
60
+ * @param tools the run's tool deck (deck + MCP + already-concatenated addon tools)
61
+ * @param bundle the loaded addon bundle whose interceptor chain wraps the deck
62
+ * @returns a new array: matched tools wrapped, unmatched tools identity-equal
63
+ */
64
+ export declare function wrapToolsWithAddons(tools: AgentTool[], bundle: AddonSurfaceBundle): AgentTool[];
65
+ /**
66
+ * Wrap one tool so its `execute` runs through the interceptor chain.
67
+ *
68
+ * The wrapper spreads the original tool (preserving `name` / `description` /
69
+ * `parameters` / `label` and any extra fields) and overrides only `execute`. The
70
+ * override:
71
+ *
72
+ * - closes over the live `toolCallId`, `signal`, and `onUpdate` so streaming
73
+ * updates and cancellation still reach the real tool (the chain only threads
74
+ * the decoded `args`, so these must be captured here);
75
+ * - hands the chain a `(args) => realExecute(args)` closure as the inner
76
+ * execution and runs `chain.run({ tool, callId, args }, …)`;
77
+ * - maps the resulting {@link InterceptResult} back to an
78
+ * {@link AgentToolResult}: a `blocked` enter short-circuits to an `isError`
79
+ * result carrying the gate's `reason` (the real tool never ran); otherwise
80
+ * the chain's (possibly exit-rewritten) `result` is returned.
81
+ *
82
+ * The chain — not this wrapper — owns fault isolation and the throw-on-no-recover
83
+ * semantics, so a tool error that no exit stage recovers propagates exactly as it
84
+ * would unwrapped.
85
+ *
86
+ * @param tool the real tool to fold the chain around
87
+ * @param chain the interceptor chain matching this tool's name
88
+ */
89
+ export declare function wrapOne(tool: AgentTool, chain: InterceptorChain): AgentTool;
90
+ /**
91
+ * Concatenate an addon bundle's contributed tools onto an existing deck, dropping
92
+ * any whose name a deck tool already claims.
93
+ *
94
+ * The host de-dupes its OWN tools against each other, but not against the product
95
+ * deck or MCP tools — so an addon tool named `read` would otherwise shadow (or
96
+ * be appended alongside) the core read tool. This filter keeps the first
97
+ * claimant (the existing deck) and admits only addon tools with a fresh name.
98
+ *
99
+ * @param deck the existing tool deck (deck + MCP)
100
+ * @param bundle the loaded addon bundle whose `tools` are appended
101
+ * @returns the deck followed by the non-conflicting addon tools
102
+ */
103
+ export declare function concatAddonTools(deck: AgentTool[], bundle: AddonSurfaceBundle): AgentTool[];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Addon wiring — boot-time activation of the addon host (item #19).
3
+ *
4
+ * These tests exercise the two seams this module adds to the boot path WITHOUT a
5
+ * real conductor, real jiti, or disk: a fake {@link ModuleLoader} maps a source
6
+ * path to a scripted {@link AddonManifest}, the host loads it through
7
+ * `explicitPaths` (which never touches the filesystem), and the resulting bundle
8
+ * is folded around a synthetic tool deck.
9
+ *
10
+ * Four concerns:
11
+ * 1. An `enter` block short-circuits the call — the wrapper returns an
12
+ * `isError` result carrying the gate reason and the real `execute` NEVER
13
+ * runs.
14
+ * 2. An `exit` rewrite replaces the tool's result.
15
+ * 3. A tool no interceptor matches is returned IDENTITY-equal (the empty-bundle
16
+ * / no-addons path is a perfect no-op).
17
+ * 4. Contributed addon tools are concatenated, de-duped against the deck.
18
+ */
19
+ export {};
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Per-session file-checkpoint store — the product half of rewind (#24).
3
+ *
4
+ * The framework's mutating file tools (write/edit) consult a host-injected handle
5
+ * on `ctx.framework` under the string-literal key {@link CHECKPOINT_HANDLE_KEY}.
6
+ * Immediately before a file is written — and after the read-before-edit gate has
7
+ * passed — the framework reads the file's CURRENT on-disk content and hands it to
8
+ * the handle as `record(absPath, previous)`, where `previous` is the old bytes or
9
+ * `null` when the file did not yet exist. This module supplies the missing host
10
+ * piece: a store that files those pre-mutation snapshots against the transcript
11
+ * node that was active when the mutation happened, so a later rewind to that node
12
+ * can roll the working tree back to exactly that point.
13
+ *
14
+ * Design notes:
15
+ * - **Keyed by the ACTIVE transcript node id.** Each snapshot is filed under the
16
+ * id the conductor pins as the active node (the head leaf at turn start). All
17
+ * of a turn's edits therefore land under ONE node id; navigating back to that
18
+ * node and restoring reverts the whole turn's file changes at once.
19
+ * - **First-seen wins per path within a node.** If the same file is edited twice
20
+ * in one turn, only the FIRST snapshot (the content as it was when the node
21
+ * became active) is kept. Overwriting it would lose the true pre-turn state,
22
+ * so a second `record` of the same path under the same node is ignored. This
23
+ * also makes the framework's pre-write `record` idempotent across the multiple
24
+ * mutations one tool call may perform.
25
+ * - **`null` previous means "file was absent".** Restoring a node whose snapshot
26
+ * marks a file `null` DELETES that file (it did not exist at that point), so a
27
+ * rewind that created a file is undone by removing it.
28
+ * - **Duck-typed handle.** The store implements exactly the framework's
29
+ * `CheckpointHandle` shape (`record(absPath, previous: string | null): void`)
30
+ * so the host injector and the framework consumer agree by STRING KEY and
31
+ * STRUCTURAL SHAPE — no cross-package type import. `ctx.framework` is an open
32
+ * record, so the injection typechecks immediately; the framework consumption
33
+ * activates after the framework is rebuilt and the product refreshed.
34
+ * - **One instance PER SESSION** (minted in `session.ts`), shared across the
35
+ * write/edit cards through the single `ctx.framework` bag, and handed to the
36
+ * conductor so the conductor can advance the active node id as the head moves.
37
+ *
38
+ * Mirrors the read-edit-gate's `ReadStateStore` conventions (`READ_STATE_HANDLE_KEY`,
39
+ * `createReadStateStore`); both coexist in the SAME `ctx.framework` open bag.
40
+ */
41
+ /**
42
+ * The string-literal key under which the host wires this store onto the deck's
43
+ * `ctx.framework` bag.
44
+ *
45
+ * Mirrors the existing `DELEGATE_HANDLE_KEY = "delegate"`,
46
+ * `MEMORY_HANDLE_KEY = "memoryStore"`, and `READ_STATE_HANDLE_KEY = "readState"`
47
+ * conventions. The framework's mutating file tools read
48
+ * `ctx.framework["checkpoint"]` by the same literal, so the two sides agree by
49
+ * string rather than by a shared imported symbol.
50
+ */
51
+ export declare const CHECKPOINT_HANDLE_KEY: "checkpoint";
52
+ /**
53
+ * One file's pre-mutation snapshot: the path that was mutated and the content it
54
+ * held immediately before. `previous === null` records that the file did not
55
+ * exist on disk before the mutation, so restoring this snapshot deletes it.
56
+ */
57
+ export interface FileSnapshot {
58
+ readonly path: string;
59
+ readonly previous: string | null;
60
+ }
61
+ /**
62
+ * Per-session file-checkpoint store, keyed by transcript node id.
63
+ *
64
+ * For each node id it holds a map of normalized absolute path → the FIRST-seen
65
+ * pre-mutation content for that path under that node. The framework calls
66
+ * {@link record} (via the duck-typed handle) before every write; the host pins
67
+ * the active node id with {@link setActiveNodeId} as the conductor's head moves;
68
+ * and {@link restore} rolls the working tree back to a node's recorded state.
69
+ */
70
+ export declare class CheckpointStore {
71
+ /** node id → (normalized abs path → first-seen pre-mutation content). */
72
+ private readonly byNode;
73
+ /** The transcript node the next {@link record} files its snapshot under. */
74
+ private current;
75
+ /** Normalize an absolute path into the store's canonical per-node key. */
76
+ private key;
77
+ /** The node id new snapshots are currently filed under (`null` until pinned). */
78
+ activeNodeId(): string | null;
79
+ /**
80
+ * Pin the transcript node subsequent {@link record} calls file snapshots under.
81
+ *
82
+ * The conductor calls this as its head advances (at turn start), so all of a
83
+ * turn's edits key to the node that was active before the turn ran. Passing the
84
+ * same id again is harmless.
85
+ *
86
+ * @param id the active transcript node id, or `null` to fall back to the root
87
+ */
88
+ setActiveNodeId(id: string | null): void;
89
+ /** The node id snapshots are filed under right now (active id or the root). */
90
+ private resolveNodeId;
91
+ /**
92
+ * Capture the pre-mutation content of `absPath` under the active node.
93
+ *
94
+ * The framework's duck-typed `CheckpointHandle` entry point. Called once per
95
+ * mutated path immediately before the write lands, with the file's OLD on-disk
96
+ * content (or `null` when the file did not exist). First-seen wins: a later
97
+ * `record` of the same path under the same node is ignored so the snapshot
98
+ * reflects the file's state when the node became active, not a mid-turn rewrite.
99
+ *
100
+ * @param absPath absolute path of the file about to be written
101
+ * @param previous the file's content before this mutation, or `null` if absent
102
+ */
103
+ record(absPath: string, previous: string | null): void;
104
+ /**
105
+ * The recorded snapshots for a node, or `[]` when nothing was tracked there.
106
+ *
107
+ * Each entry pairs the normalized absolute path with the content it held before
108
+ * the node's first mutation of it (`null` = the file was absent).
109
+ *
110
+ * @param nodeId the transcript node to read snapshots for
111
+ */
112
+ snapshotFor(nodeId: string): readonly FileSnapshot[];
113
+ /** Whether a node has ANY recorded file snapshot (the picker's restore gate). */
114
+ hasSnapshot(nodeId: string): boolean;
115
+ /**
116
+ * Roll the working tree back to a node's recorded state.
117
+ *
118
+ * For each path the node tracked: when its snapshot is a string, the file is
119
+ * rewritten with that pre-mutation content (creating parent directories as
120
+ * needed); when the snapshot is `null` the file is DELETED (it did not exist at
121
+ * that point). Returns the absolute paths that were restored, in record order.
122
+ *
123
+ * A safe no-op when the node has no snapshot — navigating to a node that never
124
+ * mutated files restores nothing. Per-path failures are swallowed so one
125
+ * unwritable path never aborts the rest of the restore.
126
+ *
127
+ * @param nodeId the transcript node whose file state to restore to
128
+ * @returns the absolute paths that were written or deleted
129
+ */
130
+ restore(nodeId: string): string[];
131
+ }
132
+ /** Mint a fresh, empty per-session checkpoint store. */
133
+ export declare function createCheckpointStore(): CheckpointStore;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * CheckpointStore unit tests — the product half of rewind (#24).
3
+ *
4
+ * Cover the store's contract in isolation (no framework tools):
5
+ * - per-node FIRST-SEEN snapshot (a path's snapshot within one node is never
6
+ * overwritten by a later record);
7
+ * - keying follows the active node id (set via `setActiveNodeId`);
8
+ * - `restore` writes the recorded old content back AND deletes files whose
9
+ * snapshot was `null` (absent at that point);
10
+ * - `restore` of an untracked node is a safe no-op.
11
+ */
12
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Boot helper: a live {@link DelegateRunner} that makes the `task` tool real.
3
+ *
4
+ * The `task` capability ({@link "../../capability-deck/cards/task-card"}) advertises
5
+ * sub-agent delegation but only *runs* it when a {@link DelegateRunner} is wired
6
+ * into the deck context under {@link DELEGATE_HANDLE_KEY}; absent one it degrades
7
+ * to a typed `STUB_NOTE`. This module supplies that runner.
8
+ *
9
+ * Each delegated objective spins a *fresh, isolated* framework {@link Agent} (the
10
+ * same `facade/bot` Agent the conductor already drives — NOT the swarm `Crew` nor
11
+ * `runtime/createAgent`) bound to the parent's model id and credential resolver,
12
+ * given a survey-style system prompt and a deck that DELIBERATELY excludes the
13
+ * `task` card. That exclusion is the recursion guard: a sub-agent with no `task`
14
+ * tool cannot spawn its own sub-agents. The runner submits one prompt, lets the
15
+ * sub-agent run its own tool loop to completion, then extracts the final
16
+ * assistant turn's text as the single report handed back to the parent.
17
+ *
18
+ * The runner never throws out of `run()`: a model that does not resolve, a
19
+ * sub-agent error, or an empty transcript all map to a `{ ok:false, report }`
20
+ * result so a delegation failure surfaces to the parent agent as an ordinary
21
+ * tool error rather than crashing the turn. Cancellation is forwarded: an
22
+ * already-aborted signal short-circuits, and an abort raised mid-run calls
23
+ * `agent.abort()`.
24
+ *
25
+ * The `spawn`/`tools` options are pure test seams — they let a unit test drive
26
+ * the runner with an in-memory fake instead of a real network round-trip.
27
+ */
28
+ import { type AgentMessage } from "../../conductor";
29
+ import { type AgentTool } from "../../capability-deck";
30
+ import type { DelegateRunner } from "../../capability-deck/cards/task-card";
31
+ /**
32
+ * The minimal sub-agent surface the runner drives.
33
+ *
34
+ * A real framework {@link Agent} satisfies this structurally; a test passes a
35
+ * lightweight fake via {@link DelegateRunnerOptions.spawn}. Only the pieces the
36
+ * runner touches are named — submit a prompt, abort, and read the resulting
37
+ * transcript/error afterward.
38
+ */
39
+ export interface DelegateSubAgent {
40
+ prompt(input: string): Promise<void>;
41
+ abort(): void;
42
+ readonly state: {
43
+ messages: readonly AgentMessage[];
44
+ error?: string;
45
+ };
46
+ }
47
+ /** Configuration for {@link createDelegateRunner}. */
48
+ export interface DelegateRunnerOptions {
49
+ /** The model id the sub-agent runs under (the parent's resolved model). */
50
+ readonly modelId: string;
51
+ /** The working directory the sub-agent's deck is scoped to. */
52
+ readonly cwd: string;
53
+ /** The system prompt that shapes the sub-agent's behaviour. */
54
+ readonly system: string;
55
+ /**
56
+ * Per-call credential resolver, forwarded to the framework `Agent` unchanged
57
+ * (OAuth-only providers, short-lived token rotation). Omitted from the agent
58
+ * options entirely when undefined so the framework env-var lookup still wins.
59
+ */
60
+ readonly getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
61
+ /**
62
+ * Test seam: build the sub-agent from the objective instead of constructing a
63
+ * real framework `Agent`. When omitted the runner uses `new Agent`.
64
+ */
65
+ readonly spawn?: (objective: string, context?: string) => DelegateSubAgent;
66
+ /**
67
+ * Test seam: the tool deck the sub-agent runs with. When omitted the runner
68
+ * provisions the read-only `'authoring'` profile (which already excludes the
69
+ * `task` card — the recursion guard).
70
+ */
71
+ readonly tools?: () => AgentTool[];
72
+ }
73
+ /**
74
+ * Build a live {@link DelegateRunner} the host wires into the deck context.
75
+ *
76
+ * The model is resolved once up front; if the id resolves to nothing the runner
77
+ * still builds but every `run()` reports `{ ok:false }` rather than throwing, so
78
+ * a misconfigured model can never crash the parent's turn.
79
+ *
80
+ * @param opts the model id, cwd, system prompt, and optional credential/test seams
81
+ * @returns a runner satisfying the task card's {@link DelegateRunner} contract
82
+ */
83
+ export declare function createDelegateRunner(opts: DelegateRunnerOptions): DelegateRunner;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Delegate-runner wiring (#10 / fixes #9).
3
+ *
4
+ * Proves the chain that makes the advertised `task` tool functional: a live
5
+ * {@link createDelegateRunner} spins an isolated sub-agent, runs one objective,
6
+ * and reports back the sub-agent's final assistant text; and the `task` card,
7
+ * given that runner under {@link DELEGATE_HANDLE_KEY}, delegates for real instead
8
+ * of returning its `STUB_NOTE`.
9
+ *
10
+ * Every test drives the runner through the `opts.spawn` / `opts.tools` seams so
11
+ * no model is resolved and no network is touched.
12
+ */
13
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Durable cross-session memory — a disk-backed working-memory store and the
3
+ * project-context document that carries it into each session's system prompt.
4
+ *
5
+ * The `memory` capability ({@link "../../capability-deck/cards/memory-card"})
6
+ * already reads a host-injected store from `ctx.framework[MEMORY_HANDLE_KEY]` and
7
+ * falls back to an in-process buffer when none is wired. This module supplies the
8
+ * missing piece: a synchronous {@link DiskMemoryStore} that persists the agent's
9
+ * note to a per-cwd `MEMORY.md` under the profile directory, so a fact written in
10
+ * one session is still there the next time the agent runs in the same directory.
11
+ *
12
+ * The same file is loaded into the briefing as a {@link ContextDoc} ({@link
13
+ * loadMemoryDoc}) so the model *sees* its prior memory at the top of a fresh
14
+ * session, not only when it explicitly calls the `memory` tool. The body is
15
+ * line-and-byte capped by {@link truncateEntrypointContent} so a runaway memory
16
+ * file cannot bloat the prompt.
17
+ *
18
+ * Design notes:
19
+ * - The store is STRICTLY SYNCHRONOUS — the {@link MemoryStore} contract the
20
+ * card validates is sync (`read`/`replace`/`append` return `void`/`string`),
21
+ * and an async store would silently fail that validation and degrade to the
22
+ * in-memory fallback.
23
+ * - The memory directory is scoped UNDER `workspace.profileDir`, NOT the cwd, so
24
+ * persisting memory never writes files into the user's working tree.
25
+ * - The cwd is slugged with the SAME regex as {@link "./session".sessionScopeDir}
26
+ * so the per-cwd partitioning lines up with the session layout.
27
+ * - Files are written `0o600` (owner-only), matching the auth vault — the memory
28
+ * note can hold sensitive project facts.
29
+ */
30
+ import type { ContextDoc } from "../../briefing";
31
+ import { MEMORY_HANDLE_KEY, type MemoryStore } from "../../capability-deck";
32
+ import type { Workspace } from "../contract";
33
+ /** The on-disk filename the durable memory note is stored under. */
34
+ export declare const MEMORY_ENTRYPOINT = "MEMORY.md";
35
+ /** Maximum number of lines of the memory note inlined into the prompt. */
36
+ export declare const MAX_ENTRYPOINT_LINES = 200;
37
+ /**
38
+ * Maximum bytes of the memory note inlined into the prompt.
39
+ *
40
+ * ~125 chars/line at 200 lines: this catches long-line indexes that slip past
41
+ * the line cap (a few very long lines can be far larger than 200 short ones).
42
+ */
43
+ export declare const MAX_ENTRYPOINT_BYTES = 25000;
44
+ /**
45
+ * Cap the memory note to {@link MAX_ENTRYPOINT_LINES} and
46
+ * {@link MAX_ENTRYPOINT_BYTES}, appending a warning naming which cap fired.
47
+ *
48
+ * Line-truncates first (a natural boundary), then byte-truncates at the last
49
+ * newline before the cap so a line is never cut mid-way. Content within both
50
+ * caps is returned trimmed and unchanged.
51
+ *
52
+ * @param raw the raw memory-file text
53
+ * @returns the prompt-safe body (possibly with a trailing truncation warning)
54
+ */
55
+ export declare function truncateEntrypointContent(raw: string): string;
56
+ /**
57
+ * The per-cwd memory directory under the profile dir: `<profileDir>/memory/--<slug>--`.
58
+ *
59
+ * The cwd is slugged — every non-alphanumeric run collapsed to a single dash —
60
+ * and wrapped in `--…--` markers, the SAME scheme `sessionScopeDir` uses for the
61
+ * session transcript layout, so the two partitionings line up. The directory
62
+ * lives under `workspace.profileDir`, never inside the repo, so persisted memory
63
+ * never pollutes the user's working tree.
64
+ *
65
+ * @param workspace the resolved on-disk layout (supplies the profile dir)
66
+ * @param cwd the run's working directory
67
+ */
68
+ export declare function memoryDirFor(workspace: Workspace, cwd: string): string;
69
+ /**
70
+ * A synchronous, disk-backed {@link MemoryStore} persisting the working-memory
71
+ * note to `<memDir>/MEMORY.md`.
72
+ *
73
+ * Satisfies the `memory` card's narrow three-method port. STRICTLY synchronous:
74
+ * the card validates that each of `read`/`replace`/`append` is a function and
75
+ * calls them inline, so any async variant would break the contract and silently
76
+ * fall back to the in-memory store. Writes create the directory and use mode
77
+ * `0o600` (owner-only), matching the auth vault.
78
+ */
79
+ export declare class DiskMemoryStore implements MemoryStore {
80
+ private readonly memDir;
81
+ private readonly file;
82
+ constructor(memDir: string);
83
+ /** The current note, or `""` when no file has been written yet. */
84
+ read(): string;
85
+ /** Overwrite the note, creating the memory directory as needed (mode 0o600). */
86
+ replace(content: string): void;
87
+ /** Add a single line to the end of the note (composed of read + replace). */
88
+ append(line: string): void;
89
+ }
90
+ /**
91
+ * Load the per-cwd memory note as a {@link ContextDoc} for the briefing's
92
+ * `# Project context` section, or `undefined` when there is nothing to show.
93
+ *
94
+ * An absent file, an empty/whitespace-only note, or any read error yields
95
+ * `undefined` (no project-context block); a non-empty note is capped by
96
+ * {@link truncateEntrypointContent} before inlining. The doc's `path` is the
97
+ * bare `MEMORY.md` label, not the on-disk location, so the heading reads cleanly.
98
+ *
99
+ * @param memDir the per-cwd memory directory from {@link memoryDirFor}
100
+ */
101
+ export declare function loadMemoryDoc(memDir: string): ContextDoc | undefined;
102
+ /** Re-exported so hosts wiring a store can use one import. */
103
+ export { MEMORY_HANDLE_KEY };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Durable cross-session memory (memdir, #27).
3
+ *
4
+ * Proves the disk-backed memory chain: {@link memoryDirFor} scopes a per-cwd
5
+ * directory under the profile (never the cwd); {@link DiskMemoryStore} round-trips
6
+ * the `memory` card's sync `read`/`replace`/`append` to `<memDir>/MEMORY.md` with
7
+ * owner-only perms; and {@link loadMemoryDoc} surfaces that file as a briefing
8
+ * {@link ContextDoc} (capped by {@link truncateEntrypointContent}) — absent or
9
+ * empty for a fresh cwd. Together they let a fact written in one session reappear
10
+ * in the next session's prompt.
11
+ */
12
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Per-session read-state store — the product half of the read-before-edit gate
3
+ * (read-edit-gate, #11).
4
+ *
5
+ * The framework's file tools (read/write/edit) consult a host-injected handle on
6
+ * `ctx.framework` under the string-literal key {@link READ_STATE_HANDLE_KEY} to
7
+ * decide whether an edit/write may proceed: a file must have been read before it
8
+ * is mutated, and an edit is refused when the file changed on disk since that
9
+ * read. This module supplies the missing host piece — an in-memory map of which
10
+ * files have been read this session and the stat fingerprint at read time — so
11
+ * the gate has somewhere to record and look up that state.
12
+ *
13
+ * Design notes:
14
+ * - The store is a thin wrapper over a `Map`, keyed by `path.normalize(absPath)`
15
+ * so a write and a later lookup of the SAME file agree on one key regardless
16
+ * of how the path was spelled (trailing slashes, `.`/`..` segments).
17
+ * - One instance is created PER SESSION (in `session.ts` `selectTools`) and
18
+ * shared across the read/write/edit cards through the single `ctx.framework`
19
+ * bag, so a read recorded by one tool is visible to the gate in another.
20
+ * - The record carries the stat fields the framework gate compares against:
21
+ * `mtimeMs` (the file's last-modified time at read), `size`, an optional
22
+ * `contentHash` for a content-equality fallback, and `readAt` (when the host
23
+ * recorded the read). The framework's own stat result exposes the modified
24
+ * time as `modifiedMs`; the host normalizes it into `mtimeMs` on the way in,
25
+ * so this record is the single agreed shape.
26
+ * - The handle key is a bare string literal (mirroring the deck's
27
+ * `DELEGATE_HANDLE_KEY` / `MEMORY_HANDLE_KEY`) so the framework consumer and
28
+ * this product injector agree WITHOUT a cross-package type import. `ctx.framework`
29
+ * is an open record, so the injection typechecks immediately.
30
+ */
31
+ /**
32
+ * The string-literal key under which the host wires this store onto the deck's
33
+ * `ctx.framework` bag.
34
+ *
35
+ * Mirrors the existing `DELEGATE_HANDLE_KEY = "delegate"` and
36
+ * `MEMORY_HANDLE_KEY = "memoryStore"` conventions. The framework's file tools
37
+ * read `ctx.framework["readState"]` by the same literal, so the two sides agree
38
+ * by string rather than by a shared imported symbol.
39
+ */
40
+ export declare const READ_STATE_HANDLE_KEY: "readState";
41
+ /**
42
+ * One file's read fingerprint, recorded when the agent reads it this session.
43
+ *
44
+ * - `mtimeMs` — the file's last-modified time (ms) at read. The gate refuses an
45
+ * edit when the on-disk mtime has advanced past this. (The framework's stat
46
+ * result names this `modifiedMs`; the host maps it to `mtimeMs` here.)
47
+ * - `size` — the file's byte length at read, a cheap secondary change signal.
48
+ * - `contentHash` — optional digest of the content read, for a content-equality
49
+ * fallback when the mtime advanced but the bytes are unchanged.
50
+ * - `readAt` — when the host recorded this read (ms since epoch).
51
+ */
52
+ export interface ReadStateRecord {
53
+ readonly mtimeMs: number;
54
+ readonly size: number;
55
+ readonly contentHash?: string;
56
+ readonly readAt: number;
57
+ }
58
+ /**
59
+ * In-memory map of read files for one session, keyed by normalized absolute path.
60
+ *
61
+ * The framework gate `set`s a record after every read and after every successful
62
+ * mutation, and `get`s/`has`-checks it before an edit. Keys are normalized in one
63
+ * place ({@link ReadStateStore.key}) so writes and lookups of the same file land
64
+ * on the same entry no matter how the caller spelled the path.
65
+ */
66
+ export declare class ReadStateStore {
67
+ private readonly entries;
68
+ /** Normalize an absolute path into the store's canonical key. */
69
+ private key;
70
+ /** The recorded read fingerprint for a path, or `undefined` if never read. */
71
+ get(absPath: string): ReadStateRecord | undefined;
72
+ /** Record (or replace) the read fingerprint for a path; returns this store. */
73
+ set(absPath: string, value: ReadStateRecord): this;
74
+ /** Whether a path has a recorded read fingerprint this session. */
75
+ has(absPath: string): boolean;
76
+ /** Drop a path's recorded read fingerprint; returns whether one existed. */
77
+ delete(absPath: string): boolean;
78
+ /** Forget every recorded read for this session. */
79
+ clear(): void;
80
+ }
81
+ /** Mint a fresh, empty per-session read-state store. */
82
+ export declare function createReadStateStore(): ReadStateStore;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Per-session read-state store (read-edit-gate, #11, product half).
3
+ *
4
+ * Proves the in-memory map the read-before-edit gate records into: records are
5
+ * keyed by `path.normalize(absPath)` (so differently-spelled paths to the same
6
+ * file collapse to one entry), `get`/`set`/`has`/`delete`/`clear` round-trip the
7
+ * `{ mtimeMs, size, contentHash?, readAt }` record, the handle key matches the
8
+ * literal the framework consumer reads, and each session gets its own store.
9
+ */
10
+ export {};
@@ -8,8 +8,12 @@
8
8
  * {@link ModelMatcher}; the conductor itself is built lazily so no framework agent
9
9
  * is constructed until the first turn runs.
10
10
  */
11
- import { type SessionConductor } from "../../conductor";
12
- import type { BootContext } from "../contract";
11
+ import { type AgentTool, type SessionConductor } from "../../conductor";
12
+ import { type MemoryStore } from "../../capability-deck";
13
+ import { type DelegateRunner } from "../../capability-deck/cards/task-card";
14
+ import { type CheckpointStore } from "./checkpoint";
15
+ import { type ContextDoc, type SkillCard } from "../../briefing";
16
+ import type { BootContext, Invocation } from "../contract";
13
17
  /**
14
18
  * Resolve the model id for this run.
15
19
  *
@@ -21,6 +25,37 @@ import type { BootContext } from "../contract";
21
25
  * @returns the canonical model id to bind the session to
22
26
  */
23
27
  export declare function resolveModelId(ctx: BootContext): string;
28
+ /**
29
+ * Gather the on-disk skill cards the model may invoke this run: walk the project
30
+ * and user `.indusagi/skills` roots and drop any card flagged
31
+ * `disable-model-invocation` (those stay loadable explicitly, but the model is
32
+ * not told about them). A filesystem walk error degrades to an empty list so a
33
+ * bad/unreadable skills dir never sinks the session.
34
+ */
35
+ export declare function gatherModelSkills(cwd: string): SkillCard[];
36
+ /**
37
+ * Select the tool deck for the run, honouring `--no-tools` (empty) and `--tools`
38
+ * (allow-list). Tool ids are matched case-insensitively with `_`/`-` stripped, so
39
+ * a `--tools web_fetch,todo_read` request lines up with the deck's `webfetch` /
40
+ * `todoread` ids.
41
+ */
42
+ /**
43
+ * @param runner an optional live sub-agent {@link DelegateRunner}; when present it
44
+ * is injected under {@link DELEGATE_HANDLE_KEY} so the `task` card delegates for
45
+ * real instead of returning its `STUB_NOTE`. Omitted by callers that only need
46
+ * a deck (the card then degrades gracefully).
47
+ * @param checkpoint an optional per-session {@link CheckpointStore}; when present
48
+ * it is injected under {@link CHECKPOINT_HANDLE_KEY} so the framework's
49
+ * write/edit tools snapshot a file's pre-mutation content (rewind, #24). Omitted
50
+ * callers get no checkpointing (the tools no-op the snapshot).
51
+ */
52
+ export declare function selectTools(cwd: string, inv: Invocation, runner?: DelegateRunner, memoryStore?: MemoryStore, checkpoint?: CheckpointStore): AgentTool[];
53
+ /**
54
+ * Compose the run's system prompt: `--system` replaces the built-in briefing,
55
+ * `--append-system` adds a trailing block, and both compose (override then
56
+ * append). With neither, it is the tool-aware built-in briefing.
57
+ */
58
+ export declare function composeSystem(tools: AgentTool[], inv: Invocation, cwd: string, skills?: readonly SkillCard[], memoryDoc?: ContextDoc): string;
24
59
  /**
25
60
  * The cwd-scoped session directory under the workspace `sessions/` root.
26
61
  *
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Skills-surface wiring (#7 / fixes #11).
3
+ *
4
+ * Proves the chain that makes on-disk `SKILL.md` cards visible to the model:
5
+ * `gatherModelSkills` walks `cwd/.indusagi/skills`, drops any card flagged
6
+ * `disable-model-invocation`, and the survivors render into the briefing's
7
+ * `<available_skills>` block via `composeSystem`. A `--system` override must NOT
8
+ * carry the skills block (it replaces the whole prompt).
9
+ */
10
+ export {};