@basou/core 0.3.1 → 0.5.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,108 +2,47 @@ 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.
10
- */
11
- declare const ID_PREFIXES: readonly ["ws", "task", "ses", "evt", "appr", "decision"];
12
- /**
13
- * Type prefix used for Basou entity IDs.
14
- * Format: `<prefix>_<26-char ULID>`, e.g. `ws_01HXABCDEF1234567890ABCDEF`.
15
- */
16
- type IdPrefix = (typeof ID_PREFIXES)[number];
17
- /**
18
- * A Basou entity ID as a template literal type.
19
- *
20
- * `PrefixedId<"ses">` narrows to ``ses_${string}`` so a session schema can
21
- * preserve the prefix in its inferred type beyond runtime validation.
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.
22
10
  */
23
- type PrefixedId<P extends IdPrefix = IdPrefix> = `${P}_${string}`;
11
+ declare const claudeCodeAdapterMetadata: {
12
+ readonly kind: "claude-code-adapter";
13
+ readonly version: "0.1.0";
14
+ };
24
15
  /**
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.
30
- *
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.
35
- *
36
- * @param seedTime Optional millisecond timestamp passed to the monotonic
37
- * factory. Useful for ordered generation in tests; not deterministic.
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.
38
20
  */
39
- declare function ulid(seedTime?: number): string;
21
+ type CommandLookup = (command: string) => Promise<boolean>;
40
22
  /**
41
- * Generate a prefixed Basou ID, e.g. `ses_01HXABCDEF1234567890ABCDEF`.
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.
42
25
  *
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.
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.
45
28
  *
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.
29
+ * @throws Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.")
49
30
  */
50
- declare function prefixedUlid<P extends IdPrefix>(prefix: P): PrefixedId<P>;
31
+ declare function resolveClaudeCodeCommand(lookup?: CommandLookup): Promise<{
32
+ command: string;
33
+ }>;
51
34
  /**
52
- * Check whether the given string is a valid prefixed Basou ID.
53
- *
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.
35
+ * Stub for the future `adapter_output` summary generator.
59
36
  *
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.
62
- */
63
- declare function isValidPrefixedId(value: string): boolean;
64
-
65
- /**
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.
68
- */
69
- declare const SchemaVersionSchema: z.ZodLiteral<"0.1.0">;
70
- /**
71
- * ISO 8601 timestamp with explicit timezone offset (e.g. `+09:00`).
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.
72
42
  *
73
- * The spec samples include offsets, so the default zod `.datetime()` (which
74
- * rejects offsets) is insufficient; `{ offset: true }` is required.
75
- */
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>>;
89
- /**
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.
92
- */
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>;
101
- /**
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.
43
+ * @throws Error - always; not implemented in this release.
105
44
  */
106
- declare const EventSourceSchema: z.ZodString;
45
+ declare function summarizeAdapterOutput(_stream: "stdout" | "stderr", _raw: string): string;
107
46
 
108
47
  /**
109
48
  * Schema for `.basou/manifest.yaml`. The minimal manifest carries
@@ -154,79 +93,81 @@ declare const ManifestSchema: z.ZodObject<{
154
93
  /** Inferred runtime type for {@link ManifestSchema}. */
155
94
  type Manifest = z.infer<typeof ManifestSchema>;
156
95
 
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>;
96
+ declare const SessionInnerImportSchema: z.ZodObject<{
97
+ id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
98
+ label: z.ZodOptional<z.ZodString>;
99
+ task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
100
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
101
+ source: z.ZodObject<{
102
+ kind: z.ZodEnum<{
103
+ "claude-code-adapter": "claude-code-adapter";
104
+ "claude-code-import": "claude-code-import";
105
+ "codex-import": "codex-import";
106
+ human: "human";
107
+ import: "import";
108
+ terminal: "terminal";
109
+ }>;
110
+ version: z.ZodLiteral<"0.1.0">;
111
+ external_id: z.ZodOptional<z.ZodString>;
112
+ }, z.core.$strip>;
113
+ started_at: z.ZodString;
114
+ ended_at: z.ZodOptional<z.ZodString>;
115
+ status: z.ZodEnum<{
116
+ initialized: "initialized";
117
+ running: "running";
118
+ waiting_approval: "waiting_approval";
119
+ completed: "completed";
120
+ failed: "failed";
121
+ interrupted: "interrupted";
122
+ imported: "imported";
123
+ archived: "archived";
124
+ }>;
125
+ working_directory: z.ZodString;
126
+ invocation: z.ZodObject<{
127
+ command: z.ZodString;
128
+ args: z.ZodArray<z.ZodString>;
129
+ exit_code: z.ZodNullable<z.ZodNumber>;
130
+ }, z.core.$strip>;
131
+ related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
132
+ events_log: z.ZodOptional<z.ZodString>;
133
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
134
+ metrics: z.ZodOptional<z.ZodObject<{
135
+ output_tokens: z.ZodOptional<z.ZodNumber>;
136
+ input_tokens: z.ZodOptional<z.ZodNumber>;
137
+ cached_input_tokens: z.ZodOptional<z.ZodNumber>;
138
+ reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
139
+ active_time_ms: z.ZodOptional<z.ZodNumber>;
140
+ active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
141
+ start: z.ZodString;
142
+ end: z.ZodString;
143
+ }, z.core.$strip>>>;
144
+ active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
145
+ active_time_method: z.ZodOptional<z.ZodString>;
146
+ }, z.core.$strip>>;
184
147
  }, 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
148
  /**
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.
149
+ * Schema for the round-trip JSON payload accepted by `basou session import
150
+ * --format json`. The top level is `.strict()`; unknown keys at the outer
151
+ * envelope are rejected.
214
152
  */
215
- declare const SessionSchema: z.ZodObject<{
216
- schema_version: z.ZodLiteral<"0.1.0">;
153
+ declare const SessionImportPayloadSchema: z.ZodObject<{
154
+ schema_version: z.ZodString;
217
155
  session: z.ZodObject<{
218
- id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
156
+ id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
219
157
  label: z.ZodOptional<z.ZodString>;
220
158
  task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
221
159
  workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
222
160
  source: z.ZodObject<{
223
161
  kind: z.ZodEnum<{
224
162
  "claude-code-adapter": "claude-code-adapter";
163
+ "claude-code-import": "claude-code-import";
164
+ "codex-import": "codex-import";
225
165
  human: "human";
226
166
  import: "import";
227
167
  terminal: "terminal";
228
168
  }>;
229
169
  version: z.ZodLiteral<"0.1.0">;
170
+ external_id: z.ZodOptional<z.ZodString>;
230
171
  }, z.core.$strip>;
231
172
  started_at: z.ZodString;
232
173
  ended_at: z.ZodOptional<z.ZodString>;
@@ -243,94 +184,380 @@ declare const SessionSchema: z.ZodObject<{
243
184
  working_directory: z.ZodString;
244
185
  invocation: z.ZodObject<{
245
186
  command: z.ZodString;
246
- args: z.ZodDefault<z.ZodArray<z.ZodString>>;
187
+ args: z.ZodArray<z.ZodString>;
247
188
  exit_code: z.ZodNullable<z.ZodNumber>;
248
189
  }, z.core.$strip>;
249
190
  related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
250
- events_log: z.ZodDefault<z.ZodString>;
191
+ events_log: z.ZodOptional<z.ZodString>;
251
192
  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>>;
193
+ metrics: z.ZodOptional<z.ZodObject<{
194
+ output_tokens: z.ZodOptional<z.ZodNumber>;
195
+ input_tokens: z.ZodOptional<z.ZodNumber>;
196
+ cached_input_tokens: z.ZodOptional<z.ZodNumber>;
197
+ reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
198
+ active_time_ms: z.ZodOptional<z.ZodNumber>;
199
+ active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
200
+ start: z.ZodString;
201
+ end: z.ZodString;
202
+ }, z.core.$strip>>>;
203
+ active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
204
+ active_time_method: z.ZodOptional<z.ZodString>;
205
+ }, z.core.$strip>>;
206
+ }, z.core.$strict>;
207
+ events: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
208
+ schema_version: z.ZodLiteral<"0.1.0">;
209
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
210
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
211
+ occurred_at: z.ZodString;
212
+ source: z.ZodString;
213
+ type: z.ZodLiteral<"session_started">;
214
+ }, z.core.$strip>, z.ZodObject<{
215
+ schema_version: z.ZodLiteral<"0.1.0">;
216
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
217
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
218
+ occurred_at: z.ZodString;
219
+ source: z.ZodString;
220
+ type: z.ZodLiteral<"session_ended">;
221
+ exit_code: z.ZodOptional<z.ZodNumber>;
222
+ }, z.core.$strip>, z.ZodObject<{
223
+ schema_version: z.ZodLiteral<"0.1.0">;
224
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
225
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
226
+ occurred_at: z.ZodString;
227
+ source: z.ZodString;
228
+ type: z.ZodLiteral<"session_status_changed">;
229
+ from: z.ZodString;
230
+ to: z.ZodString;
231
+ }, z.core.$strip>, z.ZodObject<{
232
+ schema_version: z.ZodLiteral<"0.1.0">;
233
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
234
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
235
+ occurred_at: z.ZodString;
236
+ source: z.ZodString;
237
+ type: z.ZodLiteral<"approval_requested">;
238
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
239
+ expires_at: z.ZodDefault<z.ZodNullable<z.ZodString>>;
240
+ risk_level: z.ZodEnum<{
241
+ low: "low";
242
+ medium: "medium";
243
+ high: "high";
244
+ critical: "critical";
245
+ }>;
246
+ action: z.ZodObject<{
247
+ kind: z.ZodString;
248
+ }, z.core.$loose>;
249
+ reason: z.ZodString;
250
+ status: z.ZodLiteral<"pending">;
251
+ }, z.core.$strip>, z.ZodObject<{
252
+ schema_version: z.ZodLiteral<"0.1.0">;
253
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
254
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
255
+ occurred_at: z.ZodString;
256
+ source: z.ZodString;
257
+ type: z.ZodLiteral<"approval_approved">;
258
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
259
+ resolver: z.ZodOptional<z.ZodString>;
260
+ note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
261
+ }, z.core.$strip>, z.ZodObject<{
262
+ schema_version: z.ZodLiteral<"0.1.0">;
263
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
264
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
265
+ occurred_at: z.ZodString;
266
+ source: z.ZodString;
267
+ type: z.ZodLiteral<"approval_rejected">;
268
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
269
+ resolver: z.ZodOptional<z.ZodString>;
270
+ reason: z.ZodString;
271
+ }, z.core.$strip>, z.ZodObject<{
272
+ schema_version: z.ZodLiteral<"0.1.0">;
273
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
274
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
275
+ occurred_at: z.ZodString;
276
+ source: z.ZodString;
277
+ type: z.ZodLiteral<"approval_expired">;
278
+ approval_id: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
279
+ }, z.core.$strip>, z.ZodObject<{
280
+ schema_version: z.ZodLiteral<"0.1.0">;
281
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
282
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
283
+ occurred_at: z.ZodString;
284
+ source: z.ZodString;
285
+ type: z.ZodLiteral<"command_executed">;
286
+ command: z.ZodString;
287
+ args: z.ZodArray<z.ZodString>;
288
+ cwd: z.ZodString;
289
+ exit_code: z.ZodNullable<z.ZodNumber>;
290
+ signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
291
+ received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
292
+ duration_ms: z.ZodNumber;
293
+ }, z.core.$strip>, z.ZodObject<{
294
+ schema_version: z.ZodLiteral<"0.1.0">;
295
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
296
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
297
+ occurred_at: z.ZodString;
298
+ source: z.ZodString;
299
+ type: z.ZodLiteral<"git_snapshot">;
300
+ head: z.ZodString;
301
+ branch: z.ZodString;
302
+ dirty: z.ZodBoolean;
303
+ staged: z.ZodArray<z.ZodString>;
304
+ unstaged: z.ZodArray<z.ZodString>;
305
+ untracked: z.ZodArray<z.ZodString>;
306
+ ahead: z.ZodOptional<z.ZodNumber>;
307
+ behind: z.ZodOptional<z.ZodNumber>;
308
+ }, z.core.$strip>, z.ZodObject<{
309
+ schema_version: z.ZodLiteral<"0.1.0">;
310
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
311
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
312
+ occurred_at: z.ZodString;
313
+ source: z.ZodString;
314
+ type: z.ZodLiteral<"file_changed">;
315
+ path: z.ZodString;
316
+ change_type: z.ZodEnum<{
317
+ added: "added";
318
+ modified: "modified";
319
+ deleted: "deleted";
320
+ renamed: "renamed";
321
+ }>;
322
+ old_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
323
+ }, z.core.$strip>, z.ZodObject<{
324
+ schema_version: z.ZodLiteral<"0.1.0">;
325
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
326
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
327
+ occurred_at: z.ZodString;
328
+ source: z.ZodString;
329
+ type: z.ZodLiteral<"decision_recorded">;
330
+ decision_id: z.ZodString & z.ZodType<`decision_${string}`, string, z.core.$ZodTypeInternals<`decision_${string}`, string>>;
291
331
  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";
332
+ rationale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
333
+ alternatives: z.ZodOptional<z.ZodArray<z.ZodString>>;
334
+ rejected_reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
335
+ linked_events: z.ZodOptional<z.ZodArray<z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>>>;
336
+ linked_files: z.ZodOptional<z.ZodArray<z.ZodString>>;
337
+ }, z.core.$strip>, z.ZodObject<{
338
+ schema_version: z.ZodLiteral<"0.1.0">;
339
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
340
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
341
+ occurred_at: z.ZodString;
342
+ source: z.ZodString;
343
+ type: z.ZodLiteral<"task_created">;
344
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
345
+ title: z.ZodString;
346
+ }, z.core.$strip>, z.ZodObject<{
347
+ schema_version: z.ZodLiteral<"0.1.0">;
348
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
349
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
350
+ occurred_at: z.ZodString;
351
+ source: z.ZodString;
352
+ type: z.ZodLiteral<"task_status_changed">;
353
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
354
+ from: z.ZodString;
355
+ to: z.ZodString;
356
+ }, z.core.$strip>, z.ZodObject<{
357
+ schema_version: z.ZodLiteral<"0.1.0">;
358
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
359
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
360
+ occurred_at: z.ZodString;
361
+ source: z.ZodString;
362
+ type: z.ZodLiteral<"task_reconciled">;
363
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
364
+ removed_created_in_session: z.ZodDefault<z.ZodNullable<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
365
+ created_in_session_replacement: z.ZodDefault<z.ZodNullable<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
366
+ removed_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
367
+ }, z.core.$strict>, z.ZodObject<{
368
+ schema_version: z.ZodLiteral<"0.1.0">;
369
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
370
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
371
+ occurred_at: z.ZodString;
372
+ source: z.ZodString;
373
+ type: z.ZodLiteral<"task_linkage_refreshed">;
374
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
375
+ added_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
376
+ removed_linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
377
+ final_count: z.ZodOptional<z.ZodNumber>;
378
+ }, z.core.$strict>, z.ZodObject<{
379
+ schema_version: z.ZodLiteral<"0.1.0">;
380
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
381
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
382
+ occurred_at: z.ZodString;
383
+ source: z.ZodString;
384
+ type: z.ZodLiteral<"task_deleted">;
385
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
386
+ title: z.ZodString;
387
+ }, z.core.$strict>, z.ZodObject<{
388
+ schema_version: z.ZodLiteral<"0.1.0">;
389
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
390
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
391
+ occurred_at: z.ZodString;
392
+ source: z.ZodString;
393
+ type: z.ZodLiteral<"task_archived">;
394
+ task_id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
395
+ title: z.ZodString;
396
+ }, z.core.$strict>, z.ZodObject<{
397
+ schema_version: z.ZodLiteral<"0.1.0">;
398
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
399
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
400
+ occurred_at: z.ZodString;
401
+ source: z.ZodString;
402
+ type: z.ZodLiteral<"note_added">;
403
+ body: z.ZodString;
404
+ }, z.core.$strip>, z.ZodObject<{
405
+ schema_version: z.ZodLiteral<"0.1.0">;
406
+ id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
407
+ session_id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
408
+ occurred_at: z.ZodString;
409
+ source: z.ZodString;
410
+ type: z.ZodLiteral<"adapter_output">;
411
+ stream: z.ZodEnum<{
412
+ stdout: "stdout";
413
+ stderr: "stderr";
298
414
  }>;
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>;
415
+ summary: z.ZodString;
416
+ raw_ref: z.ZodString;
417
+ redacted: z.ZodOptional<z.ZodBoolean>;
418
+ }, z.core.$strict>], "type">>;
419
+ }, z.core.$strict>;
420
+ /** Inferred runtime type for {@link SessionImportPayloadSchema}. */
421
+ type SessionImportPayload = z.infer<typeof SessionImportPayloadSchema>;
422
+ /** Inferred runtime type for {@link SessionInnerImportSchema}. */
423
+ type SessionInnerImportInput = z.infer<typeof SessionInnerImportSchema>;
308
424
 
