indusagi-coding-agent 0.1.62 → 0.2.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 (67) hide show
  1. package/dist/entry.js +5395 -1509
  2. package/dist/guardrails.js +2031 -0
  3. package/dist/index.js +18371 -0
  4. package/dist/types/boot/contract.d.ts +2 -0
  5. package/dist/types/boot/index.d.ts +1 -1
  6. package/dist/types/boot/runners/addon-wiring.d.ts +103 -0
  7. package/dist/types/boot/runners/addon-wiring.test.d.ts +19 -0
  8. package/dist/types/boot/runners/checkpoint.d.ts +133 -0
  9. package/dist/types/boot/runners/checkpoint.test.d.ts +12 -0
  10. package/dist/types/boot/runners/delegate-runner.d.ts +83 -0
  11. package/dist/types/boot/runners/delegate-runner.test.d.ts +13 -0
  12. package/dist/types/boot/runners/index.d.ts +2 -0
  13. package/dist/types/boot/runners/memdir.d.ts +103 -0
  14. package/dist/types/boot/runners/memdir.test.d.ts +12 -0
  15. package/dist/types/boot/runners/read-state.d.ts +82 -0
  16. package/dist/types/boot/runners/read-state.test.d.ts +10 -0
  17. package/dist/types/boot/runners/session.d.ts +37 -2
  18. package/dist/types/boot/runners/session.test.d.ts +10 -0
  19. package/dist/types/briefing/context-docs.d.ts +38 -0
  20. package/dist/types/briefing/context-docs.test.d.ts +18 -0
  21. package/dist/types/briefing/index.d.ts +2 -0
  22. package/dist/types/capability-deck/cards/index.d.ts +6 -0
  23. package/dist/types/capability-deck/cards/memory-card.d.ts +9 -10
  24. package/dist/types/capability-deck/cards/plan-file.d.ts +56 -0
  25. package/dist/types/capability-deck/cards/plan-tools.d.ts +97 -0
  26. package/dist/types/capability-deck/cards/plan-tools.test.d.ts +9 -0
  27. package/dist/types/capability-deck/checkpoint.int.test.d.ts +25 -0
  28. package/dist/types/capability-deck/index.d.ts +1 -1
  29. package/dist/types/capability-deck/read-edit-gate.int.test.d.ts +21 -0
  30. package/dist/types/conductor/bash-guard.d.ts +106 -0
  31. package/dist/types/conductor/bash-guard.test.d.ts +17 -0
  32. package/dist/types/conductor/conductor.d.ts +37 -6
  33. package/dist/types/conductor/contract.d.ts +214 -2
  34. package/dist/types/conductor/diagnostics.d.ts +183 -0
  35. package/dist/types/conductor/diagnostics.test.d.ts +10 -0
  36. package/dist/types/conductor/index.d.ts +4 -1
  37. package/dist/types/conductor/permission-gate.integration.test.d.ts +22 -0
  38. package/dist/types/conductor/permission-wiring.test.d.ts +14 -0
  39. package/dist/types/conductor/permissions.d.ts +217 -0
  40. package/dist/types/conductor/permissions.test.d.ts +12 -0
  41. package/dist/types/conductor/plan-mode.integration.test.d.ts +23 -0
  42. package/dist/types/conductor/post-edit-diagnostics.test.d.ts +13 -0
  43. package/dist/types/conductor/transcript-store/serialize.test.d.ts +10 -0
  44. package/dist/types/conductor/transcript-store/store.d.ts +18 -0
  45. package/dist/types/console/components/StatusBar.d.ts +14 -3
  46. package/dist/types/console/components/WorkingIndicator.d.ts +44 -0
  47. package/dist/types/console/components/WorkingIndicator.test.d.ts +9 -0
  48. package/dist/types/console/contract.d.ts +2 -1
  49. package/dist/types/console/input/keymap.d.ts +10 -1
  50. package/dist/types/console/overlays/approval-queue.d.ts +71 -0
  51. package/dist/types/console/overlays/approval.d.ts +104 -0
  52. package/dist/types/console/overlays/approval.test.d.ts +17 -0
  53. package/dist/types/console/overlays/host.d.ts +4 -3
  54. package/dist/types/console/overlays/index.d.ts +2 -0
  55. package/dist/types/guardrails.d.ts +33 -0
  56. package/dist/types/launch/contract.d.ts +2 -0
  57. package/dist/types/launch/index.d.ts +1 -1
  58. package/dist/types/launch/oauth.d.ts +13 -0
  59. package/dist/types/settings/contract.d.ts +47 -0
  60. package/dist/types/settings/index.d.ts +2 -2
  61. package/dist/types/window-budget/condenser.d.ts +15 -1
  62. package/dist/types/window-budget/index.d.ts +3 -1
  63. package/dist/types/window-budget/microcompact.d.ts +68 -0
  64. package/dist/types/window-budget/microcompact.test.d.ts +16 -0
  65. package/dist/types/window-budget/rehydrate.d.ts +56 -0
  66. package/dist/types/workspace/brand.d.ts +1 -1
  67. package/package.json +14 -3
