@basou/core 0.3.1 → 0.4.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.
package/dist/index.d.ts CHANGED
@@ -2,369 +2,208 @@ import { z } from 'zod';
2
2
  import { ChildProcess } from 'node:child_process';
3
3
 
4
4
  /**
5
- * Allowed ID type prefixes for Basou entities.
6
- *
7
- * Frozen at runtime so that mutating the exported array cannot diverge from
8
- * the validation set used internally. The single source of truth for both
9
- * the `IdPrefix` type and runtime prefix checks.
5
+ * Static metadata identifying the claude-code adapter as the session source.
6
+ * Consumed by the CLI orchestration when populating `session.yaml.source`
7
+ * and event `source` fields. The literal `kind` is part of the wire format
8
+ * defined by the session schema; do not change without coordinated schema
9
+ * migration.
10
10
  */
11
- declare const ID_PREFIXES: readonly ["ws", "task", "ses", "evt", "appr", "decision"];
11
+ declare const claudeCodeAdapterMetadata: {
12
+ readonly kind: "claude-code-adapter";
13
+ readonly version: "0.1.0";
14
+ };
12
15
  /**
13
- * Type prefix used for Basou entity IDs.
14
- * Format: `<prefix>_<26-char ULID>`, e.g. `ws_01HXABCDEF1234567890ABCDEF`.
16
+ * Lookup predicate used by {@link resolveClaudeCodeCommand} to decide
17
+ * whether a candidate executable is reachable on PATH. Exposed as a
18
+ * parameter so tests can substitute a deterministic mock; production
19
+ * callers should omit it and rely on the default `which`-based lookup.
15
20
  */
16
- type IdPrefix = (typeof ID_PREFIXES)[number];
21
+ type CommandLookup = (command: string) => Promise<boolean>;
17
22
  /**
18
- * A Basou entity ID as a template literal type.
23
+ * Resolve the Claude Code CLI executable name. Tries `claude-code` first
24
+ * and falls back to `claude`; the first candidate found on PATH wins.
19
25
  *
20
- * `PrefixedId<"ses">` narrows to ``ses_${string}`` so a session schema can
21
- * preserve the prefix in its inferred type beyond runtime validation.
26
+ * Throws a fixed-message Error when neither candidate is reachable, so
27
+ * callers can present a single user-facing prompt to install the CLI.
28
+ *
29
+ * @throws Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.")
22
30
  */
23
- type PrefixedId<P extends IdPrefix = IdPrefix> = `${P}_${string}`;
31
+ declare function resolveClaudeCodeCommand(lookup?: CommandLookup): Promise<{
32
+ command: string;
33
+ }>;
24
34
  /**
25
- * Generate a Crockford Base32 ULID.
26
- *
27
- * The result is a 26-character, lexicographically time-sortable identifier.
28
- * Multiple calls within the same millisecond are strictly increasing for the
29
- * lifetime of the current process.
35
+ * Stub for the future `adapter_output` summary generator.
30
36
  *
31
- * NOTE: `seedTime` is forwarded to the underlying monotonic factory and is
32
- * NOT a deterministic seed: repeated calls with the same `seedTime` still
33
- * return strictly increasing values, because the factory increments its
34
- * internal counter on each call.
37
+ * The current release keeps `capture: "none"` and intentionally does
38
+ * not emit `adapter_output` events, so this hook has no production
39
+ * callers yet. The signature is committed so a later release can
40
+ * implement raw_ref generation without retrofitting the adapter
41
+ * scaffold.
35
42
  *
36
- * @param seedTime Optional millisecond timestamp passed to the monotonic
37
- * factory. Useful for ordered generation in tests; not deterministic.
43
+ * @throws Error - always; not implemented in this release.
38
44
  */
39
- declare function ulid(seedTime?: number): string;
45
+ declare function summarizeAdapterOutput(_stream: "stdout" | "stderr", _raw: string): string;
46
+
40
47
  /**
41
- * Generate a prefixed Basou ID, e.g. `ses_01HXABCDEF1234567890ABCDEF`.
48
+ * Lifecycle states of a Basou approval. The status is stored directly on
49
+ * the approval YAML (flat shape) so that pending → resolved transitions
50
+ * are atomic-move + in-place rewrites rather than schema-variant swaps.
51
+ */
52
+ declare const ApprovalStatusSchema: z.ZodEnum<{
53
+ pending: "pending";
54
+ approved: "approved";
55
+ rejected: "rejected";
56
+ expired: "expired";
57
+ }>;
58
+ /** Inferred runtime type for {@link ApprovalStatusSchema}. */
59
+ type ApprovalStatus = z.infer<typeof ApprovalStatusSchema>;
60
+ /**
61
+ * Schema for `.basou/approvals/{pending,resolved}/<approval_id>.yaml`.
42
62
  *
43
- * The return type preserves the prefix as a template literal type so that
44
- * downstream zod schemas can narrow an `IdPrefix` parameter through the API.
63
+ * The schema is intentionally flat (one shape regardless of `status`) so
64
+ * that pending and resolved YAMLs share the same parser. Required vs.
65
+ * optional semantics by status (e.g. `rejection_reason` MUST be set when
66
+ * `status === "rejected"`) are enforced at the CLI orchestration layer
67
+ * rather than here, mirroring the approval event variants in
68
+ * `event.schema.ts`.
45
69
  *
46
- * Throws if `prefix` is not one of {@link ID_PREFIXES}. The runtime guard
47
- * defends against JavaScript callers and casted TypeScript that bypass the
48
- * compile-time `IdPrefix` constraint.
70
+ * The `action` field is `{ kind: string }` with `passthrough()` so that
71
+ * adapter-defined keys (e.g. `command`, `path`, `target_url`) survive the
72
+ * round-trip without being stripped — matching the approval_requested
73
+ * event variant.
49
74
  */
50
- declare function prefixedUlid<P extends IdPrefix>(prefix: P): PrefixedId<P>;
75
+ declare const ApprovalSchema: z.ZodObject<{
76
+ schema_version: z.ZodLiteral<"0.1.0">;
77
+ id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
78
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
79
+ created_at: z.ZodString;
80
+ status: z.ZodEnum<{
81
+ pending: "pending";
82
+ approved: "approved";
83
+ rejected: "rejected";
84
+ expired: "expired";
85
+ }>;
86
+ risk_level: z.ZodEnum<{
87
+ low: "low";
88
+ medium: "medium";
89
+ high: "high";
90
+ critical: "critical";
91
+ }>;
92
+ action: z.ZodObject<{
93
+ kind: z.ZodString;
94
+ }, z.core.$loose>;
95
+ reason: z.ZodString;
96
+ expires_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
97
+ resolver: z.ZodDefault<z.ZodNullable<z.ZodString>>;
98
+ resolved_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
99
+ note: z.ZodDefault<z.ZodNullable<z.ZodString>>;
100
+ rejection_reason: z.ZodDefault<z.ZodNullable<z.ZodString>>;
101
+ }, z.core.$strip>;
102
+ /** Inferred runtime type for {@link ApprovalSchema}. */
103
+ type Approval = z.infer<typeof ApprovalSchema>;
104
+
51
105
  /**
52
- * Check whether the given string is a valid prefixed Basou ID.
106
+ * Absolute paths to the standard `.basou/` directory layout, derived from a
107
+ * given repository root. The shape mirrors the canonical `.basou/` tree
108
+ * (see `docs/spec/workspace.md`). `root` is the `.basou/` directory itself
109
+ * (i.e. `repositoryRoot/.basou`).
53
110
  *
54
- * Returns true only if the string has shape `<prefix>_<ULID>` where prefix is
55
- * one of {@link ID_PREFIXES} and the trailing 26 characters form a valid
56
- * Crockford Base32 ULID. Validation combines a strict shape regex (to enforce
57
- * the 0-7 leading char and the I/L/O/U exclusion) with the npm `ulid`
58
- * library's `isValid` for forward compatibility.
111
+ * `files` exposes the well-known top-level files inside `.basou/`. Each path
112
+ * is computed but not created they are written by their respective
113
+ * subsystems (e.g. `writeManifest` for `manifest.yaml`).
59
114
  *
60
- * NOTE: This validates the prefix is known. Schemas that require a specific
61
- * prefix (e.g. only `ses_*` for a session ID) must add their own narrowing.
115
+ * All fields are deeply readonly; consumers must not mutate the returned
116
+ * object.
62
117
  */
63
- declare function isValidPrefixedId(value: string): boolean;
64
-
118
+ type BasouPaths = {
119
+ readonly root: string;
120
+ readonly sessions: string;
121
+ readonly tasks: string;
122
+ readonly approvals: {
123
+ readonly pending: string;
124
+ readonly resolved: string;
125
+ };
126
+ readonly locks: string;
127
+ readonly logs: string;
128
+ readonly raw: string;
129
+ readonly tmp: string;
130
+ readonly files: {
131
+ readonly manifest: string;
132
+ readonly status: string;
133
+ readonly handoff: string;
134
+ readonly decisions: string;
135
+ };
136
+ };
65
137
  /**
66
- * Schema version literal pinned to "0.1.0" for Basou v0.1.
67
- * Reused across every entity schema so inferred types narrow to the literal.
138
+ * Compute absolute paths to the standard `.basou/` directory layout under
139
+ * `repositoryRoot`. Pure: performs no I/O and is safe to call before the
140
+ * directory exists.
141
+ *
142
+ * @param repositoryRoot Absolute path to the git repository root (the
143
+ * parent directory of `.basou/`). Caller is responsible for resolving
144
+ * `process.cwd()` or running `git rev-parse --show-toplevel` upstream;
145
+ * this function does not validate that the path exists or is a git
146
+ * repository.
68
147
  */
69
- declare const SchemaVersionSchema: z.ZodLiteral<"0.1.0">;
148
+ declare function basouPaths(repositoryRoot: string): BasouPaths;
70
149
  /**
71
- * ISO 8601 timestamp with explicit timezone offset (e.g. `+09:00`).
150
+ * Create the standard `.basou/` directory layout under `repositoryRoot`.
72
151
  *
73
- * The spec samples include offsets, so the default zod `.datetime()` (which
74
- * rejects offsets) is insufficient; `{ offset: true }` is required.
152
+ * Idempotent: a no-op on an already-initialized layout. Returns the resolved
153
+ * {@link BasouPaths} so callers can immediately use them.
154
+ *
155
+ * Throws if `repositoryRoot/.basou` (or any required subdirectory) exists
156
+ * but is not a directory, or if filesystem permissions prevent creation.
157
+ * All thrown error messages are pathless; the original native error is
158
+ * attached as `cause` for diagnostics.
159
+ *
160
+ * @param repositoryRoot Absolute path to the git repository root. See
161
+ * {@link basouPaths} for the contract on this parameter.
75
162
  */
76
- declare const IsoTimestampSchema: z.ZodString;
77
- /** Workspace ID schema: validates `ws_<26-char ULID>`. */
78
- declare const WorkspaceIdSchema: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
79
- /** Task ID schema: validates `task_<26-char ULID>`. */
80
- declare const TaskIdSchema: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
81
- /** Session ID schema: validates `ses_<26-char ULID>`. */
82
- declare const SessionIdSchema: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
83
- /** Event ID schema: validates `evt_<26-char ULID>`. */
84
- declare const EventIdSchema: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
85
- /** Approval ID schema: validates `appr_<26-char ULID>`. */
86
- declare const ApprovalIdSchema: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
87
- /** Decision ID schema: validates `decision_<26-char ULID>`. */
88
- declare const DecisionIdSchema: z.ZodString & z.ZodType<`decision_${string}`, string, z.core.$ZodTypeInternals<`decision_${string}`, string>>;
163
+ declare function ensureBasouDirectory(repositoryRoot: string): Promise<BasouPaths>;
164
+
165
+ /** Which side of `.basou/approvals/` an approval YAML lives on. */
166
+ type ApprovalLocation = "pending" | "resolved";
167
+ /** Result returned by {@link loadApproval}: the parsed approval and where it was found. */
168
+ type LoadedApproval = {
169
+ approval: Approval;
170
+ location: ApprovalLocation;
171
+ };
89
172
  /**
90
- * Risk level vocabulary fixed by the spec. Adapters MUST emit one of these
91
- * four values; arbitrary strings are rejected at schema parse time.
173
+ * Locate and load the approval YAML for `approvalId`. Searches resolved
174
+ * first so that a duplicated YAML (the crash-window scenario where both
175
+ * pending and resolved exist for the same id) returns the resolved-side
176
+ * record — matching the dedupe rule used by `approval list` and
177
+ * `resolveApprovalId`. Returns null if neither directory contains the
178
+ * YAML. Throws with a pathless message on read or schema-validation
179
+ * failure.
92
180
  */
93
- declare const RiskLevelSchema: z.ZodEnum<{
94
- low: "low";
95
- medium: "medium";
96
- high: "high";
97
- critical: "critical";
98
- }>;
99
- /** Inferred runtime type for {@link RiskLevelSchema}. */
100
- type RiskLevel = z.infer<typeof RiskLevelSchema>;
181
+ declare function loadApproval(paths: BasouPaths, approvalId: string): Promise<LoadedApproval | null>;
101
182
  /**
102
- * Source attribution for events (e.g. "claude-code-adapter",
103
- * "git-capability", "terminal-recording", "local-cli", "human"). Free-form
104
- * non-empty string in v0.1; a stricter enum may be introduced post-v0.1.
183
+ * Enumerate approval IDs by inspecting `<id>.yaml` filenames in pending
184
+ * and resolved. ENOENT on either directory is treated as empty (e.g. a
185
+ * workspace that has no resolved approvals yet). YAML parse and schema
186
+ * validation are NOT performed; callers that need the parsed approval
187
+ * should use {@link loadApproval} per ID.
105
188
  */
106
- declare const EventSourceSchema: z.ZodString;
107
-
189
+ declare function enumerateApprovals(paths: BasouPaths): Promise<{
190
+ pending: string[];
191
+ resolved: string[];
192
+ }>;
108
193
  /**
109
- * Schema for `.basou/manifest.yaml`. The minimal manifest carries
110
- * schema_version, basou_version, workspace metadata, project info, enabled
111
- * capabilities, approval policy, adapter config, and git policy. The
112
- * `adapters."claude-code"` key uses a hyphen; downstream code accesses it
113
- * via bracket notation.
194
+ * Return true when an approval is in `pending` state and its `expires_at`
195
+ * timestamp has elapsed. Used by `basou approval list` / `show` to surface
196
+ * a `(expired)` label without mutating the YAML file. Approval expiry uses
197
+ * lazy-evaluation semantics; actual `approval_expired` event firing is
198
+ * deferred to a later step.
199
+ *
200
+ * `now` is taken as a parameter so a single CLI invocation can share one
201
+ * "now" across every record it inspects (avoids boundary races where two
202
+ * reads of `Date.now()` straddle an expiry instant).
114
203
  */