309
425
  /**
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.
426
+ * The `source` string stamped on every event derived from a Claude Code
427
+ * native transcript, and the matching session `source.kind`.
313
428
  */
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>;
429
+ declare const CLAUDE_IMPORT_SOURCE = "claude-code-import";
322
430
  /**
323
- * Schema for `.basou/approvals/{pending,resolved}/<approval_id>.yaml`.
431
+ * One parsed line of a Claude Code native transcript
432
+ * (`~/.claude/projects/<encoded-cwd>/<uuid>.jsonl`). The shape is the
433
+ * vendor's internal message log, not Basou's event schema, so every field
434
+ * is read defensively — unknown record types and missing fields are skipped
435
+ * rather than rejected (transcripts are an undocumented format that may gain
436
+ * fields between Claude Code releases).
437
+ */
438
+ type ClaudeTranscriptRecord = Record<string, unknown>;
439
+ /** Options for {@link claudeTranscriptToImportPayload}. */
440
+ type ClaudeTranscriptToPayloadOptions = {
441
+ /** Workspace id of the target Basou workspace (from its manifest). */
442
+ workspaceId: Manifest["workspace"]["id"];
443
+ /**
444
+ * Claude Code session id (transcript filename / `sessionId`). Stored as
445
+ * `session.source.external_id` so re-imports can be deduplicated. Falls
446
+ * back to the `sessionId` read from the records when omitted.
447
+ */
448
+ externalId?: string;
449
+ };
450
+ /**
451
+ * Transform a Claude Code native transcript into a Basou
452
+ * {@link SessionImportPayload}, ready to hand to `importSessionFromJson`.
324
453
  *
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`.
454
+ * This is a pure function: no disk or environment access. It DERIVES Basou's
455
+ * provenance-level events from the transcript's message-level records, rather
456
+ * than mapping one-to-one:
331
457
  *
332
- * The `action` field is `{ kind: string }` with `passthrough()` so that
333
- * adapter-defined keys (e.g. `command`, `path`, `target_url`) survive the
458
+ * - `session_started` / `session_ended` from the first / last timestamped record.
459
+ * - `command_executed` from each `Bash` tool use, recorded as `bash -c "<cmd>"`
460
+ * (the transcript carries the shell line, not a parsed argv).
461
+ * - `file_changed` from each `Edit` / `Write` / `NotebookEdit` tool use.
462
+ * - `decision_recorded` from each `AskUserQuestion` tool use: one decision per
463
+ * question, titled `<question> -> <chosen answer>`. The chosen answer is read
464
+ * from the paired result record's structured `toolUseResult.answers` map; a
465
+ * question with no recorded string answer is skipped.
466
+ *
467
+ * Exit codes and per-command durations are not present in the transcript, so
468
+ * `command_executed.exit_code` is `null` and `duration_ms` is `0`.
469
+ *
470
+ * Returns `null` when the transcript has no timestamped records, or no
471
+ * observable command / file / decision action — such sessions carry no
472
+ * provenance worth importing and are skipped by the caller.
473
+ *
474
+ * Event `id` / `session_id` are placeholders; `importSessionFromJson` mints
475
+ * fresh ids on the way in. They are valid-by-construction so the payload
476
+ * still passes `SessionImportPayloadSchema` validation upstream.
477
+ */
478
+ declare function claudeTranscriptToImportPayload(records: ReadonlyArray<ClaudeTranscriptRecord>, options: ClaudeTranscriptToPayloadOptions): SessionImportPayload | null;
479
+
480
+ /**
481
+ * The `source` string stamped on every event derived from an OpenAI Codex
482
+ * native rollout log, and the matching session `source.kind`.
483
+ */
484
+ declare const CODEX_IMPORT_SOURCE = "codex-import";
485
+ /**
486
+ * One parsed line of a Codex rollout log
487
+ * (`~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl`). Each line is an
488
+ * envelope `{ type, timestamp, payload }` where `payload` shape depends on
489
+ * `type`. As with the Claude importer the format is the vendor's internal
490
+ * log, not Basou's schema, so every field is read defensively — unknown
491
+ * record / payload types and missing fields are skipped rather than rejected.
492
+ */
493
+ type CodexRolloutRecord = Record<string, unknown>;
494
+ /** Options for {@link codexRolloutToImportPayload}. */
495
+ type CodexRolloutToPayloadOptions = {
496
+ /** Workspace id of the target Basou workspace (from its manifest). */
497
+ workspaceId: Manifest["workspace"]["id"];
498
+ /**
499
+ * Codex session id (`session_meta.payload.id`). Stored as
500
+ * `session.source.external_id` so re-imports can be deduplicated. Falls back
501
+ * to the id read from the rollout's `session_meta` record when omitted.
502
+ */
503
+ externalId?: string;
504
+ };
505
+ /**
506
+ * Transform a Codex native rollout log into a Basou {@link SessionImportPayload},
507
+ * ready to hand to `importSessionFromJson`.
508
+ *
509
+ * This is a pure function: no disk or environment access. It DERIVES Basou's
510
+ * provenance-level events from the rollout's message-level records:
511
+ *
512
+ * - `session_started` / `session_ended` from the first / last timestamped record.
513
+ * - `command_executed` from each `exec_command` function call, recorded as
514
+ * `bash -c "<cmd>"`. The shell line and working directory come from the
515
+ * call's JSON `arguments` (`{ cmd, workdir }`); the exit code and duration
516
+ * are parsed from the paired `function_call_output` (matched by `call_id`),
517
+ * whose text carries `Process exited with code N` and `Wall time: X seconds`.
518
+ *
519
+ * Unlike the Claude importer this derives no `file_changed`: Codex has no
520
+ * dedicated edit tool and applies edits inside `exec_command` (e.g.
521
+ * `apply_patch`), so there is no clean file-change signal to map. Decisions
522
+ * and approvals are likewise not derivable — Codex records an `approval_policy`
523
+ * (a policy, not a per-action approval) and has no structured question/answer
524
+ * record. Both are deferred.
525
+ *
526
+ * Returns `null` when the rollout has no timestamped records or no observable
527
+ * `exec_command` — such sessions carry no provenance worth importing and are
528
+ * skipped by the caller.
529
+ *
530
+ * Event `id` / `session_id` are placeholders; `importSessionFromJson` mints
531
+ * fresh ids on the way in. They are valid-by-construction so the payload still
532
+ * passes `SessionImportPayloadSchema` validation upstream.
533
+ */
534
+ declare function codexRolloutToImportPayload(records: ReadonlyArray<CodexRolloutRecord>, options: CodexRolloutToPayloadOptions): SessionImportPayload | null;
535
+
536
+ /**
537
+ * Lifecycle states of a Basou approval. The status is stored directly on
538
+ * the approval YAML (flat shape) so that pending → resolved transitions
539
+ * are atomic-move + in-place rewrites rather than schema-variant swaps.
540
+ */
541
+ declare const ApprovalStatusSchema: z.ZodEnum<{
542
+ pending: "pending";
543
+ approved: "approved";
544
+ rejected: "rejected";
545
+ expired: "expired";
546
+ }>;
547
+ /** Inferred runtime type for {@link ApprovalStatusSchema}. */
548
+ type ApprovalStatus = z.infer<typeof ApprovalStatusSchema>;
549
+ /**
550
+ * Schema for `.basou/approvals/{pending,resolved}/<approval_id>.yaml`.
551
+ *
552
+ * The schema is intentionally flat (one shape regardless of `status`) so
553
+ * that pending and resolved YAMLs share the same parser. Required vs.
554
+ * optional semantics by status (e.g. `rejection_reason` MUST be set when
555
+ * `status === "rejected"`) are enforced at the CLI orchestration layer
556
+ * rather than here, mirroring the approval event variants in
557
+ * `event.schema.ts`.
558
+ *
559
+ * The `action` field is `{ kind: string }` with `passthrough()` so that
560
+ * adapter-defined keys (e.g. `command`, `path`, `target_url`) survive the
334
561
  * round-trip without being stripped — matching the approval_requested
335
562
  * event variant.
336
563
  */
