indusagi-coding-agent 0.2.8 → 0.2.9

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 (219) hide show
  1. package/package.json +2 -1
  2. package/src/_decls/entry.ts +18 -0
  3. package/src/_decls/guardrails.ts +35 -0
  4. package/src/_decls/index.ts +26 -0
  5. package/src/addons/contract.ts +236 -0
  6. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  7. package/src/addons/dispatch/index.ts +25 -0
  8. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  9. package/src/addons/host.ts +225 -0
  10. package/src/addons/index.ts +112 -0
  11. package/src/addons/manifest.ts +158 -0
  12. package/src/addons/sandbox.ts +170 -0
  13. package/src/addons/surface.ts +78 -0
  14. package/src/boot/auth-vault.ts +195 -0
  15. package/src/boot/boot.ts +138 -0
  16. package/src/boot/contract.ts +238 -0
  17. package/src/boot/heap.ts +59 -0
  18. package/src/boot/index.ts +28 -0
  19. package/src/boot/invocation.ts +93 -0
  20. package/src/boot/runners/addon-wiring.ts +153 -0
  21. package/src/boot/runners/checkpoint.ts +169 -0
  22. package/src/boot/runners/delegate-runner.ts +294 -0
  23. package/src/boot/runners/index.ts +13 -0
  24. package/src/boot/runners/link-runner.ts +45 -0
  25. package/src/boot/runners/memdir.ts +168 -0
  26. package/src/boot/runners/oneshot-runner.ts +58 -0
  27. package/src/boot/runners/read-state.ts +90 -0
  28. package/src/boot/runners/registry.ts +42 -0
  29. package/src/boot/runners/repl-runner.ts +143 -0
  30. package/src/boot/runners/server-mode.ts +121 -0
  31. package/src/boot/runners/session.ts +641 -0
  32. package/src/boot/server-token.ts +148 -0
  33. package/src/boot/stages.ts +167 -0
  34. package/src/boot/upgrade/apply.ts +94 -0
  35. package/src/boot/upgrade/index.ts +13 -0
  36. package/src/boot/upgrade/upgrades.ts +289 -0
  37. package/src/briefing/compose.ts +150 -0
  38. package/src/briefing/context-docs.ts +19 -0
  39. package/src/briefing/contract.ts +717 -0
  40. package/src/briefing/index.ts +31 -0
  41. package/src/briefing/macros.ts +97 -0
  42. package/src/briefing/skills.ts +47 -0
  43. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  44. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  45. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  46. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  47. package/src/capability-deck/builtin-bridge.ts +312 -0
  48. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  49. package/src/capability-deck/cards/index.ts +115 -0
  50. package/src/capability-deck/cards/memory-card.ts +146 -0
  51. package/src/capability-deck/cards/plan-file.ts +97 -0
  52. package/src/capability-deck/cards/plan-tools.ts +185 -0
  53. package/src/capability-deck/cards/saas-card.ts +183 -0
  54. package/src/capability-deck/cards/task-card.ts +207 -0
  55. package/src/capability-deck/cards/todo-card.ts +168 -0
  56. package/src/capability-deck/cards/workflow-card.ts +247 -0
  57. package/src/capability-deck/contract.ts +388 -0
  58. package/src/capability-deck/index.ts +48 -0
  59. package/src/capability-deck/manifest.ts +109 -0
  60. package/src/capability-deck/provision.ts +169 -0
  61. package/src/channels/contract.ts +191 -0
  62. package/src/channels/framer.ts +50 -0
  63. package/src/channels/index.ts +101 -0
  64. package/src/channels/link/dialog.ts +129 -0
  65. package/src/channels/link/driver.ts +190 -0
  66. package/src/channels/link/index.ts +34 -0
  67. package/src/channels/link/server.ts +134 -0
  68. package/src/channels/oneshot.ts +90 -0
  69. package/src/channels/ops.ts +65 -0
  70. package/src/channels/session-ops.ts +81 -0
  71. package/src/conductor/bash-guard.ts +599 -0
  72. package/src/conductor/catalog/catalog.ts +116 -0
  73. package/src/conductor/catalog/index.ts +8 -0
  74. package/src/conductor/catalog/matcher.ts +134 -0
  75. package/src/conductor/conductor.ts +234 -0
  76. package/src/conductor/contract.ts +842 -0
  77. package/src/conductor/diagnostics.ts +227 -0
  78. package/src/conductor/index.ts +33 -0
  79. package/src/conductor/permissions.ts +588 -0
  80. package/src/conductor/quota-error.ts +49 -0
  81. package/src/conductor/signal-hub/hub.ts +46 -0
  82. package/src/conductor/signal-hub/index.ts +2 -0
  83. package/src/conductor/signal-hub/translate.test.ts +81 -0
  84. package/src/conductor/signal-hub/translate.ts +74 -0
  85. package/src/conductor/skill-parse/index.ts +2 -0
  86. package/src/conductor/skill-parse/parse.ts +108 -0
  87. package/src/conductor/transcript-store/index.ts +22 -0
  88. package/src/conductor/transcript-store/serialize.ts +116 -0
  89. package/src/conductor/transcript-store/store.ts +205 -0
  90. package/src/console/auth-status.ts +56 -0
  91. package/src/console/components/AgentsView.ts +165 -0
  92. package/src/console/components/BackgroundAgents.ts +155 -0
  93. package/src/console/components/Banner.ts +334 -0
  94. package/src/console/components/Composer.ts +94 -0
  95. package/src/console/components/StatusBar.ts +49 -0
  96. package/src/console/components/TerminalConsole.ts +1090 -0
  97. package/src/console/components/WorkingIndicator.ts +98 -0
  98. package/src/console/components/banner-sweep.ts +24 -0
  99. package/src/console/components/welcome.ts +74 -0
  100. package/src/console/contract.ts +630 -0
  101. package/src/console/index.ts +34 -0
  102. package/src/console/input/complete.ts +127 -0
  103. package/src/console/input/dir-reader.ts +34 -0
  104. package/src/console/input/index.ts +23 -0
  105. package/src/console/input/keymap.ts +159 -0
  106. package/src/console/input/paste.ts +104 -0
  107. package/src/console/mount.ts +56 -0
  108. package/src/console/overlays/approval-queue.ts +57 -0
  109. package/src/console/overlays/approval.ts +130 -0
  110. package/src/console/overlays/auth.ts +342 -0
  111. package/src/console/overlays/boards.ts +308 -0
  112. package/src/console/overlays/host.ts +36 -0
  113. package/src/console/overlays/index.ts +26 -0
  114. package/src/console/overlays/pickers.ts +258 -0
  115. package/src/console/overlays/sessions.ts +190 -0
  116. package/src/console/reducer.ts +182 -0
  117. package/src/console/slash/builtins.ts +81 -0
  118. package/src/console/slash/commands/dynamic.ts +83 -0
  119. package/src/console/slash/commands/integrations.ts +695 -0
  120. package/src/console/slash/commands/shared.ts +75 -0
  121. package/src/console/slash/commands/transcript.ts +263 -0
  122. package/src/console/slash/commands/workbench.ts +246 -0
  123. package/src/console/slash/index.ts +15 -0
  124. package/src/console/slash/registry.ts +70 -0
  125. package/src/console/slash/resolve.ts +63 -0
  126. package/src/console/startup.ts +209 -0
  127. package/src/console/theme/adapter.ts +45 -0
  128. package/src/console/theme/index.ts +7 -0
  129. package/src/console/theme/palette.ts +68 -0
  130. package/src/console/theme/resolve.ts +39 -0
  131. package/src/console/theme/tokens.ts +71 -0
  132. package/src/entry.ts +55 -0
  133. package/src/guardrails.ts +37 -0
  134. package/src/index.ts +18 -0
  135. package/src/insight/channel.ts +88 -0
  136. package/src/insight/contract.ts +185 -0
  137. package/src/insight/index.ts +110 -0
  138. package/src/insight/recorder.ts +213 -0
  139. package/src/insight/redaction.ts +157 -0
  140. package/src/insight/replay.ts +158 -0
  141. package/src/insight/sampling.ts +70 -0
  142. package/src/insight/serialize.ts +50 -0
  143. package/src/insight/sinks/console.ts +64 -0
  144. package/src/insight/sinks/file.ts +40 -0
  145. package/src/insight/sinks/index.ts +24 -0
  146. package/src/insight/sinks/stream.ts +54 -0
  147. package/src/integrations/sarvam/attach.ts +239 -0
  148. package/src/integrations/sarvam/config.ts +156 -0
  149. package/src/integrations/sarvam/index.ts +25 -0
  150. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  151. package/src/integrations/sarvam/types.ts +27 -0
  152. package/src/integrations/zoho/attach.ts +342 -0
  153. package/src/integrations/zoho/config.ts +125 -0
  154. package/src/integrations/zoho/index.ts +27 -0
  155. package/src/integrations/zoho/types.ts +21 -0
  156. package/src/integrations/zoho/zoho.test.ts +50 -0
  157. package/src/kit/clipboard-image.ts +107 -0
  158. package/src/kit/external-editor.ts +48 -0
  159. package/src/kit/image.ts +59 -0
  160. package/src/kit/index.ts +51 -0
  161. package/src/kit/shell.ts +19 -0
  162. package/src/kit/tool-fetch.ts +85 -0
  163. package/src/launch/catalog.ts +148 -0
  164. package/src/launch/contract.ts +187 -0
  165. package/src/launch/credentials.ts +625 -0
  166. package/src/launch/index.ts +98 -0
  167. package/src/launch/invocation/attachments.ts +179 -0
  168. package/src/launch/invocation/flags.ts +196 -0
  169. package/src/launch/invocation/index.ts +25 -0
  170. package/src/launch/invocation/read.ts +260 -0
  171. package/src/launch/invocation/usage.ts +67 -0
  172. package/src/launch/login.ts +324 -0
  173. package/src/launch/oauth.test.ts +18 -0
  174. package/src/launch/oauth.ts +203 -0
  175. package/src/launch/packages.ts +194 -0
  176. package/src/launch/pickers.ts +189 -0
  177. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  178. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  179. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  180. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  181. package/src/runtime-bridge/bridges/index.ts +33 -0
  182. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  183. package/src/runtime-bridge/broker.ts +227 -0
  184. package/src/runtime-bridge/contract.ts +122 -0
  185. package/src/runtime-bridge/index.ts +79 -0
  186. package/src/runtime-bridge/sink.ts +180 -0
  187. package/src/sessions/contract.ts +81 -0
  188. package/src/sessions/index.ts +13 -0
  189. package/src/sessions/library.ts +229 -0
  190. package/src/settings/contract.ts +114 -0
  191. package/src/settings/index.ts +32 -0
  192. package/src/settings/manager.ts +117 -0
  193. package/src/transcript-export/index.ts +45 -0
  194. package/src/transcript-export/publish.ts +260 -0
  195. package/src/transcript-export/sgr.ts +315 -0
  196. package/src/transcript-export/template.ts +272 -0
  197. package/src/transcript-export/theme-bridge.ts +150 -0
  198. package/src/window-budget/budget/estimate.ts +135 -0
  199. package/src/window-budget/budget/gate.ts +33 -0
  200. package/src/window-budget/budget/index.ts +16 -0
  201. package/src/window-budget/budget/slice.ts +56 -0
  202. package/src/window-budget/condenser.ts +58 -0
  203. package/src/window-budget/contract.ts +184 -0
  204. package/src/window-budget/index.ts +19 -0
  205. package/src/window-budget/microcompact.ts +95 -0
  206. package/src/window-budget/rehydrate.ts +136 -0
  207. package/src/window-budget/summarize/condense.ts +103 -0
  208. package/src/window-budget/summarize/index.ts +14 -0
  209. package/src/window-budget/summarize/prompt.ts +149 -0
  210. package/src/workflow-engine/agent-runner.ts +181 -0
  211. package/src/workflow-engine/display.ts +224 -0
  212. package/src/workflow-engine/engine.ts +294 -0
  213. package/src/workflow-engine/index.ts +23 -0
  214. package/src/workflow-engine/parse.ts +172 -0
  215. package/src/workflow-engine/structured-output.ts +35 -0
  216. package/src/workspace/brand.ts +29 -0
  217. package/src/workspace/index.ts +18 -0
  218. package/src/workspace/locator.ts +103 -0
  219. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Capability cards — the app-novel capabilities the deck builds in-house.