115
- declare const ManifestSchema: z.ZodObject<{
116
- schema_version: z.ZodLiteral<"0.1.0">;
117
- basou_version: z.ZodLiteral<"0.1.0">;
118
- workspace: z.ZodObject<{
119
- id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
120
- name: z.ZodString;
121
- created_at: z.ZodString;
122
- updated_at: z.ZodString;
123
- }, z.core.$strip>;
124
- project: z.ZodObject<{
125
- name: z.ZodOptional<z.ZodString>;
126
- description: z.ZodOptional<z.ZodString>;
127
- repository_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
128
- }, z.core.$strip>;
129
- capabilities: z.ZodObject<{
130
- enabled: z.ZodArray<z.ZodString>;
131
- }, z.core.$strip>;
132
- approval: z.ZodObject<{
133
- required_for: z.ZodOptional<z.ZodArray<z.ZodString>>;
134
- default_risk_level: z.ZodEnum<{
135
- low: "low";
136
- medium: "medium";
137
- high: "high";
138
- critical: "critical";
139
- }>;
140
- }, z.core.$strip>;
141
- adapters: z.ZodObject<{
142
- "claude-code": z.ZodObject<{
143
- enabled: z.ZodBoolean;
144
- config_path: z.ZodOptional<z.ZodString>;
145
- }, z.core.$strip>;
146
- }, z.core.$strip>;
147
- git: z.ZodObject<{
148
- events_log: z.ZodDefault<z.ZodEnum<{
149
- ignore: "ignore";
150
- commit: "commit";
151
- }>>;
152
- }, z.core.$strip>;
153
- }, z.core.$strip>;
154
- /** Inferred runtime type for {@link ManifestSchema}. */
155
- type Manifest = z.infer<typeof ManifestSchema>;
156
-
157
- /**
158
- * Schema for `.basou/status.json` — a forward-incompat cache of the current
159
- * workspace state.
160
- *
161
- * Each level uses `.strict()` so unknown keys are rejected rather than
162
- * silently stripped. A v0.1 reader that encounters a future-shape
163
- * `status.json` therefore fails parsing instead of returning a partially
164
- * empty snapshot; callers regenerate by calling `buildStatusSnapshot` +
165
- * `writeStatus` rather than trying to migrate.
166
- */
167
- declare const StatusSchema: z.ZodObject<{
168
- schema_version: z.ZodLiteral<"0.1.0">;
169
- generated_at: z.ZodString;
170
- workspace: z.ZodObject<{
171
- id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
172
- name: z.ZodString;
173
- basou_version: z.ZodLiteral<"0.1.0">;
174
- }, z.core.$strict>;
175
- directories_present: z.ZodObject<{
176
- sessions: z.ZodBoolean;
177
- tasks: z.ZodBoolean;
178
- approvals_pending: z.ZodBoolean;
179
- approvals_resolved: z.ZodBoolean;
180
- logs: z.ZodBoolean;
181
- raw: z.ZodBoolean;
182
- tmp: z.ZodBoolean;
183
- }, z.core.$strict>;
184
- }, z.core.$strict>;
185
- /** Inferred runtime type for {@link StatusSchema}. */
186
- type StatusSnapshot = z.infer<typeof StatusSchema>;
187
-
188
- /** Session lifecycle states. */
189
- declare const SessionStatusSchema: z.ZodEnum<{
190
- initialized: "initialized";
191
- running: "running";
192
- waiting_approval: "waiting_approval";
193
- completed: "completed";
194
- failed: "failed";
195
- interrupted: "interrupted";
196
- imported: "imported";
197
- archived: "archived";
198
- }>;
199
- /** Inferred runtime type for {@link SessionStatusSchema}. */
200
- type SessionStatus = z.infer<typeof SessionStatusSchema>;
201
- /** Source kind that produced the session. */
202
- declare const SessionSourceKindSchema: z.ZodEnum<{
203
- "claude-code-adapter": "claude-code-adapter";
204
- human: "human";
205
- import: "import";
206
- terminal: "terminal";
207
- }>;
208
- /** Inferred runtime type for {@link SessionSourceKindSchema}. */
209
- type SessionSourceKind = z.infer<typeof SessionSourceKindSchema>;
210
- /**
211
- * Schema for `.basou/sessions/<session_id>/session.yaml`. The minimal
212
- * session document carries the actual fields nested under the outer
213
- * `session:` key.
214
- */
215
- declare const SessionSchema: z.ZodObject<{
216
- schema_version: z.ZodLiteral<"0.1.0">;
217
- session: z.ZodObject<{
218
- id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
219
- label: z.ZodOptional<z.ZodString>;
220
- task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
221
- workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
222
- source: z.ZodObject<{
223
- kind: z.ZodEnum<{
224
- "claude-code-adapter": "claude-code-adapter";
225
- human: "human";
226
- import: "import";
227
- terminal: "terminal";
228
- }>;
229
- version: z.ZodLiteral<"0.1.0">;
230
- }, z.core.$strip>;
231
- started_at: z.ZodString;
232
- ended_at: z.ZodOptional<z.ZodString>;
233
- status: z.ZodEnum<{
234
- initialized: "initialized";
235
- running: "running";
236
- waiting_approval: "waiting_approval";
237
- completed: "completed";
238
- failed: "failed";
239
- interrupted: "interrupted";
240
- imported: "imported";
241
- archived: "archived";
242
- }>;
243
- working_directory: z.ZodString;
244
- invocation: z.ZodObject<{
245
- command: z.ZodString;
246
- args: z.ZodDefault<z.ZodArray<z.ZodString>>;
247
- exit_code: z.ZodNullable<z.ZodNumber>;
248
- }, z.core.$strip>;
249
- related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
250
- events_log: z.ZodDefault<z.ZodString>;
251
- summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
252
- }, z.core.$strip>;
253
- }, z.core.$strip>;
254
- /** Inferred runtime type for {@link SessionSchema}. */
255
- type Session = z.infer<typeof SessionSchema>;
256
-
257
- /**
258
- * Task lifecycle states.
259
- *
260
- * The storage layer's `ALLOWED_TRANSITIONS` map (= source of truth in
261
- * `tasks.ts`) is the authoritative graph; the comment below is a snapshot.
262
- * `planned` reaches `done` / `cancelled` directly so tasks completed (or
263
- * abandoned) outside an explicit in-progress phase can close in a single
264
- * CLI call:
265
- *
266
- * planned → {in_progress | done | cancelled}
267
- * in_progress → {done | cancelled}
268
- * done / cancelled = terminal
269
- *
270
- * Self-edges are rejected so the audit trail stays monotonic.
271
- */
272
- declare const TaskStatusSchema: z.ZodEnum<{
273
- planned: "planned";
274
- in_progress: "in_progress";
275
- done: "done";
276
- cancelled: "cancelled";
277
- }>;
278
- /** Inferred runtime type for {@link TaskStatusSchema}. */
279
- type TaskStatus = z.infer<typeof TaskStatusSchema>;
280
- /**
281
- * Schema for the YAML front matter of `.basou/tasks/<task_id>.md`.
282
- *
283
- * The markdown body after the front matter is intentionally NOT modelled
284
- * here — it is free-form user-edited content. The storage layer splits
285
- * the file into `task` (this schema) and `body` (the trailing string).
286
- */
287
- declare const TaskSchema: z.ZodObject<{
288
- schema_version: z.ZodLiteral<"0.1.0">;
289
- task: z.ZodObject<{
290
- id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
291
- title: z.ZodString;
292
- label: z.ZodOptional<z.ZodString>;
293
- status: z.ZodEnum<{
294
- planned: "planned";
295
- in_progress: "in_progress";
296
- done: "done";
297
- cancelled: "cancelled";
298
- }>;
299
- created_at: z.ZodString;
300
- updated_at: z.ZodString;
301
- workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
302
- created_in_session: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
303
- linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
304
- }, z.core.$strip>;
305
- }, z.core.$strip>;
306
- /** Inferred runtime type for {@link TaskSchema}. */
307
- type Task = z.infer<typeof TaskSchema>;
308
-
309
- /**
310
- * Lifecycle states of a Basou approval. The status is stored directly on
311
- * the approval YAML (flat shape) so that pending → resolved transitions
312
- * are atomic-move + in-place rewrites rather than schema-variant swaps.
313
- */
314
- declare const ApprovalStatusSchema: z.ZodEnum<{
315
- pending: "pending";
316
- approved: "approved";
317
- rejected: "rejected";
318
- expired: "expired";
319
- }>;
320
- /** Inferred runtime type for {@link ApprovalStatusSchema}. */
321
- type ApprovalStatus = z.infer<typeof ApprovalStatusSchema>;
322
- /**
323
- * Schema for `.basou/approvals/{pending,resolved}/<approval_id>.yaml`.
324
- *
325
- * The schema is intentionally flat (one shape regardless of `status`) so
326
- * that pending and resolved YAMLs share the same parser. Required vs.
327
- * optional semantics by status (e.g. `rejection_reason` MUST be set when
328
- * `status === "rejected"`) are enforced at the CLI orchestration layer
329
- * rather than here, mirroring the approval event variants in
330
- * `event.schema.ts`.
331
- *
332
- * The `action` field is `{ kind: string }` with `passthrough()` so that
333
- * adapter-defined keys (e.g. `command`, `path`, `target_url`) survive the
334
- * round-trip without being stripped — matching the approval_requested
335
- * event variant.
336
- */
337
- declare const ApprovalSchema: z.ZodObject<{
338
- schema_version: z.ZodLiteral<"0.1.0">;
339
- id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
340
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
341
- created_at: z.ZodString;
342
- status: z.ZodEnum<{
343
- pending: "pending";
344
- approved: "approved";
345
- rejected: "rejected";
346
- expired: "expired";
347
- }>;
348
- risk_level: z.ZodEnum<{
349
- low: "low";
350
- medium: "medium";
351
- high: "high";
352
- critical: "critical";
353
- }>;
354
- action: z.ZodObject<{
355
- kind: z.ZodString;
356
- }, z.core.$loose>;
357
- reason: z.ZodString;
358
- expires_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
359
- resolver: z.ZodDefault<z.ZodNullable<z.ZodString>>;
360
- resolved_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
361
- note: z.ZodDefault<z.ZodNullable<z.ZodString>>;
362
- rejection_reason: z.ZodDefault<z.ZodNullable<z.ZodString>>;
363
- }, z.core.$strip>;
364
- /** Inferred runtime type for {@link ApprovalSchema}. */
365
- type Approval = z.infer<typeof ApprovalSchema>;
366
-
367
- declare const SessionStartedEventSchema: z.ZodObject<{
204
+ declare function isLazyExpired(approval: Approval, now: Date): boolean;
205
+
206
+ declare const SessionStartedEventSchema: z.ZodObject<{
368
207
  schema_version: z.ZodLiteral<"0.1.0">;
369
208
  id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
370
209
  session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
@@ -852,51 +691,93 @@ type NoteAddedEvent = z.infer<typeof NoteAddedEventSchema>;
852
691
  /** Narrowed runtime type for the `adapter_output` event variant (.strict()). */
853
692
  type AdapterOutputEvent = z.infer<typeof AdapterOutputEventSchema>;
854
693
 
855
- declare const SessionInnerImportSchema: z.ZodObject<{
856
- id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
857
- label: z.ZodOptional<z.ZodString>;
858
- task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
859
- workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
860
- source: z.ZodObject<{
861
- kind: z.ZodEnum<{
862
- "claude-code-adapter": "claude-code-adapter";
863
- human: "human";
864
- import: "import";
865
- terminal: "terminal";
866
- }>;
867
- version: z.ZodLiteral<"0.1.0">;
868
- }, z.core.$strip>;
869
- started_at: z.ZodString;
870
- ended_at: z.ZodOptional<z.ZodString>;
871
- status: z.ZodEnum<{
872
- initialized: "initialized";
873
- running: "running";
874
- waiting_approval: "waiting_approval";
875
- completed: "completed";
876
- failed: "failed";
877
- interrupted: "interrupted";
878
- imported: "imported";
879
- archived: "archived";
880
- }>;
881
- working_directory: z.ZodString;
882
- invocation: z.ZodObject<{
883
- command: z.ZodString;
884
- args: z.ZodArray<z.ZodString>;
885
- exit_code: z.ZodNullable<z.ZodNumber>;
886
- }, z.core.$strip>;
887
- related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
888
- events_log: z.ZodOptional<z.ZodString>;
889
- summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
890
- }, z.core.$strict>;
891
694
  /**
892
- * Schema for the round-trip JSON payload accepted by `basou session import
893
- * --format json`. The top level is `.strict()`; unknown keys at the outer
894
- * envelope are rejected.
695
+ * Recoverable warning surfaced via {@link ReplayOptions.onWarning}. The replay
696
+ * generator never throws on these it skips the offending line and continues.
697
+ *
698
+ * `partial_trailing_line` indicates the events.jsonl did not end with `\n` and
699
+ * the unterminated tail parsed as a complete event. The line is dropped
700
+ * instead of yielded so consumers cannot accidentally observe a
701
+ * partially-written record.
895
702
  */
896
- declare const SessionImportPayloadSchema: z.ZodObject<{
897
- schema_version: z.ZodString;
703
+ type ReplayWarning = {
704
+ kind: "partial_trailing_line";
705
+ line: number;
706
+ } | {
707
+ kind: "malformed_json";
708
+ line: number;
709
+ cause: unknown;
710
+ } | {
711
+ kind: "schema_violation";
712
+ line: number;
713
+ cause: unknown;
714
+ };
715
+ type ReplayOptions = {
716
+ /**
717
+ * Hook to receive recoverable warnings (partial line / malformed JSON /
718
+ * schema violation). When omitted, warnings are silently dropped — callers
719
+ * that want to surface them (e.g. CLI orchestration) MUST provide this hook.
720
+ */
721
+ onWarning?: (warning: ReplayWarning) => void;
722
+ };
723
+ /**
724
+ * Stream events from `<sessionDir>/events.jsonl` line by line.
725
+ *
726
+ * Behavior:
727
+ * - ENOENT or empty file: yields nothing without warning.
728
+ * - I/O error: throws `Error("Failed to read events.jsonl")` with the native
729
+ * error attached as `cause`. The thrown message never embeds an absolute
730
+ * path (pathless contract).
731
+ * - Trailing partial line that parses as a valid event: dropped silently when
732
+ * {@link ReplayOptions.onWarning} is omitted; otherwise reported as
733
+ * `partial_trailing_line`. A trailing partial line that fails JSON parsing
734
+ * is reported as `malformed_json` instead.
735
+ * - Malformed JSON / schema violation: skipped, with the corresponding
736
+ * warning when a hook is provided.
737
+ *
738
+ * Single-writer-per-session is assumed (see `event-writer.ts` JSDoc on
739
+ * {@link appendEvent}). Concurrent writers may interleave lines beyond
740
+ * `PIPE_BUF` and are not recovered here in v0.1.
741
+ */
742
+ declare function replayEvents(sessionDir: string, options?: ReplayOptions): AsyncGenerator<Event, void, void>;
743
+ /**
744
+ * Eager array helper: collect every event from {@link replayEvents} into
745
+ * memory. Convenience for callers that need the full list in one structure
746
+ * (e.g. `basou session show` rendering).
747
+ */
748
+ declare function readAllEvents(sessionDir: string, options?: ReplayOptions): Promise<Event[]>;
749
+
750
+ /** Session lifecycle states. */
751
+ declare const SessionStatusSchema: z.ZodEnum<{
752
+ initialized: "initialized";
753
+ running: "running";
754
+ waiting_approval: "waiting_approval";
755
+ completed: "completed";
756
+ failed: "failed";
757
+ interrupted: "interrupted";
758
+ imported: "imported";
759
+ archived: "archived";
760
+ }>;
761
+ /** Inferred runtime type for {@link SessionStatusSchema}. */
762
+ type SessionStatus = z.infer<typeof SessionStatusSchema>;
763
+ /** Source kind that produced the session. */
764
+ declare const SessionSourceKindSchema: z.ZodEnum<{
765
+ "claude-code-adapter": "claude-code-adapter";
766
+ human: "human";
767
+ import: "import";
768
+ terminal: "terminal";
769
+ }>;
770
+ /** Inferred runtime type for {@link SessionSourceKindSchema}. */
771
+ type SessionSourceKind = z.infer<typeof SessionSourceKindSchema>;
772
+ /**
773
+ * Schema for `.basou/sessions/<session_id>/session.yaml`. The minimal
774
+ * session document carries the actual fields nested under the outer
775
+ * `session:` key.
776
+ */
777
+ declare const SessionSchema: z.ZodObject<{
778
+ schema_version: z.ZodLiteral<"0.1.0">;
898
779
  session: z.ZodObject<{
899
- id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
780
+ id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
900
781
  label: z.ZodOptional<z.ZodString>;
901
782
  task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
902
783
  workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
@@ -924,804 +805,446 @@ declare const SessionImportPayloadSchema: z.ZodObject<{
924
805
  working_directory: z.ZodString;
925
806
  invocation: z.ZodObject<{
926
807
  command: z.ZodString;
927
- args: z.ZodArray<z.ZodString>;
808
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
928
809
  exit_code: z.ZodNullable<z.ZodNumber>;
929
810
  }, z.core.$strip>;
930
811
  related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
931
- events_log: z.ZodOptional<z.ZodString>;
812
+ events_log: z.ZodDefault<z.ZodString>;
932
813
  summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
933
- }, z.core.$strict>;
934
- events: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
935
- schema_version: z.ZodLiteral<"0.1.0">;
936
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
937
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
938
- occurred_at: z.ZodString;
939
- source: z.ZodString;
940
- type: z.ZodLiteral<"session_started">;
941
- }, z.core.$strip>, z.ZodObject<{
942
- schema_version: z.ZodLiteral<"0.1.0">;
943
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
944
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
945
- occurred_at: z.ZodString;
946
- source: z.ZodString;
947
- type: z.ZodLiteral<"session_ended">;
948
- exit_code: z.ZodOptional<z.ZodNumber>;
949
- }, z.core.$strip>, z.ZodObject<{
950
- schema_version: z.ZodLiteral<"0.1.0">;
951
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
952
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
953
- occurred_at: z.ZodString;
954
- source: z.ZodString;
955
- type: z.ZodLiteral<"session_status_changed">;
956
- from: z.ZodString;
957
- to: z.ZodString;
958
- }, z.core.$strip>, z.ZodObject<{
959
- schema_version: z.ZodLiteral<"0.1.0">;
960
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
961
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
962
- occurred_at: z.ZodString;
963
- source: z.ZodString;
964
- type: z.ZodLiteral<"approval_requested">;
965
- approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
966
- expires_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
967
- risk_level: z.ZodEnum<{
968
- low: "low";
969
- medium: "medium";
970
- high: "high";
971
- critical: "critical";
972
- }>;
973
- action: z.ZodObject<{
974
- kind: z.ZodString;
975
- }, z.core.$loose>;
976
- reason: z.ZodString;
977
- status: z.ZodLiteral<"pending">;
978
- }, z.core.$strip>, z.ZodObject<{
979
- schema_version: z.ZodLiteral<"0.1.0">;
980
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
981
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
982
- occurred_at: z.ZodString;
983
- source: z.ZodString;
984
- type: z.ZodLiteral<"approval_approved">;
985
- approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
986
- resolver: z.ZodOptional<z.ZodString>;
987
- note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
988
- }, z.core.$strip>, z.ZodObject<{
989
- schema_version: z.ZodLiteral<"0.1.0">;
990
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
991
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
992
- occurred_at: z.ZodString;
993
- source: z.ZodString;
994
- type: z.ZodLiteral<"approval_rejected">;
995
- approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
996
- resolver: z.ZodOptional<z.ZodString>;
997
- reason: z.ZodString;
998
- }, z.core.$strip>, z.ZodObject<{
999
- schema_version: z.ZodLiteral<"0.1.0">;
1000
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1001
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1002
- occurred_at: z.ZodString;
1003
- source: z.ZodString;
1004
- type: z.ZodLiteral<"approval_expired">;
1005
- approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
1006
- }, z.core.$strip>, z.ZodObject<{
1007
- schema_version: z.ZodLiteral<"0.1.0">;
1008
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1009
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1010
- occurred_at: z.ZodString;
1011
- source: z.ZodString;
1012
- type: z.ZodLiteral<"command_executed">;
1013
- command: z.ZodString;
1014
- args: z.ZodArray<z.ZodString>;
1015
- cwd: z.ZodString;
1016
- exit_code: z.ZodNullable<z.ZodNumber>;
1017
- signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1018
- received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1019
- duration_ms: z.ZodNumber;
1020
- }, z.core.$strip>, z.ZodObject<{
1021
- schema_version: z.ZodLiteral<"0.1.0">;
1022
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1023
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1024
- occurred_at: z.ZodString;
1025
- source: z.ZodString;
1026
- type: z.ZodLiteral<"git_snapshot">;
1027
- head: z.ZodString;
1028
- branch: z.ZodString;
1029
- dirty: z.ZodBoolean;
1030
- staged: z.ZodArray<z.ZodString>;
1031
- unstaged: z.ZodArray<z.ZodString>;
1032
- untracked: z.ZodArray<z.ZodString>;
1033
- ahead: z.ZodOptional<z.ZodNumber>;
1034
- behind: z.ZodOptional<z.ZodNumber>;
1035
- }, z.core.$strip>, z.ZodObject<{
1036
- schema_version: z.ZodLiteral<"0.1.0">;
1037
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1038
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1039
- occurred_at: z.ZodString;
1040
- source: z.ZodString;
1041
- type: z.ZodLiteral<"file_changed">;
1042
- path: z.ZodString;
1043
- change_type: z.ZodEnum<{
1044
- added: "added";
1045
- modified: "modified";
1046
- deleted: "deleted";
1047
- renamed: "renamed";
1048
- }>;
1049
- old_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1050
- }, z.core.$strip>, z.ZodObject<{
1051
- schema_version: z.ZodLiteral<"0.1.0">;
1052
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1053
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1054
- occurred_at: z.ZodString;
1055
- source: z.ZodString;
1056
- type: z.ZodLiteral<"decision_recorded">;
1057
- decision_id: z.ZodString & z.ZodType<`decision_${string}`, string, z.core.$ZodTypeInternals<`decision_${string}`, string>>;
1058
- title: z.ZodString;
1059
- rationale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1060
- alternatives: z.ZodOptional<z.ZodArray<z.ZodString>>;
1061
- rejected_reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1062
- linked_events: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>>>;
1063
- linked_files: z.ZodOptional<z.ZodArray<z.ZodString>>;
1064
- }, z.core.$strip>, z.ZodObject<{
1065
- schema_version: z.ZodLiteral<"0.1.0">;
1066
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1067
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1068
- occurred_at: z.ZodString;
1069
- source: z.ZodString;
1070
- type: z.ZodLiteral<"task_created">;
1071
- task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1072
- title: z.ZodString;
1073
- }, z.core.$strip>, z.ZodObject<{
1074
- schema_version: z.ZodLiteral<"0.1.0">;
1075
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1076
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1077
- occurred_at: z.ZodString;
1078
- source: z.ZodString;
1079
- type: z.ZodLiteral<"task_status_changed">;
1080
- task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1081
- from: z.ZodString;
1082
- to: z.ZodString;
1083
- }, z.core.$strip>, z.ZodObject<{
1084
- schema_version: z.ZodLiteral<"0.1.0">;
1085
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1086
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1087
- occurred_at: z.ZodString;
1088
- source: z.ZodString;
1089
- type: z.ZodLiteral<"task_reconciled">;
1090
- task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1091
- removed_created_in_session: z.ZodDefault<z.ZodNullable<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1092
- created_in_session_replacement: z.ZodDefault<z.ZodNullable<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1093
- removed_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1094
- }, z.core.$strict>, z.ZodObject<{
1095
- schema_version: z.ZodLiteral<"0.1.0">;
1096
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1097
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1098
- occurred_at: z.ZodString;
1099
- source: z.ZodString;
1100
- type: z.ZodLiteral<"task_linkage_refreshed">;
1101
- task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1102
- added_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1103
- removed_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1104
- final_count: z.ZodOptional<z.ZodNumber>;
1105
- }, z.core.$strict>, z.ZodObject<{
1106
- schema_version: z.ZodLiteral<"0.1.0">;
1107
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1108
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1109
- occurred_at: z.ZodString;
1110
- source: z.ZodString;
1111
- type: z.ZodLiteral<"task_deleted">;
1112
- task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1113
- title: z.ZodString;
1114
- }, z.core.$strict>, z.ZodObject<{
1115
- schema_version: z.ZodLiteral<"0.1.0">;
1116
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1117
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1118
- occurred_at: z.ZodString;
1119
- source: z.ZodString;
1120
- type: z.ZodLiteral<"task_archived">;
1121
- task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1122
- title: z.ZodString;
1123
- }, z.core.$strict>, z.ZodObject<{
1124
- schema_version: z.ZodLiteral<"0.1.0">;
1125
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1126
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1127
- occurred_at: z.ZodString;
1128
- source: z.ZodString;
1129
- type: z.ZodLiteral<"note_added">;
1130
- body: z.ZodString;
1131
- }, z.core.$strip>, z.ZodObject<{
1132
- schema_version: z.ZodLiteral<"0.1.0">;
1133
- id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
1134
- session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1135
- occurred_at: z.ZodString;
1136
- source: z.ZodString;
1137
- type: z.ZodLiteral<"adapter_output">;
1138
- stream: z.ZodEnum<{
1139
- stdout: "stdout";
1140
- stderr: "stderr";
1141
- }>;
1142
- summary: z.ZodString;
1143
- raw_ref: z.ZodString;
1144
- redacted: z.ZodOptional<z.ZodBoolean>;
1145
- }, z.core.$strict>], "type">>;
1146
- }, z.core.$strict>;
1147
- /** Inferred runtime type for {@link SessionImportPayloadSchema}. */
1148
- type SessionImportPayload = z.infer<typeof SessionImportPayloadSchema>;
1149
- /** Inferred runtime type for {@link SessionInnerImportSchema}. */
1150
- type SessionInnerImportInput = z.infer<typeof SessionInnerImportSchema>;
814
+ }, z.core.$strip>;
815
+ }, z.core.$strip>;
816
+ /** Inferred runtime type for {@link SessionSchema}. */
817
+ type Session = z.infer<typeof SessionSchema>;
1151
818
 