@@ -364,6 +591,107 @@ declare const ApprovalSchema: z.ZodObject<{
364
591
  /** Inferred runtime type for {@link ApprovalSchema}. */
365
592
  type Approval = z.infer<typeof ApprovalSchema>;
366
593
 
594
+ /**
595
+ * Absolute paths to the standard `.basou/` directory layout, derived from a
596
+ * given repository root. The shape mirrors the canonical `.basou/` tree
597
+ * (see `docs/spec/workspace.md`). `root` is the `.basou/` directory itself
598
+ * (i.e. `repositoryRoot/.basou`).
599
+ *
600
+ * `files` exposes the well-known top-level files inside `.basou/`. Each path
601
+ * is computed but not created — they are written by their respective
602
+ * subsystems (e.g. `writeManifest` for `manifest.yaml`).
603
+ *
604
+ * All fields are deeply readonly; consumers must not mutate the returned
605
+ * object.
606
+ */
607
+ type BasouPaths = {
608
+ readonly root: string;
609
+ readonly sessions: string;
610
+ readonly tasks: string;
611
+ readonly approvals: {
612
+ readonly pending: string;
613
+ readonly resolved: string;
614
+ };
615
+ readonly locks: string;
616
+ readonly logs: string;
617
+ readonly raw: string;
618
+ readonly tmp: string;
619
+ readonly files: {
620
+ readonly manifest: string;
621
+ readonly status: string;
622
+ readonly handoff: string;
623
+ readonly decisions: string;
624
+ };
625
+ };
626
+ /**
627
+ * Compute absolute paths to the standard `.basou/` directory layout under
628
+ * `repositoryRoot`. Pure: performs no I/O and is safe to call before the
629
+ * directory exists.
630
+ *
631
+ * @param repositoryRoot Absolute path to the git repository root (the
632
+ * parent directory of `.basou/`). Caller is responsible for resolving
633
+ * `process.cwd()` or running `git rev-parse --show-toplevel` upstream;
634
+ * this function does not validate that the path exists or is a git
635
+ * repository.
636
+ */
637
+ declare function basouPaths(repositoryRoot: string): BasouPaths;
638
+ /**
639
+ * Create the standard `.basou/` directory layout under `repositoryRoot`.
640
+ *
641
+ * Idempotent: a no-op on an already-initialized layout. Returns the resolved
642
+ * {@link BasouPaths} so callers can immediately use them.
643
+ *
644
+ * Throws if `repositoryRoot/.basou` (or any required subdirectory) exists
645
+ * but is not a directory, or if filesystem permissions prevent creation.
646
+ * All thrown error messages are pathless; the original native error is
647
+ * attached as `cause` for diagnostics.
648
+ *
649
+ * @param repositoryRoot Absolute path to the git repository root. See
650
+ * {@link basouPaths} for the contract on this parameter.
651
+ */
652
+ declare function ensureBasouDirectory(repositoryRoot: string): Promise<BasouPaths>;
653
+
654
+ /** Which side of `.basou/approvals/` an approval YAML lives on. */
655
+ type ApprovalLocation = "pending" | "resolved";
656
+ /** Result returned by {@link loadApproval}: the parsed approval and where it was found. */
657
+ type LoadedApproval = {
658
+ approval: Approval;
659
+ location: ApprovalLocation;
660
+ };
661
+ /**
662
+ * Locate and load the approval YAML for `approvalId`. Searches resolved
663
+ * first so that a duplicated YAML (the crash-window scenario where both
664
+ * pending and resolved exist for the same id) returns the resolved-side
665
+ * record — matching the dedupe rule used by `approval list` and
666
+ * `resolveApprovalId`. Returns null if neither directory contains the
667
+ * YAML. Throws with a pathless message on read or schema-validation
668
+ * failure.
669
+ */
670
+ declare function loadApproval(paths: BasouPaths, approvalId: string): Promise<LoadedApproval | null>;
671
+ /**
672
+ * Enumerate approval IDs by inspecting `<id>.yaml` filenames in pending
673
+ * and resolved. ENOENT on either directory is treated as empty (e.g. a
674
+ * workspace that has no resolved approvals yet). YAML parse and schema
675
+ * validation are NOT performed; callers that need the parsed approval
676
+ * should use {@link loadApproval} per ID.
677
+ */
678
+ declare function enumerateApprovals(paths: BasouPaths): Promise<{
679
+ pending: string[];
680
+ resolved: string[];
681
+ }>;
682
+ /**
683
+ * Return true when an approval is in `pending` state and its `expires_at`
684
+ * timestamp has elapsed. Used by `basou approval list` / `show` to surface
685
+ * a `(expired)` label without mutating the YAML file. Approval expiry uses
686
+ * lazy-evaluation semantics; actual `approval_expired` event firing is
687
+ * deferred to a later step.
688
+ *
689
+ * `now` is taken as a parameter so a single CLI invocation can share one
690
+ * "now" across every record it inspects (avoids boundary races where two
691
+ * reads of `Date.now()` straddle an expiry instant).
692
+ */
693
+ declare function isLazyExpired(approval: Approval, now: Date): boolean;
694
+
367
695
  declare const SessionStartedEventSchema: z.ZodObject<{
368
696
  schema_version: z.ZodLiteral<"0.1.0">;
369
697
  id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
@@ -846,602 +1174,11 @@ type TaskLinkageRefreshedEvent = z.infer<typeof TaskLinkageRefreshedEventSchema>
846
1174
  /** Narrowed runtime type for the `task_deleted` event variant (.strict()). */
847
1175
  type TaskDeletedEvent = z.infer<typeof TaskDeletedEventSchema>;
848
1176
  /** Narrowed runtime type for the `task_archived` event variant (.strict()). */
849
- type TaskArchivedEvent = z.infer<typeof TaskArchivedEventSchema>;
850
- /** Narrowed runtime type for the `note_added` event variant. */
851
- type NoteAddedEvent = z.infer<typeof NoteAddedEventSchema>;
852
- /** Narrowed runtime type for the `adapter_output` event variant (.strict()). */
853
- type AdapterOutputEvent = z.infer<typeof AdapterOutputEventSchema>;
854
-
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
- /**
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.
895
- */
896
- declare const SessionImportPayloadSchema: z.ZodObject<{
897
- schema_version: z.ZodString;
898
- session: z.ZodObject<{
899
- id: z.ZodOptional<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>;
900
- label: z.ZodOptional<z.ZodString>;
901
- task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
902
- workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
903
- source: z.ZodObject<{
904
- kind: z.ZodEnum<{
905
- "claude-code-adapter": "claude-code-adapter";
906
- human: "human";
907
- import: "import";
908
- terminal: "terminal";
909
- }>;
910
- version: z.ZodLiteral<"0.1.0">;
911
- }, z.core.$strip>;
912
- started_at: z.ZodString;
913
- ended_at: z.ZodOptional<z.ZodString>;
914
- status: z.ZodEnum<{
915
- initialized: "initialized";
916
- running: "running";
917
- waiting_approval: "waiting_approval";
918
- completed: "completed";
919
- failed: "failed";
920
- interrupted: "interrupted";
921
- imported: "imported";
922
- archived: "archived";
923
- }>;
924
- working_directory: z.ZodString;
925
- invocation: z.ZodObject<{
926
- command: z.ZodString;
927
- args: z.ZodArray<z.ZodString>;
928
- exit_code: z.ZodNullable<z.ZodNumber>;
929
- }, z.core.$strip>;
930
- related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
931
- events_log: z.ZodOptional<z.ZodString>;
932
- 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>;
1151
-
1152
- /**
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`).
1161
- *
1162
- * All fields are deeply readonly; consumers must not mutate the returned
1163
- * object.
1164
- */
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
- };
1183
- };
1184
- /**
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.
1188
- *
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.
1194
- */
1195
- declare function basouPaths(repositoryRoot: string): BasouPaths;
1196
- /**
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.
1201
- *
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.
1206
- *
1207
- * @param repositoryRoot Absolute path to the git repository root. See
1208
- * {@link basouPaths} for the contract on this parameter.
1209
- */
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
- };
1219
- /**
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.
1227
- */
1228
- declare function loadApproval(paths: BasouPaths, approvalId: string): Promise<LoadedApproval | null>;
1229
- /**
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.
1235
- */
1236
- declare function enumerateApprovals(paths: BasouPaths): Promise<{
1237
- pending: string[];
1238
- resolved: string[];
1239
- }>;
1240
- /**
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.
1246
- *
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).
1250
- */
1251
- declare function isLazyExpired(approval: Approval, now: Date): boolean;
1252
-
1253
- /**
1254
- * Read a YAML file as `unknown`. Caller MUST validate via a zod schema.
1255
- *
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.
1266
- */
1267
- declare function writeYamlFile(filePath: string, value: unknown): Promise<void>;
1268
- /**
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.
1272
- *
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.
1276
- *
1277
- * Throws `Error("Failed to write YAML file", { cause })` on failure; if
1278
- * `cause.code === "EEXIST"` the caller can detect a target-exists race.
1279
- */
1280
- declare function linkYamlFile(filePath: string, value: unknown): Promise<void>;
1281
- /**
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).
1286
- */
1287
- declare function overwriteYamlFile(filePath: string, value: unknown): Promise<void>;
1288
-
1289
- /**
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`.
1308
- */
1309
- declare function createManifest(input: CreateManifestInput): Manifest;
1310
- /**
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`.
1315
- */
1316
- declare function writeManifest(paths: BasouPaths, manifest: Manifest, options?: {
1317
- force?: boolean;
1318
- }): Promise<void>;
1319
- /**
1320
- * Read and parse a Manifest from `paths.files.manifest`. Throws if the file
1321
- * is missing or contents fail `ManifestSchema` validation.
1322
- */
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;
1328
- };
1329
- /**
1330
- * Append Basou's default `.gitignore` block to `repositoryRoot/.gitignore`.
1331
- *
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.
1335
- *
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.
1343
- *
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`.
1347
- */
1348
- declare function appendBasouGitignore(repositoryRoot: string): Promise<AppendBasouGitignoreResult>;
1349
-
1350
- /**
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.
1355
- */
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
- };
1365
- /**
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.
1370
- *
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.
1381
- *
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.
1385
- */
1386
- declare function acquireLock(paths: BasouPaths, scope: LockScope, resourceId: string): Promise<LockHandle>;
1387
-
1388
- /**
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.
1393
- */
1394
- declare function findErrorCode(error: unknown, code: string, depth?: number): boolean;
1395
-
1396
- /**
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.
1401
- *
1402
- * If `.basou` is absent the underlying ENOENT is propagated (wrapped) so
1403
- * callers can map it to "workspace not initialized" via `findErrorCode`.
1404
- *
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.
1409
- */
1410
- declare function assertBasouRootSafe(rootPath: string): Promise<void>;
1411
- /**
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.
1416
- *
1417
- * @param input.now Override for testing; defaults to `new Date()`.
1418
- */
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>;
1438
- /**
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`.
1443
- */
1444
- declare function readStatus(paths: BasouPaths): Promise<StatusSnapshot>;
1177
+ type TaskArchivedEvent = z.infer<typeof TaskArchivedEventSchema>;
1178
+ /** Narrowed runtime type for the `note_added` event variant. */
1179
+ type NoteAddedEvent = z.infer<typeof NoteAddedEventSchema>;
1180
+ /** Narrowed runtime type for the `adapter_output` event variant (.strict()). */
1181
+ type AdapterOutputEvent = z.infer<typeof AdapterOutputEventSchema>;
1445
1182
 
1446
1183
  /**
1447
1184
  * Recoverable warning surfaced via {@link ReplayOptions.onWarning}. The replay
@@ -1499,6 +1236,137 @@ declare function replayEvents(sessionDir: string, options?: ReplayOptions): Asyn
1499
1236
  */
1500
1237
  declare function readAllEvents(sessionDir: string, options?: ReplayOptions): Promise<Event[]>;
1501
1238
 
1239
+ /** Session lifecycle states. */
1240
+ declare const SessionStatusSchema: z.ZodEnum<{
1241
+ initialized: "initialized";
1242
+ running: "running";
1243
+ waiting_approval: "waiting_approval";
1244
+ completed: "completed";
1245
+ failed: "failed";
1246
+ interrupted: "interrupted";
1247
+ imported: "imported";
1248
+ archived: "archived";
1249
+ }>;
1250
+ /** Inferred runtime type for {@link SessionStatusSchema}. */
1251
+ type SessionStatus = z.infer<typeof SessionStatusSchema>;
1252
+ /**
1253
+ * Source kind that produced the session.
1254
+ *
1255
+ * - `claude-code-adapter` — a live `basou run claude-code` process wrap.
1256
+ * - `claude-code-import` — derived after the fact from a Claude Code native
1257
+ * transcript (`~/.claude/projects/*.jsonl`) by `basou import claude-code`.
1258
+ * - `codex-import` — derived after the fact from an OpenAI Codex native
1259
+ * rollout log (date-partitioned `~/.codex/sessions`) by `basou import codex`.
1260
+ * - `import` — a round-trip of a Basou-format export (`basou session import`).
1261
+ * - `human` / `terminal` — manually-authored / terminal-recorded sessions.
1262
+ */
1263
+ declare const SessionSourceKindSchema: z.ZodEnum<{
1264
+ "claude-code-adapter": "claude-code-adapter";
1265
+ "claude-code-import": "claude-code-import";
1266
+ "codex-import": "codex-import";
1267
+ human: "human";
1268
+ import: "import";
1269
+ terminal: "terminal";
1270
+ }>;
1271
+ /** Inferred runtime type for {@link SessionSourceKindSchema}. */
1272
+ type SessionSourceKind = z.infer<typeof SessionSourceKindSchema>;
1273
+ /**
1274
+ * Optional per-session metrics, computed at import time from the source tool's
1275
+ * native log. Two groups, both optional because not every source records them:
1276
+ *
1277
+ * - Model-usage rollup (`*_tokens`): the transcript carries per-message token
1278
+ * usage; these are the session totals. `reasoning_output_tokens` is
1279
+ * Codex-only, and live `run`/`exec` sessions carry no token usage at all.
1280
+ * - Engaged-time metrics (`active_*`): the billing-oriented active time derived
1281
+ * from the session's genuine engagement timestamps (conversation turns plus
1282
+ * action events), with idle gaps capped. `active_intervals` are the merged
1283
+ * wall-clock ranges (so cross-session totals can de-duplicate overlapping
1284
+ * work by interval union); `active_time_ms` is their summed duration;
1285
+ * `active_gap_cap_ms` and `active_time_method` lock the methodology so the
1286
+ * stored numbers stay interpretable if the method changes later.
1287
+ *
1288
+ * Absent on sessions imported before a given field existed (re-import to
1289
+ * backfill). Live sessions carry no engaged-time metrics and fall back to
1290
+ * event-derived active time at stats time.
1291
+ */
1292
+ declare const SessionMetricsSchema: z.ZodObject<{
1293
+ output_tokens: z.ZodOptional<z.ZodNumber>;
1294
+ input_tokens: z.ZodOptional<z.ZodNumber>;
1295
+ cached_input_tokens: z.ZodOptional<z.ZodNumber>;
1296
+ reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
1297
+ active_time_ms: z.ZodOptional<z.ZodNumber>;
1298
+ active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
1299
+ start: z.ZodString;
1300
+ end: z.ZodString;
1301
+ }, z.core.$strip>>>;
1302
+ active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
1303
+ active_time_method: z.ZodOptional<z.ZodString>;
1304
+ }, z.core.$strip>;
1305
+ /** Inferred runtime type for {@link SessionMetricsSchema}. */
1306
+ type SessionMetrics = z.infer<typeof SessionMetricsSchema>;
1307
+ /**
1308
+ * Schema for `.basou/sessions/<session_id>/session.yaml`. The minimal
1309
+ * session document carries the actual fields nested under the outer
1310
+ * `session:` key.
1311
+ */
1312
+ declare const SessionSchema: z.ZodObject<{
1313
+ schema_version: z.ZodLiteral<"0.1.0">;
1314
+ session: z.ZodObject<{
1315
+ id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1316
+ label: z.ZodOptional<z.ZodString>;
1317
+ task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
1318
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
1319
+ source: z.ZodObject<{
1320
+ kind: z.ZodEnum<{
1321
+ "claude-code-adapter": "claude-code-adapter";
1322
+ "claude-code-import": "claude-code-import";
1323
+ "codex-import": "codex-import";
1324
+ human: "human";
1325
+ import: "import";
1326
+ terminal: "terminal";
1327
+ }>;
1328
+ version: z.ZodLiteral<"0.1.0">;
1329
+ external_id: z.ZodOptional<z.ZodString>;
1330
+ }, z.core.$strip>;
1331
+ started_at: z.ZodString;
1332
+ ended_at: z.ZodOptional<z.ZodString>;
1333
+ status: z.ZodEnum<{
1334
+ initialized: "initialized";
1335
+ running: "running";
1336
+ waiting_approval: "waiting_approval";
1337
+ completed: "completed";
1338
+ failed: "failed";
1339
+ interrupted: "interrupted";
1340
+ imported: "imported";
1341
+ archived: "archived";
1342
+ }>;
1343
+ working_directory: z.ZodString;
1344
+ invocation: z.ZodObject<{
1345
+ command: z.ZodString;
1346
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
1347
+ exit_code: z.ZodNullable<z.ZodNumber>;
1348
+ }, z.core.$strip>;
1349
+ related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
1350
+ events_log: z.ZodDefault<z.ZodString>;
1351
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1352
+ metrics: z.ZodOptional<z.ZodObject<{
1353
+ output_tokens: z.ZodOptional<z.ZodNumber>;
1354
+ input_tokens: z.ZodOptional<z.ZodNumber>;
1355
+ cached_input_tokens: z.ZodOptional<z.ZodNumber>;
1356
+ reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
1357
+ active_time_ms: z.ZodOptional<z.ZodNumber>;
1358
+ active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
1359
+ start: z.ZodString;
1360
+ end: z.ZodString;
1361
+ }, z.core.$strip>>>;
1362
+ active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
1363
+ active_time_method: z.ZodOptional<z.ZodString>;
1364
+ }, z.core.$strip>>;
1365
+ }, z.core.$strip>;
1366
+ }, z.core.$strip>;
1367
+ /** Inferred runtime type for {@link SessionSchema}. */
1368
+ type Session = z.infer<typeof SessionSchema>;
1369
+
1502
1370
  /**
1503
1371
  * Threshold above which a still-`running` session with no `session_ended`
1504
1372
  * event is flagged suspect.
@@ -1595,133 +1463,290 @@ declare function classifySuspect(paths: BasouPaths, sessionId: string, session:
1595
1463
  */