3
+ *
4
+ * Each card is a {@link CapabilityCard}: metadata plus a `build(ctx)` factory
5
+ * that mints a live {@link Capability} (the framework `AgentTool` shape) for a
6
+ * working context. These are written fresh against the framework contract — not
7
+ * derived from any prior tooling layer — and stay framework-agnostic where the
8
+ * behavior is original (todo and bg-process wrap only Node + the deck contract).
9
+ *
10
+ * The connector and memory cards expose a thin, clearly-typed adapter seam over
11
+ * a framework handle injected through {@link DeckContext.framework}; when no
12
+ * handle is wired they degrade to a typed stub so every card builds and runs in
13
+ * any environment, including tests.
14
+ *
15
+ * {@link APP_NOVEL_CARDS} is the contribution this module makes to the catalog;
16
+ * the manifest module composes it with the builtin-bridge cards (the framework's
17
+ * file/shell/search/web tools) to form the single `CAPABILITY_CARDS` source of
18
+ * truth from which the deck's index and profiles are derived.
19
+ */
20
+ import type { CapabilityCard } from "../contract.js";
21
+
22
+ export {
23
+ todoCard,
24
+ buildTodoCapability,
25
+ TodoLedger,
26
+ type TodoItem,
27
+ type TodoState,
28
+ type TodoWeight,
29
+ type TodoParamsType,
30
+ type TodoDetails,
31
+ } from "./todo-card.js";
32
+ export {
33
+ daemonCard,
34
+ buildDaemonCapability,
35
+ DaemonTable,
36
+ type DaemonState,
37
+ type DaemonParamsType,
38
+ type DaemonDetails,
39
+ } from "./bg-process-card.js";
40
+ export {
41
+ taskCard,
42
+ buildTaskCapability,
43
+ DELEGATE_HANDLE_KEY,
44
+ type DelegateRunner,
45
+ type DelegateRequest,
46
+ type DelegateResult,
47
+ type TaskParamsType,
48
+ type TaskDetails,
49
+ } from "./task-card.js";
50
+ export {
51
+ workflowCard,
52
+ buildWorkflowCapability,
53
+ WORKFLOW_HANDLE_KEY,
54
+ type WorkflowParamsType,
55
+ type WorkflowDetails,
56
+ } from "./workflow-card.js";
57
+ export {
58
+ saasCard,
59
+ buildSaasCapability,
60
+ SAAS_GATEWAY_KEY,
61
+ type SaasGatewayPort,
62
+ type RemoteToolSummary,
63
+ type RemoteExecution,
64
+ type SaasParamsType,
65
+ type SaasDetails,
66
+ } from "./saas-card.js";
67
+ export {
68
+ memoryCard,
69
+ buildMemoryCapability,
70
+ InMemoryStore,
71
+ MEMORY_HANDLE_KEY,
72
+ type MemoryStore,
73
+ type MemoryParamsType,
74
+ type MemoryDetails,
75
+ } from "./memory-card.js";
76
+ export {
77
+ enterPlanModeCard,
78
+ exitPlanModeCard,
79
+ buildEnterPlanModeCapability,
80
+ buildExitPlanModeCapability,
81
+ PLAN_HANDLE_KEY,
82
+ type PlanController,
83
+ type EnterPlanParamsType,
84
+ type EnterPlanDetails,
85
+ type ExitPlanParamsType,
86
+ type ExitPlanDetails,
87
+ } from "./plan-tools.js";
88
+ export { planSlug, planFilePath, writePlan, readPlan, PLANS_DIRNAME } from "./plan-file.js";
89
+
90
+ import { todoCard } from "./todo-card.js";
91
+ import { daemonCard } from "./bg-process-card.js";
92
+ import { taskCard } from "./task-card.js";
93
+ import { workflowCard } from "./workflow-card.js";
94
+ import { saasCard } from "./saas-card.js";
95
+ import { memoryCard } from "./memory-card.js";
96
+ import { enterPlanModeCard, exitPlanModeCard } from "./plan-tools.js";
97
+
98
+ /**
99
+ * The app-novel cards, in catalog order. The manifest module concatenates these
100
+ * with the builtin-bridge cards to build the full `CAPABILITY_CARDS` array.
101
+ *
102
+ * The plan-mode tools live here (the broadest `all` profile only): plan mode is a
103
+ * gate over mutating work, so the tools are only meaningful in a full-access deck —
104
+ * never the read-only `authoring` subset.
105
+ */
106
+ export const APP_NOVEL_CARDS: readonly CapabilityCard[] = [
107
+ todoCard,
108
+ daemonCard,
109
+ taskCard,
110
+ workflowCard,
111
+ saasCard,
112
+ memoryCard,
113
+ enterPlanModeCard,
114
+ exitPlanModeCard,
115
+ ];
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Working-memory capability — read and update a persistent scratch note the
3
+ * agent carries across turns.
4
+ *
5
+ * Status: minimal in-memory implementation + clearly-typed seam for the
6
+ * framework memory subsystem.
7
+ *
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.
17
+ *
18
+ * The single tool keys behavior on an `action` discriminant:
19
+ * - `read` — return the current working-memory note.
20
+ * - `replace` — overwrite the note with the supplied `content`.
21
+ * - `append` — add a line to the end of the note.
22
+ */
23
+ import { Type, type Static } from "@sinclair/typebox";
24
+ import { capabilityId, type Capability, type CapabilityCard, type DeckContext } from "../contract.js";
25
+
26
+ /** Key under which a host may wire a framework-backed memory store in later. */
27
+ export const MEMORY_HANDLE_KEY = "memoryStore" as const;
28
+
29
+ /**
30
+ * The narrow port the memory capability binds to.
31
+ *
32
+ * The in-memory default below satisfies it; a future framework-backed store can
33
+ * be adapted to the same three methods and injected through the context.
34
+ */
35
+ export interface MemoryStore {
36
+ read(): string;
37
+ replace(content: string): void;
38
+ append(line: string): void;
39
+ }
40
+
41
+ /** A trivial in-process working-memory buffer, one per built capability. */
42
+ export class InMemoryStore implements MemoryStore {
43
+ private buffer = "";
44
+
45
+ read(): string {
46
+ return this.buffer;
47
+ }
48
+
49
+ replace(content: string): void {
50
+ this.buffer = content;
51
+ }
52
+
53
+ append(line: string): void {
54
+ this.buffer = this.buffer.length === 0 ? line : `${this.buffer}\n${line}`;
55
+ }
56
+ }
57
+
58
+ const MemoryParams = Type.Object(
59
+ {
60
+ action: Type.Union(
61
+ [Type.Literal("read"), Type.Literal("replace"), Type.Literal("append")],
62
+ {
63
+ description:
64
+ "`read` returns the note; `replace` overwrites it; `append` adds a line to the end.",
65
+ },
66
+ ),
67
+ content: Type.Optional(
68
+ Type.String({
69
+ description:
70
+ "New note body (for `replace`) or the line to add (for `append`). Ignored for `read`.",
71
+ }),
72
+ ),
73
+ },
74
+ { additionalProperties: false },
75
+ );
76
+
77
+ /** Statically-inferred parameter type the capability's `execute` receives. */
78
+ export type MemoryParamsType = Static<typeof MemoryParams>;
79
+
80
+ /** Structured detail returned alongside the model-facing content. */
81
+ export interface MemoryDetails {
82
+ readonly action: "read" | "replace" | "append";
83
+ readonly ok: boolean;
84
+ /** Length of the note after the call, for quick status. */
85
+ readonly length: number;
86
+ }
87
+
88
+ const MEMORY_DESCRIPTION =
89
+ 'Keep a small working-memory note that persists across turns \u2014 durable facts, decisions, or reminders you want to retain even after older messages scroll out of context. Use `action:"read"` to recall it, `action:"replace"` to rewrite it from scratch, and `action:"append"` to add a single line. Keep it concise; it is a scratchpad, not a log.';
90
+
91
+ /** Read the optional memory store handle from the deck context, falling back to in-memory. */
92
+ function readStore(ctx: DeckContext): MemoryStore {
93
+ const handle = ctx.framework?.[MEMORY_HANDLE_KEY];
94
+ if (
95
+ handle &&
96
+ typeof (handle as MemoryStore).read === "function" &&
97
+ typeof (handle as MemoryStore).replace === "function" &&
98
+ typeof (handle as MemoryStore).append === "function"
99
+ ) {
100
+ return handle as MemoryStore;
101
+ }
102
+ return new InMemoryStore();
103
+ }
104
+
105
+ /**
106
+ * Build the working-memory capability, binding it to an injected store when one
107
+ * is wired into the context, or a fresh in-memory store otherwise.
108
+ *
109
+ * @param ctx the deck context; an optional store is read from
110
+ * `ctx.framework[MEMORY_HANDLE_KEY]`.
111
+ */
112
+ export function buildMemoryCapability(ctx: DeckContext): Capability<typeof MemoryParams, MemoryDetails> {
113
+ const store = readStore(ctx);
114
+ return {
115
+ name: "memory",
116
+ label: "Working memory",
117
+ description: MEMORY_DESCRIPTION,
118
+ parameters: MemoryParams,
119
+ async execute(_toolCallId, params) {
120
+ if (params.action === "replace") store.replace(params.content ?? "");
121
+ else if (params.action === "append" && params.content) store.append(params.content);
122
+ const note = store.read();
123
+ const heading =
124
+ params.action === "read"
125
+ ? "Working memory:"
126
+ : params.action === "replace"
127
+ ? "Working memory replaced:"
128
+ : "Working memory appended:";
129
+ const body = note.length === 0 ? "(empty)" : note;
130
+ return {
131
+ content: [{ type: "text", text: `${heading}\n${body}` }],
132
+ details: { action: params.action, ok: true, length: note.length },
133
+ };
134
+ },
135
+ };
136
+ }
137
+
138
+ /** Catalog row for the working-memory capability. */
139
+ export const memoryCard: CapabilityCard = {
140
+ id: capabilityId("memory"),
141
+ title: "Working memory",
142
+ summary: "Read and update a persistent scratch note that survives across turns.",
143
+ // Erase the precisely-typed builder to the `Capability` card surface (through
144
+ // `unknown`, as TypeBox tools are invariant in their parameter schema).
145
+ build: (ctx) => buildMemoryCapability(ctx),
146
+ };
@@ -0,0 +1,97 @@
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
+ import { mkdirSync, writeFileSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ /** The directory plans are written under (relative to the session scope dir). */
22
+ export const PLANS_DIRNAME = "plans" as const;
23
+
24
+ /** Maximum slug length before truncation. */
25
+ const MAX_SLUG_LEN = 48;
26
+
27
+ /**
28
+ * Derive a filesystem-safe slug from a plan's text — its first non-empty line,
29
+ * lower-cased, with every run of non-alphanumeric characters collapsed to a
30
+ * single dash and the result trimmed and length-capped. Falls back to `"plan"`
31
+ * when the text yields nothing usable (empty, or punctuation-only).
32
+ *
33
+ * @param plan the plan body whose first line seeds the slug
34
+ */
35
+ export function planSlug(plan: string): string {
36
+ const firstLine = plan
37
+ .split("\n")
38
+ .map((l) => l.trim())
39
+ .find((l) => l.length > 0);
40
+ const base = (firstLine ?? "")
41
+ .replace(/^#+\s*/, "")
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9]+/g, "-")
44
+ .replace(/^-+|-+$/g, "")
45
+ .slice(0, MAX_SLUG_LEN)
46
+ .replace(/-+$/g, "");
47
+ return base.length > 0 ? base : "plan";
48
+ }
49
+
50
+ /**
51
+ * Resolve the absolute path a plan with `slug` is written to under `sessionDir`.
52
+ * The `plans/` sub-directory is implied; callers pass the session scope dir and
53
+ * get back `<sessionDir>/plans/<slug>.md`.
54
+ *
55
+ * @param sessionDir the per-session directory plans live beneath
56
+ * @param slug the derived slug (see {@link planSlug})
57
+ */
58
+ export function planFilePath(sessionDir: string, slug: string): string {
59
+ return join(sessionDir, PLANS_DIRNAME, `${slug}.md`);
60
+ }
61
+
62
+ /**
63
+ * Write the plan body to a slug-named markdown file under `sessionDir/plans`,
64
+ * creating the directory as needed, and return the absolute path written. The
65
+ * filename is `<short-timestamp>-<slug>.md` so repeated plans in one session do
66
+ * not overwrite each other.
67
+ *
68
+ * Throws on an I/O failure (the conductor swallows it into a non-fatal note).
69
+ *
70
+ * @param sessionDir the per-session directory plans live beneath
71
+ * @param plan the plan body to persist
72
+ */
73
+ export function writePlan(sessionDir: string, plan: string): string {
74
+ const dir = join(sessionDir, PLANS_DIRNAME);
75
+ mkdirSync(dir, { recursive: true });
76
+ const stamp = Date.now().toString(36);
77
+ const path = join(dir, `${stamp}-${planSlug(plan)}.md`);
78
+ writeFileSync(path, plan.endsWith("\n") ? plan : `${plan}\n`, {
79
+ encoding: "utf8",
80
+ mode: 0o600,
81
+ });
82
+ return path;
83
+ }
84
+
85
+ /**
86
+ * Read a previously-written plan file back, or `undefined` when it is missing or
87
+ * unreadable. Used by tests and by any review surface that wants the saved plan.
88
+ *
89
+ * @param path the absolute plan-file path (see {@link planFilePath} / {@link writePlan})
90
+ */
91
+ export function readPlan(path: string): string | undefined {
92
+ try {
93
+ return readFileSync(path, "utf8");
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ }
@@ -0,0 +1,185 @@
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, type Static } from "@sinclair/typebox";
26
+ import { capabilityId, type Capability, type CapabilityCard, type DeckContext } from "../contract.js";
27
+
28
+ /**
29
+ * Reserved key under which a host MAY wire a plan-mode controller into the deck
30
+ * context. The conductor-driven design intercepts the plan tools' result details
31
+ * directly (no handle required), so this is exported only for forward-compat /
32
+ * an alternate host that wants the tool to drive the flip itself.
33
+ */
34
+ export const PLAN_HANDLE_KEY = "planController" as const;
35
+
36
+ /**
37
+ * The narrow port a host may wire to flip the live session into plan mode. Unused
38
+ * by the default (conductor-intercept) wiring; kept for hosts that prefer the tool
39
+ * to signal directly rather than via the tool-result seam.
40
+ */
41
+ export interface PlanController {
42
+ /** Switch the session into read-only plan mode for subsequent tool calls. */
43
+ enterPlanMode(): void;
44
+ }
45
+
46
+ const EnterPlanParams = Type.Object({}, { additionalProperties: false });
47
+
48
+ /** Statically-inferred parameter type EnterPlanMode's `execute` receives. */
49
+ export type EnterPlanParamsType = Static<typeof EnterPlanParams>;
50
+
51
+ /** Structured detail returned by EnterPlanMode. */
52
+ export interface EnterPlanDetails {
53
+ /**
54
+ * The marker the conductor intercepts on the tool-result seam to switch the
55
+ * session into plan mode (and capture the pre-plan mode for a later restore).
56
+ */
57
+ readonly enterPlanMode: true;
58
+ /** Whether an optional in-context controller also applied the switch directly. */
59
+ readonly applied: boolean;
60
+ }
61
+
62
+ const ExitPlanParams = Type.Object(
63
+ {
64
+ plan: Type.String({
65
+ description:
66
+ "The plan you intend to carry out, in markdown. The user reviews this before approving the exit from plan mode; on approval you leave plan mode and begin.",
67
+ }),
68
+ },
69
+ { additionalProperties: false },
70
+ );
71
+
72
+ /** Statically-inferred parameter type ExitPlanMode's `execute` receives. */
73
+ export type ExitPlanParamsType = Static<typeof ExitPlanParams>;
74
+
75
+ /**
76
+ * Structured detail returned by ExitPlanMode. The conductor watches the
77
+ * tool-result seam for `exitPlan === true`, captures the {@link plan}, and runs
78
+ * the approval handshake (the tool itself does not flip the mode or prompt).
79
+ */
80
+ export interface ExitPlanDetails {
81
+ /** The marker the conductor intercepts to start the exit-plan handshake. */
82
+ readonly exitPlan: true;
83
+ /** The plan text the model authored, surfaced to the user for approval. */
84
+ readonly plan: string;
85
+ }
86
+
87
+ const ENTER_PLAN_DESCRIPTION =
88
+ "Enter plan mode: a read-only research phase in which you may only read, search, and inspect \u2014 every file-mutating and shell tool is blocked until you leave. Use this when a task needs investigation and a plan before any change is made. While in plan mode, gather all the context you need, then call `exit_plan_mode` with the plan you intend to carry out; the user reviews and approves it before you act.";
89
+
90
+ const ENTER_PLAN_PROSE =
91
+ "You are now in plan mode (read-only). Investigate freely with the read, search, and listing tools, but do not attempt to edit files or run shell commands \u2014 they are blocked here. When you have a concrete plan, call `exit_plan_mode` with the full plan text so the user can approve it before you make any changes.";
92
+
93
+ const EXIT_PLAN_DESCRIPTION =
94
+ "Leave plan mode and propose the plan you intend to carry out. Provide the full plan as `plan`; the user reviews it and either approves (you exit plan mode and begin the work) or rejects (you stay in plan mode to revise). Only call this once you have a concrete, reviewed plan \u2014 not to do work, but to ask permission to start it.";
95
+
96
+ /** Read the optional plan controller handle from the deck context. */
97
+ function readPlanController(ctx: DeckContext): PlanController | undefined {
98
+ const handle = ctx.framework?.[PLAN_HANDLE_KEY];
99
+ if (handle && typeof (handle as PlanController).enterPlanMode === "function") {
100
+ return handle as PlanController;
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ /**
106
+ * Build the EnterPlanMode capability. On invoke it asks the wired
107
+ * {@link PlanController} to switch the session into plan mode, then returns
108
+ * explore-only prose. It is `readOnly: true`, so the permission gate always
109
+ * permits it (including inside plan mode itself).
110
+ *
111
+ * @param ctx the deck context; an optional controller is read from
112
+ * `ctx.framework[PLAN_HANDLE_KEY]`.
113
+ */
114
+ export function buildEnterPlanModeCapability(
115
+ ctx: DeckContext,
116
+ ): Capability<typeof EnterPlanParams, EnterPlanDetails> {
117
+ const controller = readPlanController(ctx);
118
+ return {
119
+ name: "enter_plan_mode",
120
+ label: "Enter plan mode",
121
+ description: ENTER_PLAN_DESCRIPTION,
122
+ readOnly: true,
123
+ parameters: EnterPlanParams,
124
+ async execute() {
125
+ const applied = controller !== undefined;
126
+ if (controller) controller.enterPlanMode();
127
+ return {
128
+ content: [{ type: "text", text: ENTER_PLAN_PROSE }],
129
+ details: { enterPlanMode: true, applied },
130
+ };
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Build the ExitPlanMode capability. It performs no mode flip and no prompt of
137
+ * its own — it returns the `{ exitPlan: true, plan }` detail the conductor
138
+ * intercepts. The model-facing content echoes the proposed plan so a transcript
139
+ * reader sees what was put up for approval.
140
+ *
141
+ * Not marked read-only: it represents the request to RESUME mutating work, so in
142
+ * plan mode it must still be permitted explicitly (the gate special-cases the
143
+ * plan tools alongside read-only tools — see the conductor wiring).
144
+ */
145
+ export function buildExitPlanModeCapability(
146
+ _ctx: DeckContext,
147
+ ): Capability<typeof ExitPlanParams, ExitPlanDetails> {
148
+ return {
149
+ name: "exit_plan_mode",
150
+ label: "Exit plan mode",
151
+ description: EXIT_PLAN_DESCRIPTION,
152
+ // Marked read-only so the plan-mode gate never blocks the very tool used to
153
+ // request leaving plan mode (it mutates nothing — the conductor owns the flip).
154
+ readOnly: true,
155
+ parameters: ExitPlanParams,
156
+ async execute(_toolCallId, params) {
157
+ const plan = params.plan;
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `Proposed plan (awaiting your approval to leave plan mode):\n\n${plan}`,
163
+ },
164
+ ],
165
+ details: { exitPlan: true, plan },
166
+ };
167
+ },
168
+ };
169
+ }
170
+
171
+ /** Catalog row for the EnterPlanMode capability. */
172
+ export const enterPlanModeCard: CapabilityCard = {
173
+ id: capabilityId("enter_plan_mode"),
174
+ title: "Enter plan mode",
175
+ summary: "Switch into a read-only research phase where mutating tools are blocked.",
176
+ build: (ctx) => buildEnterPlanModeCapability(ctx),
177
+ };
178
+
179
+ /** Catalog row for the ExitPlanMode capability. */
180
+ export const exitPlanModeCard: CapabilityCard = {
181
+ id: capabilityId("exit_plan_mode"),
182
+ title: "Exit plan mode",
183
+ summary: "Propose a plan and request approval to leave plan mode and begin work.",
184
+ build: (ctx) => buildExitPlanModeCapability(ctx),
185
+ };