1152
819
  /**
1153
- * Absolute paths to the standard `.basou/` directory layout, derived from a
1154
- * given repository root. The shape mirrors the canonical `.basou/` tree
1155
- * (see `docs/spec/workspace.md`). `root` is the `.basou/` directory itself
1156
- * (i.e. `repositoryRoot/.basou`).
1157
- *
1158
- * `files` exposes the well-known top-level files inside `.basou/`. Each path
1159
- * is computed but not created — they are written by their respective
1160
- * subsystems (e.g. `writeManifest` for `manifest.yaml`).
820
+ * Threshold above which a still-`running` session with no `session_ended`
821
+ * event is flagged suspect.
1161
822
  *
1162
- * All fields are deeply readonly; consumers must not mutate the returned
1163
- * object.
823
+ * 24h: long enough that an active long-running session will not be flagged,
824
+ * short enough that an abandoned process is surfaced within a working day.
825
+ * Tunable via CLI option in a later step (continuation backlog #23).
1164
826
  */
1165
- type BasouPaths = {
1166
- readonly root: string;
1167
- readonly sessions: string;
1168
- readonly tasks: string;
1169
- readonly approvals: {
1170
- readonly pending: string;
1171
- readonly resolved: string;
1172
- };
1173
- readonly locks: string;
1174
- readonly logs: string;
1175
- readonly raw: string;
1176
- readonly tmp: string;
1177
- readonly files: {
1178
- readonly manifest: string;
1179
- readonly status: string;
1180
- readonly handoff: string;
1181
- readonly decisions: string;
1182
- };
827
+ declare const STUCK_THRESHOLD_MS: number;
828
+ type SuspectReason = "events_say_ended_but_yaml_running" | "running_no_end_event";
829
+ type SessionEntry = {
830
+ sessionId: string;
831
+ session: Session;
832
+ suspect: boolean;
833
+ suspectReason: SuspectReason | null;
1183
834
  };
1184
835
  /**
1185
- * Compute absolute paths to the standard `.basou/` directory layout under
1186
- * `repositoryRoot`. Pure: performs no I/O and is safe to call before the
1187
- * directory exists.
836
+ * Per-session degradation reason emitted by {@link loadSessionEntries.onSkip}.
1188
837
  *
1189
- * @param repositoryRoot Absolute path to the git repository root (the
1190
- * parent directory of `.basou/`). Caller is responsible for resolving
1191
- * `process.cwd()` or running `git rev-parse --show-toplevel` upstream;
1192
- * this function does not validate that the path exists or is a git
1193
- * repository.
838
+ * - `session_yaml_missing` (ENOENT) and `session_yaml_invalid` (parse or schema
839
+ * failure) both omit the entry from the result.
840
+ * - `events_jsonl_unreadable` still pushes the entry with `suspect=false` so
841
+ * the session row remains visible to the caller; only the suspect check is
842
+ * degraded. Matches the existing CLI behaviour at
843
+ * `packages/cli/src/commands/session.ts` (suspect-check stderr warning).
1194
844
  */
1195
- declare function basouPaths(repositoryRoot: string): BasouPaths;
845
+ type SessionSkipReason = "session_yaml_missing" | "session_yaml_invalid" | "events_jsonl_unreadable";
846
+ type LoadSessionEntriesOptions = {
847
+ /**
848
+ * Single `now` shared across every {@link classifySuspect} call so that
849
+ * sessions classified back-to-back observe the same instant. Avoids
850
+ * boundary races where a session at age ≈ 24h would flip between calls.
851
+ */
852
+ now: Date;
853
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
854
+ onSkip?: (sessionId: string, reason: SessionSkipReason) => void;
855
+ };
1196
856
  /**
1197
- * Create the standard `.basou/` directory layout under `repositoryRoot`.
1198
- *
1199
- * Idempotent: a no-op on an already-initialized layout. Returns the resolved
1200
- * {@link BasouPaths} so callers can immediately use them.
857
+ * List session directory names under `paths.sessions`, ULID ascending.
1201
858
  *
1202
- * Throws if `repositoryRoot/.basou` (or any required subdirectory) exists
1203
- * but is not a directory, or if filesystem permissions prevent creation.
1204
- * All thrown error messages are pathless; the original native error is
1205
- * attached as `cause` for diagnostics.
859
+ * - Returns `[]` when the sessions directory does not exist (empty workspace
860
+ * or pre-init state).
861
+ * - Throws `Error("Failed to enumerate sessions", { cause })` on other I/O.
862
+ * - Only directories are returned (`.gitkeep` and other files are filtered).
1206
863
  *
1207
- * @param repositoryRoot Absolute path to the git repository root. See
1208
- * {@link basouPaths} for the contract on this parameter.
864
+ * Sort order is `Array.prototype.sort()` default (Unicode code-point
865
+ * compare). ULIDs are Crockford base32 in uppercase, so the natural sort
866
+ * is also chronological session-start order.
1209
867
  */
1210
- declare function ensureBasouDirectory(repositoryRoot: string): Promise<BasouPaths>;
1211
-
1212
- /** Which side of `.basou/approvals/` an approval YAML lives on. */
1213
- type ApprovalLocation = "pending" | "resolved";
1214
- /** Result returned by {@link loadApproval}: the parsed approval and where it was found. */
1215
- type LoadedApproval = {
1216
- approval: Approval;
1217
- location: ApprovalLocation;
1218
- };
868
+ declare function enumerateSessionDirs(paths: BasouPaths): Promise<string[]>;
1219
869
  /**
1220
- * Locate and load the approval YAML for `approvalId`. Searches resolved
1221
- * first so that a duplicated YAML (the crash-window scenario where both
1222
- * pending and resolved exist for the same id) returns the resolved-side
1223
- * record — matching the dedupe rule used by `approval list` and
1224
- * `resolveApprovalId`. Returns null if neither directory contains the
1225
- * YAML. Throws with a pathless message on read or schema-validation
1226
- * failure.
870
+ * Read and validate `<paths.sessions>/<sessionId>/session.yaml`.
871
+ *
872
+ * - Re-throws the yaml-store fixed-message `"YAML file not found"` for
873
+ * ENOENT so the caller can branch on it.
874
+ * - Throws `Error("Failed to read session.yaml", { cause })` for parse
875
+ * failures and schema violations (cause is either the YAML parser error
876
+ * or the zod error).
1227
877
  */
1228
- declare function loadApproval(paths: BasouPaths, approvalId: string): Promise<LoadedApproval | null>;
878
+ declare function readSessionYaml(paths: BasouPaths, sessionId: string): Promise<Session>;
1229
879
  /**
1230
- * Enumerate approval IDs by inspecting `<id>.yaml` filenames in pending
1231
- * and resolved. ENOENT on either directory is treated as empty (e.g. a
1232
- * workspace that has no resolved approvals yet). YAML parse and schema
1233
- * validation are NOT performed; callers that need the parsed approval
1234
- * should use {@link loadApproval} per ID.
880
+ * Classify a `running` session as suspect using one of two rules:
881
+ *
882
+ * - Rule A (`events_say_ended_but_yaml_running`): events.jsonl contains a
883
+ * `session_ended` event but the session.yaml is still `running`. The
884
+ * session ended cleanly in the event log but the YAML write was lost or
885
+ * never reached.
886
+ * - Rule B (`running_no_end_event`): no `session_ended` event and the last
887
+ * event is older than {@link STUCK_THRESHOLD_MS}. The process likely
888
+ * crashed or was killed.
889
+ *
890
+ * Sessions that are not `running` are never suspect.
891
+ *
892
+ * I/O failure on events.jsonl is re-thrown unwrapped so the caller can
893
+ * degrade with a warning instead of treating the session as healthy. The
894
+ * caller is also responsible for surfacing replay warnings via `onWarning`.
1235
895
  */
1236
- declare function enumerateApprovals(paths: BasouPaths): Promise<{
1237
- pending: string[];
1238
- resolved: string[];
896
+ declare function classifySuspect(paths: BasouPaths, sessionId: string, session: Session, now: Date, onWarning?: (warning: ReplayWarning) => void): Promise<{
897
+ suspect: boolean;
898
+ suspectReason: SuspectReason | null;
1239
899
  }>;
1240
900
  /**
1241
- * Return true when an approval is in `pending` state and its `expires_at`
1242
- * timestamp has elapsed. Used by `basou approval list` / `show` to surface
1243
- * a `(expired)` label without mutating the YAML file. Approval expiry uses
1244
- * lazy-evaluation semantics; actual `approval_expired` event firing is
1245
- * deferred to a later step.
901
+ * High-level helper that enumerates session dirs, reads each `session.yaml`,
902
+ * and classifies suspect for `running` sessions in one pass.
1246
903
  *
1247
- * `now` is taken as a parameter so a single CLI invocation can share one
1248
- * "now" across every record it inspects (avoids boundary races where two
1249
- * reads of `Date.now()` straddle an expiry instant).
904
+ * Per-session degradations are surfaced via `options.onSkip`:
905
+ * - `session_yaml_missing` (ENOENT) and `session_yaml_invalid` (parse or
906
+ * schema violation): the entry is omitted from the result.
907
+ * - `events_jsonl_unreadable`: the entry is still pushed with `suspect=false`
908
+ * so callers can render the session row plus a CLI-side warning.
909
+ *
910
+ * `options.now` is taken once and threaded into every {@link classifySuspect}
911
+ * call so age comparisons are consistent across sessions.
1250
912
  */
1251
- declare function isLazyExpired(approval: Approval, now: Date): boolean;
913
+ declare function loadSessionEntries(paths: BasouPaths, options: LoadSessionEntriesOptions): Promise<SessionEntry[]>;
1252
914
 
915
+ type DecisionsRendererInput = {
916
+ paths: BasouPaths;
917
+ nowIso: string;
918
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
919
+ onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
920
+ };
921
+ type DecisionsRendererResult = {
922
+ /** Generated body WITHOUT BASOU:GENERATED markers. */
923
+ body: string;
924
+ decisionCount: number;
925
+ };
1253
926
  /**
1254
- * Read a YAML file as `unknown`. Caller MUST validate via a zod schema.
927
+ * Render the body of `decisions.md` from `decision_recorded` events across
928
+ * every healthy session in the workspace.
1255
929
  *
1256
- * Throws Error with pathless message and the original native error attached
1257
- * as `cause` for I/O failures and YAML parse errors. All fs and parse exits
1258
- * go through fixed messages so absolute paths cannot leak via `error.message`.
1259
- */
1260
- declare function readYamlFile(filePath: string): Promise<unknown>;
1261
- /**
1262
- * Write a value as YAML using {@link atomicReplace} for crash-resistant
1263
- * atomicity. The shared helper handles the tmp-file + rename sequence,
1264
- * `wx` collision guard, and best-effort tmp cleanup on failure. This
1265
- * wrapper adds the YAML serialisation and the pathless error vocabulary.
930
+ * Session enumeration goes through {@link loadSessionEntries} (the same path
931
+ * the handoff renderer uses) so that `session.yaml`-broken sessions are
932
+ * skipped in BOTH outputs and the handoff's `decisionCount` summary stays
933
+ * consistent with the number of sections rendered here.
934
+ *
935
+ * Order: `occurred_at` ascending with `decisionId` (= ULID) as tie-breaker.
936
+ * Both fields are monotonic, so the result is a stable cross-session
937
+ * timeline.
938
+ *
939
+ * The decision rich fields (rationale / alternatives / rejected_reason /
940
+ * linked_events / linked_files) are rendered when the event carries them.
941
+ * `linked_events` and `linked_files` are OPAQUE references: the schema only
942
+ * validates the SHAPE, not existence — references that cannot be resolved
943
+ * to a known event id or an existing file on disk are surfaced inline as
944
+ * `(missing)` so cross-workspace round-trips never reject parse-time.
1266
945
  */
1267
- declare function writeYamlFile(filePath: string, value: unknown): Promise<void>;
946
+ declare function renderDecisions(input: DecisionsRendererInput): Promise<DecisionsRendererResult>;
947
+
1268
948
  /**
1269
- * Atomically create a new YAML file. Like {@link writeYamlFile} but
1270
- * delegates to {@link atomicCreate} so a pre-existing target fails with
1271
- * EEXIST instead of being silently overwritten.
949
+ * Append a single Basou event to `<sessionDir>/events.jsonl`.
1272
950
  *
1273
- * Used by `basou approval approve` / `reject` to write the resolved-side
1274
- * YAML, so a concurrent resolver cannot overwrite an already-resolved
1275
- * approval.
951
+ * The event is validated against the discriminated union {@link EventSchema}
952
+ * before being serialized as a single JSONL line (UTF-8, terminated by `\n`).
953
+ * Validation enforces the per-variant contract (required fields, source
954
+ * vocabulary, strict variants such as `adapter_output`).
1276
955
  *
1277
- * Throws `Error("Failed to write YAML file", { cause })` on failure; if
1278
- * `cause.code === "EEXIST"` the caller can detect a target-exists race.
956
+ * Atomicity: writes go through `appendFile` which uses `O_APPEND`. Lines up
957
+ * to `PIPE_BUF` bytes (Linux 4096 / macOS 512) are written atomically by the
958
+ * kernel; longer lines may interleave with concurrent writers and are not
959
+ * recovered here. v0.1 assumes a single writer per session, so partial-line
960
+ * recovery is delegated to the read side (event replay) when introduced.
961
+ *
962
+ * Throws if validation fails or the underlying append errors. The thrown
963
+ * Error message is pathless; the original error is attached as `cause`.
964
+ *
965
+ * @param sessionDir absolute path to `.basou/sessions/<session_id>/`
966
+ * @param event unknown payload to validate and append
1279
967
  */
1280
- declare function linkYamlFile(filePath: string, value: unknown): Promise<void>;
968
+ declare function appendEvent(sessionDir: string, event: unknown): Promise<void>;
1281
969
  /**
1282
- * Overwrite an existing YAML file atomically. Like {@link writeYamlFile}
1283
- * but with a distinct pathless message label, used for files that
1284
- * legitimately need in-place mutation (e.g. session.yaml's status /
1285
- * ended_at lifecycle updates).
970
+ * Write `events.jsonl` in one atomic tmp+rename pass via {@link atomicReplace},
971
+ * validating every event against {@link EventSchema} before any disk I/O so
972
+ * a payload that fails validation never leaves a partial file behind.
973
+ *
974
+ * The helper is used by the round-trip importer (`session-import.ts`) and the
975
+ * ad-hoc session orchestrator (`ad-hoc-session.ts`) where a small, fixed batch
976
+ * of events must land together or not at all. Zero events produces a
977
+ * zero-byte file so the session_yaml `events_log` pointer remains valid.
978
+ *
979
+ * Throws `"Invalid Basou event payload"` (same fixed message as
980
+ * {@link appendEvent}) on validation failure, or `"Failed to write
981
+ * events.jsonl"` on a disk I/O failure. The original native error is attached
982
+ * as `cause`.
1286
983
  */
1287
- declare function overwriteYamlFile(filePath: string, value: unknown): Promise<void>;
984
+ declare function writeEventsBulk(sessionDir: string, events: Event[]): Promise<void>;
1288
985
 
1289
986
  /**
1290
- * Inputs for {@link createManifest}. Optional fields drop out of the
1291
- * resulting Manifest entirely (they are not emitted as `null`/`undefined`
1292
- * in YAML); pass `null` for `repositoryUrl` to keep an explicit `null`.
1293
- */
1294
- type CreateManifestInput = {
1295
- workspaceName: string;
1296
- projectName?: string;
1297
- projectDescription?: string;
1298
- repositoryUrl?: string | null;
1299
- /** Override for tests; defaults to `new Date()`. */
1300
- now?: Date;
1301
- /** Override for tests; defaults to a freshly generated `ws_<ULID>`. */
1302
- workspaceId?: PrefixedId<"ws">;
1303
- };
1304
- /**
1305
- * Build a fresh Manifest object that satisfies the manifest schema's
1306
- * minimum shape. Performs no I/O. Returned object is parse-validated by
1307
- * `ManifestSchema`.
987
+ * Status classification used by the `file_changed` event schema. Limited to
988
+ * the four classes that simple-git's `git diff --name-status` reliably
989
+ * surfaces; copy / unmerged / typechange entries are intentionally dropped
990
+ * to keep the event payload shape narrow.
1308
991
  */
1309
- declare function createManifest(input: CreateManifestInput): Manifest;
992
+ type FileChangeStatus = "added" | "modified" | "deleted" | "renamed";
1310
993
  /**
1311
- * Write a Manifest to `paths.files.manifest`. Re-validates via
1312
- * `ManifestSchema` before serialization.
1313
- *
1314
- * Refuses to overwrite an existing manifest unless `force: true`.
994
+ * Single file-level change observed between two refs. `old_path` is set
995
+ * only for `renamed` entries (the previous path of the file).
1315
996
  */
1316
- declare function writeManifest(paths: BasouPaths, manifest: Manifest, options?: {
1317
- force?: boolean;
1318
- }): Promise<void>;
997
+ type FileChange = {
998
+ path: string;
999
+ old_path?: string;
1000
+ status: FileChangeStatus;
1001
+ };
1319
1002
  /**
1320
- * Read and parse a Manifest from `paths.files.manifest`. Throws if the file
1321
- * is missing or contents fail `ManifestSchema` validation.
1003
+ * Result of {@link getDiff}. The `changed_files` array is in git's natural
1004
+ * `--name-status` order; callers requiring deterministic ordering should
1005
+ * sort by `path` themselves.
1322
1006
  */
1323
- declare function readManifest(paths: BasouPaths): Promise<Manifest>;
1324
-
1325
- type AppendBasouGitignoreResult = {
1326
- /** True if the block was appended (or the file was newly created). */
1327
- readonly appended: boolean;
1007
+ type DiffResult = {
1008
+ changed_files: FileChange[];
1328
1009
  };
1329
1010
  /**
1330
- * Append Basou's default `.gitignore` block to `repositoryRoot/.gitignore`.
1011
+ * Compute the file-level diff between two git refs.
1331
1012
  *
1332
- * The block contents are derived from the Basou v0.1 specification (the
1333
- * standard ignore + commit recommendations). Callers must pass an absolute
1334
- * path to a Git repository root.
1013
+ * Returns a list of changed file paths classified by status (added /
1014
+ * modified / deleted / renamed). Diff content is intentionally NOT
1015
+ * returned `file_changed` events record paths only, and raw diff bodies
1016
+ * are excluded so the trace cannot inadvertently leak source code that may
1017
+ * be sensitive. Use `git show <ref>` to obtain the underlying diff.
1335
1018
  *
1336
- * Behavior:
1337
- * - If `.gitignore` does not exist, it is created with the Basou block.
1338
- * - If a line starting with `# Basou - default ignore` is already present,
1339
- * the file is left untouched and `appended: false` is returned
1340
- * (idempotent).
1341
- * - If `.gitignore` is a symlink, the link is followed and the target file
1342
- * is updated. Symlinks are not rejected.
1019
+ * Pathless contract: every thrown message is a fixed string from the set
1020
+ * {`Not a git repository`, `Git executable not found in PATH. Install git
1021
+ * first.`, `Invalid ref`, `Failed to compute git diff`}; native errors are
1022
+ * preserved on `Error.cause`.
1343
1023
  *
1344
- * On I/O failure throws Error with a pathless message
1345
- * (`Failed to read .gitignore` / `Failed to write .gitignore`) and the
1346
- * original native error attached as `cause`.
1024
+ * Special cases:
1025
+ * - `baseRef === headRef` short-circuits to an empty result
1026
+ * - copy / unmerged / typechange / unknown status codes are skipped
1027
+ *
1028
+ * @param repoRoot absolute path to the git repository root
1029
+ * @param baseRef base ref (e.g. session-start HEAD sha)
1030
+ * @param headRef head ref (e.g. session-end HEAD sha)
1347
1031
  */
1348
- declare function appendBasouGitignore(repositoryRoot: string): Promise<AppendBasouGitignoreResult>;
1032
+ declare function getDiff(repoRoot: string, baseRef: string, headRef: string): Promise<DiffResult>;
1349
1033
 
1350
1034
  /**
1351
- * The two lock scopes basou uses. `task` guards the read-modify-write window
1352
- * around a single `task.md`; `session` guards the events.jsonl append plus
1353
- * surrounding `session.yaml` mutation for a single session. Two scopes use
1354
- * different lockfile names so they never collide on disk.
1035
+ * Payload subset of `git_snapshot` event, mechanically derived from the
1036
+ * zod-inferred event type. The wrapping event-shape fields
1037
+ * (schema_version, id, session_id, occurred_at, source, type) are added by
1038
+ * the caller (session lifecycle in later steps) when constructing the
1039
+ * event, so the schema remains the single source of truth.
1040
+ *
1041
+ * `ahead` / `behind` are omitted when there is no remote or no upstream
1042
+ * tracking; the schema declares both as optional non-negative integers.
1355
1043
  */
1356
- type LockScope = "task" | "session";
1357
- type LockHandle = {
1358
- /**
1359
- * Release the lock by unlinking the lockfile. Best-effort: any unlink error
1360
- * is swallowed so a doubled release does not raise, and disk state never
1361
- * holds a stranded lockfile after the caller's `finally` block.
1362
- */
1363
- release: () => Promise<void>;
1364
- };
1044
+ type GitSnapshot = Omit<GitSnapshotEvent, "schema_version" | "id" | "session_id" | "occurred_at" | "source" | "type">;
1365
1045
  /**
1366
- * Acquire an advisory lock at `<paths.locks>/<scope>_<id>.lock` for the
1367
- * lifetime of the returned handle. Lockfile body records the holder's pid
1368
- * and acquire timestamp so a competitor can detect stale locks left by a
1369
- * SIGINT'd CLI run and recover automatically.
1046
+ * Resolve the absolute path of the Git repository root that contains `cwd`.
1047
+ * Equivalent to `git rev-parse --show-toplevel`.
1370
1048
  *
1371
- * Acquisition strategy:
1372
- * 1. {@link atomicCreate} the lockfile (POSIX link(2) + EEXIST).
1373
- * 2. On EEXIST, probe the existing lockfile via {@link isStaleLock}.
1374
- * - If stale (= holder pid is dead or lock is older than
1375
- * {@link STALE_LOCK_MAX_AGE_MS}), `unlink` the stale file and retry
1376
- * the atomic create once.
1377
- * - If still EEXIST after the retry (= another competitor won the race),
1378
- * throw `"Lock is held by another process"`.
1379
- * - If the holder is alive, throw `"Lock is held by another process"`
1380
- * without retrying.
1049
+ * Throws `Error("Git executable not found in PATH. Install git first.")`
1050
+ * with the spawn error attached as `cause` when git itself is missing.
1051
+ * Throws `Error("Not a git repository")` (without command-specific suffix)
1052
+ * when `cwd` is not inside a repository callers MAY wrap with their own
1053
+ * "Run 'git init' first, then re-run 'basou XXX'." suffix.
1381
1054
  *
1382
- * The caller MUST call `release()` (typically from a `finally` block); the
1383
- * `process.exit()` path or a fatal crash relies on stale-lock detection on
1384
- * the next acquire to recover.
1055
+ * Pathless contract: the thrown message never embeds `cwd` or any absolute
1056
+ * path; native errors are kept on `error.cause` for verbose surfacing.
1385
1057
  */
1386
- declare function acquireLock(paths: BasouPaths, scope: LockScope, resourceId: string): Promise<LockHandle>;
1387
-
1058
+ declare function resolveRepositoryRoot(cwd: string): Promise<string>;
1388
1059
  /**
1389
- * Walk the cause chain (up to `depth` levels) looking for an Error whose
1390
- * errno-style `code` matches `code`. Returns true on the first match.
1391
- * Resilient to wrapper depth changes so that ENOENT detection survives
1392
- * future error-wrapping refactors.
1060
+ * Read `remote.origin.url` from the local repository config. Returns
1061
+ * `undefined` if the remote is unset, the value is empty, or the lookup
1062
+ * fails for any reason (best-effort).
1063
+ *
1064
+ * The `--local` scope is critical: callers MUST NOT pick up the developer's
1065
+ * global remote.origin.url, which could leak the wrong repository URL into
1066
+ * `manifest.yaml`.
1393
1067
  */
1394
- declare function findErrorCode(error: unknown, code: string, depth?: number): boolean;
1395
-
1068
+ declare function tryRemoteUrl(repositoryRoot: string): Promise<string | undefined>;
1396
1069
  /**
1397
- * Refuse to operate on `.basou` if it is a symlink or not a directory. This
1398
- * prevents `writeStatus` from being tricked into writing `status.json`
1399
- * outside the repository root via a swapped `.basou` symlink. Mirrors
1400
- * `ensureBasouDirectory`'s lstat-based guard.
1070
+ * Build a {@link GitSnapshot} for the repository at `repositoryRoot`. The
1071
+ * caller is responsible for ensuring `repositoryRoot` is the canonical root
1072
+ * (typically obtained via {@link resolveRepositoryRoot}); this function
1073
+ * verifies repo membership via `git rev-parse --is-inside-work-tree` to
1074
+ * distinguish a non-git directory from an empty repository.
1401
1075
  *
1402
- * If `.basou` is absent the underlying ENOENT is propagated (wrapped) so
1403
- * callers can map it to "workspace not initialized" via `findErrorCode`.
1076
+ * Edge cases:
1077
+ * - **non-git directory**: throws `Error("Not a git repository")`
1078
+ * - **empty repo (no commits)**: throws `Error("No commits in repository")`
1079
+ * - **detached HEAD**: `branch = "HEAD"`, `head = commit hash`,
1080
+ * `ahead`/`behind` omitted
1081
+ * - **no remote / no upstream tracking**: `ahead`/`behind` omitted
1404
1082
  *
1405
- * Note: this is a baseline safety net, not a TOCTOU fix — the directory
1406
- * could still be replaced between this check and the subsequent write. The
1407
- * goal is to detect already-swapped symlinks, not to race-proof the
1408
- * filesystem.
1083
+ * Pathless contract preserved on every throw path.
1409
1084
  */
1410
- declare function assertBasouRootSafe(rootPath: string): Promise<void>;
1085
+ declare function getSnapshot(repositoryRoot: string): Promise<GitSnapshot>;
1086
+
1411
1087
  /**
1412
- * Build a StatusSnapshot from a manifest plus the path layout, observing
1413
- * each subdirectory's presence via `lstat`. Read-only with respect to the
1414
- * workspace state; writes nothing. The result is re-validated by
1415
- * `StatusSchema.parse` before being returned.
1088
+ * Allowed ID type prefixes for Basou entities.
1416
1089
  *
1417
- * @param input.now Override for testing; defaults to `new Date()`.
1090
+ * Frozen at runtime so that mutating the exported array cannot diverge from
1091
+ * the validation set used internally. The single source of truth for both
1092
+ * the `IdPrefix` type and runtime prefix checks.
1418
1093
  */
1419
- declare function buildStatusSnapshot(input: {
1420
- manifest: Manifest;
1421
- paths: BasouPaths;
1422
- now?: Date;
1423
- }): Promise<StatusSnapshot>;
1424
- /**
1425
- * Atomically write a StatusSnapshot to `paths.files.status`.
1426
- *
1427
- * Re-validates via `StatusSchema.parse` before any file I/O, so an invalid
1428
- * snapshot throws synchronously and never overwrites the existing
1429
- * `status.json`. Delegates the tmp-file + rename pass to {@link atomicReplace}.
1430
- *
1431
- * **Precondition**: callers MUST invoke {@link assertBasouRootSafe} on
1432
- * `paths.root` first to ensure `.basou` is a real directory and not a
1433
- * swapped symlink. `writeStatus` does not redo this guard — it trusts the
1434
- * caller — so a direct invocation without the guard could write
1435
- * `status.json` outside the repository root.
1436
- */
1437
- declare function writeStatus(paths: BasouPaths, snapshot: StatusSnapshot): Promise<void>;
1094
+ declare const ID_PREFIXES: readonly ["ws", "task", "ses", "evt", "appr", "decision"];
1438
1095
  /**
1439
- * Read `.basou/status.json` for the current schema_version (0.1.0). This
1440
- * is a cache reader only; cross-version migration is not supported here.
1441
- * Older or newer status.json shapes will fail `StatusSchema.parse` —
1442
- * callers regenerate by calling `buildStatusSnapshot` + `writeStatus`.
1096
+ * Type prefix used for Basou entity IDs.
1097
+ * Format: `<prefix>_<26-char ULID>`, e.g. `ws_01HXABCDEF1234567890ABCDEF`.
1443
1098
  */
1444
- declare function readStatus(paths: BasouPaths): Promise<StatusSnapshot>;
1445
-
1099
+ type IdPrefix = (typeof ID_PREFIXES)[number];
1446
1100
  /**
1447
- * Recoverable warning surfaced via {@link ReplayOptions.onWarning}. The replay
1448
- * generator never throws on these — it skips the offending line and continues.
1101
+ * A Basou entity ID as a template literal type.
1449
1102
  *
1450
- * `partial_trailing_line` indicates the events.jsonl did not end with `\n` and
1451
- * the unterminated tail parsed as a complete event. The line is dropped
1452
- * instead of yielded so consumers cannot accidentally observe a
1453
- * partially-written record.
1103
+ * `PrefixedId<"ses">` narrows to ``ses_${string}`` so a session schema can
1104
+ * preserve the prefix in its inferred type beyond runtime validation.
1454
1105
  */
1455
- type ReplayWarning = {
1456
- kind: "partial_trailing_line";
1457
- line: number;
1458
- } | {
1459
- kind: "malformed_json";
1460
- line: number;
1461
- cause: unknown;
1462
- } | {
1463
- kind: "schema_violation";
1464
- line: number;
1465
- cause: unknown;
1466
- };
1467
- type ReplayOptions = {
1468
- /**
1469
- * Hook to receive recoverable warnings (partial line / malformed JSON /
1470
- * schema violation). When omitted, warnings are silently dropped — callers
1471
- * that want to surface them (e.g. CLI orchestration) MUST provide this hook.
1472
- */
1473
- onWarning?: (warning: ReplayWarning) => void;
1474
- };
1106
+ type PrefixedId<P extends IdPrefix = IdPrefix> = `${P}_${string}`;
1475
1107
  /**
1476
- * Stream events from `<sessionDir>/events.jsonl` line by line.
1108
+ * Generate a Crockford Base32 ULID.
1477
1109
  *
1478
- * Behavior:
1479
- * - ENOENT or empty file: yields nothing without warning.
1480
- * - I/O error: throws `Error("Failed to read events.jsonl")` with the native
1481
- * error attached as `cause`. The thrown message never embeds an absolute
1482
- * path (pathless contract).
1483
- * - Trailing partial line that parses as a valid event: dropped silently when
1484
- * {@link ReplayOptions.onWarning} is omitted; otherwise reported as
1485
- * `partial_trailing_line`. A trailing partial line that fails JSON parsing
1486
- * is reported as `malformed_json` instead.
1487
- * - Malformed JSON / schema violation: skipped, with the corresponding
1488
- * warning when a hook is provided.
1110
+ * The result is a 26-character, lexicographically time-sortable identifier.
1111
+ * Multiple calls within the same millisecond are strictly increasing for the
1112
+ * lifetime of the current process.
1489
1113
  *
1490
- * Single-writer-per-session is assumed (see `event-writer.ts` JSDoc on
1491
- * {@link appendEvent}). Concurrent writers may interleave lines beyond
1492
- * `PIPE_BUF` and are not recovered here in v0.1.
1493
- */
1494
- declare function replayEvents(sessionDir: string, options?: ReplayOptions): AsyncGenerator<Event, void, void>;
1495
- /**
1496
- * Eager array helper: collect every event from {@link replayEvents} into
1497
- * memory. Convenience for callers that need the full list in one structure
1498
- * (e.g. `basou session show` rendering).
1499
- */
1500
- declare function readAllEvents(sessionDir: string, options?: ReplayOptions): Promise<Event[]>;
1501
-
1502
- /**
1503
- * Threshold above which a still-`running` session with no `session_ended`
1504
- * event is flagged suspect.
1114
+ * NOTE: `seedTime` is forwarded to the underlying monotonic factory and is
1115
+ * NOT a deterministic seed: repeated calls with the same `seedTime` still
1116
+ * return strictly increasing values, because the factory increments its
1117
+ * internal counter on each call.
1505
1118
  *
1506
- * 24h: long enough that an active long-running session will not be flagged,
1507
- * short enough that an abandoned process is surfaced within a working day.
1508
- * Tunable via CLI option in a later step (continuation backlog #23).
1119
+ * @param seedTime Optional millisecond timestamp passed to the monotonic
1120
+ * factory. Useful for ordered generation in tests; not deterministic.
1509
1121
  */
1510
- declare const STUCK_THRESHOLD_MS: number;
1511
- type SuspectReason = "events_say_ended_but_yaml_running" | "running_no_end_event";
1512
- type SessionEntry = {
1513
- sessionId: string;
1514
- session: Session;
1515
- suspect: boolean;
1516
- suspectReason: SuspectReason | null;
1517
- };
1122
+ declare function ulid(seedTime?: number): string;
1518
1123
  /**
1519
- * Per-session degradation reason emitted by {@link loadSessionEntries.onSkip}.
1124
+ * Generate a prefixed Basou ID, e.g. `ses_01HXABCDEF1234567890ABCDEF`.
1520
1125
  *
1521
- * - `session_yaml_missing` (ENOENT) and `session_yaml_invalid` (parse or schema
1522
- * failure) both omit the entry from the result.
1523
- * - `events_jsonl_unreadable` still pushes the entry with `suspect=false` so
1524
- * the session row remains visible to the caller; only the suspect check is
1525
- * degraded. Matches the existing CLI behaviour at
1526
- * `packages/cli/src/commands/session.ts` (suspect-check stderr warning).
1126
+ * The return type preserves the prefix as a template literal type so that
1127
+ * downstream zod schemas can narrow an `IdPrefix` parameter through the API.
1128
+ *
1129
+ * Throws if `prefix` is not one of {@link ID_PREFIXES}. The runtime guard
1130
+ * defends against JavaScript callers and casted TypeScript that bypass the
1131
+ * compile-time `IdPrefix` constraint.
1527
1132
  */
1528
- type SessionSkipReason = "session_yaml_missing" | "session_yaml_invalid" | "events_jsonl_unreadable";
1529
- type LoadSessionEntriesOptions = {
1530
- /**
1531
- * Single `now` shared across every {@link classifySuspect} call so that
1532
- * sessions classified back-to-back observe the same instant. Avoids
1533
- * boundary races where a session at age ≈ 24h would flip between calls.
1534
- */
1535
- now: Date;
1536
- onWarning?: (warning: ReplayWarning, sessionId: string) => void;
1537
- onSkip?: (sessionId: string, reason: SessionSkipReason) => void;
1538
- };
1133
+ declare function prefixedUlid<P extends IdPrefix>(prefix: P): PrefixedId<P>;
1539
1134
  /**
1540
- * List session directory names under `paths.sessions`, ULID ascending.
1135
+ * Check whether the given string is a valid prefixed Basou ID.
1541
1136
  *
1542
- * - Returns `[]` when the sessions directory does not exist (empty workspace
1543
- * or pre-init state).
1544
- * - Throws `Error("Failed to enumerate sessions", { cause })` on other I/O.
1545
- * - Only directories are returned (`.gitkeep` and other files are filtered).
1137
+ * Returns true only if the string has shape `<prefix>_<ULID>` where prefix is
1138
+ * one of {@link ID_PREFIXES} and the trailing 26 characters form a valid
1139
+ * Crockford Base32 ULID. Validation combines a strict shape regex (to enforce
1140
+ * the 0-7 leading char and the I/L/O/U exclusion) with the npm `ulid`
1141
+ * library's `isValid` for forward compatibility.
1546
1142
  *
1547
- * Sort order is `Array.prototype.sort()` default (Unicode code-point
1548
- * compare). ULIDs are Crockford base32 in uppercase, so the natural sort
1549
- * is also chronological session-start order.
1143
+ * NOTE: This validates the prefix is known. Schemas that require a specific
1144
+ * prefix (e.g. only `ses_*` for a session ID) must add their own narrowing.
1550
1145
  */
1551
- declare function enumerateSessionDirs(paths: BasouPaths): Promise<string[]>;
1146
+ declare function isValidPrefixedId(value: string): boolean;
1147
+
1552
1148
  /**
1553
- * Read and validate `<paths.sessions>/<sessionId>/session.yaml`.
1554
- *
1555
- * - Re-throws the yaml-store fixed-message `"YAML file not found"` for
1556
- * ENOENT so the caller can branch on it.
1557
- * - Throws `Error("Failed to read session.yaml", { cause })` for parse
1558
- * failures and schema violations (cause is either the YAML parser error
1559
- * or the zod error).
1149
+ * Schema for `.basou/manifest.yaml`. The minimal manifest carries
1150
+ * schema_version, basou_version, workspace metadata, project info, enabled
1151
+ * capabilities, approval policy, adapter config, and git policy. The
1152
+ * `adapters."claude-code"` key uses a hyphen; downstream code accesses it
1153
+ * via bracket notation.
1560
1154
  */
1561
- declare function readSessionYaml(paths: BasouPaths, sessionId: string): Promise<Session>;
1155
+ declare const ManifestSchema: z.ZodObject<{
1156
+ schema_version: z.ZodLiteral<"0.1.0">;
1157
+ basou_version: z.ZodLiteral<"0.1.0">;
1158
+ workspace: z.ZodObject<{
1159
+ id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
1160
+ name: z.ZodString;
1161
+ created_at: z.ZodString;
1162
+ updated_at: z.ZodString;
1163
+ }, z.core.$strip>;
1164
+ project: z.ZodObject<{
1165
+ name: z.ZodOptional<z.ZodString>;
1166
+ description: z.ZodOptional<z.ZodString>;
1167
+ repository_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1168
+ }, z.core.$strip>;
1169
+ capabilities: z.ZodObject<{
1170
+ enabled: z.ZodArray<z.ZodString>;
1171
+ }, z.core.$strip>;
1172
+ approval: z.ZodObject<{
1173
+ required_for: z.ZodOptional<z.ZodArray<z.ZodString>>;
1174
+ default_risk_level: z.ZodEnum<{
1175
+ low: "low";
1176
+ medium: "medium";
1177
+ high: "high";
1178
+ critical: "critical";
1179
+ }>;
1180
+ }, z.core.$strip>;
1181
+ adapters: z.ZodObject<{
1182
+ "claude-code": z.ZodObject<{
1183
+ enabled: z.ZodBoolean;
1184
+ config_path: z.ZodOptional<z.ZodString>;
1185
+ }, z.core.$strip>;
1186
+ }, z.core.$strip>;
1187
+ git: z.ZodObject<{
1188
+ events_log: z.ZodDefault<z.ZodEnum<{
1189
+ ignore: "ignore";
1190
+ commit: "commit";
1191
+ }>>;
1192
+ }, z.core.$strip>;
1193
+ }, z.core.$strip>;
1194
+ /** Inferred runtime type for {@link ManifestSchema}. */
1195
+ type Manifest = z.infer<typeof ManifestSchema>;
1196
+
1562
1197
  /**
1563
- * Classify a `running` session as suspect using one of two rules:
1198
+ * Task lifecycle states.
1564
1199
  *
1565
- * - Rule A (`events_say_ended_but_yaml_running`): events.jsonl contains a
1566
- * `session_ended` event but the session.yaml is still `running`. The
1567
- * session ended cleanly in the event log but the YAML write was lost or
1568
- * never reached.
1569
- * - Rule B (`running_no_end_event`): no `session_ended` event and the last
1570
- * event is older than {@link STUCK_THRESHOLD_MS}. The process likely
1571
- * crashed or was killed.
1200
+ * The storage layer's `ALLOWED_TRANSITIONS` map (= source of truth in
1201
+ * `tasks.ts`) is the authoritative graph; the comment below is a snapshot.
1202
+ * `planned` reaches `done` / `cancelled` directly so tasks completed (or
1203
+ * abandoned) outside an explicit in-progress phase can close in a single
1204
+ * CLI call:
1572
1205
  *
1573
- * Sessions that are not `running` are never suspect.
1206
+ * planned {in_progress | done | cancelled}
1207
+ * in_progress → {done | cancelled}
1208
+ * done / cancelled = terminal
1574
1209
  *
1575
- * I/O failure on events.jsonl is re-thrown unwrapped so the caller can
1576
- * degrade with a warning instead of treating the session as healthy. The
1577
- * caller is also responsible for surfacing replay warnings via `onWarning`.
1210
+ * Self-edges are rejected so the audit trail stays monotonic.
1578
1211
  */
1579
- declare function classifySuspect(paths: BasouPaths, sessionId: string, session: Session, now: Date, onWarning?: (warning: ReplayWarning) => void): Promise<{
1580
- suspect: boolean;
1581
- suspectReason: SuspectReason | null;
1212
+ declare const TaskStatusSchema: z.ZodEnum<{
1213
+ planned: "planned";
1214
+ in_progress: "in_progress";
1215
+ done: "done";
1216
+ cancelled: "cancelled";
1582
1217
  }>;
1218
+ /** Inferred runtime type for {@link TaskStatusSchema}. */
1219
+ type TaskStatus = z.infer<typeof TaskStatusSchema>;
1583
1220
  /**
1584
- * High-level helper that enumerates session dirs, reads each `session.yaml`,
1585
- * and classifies suspect for `running` sessions in one pass.
1586
- *
1587
- * Per-session degradations are surfaced via `options.onSkip`:
1588
- * - `session_yaml_missing` (ENOENT) and `session_yaml_invalid` (parse or
1589
- * schema violation): the entry is omitted from the result.
1590
- * - `events_jsonl_unreadable`: the entry is still pushed with `suspect=false`
1591
- * so callers can render the session row plus a CLI-side warning.
1221
+ * Schema for the YAML front matter of `.basou/tasks/<task_id>.md`.
1592
1222
  *
1593
- * `options.now` is taken once and threaded into every {@link classifySuspect}
1594
- * call so age comparisons are consistent across sessions.
1223
+ * The markdown body after the front matter is intentionally NOT modelled
1224
+ * here it is free-form user-edited content. The storage layer splits
1225
+ * the file into `task` (this schema) and `body` (the trailing string).
1595
1226
  */
1596
- declare function loadSessionEntries(paths: BasouPaths, options: LoadSessionEntriesOptions): Promise<SessionEntry[]>;
1597
-
1598
- /** Marker line that begins the auto-generated region. */
1599
- declare const GENERATED_START = "<!-- BASOU:GENERATED:START -->";
1600
- /** Marker line that ends the auto-generated region. */
1601
- declare const GENERATED_END = "<!-- BASOU:GENERATED:END -->";
1602
- /**
1603
- * Result of parsing a markdown body for the BASOU:GENERATED marker region.
1604
- *
1605
- * The spec mandates strict line-level matching (see
1606
- * `docs/spec/generated-markdown.md#102-marker-convention`): a marker is
1607
- * only recognized when an entire line is exactly the marker string.
1608
- * Leading/trailing whitespace, comment compression, and BOM are treated as
1609
- * legacy formats (`no_markers`) so that re-generation refuses to silently
1610
- * overwrite a mismatched manual edit.
1611
- */
1612
- type MarkerSection = {
1613
- kind: "ok";
1614
- before: string;
1615
- generated: string;
1616
- after: string;
1617
- } | {
1618
- kind: "no_markers";
1619
- } | {
1620
- kind: "missing_start";
1621
- } | {
1622
- kind: "missing_end";
1623
- } | {
1624
- kind: "multiple_pairs";
1625
- } | {
1626
- kind: "wrong_order";
1627
- };
1628
- /**
1629
- * Read a markdown file as UTF-8 text. Returns `null` when the file does not
1630
- * exist; throws `Error("Failed to read markdown file", { cause })` for other
1631
- * I/O failures (pathless contract — never embed the absolute path in the
1632
- * thrown `message`).
1633
- */
1634
- declare function readMarkdownFile(filePath: string): Promise<string | null>;
1635
- /**
1636
- * Atomically write a markdown body via {@link atomicReplace}. The shared
1637
- * helper handles the tmp-file + rename sequence, `wx` collision guard, and
1638
- * best-effort tmp cleanup on failure.
1639
- *
1640
- * On any failure the original error is re-thrown as
1641
- * `Error("Failed to write markdown file", { cause })` (pathless contract).
1642
- */
1643
- declare function writeMarkdownFile(filePath: string, body: string): Promise<void>;
1644
- /**
1645
- * Parse a markdown body and identify the BASOU:GENERATED marker region.
1646
- *
1647
- * Returns one of six `kind` discriminants:
1648
- * - `ok`: exactly one START line followed by exactly one END line in the
1649
- * correct order. `before` / `generated` / `after` slice the original
1650
- * text by character offsets so CRLF / LF are preserved verbatim outside
1651
- * the marker region.
1652
- * - `no_markers`: both START and END absent (legacy file / fresh write).
1653
- * - `missing_start` / `missing_end`: exactly one of the pair is present.
1654
- * - `multiple_pairs`: more than one START or END line.
1655
- * - `wrong_order`: END appears before START.
1656
- *
1657
- * Matching is strict: leading/trailing whitespace, BOM, and comment
1658
- * compression (`<!--BASOU:...-->`) all bypass the marker and are treated
1659
- * as legacy content.
1660
- */
1661
- declare function parseMarkers(content: string): MarkerSection;
1662
- /**
1663
- * Build the final markdown body by replacing the BASOU:GENERATED region.
1664
- *
1665
- * - `existing === null` (no file yet): return `<START>\n<generated>\n<END>\n`.
1666
- * - existing parses to `ok`: replace the marked region and keep everything
1667
- * before START and after END untouched (preserving manual additions).
1668
- * - any other parse result: throw a pathless error referencing `fileLabel`.
1669
- *
1670
- * The caller passes `fileLabel` (e.g. `"handoff.md"` or `"decisions.md"`)
1671
- * so the error message is informative without leaking an absolute path.
1672
- */
1673
- declare function renderWithMarkers(existing: string | null, generated: string, fileLabel: string): string;
1674
-
1675
- /**
1676
- * Options for {@link importSessionFromJson}. All fields are optional.
1677
- *
1678
- * - `labelOverride` / `taskIdOverride` come from the CLI `--label` / `--task`
1679
- * flags and win over the corresponding fields on the input payload.
1680
- * - `dryRun` skips disk writes entirely and returns a preview result.
1681
- */
1682
- type ImportSessionOptions = {
1683
- labelOverride?: string;
1684
- taskIdOverride?: string;
1685
- dryRun?: boolean;
1686
- };
1687
- /**
1688
- * Result of a successful import. `finalStatus` is always the literal
1689
- * `"imported"` (per the import-session lifecycle policy); `finalSourceKind`
1690
- * mirrors the input's `session.source.kind` so round-trip imports preserve
1691
- * provenance.
1692
- *
1693
- * `pathSanitizeReport` summarises how many path-shaped fields the importer
1694
- * rewrote on the way in: `related_files[]` entries plus a single boolean
1695
- * for `working_directory`. The CLI wrapper surfaces this as a one-line
1696
- * stderr warning when the total is non-zero so the operator sees that
1697
- * machine-private prefixes were stripped.
1698
- */
1699
- type ImportSessionResult = {
1700
- sessionId: PrefixedId<"ses">;
1701
- eventCount: number;
1702
- finalStatus: SessionStatus;
1703
- finalSourceKind: SessionSourceKind;
1704
- pathSanitizeReport: {
1705
- relatedFiles: number;
1706
- workingDirectoryRewritten: boolean;
1707
- };
1708
- };
1709
- /**
1710
- * Import a round-trip JSON payload into `.basou/sessions/<new>/`. The caller
1711
- * MUST validate the payload against {@link SessionImportPayloadSchema} first
1712
- * and gate the `schema_version === "0.1.0"` literal check externally; this
1713
- * function trusts both invariants.
1714
- *
1715
- * On success a fresh session ID is minted and a complete
1716
- * `session.yaml` + `events.jsonl` pair is written atomically. On any post-
1717
- * mkdir failure the session directory is removed best-effort so partial
1718
- * imports do not leave `session_yaml_missing` half-states behind.
1719
- *
1720
- * Throws `Error` with one of the fixed messages enumerated by the import contract
1721
- * §"Error messages" table; the original native error is attached as `cause`
1722
- * for `--verbose` rendering.
1723
- */
1724
- declare function importSessionFromJson(paths: BasouPaths, manifest: Manifest, payload: SessionImportPayload, options: ImportSessionOptions): Promise<ImportSessionResult>;
1227
+ declare const TaskSchema: z.ZodObject<{
1228
+ schema_version: z.ZodLiteral<"0.1.0">;
1229
+ task: z.ZodObject<{
1230
+ id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1231
+ title: z.ZodString;
1232
+ label: z.ZodOptional<z.ZodString>;
1233
+ status: z.ZodEnum<{
1234
+ planned: "planned";
1235
+ in_progress: "in_progress";
1236
+ done: "done";
1237
+ cancelled: "cancelled";
1238
+ }>;
1239
+ created_at: z.ZodString;
1240
+ updated_at: z.ZodString;
1241
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
1242
+ created_in_session: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1243
+ linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1244
+ }, z.core.$strip>;
1245
+ }, z.core.$strip>;
1246
+ /** Inferred runtime type for {@link TaskSchema}. */
1247
+ type Task = z.infer<typeof TaskSchema>;
1725
1248
 
1726
1249
  /**
1727
1250
  * Thrown when the ad-hoc session was fully written to disk (4 lifecycle
@@ -2381,10 +1904,88 @@ type ArchiveTaskResult = {
2381
1904
  */
2382
1905
  declare function archiveTask(input: ArchiveTaskInput): Promise<ArchiveTaskResult>;
2383
1906
 
1907
+ /** Input contract for {@link renderHandoff}. */
1908
+ type HandoffRendererInput = {
1909
+ paths: BasouPaths;
1910
+ /** ISO timestamp embedded in the generated body header. Caller-provided for testability. */
1911
+ nowIso: string;
1912
+ /** Forwarded to {@link replayEvents} / {@link loadSessionEntries} per session. */
1913
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
1914
+ /**
1915
+ * Per-session degradation reasons (missing/invalid session.yaml or
1916
+ * unreadable events.jsonl). The CLI maps `events_jsonl_unreadable` to the
1917
+ * existing suspect-check stderr wording to keep the user-facing surface
1918
+ * consistent with `basou session list`.
1919
+ */
1920
+ onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
1921
+ /**
1922
+ * Per-task degradation reasons (invalid front matter / unreadable file).
1923
+ * Surfaced so the CLI can warn the operator about a malformed task.md
1924
+ * without aborting the handoff render.
1925
+ */
1926
+ onTaskSkip?: (taskId: string, reason: TaskSkipReason) => void;
1927
+ /** Maximum related_files entries to display before `... +N more`. Default 20. */
1928
+ relatedFilesLimit?: number;
1929
+ };
1930
+ type HandoffRendererResult = {
1931
+ /** Generated body WITHOUT BASOU:GENERATED markers (markdown-store wraps them). */
1932
+ body: string;
1933
+ sessionCount: number;
1934
+ decisionCount: number;
1935
+ pendingApprovalsCount: number;
1936
+ suspectCount: number;
1937
+ /** Total number of task.md files successfully loaded. */
1938
+ taskCount: number;
1939
+ /** Tasks whose status is `planned` or `in_progress` (= shown in 次に実行すべき作業). */
1940
+ pendingTaskCount: number;
1941
+ };
1942
+ /**
1943
+ * Render the body of `handoff.md` from the current workspace state.
1944
+ *
1945
+ * The renderer is a pure function (no I/O beyond {@link replayEvents} /
1946
+ * {@link loadSessionEntries} / {@link enumerateApprovals}). It assembles the
1947
+ * the spec's `handoff.md` sections in order:
1948
+ *
1949
+ * 1. `現在の状態`: latest live session (status not archived, source not import).
1950
+ * 2. `直近の変更ファイル`: union of `related_files` across sessions, dedup +
1951
+ * sorted asc + truncated to `relatedFilesLimit` (default 20).
1952
+ * 3. `直近の判断`: latest `decision_recorded` event (chronological).
1953
+ * 4. `未決事項`: pending-approval count + suspect-session count.
1954
+ * 5. `次に読むべきファイル`: `.basou/decisions.md` + top-3 related files
1955
+ * (the same `displayedFiles` source is intentionally reused in two
1956
+ * sections — overview vs. resume context).
1957
+ * 6. `次に実行すべき作業`: placeholder until task events land.
1958
+ * 7. `セッション一覧`: all sessions newest first with inline suspect labels.
1959
+ *
1960
+ * Session enumeration goes through {@link loadSessionEntries} so the set of
1961
+ * sessions whose `decision_recorded` events we replay matches the
1962
+ * decisions renderer.
1963
+ */
1964
+ declare function renderHandoff(input: HandoffRendererInput): Promise<HandoffRendererResult>;
1965
+
1966
+ /**
1967
+ * Parse a unit-suffixed duration string (e.g. `30s`, `5m`, `1h`, `100ms`)
1968
+ * into milliseconds.
1969
+ *
1970
+ * Rejects formats that cannot represent a positive, finite millisecond
1971
+ * value: malformed inputs, zero, leading-zero values, and computations that
1972
+ * overflow to `Infinity`. The returned number is always a positive integer.
1973
+ *
1974
+ * Supported units: `ms` (milliseconds), `s` (seconds), `m` (minutes),
1975
+ * `h` (hours).
1976
+ *
1977
+ * @param input duration string with required unit suffix
1978
+ * @returns duration in milliseconds (positive, finite)
1979
+ * @throws Error with message
1980
+ * `Invalid duration: <input>. Expected format: <positive-integer><unit> where unit is ms/s/m/h`
1981
+ * for format errors, or `Duration overflow: <input>` for non-finite results.
1982
+ */
1983
+ declare function parseDuration(input: string): number;
1984
+
2384
1985
  /**
2385
1986
  * Resolve a possibly-truncated session id prefix to a full session id by
2386
1987
  * scanning `<paths.sessions>/`. Existing message contract (carried over
2387
- * from `packages/cli/src/commands/session.ts` 経由の Step 12 実装) is
1988
+ * from `packages/cli/src/commands/session.ts`) is
2388
1989
  * preserved exactly so callers that grep stderr keep working:
2389
1990
  *
2390
1991
  * - `"Session id is empty"`
@@ -2492,413 +2093,813 @@ type SanitizeRelatedFilesResult = {
2492
2093
  */
2493
2094
  declare function sanitizeRelatedFiles(paths: ReadonlyArray<string>, opts: SanitizePathOptions): SanitizeRelatedFilesResult;
2494
2095
 
2495
- /** Input contract for {@link renderHandoff}. */
2496
- type HandoffRendererInput = {
2497
- paths: BasouPaths;
2498
- /** ISO timestamp embedded in the generated body header. Caller-provided for testability. */
2499
- nowIso: string;
2500
- /** Forwarded to {@link replayEvents} / {@link loadSessionEntries} per session. */
2501
- onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2502
- /**
2503
- * Per-session degradation reasons (missing/invalid session.yaml or
2504
- * unreadable events.jsonl). The CLI maps `events_jsonl_unreadable` to the
2505
- * existing suspect-check stderr wording to keep the user-facing surface
2506
- * consistent with `basou session list`.
2507
- */
2508
- onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2509
- /**
2510
- * Per-task degradation reasons (invalid front matter / unreadable file).
2511
- * Surfaced so the CLI can warn the operator about a malformed task.md
2512
- * without aborting the handoff render.
2513
- */
2514
- onTaskSkip?: (taskId: string, reason: TaskSkipReason) => void;
2515
- /** Maximum related_files entries to display before `... +N more`. Default 20. */
2516
- relatedFilesLimit?: number;
2517
- };
2518
- type HandoffRendererResult = {
2519
- /** Generated body WITHOUT BASOU:GENERATED markers (markdown-store wraps them). */
2520
- body: string;
2521
- sessionCount: number;
2522
- decisionCount: number;
2523
- pendingApprovalsCount: number;
2524
- suspectCount: number;
2525
- /** Total number of task.md files successfully loaded. */
2526
- taskCount: number;
2527
- /** Tasks whose status is `planned` or `in_progress` (= shown in 次に実行すべき作業). */
2528
- pendingTaskCount: number;
2529
- };
2530
2096
  /**
2531
- * Render the body of `handoff.md` from the current workspace state.
2097
+ * Internal abstraction over child-process execution.
2532
2098
  *
2533
- * The renderer is a pure function (no I/O beyond {@link replayEvents} /
2534
- * {@link loadSessionEntries} / {@link enumerateApprovals}). It assembles the
2535
- * the spec's `handoff.md` sections in order:
2099
+ * The v0.1 implementation is intentionally minimal:
2100
+ * - Optional UTF-8 stdout/stderr capture (`capture: "buffer"`, default) or
2101
+ * pass-through to the parent's stdio (`capture: "none"`).
2102
+ * - No stream callbacks for partial chunks.
2103
+ * - No event emission. Callers wire any event flow separately.
2536
2104
  *
2537
- * 1. `現在の状態`: latest live session (status not archived, source not import).
2538
- * 2. `直近の変更ファイル`: union of `related_files` across sessions, dedup +
2539
- * sorted asc + truncated to `relatedFilesLimit` (default 20).
2540
- * 3. `直近の判断`: latest `decision_recorded` event (chronological).
2541
- * 4. `未決事項`: pending-approval count + suspect-session count.
2542
- * 5. `次に読むべきファイル`: `.basou/decisions.md` + top-3 related files
2543
- * (the same `displayedFiles` source is intentionally reused in two
2544
- * sections — overview vs. resume context).
2545
- * 6. `次に実行すべき作業`: placeholder until task events land.
2546
- * 7. `セッション一覧`: all sessions newest first with inline suspect labels.
2105
+ * The boundary is internal: ProcessRunner is not part of the public
2106
+ * adapter surface. Adapters do not import or instantiate it directly;
2107
+ * CLI / Core orchestration owns construction and invocation.
2108
+ */
2109
+ /**
2110
+ * Output capture mode.
2547
2111
  *
2548
- * Session enumeration goes through {@link loadSessionEntries} so the set of
2549
- * sessions whose `decision_recorded` events we replay matches the
2550
- * decisions renderer.
2112
+ * - `"buffer"` (default): pipe stdout/stderr to the runner and accumulate
2113
+ * the full UTF-8 string into {@link RunResult}.
2114
+ * - `"none"`: inherit the parent's stdio. The child writes directly to the
2115
+ * parent terminal in real time and {@link RunResult.stdout} /
2116
+ * {@link RunResult.stderr} are empty strings. `stdin` cannot be combined
2117
+ * with `"none"` because the child has no writable stdin pipe.
2551
2118
  */
2552
- declare function renderHandoff(input: HandoffRendererInput): Promise<HandoffRendererResult>;
2553
-
2554
- type DecisionsRendererInput = {
2555
- paths: BasouPaths;
2556
- nowIso: string;
2557
- onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2558
- onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2119
+ type CaptureMode = "buffer" | "none";
2120
+ type RunOptions = {
2121
+ /**
2122
+ * Working directory for the child process. Required: callers resolve
2123
+ * the workspace root themselves; the runner does not validate cwd
2124
+ * existence and surfaces native spawn errors via classification.
2125
+ */
2126
+ readonly cwd: string;
2127
+ /**
2128
+ * Environment variables for the child. When omitted, the parent's
2129
+ * `process.env` is inherited verbatim. Callers wanting a sanitized
2130
+ * environment must build it explicitly.
2131
+ */
2132
+ readonly env?: NodeJS.ProcessEnv;
2133
+ /**
2134
+ * External cancellation. Aborting the signal triggers a two-stage
2135
+ * kill (SIGTERM, then SIGKILL after a short grace period).
2136
+ */
2137
+ readonly signal?: AbortSignal;
2138
+ /**
2139
+ * Internal timeout in milliseconds. Must be a positive finite number.
2140
+ * Triggers the same two-stage kill as `signal`.
2141
+ */
2142
+ readonly timeout_ms?: number;
2143
+ /**
2144
+ * Optional input written to the child's stdin. The pipe is closed
2145
+ * after the value is written. Incompatible with `capture: "none"`.
2146
+ */
2147
+ readonly stdin?: string | Buffer;
2148
+ /**
2149
+ * Output capture mode. Defaults to `"buffer"`. See {@link CaptureMode}.
2150
+ */
2151
+ readonly capture?: CaptureMode;
2152
+ /**
2153
+ * Invoked synchronously immediately after the child has been spawned,
2154
+ * before the runner waits for completion. Callers use this to retain a
2155
+ * reference for parent-side cleanup (e.g. an `exit` hook that SIGKILLs
2156
+ * the child if the parent is forcibly terminated). The runner takes no
2157
+ * action if the callback throws.
2158
+ */
2159
+ readonly onSpawn?: (child: ChildProcess) => void;
2559
2160
  };
2560
- type DecisionsRendererResult = {
2561
- /** Generated body WITHOUT BASOU:GENERATED markers. */
2562
- body: string;
2563
- decisionCount: number;
2161
+ type RunResult = {
2162
+ readonly command: string;
2163
+ readonly args: readonly string[];
2164
+ readonly cwd: string;
2165
+ /** `null` when the process was killed by a signal. */
2166
+ readonly exit_code: number | null;
2167
+ readonly signal: NodeJS.Signals | null;
2168
+ readonly stdout: string;
2169
+ readonly stderr: string;
2170
+ /** ISO 8601 timestamp captured before spawn. */
2171
+ readonly started_at: string;
2172
+ /** ISO 8601 timestamp captured on the `close` event. */
2173
+ readonly ended_at: string;
2174
+ readonly duration_ms: number;
2175
+ readonly pid: number | null;
2176
+ };
2177
+ type ProcessRunner = {
2178
+ run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2564
2179
  };
2180
+
2565
2181
  /**
2566
- * Render the body of `decisions.md` from `decision_recorded` events across
2567
- * every healthy session in the workspace.
2568
- *
2569
- * Session enumeration goes through {@link loadSessionEntries} (the same path
2570
- * the handoff renderer uses) so that `session.yaml`-broken sessions are
2571
- * skipped in BOTH outputs and the handoff's `decisionCount` summary stays
2572
- * consistent with the number of sections rendered here.
2182
+ * Spawn-based ProcessRunner implementation.
2573
2183
  *
2574
- * Order: `occurred_at` ascending with `decisionId` (= ULID) as tie-breaker.
2575
- * Both fields are monotonic, so the result is a stable cross-session
2576
- * timeline.
2184
+ * Behavior:
2185
+ * - `shell: false` and `detached: false`. The process group is not
2186
+ * detached, but the OS does not guarantee the child is reaped when
2187
+ * the parent terminates abruptly; callers handle SIGINT/SIGTERM/exit
2188
+ * hooks themselves.
2189
+ * - `capture: "buffer"` (default): `stdio: ['pipe', 'pipe', 'pipe']`,
2190
+ * stdout / stderr are decoded as UTF-8 and accumulated as full
2191
+ * strings (no streaming callbacks).
2192
+ * - `capture: "none"`: `stdio: ['inherit', 'inherit', 'inherit']`, the
2193
+ * child writes directly to the parent terminal in real time and
2194
+ * `RunResult.stdout` / `stderr` are empty strings. `stdin` is
2195
+ * incompatible with this mode (the child has no writable stdin pipe)
2196
+ * and the combination is rejected before spawn.
2197
+ * - `timeout_ms` and `AbortSignal` both trigger a two-stage kill:
2198
+ * `SIGTERM`, then `SIGKILL` after `DEFAULT_KILL_GRACE_MS` (5_000 ms).
2199
+ * - A non-zero `exit_code` does not throw; it is returned via
2200
+ * `RunResult`. Spawn-time errors throw with a pathless message and
2201
+ * the original error attached as `cause`.
2577
2202
  *
2578
- * The decision rich fields (rationale / alternatives / rejected_reason /
2579
- * linked_events / linked_files) are rendered when the event carries them.
2580
- * `linked_events` and `linked_files` are OPAQUE references: the schema only
2581
- * validates the SHAPE, not existence — references that cannot be resolved
2582
- * to a known event id or an existing file on disk are surfaced inline as
2583
- * `(missing)` so cross-workspace round-trips never reject parse-time.
2203
+ * Error message contract: messages never include `cwd` or absolute
2204
+ * command paths. The original errno (and any nested wrapping) is
2205
+ * preserved on `Error.cause`, allowing callers to classify with
2206
+ * `findErrorCode` when needed.
2584
2207
  */
2585
- declare function renderDecisions(input: DecisionsRendererInput): Promise<DecisionsRendererResult>;
2208
+ declare class ChildProcessRunner implements ProcessRunner {
2209
+ run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2210
+ }
2211
+
2212
+ declare const SessionInnerImportSchema: z.ZodObject<{
2213
+ id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
2214
+ label: z.ZodOptional<z.ZodString>;
2215
+ task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
2216
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2217
+ source: z.ZodObject<{
2218
+ kind: z.ZodEnum<{
2219
+ "claude-code-adapter": "claude-code-adapter";
2220
+ human: "human";
2221
+ import: "import";
2222
+ terminal: "terminal";
2223
+ }>;
2224
+ version: z.ZodLiteral<"0.1.0">;
2225
+ }, z.core.$strip>;
2226
+ started_at: z.ZodString;
2227
+ ended_at: z.ZodOptional<z.ZodString>;
2228
+ status: z.ZodEnum<{
2229
+ initialized: "initialized";
2230
+ running: "running";
2231
+ waiting_approval: "waiting_approval";
2232
+ completed: "completed";
2233
+ failed: "failed";
2234
+ interrupted: "interrupted";
2235
+ imported: "imported";
2236
+ archived: "archived";
2237
+ }>;
2238
+ working_directory: z.ZodString;
2239
+ invocation: z.ZodObject<{
2240
+ command: z.ZodString;
2241
+ args: z.ZodArray<z.ZodString>;
2242
+ exit_code: z.ZodNullable<z.ZodNumber>;
2243
+ }, z.core.$strip>;
2244
+ related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
2245
+ events_log: z.ZodOptional<z.ZodString>;
2246
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2247
+ }, z.core.$strict>;
2248
+ /**
2249
+ * Schema for the round-trip JSON payload accepted by `basou session import
2250
+ * --format json`. The top level is `.strict()`; unknown keys at the outer
2251
+ * envelope are rejected.
2252
+ */
2253
+ declare const SessionImportPayloadSchema: z.ZodObject<{
2254
+ schema_version: z.ZodString;
2255
+ session: z.ZodObject<{
2256
+ id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
2257
+ label: z.ZodOptional<z.ZodString>;
2258
+ task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
2259
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2260
+ source: z.ZodObject<{
2261
+ kind: z.ZodEnum<{
2262
+ "claude-code-adapter": "claude-code-adapter";
2263
+ human: "human";
2264
+ import: "import";
2265
+ terminal: "terminal";
2266
+ }>;
2267
+ version: z.ZodLiteral<"0.1.0">;
2268
+ }, z.core.$strip>;
2269
+ started_at: z.ZodString;
2270
+ ended_at: z.ZodOptional<z.ZodString>;
2271
+ status: z.ZodEnum<{
2272
+ initialized: "initialized";
2273
+ running: "running";
2274
+ waiting_approval: "waiting_approval";
2275
+ completed: "completed";
2276
+ failed: "failed";
2277
+ interrupted: "interrupted";
2278
+ imported: "imported";
2279
+ archived: "archived";
2280
+ }>;
2281
+ working_directory: z.ZodString;
2282
+ invocation: z.ZodObject<{
2283
+ command: z.ZodString;
2284
+ args: z.ZodArray<z.ZodString>;
2285
+ exit_code: z.ZodNullable<z.ZodNumber>;
2286
+ }, z.core.$strip>;
2287
+ related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
2288
+ events_log: z.ZodOptional<z.ZodString>;
2289
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2290
+ }, z.core.$strict>;
2291
+ events: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
2292
+ schema_version: z.ZodLiteral<"0.1.0">;
2293
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2294
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2295
+ occurred_at: z.ZodString;
2296
+ source: z.ZodString;
2297
+ type: z.ZodLiteral<"session_started">;
2298
+ }, z.core.$strip>, z.ZodObject<{
2299
+ schema_version: z.ZodLiteral<"0.1.0">;
2300
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2301
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2302
+ occurred_at: z.ZodString;
2303
+ source: z.ZodString;
2304
+ type: z.ZodLiteral<"session_ended">;
2305
+ exit_code: z.ZodOptional<z.ZodNumber>;
2306
+ }, z.core.$strip>, z.ZodObject<{
2307
+ schema_version: z.ZodLiteral<"0.1.0">;
2308
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2309
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2310
+ occurred_at: z.ZodString;
2311
+ source: z.ZodString;
2312
+ type: z.ZodLiteral<"session_status_changed">;
2313
+ from: z.ZodString;
2314
+ to: z.ZodString;
2315
+ }, z.core.$strip>, z.ZodObject<{
2316
+ schema_version: z.ZodLiteral<"0.1.0">;
2317
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2318
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2319
+ occurred_at: z.ZodString;
2320
+ source: z.ZodString;
2321
+ type: z.ZodLiteral<"approval_requested">;
2322
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
2323
+ expires_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
2324
+ risk_level: z.ZodEnum<{
2325
+ low: "low";
2326
+ medium: "medium";
2327
+ high: "high";
2328
+ critical: "critical";
2329
+ }>;
2330
+ action: z.ZodObject<{
2331
+ kind: z.ZodString;
2332
+ }, z.core.$loose>;
2333
+ reason: z.ZodString;
2334
+ status: z.ZodLiteral<"pending">;
2335
+ }, z.core.$strip>, z.ZodObject<{
2336
+ schema_version: z.ZodLiteral<"0.1.0">;
2337
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2338
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2339
+ occurred_at: z.ZodString;
2340
+ source: z.ZodString;
2341
+ type: z.ZodLiteral<"approval_approved">;
2342
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
2343
+ resolver: z.ZodOptional<z.ZodString>;
2344
+ note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2345
+ }, z.core.$strip>, z.ZodObject<{
2346
+ schema_version: z.ZodLiteral<"0.1.0">;
2347
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2348
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2349
+ occurred_at: z.ZodString;
2350
+ source: z.ZodString;
2351
+ type: z.ZodLiteral<"approval_rejected">;
2352
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
2353
+ resolver: z.ZodOptional<z.ZodString>;
2354
+ reason: z.ZodString;
2355
+ }, z.core.$strip>, z.ZodObject<{
2356
+ schema_version: z.ZodLiteral<"0.1.0">;
2357
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2358
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2359
+ occurred_at: z.ZodString;
2360
+ source: z.ZodString;
2361
+ type: z.ZodLiteral<"approval_expired">;
2362
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
2363
+ }, z.core.$strip>, z.ZodObject<{
2364
+ schema_version: z.ZodLiteral<"0.1.0">;
2365
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2366
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2367
+ occurred_at: z.ZodString;
2368
+ source: z.ZodString;
2369
+ type: z.ZodLiteral<"command_executed">;
2370
+ command: z.ZodString;
2371
+ args: z.ZodArray<z.ZodString>;
2372
+ cwd: z.ZodString;
2373
+ exit_code: z.ZodNullable<z.ZodNumber>;
2374
+ signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2375
+ received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2376
+ duration_ms: z.ZodNumber;
2377
+ }, z.core.$strip>, z.ZodObject<{
2378
+ schema_version: z.ZodLiteral<"0.1.0">;
2379
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2380
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2381
+ occurred_at: z.ZodString;
2382
+ source: z.ZodString;
2383
+ type: z.ZodLiteral<"git_snapshot">;
2384
+ head: z.ZodString;
2385
+ branch: z.ZodString;
2386
+ dirty: z.ZodBoolean;
2387
+ staged: z.ZodArray<z.ZodString>;
2388
+ unstaged: z.ZodArray<z.ZodString>;
2389
+ untracked: z.ZodArray<z.ZodString>;
2390
+ ahead: z.ZodOptional<z.ZodNumber>;
2391
+ behind: z.ZodOptional<z.ZodNumber>;
2392
+ }, z.core.$strip>, z.ZodObject<{
2393
+ schema_version: z.ZodLiteral<"0.1.0">;
2394
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2395
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2396
+ occurred_at: z.ZodString;
2397
+ source: z.ZodString;
2398
+ type: z.ZodLiteral<"file_changed">;
2399
+ path: z.ZodString;
2400
+ change_type: z.ZodEnum<{
2401
+ added: "added";
2402
+ modified: "modified";
2403
+ deleted: "deleted";
2404
+ renamed: "renamed";
2405
+ }>;
2406
+ old_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2407
+ }, z.core.$strip>, z.ZodObject<{
2408
+ schema_version: z.ZodLiteral<"0.1.0">;
2409
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2410
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2411
+ occurred_at: z.ZodString;
2412
+ source: z.ZodString;
2413
+ type: z.ZodLiteral<"decision_recorded">;
2414
+ decision_id: z.ZodString & z.ZodType<`decision_${string}`, string, z.core.$ZodTypeInternals<`decision_${string}`, string>>;
2415
+ title: z.ZodString;
2416
+ rationale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2417
+ alternatives: z.ZodOptional<z.ZodArray<z.ZodString>>;
2418
+ rejected_reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2419
+ linked_events: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>>>;
2420
+ linked_files: z.ZodOptional<z.ZodArray<z.ZodString>>;
2421
+ }, z.core.$strip>, z.ZodObject<{
2422
+ schema_version: z.ZodLiteral<"0.1.0">;
2423
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2424
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2425
+ occurred_at: z.ZodString;
2426
+ source: z.ZodString;
2427
+ type: z.ZodLiteral<"task_created">;
2428
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2429
+ title: z.ZodString;
2430
+ }, z.core.$strip>, z.ZodObject<{
2431
+ schema_version: z.ZodLiteral<"0.1.0">;
2432
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2433
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2434
+ occurred_at: z.ZodString;
2435
+ source: z.ZodString;
2436
+ type: z.ZodLiteral<"task_status_changed">;
2437
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2438
+ from: z.ZodString;
2439
+ to: z.ZodString;
2440
+ }, z.core.$strip>, z.ZodObject<{
2441
+ schema_version: z.ZodLiteral<"0.1.0">;
2442
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2443
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2444
+ occurred_at: z.ZodString;
2445
+ source: z.ZodString;
2446
+ type: z.ZodLiteral<"task_reconciled">;
2447
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2448
+ removed_created_in_session: z.ZodDefault<z.ZodNullable<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
2449
+ created_in_session_replacement: z.ZodDefault<z.ZodNullable<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
2450
+ removed_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
2451
+ }, z.core.$strict>, z.ZodObject<{
2452
+ schema_version: z.ZodLiteral<"0.1.0">;
2453
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2454
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2455
+ occurred_at: z.ZodString;
2456
+ source: z.ZodString;
2457
+ type: z.ZodLiteral<"task_linkage_refreshed">;
2458
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2459
+ added_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
2460
+ removed_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
2461
+ final_count: z.ZodOptional<z.ZodNumber>;
2462
+ }, z.core.$strict>, z.ZodObject<{
2463
+ schema_version: z.ZodLiteral<"0.1.0">;
2464
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2465
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2466
+ occurred_at: z.ZodString;
2467
+ source: z.ZodString;
2468
+ type: z.ZodLiteral<"task_deleted">;
2469
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2470
+ title: z.ZodString;
2471
+ }, z.core.$strict>, z.ZodObject<{
2472
+ schema_version: z.ZodLiteral<"0.1.0">;
2473
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2474
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2475
+ occurred_at: z.ZodString;
2476
+ source: z.ZodString;
2477
+ type: z.ZodLiteral<"task_archived">;
2478
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2479
+ title: z.ZodString;
2480
+ }, z.core.$strict>, z.ZodObject<{
2481
+ schema_version: z.ZodLiteral<"0.1.0">;
2482
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2483
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2484
+ occurred_at: z.ZodString;
2485
+ source: z.ZodString;
2486
+ type: z.ZodLiteral<"note_added">;
2487
+ body: z.ZodString;
2488
+ }, z.core.$strip>, z.ZodObject<{
2489
+ schema_version: z.ZodLiteral<"0.1.0">;
2490
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2491
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2492
+ occurred_at: z.ZodString;
2493
+ source: z.ZodString;
2494
+ type: z.ZodLiteral<"adapter_output">;
2495
+ stream: z.ZodEnum<{
2496
+ stdout: "stdout";
2497
+ stderr: "stderr";
2498
+ }>;
2499
+ summary: z.ZodString;
2500
+ raw_ref: z.ZodString;
2501
+ redacted: z.ZodOptional<z.ZodBoolean>;
2502
+ }, z.core.$strict>], "type">>;
2503
+ }, z.core.$strict>;
2504
+ /** Inferred runtime type for {@link SessionImportPayloadSchema}. */
2505
+ type SessionImportPayload = z.infer<typeof SessionImportPayloadSchema>;
2506
+ /** Inferred runtime type for {@link SessionInnerImportSchema}. */
2507
+ type SessionInnerImportInput = z.infer<typeof SessionInnerImportSchema>;
2508
+
2509
+ /**
2510
+ * Schema version literal pinned to "0.1.0" for Basou v0.1.
2511
+ * Reused across every entity schema so inferred types narrow to the literal.
2512
+ */
2513
+ declare const SchemaVersionSchema: z.ZodLiteral<"0.1.0">;
2514
+ /**
2515
+ * ISO 8601 timestamp with explicit timezone offset (e.g. `+09:00`).
2516
+ *
2517
+ * The spec samples include offsets, so the default zod `.datetime()` (which
2518
+ * rejects offsets) is insufficient; `{ offset: true }` is required.
2519
+ */
2520
+ declare const IsoTimestampSchema: z.ZodString;
2521
+ /** Workspace ID schema: validates `ws_<26-char ULID>`. */
2522
+ declare const WorkspaceIdSchema: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2523
+ /** Task ID schema: validates `task_<26-char ULID>`. */
2524
+ declare const TaskIdSchema: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2525
+ /** Session ID schema: validates `ses_<26-char ULID>`. */
2526
+ declare const SessionIdSchema: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2527
+ /** Event ID schema: validates `evt_<26-char ULID>`. */
2528
+ declare const EventIdSchema: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2529
+ /** Approval ID schema: validates `appr_<26-char ULID>`. */
2530
+ declare const ApprovalIdSchema: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
2531
+ /** Decision ID schema: validates `decision_<26-char ULID>`. */
2532
+ declare const DecisionIdSchema: z.ZodString & z.ZodType<`decision_${string}`, string, z.core.$ZodTypeInternals<`decision_${string}`, string>>;
2533
+ /**
2534
+ * Risk level vocabulary fixed by the spec. Adapters MUST emit one of these
2535
+ * four values; arbitrary strings are rejected at schema parse time.
2536
+ */
2537
+ declare const RiskLevelSchema: z.ZodEnum<{
2538
+ low: "low";
2539
+ medium: "medium";
2540
+ high: "high";
2541
+ critical: "critical";
2542
+ }>;
2543
+ /** Inferred runtime type for {@link RiskLevelSchema}. */
2544
+ type RiskLevel = z.infer<typeof RiskLevelSchema>;
2545
+ /**
2546
+ * Source attribution for events (e.g. "claude-code-adapter",
2547
+ * "git-capability", "terminal-recording", "local-cli", "human"). Free-form
2548
+ * non-empty string in v0.1; a stricter enum may be introduced post-v0.1.
2549
+ */
2550
+ declare const EventSourceSchema: z.ZodString;
2551
+
2552
+ /**
2553
+ * Schema for `.basou/status.json` — a forward-incompat cache of the current
2554
+ * workspace state.
2555
+ *
2556
+ * Each level uses `.strict()` so unknown keys are rejected rather than
2557
+ * silently stripped. A v0.1 reader that encounters a future-shape
2558
+ * `status.json` therefore fails parsing instead of returning a partially
2559
+ * empty snapshot; callers regenerate by calling `buildStatusSnapshot` +
2560
+ * `writeStatus` rather than trying to migrate.
2561
+ */
2562
+ declare const StatusSchema: z.ZodObject<{
2563
+ schema_version: z.ZodLiteral<"0.1.0">;
2564
+ generated_at: z.ZodString;
2565
+ workspace: z.ZodObject<{
2566
+ id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2567
+ name: z.ZodString;
2568
+ basou_version: z.ZodLiteral<"0.1.0">;
2569
+ }, z.core.$strict>;
2570
+ directories_present: z.ZodObject<{
2571
+ sessions: z.ZodBoolean;
2572
+ tasks: z.ZodBoolean;
2573
+ approvals_pending: z.ZodBoolean;
2574
+ approvals_resolved: z.ZodBoolean;
2575
+ logs: z.ZodBoolean;
2576
+ raw: z.ZodBoolean;
2577
+ tmp: z.ZodBoolean;
2578
+ }, z.core.$strict>;
2579
+ }, z.core.$strict>;
2580
+ /** Inferred runtime type for {@link StatusSchema}. */
2581
+ type StatusSnapshot = z.infer<typeof StatusSchema>;
2586
2582
 
2583
+ type AppendBasouGitignoreResult = {
2584
+ /** True if the block was appended (or the file was newly created). */
2585
+ readonly appended: boolean;
2586
+ };
2587
2587
  /**
2588
- * Internal abstraction over child-process execution.
2588
+ * Append Basou's default `.gitignore` block to `repositoryRoot/.gitignore`.
2589
2589
  *
2590
- * The v0.1 implementation is intentionally minimal:
2591
- * - Optional UTF-8 stdout/stderr capture (`capture: "buffer"`, default) or
2592
- * pass-through to the parent's stdio (`capture: "none"`).
2593
- * - No stream callbacks for partial chunks.
2594
- * - No event emission. Callers wire any event flow separately.
2590
+ * The block contents are derived from the Basou v0.1 specification (the
2591
+ * standard ignore + commit recommendations). Callers must pass an absolute
2592
+ * path to a Git repository root.
2595
2593
  *
2596
- * The boundary is internal: ProcessRunner is not part of the public
2597
- * adapter surface. Adapters do not import or instantiate it directly;
2598
- * CLI / Core orchestration owns construction and invocation.
2594
+ * Behavior:
2595
+ * - If `.gitignore` does not exist, it is created with the Basou block.
2596
+ * - If a line starting with `# Basou - default ignore` is already present,
2597
+ * the file is left untouched and `appended: false` is returned
2598
+ * (idempotent).
2599
+ * - If `.gitignore` is a symlink, the link is followed and the target file
2600
+ * is updated. Symlinks are not rejected.
2601
+ *
2602
+ * On I/O failure throws Error with a pathless message
2603
+ * (`Failed to read .gitignore` / `Failed to write .gitignore`) and the
2604
+ * original native error attached as `cause`.
2599
2605
  */
2606
+ declare function appendBasouGitignore(repositoryRoot: string): Promise<AppendBasouGitignoreResult>;
2607
+
2600
2608
  /**
2601
- * Output capture mode.
2602
- *
2603
- * - `"buffer"` (default): pipe stdout/stderr to the runner and accumulate
2604
- * the full UTF-8 string into {@link RunResult}.
2605
- * - `"none"`: inherit the parent's stdio. The child writes directly to the
2606
- * parent terminal in real time and {@link RunResult.stdout} /
2607
- * {@link RunResult.stderr} are empty strings. `stdin` cannot be combined
2608
- * with `"none"` because the child has no writable stdin pipe.
2609
+ * The two lock scopes basou uses. `task` guards the read-modify-write window
2610
+ * around a single `task.md`; `session` guards the events.jsonl append plus
2611
+ * surrounding `session.yaml` mutation for a single session. Two scopes use
2612
+ * different lockfile names so they never collide on disk.
2609
2613
  */
2610
- type CaptureMode = "buffer" | "none";
2611
- type RunOptions = {
2612
- /**
2613
- * Working directory for the child process. Required: callers resolve
2614
- * the workspace root themselves; the runner does not validate cwd
2615
- * existence and surfaces native spawn errors via classification.
2616
- */
2617
- readonly cwd: string;
2618
- /**
2619
- * Environment variables for the child. When omitted, the parent's
2620
- * `process.env` is inherited verbatim. Callers wanting a sanitized
2621
- * environment must build it explicitly.
2622
- */
2623
- readonly env?: NodeJS.ProcessEnv;
2624
- /**
2625
- * External cancellation. Aborting the signal triggers a two-stage
2626
- * kill (SIGTERM, then SIGKILL after a short grace period).
2627
- */
2628
- readonly signal?: AbortSignal;
2629
- /**
2630
- * Internal timeout in milliseconds. Must be a positive finite number.
2631
- * Triggers the same two-stage kill as `signal`.
2632
- */
2633
- readonly timeout_ms?: number;
2634
- /**
2635
- * Optional input written to the child's stdin. The pipe is closed
2636
- * after the value is written. Incompatible with `capture: "none"`.
2637
- */
2638
- readonly stdin?: string | Buffer;
2639
- /**
2640
- * Output capture mode. Defaults to `"buffer"`. See {@link CaptureMode}.
2641
- */
2642
- readonly capture?: CaptureMode;
2614
+ type LockScope = "task" | "session";
2615
+ type LockHandle = {
2643
2616
  /**
2644
- * Invoked synchronously immediately after the child has been spawned,
2645
- * before the runner waits for completion. Callers use this to retain a
2646
- * reference for parent-side cleanup (e.g. an `exit` hook that SIGKILLs
2647
- * the child if the parent is forcibly terminated). The runner takes no
2648
- * action if the callback throws.
2617
+ * Release the lock by unlinking the lockfile. Best-effort: any unlink error
2618
+ * is swallowed so a doubled release does not raise, and disk state never
2619
+ * holds a stranded lockfile after the caller's `finally` block.
2649
2620
  */
2650
- readonly onSpawn?: (child: ChildProcess) => void;
2651
- };
2652
- type RunResult = {
2653
- readonly command: string;
2654
- readonly args: readonly string[];
2655
- readonly cwd: string;
2656
- /** `null` when the process was killed by a signal. */
2657
- readonly exit_code: number | null;
2658
- readonly signal: NodeJS.Signals | null;
2659
- readonly stdout: string;
2660
- readonly stderr: string;
2661
- /** ISO 8601 timestamp captured before spawn. */
2662
- readonly started_at: string;
2663
- /** ISO 8601 timestamp captured on the `close` event. */
2664
- readonly ended_at: string;
2665
- readonly duration_ms: number;
2666
- readonly pid: number | null;
2667
- };
2668
- type ProcessRunner = {
2669
- run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2621
+ release: () => Promise<void>;
2670
2622
  };
2671
-
2672
2623
  /**
2673
- * Spawn-based ProcessRunner implementation.
2624
+ * Acquire an advisory lock at `<paths.locks>/<scope>_<id>.lock` for the
2625
+ * lifetime of the returned handle. Lockfile body records the holder's pid
2626
+ * and acquire timestamp so a competitor can detect stale locks left by a
2627
+ * SIGINT'd CLI run and recover automatically.
2674
2628
  *
2675
- * Behavior:
2676
- * - `shell: false` and `detached: false`. The process group is not
2677
- * detached, but the OS does not guarantee the child is reaped when
2678
- * the parent terminates abruptly; callers handle SIGINT/SIGTERM/exit
2679
- * hooks themselves.
2680
- * - `capture: "buffer"` (default): `stdio: ['pipe', 'pipe', 'pipe']`,
2681
- * stdout / stderr are decoded as UTF-8 and accumulated as full
2682
- * strings (no streaming callbacks).
2683
- * - `capture: "none"`: `stdio: ['inherit', 'inherit', 'inherit']`, the
2684
- * child writes directly to the parent terminal in real time and
2685
- * `RunResult.stdout` / `stderr` are empty strings. `stdin` is
2686
- * incompatible with this mode (the child has no writable stdin pipe)
2687
- * and the combination is rejected before spawn.
2688
- * - `timeout_ms` and `AbortSignal` both trigger a two-stage kill:
2689
- * `SIGTERM`, then `SIGKILL` after `DEFAULT_KILL_GRACE_MS` (5_000 ms).
2690
- * - A non-zero `exit_code` does not throw; it is returned via
2691
- * `RunResult`. Spawn-time errors throw with a pathless message and
2692
- * the original error attached as `cause`.
2629
+ * Acquisition strategy:
2630
+ * 1. {@link atomicCreate} the lockfile (POSIX link(2) + EEXIST).
2631
+ * 2. On EEXIST, probe the existing lockfile via {@link isStaleLock}.
2632
+ * - If stale (= holder pid is dead or lock is older than
2633
+ * {@link STALE_LOCK_MAX_AGE_MS}), `unlink` the stale file and retry
2634
+ * the atomic create once.
2635
+ * - If still EEXIST after the retry (= another competitor won the race),
2636
+ * throw `"Lock is held by another process"`.
2637
+ * - If the holder is alive, throw `"Lock is held by another process"`
2638
+ * without retrying.
2693
2639
  *
2694
- * Error message contract: messages never include `cwd` or absolute
2695
- * command paths. The original errno (and any nested wrapping) is
2696
- * preserved on `Error.cause`, allowing callers to classify with
2697
- * `findErrorCode` when needed.
2640
+ * The caller MUST call `release()` (typically from a `finally` block); the
2641
+ * `process.exit()` path or a fatal crash relies on stale-lock detection on
2642
+ * the next acquire to recover.
2698
2643
  */
2699
- declare class ChildProcessRunner implements ProcessRunner {
2700
- run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2701
- }
2644
+ declare function acquireLock(paths: BasouPaths, scope: LockScope, resourceId: string): Promise<LockHandle>;
2702
2645
 
2703
2646
  /**
2704
- * Payload subset of `git_snapshot` event, mechanically derived from the
2705
- * zod-inferred event type. The wrapping event-shape fields
2706
- * (schema_version, id, session_id, occurred_at, source, type) are added by
2707
- * the caller (session lifecycle in later steps) when constructing the
2708
- * event, so the schema remains the single source of truth.
2647
+ * Inputs for {@link createManifest}. Optional fields drop out of the
2648
+ * resulting Manifest entirely (they are not emitted as `null`/`undefined`
2649
+ * in YAML); pass `null` for `repositoryUrl` to keep an explicit `null`.
2650
+ */
2651
+ type CreateManifestInput = {
2652
+ workspaceName: string;
2653
+ projectName?: string;
2654
+ projectDescription?: string;
2655
+ repositoryUrl?: string | null;
2656
+ /** Override for tests; defaults to `new Date()`. */
2657
+ now?: Date;
2658
+ /** Override for tests; defaults to a freshly generated `ws_<ULID>`. */
2659
+ workspaceId?: PrefixedId<"ws">;
2660
+ };
2661
+ /**
2662
+ * Build a fresh Manifest object that satisfies the manifest schema's
2663
+ * minimum shape. Performs no I/O. Returned object is parse-validated by
2664
+ * `ManifestSchema`.
2665
+ */
2666
+ declare function createManifest(input: CreateManifestInput): Manifest;
2667
+ /**
2668
+ * Write a Manifest to `paths.files.manifest`. Re-validates via
2669
+ * `ManifestSchema` before serialization.
2709
2670
  *
2710
- * `ahead` / `behind` are omitted when there is no remote or no upstream
2711
- * tracking; the schema declares both as optional non-negative integers.
2671
+ * Refuses to overwrite an existing manifest unless `force: true`.
2712
2672
  */
2713
- type GitSnapshot = Omit<GitSnapshotEvent, "schema_version" | "id" | "session_id" | "occurred_at" | "source" | "type">;
2673
+ declare function writeManifest(paths: BasouPaths, manifest: Manifest, options?: {
2674
+ force?: boolean;
2675
+ }): Promise<void>;
2714
2676
  /**
2715
- * Resolve the absolute path of the Git repository root that contains `cwd`.
2716
- * Equivalent to `git rev-parse --show-toplevel`.
2677
+ * Read and parse a Manifest from `paths.files.manifest`. Throws if the file
2678
+ * is missing or contents fail `ManifestSchema` validation.
2679
+ */
2680
+ declare function readManifest(paths: BasouPaths): Promise<Manifest>;
2681
+
2682
+ /** Marker line that begins the auto-generated region. */
2683
+ declare const GENERATED_START = "<!-- BASOU:GENERATED:START -->";
2684
+ /** Marker line that ends the auto-generated region. */
2685
+ declare const GENERATED_END = "<!-- BASOU:GENERATED:END -->";
2686
+ /**
2687
+ * Result of parsing a markdown body for the BASOU:GENERATED marker region.
2717
2688
  *
2718
- * Throws `Error("Git executable not found in PATH. Install git first.")`
2719
- * with the spawn error attached as `cause` when git itself is missing.
2720
- * Throws `Error("Not a git repository")` (without command-specific suffix)
2721
- * when `cwd` is not inside a repository callers MAY wrap with their own
2722
- * "Run 'git init' first, then re-run 'basou XXX'." suffix.
2689
+ * The spec mandates strict line-level matching (see
2690
+ * `docs/spec/generated-markdown.md#102-marker-convention`): a marker is
2691
+ * only recognized when an entire line is exactly the marker string.
2692
+ * Leading/trailing whitespace, comment compression, and BOM are treated as
2693
+ * legacy formats (`no_markers`) so that re-generation refuses to silently
2694
+ * overwrite a mismatched manual edit.
2695
+ */
2696
+ type MarkerSection = {
2697
+ kind: "ok";
2698
+ before: string;
2699
+ generated: string;
2700
+ after: string;
2701
+ } | {
2702
+ kind: "no_markers";
2703
+ } | {
2704
+ kind: "missing_start";
2705
+ } | {
2706
+ kind: "missing_end";
2707
+ } | {
2708
+ kind: "multiple_pairs";
2709
+ } | {
2710
+ kind: "wrong_order";
2711
+ };
2712
+ /**
2713
+ * Read a markdown file as UTF-8 text. Returns `null` when the file does not
2714
+ * exist; throws `Error("Failed to read markdown file", { cause })` for other
2715
+ * I/O failures (pathless contract — never embed the absolute path in the
2716
+ * thrown `message`).
2717
+ */
2718
+ declare function readMarkdownFile(filePath: string): Promise<string | null>;
2719
+ /**
2720
+ * Atomically write a markdown body via {@link atomicReplace}. The shared
2721
+ * helper handles the tmp-file + rename sequence, `wx` collision guard, and
2722
+ * best-effort tmp cleanup on failure.
2723
2723
  *
2724
- * Pathless contract: the thrown message never embeds `cwd` or any absolute
2725
- * path; native errors are kept on `error.cause` for verbose surfacing.
2724
+ * On any failure the original error is re-thrown as
2725
+ * `Error("Failed to write markdown file", { cause })` (pathless contract).
2726
2726
  */
2727
- declare function resolveRepositoryRoot(cwd: string): Promise<string>;
2727
+ declare function writeMarkdownFile(filePath: string, body: string): Promise<void>;
2728
2728
  /**
2729
- * Read `remote.origin.url` from the local repository config. Returns
2730
- * `undefined` if the remote is unset, the value is empty, or the lookup
2731
- * fails for any reason (best-effort).
2729
+ * Parse a markdown body and identify the BASOU:GENERATED marker region.
2730
+ *
2731
+ * Returns one of six `kind` discriminants:
2732
+ * - `ok`: exactly one START line followed by exactly one END line in the
2733
+ * correct order. `before` / `generated` / `after` slice the original
2734
+ * text by character offsets so CRLF / LF are preserved verbatim outside
2735
+ * the marker region.
2736
+ * - `no_markers`: both START and END absent (legacy file / fresh write).
2737
+ * - `missing_start` / `missing_end`: exactly one of the pair is present.
2738
+ * - `multiple_pairs`: more than one START or END line.
2739
+ * - `wrong_order`: END appears before START.
2732
2740
  *
2733
- * The `--local` scope is critical: callers MUST NOT pick up the developer's
2734
- * global remote.origin.url, which could leak the wrong repository URL into
2735
- * `manifest.yaml`.
2741
+ * Matching is strict: leading/trailing whitespace, BOM, and comment
2742
+ * compression (`<!--BASOU:...-->`) all bypass the marker and are treated
2743
+ * as legacy content.
2736
2744
  */
2737
- declare function tryRemoteUrl(repositoryRoot: string): Promise<string | undefined>;
2745
+ declare function parseMarkers(content: string): MarkerSection;
2738
2746
  /**
2739
- * Build a {@link GitSnapshot} for the repository at `repositoryRoot`. The
2740
- * caller is responsible for ensuring `repositoryRoot` is the canonical root
2741
- * (typically obtained via {@link resolveRepositoryRoot}); this function
2742
- * verifies repo membership via `git rev-parse --is-inside-work-tree` to
2743
- * distinguish a non-git directory from an empty repository.
2747
+ * Build the final markdown body by replacing the BASOU:GENERATED region.
2744
2748
  *
2745
- * Edge cases:
2746
- * - **non-git directory**: throws `Error("Not a git repository")`
2747
- * - **empty repo (no commits)**: throws `Error("No commits in repository")`
2748
- * - **detached HEAD**: `branch = "HEAD"`, `head = commit hash`,
2749
- * `ahead`/`behind` omitted
2750
- * - **no remote / no upstream tracking**: `ahead`/`behind` omitted
2749
+ * - `existing === null` (no file yet): return `<START>\n<generated>\n<END>\n`.
2750
+ * - existing parses to `ok`: replace the marked region and keep everything
2751
+ * before START and after END untouched (preserving manual additions).
2752
+ * - any other parse result: throw a pathless error referencing `fileLabel`.
2751
2753
  *
2752
- * Pathless contract preserved on every throw path.
2754
+ * The caller passes `fileLabel` (e.g. `"handoff.md"` or `"decisions.md"`)
2755
+ * so the error message is informative without leaking an absolute path.
2753
2756
  */
2754
- declare function getSnapshot(repositoryRoot: string): Promise<GitSnapshot>;
2757
+ declare function renderWithMarkers(existing: string | null, generated: string, fileLabel: string): string;
2755
2758
 
2756
2759
  /**
2757
- * Status classification used by the `file_changed` event schema. Limited to
2758
- * the four classes that simple-git's `git diff --name-status` reliably
2759
- * surfaces; copy / unmerged / typechange entries are intentionally dropped
2760
- * to keep the event payload shape narrow.
2761
- */
2762
- type FileChangeStatus = "added" | "modified" | "deleted" | "renamed";
2763
- /**
2764
- * Single file-level change observed between two refs. `old_path` is set
2765
- * only for `renamed` entries (the previous path of the file).
2760
+ * Options for {@link importSessionFromJson}. All fields are optional.
2761
+ *
2762
+ * - `labelOverride` / `taskIdOverride` come from the CLI `--label` / `--task`
2763
+ * flags and win over the corresponding fields on the input payload.
2764
+ * - `dryRun` skips disk writes entirely and returns a preview result.
2766
2765
  */
2767
- type FileChange = {
2768
- path: string;
2769
- old_path?: string;
2770
- status: FileChangeStatus;
2766
+ type ImportSessionOptions = {
2767
+ labelOverride?: string;
2768
+ taskIdOverride?: string;
2769
+ dryRun?: boolean;
2771
2770
  };
2772
2771
  /**
2773
- * Result of {@link getDiff}. The `changed_files` array is in git's natural
2774
- * `--name-status` order; callers requiring deterministic ordering should
2775
- * sort by `path` themselves.
2772
+ * Result of a successful import. `finalStatus` is always the literal
2773
+ * `"imported"` (per the import-session lifecycle policy); `finalSourceKind`
2774
+ * mirrors the input's `session.source.kind` so round-trip imports preserve
2775
+ * provenance.
2776
+ *
2777
+ * `pathSanitizeReport` summarises how many path-shaped fields the importer
2778
+ * rewrote on the way in: `related_files[]` entries plus a single boolean
2779
+ * for `working_directory`. The CLI wrapper surfaces this as a one-line
2780
+ * stderr warning when the total is non-zero so the operator sees that
2781
+ * machine-private prefixes were stripped.
2776
2782
  */
2777
- type DiffResult = {
2778
- changed_files: FileChange[];
2783
+ type ImportSessionResult = {
2784
+ sessionId: PrefixedId<"ses">;
2785
+ eventCount: number;
2786
+ finalStatus: SessionStatus;
2787
+ finalSourceKind: SessionSourceKind;
2788
+ pathSanitizeReport: {
2789
+ relatedFiles: number;
2790
+ workingDirectoryRewritten: boolean;
2791
+ };
2779
2792
  };
2780
2793
  /**
2781
- * Compute the file-level diff between two git refs.
2782
- *
2783
- * Returns a list of changed file paths classified by status (added /
2784
- * modified / deleted / renamed). Diff content is intentionally NOT
2785
- * returned — `file_changed` events record paths only, and raw diff bodies
2786
- * are excluded so the trace cannot inadvertently leak source code that may
2787
- * be sensitive. Use `git show <ref>` to obtain the underlying diff.
2788
- *
2789
- * Pathless contract: every thrown message is a fixed string from the set
2790
- * {`Not a git repository`, `Git executable not found in PATH. Install git
2791
- * first.`, `Invalid ref`, `Failed to compute git diff`}; native errors are
2792
- * preserved on `Error.cause`.
2794
+ * Import a round-trip JSON payload into `.basou/sessions/<new>/`. The caller
2795
+ * MUST validate the payload against {@link SessionImportPayloadSchema} first
2796
+ * and gate the `schema_version === "0.1.0"` literal check externally; this
2797
+ * function trusts both invariants.
2793
2798
  *
2794
- * Special cases:
2795
- * - `baseRef === headRef` short-circuits to an empty result
2796
- * - copy / unmerged / typechange / unknown status codes are skipped
2799
+ * On success a fresh session ID is minted and a complete
2800
+ * `session.yaml` + `events.jsonl` pair is written atomically. On any post-
2801
+ * mkdir failure the session directory is removed best-effort so partial
2802
+ * imports do not leave `session_yaml_missing` half-states behind.
2797
2803
  *
2798
- * @param repoRoot absolute path to the git repository root
2799
- * @param baseRef base ref (e.g. session-start HEAD sha)
2800
- * @param headRef head ref (e.g. session-end HEAD sha)
2804
+ * Throws `Error` with one of the fixed messages enumerated by the import contract
2805
+ * §"Error messages" table; the original native error is attached as `cause`
2806
+ * for `--verbose` rendering.
2801
2807
  */
2802
- declare function getDiff(repoRoot: string, baseRef: string, headRef: string): Promise<DiffResult>;
2808
+ declare function importSessionFromJson(paths: BasouPaths, manifest: Manifest, payload: SessionImportPayload, options: ImportSessionOptions): Promise<ImportSessionResult>;
2803
2809
 
2804
2810
  /**
2805
- * Static metadata identifying the claude-code adapter as the session source.
2806
- * Consumed by the CLI orchestration when populating `session.yaml.source`
2807
- * and event `source` fields. The literal `kind` is part of the wire format
2808
- * defined by the session schema; do not change without coordinated schema
2809
- * migration.
2810
- */
2811
- declare const claudeCodeAdapterMetadata: {
2812
- readonly kind: "claude-code-adapter";
2813
- readonly version: "0.1.0";
2814
- };
2815
- /**
2816
- * Lookup predicate used by {@link resolveClaudeCodeCommand} to decide
2817
- * whether a candidate executable is reachable on PATH. Exposed as a
2818
- * parameter so tests can substitute a deterministic mock; production
2819
- * callers should omit it and rely on the default `which`-based lookup.
2811
+ * Walk the cause chain (up to `depth` levels) looking for an Error whose
2812
+ * errno-style `code` matches `code`. Returns true on the first match.
2813
+ * Resilient to wrapper depth changes so that ENOENT detection survives
2814
+ * future error-wrapping refactors.
2820
2815
  */
2821
- type CommandLookup = (command: string) => Promise<boolean>;
2816
+ declare function findErrorCode(error: unknown, code: string, depth?: number): boolean;
2817
+
2822
2818
  /**
2823
- * Resolve the Claude Code CLI executable name. Tries `claude-code` first
2824
- * and falls back to `claude`; the first candidate found on PATH wins.
2819
+ * Refuse to operate on `.basou` if it is a symlink or not a directory. This
2820
+ * prevents `writeStatus` from being tricked into writing `status.json`
2821
+ * outside the repository root via a swapped `.basou` symlink. Mirrors
2822
+ * `ensureBasouDirectory`'s lstat-based guard.
2825
2823
  *
2826
- * Throws a fixed-message Error when neither candidate is reachable, so
2827
- * callers can present a single user-facing prompt to install the CLI.
2824
+ * If `.basou` is absent the underlying ENOENT is propagated (wrapped) so
2825
+ * callers can map it to "workspace not initialized" via `findErrorCode`.
2828
2826
  *
2829
- * @throws Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.")
2827
+ * Note: this is a baseline safety net, not a TOCTOU fix the directory
2828
+ * could still be replaced between this check and the subsequent write. The
2829
+ * goal is to detect already-swapped symlinks, not to race-proof the
2830
+ * filesystem.
2830
2831
  */
2831
- declare function resolveClaudeCodeCommand(lookup?: CommandLookup): Promise<{
2832
- command: string;
2833
- }>;
2832
+ declare function assertBasouRootSafe(rootPath: string): Promise<void>;
2834
2833
  /**
2835
- * Stub for the future `adapter_output` summary generator.
2836
- *
2837
- * v0.1 Step 11 keeps `capture: "none"` and intentionally does not emit
2838
- * `adapter_output` events, so this hook has no production callers yet.
2839
- * The signature is committed so that Step 12+ can implement raw_ref
2840
- * generation without retrofitting the adapter scaffold.
2834
+ * Build a StatusSnapshot from a manifest plus the path layout, observing
2835
+ * each subdirectory's presence via `lstat`. Read-only with respect to the
2836
+ * workspace state; writes nothing. The result is re-validated by
2837
+ * `StatusSchema.parse` before being returned.
2841
2838
  *
2842
- * @throws Error - always; not implemented in v0.1 Step 11.
2839
+ * @param input.now Override for testing; defaults to `new Date()`.
2843
2840
  */
2844
- declare function summarizeAdapterOutput(_stream: "stdout" | "stderr", _raw: string): string;
2845
-
2841
+ declare function buildStatusSnapshot(input: {
2842
+ manifest: Manifest;
2843
+ paths: BasouPaths;
2844
+ now?: Date;
2845
+ }): Promise<StatusSnapshot>;
2846
2846
  /**
2847
- * Append a single Basou event to `<sessionDir>/events.jsonl`.
2848
- *
2849
- * The event is validated against the discriminated union {@link EventSchema}
2850
- * before being serialized as a single JSONL line (UTF-8, terminated by `\n`).
2851
- * Validation enforces the per-variant contract (required fields, source
2852
- * vocabulary, strict variants such as `adapter_output`).
2853
- *
2854
- * Atomicity: writes go through `appendFile` which uses `O_APPEND`. Lines up
2855
- * to `PIPE_BUF` bytes (Linux 4096 / macOS 512) are written atomically by the
2856
- * kernel; longer lines may interleave with concurrent writers and are not
2857
- * recovered here. v0.1 assumes a single writer per session, so partial-line
2858
- * recovery is delegated to the read side (event replay) when introduced.
2847
+ * Atomically write a StatusSnapshot to `paths.files.status`.
2859
2848
  *
2860
- * Throws if validation fails or the underlying append errors. The thrown
2861
- * Error message is pathless; the original error is attached as `cause`.
2849
+ * Re-validates via `StatusSchema.parse` before any file I/O, so an invalid
2850
+ * snapshot throws synchronously and never overwrites the existing
2851
+ * `status.json`. Delegates the tmp-file + rename pass to {@link atomicReplace}.
2862
2852
  *
2863
- * @param sessionDir absolute path to `.basou/sessions/<session_id>/`
2864
- * @param event unknown payload to validate and append
2853
+ * **Precondition**: callers MUST invoke {@link assertBasouRootSafe} on
2854
+ * `paths.root` first to ensure `.basou` is a real directory and not a
2855
+ * swapped symlink. `writeStatus` does not redo this guard — it trusts the
2856
+ * caller — so a direct invocation without the guard could write
2857
+ * `status.json` outside the repository root.
2865
2858
  */
2866
- declare function appendEvent(sessionDir: string, event: unknown): Promise<void>;
2859
+ declare function writeStatus(paths: BasouPaths, snapshot: StatusSnapshot): Promise<void>;
2867
2860
  /**
2868
- * Write `events.jsonl` in one atomic tmp+rename pass via {@link atomicReplace},
2869
- * validating every event against {@link EventSchema} before any disk I/O so
2870
- * a payload that fails validation never leaves a partial file behind.
2871
- *
2872
- * The helper is used by the round-trip importer (`session-import.ts`) and the
2873
- * ad-hoc session orchestrator (`ad-hoc-session.ts`) where a small, fixed batch
2874
- * of events must land together or not at all. Zero events produces a
2875
- * zero-byte file so the session_yaml `events_log` pointer remains valid.
2876
- *
2877
- * Throws `"Invalid Basou event payload"` (same fixed message as
2878
- * {@link appendEvent}) on validation failure, or `"Failed to write
2879
- * events.jsonl"` on a disk I/O failure. The original native error is attached
2880
- * as `cause`.
2861
+ * Read `.basou/status.json` for the current schema_version (0.1.0). This
2862
+ * is a cache reader only; cross-version migration is not supported here.
2863
+ * Older or newer status.json shapes will fail `StatusSchema.parse`
2864
+ * callers regenerate by calling `buildStatusSnapshot` + `writeStatus`.
2881
2865
  */
2882
- declare function writeEventsBulk(sessionDir: string, events: Event[]): Promise<void>;
2866
+ declare function readStatus(paths: BasouPaths): Promise<StatusSnapshot>;
2883
2867
 
2884
2868
  /**
2885
- * Parse a unit-suffixed duration string (e.g. `30s`, `5m`, `1h`, `100ms`)
2886
- * into milliseconds.
2869
+ * Read a YAML file as `unknown`. Caller MUST validate via a zod schema.
2887
2870
  *
2888
- * Rejects formats that cannot represent a positive, finite millisecond
2889
- * value: malformed inputs, zero, leading-zero values, and computations that
2890
- * overflow to `Infinity`. The returned number is always a positive integer.
2871
+ * Throws Error with pathless message and the original native error attached
2872
+ * as `cause` for I/O failures and YAML parse errors. All fs and parse exits
2873
+ * go through fixed messages so absolute paths cannot leak via `error.message`.
2874
+ */
2875
+ declare function readYamlFile(filePath: string): Promise<unknown>;
2876
+ /**
2877
+ * Write a value as YAML using {@link atomicReplace} for crash-resistant
2878
+ * atomicity. The shared helper handles the tmp-file + rename sequence,
2879
+ * `wx` collision guard, and best-effort tmp cleanup on failure. This
2880
+ * wrapper adds the YAML serialisation and the pathless error vocabulary.
2881
+ */
2882
+ declare function writeYamlFile(filePath: string, value: unknown): Promise<void>;
2883
+ /**
2884
+ * Atomically create a new YAML file. Like {@link writeYamlFile} but
2885
+ * delegates to {@link atomicCreate} so a pre-existing target fails with
2886
+ * EEXIST instead of being silently overwritten.
2891
2887
  *
2892
- * Supported units: `ms` (milliseconds), `s` (seconds), `m` (minutes),
2893
- * `h` (hours).
2888
+ * Used by `basou approval approve` / `reject` to write the resolved-side
2889
+ * YAML, so a concurrent resolver cannot overwrite an already-resolved
2890
+ * approval.
2894
2891
  *
2895
- * @param input duration string with required unit suffix
2896
- * @returns duration in milliseconds (positive, finite)
2897
- * @throws Error with message
2898
- * `Invalid duration: <input>. Expected format: <positive-integer><unit> where unit is ms/s/m/h`
2899
- * for format errors, or `Duration overflow: <input>` for non-finite results.
2892
+ * Throws `Error("Failed to write YAML file", { cause })` on failure; if
2893
+ * `cause.code === "EEXIST"` the caller can detect a target-exists race.
2900
2894
  */
2901
- declare function parseDuration(input: string): number;
2895
+ declare function linkYamlFile(filePath: string, value: unknown): Promise<void>;
2896
+ /**
2897
+ * Overwrite an existing YAML file atomically. Like {@link writeYamlFile}
2898
+ * but with a distinct pathless message label, used for files that
2899
+ * legitimately need in-place mutation (e.g. session.yaml's status /
2900
+ * ended_at lifecycle updates).
2901
+ */
2902
+ declare function overwriteYamlFile(filePath: string, value: unknown): Promise<void>;
2902
2903
 
2903
2904
  /**
2904
2905
  * Version of the `@basou/core` package, aligned with `manifest.yaml`'s