1596
1464
  declare function loadSessionEntries(paths: BasouPaths, options: LoadSessionEntriesOptions): Promise<SessionEntry[]>;
1597
1465
 
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 -->";
1466
+ type DecisionsRendererInput = {
1467
+ paths: BasouPaths;
1468
+ nowIso: string;
1469
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
1470
+ onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
1471
+ };
1472
+ type DecisionsRendererResult = {
1473
+ /** Generated body WITHOUT BASOU:GENERATED markers. */
1474
+ body: string;
1475
+ decisionCount: number;
1476
+ };
1602
1477
  /**
1603
- * Result of parsing a markdown body for the BASOU:GENERATED marker region.
1478
+ * Render the body of `decisions.md` from `decision_recorded` events across
1479
+ * every healthy session in the workspace.
1480
+ *
1481
+ * Session enumeration goes through {@link loadSessionEntries} (the same path
1482
+ * the handoff renderer uses) so that `session.yaml`-broken sessions are
1483
+ * skipped in BOTH outputs and the handoff's `decisionCount` summary stays
1484
+ * consistent with the number of sections rendered here.
1485
+ *
1486
+ * Order: `occurred_at` ascending with `decisionId` (= ULID) as tie-breaker.
1487
+ * Both fields are monotonic, so the result is a stable cross-session
1488
+ * timeline.
1489
+ *
1490
+ * The decision rich fields (rationale / alternatives / rejected_reason /
1491
+ * linked_events / linked_files) are rendered when the event carries them.
1492
+ * `linked_events` and `linked_files` are OPAQUE references: the schema only
1493
+ * validates the SHAPE, not existence — references that cannot be resolved
1494
+ * to a known event id or an existing file on disk are surfaced inline as
1495
+ * `(missing)` so cross-workspace round-trips never reject parse-time.
1496
+ */
1497
+ declare function renderDecisions(input: DecisionsRendererInput): Promise<DecisionsRendererResult>;
1498
+
1499
+ /**
1500
+ * Append a single Basou event to `<sessionDir>/events.jsonl`.
1501
+ *
1502
+ * The event is validated against the discriminated union {@link EventSchema}
1503
+ * before being serialized as a single JSONL line (UTF-8, terminated by `\n`).
1504
+ * Validation enforces the per-variant contract (required fields, source
1505
+ * vocabulary, strict variants such as `adapter_output`).
1506
+ *
1507
+ * Atomicity: writes go through `appendFile` which uses `O_APPEND`. Lines up
1508
+ * to `PIPE_BUF` bytes (Linux 4096 / macOS 512) are written atomically by the
1509
+ * kernel; longer lines may interleave with concurrent writers and are not
1510
+ * recovered here. v0.1 assumes a single writer per session, so partial-line
1511
+ * recovery is delegated to the read side (event replay) when introduced.
1512
+ *
1513
+ * Throws if validation fails or the underlying append errors. The thrown
1514
+ * Error message is pathless; the original error is attached as `cause`.
1515
+ *
1516
+ * @param sessionDir absolute path to `.basou/sessions/<session_id>/`
1517
+ * @param event unknown payload to validate and append
1518
+ */
1519
+ declare function appendEvent(sessionDir: string, event: unknown): Promise<void>;
1520
+ /**
1521
+ * Write `events.jsonl` in one atomic tmp+rename pass via {@link atomicReplace},
1522
+ * validating every event against {@link EventSchema} before any disk I/O so
1523
+ * a payload that fails validation never leaves a partial file behind.
1524
+ *
1525
+ * The helper is used by the round-trip importer (`session-import.ts`) and the
1526
+ * ad-hoc session orchestrator (`ad-hoc-session.ts`) where a small, fixed batch
1527
+ * of events must land together or not at all. Zero events produces a
1528
+ * zero-byte file so the session_yaml `events_log` pointer remains valid.
1529
+ *
1530
+ * Throws `"Invalid Basou event payload"` (same fixed message as
1531
+ * {@link appendEvent}) on validation failure, or `"Failed to write
1532
+ * events.jsonl"` on a disk I/O failure. The original native error is attached
1533
+ * as `cause`.
1534
+ */
1535
+ declare function writeEventsBulk(sessionDir: string, events: Event[]): Promise<void>;
1536
+
1537
+ /**
1538
+ * Status classification used by the `file_changed` event schema. Limited to
1539
+ * the four classes that simple-git's `git diff --name-status` reliably
1540
+ * surfaces; copy / unmerged / typechange entries are intentionally dropped
1541
+ * to keep the event payload shape narrow.
1542
+ */
1543
+ type FileChangeStatus = "added" | "modified" | "deleted" | "renamed";
1544
+ /**
1545
+ * Single file-level change observed between two refs. `old_path` is set
1546
+ * only for `renamed` entries (the previous path of the file).
1547
+ */
1548
+ type FileChange = {
1549
+ path: string;
1550
+ old_path?: string;
1551
+ status: FileChangeStatus;
1552
+ };
1553
+ /**
1554
+ * Result of {@link getDiff}. The `changed_files` array is in git's natural
1555
+ * `--name-status` order; callers requiring deterministic ordering should
1556
+ * sort by `path` themselves.
1557
+ */
1558
+ type DiffResult = {
1559
+ changed_files: FileChange[];
1560
+ };
1561
+ /**
1562
+ * Compute the file-level diff between two git refs.
1563
+ *
1564
+ * Returns a list of changed file paths classified by status (added /
1565
+ * modified / deleted / renamed). Diff content is intentionally NOT
1566
+ * returned — `file_changed` events record paths only, and raw diff bodies
1567
+ * are excluded so the trace cannot inadvertently leak source code that may
1568
+ * be sensitive. Use `git show <ref>` to obtain the underlying diff.
1569
+ *
1570
+ * Pathless contract: every thrown message is a fixed string from the set
1571
+ * {`Not a git repository`, `Git executable not found in PATH. Install git
1572
+ * first.`, `Invalid ref`, `Failed to compute git diff`}; native errors are
1573
+ * preserved on `Error.cause`.
1574
+ *
1575
+ * Special cases:
1576
+ * - `baseRef === headRef` short-circuits to an empty result
1577
+ * - copy / unmerged / typechange / unknown status codes are skipped
1578
+ *
1579
+ * @param repoRoot absolute path to the git repository root
1580
+ * @param baseRef base ref (e.g. session-start HEAD sha)
1581
+ * @param headRef head ref (e.g. session-end HEAD sha)
1582
+ */
1583
+ declare function getDiff(repoRoot: string, baseRef: string, headRef: string): Promise<DiffResult>;
1584
+
1585
+ /**
1586
+ * Payload subset of `git_snapshot` event, mechanically derived from the
1587
+ * zod-inferred event type. The wrapping event-shape fields
1588
+ * (schema_version, id, session_id, occurred_at, source, type) are added by
1589
+ * the caller (session lifecycle in later steps) when constructing the
1590
+ * event, so the schema remains the single source of truth.
1591
+ *
1592
+ * `ahead` / `behind` are omitted when there is no remote or no upstream
1593
+ * tracking; the schema declares both as optional non-negative integers.
1594
+ */
1595
+ type GitSnapshot = Omit<GitSnapshotEvent, "schema_version" | "id" | "session_id" | "occurred_at" | "source" | "type">;
1596
+ /**
1597
+ * Resolve the absolute path of the Git repository root that contains `cwd`.
1598
+ * Equivalent to `git rev-parse --show-toplevel`.
1599
+ *
1600
+ * Throws `Error("Git executable not found in PATH. Install git first.")`
1601
+ * with the spawn error attached as `cause` when git itself is missing.
1602
+ * Throws `Error("Not a git repository")` (without command-specific suffix)
1603
+ * when `cwd` is not inside a repository — callers MAY wrap with their own
1604
+ * "Run 'git init' first, then re-run 'basou XXX'." suffix.
1605
+ *
1606
+ * Pathless contract: the thrown message never embeds `cwd` or any absolute
1607
+ * path; native errors are kept on `error.cause` for verbose surfacing.
1608
+ */
1609
+ declare function resolveRepositoryRoot(cwd: string): Promise<string>;
1610
+ /**
1611
+ * Read `remote.origin.url` from the local repository config. Returns
1612
+ * `undefined` if the remote is unset, the value is empty, or the lookup
1613
+ * fails for any reason (best-effort).
1614
+ *
1615
+ * The `--local` scope is critical: callers MUST NOT pick up the developer's
1616
+ * global remote.origin.url, which could leak the wrong repository URL into
1617
+ * `manifest.yaml`.
1618
+ */
1619
+ declare function tryRemoteUrl(repositoryRoot: string): Promise<string | undefined>;
1620
+ /**
1621
+ * Build a {@link GitSnapshot} for the repository at `repositoryRoot`. The
1622
+ * caller is responsible for ensuring `repositoryRoot` is the canonical root
1623
+ * (typically obtained via {@link resolveRepositoryRoot}); this function
1624
+ * verifies repo membership via `git rev-parse --is-inside-work-tree` to
1625
+ * distinguish a non-git directory from an empty repository.
1626
+ *
1627
+ * Edge cases:
1628
+ * - **non-git directory**: throws `Error("Not a git repository")`
1629
+ * - **empty repo (no commits)**: throws `Error("No commits in repository")`
1630
+ * - **detached HEAD**: `branch = "HEAD"`, `head = commit hash`,
1631
+ * `ahead`/`behind` omitted
1632
+ * - **no remote / no upstream tracking**: `ahead`/`behind` omitted
1633
+ *
1634
+ * Pathless contract preserved on every throw path.
1635
+ */
1636
+ declare function getSnapshot(repositoryRoot: string): Promise<GitSnapshot>;
1637
+
1638
+ /**
1639
+ * Allowed ID type prefixes for Basou entities.
1604
1640
  *
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.
1641
+ * Frozen at runtime so that mutating the exported array cannot diverge from
1642
+ * the validation set used internally. The single source of truth for both
1643
+ * the `IdPrefix` type and runtime prefix checks.
1611
1644
  */
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
- };
1645
+ declare const ID_PREFIXES: readonly ["ws", "task", "ses", "evt", "appr", "decision"];
1628
1646
  /**
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`).
1647
+ * Type prefix used for Basou entity IDs.
1648
+ * Format: `<prefix>_<26-char ULID>`, e.g. `ws_01HXABCDEF1234567890ABCDEF`.
1633
1649
  */
1634
- declare function readMarkdownFile(filePath: string): Promise<string | null>;
1650
+ type IdPrefix = (typeof ID_PREFIXES)[number];
1635
1651
  /**
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.
1652
+ * A Basou entity ID as a template literal type.
1639
1653
  *
1640
- * On any failure the original error is re-thrown as
1641
- * `Error("Failed to write markdown file", { cause })` (pathless contract).
1654
+ * `PrefixedId<"ses">` narrows to ``ses_${string}`` so a session schema can
1655
+ * preserve the prefix in its inferred type beyond runtime validation.
1642
1656
  */
1643
- declare function writeMarkdownFile(filePath: string, body: string): Promise<void>;
1657
+ type PrefixedId<P extends IdPrefix = IdPrefix> = `${P}_${string}`;
1644
1658
  /**
1645
- * Parse a markdown body and identify the BASOU:GENERATED marker region.
1659
+ * Generate a Crockford Base32 ULID.
1646
1660
  *
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.
1661
+ * The result is a 26-character, lexicographically time-sortable identifier.
1662
+ * Multiple calls within the same millisecond are strictly increasing for the
1663
+ * lifetime of the current process.
1656
1664
  *
1657
- * Matching is strict: leading/trailing whitespace, BOM, and comment
1658
- * compression (`<!--BASOU:...-->`) all bypass the marker and are treated
1659
- * as legacy content.
1665
+ * NOTE: `seedTime` is forwarded to the underlying monotonic factory and is
1666
+ * NOT a deterministic seed: repeated calls with the same `seedTime` still
1667
+ * return strictly increasing values, because the factory increments its
1668
+ * internal counter on each call.
1669
+ *
1670
+ * @param seedTime Optional millisecond timestamp passed to the monotonic
1671
+ * factory. Useful for ordered generation in tests; not deterministic.
1660
1672
  */
1661
- declare function parseMarkers(content: string): MarkerSection;
1673
+ declare function ulid(seedTime?: number): string;
1662
1674
  /**
1663
- * Build the final markdown body by replacing the BASOU:GENERATED region.
1675
+ * Generate a prefixed Basou ID, e.g. `ses_01HXABCDEF1234567890ABCDEF`.
1664
1676
  *
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`.
1677
+ * The return type preserves the prefix as a template literal type so that
1678
+ * downstream zod schemas can narrow an `IdPrefix` parameter through the API.
1669
1679
  *
1670
- * The caller passes `fileLabel` (e.g. `"handoff.md"` or `"decisions.md"`)
1671
- * so the error message is informative without leaking an absolute path.
1680
+ * Throws if `prefix` is not one of {@link ID_PREFIXES}. The runtime guard
1681
+ * defends against JavaScript callers and casted TypeScript that bypass the
1682
+ * compile-time `IdPrefix` constraint.
1672
1683
  */
1673
- declare function renderWithMarkers(existing: string | null, generated: string, fileLabel: string): string;
1674
-
1684
+ declare function prefixedUlid<P extends IdPrefix>(prefix: P): PrefixedId<P>;
1675
1685
  /**
1676
- * Options for {@link importSessionFromJson}. All fields are optional.
1686
+ * Check whether the given string is a valid prefixed Basou ID.
1677
1687
  *
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.
1688
+ * Returns true only if the string has shape `<prefix>_<ULID>` where prefix is
1689
+ * one of {@link ID_PREFIXES} and the trailing 26 characters form a valid
1690
+ * Crockford Base32 ULID. Validation combines a strict shape regex (to enforce
1691
+ * the 0-7 leading char and the I/L/O/U exclusion) with the npm `ulid`
1692
+ * library's `isValid` for forward compatibility.
1693
+ *
1694
+ * NOTE: This validates the prefix is known. Schemas that require a specific
1695
+ * prefix (e.g. only `ses_*` for a session ID) must add their own narrowing.
1681
1696
  */