@@ -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 {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Project-context document loader — gathers repository convention files
3
+ * (CLAUDE.md / AGENTS.md and brand variants) into {@link ContextDoc}s that the
4
+ * briefing's `PROJECT_CONTEXT_SECTION` inlines under `# Project context`.
5
+ *
6
+ * The walk runs the ancestor chain from the filesystem root down to the cwd, so
7
+ * the cwd's own files land LAST in the returned array — and since the renderer
8
+ * treats array order as priority order, the closest (most specific) file wins.
9
+ * The home directory is scanned after the chain as a global fallback. Each file
10
+ * may pull additional files inline via `@import` references (bounded to a depth
11
+ * of {@link DEFAULT_MAX_IMPORT_DEPTH} with a per-walk realpath guard so cycles
12
+ * and symlink loops cannot recurse or hang).
13
+ *
14
+ * The whole body is wrapped so that a permissions error on one ancestor, a
15
+ * binary file, or a malformed tree degrades to `[]` rather than crashing boot —
16
+ * repo conventions are a best-effort enrichment, never a hard dependency.
17
+ */
18
+ import type { ContextDoc } from "./contract";
19
+ /** Options for {@link gatherContextDocs}. Every field is optional. */
20
+ export interface GatherContextDocsOptions {
21
+ /** Maximum `@import` recursion depth. Defaults to {@link DEFAULT_MAX_IMPORT_DEPTH}. */
22
+ readonly maxImportDepth?: number;
23
+ /** Maximum bytes retained per document body (post-trim). Defaults to {@link DEFAULT_MAX_BYTES_PER_DOC}. */
24
+ readonly maxBytesPerDoc?: number;
25
+ }
26
+ /**
27
+ * Gather the repository's project-context documents into an ordered
28
+ * {@link ContextDoc} array for the briefing.
29
+ *
30
+ * Order: the ancestor chain from root → cwd (so cwd's files land last = highest
31
+ * priority), then the home directory as a global fallback. Within each directory
32
+ * the {@link CANDIDATE_FILES} are read in their listed order. Each file may pull
33
+ * `@import` children inline (depth-bounded, cycle-guarded). De-duplication is by
34
+ * realpath, so a file reachable through two roots is inlined once.
35
+ *
36
+ * Never throws: any failure in the walk yields the docs gathered so far (or `[]`).
37
+ */
38
+ export declare function gatherContextDocs(cwd: string, home: string, opts?: GatherContextDocsOptions): readonly ContextDoc[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Project-context loader — colocated unit tests.
3
+ *
4
+ * Every test runs against a fresh tmpdir tree (the `src/boot/boot.test.ts`
5
+ * sandbox pattern): no real home directory, no network. The realpath of the
6
+ * tmpdir base is taken up front because macOS resolves `/var/folders` → a
7
+ * `/private/...` symlink, which the loader's realpath de-dup would otherwise
8
+ * surface in path assertions.
9
+ *
10
+ * Pinned behaviors:
11
+ * - a cwd CLAUDE.md body is inlined;
12
+ * - an ancestor AGENTS.md orders BEFORE the cwd doc (cwd = highest priority);
13
+ * - an `@import` pulls a sibling file inline;
14
+ * - a cyclic import terminates (does not hang) and inlines each file once;
15
+ * - missing files and binary (non-text-extension) imports are skipped;
16
+ * - a home-root CLAUDE.md is included exactly once even when cwd is under home.
17
+ */
18
+ export {};
@@ -25,3 +25,5 @@ export { scanMacroBody, resolveTokens, applyMacros, buildMacroScope, expandInvoc
25
25
  export type { LoadMacrosOptions, FrontmatterSplit } from "./macros";
26
26
  export { loadSkillCards, gatherSkillCards, modelInvocableCards, } from "./skills";
27
27
  export type { SkillRoot } from "./skills";
28
+ export { gatherContextDocs } from "./context-docs";
29
+ export type { GatherContextDocsOptions } from "./context-docs";
@@ -23,8 +23,14 @@ export { daemonCard, buildDaemonCapability, DaemonTable, type DaemonState, type
23
23
  export { taskCard, buildTaskCapability, DELEGATE_HANDLE_KEY, type DelegateRunner, type DelegateRequest, type DelegateResult, type TaskParamsType, type TaskDetails, } from "./task-card";
24
24
  export { saasCard, buildSaasCapability, SAAS_GATEWAY_KEY, type SaasGatewayPort, type RemoteToolSummary, type RemoteExecution, type SaasParamsType, type SaasDetails, } from "./saas-card";
25
25
  export { memoryCard, buildMemoryCapability, InMemoryStore, MEMORY_HANDLE_KEY, type MemoryStore, type MemoryParamsType, type MemoryDetails, } from "./memory-card";
26
+ export { enterPlanModeCard, exitPlanModeCard, buildEnterPlanModeCapability, buildExitPlanModeCapability, PLAN_HANDLE_KEY, type PlanController, type EnterPlanParamsType, type EnterPlanDetails, type ExitPlanParamsType, type ExitPlanDetails, } from "./plan-tools";
27
+ export { planSlug, planFilePath, writePlan, readPlan, PLANS_DIRNAME } from "./plan-file";
26
28
  /**
27
29
  * The app-novel cards, in catalog order. The manifest module concatenates these
28
30
  * with the builtin-bridge cards to build the full `CAPABILITY_CARDS` array.
31
+ *
32
+ * The plan-mode tools live here (the broadest `all` profile only): plan mode is a
33
+ * gate over mutating work, so the tools are only meaningful in a full-access deck —
34
+ * never the read-only `authoring` subset.
29
35
  */
30
36
  export declare const APP_NOVEL_CARDS: readonly CapabilityCard[];
@@ -5,16 +5,15 @@
5
5
  * Status: minimal in-memory implementation + clearly-typed seam for the
6
6
  * framework memory subsystem.
7
7
  *
8
- * The framework's `indusagi/memory` facade is not yet populated with a public
9
- * working-memory store, so this card ships a self-contained, framework-agnostic
10
- * implementation: a single mutable text buffer the agent overwrites or appends
11
- * to, scoped to one built capability (one session). When the framework exposes a
12
- * persistent memory store, swap the {@link MemoryStore} default for an adapter
13
- * over it via {@link DeckContext.framework} — the {@link Capability} surface and
14
- * the tool's wire contract do not change.
15
- *
16
- * TODO(framework-memory): adapt `indusagi/memory` once it exports a public
17
- * working-memory store; read it from `ctx.framework[MEMORY_HANDLE_KEY]`.
8
+ * This card ships a self-contained, framework-agnostic default: a single mutable
9
+ * text buffer the agent overwrites or appends to, scoped to one built capability
10
+ * (one session). A host that wants persistence injects a durable store under
11
+ * {@link MEMORY_HANDLE_KEY} via {@link DeckContext.framework} — the boot layer's
12
+ * `DiskMemoryStore` (`boot/runners/memdir.ts`) does exactly this, persisting the
13
+ * note to a per-cwd `MEMORY.md` so it survives across sessions. The
14
+ * {@link Capability} surface and the tool's wire contract do not change either
15
+ * way; {@link readStore} validates the injected store's three methods and falls
16
+ * back to {@link InMemoryStore} when none is wired.
18
17
  *
19
18
  * The single tool keys behavior on an `action` discriminant:
20
19
  * - `read` — return the current working-memory note.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Plan-file persistence — write and read the approved plan a plan-mode session
3
+ * produces, as a slug-named markdown file under a per-session plans directory.
4
+ *
5
+ * Plan mode is a read-only "research first, act second" stance: the agent
6
+ * explores under {@link EnterPlanMode}, drafts a plan, and proposes leaving plan
7
+ * mode with {@link ExitPlanMode}. When the user approves the exit, the plan text
8
+ * the model authored is durable — saved here so it survives the session and can
9
+ * be reviewed, diffed, or fed back later. The conductor owns the approval
10
+ * handshake; this module owns only the bytes-on-disk concern.
11
+ *
12
+ * The filename is derived from a stable slug of the plan's first line (its de
13
+ * facto title), prefixed with a short timestamp so successive plans in one
14
+ * session never collide. Writing is best-effort from the caller's perspective —
15
+ * a write failure surfaces as a thrown error the conductor swallows into a
16
+ * non-fatal note rather than faulting the turn.
17
+ */
18
+ /** The directory plans are written under (relative to the session scope dir). */
19
+ export declare const PLANS_DIRNAME: "plans";
20
+ /**
21
+ * Derive a filesystem-safe slug from a plan's text — its first non-empty line,
22
+ * lower-cased, with every run of non-alphanumeric characters collapsed to a
23
+ * single dash and the result trimmed and length-capped. Falls back to `"plan"`
24
+ * when the text yields nothing usable (empty, or punctuation-only).
25
+ *
26
+ * @param plan the plan body whose first line seeds the slug
27
+ */
28
+ export declare function planSlug(plan: string): string;
29
+ /**
30
+ * Resolve the absolute path a plan with `slug` is written to under `sessionDir`.
31
+ * The `plans/` sub-directory is implied; callers pass the session scope dir and
32
+ * get back `<sessionDir>/plans/<slug>.md`.
33
+ *
34
+ * @param sessionDir the per-session directory plans live beneath
35
+ * @param slug the derived slug (see {@link planSlug})
36
+ */
37
+ export declare function planFilePath(sessionDir: string, slug: string): string;
38
+ /**
39
+ * Write the plan body to a slug-named markdown file under `sessionDir/plans`,
40
+ * creating the directory as needed, and return the absolute path written. The
41
+ * filename is `<short-timestamp>-<slug>.md` so repeated plans in one session do
42
+ * not overwrite each other.
43
+ *
44
+ * Throws on an I/O failure (the conductor swallows it into a non-fatal note).
45
+ *
46
+ * @param sessionDir the per-session directory plans live beneath
47
+ * @param plan the plan body to persist
48
+ */
49
+ export declare function writePlan(sessionDir: string, plan: string): string;
50
+ /**
51
+ * Read a previously-written plan file back, or `undefined` when it is missing or
52
+ * unreadable. Used by tests and by any review surface that wants the saved plan.
53
+ *
54
+ * @param path the absolute plan-file path (see {@link planFilePath} / {@link writePlan})
55
+ */
56
+ export declare function readPlan(path: string): string | undefined;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Plan-mode capabilities — the two tools that bracket a read-only planning phase.
3
+ *
4
+ * - {@link enterPlanModeTool} (`enter_plan_mode`) is a **read-only** tool: when
5
+ * invoked it returns a structured detail `{ enterPlanMode: true }` plus
6
+ * explore-only prose. It carries no side effect of its own — the **conductor**
7
+ * watches the tool-result seam for `enterPlanMode` and switches the session
8
+ * into the `plan` permission mode (research-only — every mutating tool is
9
+ * blocked at the permission gate), capturing the pre-plan mode so it can be
10
+ * restored on exit. It is marked `readOnly: true` so it is itself always
11
+ * permitted, even inside plan mode.
12
+ *
13
+ * - {@link exitPlanModeTool} (`exit_plan_mode`) likewise does NOT flip the mode
14
+ * itself — a tool cannot drive the interactive approval dialog. It returns a
15
+ * structured detail `{ exitPlan: true, plan }` that the conductor intercepts:
16
+ * the conductor raises the user-approval prompt, and on approval restores the
17
+ * pre-plan mode + injects the approved plan as context for the next turn,
18
+ * persisting it to a plan file. On reject the session stays in plan mode.
19
+ *
20
+ * Keeping every side effect (mode flip, approval, persistence) in the conductor
21
+ * rather than the tool keeps the tools pure, side-effect-light, and assemblable in
22
+ * any environment, including tests — and lets the conductor own the single
23
+ * permission-mode source of truth.
24
+ */
25
+ import { type Static } from "@sinclair/typebox";
26
+ import type { Capability, CapabilityCard, DeckContext } from "../contract";
27
+ /**
28
+ * Reserved key under which a host MAY wire a plan-mode controller into the deck
29
+ * context. The conductor-driven design intercepts the plan tools' result details
30
+ * directly (no handle required), so this is exported only for forward-compat /
31
+ * an alternate host that wants the tool to drive the flip itself.
32
+ */
33
+ export declare const PLAN_HANDLE_KEY: "planController";
34
+ /**
35
+ * The narrow port a host may wire to flip the live session into plan mode. Unused
36
+ * by the default (conductor-intercept) wiring; kept for hosts that prefer the tool
37
+ * to signal directly rather than via the tool-result seam.
38
+ */
39
+ export interface PlanController {
40
+ /** Switch the session into read-only plan mode for subsequent tool calls. */
41
+ enterPlanMode(): void;
42
+ }
43
+ declare const EnterPlanParams: import("@sinclair/typebox").TObject<{}>;
44
+ /** Statically-inferred parameter type EnterPlanMode's `execute` receives. */
45
+ export type EnterPlanParamsType = Static<typeof EnterPlanParams>;
46
+ /** Structured detail returned by EnterPlanMode. */
47
+ export interface EnterPlanDetails {
48
+ /**
49
+ * The marker the conductor intercepts on the tool-result seam to switch the
50
+ * session into plan mode (and capture the pre-plan mode for a later restore).
51
+ */
52
+ readonly enterPlanMode: true;
53
+ /** Whether an optional in-context controller also applied the switch directly. */
54
+ readonly applied: boolean;
55
+ }
56
+ /**
57
+ * Build the EnterPlanMode capability. On invoke it asks the wired
58
+ * {@link PlanController} to switch the session into plan mode, then returns
59
+ * explore-only prose. It is `readOnly: true`, so the permission gate always
60
+ * permits it (including inside plan mode itself).
61
+ *
62
+ * @param ctx the deck context; an optional controller is read from
63
+ * `ctx.framework[PLAN_HANDLE_KEY]`.
64
+ */
65
+ export declare function buildEnterPlanModeCapability(ctx: DeckContext): Capability<typeof EnterPlanParams, EnterPlanDetails>;
66
+ declare const ExitPlanParams: import("@sinclair/typebox").TObject<{
67
+ plan: import("@sinclair/typebox").TString;
68
+ }>;
69
+ /** Statically-inferred parameter type ExitPlanMode's `execute` receives. */
70
+ export type ExitPlanParamsType = Static<typeof ExitPlanParams>;
71
+ /**
72
+ * Structured detail returned by ExitPlanMode. The conductor watches the
73
+ * tool-result seam for `exitPlan === true`, captures the {@link plan}, and runs
74
+ * the approval handshake (the tool itself does not flip the mode or prompt).
75
+ */
76
+ export interface ExitPlanDetails {
77
+ /** The marker the conductor intercepts to start the exit-plan handshake. */
78
+ readonly exitPlan: true;
79
+ /** The plan text the model authored, surfaced to the user for approval. */
80
+ readonly plan: string;
81
+ }
82
+ /**
83
+ * Build the ExitPlanMode capability. It performs no mode flip and no prompt of
84
+ * its own — it returns the `{ exitPlan: true, plan }` detail the conductor
85
+ * intercepts. The model-facing content echoes the proposed plan so a transcript
86
+ * reader sees what was put up for approval.
87
+ *
88
+ * Not marked read-only: it represents the request to RESUME mutating work, so in
89
+ * plan mode it must still be permitted explicitly (the gate special-cases the
90
+ * plan tools alongside read-only tools — see the conductor wiring).
91
+ */
92
+ export declare function buildExitPlanModeCapability(_ctx: DeckContext): Capability<typeof ExitPlanParams, ExitPlanDetails>;
93
+ /** Catalog row for the EnterPlanMode capability. */
94
+ export declare const enterPlanModeCard: CapabilityCard;
95
+ /** Catalog row for the ExitPlanMode capability. */
96
+ export declare const exitPlanModeCard: CapabilityCard;
97
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Plan-mode cards + plan-file helpers — focused unit tests.
3
+ *
4
+ * The cards are pure capability builders: EnterPlanMode is read-only and returns
5
+ * the `{ enterPlanMode: true }` marker the conductor intercepts; ExitPlanMode
6
+ * returns the `{ exitPlan: true, plan }` detail the conductor's handshake reads.
7
+ * The plan-file helpers slug a title and round-trip the bytes on disk.
8
+ */
9
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * File checkpoint / code rewind — PRODUCT integration test (rewind, #24).
3
+ *
4
+ * This proves the checkpoint records through the ACTUAL wired path the product
5
+ * uses, NOT a hand-rolled harness:
6
+ *
7
+ * 1. We provision a real deck via {@link provisionDeck} with a `framework` bag
8
+ * carrying a real per-session {@link CheckpointStore} under the SAME
9
+ * `checkpoint` handle key `session.ts` injects (`CHECKPOINT_HANDLE_KEY`).
10
+ * 2. We pull the WIRED `edit` capability out of that deck — the same `AgentTool`
11
+ * object the conductor would run — and edit a file through it.
12
+ *
13
+ * The framework's edit tool calls `ctx.framework['checkpoint'].record(absPath,
14
+ * previous)` immediately before the write lands, with the file's OLD on-disk
15
+ * content. If the bridge failed to thread the store into `createEditTool`, the
16
+ * handle would be `undefined` and nothing would be recorded — so the assertions
17
+ * below only pass when the store reaches the tool through the real wiring.
18
+ *
19
+ * We then call `store.restore(nodeId)` and assert the on-disk content reverts to
20
+ * the pre-edit bytes, closing the loop end-to-end.
21
+ *
22
+ * Note: no `readState` handle is wired here, so the read-before-edit gate is OFF
23
+ * and the edit proceeds without a prior read — isolating the checkpoint behavior.
24
+ */
25
+ export {};
@@ -31,7 +31,7 @@ export { CAPABILITY_CARDS, CAPABILITY_INDEX, CARD_PROFILES, capabilityIds, hasCa
31
31
  * (checklist, background-process proxy, delegate/sub-agent, SaaS connector,
32
32
  * working memory) plus their builders, stores, and injection-handle types.
33
33
  */
34
- export { APP_NOVEL_CARDS, todoCard, buildTodoCapability, TodoLedger, type TodoItem, type TodoState, type TodoWeight, type TodoParamsType, type TodoDetails, daemonCard, buildDaemonCapability, DaemonTable, type DaemonState, type DaemonParamsType, type DaemonDetails, taskCard, buildTaskCapability, DELEGATE_HANDLE_KEY, type DelegateRunner, type DelegateRequest, type DelegateResult, type TaskParamsType, type TaskDetails, saasCard, buildSaasCapability, SAAS_GATEWAY_KEY, type SaasGatewayPort, type RemoteToolSummary, type RemoteExecution, type SaasParamsType, type SaasDetails, memoryCard, buildMemoryCapability, InMemoryStore, MEMORY_HANDLE_KEY, type MemoryStore, type MemoryParamsType, type MemoryDetails, } from "./cards/index";
34
+ export { APP_NOVEL_CARDS, todoCard, buildTodoCapability, TodoLedger, type TodoItem, type TodoState, type TodoWeight, type TodoParamsType, type TodoDetails, daemonCard, buildDaemonCapability, DaemonTable, type DaemonState, type DaemonParamsType, type DaemonDetails, taskCard, buildTaskCapability, DELEGATE_HANDLE_KEY, type DelegateRunner, type DelegateRequest, type DelegateResult, type TaskParamsType, type TaskDetails, saasCard, buildSaasCapability, SAAS_GATEWAY_KEY, type SaasGatewayPort, type RemoteToolSummary, type RemoteExecution, type SaasParamsType, type SaasDetails, memoryCard, buildMemoryCapability, InMemoryStore, MEMORY_HANDLE_KEY, type MemoryStore, type MemoryParamsType, type MemoryDetails, enterPlanModeCard, exitPlanModeCard, buildEnterPlanModeCapability, buildExitPlanModeCapability, PLAN_HANDLE_KEY, planSlug, planFilePath, writePlan, readPlan, PLANS_DIRNAME, type PlanController, type EnterPlanParamsType, type EnterPlanDetails, type ExitPlanParamsType, type ExitPlanDetails, } from "./cards/index";
35
35
  /**
36
36
  * Bridge ledger — event-sourced enrollment of dynamically grafted MCP tools:
37
37
  * content-hash / ULID key minting, the immutable {@link BridgeLedger} value with
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Read-before-edit gate — PRODUCT integration test (read-edit-gate, #11).
3
+ *
4
+ * This proves the gate fires through the ACTUAL wired path the product uses:
5
+ *
6
+ * 1. We provision a real deck via {@link provisionDeck} with a `framework` bag
7
+ * that carries a real per-session {@link ReadStateStore} under the same
8
+ * `readState` handle key `session.ts` injects (`READ_STATE_HANDLE_KEY`).
9
+ * 2. We pull the WIRED `read` and `edit` capabilities out of that deck — the
10
+ * same `AgentTool` objects the conductor would run — and exercise them.
11
+ *
12
+ * The framework's edit tool consults `ctx.framework['readState']` to enforce the
13
+ * gate. If the bridge failed to thread the store into `createEditTool`, the
14
+ * handle would be `undefined` and the gate would no-op — so an edit of an unread
15
+ * file would succeed instead of being refused. These assertions therefore only
16
+ * pass when the store reaches the tool through the real wiring.
17
+ *
18
+ * The refusal message is byte-stable (the framework's `READ_BEFORE_EDIT_MESSAGE`)
19
+ * and asserted verbatim, so a wording drift on either side is caught.
20
+ */
21
+ export {};
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Bash command parser/classifier + catastrophic-command blocklist.
3
+ *
4
+ * The permission engine ({@link "./permissions"}) matches a tool call against
5
+ * induscode-style `Bash(...)` allow/ask/deny rules. For most tools the call is a
6
+ * single atomic action, so a rule's argument specifier matches against one
7
+ * subject string. The shell tool is different: `a && b; c | d` is FOUR commands
8
+ * in one call, and a coarse "match the whole command string" check is trivially
9
+ * bypassed — a benign-looking prefix can smuggle a destructive tail past an
10
+ * allow rule (`npm test && rm -rf /`), or hide a blocked binary behind a wrapper
11
+ * (`env X=1 rm -rf /`, `sudo rm -rf /`) or quoting.
12
+ *
13
+ * This module makes bash matching real. It is split in two concerns:
14
+ *
15
+ * 1. **A parser/classifier** — {@link parseBashCommand} splits a shell command
16
+ * string into its constituent *simple* commands across the control
17
+ * operators (`;`, `&&`, `||`, `|`, newlines) and `( … )` subshells /
18
+ * `$( … )` / `` ` … ` `` command substitutions, strips benign leading
19
+ * wrappers (`env VAR=x`, `nice`, `nohup`, `timeout N`, `xargs`, `command`,
20
+ * `builtin`, and `sudo`/`doas` — the last recorded as an *elevation* flag),
21
+ * and returns each base command + its raw argument string. Quoting is
22
+ * handled well enough that `"rm -rf /"` cannot hide behind quotes or a
23
+ * wrapper. The split is quote-aware so an operator *inside* a quoted string
24
+ * (`echo "a && b"`) is NOT treated as a command boundary.
25
+ *
26
+ * 2. **A catastrophic blocklist** — {@link isCatastrophicCommand} matches a
27
+ * single parsed sub-command against a conservative set of always-deny
28
+ * patterns (root-targeting `rm -rf`, fork bombs, `curl … | sh` of remote
29
+ * scripts, `dd of=/dev/…`, `mkfs`, `chmod -R 777 /`, redirect-to-raw-disk).
30
+ * It is deliberately narrow so normal dev commands (`rm -rf node_modules`,
31
+ * `rm -rf ./dist`, `curl … -o file`) are NOT blocked. This is
32
+ * defense-in-depth, NOT a hard security boundary — the OS sandbox and the
33
+ * permission gate are the real boundary.
34
+ *
35
+ * Everything here is pure, synchronous, and side-effect free so the classifier
36
+ * and the blocklist are exhaustively unit-testable, and the permission engine
37
+ * can fold them into its decision without any I/O.
38
+ */
39
+ /**
40
+ * One simple command extracted from a (possibly compound) shell string.
41
+ *
42
+ * `rm -rf /tmp` parses to `{ name: "rm", args: ["-rf", "/tmp"], elevated: false,
43
+ * raw: "rm -rf /tmp" }`. The `name` is the base binary AFTER benign wrappers are
44
+ * stripped; `args` are its tokens (quotes removed); `raw` is the original
45
+ * sub-command slice (wrappers included) so a caller can echo exactly what the
46
+ * model asked for; `elevated` is true when a `sudo`/`doas` wrapper was stripped.
47
+ */
48
+ export interface ParsedCommand {
49
+ /** The base binary, lower-cased for stable comparison (e.g. `"rm"`). */
50
+ readonly name: string;
51
+ /** The argument tokens after the binary, with surrounding quotes removed. */
52
+ readonly args: readonly string[];
53
+ /** Whether a `sudo`/`doas` elevation wrapper preceded this command. */
54
+ readonly elevated: boolean;
55
+ /** The original sub-command slice (wrappers included), trimmed. */
56
+ readonly raw: string;
57
+ }
58
+ /**
59
+ * Parse a (possibly compound) shell command string into the flat list of every
60
+ * simple command it runs — across operators, subshells, and command
61
+ * substitutions, with benign wrappers stripped.
62
+ *
63
+ * The result is order-preserving and exhaustive enough that the permission
64
+ * engine can evaluate a `Bash(...)` rule (and the catastrophic blocklist)
65
+ * against EACH sub-command independently: a deny on any sub-command denies the
66
+ * whole call, and an allow must cover every sub-command to auto-allow.
67
+ *
68
+ * @returns one {@link ParsedCommand} per simple command; an empty array for an
69
+ * empty/whitespace command string.
70
+ */
71
+ export declare function parseBashCommand(command: string): ParsedCommand[];
72
+ /**
73
+ * Classify a SINGLE parsed sub-command against the catastrophic blocklist.
74
+ *
75
+ * @returns the byte-stable deny reason when the command is catastrophic, else
76
+ * `undefined`. Use {@link evaluateCatastrophic} for a whole compound call (it
77
+ * also runs the cross-sub-command `curl | sh` correlation).
78
+ */
79
+ export declare function catastrophicReason(cmd: ParsedCommand): string | undefined;
80
+ /** Convenience boolean form of {@link catastrophicReason}. */
81
+ export declare function isCatastrophicCommand(cmd: ParsedCommand): boolean;
82
+ /**
83
+ * Evaluate the catastrophic blocklist over a WHOLE bash command string.
84
+ *
85
+ * Parses the (possibly compound) command, runs the per-sub-command blocklist on
86
+ * each leaf, AND runs the cross-sub-command `curl | sh` correlation. Returns a
87
+ * byte-stable deny message when ANY check fires, else `undefined`.
88
+ *
89
+ * This is what the permission engine calls for the bash tool: a catastrophic
90
+ * command is denied REGARDLESS of allow/ask/deny rules or mode (it is not even
91
+ * `bypass`-able, mirroring the original always-block guard).
92
+ */
93
+ export declare function evaluateCatastrophic(command: string): string | undefined;
94
+ /**
95
+ * Render the rule-matching SUBJECT for each sub-command of a bash call: the base
96
+ * command joined with its argument tokens (wrappers stripped). The permission
97
+ * engine matches an induscode `Bash(...)` specifier against EACH of these — an
98
+ * allow rule must cover them all to auto-allow, a deny on any one denies the
99
+ * whole call.
100
+ *
101
+ * For `npm test && rm important.txt` this yields `["npm test", "rm
102
+ * important.txt"]`, so `Bash(npm test)` allows the first but not the second.
103
+ * Returns the original command (single element) when nothing parses, so a tool
104
+ * call always has at least one subject to match against.
105
+ */
106
+ export declare function bashSubcommandSubjects(command: string): string[];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bash command parser/classifier + catastrophic blocklist — unit tests.
3
+ *
4
+ * Pins the parser ({@link parseBashCommand}) across the four concerns that make
5
+ * coarse whole-string matching trivially bypassable:
6
+ * - **compound** splitting on `;`, `&&`, `||`, `|`, `&`, newlines,
7
+ * - benign **wrapper** stripping (`env VAR=x`, `nohup`, `timeout N`, `nice -n
8
+ * N`, `xargs`, `command`, and `sudo`/`doas` as elevation),
9
+ * - **subshell / command-substitution** descent (`( … )`, `$( … )`, `` ` … ` ``),
10
+ * - **quote-aware** tokenization so an operator inside a quote is not a
11
+ * boundary and a quoted/escaped binary cannot hide from the classifier,
12
+ * plus the documented bypass cases. Then the catastrophic blocklist
13
+ * ({@link evaluateCatastrophic}) — `rm -rf /` and root-ish targets, fork bombs,
14
+ * `curl | sh`, raw-disk writes, `mkfs`, `chmod -R 777 /` — alongside the
15
+ * negative cases proving normal dev commands are NOT blocked. No I/O.
16
+ */
17
+ export {};
@@ -36,6 +36,7 @@ import { type ConductorFault, type ConductorPhase, type ConductorState, type Ses
36
36
  import { ModelMatcher } from "./catalog";
37
37
  import { SignalHub } from "./signal-hub";
38
38
  import { TranscriptStore } from "./transcript-store";
39
+ import { DiagnosticsEngine, type DiagnosticsConfig } from "./diagnostics";
39
40
  /**
40
41
  * The slice of the framework `Agent` the conductor drives.
41
42
  *
@@ -73,12 +74,30 @@ export interface AgentLike {
73
74
  setThinkingLevel?(level: ThinkingLevel): void;
74
75
  }
75
76
  /**
76
- * The pluggable condense hook. Given the active branch's messages, it returns the
77
- * (smaller) message list to replace it with. The real window-budget engine arrives
78
- * in Phase 3; the {@link noopCondense} default is an identity transform.
77
+ * Options handed to a {@link CondenseFn} on each invocation.
78
+ *
79
+ * Every field is optional so a scripted/no-op hook can ignore the bag entirely:
80
+ * - `force` — a manual `/compact` (fold to the last user turn) vs the
81
+ * budget-gated auto path.
82
+ * - `model` — the model bound to the live session, forwarded so the
83
+ * summarizer can write a *real* digest instead of the
84
+ * no-model local stub. Omitted when no model is bound.
85
+ * - `contextTokens` — the conductor's latest provider-reported context
86
+ * occupancy, an optional anchor for the slice planner.
87
+ */
88
+ export interface CondenseOpts {
89
+ readonly force?: boolean;
90
+ readonly model?: Model<any>;
91
+ readonly contextTokens?: number;
92
+ }
93
+ /**
94
+ * The pluggable condense hook. Given the active branch's messages (and an options
95
+ * bag), it returns the (smaller) message list to replace it with. The live
96
+ * window-budget engine is wired in `boot/runners/session.ts`; the
97
+ * {@link noopCondense} default is an identity transform that ignores the opts.
79
98
  */
80
- export type CondenseFn = (messages: AgentMessage[], force?: boolean) => AgentMessage[] | Promise<AgentMessage[]>;
81
- /** The default condense hook: returns the input unchanged (no-op). */
99
+ export type CondenseFn = (messages: AgentMessage[], opts?: CondenseOpts) => AgentMessage[] | Promise<AgentMessage[]>;
100
+ /** The default condense hook: returns the input unchanged (no-op), ignoring opts. */
82
101
  export declare const noopCondense: CondenseFn;
83
102
  /** Tuning for the transient-fault auto-retry. */
84
103
  export interface RetryPolicy {
@@ -105,10 +124,22 @@ export interface ConductorDeps {
105
124
  readonly condense?: CondenseFn;
106
125
  /** Auto-retry tuning. */
107
126
  readonly retry?: RetryPolicy;
108
- /** Branch length past which auto-compaction fires (default 200). */
127
+ /**
128
+ * Legacy branch-length condense threshold. The auto path is now token-gated
129
+ * ({@link isOverBudget}), so this field is accepted but no longer governs when
130
+ * auto-compaction fires; kept for back-compat with callers that pass it.
131
+ */
109
132
  readonly compactAt?: number;
110
133
  /** Sleep primitive (injected so tests don't actually wait). */
111
134
  readonly sleep?: (ms: number) => Promise<void>;
135
+ /**
136
+ * Post-edit live-diagnostics control. Pass a ready {@link DiagnosticsEngine} to
137
+ * inject one directly (tests), a {@link DiagnosticsConfig} to tune the default
138
+ * engine, or omit it to get the default: ON when `workspace` has a
139
+ * `tsconfig.json` (eslint runner added when an eslint config is present),
140
+ * otherwise inert. Set `{ enabled: false }` to opt out entirely.
141
+ */
142
+ readonly diagnostics?: DiagnosticsEngine | DiagnosticsConfig;
112
143
  }
113
144
  /** The transitions the reducer understands — fresh vocabulary, all typed. */
114
145
  type StateAction = {