1682
- type ImportSessionOptions = {
1683
- labelOverride?: string;
1684
- taskIdOverride?: string;
1685
- dryRun?: boolean;
1686
- };
1697
+ declare function isValidPrefixedId(value: string): boolean;
1698
+
1687
1699
  /**
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.
1700
+ * Task lifecycle states.
1692
1701
  *
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.
1702
+ * The storage layer's `ALLOWED_TRANSITIONS` map (= source of truth in
1703
+ * `tasks.ts`) is the authoritative graph; the comment below is a snapshot.
1704
+ * `planned` reaches `done` / `cancelled` directly so tasks completed (or
1705
+ * abandoned) outside an explicit in-progress phase can close in a single
1706
+ * CLI call:
1707
+ *
1708
+ * planned → {in_progress | done | cancelled}
1709
+ * in_progress → {done | cancelled}
1710
+ * done / cancelled = terminal
1711
+ *
1712
+ * Self-edges are rejected so the audit trail stays monotonic.
1698
1713
  */
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
- };
1714
+ declare const TaskStatusSchema: z.ZodEnum<{
1715
+ planned: "planned";
1716
+ in_progress: "in_progress";
1717
+ done: "done";
1718
+ cancelled: "cancelled";
1719
+ }>;
1720
+ /** Inferred runtime type for {@link TaskStatusSchema}. */
1721
+ type TaskStatus = z.infer<typeof TaskStatusSchema>;
1709
1722
  /**
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.
1723
+ * Schema for the YAML front matter of `.basou/tasks/<task_id>.md`.
1719
1724
  *
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.
1725
+ * The markdown body after the front matter is intentionally NOT modelled
1726
+ * here it is free-form user-edited content. The storage layer splits
1727
+ * the file into `task` (this schema) and `body` (the trailing string).
1723
1728
  */
1724
- declare function importSessionFromJson(paths: BasouPaths, manifest: Manifest, payload: SessionImportPayload, options: ImportSessionOptions): Promise<ImportSessionResult>;
1729
+ declare const TaskSchema: z.ZodObject<{
1730
+ schema_version: z.ZodLiteral<"0.1.0">;
1731
+ task: z.ZodObject<{
1732
+ id: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
1733
+ title: z.ZodString;
1734
+ label: z.ZodOptional<z.ZodString>;
1735
+ status: z.ZodEnum<{
1736
+ planned: "planned";
1737
+ in_progress: "in_progress";
1738
+ done: "done";
1739
+ cancelled: "cancelled";
1740
+ }>;
1741
+ created_at: z.ZodString;
1742
+ updated_at: z.ZodString;
1743
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
1744
+ created_in_session: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
1745
+ linked_sessions: z.ZodDefault<z.ZodArray<z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>>>;
1746
+ }, z.core.$strip>;
1747
+ }, z.core.$strip>;
1748
+ /** Inferred runtime type for {@link TaskSchema}. */
1749
+ type Task = z.infer<typeof TaskSchema>;
1725
1750
 
1726
1751
  /**
1727
1752
  * Thrown when the ad-hoc session was fully written to disk (4 lifecycle
@@ -2374,17 +2399,95 @@ type ArchiveTaskResult = {
2374
2399
  * 4. Ensure the archive directory exists.
2375
2400
  * 5. Rename main/<id>.md to archive/<id>.md (= atomic on the same fs).
2376
2401
  *
2377
- * Failure modes after step 2 surface as
2378
- * {@link TaskWriteAfterEventError} with `phase: "archive"`; the operator
2379
- * is told the event is durable but the on-disk move is incomplete and
2380
- * must be resolved manually (typically by rerunning `task archive`).
2402
+ * Failure modes after step 2 surface as
2403
+ * {@link TaskWriteAfterEventError} with `phase: "archive"`; the operator
2404
+ * is told the event is durable but the on-disk move is incomplete and
2405
+ * must be resolved manually (typically by rerunning `task archive`).
2406
+ */
2407
+ declare function archiveTask(input: ArchiveTaskInput): Promise<ArchiveTaskResult>;
2408
+
2409
+ /** Input contract for {@link renderHandoff}. */
2410
+ type HandoffRendererInput = {
2411
+ paths: BasouPaths;
2412
+ /** ISO timestamp embedded in the generated body header. Caller-provided for testability. */
2413
+ nowIso: string;
2414
+ /** Forwarded to {@link replayEvents} / {@link loadSessionEntries} per session. */
2415
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2416
+ /**
2417
+ * Per-session degradation reasons (missing/invalid session.yaml or
2418
+ * unreadable events.jsonl). The CLI maps `events_jsonl_unreadable` to the
2419
+ * existing suspect-check stderr wording to keep the user-facing surface
2420
+ * consistent with `basou session list`.
2421
+ */
2422
+ onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2423
+ /**
2424
+ * Per-task degradation reasons (invalid front matter / unreadable file).
2425
+ * Surfaced so the CLI can warn the operator about a malformed task.md
2426
+ * without aborting the handoff render.
2427
+ */
2428
+ onTaskSkip?: (taskId: string, reason: TaskSkipReason) => void;
2429
+ /** Maximum related_files entries to display before `... +N more`. Default 20. */
2430
+ relatedFilesLimit?: number;
2431
+ };
2432
+ type HandoffRendererResult = {
2433
+ /** Generated body WITHOUT BASOU:GENERATED markers (markdown-store wraps them). */
2434
+ body: string;
2435
+ sessionCount: number;
2436
+ decisionCount: number;
2437
+ pendingApprovalsCount: number;
2438
+ suspectCount: number;
2439
+ /** Total number of task.md files successfully loaded. */
2440
+ taskCount: number;
2441
+ /** Tasks whose status is `planned` or `in_progress` (= shown in 次に実行すべき作業). */
2442
+ pendingTaskCount: number;
2443
+ };
2444
+ /**
2445
+ * Render the body of `handoff.md` from the current workspace state.
2446
+ *
2447
+ * The renderer is a pure function (no I/O beyond {@link replayEvents} /
2448
+ * {@link loadSessionEntries} / {@link enumerateApprovals}). It assembles the
2449
+ * the spec's `handoff.md` sections in order:
2450
+ *
2451
+ * 1. `現在の状態`: latest live session (status not archived, source not import).
2452
+ * 2. `直近の変更ファイル`: the most recent session's `related_files`, dedup +
2453
+ * sorted asc + truncated to `relatedFilesLimit` (default 20).
2454
+ * 3. `直近の判断`: latest `decision_recorded` event (chronological).
2455
+ * 4. `未決事項`: pending-approval count + suspect-session count.
2456
+ * 5. `次に読むべきファイル`: `.basou/decisions.md` + top-3 related files
2457
+ * (the same `displayedFiles` source is intentionally reused in two
2458
+ * sections — overview vs. resume context).
2459
+ * 6. `次に実行すべき作業`: placeholder until task events land.
2460
+ * 7. `セッション一覧`: all sessions newest first with inline suspect labels.
2461
+ *
2462
+ * Session enumeration goes through {@link loadSessionEntries} so the set of
2463
+ * sessions whose `decision_recorded` events we replay matches the
2464
+ * decisions renderer.
2465
+ */
2466
+ declare function renderHandoff(input: HandoffRendererInput): Promise<HandoffRendererResult>;
2467
+
2468
+ /**
2469
+ * Parse a unit-suffixed duration string (e.g. `30s`, `5m`, `1h`, `100ms`)
2470
+ * into milliseconds.
2471
+ *
2472
+ * Rejects formats that cannot represent a positive, finite millisecond
2473
+ * value: malformed inputs, zero, leading-zero values, and computations that
2474
+ * overflow to `Infinity`. The returned number is always a positive integer.
2475
+ *
2476
+ * Supported units: `ms` (milliseconds), `s` (seconds), `m` (minutes),
2477
+ * `h` (hours).
2478
+ *
2479
+ * @param input duration string with required unit suffix
2480
+ * @returns duration in milliseconds (positive, finite)
2481
+ * @throws Error with message
2482
+ * `Invalid duration: <input>. Expected format: <positive-integer><unit> where unit is ms/s/m/h`
2483
+ * for format errors, or `Duration overflow: <input>` for non-finite results.
2381
2484
  */
2382
- declare function archiveTask(input: ArchiveTaskInput): Promise<ArchiveTaskResult>;
2485
+ declare function parseDuration(input: string): number;
2383
2486
 
2384
2487
  /**
2385
2488
  * Resolve a possibly-truncated session id prefix to a full session id by
2386
2489
  * scanning `<paths.sessions>/`. Existing message contract (carried over
2387
- * from `packages/cli/src/commands/session.ts` 経由の Step 12 実装) is
2490
+ * from `packages/cli/src/commands/session.ts`) is
2388
2491
  * preserved exactly so callers that grep stderr keep working:
2389
2492
  *
2390
2493
  * - `"Session id is empty"`
@@ -2492,98 +2595,6 @@ type SanitizeRelatedFilesResult = {
2492
2595
  */
2493
2596
  declare function sanitizeRelatedFiles(paths: ReadonlyArray<string>, opts: SanitizePathOptions): SanitizeRelatedFilesResult;
2494
2597
 
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
- /**
2531
- * Render the body of `handoff.md` from the current workspace state.
2532
- *
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:
2536
- *
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.
2547
- *
2548
- * Session enumeration goes through {@link loadSessionEntries} so the set of
2549
- * sessions whose `decision_recorded` events we replay matches the
2550
- * decisions renderer.
2551
- */
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;
2559
- };
2560
- type DecisionsRendererResult = {
2561
- /** Generated body WITHOUT BASOU:GENERATED markers. */
2562
- body: string;
2563
- decisionCount: number;
2564
- };
2565
- /**
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.
2573
- *
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.
2577
- *
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.
2584
- */
2585
- declare function renderDecisions(input: DecisionsRendererInput): Promise<DecisionsRendererResult>;
2586
-
2587
2598
  /**
2588
2599
  * Internal abstraction over child-process execution.
2589
2600
  *
@@ -2622,283 +2633,669 @@ type RunOptions = {
2622
2633
  */
2623
2634
  readonly env?: NodeJS.ProcessEnv;
2624
2635
  /**
2625
- * External cancellation. Aborting the signal triggers a two-stage
2626
- * kill (SIGTERM, then SIGKILL after a short grace period).
2636
+ * External cancellation. Aborting the signal triggers a two-stage
2637
+ * kill (SIGTERM, then SIGKILL after a short grace period).
2638
+ */
2639
+ readonly signal?: AbortSignal;
2640
+ /**
2641
+ * Internal timeout in milliseconds. Must be a positive finite number.
2642
+ * Triggers the same two-stage kill as `signal`.
2643
+ */
2644
+ readonly timeout_ms?: number;
2645
+ /**
2646
+ * Optional input written to the child's stdin. The pipe is closed
2647
+ * after the value is written. Incompatible with `capture: "none"`.
2648
+ */
2649
+ readonly stdin?: string | Buffer;
2650
+ /**
2651
+ * Output capture mode. Defaults to `"buffer"`. See {@link CaptureMode}.
2652
+ */
2653
+ readonly capture?: CaptureMode;
2654
+ /**
2655
+ * Invoked synchronously immediately after the child has been spawned,
2656
+ * before the runner waits for completion. Callers use this to retain a
2657
+ * reference for parent-side cleanup (e.g. an `exit` hook that SIGKILLs
2658
+ * the child if the parent is forcibly terminated). The runner takes no
2659
+ * action if the callback throws.
2660
+ */
2661
+ readonly onSpawn?: (child: ChildProcess) => void;
2662
+ };
2663
+ type RunResult = {
2664
+ readonly command: string;
2665
+ readonly args: readonly string[];
2666
+ readonly cwd: string;
2667
+ /** `null` when the process was killed by a signal. */
2668
+ readonly exit_code: number | null;
2669
+ readonly signal: NodeJS.Signals | null;
2670
+ readonly stdout: string;
2671
+ readonly stderr: string;
2672
+ /** ISO 8601 timestamp captured before spawn. */
2673
+ readonly started_at: string;
2674
+ /** ISO 8601 timestamp captured on the `close` event. */
2675
+ readonly ended_at: string;
2676
+ readonly duration_ms: number;
2677
+ readonly pid: number | null;
2678
+ };
2679
+ type ProcessRunner = {
2680
+ run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2681
+ };
2682
+
2683
+ /**
2684
+ * Spawn-based ProcessRunner implementation.
2685
+ *
2686
+ * Behavior:
2687
+ * - `shell: false` and `detached: false`. The process group is not
2688
+ * detached, but the OS does not guarantee the child is reaped when
2689
+ * the parent terminates abruptly; callers handle SIGINT/SIGTERM/exit
2690
+ * hooks themselves.
2691
+ * - `capture: "buffer"` (default): `stdio: ['pipe', 'pipe', 'pipe']`,
2692
+ * stdout / stderr are decoded as UTF-8 and accumulated as full
2693
+ * strings (no streaming callbacks).
2694
+ * - `capture: "none"`: `stdio: ['inherit', 'inherit', 'inherit']`, the
2695
+ * child writes directly to the parent terminal in real time and
2696
+ * `RunResult.stdout` / `stderr` are empty strings. `stdin` is
2697
+ * incompatible with this mode (the child has no writable stdin pipe)
2698
+ * and the combination is rejected before spawn.
2699
+ * - `timeout_ms` and `AbortSignal` both trigger a two-stage kill:
2700
+ * `SIGTERM`, then `SIGKILL` after `DEFAULT_KILL_GRACE_MS` (5_000 ms).
2701
+ * - A non-zero `exit_code` does not throw; it is returned via
2702
+ * `RunResult`. Spawn-time errors throw with a pathless message and
2703
+ * the original error attached as `cause`.
2704
+ *
2705
+ * Error message contract: messages never include `cwd` or absolute
2706
+ * command paths. The original errno (and any nested wrapping) is
2707
+ * preserved on `Error.cause`, allowing callers to classify with
2708
+ * `findErrorCode` when needed.
2709
+ */
2710
+ declare class ChildProcessRunner implements ProcessRunner {
2711
+ run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2712
+ }
2713
+
2714
+ /**
2715
+ * Schema version literal pinned to "0.1.0" for Basou v0.1.
2716
+ * Reused across every entity schema so inferred types narrow to the literal.
2717
+ */
2718
+ declare const SchemaVersionSchema: z.ZodLiteral<"0.1.0">;
2719
+ /**
2720
+ * ISO 8601 timestamp with explicit timezone offset (e.g. `+09:00`).
2721
+ *
2722
+ * The spec samples include offsets, so the default zod `.datetime()` (which
2723
+ * rejects offsets) is insufficient; `{ offset: true }` is required.
2724
+ */
2725
+ declare const IsoTimestampSchema: z.ZodString;
2726
+ /** Workspace ID schema: validates `ws_<26-char ULID>`. */
2727
+ declare const WorkspaceIdSchema: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2728
+ /** Task ID schema: validates `task_<26-char ULID>`. */
2729
+ declare const TaskIdSchema: z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>;
2730
+ /** Session ID schema: validates `ses_<26-char ULID>`. */
2731
+ declare const SessionIdSchema: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2732
+ /** Event ID schema: validates `evt_<26-char ULID>`. */
2733
+ declare const EventIdSchema: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
2734
+ /** Approval ID schema: validates `appr_<26-char ULID>`. */
2735
+ declare const ApprovalIdSchema: z.ZodString & z.ZodType<`appr_${string}`, string, z.core.$ZodTypeInternals<`appr_${string}`, string>>;
2736
+ /** Decision ID schema: validates `decision_<26-char ULID>`. */
2737
+ declare const DecisionIdSchema: z.ZodString & z.ZodType<`decision_${string}`, string, z.core.$ZodTypeInternals<`decision_${string}`, string>>;
2738
+ /**
2739
+ * Risk level vocabulary fixed by the spec. Adapters MUST emit one of these
2740
+ * four values; arbitrary strings are rejected at schema parse time.
2741
+ */
2742
+ declare const RiskLevelSchema: z.ZodEnum<{
2743
+ low: "low";
2744
+ medium: "medium";
2745
+ high: "high";
2746
+ critical: "critical";
2747
+ }>;
2748
+ /** Inferred runtime type for {@link RiskLevelSchema}. */
2749
+ type RiskLevel = z.infer<typeof RiskLevelSchema>;
2750
+ /**
2751
+ * Source attribution for events (e.g. "claude-code-adapter",
2752
+ * "git-capability", "terminal-recording", "local-cli", "human"). Free-form
2753
+ * non-empty string in v0.1; a stricter enum may be introduced post-v0.1.
2754
+ */
2755
+ declare const EventSourceSchema: z.ZodString;
2756
+
2757
+ /**
2758
+ * Schema for `.basou/status.json` — a forward-incompat cache of the current
2759
+ * workspace state.
2760
+ *
2761
+ * Each level uses `.strict()` so unknown keys are rejected rather than
2762
+ * silently stripped. A v0.1 reader that encounters a future-shape
2763
+ * `status.json` therefore fails parsing instead of returning a partially
2764
+ * empty snapshot; callers regenerate by calling `buildStatusSnapshot` +
2765
+ * `writeStatus` rather than trying to migrate.
2766
+ */
2767
+ declare const StatusSchema: z.ZodObject<{
2768
+ schema_version: z.ZodLiteral<"0.1.0">;
2769
+ generated_at: z.ZodString;
2770
+ workspace: z.ZodObject<{
2771
+ id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2772
+ name: z.ZodString;
2773
+ basou_version: z.ZodLiteral<"0.1.0">;
2774
+ }, z.core.$strict>;
2775
+ directories_present: z.ZodObject<{
2776
+ sessions: z.ZodBoolean;
2777
+ tasks: z.ZodBoolean;
2778
+ approvals_pending: z.ZodBoolean;
2779
+ approvals_resolved: z.ZodBoolean;
2780
+ logs: z.ZodBoolean;
2781
+ raw: z.ZodBoolean;
2782
+ tmp: z.ZodBoolean;
2783
+ }, z.core.$strict>;
2784
+ }, z.core.$strict>;
2785
+ /** Inferred runtime type for {@link StatusSchema}. */
2786
+ type StatusSnapshot = z.infer<typeof StatusSchema>;
2787
+
2788
+ /**
2789
+ * Gap longer than this between two consecutive engagement timestamps is treated
2790
+ * as idle and not credited as active time. A deliberately coarse heuristic: a
2791
+ * focus / billable-attention proxy, NOT a measure of model compute. 5 minutes.
2792
+ */
2793
+ declare const ACTIVE_GAP_CAP_MS: number;
2794
+ /** A wall-clock range expressed as ISO-8601 strings (for persistence). */
2795
+ type IsoInterval = {
2796
+ start: string;
2797
+ end: string;
2798
+ };
2799
+
2800
+ type WorkStatsInput = {
2801
+ paths: BasouPaths;
2802
+ /** Shared clock; running sessions are measured up to this instant. */
2803
+ now: Date;
2804
+ /**
2805
+ * IANA timezone used to bucket the per-day breakdown (logs are UTC, so a
2806
+ * billing day needs an explicit zone). Defaults to the host's local zone;
2807
+ * injectable for deterministic tests.
2627
2808
  */
2628
- readonly signal?: AbortSignal;
2809
+ timeZone?: string;
2810
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2811
+ onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2812
+ };
2813
+ /** Which measures are meaningful for a given session / source. */
2814
+ type MeasureAvailability = {
2815
+ /** Always true (started_at + now bound the span). */
2816
+ span: boolean;
2629
2817
  /**
2630
- * Internal timeout in milliseconds. Must be a positive finite number.
2631
- * Triggers the same two-stage kill as `signal`.
2818
+ * `commandTimeMs` reflects real shell time. False for `claude-code-import`,
2819
+ * whose transcript carries no per-command duration (recorded as 0).
2632
2820
  */
2633
- readonly timeout_ms?: number;
2821
+ commandTime: boolean;
2822
+ /** At least one active interval could be measured (stored or event-derived). */
2823
+ activeTime: boolean;
2824
+ /** Token totals were captured (model-usage metrics present). */
2825
+ tokens: boolean;
2826
+ };
2827
+ /** Token rollup. Zero when not captured; `reasoning` is Codex-only. */
2828
+ type TokenTotals = {
2829
+ output: number;
2830
+ input: number;
2831
+ cached: number;
2832
+ reasoning: number;
2833
+ };
2834
+ /** How a session's active time was derived. */
2835
+ type ActiveTimeBasis = "engaged-turns" | "events";
2836
+ type SessionWorkStats = {
2837
+ sessionId: string;
2838
+ label: string | undefined;
2839
+ status: SessionStatus;
2840
+ sourceKind: SessionSourceKind;
2841
+ startedAt: string;
2842
+ endedAt: string | undefined;
2843
+ /** ended_at absent: span is measured to `now`. */
2844
+ open: boolean;
2845
+ sessionSpanMs: number;
2846
+ commandTimeMs: number;
2847
+ activeTimeMs: number;
2634
2848
  /**
2635
- * Optional input written to the child's stdin. The pipe is closed
2636
- * after the value is written. Incompatible with `capture: "none"`.
2849
+ * How `activeTimeMs` / `activeIntervals` were derived: `engaged-turns` from
2850
+ * the engagement timestamps stored at import (captures conversation), or
2851
+ * `events` from the action-event stream (live sessions and pre-v2 imports).
2637
2852
  */
2638
- readonly stdin?: string | Buffer;
2853
+ activeTimeBasis: ActiveTimeBasis;
2639
2854
  /**
2640
- * Output capture mode. Defaults to `"buffer"`. See {@link CaptureMode}.
2855
+ * Merged active wall-clock ranges. Their summed duration equals
2856
+ * `activeTimeMs`; the aggregator unions them across sessions so overlapping
2857
+ * (concurrent) work is not double-counted in billable totals.
2641
2858
  */
2642
- readonly capture?: CaptureMode;
2859
+ activeIntervals: IsoInterval[];
2860
+ commandCount: number;
2861
+ fileChangedCount: number;
2862
+ decisionCount: number;
2863
+ eventCount: number;
2864
+ tokens: TokenTotals;
2865
+ availability: MeasureAvailability;
2866
+ /** ended_at < started_at (clock skew): span was clamped to 0. */
2867
+ spanClamped: boolean;
2868
+ /** events.jsonl could not be read: action / time counts are 0 + untrustworthy. */
2869
+ eventsUnreadable: boolean;
2870
+ };
2871
+ type SourceWorkStats = {
2872
+ sourceKind: SessionSourceKind;
2873
+ sessionCount: number;
2874
+ sessionSpanMs: number;
2875
+ commandTimeMs: number;
2876
+ activeTimeMs: number;
2877
+ commandCount: number;
2878
+ fileChangedCount: number;
2879
+ decisionCount: number;
2880
+ eventCount: number;
2881
+ tokens: TokenTotals;
2882
+ /** Every session of this kind reports real command time. */
2883
+ commandTimeReliable: boolean;
2884
+ /** At least one session of this kind captured token totals. */
2885
+ tokensAvailable: boolean;
2886
+ };
2887
+ type StatusCount = {
2888
+ status: SessionStatus;
2889
+ count: number;
2890
+ };
2891
+ /**
2892
+ * One calendar day of the time x volume billing view. `billableActiveTimeMs` is
2893
+ * the union of active intervals starting on this date (so per-day sums to the
2894
+ * de-duplicated workspace total); volume is attributed to each session's
2895
+ * `started_at` date.
2896
+ */
2897
+ type DayWorkStats = {
2898
+ /** Calendar date `YYYY-MM-DD` in the report timezone. */
2899
+ date: string;
2900
+ billableActiveTimeMs: number;
2901
+ sessionCount: number;
2902
+ commandCount: number;
2903
+ fileChangedCount: number;
2904
+ decisionCount: number;
2905
+ tokens: TokenTotals;
2906
+ };
2907
+ type WorkStatsTotals = {
2908
+ sessionCount: number;
2909
+ openSessionCount: number;
2910
+ sessionSpanMs: number;
2911
+ commandTimeMs: number;
2912
+ /** Naive sum of per-session active time; double-counts overlapping sessions. */
2913
+ activeTimeMs: number;
2643
2914
  /**
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.
2915
+ * Billable active time: the UNION of every session's active intervals, so
2916
+ * concurrent sessions do not double-count human wall-clock. Equals
2917
+ * `activeTimeMs` when no sessions overlap, and is smaller when they do.
2649
2918
  */
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;
2919
+ billableActiveTimeMs: number;
2920
+ commandCount: number;
2921
+ fileChangedCount: number;
2922
+ decisionCount: number;
2923
+ eventCount: number;
2924
+ tokens: TokenTotals;
2925
+ /** No `claude-code-import` sessions present, so command time is workspace-wide real. */
2926
+ commandTimeReliable: boolean;
2927
+ tokensAvailable: boolean;
2667
2928
  };
2668
- type ProcessRunner = {
2669
- run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2929
+ type WorkStatsResult = {
2930
+ generatedAt: string;
2931
+ /** Idle-gap cap applied to active time (methodology lock). */
2932
+ activeGapCapMs: number;
2933
+ /** IANA timezone used to bucket {@link WorkStatsResult.byDay}. */
2934
+ timeZone: string;
2935
+ totals: WorkStatsTotals;
2936
+ /** Per session, started_at ascending (loadSessionEntries order). */
2937
+ sessions: SessionWorkStats[];
2938
+ bySource: SourceWorkStats[];
2939
+ byStatus: StatusCount[];
2940
+ /** Per-day time x volume billing view, date ascending. */
2941
+ byDay: DayWorkStats[];
2670
2942
  };
2943
+ /**
2944
+ * Aggregate work + engaged-time across the workspace's sessions.
2945
+ *
2946
+ * Honesty note: this returns a LABELED SET of measures, not one number. Token
2947
+ * volume (when captured) is the most direct "how much the AI produced" signal.
2948
+ * The time measures are proxies, ordered from most to least billing-relevant:
2949
+ *
2950
+ * - `billableActiveTimeMs` (totals) is the headline for billing human harness
2951
+ * labor: the UNION of every session's active intervals, so two sessions run
2952
+ * concurrently do not bill the same wall-clock twice. `activeTimeMs` is the
2953
+ * naive sum, kept only to expose the overlap delta.
2954
+ * - Per-session active time is derived from the session's ENGAGED series. For
2955
+ * imported sessions this is the genuine engagement timestamps captured at
2956
+ * import (conversation turns plus action events), so design discussion that
2957
+ * produced few tool calls is still counted; idle gaps over `ACTIVE_GAP_CAP_MS`
2958
+ * (5 min) are not credited. Live sessions and pre-v2 imports lack that signal
2959
+ * and fall back to the action-event stream (`activeTimeBasis: "events"`).
2960
+ * - `sessionSpanMs` overcounts (includes idle) and `commandTimeMs` is
2961
+ * shell-execution only (0 for `claude-code-import`); both are kept as context.
2962
+ *
2963
+ * The per-day view buckets the union intervals by `timeZone` (logs are UTC, so
2964
+ * a billing day needs an explicit zone). A union interval crossing local
2965
+ * midnight is attributed to its start day; per-day time still sums to the
2966
+ * billable total. Availability flags let callers caveat each measure.
2967
+ *
2968
+ * Session enumeration goes through {@link loadSessionEntries} (the handoff /
2969
+ * decisions path), so `session.yaml`-broken sessions are skipped consistently.
2970
+ */
2971
+ declare function computeWorkStats(input: WorkStatsInput): Promise<WorkStatsResult>;
2972
+ /**
2973
+ * Compute one session's work stats from its inner record + event list. Pure
2974
+ * and exported so a single-session surface (e.g. `basou session show`) can
2975
+ * reuse the exact same measures the workspace aggregator produces.
2976
+ */
2977
+ declare function sessionWorkStatsFromEvents(sessionId: string, inner: Session["session"], events: ReadonlyArray<Event>, now: Date, eventsUnreadable?: boolean): SessionWorkStats;
2671
2978
 
2979
+ type AppendBasouGitignoreResult = {
2980
+ /** True if the block was appended (or the file was newly created). */
2981
+ readonly appended: boolean;
2982
+ };
2672
2983
  /**
2673
- * Spawn-based ProcessRunner implementation.
2984
+ * Append Basou's default `.gitignore` block to `repositoryRoot/.gitignore`.
2985
+ *
2986
+ * The block contents are derived from the Basou v0.1 specification (the
2987
+ * standard ignore + commit recommendations). Callers must pass an absolute
2988
+ * path to a Git repository root.
2674
2989
  *
2675
2990
  * 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`.
2991
+ * - If `.gitignore` does not exist, it is created with the Basou block.
2992
+ * - If a line starting with `# Basou - default ignore` is already present,
2993
+ * the file is left untouched and `appended: false` is returned
2994
+ * (idempotent).
2995
+ * - If `.gitignore` is a symlink, the link is followed and the target file
2996
+ * is updated. Symlinks are not rejected.
2693
2997
  *
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.
2998
+ * On I/O failure throws Error with a pathless message
2999
+ * (`Failed to read .gitignore` / `Failed to write .gitignore`) and the
3000
+ * original native error attached as `cause`.
2698
3001
  */
2699
- declare class ChildProcessRunner implements ProcessRunner {
2700
- run(command: string, args: readonly string[], options: RunOptions): Promise<RunResult>;
2701
- }
3002
+ declare function appendBasouGitignore(repositoryRoot: string): Promise<AppendBasouGitignoreResult>;
2702
3003
 
2703
3004
  /**
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.
3005
+ * The two lock scopes basou uses. `task` guards the read-modify-write window
3006
+ * around a single `task.md`; `session` guards the events.jsonl append plus
3007
+ * surrounding `session.yaml` mutation for a single session. Two scopes use
3008
+ * different lockfile names so they never collide on disk.
3009
+ */
3010
+ type LockScope = "task" | "session";
3011
+ type LockHandle = {
3012
+ /**
3013
+ * Release the lock by unlinking the lockfile. Best-effort: any unlink error
3014
+ * is swallowed so a doubled release does not raise, and disk state never
3015
+ * holds a stranded lockfile after the caller's `finally` block.
3016
+ */
3017
+ release: () => Promise<void>;
3018
+ };
3019
+ /**
3020
+ * Acquire an advisory lock at `<paths.locks>/<scope>_<id>.lock` for the
3021
+ * lifetime of the returned handle. Lockfile body records the holder's pid
3022
+ * and acquire timestamp so a competitor can detect stale locks left by a
3023
+ * SIGINT'd CLI run and recover automatically.
2709
3024
  *
2710
- * `ahead` / `behind` are omitted when there is no remote or no upstream
2711
- * tracking; the schema declares both as optional non-negative integers.
3025
+ * Acquisition strategy:
3026
+ * 1. {@link atomicCreate} the lockfile (POSIX link(2) + EEXIST).
3027
+ * 2. On EEXIST, probe the existing lockfile via {@link isStaleLock}.
3028
+ * - If stale (= holder pid is dead or lock is older than
3029
+ * {@link STALE_LOCK_MAX_AGE_MS}), `unlink` the stale file and retry
3030
+ * the atomic create once.
3031
+ * - If still EEXIST after the retry (= another competitor won the race),
3032
+ * throw `"Lock is held by another process"`.
3033
+ * - If the holder is alive, throw `"Lock is held by another process"`
3034
+ * without retrying.
3035
+ *
3036
+ * The caller MUST call `release()` (typically from a `finally` block); the
3037
+ * `process.exit()` path or a fatal crash relies on stale-lock detection on
3038
+ * the next acquire to recover.
2712
3039
  */
2713
- type GitSnapshot = Omit<GitSnapshotEvent, "schema_version" | "id" | "session_id" | "occurred_at" | "source" | "type">;
3040
+ declare function acquireLock(paths: BasouPaths, scope: LockScope, resourceId: string): Promise<LockHandle>;
3041
+
2714
3042
  /**
2715
- * Resolve the absolute path of the Git repository root that contains `cwd`.
2716
- * Equivalent to `git rev-parse --show-toplevel`.
3043
+ * Inputs for {@link createManifest}. Optional fields drop out of the
3044
+ * resulting Manifest entirely (they are not emitted as `null`/`undefined`
3045
+ * in YAML); pass `null` for `repositoryUrl` to keep an explicit `null`.
3046
+ */
3047
+ type CreateManifestInput = {
3048
+ workspaceName: string;
3049
+ projectName?: string;
3050
+ projectDescription?: string;
3051
+ repositoryUrl?: string | null;
3052
+ /** Override for tests; defaults to `new Date()`. */
3053
+ now?: Date;
3054
+ /** Override for tests; defaults to a freshly generated `ws_<ULID>`. */
3055
+ workspaceId?: PrefixedId<"ws">;
3056
+ };
3057
+ /**
3058
+ * Build a fresh Manifest object that satisfies the manifest schema's
3059
+ * minimum shape. Performs no I/O. Returned object is parse-validated by
3060
+ * `ManifestSchema`.
3061
+ */
3062
+ declare function createManifest(input: CreateManifestInput): Manifest;
3063
+ /**
3064
+ * Write a Manifest to `paths.files.manifest`. Re-validates via
3065
+ * `ManifestSchema` before serialization.
2717
3066
  *
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.
3067
+ * Refuses to overwrite an existing manifest unless `force: true`.
3068
+ */
3069
+ declare function writeManifest(paths: BasouPaths, manifest: Manifest, options?: {
3070
+ force?: boolean;
3071
+ }): Promise<void>;
3072
+ /**
3073
+ * Read and parse a Manifest from `paths.files.manifest`. Throws if the file
3074
+ * is missing or contents fail `ManifestSchema` validation.
3075
+ */
3076
+ declare function readManifest(paths: BasouPaths): Promise<Manifest>;
3077
+
3078
+ /** Marker line that begins the auto-generated region. */
3079
+ declare const GENERATED_START = "<!-- BASOU:GENERATED:START -->";
3080
+ /** Marker line that ends the auto-generated region. */
3081
+ declare const GENERATED_END = "<!-- BASOU:GENERATED:END -->";
3082
+ /**
3083
+ * Result of parsing a markdown body for the BASOU:GENERATED marker region.
2723
3084
  *
2724
- * Pathless contract: the thrown message never embeds `cwd` or any absolute
2725
- * path; native errors are kept on `error.cause` for verbose surfacing.
3085
+ * The spec mandates strict line-level matching (see
3086
+ * `docs/spec/generated-markdown.md#102-marker-convention`): a marker is
3087
+ * only recognized when an entire line is exactly the marker string.
3088
+ * Leading/trailing whitespace, comment compression, and BOM are treated as
3089
+ * legacy formats (`no_markers`) so that re-generation refuses to silently
3090
+ * overwrite a mismatched manual edit.
2726
3091
  */
2727
- declare function resolveRepositoryRoot(cwd: string): Promise<string>;
3092
+ type MarkerSection = {
3093
+ kind: "ok";
3094
+ before: string;
3095
+ generated: string;
3096
+ after: string;
3097
+ } | {
3098
+ kind: "no_markers";
3099
+ } | {
3100
+ kind: "missing_start";
3101
+ } | {
3102
+ kind: "missing_end";
3103
+ } | {
3104
+ kind: "multiple_pairs";
3105
+ } | {
3106
+ kind: "wrong_order";
3107
+ };
2728
3108
  /**
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).
3109
+ * Read a markdown file as UTF-8 text. Returns `null` when the file does not
3110
+ * exist; throws `Error("Failed to read markdown file", { cause })` for other
3111
+ * I/O failures (pathless contract — never embed the absolute path in the
3112
+ * thrown `message`).
3113
+ */
3114
+ declare function readMarkdownFile(filePath: string): Promise<string | null>;
3115
+ /**
3116
+ * Atomically write a markdown body via {@link atomicReplace}. The shared
3117
+ * helper handles the tmp-file + rename sequence, `wx` collision guard, and
3118
+ * best-effort tmp cleanup on failure.
3119
+ *
3120
+ * On any failure the original error is re-thrown as
3121
+ * `Error("Failed to write markdown file", { cause })` (pathless contract).
3122
+ */
3123
+ declare function writeMarkdownFile(filePath: string, body: string): Promise<void>;
3124
+ /**
3125
+ * Parse a markdown body and identify the BASOU:GENERATED marker region.
3126
+ *
3127
+ * Returns one of six `kind` discriminants:
3128
+ * - `ok`: exactly one START line followed by exactly one END line in the
3129
+ * correct order. `before` / `generated` / `after` slice the original
3130
+ * text by character offsets so CRLF / LF are preserved verbatim outside
3131
+ * the marker region.
3132
+ * - `no_markers`: both START and END absent (legacy file / fresh write).
3133
+ * - `missing_start` / `missing_end`: exactly one of the pair is present.
3134
+ * - `multiple_pairs`: more than one START or END line.
3135
+ * - `wrong_order`: END appears before START.
2732
3136
  *
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`.
3137
+ * Matching is strict: leading/trailing whitespace, BOM, and comment
3138
+ * compression (`<!--BASOU:...-->`) all bypass the marker and are treated
3139
+ * as legacy content.
2736
3140
  */
2737
- declare function tryRemoteUrl(repositoryRoot: string): Promise<string | undefined>;
3141
+ declare function parseMarkers(content: string): MarkerSection;
2738
3142
  /**
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.
3143
+ * Build the final markdown body by replacing the BASOU:GENERATED region.
2744
3144
  *
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
3145
+ * - `existing === null` (no file yet): return `<START>\n<generated>\n<END>\n`.
3146
+ * - existing parses to `ok`: replace the marked region and keep everything
3147
+ * before START and after END untouched (preserving manual additions).
3148
+ * - any other parse result: throw a pathless error referencing `fileLabel`.
2751
3149
  *
2752
- * Pathless contract preserved on every throw path.
3150
+ * The caller passes `fileLabel` (e.g. `"handoff.md"` or `"decisions.md"`)
3151
+ * so the error message is informative without leaking an absolute path.
2753
3152
  */
2754
- declare function getSnapshot(repositoryRoot: string): Promise<GitSnapshot>;
3153
+ declare function renderWithMarkers(existing: string | null, generated: string, fileLabel: string): string;
2755
3154
 
2756
3155
  /**
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).
3156
+ * Options for {@link importSessionFromJson}. All fields are optional.
3157
+ *
3158
+ * - `labelOverride` / `taskIdOverride` come from the CLI `--label` / `--task`
3159
+ * flags and win over the corresponding fields on the input payload.
3160
+ * - `dryRun` skips disk writes entirely and returns a preview result.
2766
3161
  */
2767
- type FileChange = {
2768
- path: string;
2769
- old_path?: string;
2770
- status: FileChangeStatus;
3162
+ type ImportSessionOptions = {
3163
+ labelOverride?: string;
3164
+ taskIdOverride?: string;
3165
+ dryRun?: boolean;
2771
3166
  };
2772
3167
  /**
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.
3168
+ * Result of a successful import. `finalStatus` is always the literal
3169
+ * `"imported"` (per the import-session lifecycle policy); `finalSourceKind`
3170
+ * mirrors the input's `session.source.kind` so round-trip imports preserve
3171
+ * provenance.
3172
+ *
3173
+ * `pathSanitizeReport` summarises how many path-shaped fields the importer
3174
+ * rewrote on the way in: `related_files[]` entries plus a single boolean
3175
+ * for `working_directory`. The CLI wrapper surfaces this as a one-line
3176
+ * stderr warning when the total is non-zero so the operator sees that
3177
+ * machine-private prefixes were stripped.
2776
3178
  */
2777
- type DiffResult = {
2778
- changed_files: FileChange[];
3179
+ type ImportSessionResult = {
3180
+ sessionId: PrefixedId<"ses">;
3181
+ eventCount: number;
3182
+ finalStatus: SessionStatus;
3183
+ finalSourceKind: SessionSourceKind;
3184
+ pathSanitizeReport: {
3185
+ relatedFiles: number;
3186
+ workingDirectoryRewritten: boolean;
3187
+ };
2779
3188
  };
2780
3189
  /**
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`.
3190
+ * Import a round-trip JSON payload into `.basou/sessions/<new>/`. The caller
3191
+ * MUST validate the payload against {@link SessionImportPayloadSchema} first
3192
+ * and gate the `schema_version === "0.1.0"` literal check externally; this
3193
+ * function trusts both invariants.
2793
3194
  *
2794
- * Special cases:
2795
- * - `baseRef === headRef` short-circuits to an empty result
2796
- * - copy / unmerged / typechange / unknown status codes are skipped
3195
+ * On success a fresh session ID is minted and a complete
3196
+ * `session.yaml` + `events.jsonl` pair is written atomically. On any post-
3197
+ * mkdir failure the session directory is removed best-effort so partial
3198
+ * imports do not leave `session_yaml_missing` half-states behind.
2797
3199
  *
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)
3200
+ * Throws `Error` with one of the fixed messages enumerated by the import contract
3201
+ * §"Error messages" table; the original native error is attached as `cause`
3202
+ * for `--verbose` rendering.
2801
3203
  */
2802
- declare function getDiff(repoRoot: string, baseRef: string, headRef: string): Promise<DiffResult>;
3204
+ declare function importSessionFromJson(paths: BasouPaths, manifest: Manifest, payload: SessionImportPayload, options: ImportSessionOptions): Promise<ImportSessionResult>;
2803
3205
 
2804
3206
  /**
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.
3207
+ * Walk the cause chain (up to `depth` levels) looking for an Error whose
3208
+ * errno-style `code` matches `code`. Returns true on the first match.
3209
+ * Resilient to wrapper depth changes so that ENOENT detection survives
3210
+ * future error-wrapping refactors.
2820
3211
  */
2821
- type CommandLookup = (command: string) => Promise<boolean>;
3212
+ declare function findErrorCode(error: unknown, code: string, depth?: number): boolean;
3213
+
2822
3214
  /**
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.
3215
+ * Refuse to operate on `.basou` if it is a symlink or not a directory. This
3216
+ * prevents `writeStatus` from being tricked into writing `status.json`
3217
+ * outside the repository root via a swapped `.basou` symlink. Mirrors
3218
+ * `ensureBasouDirectory`'s lstat-based guard.
2825
3219
  *
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.
3220
+ * If `.basou` is absent the underlying ENOENT is propagated (wrapped) so
3221
+ * callers can map it to "workspace not initialized" via `findErrorCode`.
2828
3222
  *
2829
- * @throws Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.")
3223
+ * Note: this is a baseline safety net, not a TOCTOU fix the directory
3224
+ * could still be replaced between this check and the subsequent write. The
3225
+ * goal is to detect already-swapped symlinks, not to race-proof the
3226
+ * filesystem.
2830
3227
  */
2831
- declare function resolveClaudeCodeCommand(lookup?: CommandLookup): Promise<{
2832
- command: string;
2833
- }>;
3228
+ declare function assertBasouRootSafe(rootPath: string): Promise<void>;
2834
3229
  /**
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.
3230
+ * Build a StatusSnapshot from a manifest plus the path layout, observing
3231
+ * each subdirectory's presence via `lstat`. Read-only with respect to the
3232
+ * workspace state; writes nothing. The result is re-validated by
3233
+ * `StatusSchema.parse` before being returned.
2841
3234
  *
2842
- * @throws Error - always; not implemented in v0.1 Step 11.
3235
+ * @param input.now Override for testing; defaults to `new Date()`.
2843
3236
  */
2844
- declare function summarizeAdapterOutput(_stream: "stdout" | "stderr", _raw: string): string;
2845
-
3237
+ declare function buildStatusSnapshot(input: {
3238
+ manifest: Manifest;
3239
+ paths: BasouPaths;
3240
+ now?: Date;
3241
+ }): Promise<StatusSnapshot>;
2846
3242
  /**
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.
3243
+ * Atomically write a StatusSnapshot to `paths.files.status`.
2859
3244
  *
2860
- * Throws if validation fails or the underlying append errors. The thrown
2861
- * Error message is pathless; the original error is attached as `cause`.
3245
+ * Re-validates via `StatusSchema.parse` before any file I/O, so an invalid
3246
+ * snapshot throws synchronously and never overwrites the existing
3247
+ * `status.json`. Delegates the tmp-file + rename pass to {@link atomicReplace}.
2862
3248
  *
2863
- * @param sessionDir absolute path to `.basou/sessions/<session_id>/`
2864
- * @param event unknown payload to validate and append
3249
+ * **Precondition**: callers MUST invoke {@link assertBasouRootSafe} on
3250
+ * `paths.root` first to ensure `.basou` is a real directory and not a
3251
+ * swapped symlink. `writeStatus` does not redo this guard — it trusts the
3252
+ * caller — so a direct invocation without the guard could write
3253
+ * `status.json` outside the repository root.
2865
3254
  */
2866
- declare function appendEvent(sessionDir: string, event: unknown): Promise<void>;
3255
+ declare function writeStatus(paths: BasouPaths, snapshot: StatusSnapshot): Promise<void>;
2867
3256
  /**
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`.
3257
+ * Read `.basou/status.json` for the current schema_version (0.1.0). This
3258
+ * is a cache reader only; cross-version migration is not supported here.
3259
+ * Older or newer status.json shapes will fail `StatusSchema.parse`
3260
+ * callers regenerate by calling `buildStatusSnapshot` + `writeStatus`.
2881
3261
  */
2882
- declare function writeEventsBulk(sessionDir: string, events: Event[]): Promise<void>;
3262
+ declare function readStatus(paths: BasouPaths): Promise<StatusSnapshot>;
2883
3263
 
2884
3264
  /**
2885
- * Parse a unit-suffixed duration string (e.g. `30s`, `5m`, `1h`, `100ms`)
2886
- * into milliseconds.
3265
+ * Read a YAML file as `unknown`. Caller MUST validate via a zod schema.
2887
3266
  *
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.
3267
+ * Throws Error with pathless message and the original native error attached
3268
+ * as `cause` for I/O failures and YAML parse errors. All fs and parse exits
3269
+ * go through fixed messages so absolute paths cannot leak via `error.message`.
3270
+ */
3271
+ declare function readYamlFile(filePath: string): Promise<unknown>;
3272
+ /**
3273
+ * Write a value as YAML using {@link atomicReplace} for crash-resistant
3274
+ * atomicity. The shared helper handles the tmp-file + rename sequence,
3275
+ * `wx` collision guard, and best-effort tmp cleanup on failure. This
3276
+ * wrapper adds the YAML serialisation and the pathless error vocabulary.
3277
+ */
3278
+ declare function writeYamlFile(filePath: string, value: unknown): Promise<void>;
3279
+ /**
3280
+ * Atomically create a new YAML file. Like {@link writeYamlFile} but
3281
+ * delegates to {@link atomicCreate} so a pre-existing target fails with
3282
+ * EEXIST instead of being silently overwritten.
2891
3283
  *
2892
- * Supported units: `ms` (milliseconds), `s` (seconds), `m` (minutes),
2893
- * `h` (hours).
3284
+ * Used by `basou approval approve` / `reject` to write the resolved-side
3285
+ * YAML, so a concurrent resolver cannot overwrite an already-resolved
3286
+ * approval.
2894
3287
  *
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.
3288
+ * Throws `Error("Failed to write YAML file", { cause })` on failure; if
3289
+ * `cause.code === "EEXIST"` the caller can detect a target-exists race.
2900
3290
  */
2901
- declare function parseDuration(input: string): number;
3291
+ declare function linkYamlFile(filePath: string, value: unknown): Promise<void>;
3292
+ /**
3293
+ * Overwrite an existing YAML file atomically. Like {@link writeYamlFile}
3294
+ * but with a distinct pathless message label, used for files that
3295
+ * legitimately need in-place mutation (e.g. session.yaml's status /
3296
+ * ended_at lifecycle updates).
3297
+ */
3298
+ declare function overwriteYamlFile(filePath: string, value: unknown): Promise<void>;
2902
3299
 
2903
3300
  /**
2904
3301
  * Version of the `@basou/core` package, aligned with `manifest.yaml`'s
@@ -2906,4 +3303,4 @@ declare function parseDuration(input: string): number;
2906
3303
  */
2907
3304
  declare const BASOU_CORE_VERSION = "0.1.0";
2908
3305
 
2909
- export { type AdapterOutputEvent, type AppendBasouGitignoreResult, type AppendEventToExistingInput, type AppendEventToExistingResult, type Approval, type ApprovalApprovedEvent, type ApprovalExpiredEvent, ApprovalIdSchema, type ApprovalLocation, type ApprovalRejectedEvent, type ApprovalRequestedEvent, ApprovalSchema, type ApprovalStatus, ApprovalStatusSchema, type ArchiveTaskInput, type ArchiveTaskResult, type AttachTaskInput, type AttachUpdateTaskStatusInput, type AttachableStatus, BASOU_CORE_VERSION, type BasouPaths, type CaptureMode, ChildProcessRunner, type CommandExecutedEvent, type CommandLookup, type CreateAdHocSessionInput, type CreateAdHocSessionResult, type CreateAdHocTaskInput, type CreateManifestInput, type CreateTaskInput, type CreateTaskResult, DecisionIdSchema, type DecisionRecordedEvent, type DecisionsRendererInput, type DecisionsRendererResult, type DeleteTaskInput, type DeleteTaskResult, type DiffResult, type EditTaskInput, type EditTaskResult, type Event, EventIdSchema, EventSchema, EventSourceSchema, FailedToFinalizeError, type FileChange, type FileChangeStatus, type FileChangedEvent, GENERATED_END, GENERATED_START, type GitSnapshot, type GitSnapshotEvent, type HandoffRendererInput, type HandoffRendererResult, ID_PREFIXES, type IdPrefix, type ImportSessionOptions, type ImportSessionResult, IsoTimestampSchema, type LoadSessionEntriesOptions, type LoadTaskEntriesOptions, type LoadedApproval, type LockHandle, type LockScope, type Manifest, ManifestSchema, type MarkerSection, type NoteAddedEvent, type PrefixedId, type ProcessRunner, type ReconcileAllResult, type ReconcileAllTasksInput, type ReconcileAllTasksOptions, type ReconcileFailure, type ReconcileResult, type ReconcileTaskInput, type RefreshLinkageInput, type RefreshLinkageResult, type ReplayOptions, type ReplayWarning, type RiskLevel, RiskLevelSchema, type RunOptions, type RunResult, STUCK_THRESHOLD_MS, type SanitizePathOptions, type SanitizeRelatedFilesResult, SchemaVersionSchema, type Session, type SessionEndedEvent, type SessionEntry, SessionIdSchema, type SessionImportPayload, SessionImportPayloadSchema, type SessionInnerImportInput, SessionInnerImportSchema, SessionSchema, type SessionSkipReason, type SessionSourceKind, SessionSourceKindSchema, type SessionStartedEvent, type SessionStatus, type SessionStatusChangedEvent, SessionStatusSchema, StatusSchema, type StatusSnapshot, type SuspectReason, type Task, type TaskArchivedEvent, type TaskCreatedEvent, type TaskDeletedEvent, type TaskDocument, TaskIdSchema, type TaskLinkageRefreshedEvent, type TaskReconciledEvent, TaskSchema, type TaskSkipReason, type TaskStatus, type TaskStatusChangedEvent, TaskStatusSchema, TaskWriteAfterEventError, type TaskWriteAfterEventPhase, type UpdateAdHocTaskStatusInput, type UpdateTaskStatusInput, type UpdateTaskStatusResult, WorkspaceIdSchema, type WriteTaskFileMode, acquireLock, appendBasouGitignore, appendEvent, appendEventToExistingSession, archiveTask, assertBasouRootSafe, basouPaths, buildStatusSnapshot, classifySuspect, claudeCodeAdapterMetadata, createAdHocSessionWithEvent, createManifest, createTaskWithEvent, deleteTask, editTask, ensureBasouDirectory, enumerateApprovals, enumerateArchivedTaskIds, enumerateSessionDirs, enumerateTaskIds, findErrorCode, getDiff, getSnapshot, importSessionFromJson, isLazyExpired, isValidPrefixedId, linkYamlFile, loadApproval, loadSessionEntries, loadTaskEntries, overwriteYamlFile, parseDuration, parseMarkers, prefixedUlid, readAllEvents, readManifest, readMarkdownFile, readSessionYaml, readStatus, readTaskFile, readTaskFileWithArchiveFallback, readYamlFile, reconcileAllTasks, reconcileTask, refreshTaskLinkedSessions, renderDecisions, renderHandoff, renderWithMarkers, replayEvents, resolveClaudeCodeCommand, resolveRepositoryRoot, resolveSessionId, resolveTaskId, sanitizePath, sanitizeRelatedFiles, sanitizeWorkingDirectory, summarizeAdapterOutput, tryRemoteUrl, ulid, updateTaskStatusWithEvent, writeEventsBulk, writeManifest, writeMarkdownFile, writeStatus, writeTaskFile, writeYamlFile };
3306
+ export { ACTIVE_GAP_CAP_MS, type ActiveTimeBasis, type AdapterOutputEvent, type AppendBasouGitignoreResult, type AppendEventToExistingInput, type AppendEventToExistingResult, type Approval, type ApprovalApprovedEvent, type ApprovalExpiredEvent, ApprovalIdSchema, type ApprovalLocation, type ApprovalRejectedEvent, type ApprovalRequestedEvent, ApprovalSchema, type ApprovalStatus, ApprovalStatusSchema, type ArchiveTaskInput, type ArchiveTaskResult, type AttachTaskInput, type AttachUpdateTaskStatusInput, type AttachableStatus, BASOU_CORE_VERSION, type BasouPaths, CLAUDE_IMPORT_SOURCE, CODEX_IMPORT_SOURCE, type CaptureMode, ChildProcessRunner, type ClaudeTranscriptRecord, type ClaudeTranscriptToPayloadOptions, type CodexRolloutRecord, type CodexRolloutToPayloadOptions, type CommandExecutedEvent, type CommandLookup, type CreateAdHocSessionInput, type CreateAdHocSessionResult, type CreateAdHocTaskInput, type CreateManifestInput, type CreateTaskInput, type CreateTaskResult, type DayWorkStats, DecisionIdSchema, type DecisionRecordedEvent, type DecisionsRendererInput, type DecisionsRendererResult, type DeleteTaskInput, type DeleteTaskResult, type DiffResult, type EditTaskInput, type EditTaskResult, type Event, EventIdSchema, EventSchema, EventSourceSchema, FailedToFinalizeError, type FileChange, type FileChangeStatus, type FileChangedEvent, GENERATED_END, GENERATED_START, type GitSnapshot, type GitSnapshotEvent, type HandoffRendererInput, type HandoffRendererResult, ID_PREFIXES, type IdPrefix, type ImportSessionOptions, type ImportSessionResult, IsoTimestampSchema, type LoadSessionEntriesOptions, type LoadTaskEntriesOptions, type LoadedApproval, type LockHandle, type LockScope, type Manifest, ManifestSchema, type MarkerSection, type MeasureAvailability, type NoteAddedEvent, type PrefixedId, type ProcessRunner, type ReconcileAllResult, type ReconcileAllTasksInput, type ReconcileAllTasksOptions, type ReconcileFailure, type ReconcileResult, type ReconcileTaskInput, type RefreshLinkageInput, type RefreshLinkageResult, type ReplayOptions, type ReplayWarning, type RiskLevel, RiskLevelSchema, type RunOptions, type RunResult, STUCK_THRESHOLD_MS, type SanitizePathOptions, type SanitizeRelatedFilesResult, SchemaVersionSchema, type Session, type SessionEndedEvent, type SessionEntry, SessionIdSchema, type SessionImportPayload, SessionImportPayloadSchema, type SessionInnerImportInput, SessionInnerImportSchema, type SessionMetrics, SessionMetricsSchema, SessionSchema, type SessionSkipReason, type SessionSourceKind, SessionSourceKindSchema, type SessionStartedEvent, type SessionStatus, type SessionStatusChangedEvent, SessionStatusSchema, type SessionWorkStats, type SourceWorkStats, type StatusCount, StatusSchema, type StatusSnapshot, type SuspectReason, type Task, type TaskArchivedEvent, type TaskCreatedEvent, type TaskDeletedEvent, type TaskDocument, TaskIdSchema, type TaskLinkageRefreshedEvent, type TaskReconciledEvent, TaskSchema, type TaskSkipReason, type TaskStatus, type TaskStatusChangedEvent, TaskStatusSchema, TaskWriteAfterEventError, type TaskWriteAfterEventPhase, type TokenTotals, type UpdateAdHocTaskStatusInput, type UpdateTaskStatusInput, type UpdateTaskStatusResult, type WorkStatsInput, type WorkStatsResult, type WorkStatsTotals, WorkspaceIdSchema, type WriteTaskFileMode, acquireLock, appendBasouGitignore, appendEvent, appendEventToExistingSession, archiveTask, assertBasouRootSafe, basouPaths, buildStatusSnapshot, classifySuspect, claudeCodeAdapterMetadata, claudeTranscriptToImportPayload, codexRolloutToImportPayload, computeWorkStats, createAdHocSessionWithEvent, createManifest, createTaskWithEvent, deleteTask, editTask, ensureBasouDirectory, enumerateApprovals, enumerateArchivedTaskIds, enumerateSessionDirs, enumerateTaskIds, findErrorCode, getDiff, getSnapshot, importSessionFromJson, isLazyExpired, isValidPrefixedId, linkYamlFile, loadApproval, loadSessionEntries, loadTaskEntries, overwriteYamlFile, parseDuration, parseMarkers, prefixedUlid, readAllEvents, readManifest, readMarkdownFile, readSessionYaml, readStatus, readTaskFile, readTaskFileWithArchiveFallback, readYamlFile, reconcileAllTasks, reconcileTask, refreshTaskLinkedSessions, renderDecisions, renderHandoff, renderWithMarkers, replayEvents, resolveClaudeCodeCommand, resolveRepositoryRoot, resolveSessionId, resolveTaskId, sanitizePath, sanitizeRelatedFiles, sanitizeWorkingDirectory, sessionWorkStatsFromEvents, summarizeAdapterOutput, tryRemoteUrl, ulid, updateTaskStatusWithEvent, writeEventsBulk, writeManifest, writeMarkdownFile, writeStatus, writeTaskFile, writeYamlFile };