@basou/core 0.44.0 → 0.46.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
@@ -106,6 +106,19 @@ declare function removeStopHook(settings: unknown): StopHookRemoval;
106
106
  /** Return the installed basou Stop hook command, or null if none is registered. */
107
107
  declare function findBasouStopHookCommand(settings: unknown): string | null;
108
108
 
109
+ /**
110
+ * `schema_version` stamped on NEWLY WRITTEN `.basou/manifest.yaml`.
111
+ *
112
+ * 0.2.0 requires seconds in every timestamp. That NARROWS the field's domain,
113
+ * which §7.3 forbids except under the vacuous-narrowing rule the same section
114
+ * states: no value basou has ever written omits seconds, so the set of
115
+ * documents this refuses is empty. See `docs/spec/schemas.md` for the read
116
+ * rule and the measurement. The narrowing is shared with the event format, so
117
+ * every durable document bumps together.
118
+ *
119
+ * Note: basou writes this document.
120
+ */
121
+ declare const MANIFEST_SCHEMA_VERSION: "0.2.0";
109
122
  /**
110
123
  * The manifest, plus one guard the loose object cannot express: a TOP-LEVEL
111
124
  * `confidential` is refused. It belongs under `policies`, and because the
@@ -278,9 +291,17 @@ declare const SessionInnerImportSchema: z.ZodObject<{
278
291
  * validates a payload against the published artifact gets the same answer the
279
292
  * importer will give, instead of passing validation and being rejected at run
280
293
  * time. Unrelated to the events INSIDE the envelope, which carry their own
281
- * `schema_version` and are at 0.2.0.
294
+ * `schema_version` and are at 0.3.0.
295
+ *
296
+ * Bumped to 0.2.0 when timestamps began requiring seconds. That narrowing
297
+ * reaches the envelope's OWN fields -- `session.started_at`, `session.ended_at`
298
+ * and both `active_intervals` bounds -- so unlike the 0.2.0 event bump, whose
299
+ * bytes changed only because of the union it embeds, the envelope's own
300
+ * accepted set moved and the `$id` moves with it. A third party still gets one
301
+ * answer from the artifact and the importer, which is the whole point of
302
+ * pinning the version here.
282
303
  */
283
- declare const SESSION_IMPORT_SCHEMA_VERSION: "0.1.0";
304
+ declare const SESSION_IMPORT_SCHEMA_VERSION: "0.2.0";
284
305
  declare const SessionImportPayloadSchema: z.ZodObject<{
285
306
  schema_version: z.ZodString;
286
307
  session: z.ZodObject<{
@@ -802,6 +823,21 @@ type StopHookEvaluation = ({
802
823
  * before, so consumers that read only the capture signal are unaffected.
803
824
  */
804
825
  declare function evaluateStopHook(input: StopHookEvaluationInput): StopHookEvaluation;
826
+ /**
827
+ * When the session this transcript belongs to started: the timestamp of its
828
+ * first record that carries a parseable one.
829
+ *
830
+ * Used to date what the session is HOLDING. Anything the session read at start
831
+ * — its instruction files, the protocols rendered into them — is the version
832
+ * that existed at this instant, so a managed block whose stamp says it changed
833
+ * after it is newer than the copy in that session's context.
834
+ *
835
+ * Not every record carries a timestamp (a leading `summary` record does not),
836
+ * so this scans forward rather than reading `records[0]` and giving up.
837
+ * `undefined` means the transcript never said, and callers then say nothing
838
+ * rather than guess a start.
839
+ */
840
+ declare function transcriptStartedAt(records: ClaudeTranscriptRecord[]): string | undefined;
805
841
 
806
842
  /** Alias kept for API symmetry with the claude-code adapter's `CommandLookup`. */
807
843
  type CodexCommandLookup = CommandLookup;
@@ -1016,6 +1052,19 @@ type CodexRolloutToPayloadOptions = {
1016
1052
  */
1017
1053
  declare function codexRolloutToImportPayload(records: ReadonlyArray<CodexRolloutRecord>, options: CodexRolloutToPayloadOptions): SessionImportPayload | null;
1018
1054
 
1055
+ /**
1056
+ * `schema_version` stamped on NEWLY WRITTEN `.basou/approvals/**`.
1057
+ *
1058
+ * 0.2.0 requires seconds in every timestamp. That NARROWS the field's domain,
1059
+ * which §7.3 forbids except under the vacuous-narrowing rule the same section
1060
+ * states: no value basou has ever written omits seconds, so the set of
1061
+ * documents this refuses is empty. See `docs/spec/schemas.md` for the read
1062
+ * rule and the measurement. The narrowing is shared with the event format, so
1063
+ * every durable document bumps together.
1064
+ *
1065
+ * Note: basou only READS this document -- approvals are placed by an outside orchestrator, so nothing in this tree stamps the version; the constant exists so the published artifact and the accepted set are declared in one place.
1066
+ */
1067
+ declare const APPROVAL_SCHEMA_VERSION: "0.2.0";
1019
1068
  /**
1020
1069
  * Lifecycle states of a Basou approval. The status is stored directly on
1021
1070
  * the approval YAML (flat shape) so that pending → resolved transitions
@@ -1177,9 +1226,16 @@ declare function enumerateApprovals(paths: BasouPaths): Promise<{
1177
1226
  declare function isLazyExpired(approval: Approval, now: Date): boolean;
1178
1227
 
1179
1228
  /**
1180
- * `schema_version` stamped on NEWLY WRITTEN events. Bumped to 0.2.0 when
1181
- * `command_executed.duration_ms` became nullable, which widened the field's
1182
- * domain and so is a breaking change to the format.
1229
+ * `schema_version` stamped on NEWLY WRITTEN events.
1230
+ *
1231
+ * 0.3.0 requires seconds in every timestamp. That NARROWS the field's domain,
1232
+ * which §7.3 forbids except under the vacuous-narrowing rule the same section
1233
+ * states: no value basou has ever written omits seconds, so the set of
1234
+ * documents this refuses is empty. See `docs/spec/schemas.md` for the read
1235
+ * rule and the measurement.
1236
+ *
1237
+ * 0.2.0 made `command_executed.duration_ms` nullable, which widened the
1238
+ * field's domain and so is a breaking change to the format.
1183
1239
  *
1184
1240
  * The bump does not change what any value already on disk means: `0` meant "not
1185
1241
  * observed" before and still does. What changes is that a writer now says so
@@ -1188,13 +1244,17 @@ declare function isLazyExpired(approval: Approval, now: Date): boolean;
1188
1244
  * `readObservedDuration`), and the
1189
1245
  * version is a statement about validation, not about interpretation.
1190
1246
  *
1191
- * Reading is unaffected — {@link SchemaVersionSchema} accepts any 0.x.y — so
1192
- * events already on disk keep validating, and are not rewritten in place (a
1193
- * session IS re-derived, and restamped, when its source log grows). Only EVENTS
1194
- * carry this version: the other `.basou/` documents did not change, so their
1195
- * `schema_version` stays 0.1.0.
1247
+ * Reading is unaffected by the 0.2.0 bump — {@link SchemaVersionSchema}
1248
+ * accepts any 0.x.y — so events already on disk keep validating, and are not
1249
+ * rewritten in place (a session IS re-derived, and restamped, when its source
1250
+ * log grows). The 0.3.0 narrowing is what a stored value IS checked against,
1251
+ * which is why it had to be empty of real documents before it could land.
1252
+ *
1253
+ * The 0.3.0 timestamp narrowing is shared, so the other durable documents
1254
+ * (manifest, session, task, approval) bump alongside it. The two caches
1255
+ * (`status`, `task-index`) are rebuilt rather than versioned.
1196
1256
  */
1197
- declare const EVENT_SCHEMA_VERSION: "0.2.0";
1257
+ declare const EVENT_SCHEMA_VERSION: "0.3.0";
1198
1258
  declare const SessionStartedEventSchema: z.ZodObject<{
1199
1259
  schema_version: z.ZodString;
1200
1260
  id: z.ZodString & z.ZodType<`evt_${string}`, string, z.core.$ZodTypeInternals<`evt_${string}`, string>>;
@@ -1911,747 +1971,963 @@ declare function replayEvents(sessionDir: string, options?: ReplayOptions): Asyn
1911
1971
  declare function readAllEvents(sessionDir: string, options?: ReplayOptions): Promise<Event[]>;
1912
1972
 
1913
1973
  /**
1914
- * Project roster drift (the "saddle" model). A project's repos are DECLARED
1915
- * once in the manifest's `repos` list; the capture config (`source_roots`) must
1916
- * cover every declared repo. This computes the drift between the two so
1917
- * `basou project check` can surface a declared repo that is NOT being captured
1918
- * — the class of bug where a companion repo was wired into the workspace but
1919
- * never added to `source_roots`, so its work silently fell out of capture.
1974
+ * `schema_version` stamped on NEWLY WRITTEN `.basou/sessions/<id>/session.yaml`.
1920
1975
  *
1921
- * Pure: it compares declared relative paths against captured relative paths and
1922
- * performs no filesystem or git I/O. Paths are compared as declared (both lists
1923
- * use the same machine-portable relative-path form), not resolved on disk.
1924
- */
1925
- type RepoVisibility = "public" | "private" | "future-public";
1926
- /**
1927
- * The audience-driven language axis. Independent of visibility: a private repo
1928
- * can publish English content, a public repo can carry bilingual docs. `en` /
1929
- * `ja` for a single audience, `en+ja` when both are served.
1930
- */
1931
- type RepoLanguage = "en" | "ja" | "en+ja";
1932
- /** A published surface a repo emits: a deployed website or a package registry. */
1933
- type PublishKind = "web" | "npm";
1934
- /**
1935
- * Where a repo's agent instruction files live (the instruction-source axis),
1936
- * independent of visibility / language / publishes. `hub` is basou's native,
1937
- * generated hub-and-spoke topology (canonical in the anchor, gitignored symlinks
1938
- * in each repo); `self` is the additive opt-in where the canonical AGENTS.md is a
1939
- * regular committed file in the repo itself and basou stays hands-off about its
1940
- * content. See {@link instructionMode} for the default (absent => `hub`).
1941
- */
1942
- type RepoInstructions = "hub" | "self";
1943
- /**
1944
- * One published surface. Its visibility and language are INDEPENDENT of the
1945
- * source repo's: a private repo commonly publishes a public website. Both are
1946
- * optional so a surface can be declared
1947
- * before those facts are pinned down (mirroring how `adopt` leaves repo
1948
- * visibility unset for the operator to fill in).
1949
- */
1950
- type PublishTarget = {
1951
- kind: PublishKind;
1952
- visibility?: RepoVisibility | undefined;
1953
- language?: RepoLanguage | undefined;
1954
- };
1955
- type RepoEntry = {
1956
- /** Path relative to the manifest repo root (e.g. ".", "../takuhon"). */
1957
- path: string;
1958
- visibility?: RepoVisibility | undefined;
1959
- /** Source language (commits/comments/code, read by contributors). Independent of visibility. */
1960
- language?: RepoLanguage | undefined;
1961
- /** Published surfaces this repo emits (opt-in; absent for a repo that publishes nothing). */
1962
- publishes?: PublishTarget[] | undefined;
1963
- /**
1964
- * Instruction-source mode. Absent => `hub` (basou's native generated topology),
1965
- * so an existing roster's behavior is unchanged. `self` opts the repo out of
1966
- * generation: its AGENTS.md is a hand-authored committed file and basou stays
1967
- * hands-off. Resolve the effective mode with {@link instructionMode}.
1968
- */
1969
- instructions?: RepoInstructions | undefined;
1970
- };
1971
- /**
1972
- * The effective instruction-source mode for a repo: the declared `instructions`,
1973
- * defaulting to `hub` when absent. The default is the single guarantee that an
1974
- * existing roster (which has no `instructions` field) keeps basou's current
1975
- * hub-and-spoke behavior byte-for-byte — every generator branches on this, never
1976
- * on the raw optional field, so "absent => hub" is decided in exactly one place.
1976
+ * 0.2.0 requires seconds in every timestamp. That NARROWS the field's domain,
1977
+ * which §7.3 forbids except under the vacuous-narrowing rule the same section
1978
+ * states: no value basou has ever written omits seconds, so the set of
1979
+ * documents this refuses is empty. See `docs/spec/schemas.md` for the read
1980
+ * rule and the measurement. The narrowing is shared with the event format, so
1981
+ * every durable document bumps together.
1982
+ *
1983
+ * Note: basou writes this document.
1977
1984
  */
1978
- declare function instructionMode(entry: {
1979
- instructions?: RepoInstructions | undefined;
1980
- }): RepoInstructions;
1981
- type RosterDriftSummary = {
1982
- declaredCount: number;
1983
- capturedCount: number;
1984
- /** Declared in `repos` but absent from `source_roots`: a capture gap. */
1985
- gaps: RepoEntry[];
1986
- /** In `source_roots` but not declared in `repos` (e.g. a workspace view, or a stray). */
1987
- extra: string[];
1988
- /** Declared paths that are also captured. */
1989
- matched: string[];
1990
- /** True when there is no capture gap (every declared repo is covered). */
1991
- ok: boolean;
1992
- };
1985
+ declare const SESSION_SCHEMA_VERSION: "0.2.0";
1986
+ /** Session lifecycle states. */
1987
+ declare const SessionStatusSchema: z.ZodEnum<{
1988
+ initialized: "initialized";
1989
+ running: "running";
1990
+ waiting_approval: "waiting_approval";
1991
+ completed: "completed";
1992
+ failed: "failed";
1993
+ interrupted: "interrupted";
1994
+ imported: "imported";
1995
+ archived: "archived";
1996
+ }>;
1997
+ /** Inferred runtime type for {@link SessionStatusSchema}. */
1998
+ type SessionStatus = z.infer<typeof SessionStatusSchema>;
1993
1999
  /**
1994
- * Compute the {@link RosterDriftSummary} for a project. A declared repo missing
1995
- * from the captured set is a `gap` (the surfaced suspicion); a captured path not
1996
- * in the declared set is `extra` (commonly the workspace view, which is a
1997
- * capture source but not itself a project repo). With no declared roster, there
1998
- * are no gaps (nothing to check against) and every captured path is `extra`.
2000
+ * Source kind that produced the session.
2001
+ *
2002
+ * - `claude-code-adapter` a live `basou run claude-code` process wrap.
2003
+ * - `claude-code-import` derived after the fact from a Claude Code native
2004
+ * transcript (`~/.claude/projects/*.jsonl`) by `basou import claude-code`.
2005
+ * - `codex-adapter` — a live `basou run codex` process wrap.
2006
+ * - `codex-import` — derived after the fact from an OpenAI Codex native
2007
+ * rollout log (date-partitioned `~/.codex/sessions`) by `basou import codex`.
2008
+ * - `import` — a round-trip of a Basou-format export (`basou session import`).
2009
+ * - `human` / `terminal` — manually-authored / terminal-recorded sessions.
1999
2010
  */
2000
- declare function summarizeRosterDrift(input: {
2001
- repos?: RepoEntry[];
2002
- sourceRoots?: string[];
2003
- }): RosterDriftSummary;
2004
- type SourceRootsReconcile = {
2005
- /**
2006
- * The reconciled `source_roots`: the existing entries verbatim, then every
2007
- * declared repo path that was missing (normalized, in roster order). Existing
2008
- * order and form are preserved so the manifest diff is minimal and reversible.
2009
- */
2010
- next: string[];
2011
- /** Declared repo paths (normalized) that were appended because `source_roots` did not cover them. */
2012
- added: string[];
2013
- /** True when `source_roots` already covers every declared repo (`next` equals the current list). */
2014
- unchanged: boolean;
2015
- };
2011
+ declare const SessionSourceKindSchema: z.ZodEnum<{
2012
+ "claude-code-adapter": "claude-code-adapter";
2013
+ import: "import";
2014
+ "claude-code-import": "claude-code-import";
2015
+ "codex-adapter": "codex-adapter";
2016
+ "codex-import": "codex-import";
2017
+ human: "human";
2018
+ terminal: "terminal";
2019
+ }>;
2020
+ /** Inferred runtime type for {@link SessionSourceKindSchema}. */
2021
+ type SessionSourceKind = z.infer<typeof SessionSourceKindSchema>;
2016
2022
  /**
2017
- * Derive the `source_roots` a project's declared repo roster requires. The
2018
- * roster (`repos`) is the single source of truth for which repos belong to the
2019
- * project; this is the actuator behind `basou project sync`, computing the
2020
- * additive reconciliation so every declared repo is captured.
2023
+ * Optional per-session metrics, computed at import time from the source tool's
2024
+ * native log. Two groups, both optional because not every source records them:
2021
2025
  *
2022
- * ADDITIVE ONLY: it appends declared paths that are missing and never removes
2023
- * an existing entry. A captured-but-undeclared path (commonly the generated
2024
- * workspace view a legitimate capture source that is not itself a project
2025
- * repo) is preserved; pruning strays is deferred to the slice that generates
2026
- * the view (so basou knows which extras it owns). Existing entries are kept
2027
- * byte-identical; only appended paths are normalized.
2026
+ * - Model-usage rollup (`*_tokens`): the transcript carries per-message token
2027
+ * usage; these are the session totals. `reasoning_output_tokens` is
2028
+ * Codex-only, and live `run`/`exec` sessions carry no token usage at all.
2029
+ * - Engaged-time metrics (`active_*`): the billing-oriented active time derived
2030
+ * from the session's genuine engagement timestamps (conversation turns plus
2031
+ * action events), with idle gaps capped. `active_intervals` are the merged
2032
+ * wall-clock ranges (so cross-session totals can de-duplicate overlapping
2033
+ * work by interval union); `active_time_ms` is their summed duration;
2034
+ * `active_gap_cap_ms` and `active_time_method` lock the methodology so the
2035
+ * stored numbers stay interpretable if the method changes later. When a
2036
+ * source records explicit per-turn intervals (Codex), `active_time_method` is
2037
+ * `turn-intervals` and the in-turn time is the log's real wall-clock span
2038
+ * rather than a gap-capped approximation; the active semantics are unchanged.
2039
+ * - `machine_active_time_ms`: model compute time — the summed duration of the
2040
+ * source's per-turn spans (Codex `task_complete.duration_ms`), a SUBSET of a
2041
+ * single session's engaged active time. Unlike `active_intervals` it is a
2042
+ * plain sum, NOT wall-clock-deduplicated, so two concurrent sessions can sum
2043
+ * past their billable (union) active wall-clock — that is intended (two models
2044
+ * working at once did two machine-hours in one wall-clock hour). Captured only
2045
+ * for sources that record per-turn duration (Codex); absent otherwise.
2028
2046
  *
2029
- * Pure: no filesystem or git I/O. Paths are compared in the same normalized
2030
- * form as {@link summarizeRosterDrift}, so a trailing-slash variant of an
2031
- * already-captured repo is not re-appended.
2047
+ * Absent on sessions imported before a given field existed (re-import to
2048
+ * backfill). Live sessions carry no engaged-time metrics and fall back to
2049
+ * event-derived active time at stats time.
2032
2050
  */
2033
- declare function reconcileSourceRoots(input: {
2034
- repos?: RepoEntry[];
2035
- sourceRoots?: string[];
2036
- }): SourceRootsReconcile;
2051
+ declare const SessionMetricsSchema: z.ZodObject<{
2052
+ output_tokens: z.ZodOptional<z.ZodNumber>;
2053
+ input_tokens: z.ZodOptional<z.ZodNumber>;
2054
+ cached_input_tokens: z.ZodOptional<z.ZodNumber>;
2055
+ reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
2056
+ active_time_ms: z.ZodOptional<z.ZodNumber>;
2057
+ active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
2058
+ start: z.ZodString;
2059
+ end: z.ZodString;
2060
+ }, z.core.$loose>>>;
2061
+ active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
2062
+ active_time_method: z.ZodOptional<z.ZodString>;
2063
+ machine_active_time_ms: z.ZodOptional<z.ZodNumber>;
2064
+ }, z.core.$loose>;
2065
+ /** Inferred runtime type for {@link SessionMetricsSchema}. */
2066
+ type SessionMetrics = z.infer<typeof SessionMetricsSchema>;
2037
2067
  /**
2038
- * On-disk classification of a source-root candidate during adoption: a git repo
2039
- * root (→ becomes a roster entry), a resolved-but-non-repo directory (the
2040
- * generated workspace view, `/tmp`, a scratch dir excluded), or a path that
2041
- * could not be resolved on disk (→ excluded).
2068
+ * Tamper-evidence head anchor for a session whose `events.jsonl` is hash
2069
+ * chained: `head_hash` is the hex sha-256 of the last written event line
2070
+ * (excluding the trailing newline), `event_count` the number of chained lines.
2071
+ * Written by the import / in-place re-import writers and, for a live session
2072
+ * (`exec` / `run` / ad-hoc), by the finalize once it reaches a terminal status.
2073
+ * Absent on a still-live session (the anchor is stamped at finalize) and on a
2074
+ * pre-feature unchained session. Additive optional => no schema_version bump.
2075
+ * `.strict()` because the writers fully own the shape.
2042
2076
  */
2043
- type AdoptCandidateKind = "repo" | "non-repo" | "unresolved";
2044
- type AdoptCandidate = {
2045
- /** Source-root path as declared (relative to the manifest root). */
2046
- path: string;
2047
- /** On-disk classification; the filesystem probing that produces it is the caller's job. */
2048
- kind: AdoptCandidateKind;
2049
- };
2050
- type RosterAdoptionPlan = {
2051
- /** Proposed `repos` entries: the candidates that are git repos (visibility left unset for the operator). */
2052
- repos: RepoEntry[];
2053
- /** Candidates excluded from the roster, with why (a non-repo directory, or an unresolvable path). */
2054
- excluded: {
2055
- path: string;
2056
- kind: Exclude<AdoptCandidateKind, "repo">;
2057
- }[];
2058
- };
2077
+ declare const SessionIntegritySchema: z.ZodObject<{
2078
+ head_hash: z.ZodString;
2079
+ event_count: z.ZodNumber;
2080
+ }, z.core.$strict>;
2081
+ /** Inferred runtime type for {@link SessionIntegritySchema}. */
2082
+ type SessionIntegrity = z.infer<typeof SessionIntegritySchema>;
2059
2083
  /**
2060
- * Plan a `repos` roster from classified source-root candidates (the actuator
2061
- * behind `basou project adopt`). Pure: it partitions already-classified
2062
- * candidates — the realpath / `.git` filesystem probing that produces each
2063
- * `kind` is the caller's job, so this stays testable without disk I/O.
2064
- *
2065
- * A git repo becomes a roster entry (path only; visibility is left unset because
2066
- * it is a human judgment, kept independent of the other axes). A non-repo
2067
- * (commonly the generated workspace view) or an unresolvable path is excluded and
2068
- * reported, so the operator sees what was dropped and why before editing. Repo
2069
- * paths are deduped by normalized form, preserving the first declared form and
2070
- * order.
2084
+ * Schema for `.basou/sessions/<session_id>/session.yaml`. The minimal
2085
+ * session document carries the actual fields nested under the outer
2086
+ * `session:` key.
2071
2087
  */
2072
- declare function planRosterAdoption(candidates: AdoptCandidate[]): RosterAdoptionPlan;
2088
+ declare const SessionSchema: z.ZodObject<{
2089
+ schema_version: z.ZodString;
2090
+ session: z.ZodObject<{
2091
+ id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2092
+ label: z.ZodOptional<z.ZodString>;
2093
+ task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
2094
+ workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2095
+ source: z.ZodObject<{
2096
+ kind: z.ZodEnum<{
2097
+ "claude-code-adapter": "claude-code-adapter";
2098
+ import: "import";
2099
+ "claude-code-import": "claude-code-import";
2100
+ "codex-adapter": "codex-adapter";
2101
+ "codex-import": "codex-import";
2102
+ human: "human";
2103
+ terminal: "terminal";
2104
+ }>;
2105
+ version: z.ZodLiteral<"0.1.0">;
2106
+ external_id: z.ZodOptional<z.ZodString>;
2107
+ source_size_bytes: z.ZodOptional<z.ZodNumber>;
2108
+ }, z.core.$loose>;
2109
+ started_at: z.ZodString;
2110
+ ended_at: z.ZodOptional<z.ZodString>;
2111
+ status: z.ZodEnum<{
2112
+ initialized: "initialized";
2113
+ running: "running";
2114
+ waiting_approval: "waiting_approval";
2115
+ completed: "completed";
2116
+ failed: "failed";
2117
+ interrupted: "interrupted";
2118
+ imported: "imported";
2119
+ archived: "archived";
2120
+ }>;
2121
+ working_directory: z.ZodString;
2122
+ invocation: z.ZodObject<{
2123
+ command: z.ZodString;
2124
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
2125
+ exit_code: z.ZodNullable<z.ZodNumber>;
2126
+ }, z.core.$loose>;
2127
+ related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
2128
+ events_log: z.ZodDefault<z.ZodString>;
2129
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2130
+ metrics: z.ZodOptional<z.ZodObject<{
2131
+ output_tokens: z.ZodOptional<z.ZodNumber>;
2132
+ input_tokens: z.ZodOptional<z.ZodNumber>;
2133
+ cached_input_tokens: z.ZodOptional<z.ZodNumber>;
2134
+ reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
2135
+ active_time_ms: z.ZodOptional<z.ZodNumber>;
2136
+ active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
2137
+ start: z.ZodString;
2138
+ end: z.ZodString;
2139
+ }, z.core.$loose>>>;
2140
+ active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
2141
+ active_time_method: z.ZodOptional<z.ZodString>;
2142
+ machine_active_time_ms: z.ZodOptional<z.ZodNumber>;
2143
+ }, z.core.$loose>>;
2144
+ integrity: z.ZodOptional<z.ZodObject<{
2145
+ head_hash: z.ZodString;
2146
+ event_count: z.ZodNumber;
2147
+ }, z.core.$strict>>;
2148
+ }, z.core.$loose>;
2149
+ }, z.core.$loose>;
2150
+ /** Inferred runtime type for {@link SessionSchema}. */
2151
+ type Session = z.infer<typeof SessionSchema>;
2073
2152
 
2074
2153
  /**
2075
- * The language of the GENERATED-VIEW chrome (headings, labels, verdict prose)
2076
- * in handoff.md / orientation.md / decisions.md / report output.
2154
+ * Threshold above which a still-`running` session with no `session_ended`
2155
+ * event is flagged suspect.
2077
2156
  *
2078
- * This is deliberately narrower than the manifest's repo `language` axis
2079
- * (`en | ja | en+ja`): a generated view has exactly one chrome language, so
2080
- * `en+ja` resolves to `en`. User data (decision titles, notes, labels, file
2081
- * paths) always passes through verbatim — only the tool-generated strings are
2082
- * localized, which is exactly the split this type exists to keep honest.
2157
+ * 24h: long enough that an active long-running session will not be flagged,
2158
+ * short enough that an abandoned process is surfaced within a working day.
2159
+ * Tunable via CLI option in a later step (continuation backlog #23).
2083
2160
  */
2084
- type ViewLanguage = "en" | "ja";
2161
+ declare const STUCK_THRESHOLD_MS: number;
2162
+ type SuspectReason = "events_say_ended_but_yaml_running" | "running_no_end_event";
2163
+ type SessionEntry = {
2164
+ sessionId: string;
2165
+ session: Session;
2166
+ suspect: boolean;
2167
+ suspectReason: SuspectReason | null;
2168
+ /**
2169
+ * The trail store this entry was read from. Its `sessions` directory locates
2170
+ * the session's `events.jsonl`, so a federated caller can replay events from
2171
+ * the store the session actually lives in (not the local store). For a plain
2172
+ * local load this is the `paths` passed to {@link loadSessionEntries}.
2173
+ */
2174
+ sourceRoot: BasouPaths;
2175
+ /**
2176
+ * Federation host label from the registry (`~/.basou/hosts.yaml`), or `null`
2177
+ * for the local store. Surfaced by orientation so a merged, multi-host view
2178
+ * can attribute the latest session / decision / next-step to its host.
2179
+ */
2180
+ host: string | null;
2181
+ };
2085
2182
  /**
2086
- * Resolve the generated-view language from a manifest: the workspace speaks
2087
- * the language of its ANCHOR repo (the `repos[]` entry whose path is `.`).
2183
+ * Per-session degradation reason emitted by {@link loadSessionEntries.onSkip}.
2088
2184
  *
2089
- * Rules (fixed by design):
2090
- * - anchor declares `ja` -> `ja`
2091
- * - anchor declares `en` / `en+ja` -> `en` (a bilingual surface renders one
2092
- * chrome; en is the shared floor)
2093
- * - no roster / no anchor entry / no declared language -> `en` (the default
2094
- * for basou's English-first OSS surface)
2185
+ * - `session_yaml_missing` (ENOENT) and `session_yaml_invalid` (parse or schema
2186
+ * failure) both omit the entry from the result.
2187
+ * - `events_jsonl_unreadable` still pushes the entry with `suspect=false` so
2188
+ * the session row remains visible to the caller; only the suspect check is
2189
+ * degraded. Matches the existing CLI behaviour at
2190
+ * `packages/cli/src/commands/session.ts` (suspect-check stderr warning).
2191
+ */
2192
+ type SessionSkipReason = "session_yaml_missing" | "session_yaml_invalid" | "events_jsonl_unreadable";
2193
+ type LoadSessionEntriesOptions = {
2194
+ /**
2195
+ * Single `now` shared across every {@link classifySuspect} call so that
2196
+ * sessions classified back-to-back observe the same instant. Avoids
2197
+ * boundary races where a session at age ≈ 24h would flip between calls.
2198
+ */
2199
+ now: Date;
2200
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2201
+ onSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2202
+ };
2203
+ /**
2204
+ * A trail store to read in a federated load, tagged with its host label.
2205
+ * `host: null` denotes the local store; a non-null label comes from the host
2206
+ * registry (`~/.basou/hosts.yaml`). `paths` is where that store is reachable
2207
+ * as a local path on this machine (an SSHFS mount, an rsync mirror, etc.) —
2208
+ * basou itself never performs any network I/O to obtain it.
2209
+ */
2210
+ type FederatedRoot = {
2211
+ paths: BasouPaths;
2212
+ host: string | null;
2213
+ };
2214
+ type LoadFederatedOptions = LoadSessionEntriesOptions & {
2215
+ /**
2216
+ * Called when a NON-local root cannot be enumerated (present-but-unreadable
2217
+ * mount, permission error). That root is skipped best-effort so the local
2218
+ * store and other roots still load. The local root (`host: null`) is never
2219
+ * degraded here — its errors propagate, preserving single-store behaviour.
2220
+ * (An absent root path is not an error: {@link enumerateSessionDirs} returns
2221
+ * `[]` on ENOENT, so a dropped mount is simply an empty host.)
2222
+ */
2223
+ onRootUnavailable?: (host: string, error: unknown) => void;
2224
+ };
2225
+ /**
2226
+ * List session directory names under `paths.sessions`, ULID ascending.
2095
2227
  *
2096
- * Binding the view to the anchor's language is a deliberate, documented
2097
- * coupling: the anchor is the planning/trail home the views live in, so its
2098
- * declared audience is the views' audience. Other repos' languages do not
2099
- * participate.
2228
+ * - Returns `[]` when the sessions directory does not exist (empty workspace
2229
+ * or pre-init state).
2230
+ * - Throws `Error("Failed to enumerate sessions", { cause })` on other I/O.
2231
+ * - Only directories are returned (`.gitkeep` and other files are filtered).
2232
+ *
2233
+ * Sort order is `Array.prototype.sort()` default (Unicode code-point
2234
+ * compare). ULIDs are Crockford base32 in uppercase, so the natural sort
2235
+ * is also chronological session-start order.
2100
2236
  */
2101
- declare function resolveViewLanguage(manifest: Pick<Manifest, "repos"> | null): ViewLanguage;
2237
+ declare function enumerateSessionDirs(paths: BasouPaths): Promise<string[]>;
2102
2238
  /**
2103
- * Manifest-reading convenience for the renderers: resolve the view language
2104
- * for a workspace, defaulting to `en` when the manifest is missing or
2105
- * unreadable (mirrors the orientation renderer's tolerant source_roots read
2106
- * a broken manifest must never break a view render).
2239
+ * Read and validate `<paths.sessions>/<sessionId>/session.yaml`.
2240
+ *
2241
+ * - Re-throws the yaml-store fixed-message `"YAML file not found"` for
2242
+ * ENOENT so the caller can branch on it.
2243
+ * - Throws `Error("Failed to read session.yaml", { cause })` for parse
2244
+ * failures and schema violations (cause is either the YAML parser error
2245
+ * or the zod error).
2107
2246
  */
2108
- declare function resolveViewLanguageFromPaths(paths: BasouPaths): Promise<ViewLanguage>;
2247
+ declare function readSessionYaml(paths: BasouPaths, sessionId: string): Promise<Session>;
2109
2248
  /**
2110
- * Every localized string the four view renderers emit, grouped per renderer
2111
- * with a small `common` set for lines that are byte-identical across views.
2112
- * Parameterized lines are functions so the two languages can order their
2113
- * parts naturally.
2249
+ * Apply a terminal-status mutation to a live session's `session.yaml` AND, in
2250
+ * the same locked write, stamp the tamper-evidence head anchor derived from the
2251
+ * on-disk `events.jsonl` tail. Used by the `exec` / `run` orchestrators for
2252
+ * BOTH terminal writers (the normal end-of-run finalize and the spawn-failure
2253
+ * `failed` finalize).
2114
2254
  *
2115
- * This module is the SINGLE home for generated Japanese the view chrome here
2116
- * and the instruction-file content in {@link PresetStrings} (the E-5
2117
- * language-lint allowlist points here, not at the renderers/generators), so
2118
- * "user data language" and "tool-generated content language" can never blur
2119
- * together again.
2255
+ * Why locked + anchor-from-tail: live appends chain the LOG only and leave the
2256
+ * anchor for finalize. Reading the final tail under the session lock means a
2257
+ * foreign line appended just before finalize (e.g. a `decision record` attached
2258
+ * to a still-running session) is included in the anchor, and a foreign attach
2259
+ * that arrives after the terminal status is set is rejected by the attach gate
2260
+ * — so the anchor can never disagree with the at-rest log. The whole-document
2261
+ * read-modify-write also preserves any field a foreign locked writer set (e.g.
2262
+ * a task attach's `task_id`).
2263
+ *
2264
+ * The anchor is written only when the log is actually chained with at least one
2265
+ * line; a legacy unchained session (and an empty log) is left with no
2266
+ * `integrity` anchor, matching the import writers. The mutator receives the
2267
+ * full {@link Session} document and typically sets
2268
+ * `session.session.status` / `ended_at` / `invocation.exit_code` /
2269
+ * `related_files`.
2270
+ *
2271
+ * Throws the {@link inspectChainTail} errors (torn / mixed log), the
2272
+ * {@link readSessionYaml} errors, a zod error if the mutation produces an
2273
+ * invalid document, or `Error("Failed to overwrite YAML file")` on a disk
2274
+ * failure.
2120
2275
  */
2121
- type ViewStrings = {
2122
- /** Localized relative age for prose lines, e.g. "3日4時間前" / "3d 4h ago". */
2123
- relativeAge: (startedAt: string | null, now: Date) => string;
2124
- common: {
2125
- /** "最終 session" — the latest live session pointer. */
2126
- lastSessionLabel: string;
2127
- /** "直近の判断" — the latest recorded decision pointer. */
2128
- latestDecisionLabel: string;
2129
- /** "直近の変更ファイル" — the latest session's related files. */
2130
- recentFilesLabel: string;
2131
- /** "理由" — a track's rationale label. */
2132
- trackWhyLabel: string;
2133
- /** Note that the latest decision comes from a different session. */
2134
- decisionOtherSessionNote: (shortSessionId: string) => string;
2135
- };
2136
- orientation: {
2137
- headingWhere: string;
2138
- headingRecent: (sessionCount: number) => string;
2139
- headingInFlight: string;
2140
- headingForward: string;
2141
- headingCurrency: string;
2142
- inFlightTasksHeading: (n: number) => string;
2143
- /**
2144
- * Body line under the in-flight-tasks heading when NO task was ever
2145
- * recorded here. "(none)" states that nothing is pending — a claim about
2146
- * the work. This one claims only what it can see: that the record is
2147
- * empty. It says nothing about whether the workspace should use tasks,
2148
- * and names no command: the renderers report position, and a nudge that
2149
- * cannot be silenced is noise (see `trackNudge`, which is gated).
2150
- */
2151
- noTasksRecorded: string;
2152
- pendingApprovalsHeading: (n: number) => string;
2153
- suspectSessionsHeading: (n: number) => string;
2154
- openTracksHeading: (n: number) => string;
2155
- /** Stale-decision honesty note under 直近の判断. */
2156
- decisionStaleNote: (activityAge: string) => string;
2157
- outOfRootWarning: (count: number, files: string) => string;
2158
- recentEmpty: string;
2159
- recentDecisionsLabel: string;
2160
- recentNextStepLabel: string;
2161
- recentChangedLabel: string;
2162
- /** Trails the recent-files line when scratch paths were left out of it. */
2163
- scratchOmitted: (count: number) => string;
2164
- trackCloseInstruction: string;
2165
- nextStepRecordedLabel: (age: string) => string;
2166
- noteStaleNote: (activityAge: string) => string;
2167
- fallbackStaleDirection: string;
2168
- fallbackStaleReferenceLabel: string;
2169
- trackNudge: string;
2170
- federatedFreshnessNote: string;
2171
- bannerUnverifiable: (n: number) => string;
2172
- bannerStale: (parts: string) => string;
2173
- partNew: (n: number) => string;
2174
- partUpdated: (n: number) => string;
2175
- partsJoiner: string;
2176
- verdictUnverifiable: (n: number) => [string, string];
2177
- verdictStale: (parts: string) => [string, string];
2178
- verdictUpdatedOnly: (n: number) => [string, string];
2179
- verdictSuspectsAlso: (n: number) => string;
2180
- verdictEmpty: [string, string];
2181
- verdictUnprobed: (rel: string, tool: string) => [string, string];
2182
- verdictCurrent: (rel: string, tool: string, hasHosts: boolean) => string;
2183
- verdictSuspectsCaveat: (n: number) => string;
2184
- verdictScopeDisclaimer: string;
2185
- toolTerminal: string;
2186
- toolHuman: string;
2187
- toolImport: string;
2188
- toolUnknown: string;
2189
- };
2190
- handoff: {
2191
- headingCurrentState: string;
2192
- headingRecentFiles: string;
2193
- headingLatestDecision: string;
2194
- headingOpenTracks: string;
2195
- headingUnresolved: string;
2196
- headingReadNext: string;
2197
- headingNextWork: string;
2198
- headingSessions: string;
2199
- lastTaskLabel: string;
2200
- /** "Work to do next" placeholder: tasks exist, none are open. */
2201
- noPendingTasks: string;
2202
- /** "Work to do next" placeholder: no task was ever recorded. */
2203
- noTasksRecorded: string;
2204
- decisionStaleNote: string;
2205
- trackCloseInstruction: string;
2206
- };
2207
- decisions: {
2208
- dateLabel: string;
2209
- trackKindLine: string;
2210
- decisionLabel: string;
2211
- };
2212
- report: {
2213
- headingSummary: string;
2214
- headingVolume: string;
2215
- headingDecisions: string;
2216
- headingApprovals: string;
2217
- headingTasks: string;
2218
- headingChangedFiles: string;
2219
- headingSessions: string;
2220
- headingIntegrity: string;
2221
- };
2222
- };
2223
- /** Look up the string table for a resolved view language. */
2224
- declare function viewStrings(language: ViewLanguage): ViewStrings;
2276
+ declare function finalizeSessionYaml(paths: BasouPaths, sessionId: string, mutate: (session: Session) => void): Promise<void>;
2225
2277
  /**
2226
- * Resolve a GENERATED INSTRUCTION-FILE's content language from the target
2227
- * repo's declared `language`. Unlike the views (workspace-level artifacts that
2228
- * follow the anchor), a preset block lives inside one repo's instruction file,
2229
- * so its audience is that repo's declared audience: `ja` renders Japanese
2230
- * (byte-identical to the pre-i18n output), `en` / `en+ja` / undeclared render
2231
- * English (one content language per generated block; en is the shared floor).
2278
+ * Classify a `running` session as suspect using one of two rules:
2279
+ *
2280
+ * - Rule A (`events_say_ended_but_yaml_running`): events.jsonl contains a
2281
+ * `session_ended` event but the session.yaml is still `running`. The
2282
+ * session ended cleanly in the event log but the YAML write was lost or
2283
+ * never reached.
2284
+ * - Rule B (`running_no_end_event`): no `session_ended` event and the last
2285
+ * event is older than {@link STUCK_THRESHOLD_MS}. The process likely
2286
+ * crashed or was killed.
2287
+ *
2288
+ * Sessions that are not `running` are never suspect.
2289
+ *
2290
+ * I/O failure on events.jsonl is re-thrown unwrapped so the caller can
2291
+ * degrade with a warning instead of treating the session as healthy. The
2292
+ * caller is also responsible for surfacing replay warnings via `onWarning`.
2232
2293
  */
2233
- declare function resolveRepoContentLanguage(language: RepoLanguage | undefined): ViewLanguage;
2294
+ declare function classifySuspect(paths: BasouPaths, sessionId: string, session: Session, now: Date, onWarning?: (warning: ReplayWarning) => void): Promise<{
2295
+ suspect: boolean;
2296
+ suspectReason: SuspectReason | null;
2297
+ }>;
2298
+ declare function loadSessionEntries(paths: BasouPaths, options: LoadSessionEntriesOptions): Promise<SessionEntry[]>;
2234
2299
  /**
2235
- * Resolve the content language of a WORKSPACE-LEVEL instruction artifact (the
2236
- * view's AGENTS.md block, the anchor's starter) from an already-gathered
2237
- * roster: the entry flagged `anchor` speaks for the workspace, mirroring the
2238
- * views' anchor-language rule. No anchor entry (or no declared language)
2239
- * resolves to English. When more than one entry carries the flag, the first
2240
- * wins (declared order).
2241
- *
2242
- * Note the anchor is identified by the CALLER-SET flag, not by this module:
2243
- * {@link resolveViewLanguage} keys on the manifest path being `.`, while the
2244
- * instruction-file callers flag the anchor by resolved-path identity. For a
2245
- * conventional manifest (anchor declared as `.`) the two agree; a roster that
2246
- * reaches the anchor only through an aliased path is where they can diverge,
2247
- * and the caller's flag is authoritative for the instruction files.
2300
+ * Federated load across multiple trail stores. Each root's sessions are tagged
2301
+ * with that root's host label and `sourceRoot`, so a caller replays events from
2302
+ * the store the session lives in. De-duped by `sessionId` (a per-host random
2303
+ * ULID), then by `source.external_id` when present first occurrence wins, so
2304
+ * pass the local root FIRST to keep it authoritative (e.g. over a re-imported
2305
+ * copy of the same vendor session on another host). A non-local root that
2306
+ * cannot be enumerated is reported via `onRootUnavailable` and skipped; the
2307
+ * local root's errors propagate, matching {@link loadSessionEntries}.
2248
2308
  */
2249
- declare function resolveAnchorContentLanguage(repos: ReadonlyArray<{
2250
- anchor?: boolean | undefined;
2251
- language?: RepoLanguage | undefined;
2252
- }>): ViewLanguage;
2309
+ declare function loadFederatedSessionEntries(roots: ReadonlyArray<FederatedRoot>, options: LoadFederatedOptions): Promise<SessionEntry[]>;
2310
+
2253
2311
  /**
2254
- * Every localized string the instruction-file generators emit: the per-repo
2255
- * preset block, the workspace view's block, and the anchor's starter. Lives in
2256
- * this module for the same reason as {@link ViewStrings}: it is the SINGLE
2257
- * home for generated Japanese, so the language-lint E-5 allowlist stays one
2258
- * file and "generated content language" is always a declaration-driven table
2259
- * lookup, never a hardcode.
2312
+ * Decision-gap surfacer: which recorded decisions are still waiting for someone.
2313
+ *
2314
+ * The rule it applies, stated by the report that motivated it: a decision that
2315
+ * changes what gets built should have a task carrying it, and the ones with no
2316
+ * task are listed. It reads only captured provenance and writes nothing.
2317
+ *
2318
+ * The question is a RELATION — "is any task carrying this decision" — and the
2319
+ * text scan below is only today's way of answering it. Stating it that way is
2320
+ * deliberate: when `task_created` grows a field naming the decision it serves,
2321
+ * honouring that field answers the same question better and is an
2322
+ * implementation change, where redefining the question would not be.
2323
+ *
2324
+ * Two properties are load-bearing:
2325
+ *
2326
+ * - Nothing here reads what a decision MEANS, so the answer does not depend on
2327
+ * a model and does not change between runs over the same store.
2328
+ * - The default is fail-closed: a decision with no task is listed. An opt-in
2329
+ * mark ("this one produces work") was rejected upstream because forgetting
2330
+ * the mark is the same failure the report exists to catch.
2331
+ *
2332
+ * Fail-closed only pays for itself if the list is drainable, so the population
2333
+ * is cut on four structural grounds — a timestamp, a `source`, a `kind`, and a
2334
+ * void. None requires reading meaning, and each is reported as a count so the
2335
+ * cut is never silent:
2336
+ *
2337
+ * - {@link DECISION_GAPS_EPOCH}: decisions predating this feature are out of
2338
+ * scope. Applying the rule to a whole history measured ~1450 entries on the
2339
+ * store it was built against, which buries the signal on day one.
2340
+ * - {@link LOCAL_CLI_EVENT_SOURCE}: only decisions somebody recorded by
2341
+ * running basou (`basou decision capture` or `basou decision record`), as
2342
+ * opposed to ones a reader derived. An importer derives decisions from a
2343
+ * transcript's in-conversation questions ("how far should I implement? ->
2344
+ * findings 1 and 3"), which were 70% of that store's decisions and 0 of the
2345
+ * ones any task carried. Those are answers given while working, not plans
2346
+ * anybody ratified.
2347
+ * - `kind: "track"`: a track is ALREADY resurfaced, every session, until it is
2348
+ * voided — orientation and handoff both carry it under "open tracks". For a
2349
+ * track the premise "recorded, then never surfaced again" is false by
2350
+ * construction, so listing it here says nothing the reader is not already
2351
+ * shown, and it measured 27-45% of the list.
2352
+ * - voided: `basou decision void` is the closing verb this product tells the
2353
+ * operator to use, and a direction no longer in force is not waiting for
2354
+ * anyone. Honouring it everywhere gives the operator ONE verb that closes;
2355
+ * refusing it here would leave a list drainable only by writing a sham task.
2356
+ *
2357
+ * What it will not do is decide a decision's fate on anything it cannot check.
2358
+ * A task naming an id that no decision in the store has does NOT count as
2359
+ * carrying it, and a store it could only partly read says so rather than
2360
+ * reporting a clean answer over the part it managed.
2361
+ */
2362
+ /**
2363
+ * Start of the population: decisions recorded before this instant are out of
2364
+ * scope, whatever else is true of them.
2365
+ *
2366
+ * A fixed constant rather than per-workspace state, so every workspace answers
2367
+ * the same question and nothing has to be initialised or migrated. The value is
2368
+ * the instant this was written; a release landing later, or a workspace first
2369
+ * running it later, only means that run starts with the decisions recorded in
2370
+ * between, and {@link DecisionGapsSummary.scope} reports the boundary on every
2371
+ * run so the head start is never silent. `--since` overrides it.
2372
+ */
2373
+ declare const DECISION_GAPS_EPOCH = "2026-09-18T12:00:00.000Z";
2374
+ /** A decision in the population that no task carries. */
2375
+ type DecisionGap = {
2376
+ decisionId: string;
2377
+ title: string;
2378
+ recordedAt: string;
2379
+ sessionId: string;
2380
+ };
2381
+ /** The population's boundaries, as the run actually applied them. */
2382
+ type DecisionGapsScope = {
2383
+ /** Decisions recorded before this instant are out of scope. */
2384
+ start: string;
2385
+ /** Only decisions this event source recorded are in scope. */
2386
+ source: string;
2387
+ };
2388
+ /**
2389
+ * Why decisions left the population, by ground. The grounds are applied in this
2390
+ * order and each decision is counted under the FIRST that excludes it, so these
2391
+ * partition the excluded set.
2260
2392
  */
2261
- type PresetStrings = {
2262
- repoBlock: {
2263
- heading: string;
2264
- intro: string;
2265
- /** Source git-visibility, rendered with the consequence the agent must respect. */
2266
- visibilityLabel: (v: RepoVisibility | undefined) => string;
2267
- /**
2268
- * Source language (commits/comments/code), rendered with the audience it
2269
- * serves. Invariant note: the table itself is SELECTED by this same field
2270
- * (ja -> JA table, everything else -> EN), so the JA table's en / en+ja /
2271
- * unset branches and the EN table's ja branch are unreachable from
2272
- * renderPresetBlock — they exist for table completeness (and the
2273
- * both-language sweep test), not because a render can emit them.
2274
- */
2275
- sourceLanguageLabel: (l: RepoLanguage | undefined) => string;
2276
- /** Published-surface kind. */
2277
- publishKindLabel: (k: PublishTarget["kind"]) => string;
2278
- /** A published surface's visibility (independent of the source repo's). */
2279
- publishVisibilityLabel: (v: RepoVisibility | undefined) => string;
2280
- /** A published surface's content language (read by end users; may differ from source). */
2281
- contentLanguageLabel: (l: RepoLanguage | undefined) => string;
2282
- /** "ソース可視性" — the source-visibility line label. */
2283
- sourceVisibilityLabel: string;
2284
- /** "ソース言語" — the source-language line label. */
2285
- sourceLanguageLineLabel: string;
2286
- /** "- 配信物: なし" — no published surfaces. */
2287
- publishesNone: string;
2288
- /** "- 配信物:" — the published-surfaces list header. */
2289
- publishesHeader: string;
2290
- };
2291
- viewBlock: {
2292
- heading: string;
2293
- intro: string;
2294
- selfNote: (viewName: string) => string;
2295
- aggregates: (repoCount: number) => string;
2296
- reposHeading: string;
2297
- tableHeader: string;
2298
- /** Instruction-file ownership labels: who writes the repo's AGENTS.md. */
2299
- instructionsAnchor: string;
2300
- instructionsSelf: string;
2301
- instructionsHub: string;
2302
- /** "未設定" — the short table cell for an undeclared visibility / language. */
2303
- unsetShort: string;
2304
- commitHeading: string;
2305
- commitBody: string;
2306
- conventionsHeading: string;
2307
- conventionsBody: string;
2308
- principlesHeading: string;
2309
- principleStateless: string;
2310
- principleNoFiles: string;
2311
- };
2312
- anchorStarter: {
2313
- identityLine: (title: string) => string;
2314
- starterNote: string;
2315
- basicsHeading: string;
2316
- basicsTodo: string;
2317
- commitHeading: string;
2318
- commitPlanning: string;
2319
- commitImplementation: string;
2320
- commitView: string;
2321
- conventionsHeading: string;
2322
- conventionsBody: string;
2323
- viewPointerLine: (viewName: string) => string;
2324
- policyHeading: string;
2325
- policyTodo: string[];
2326
- };
2393
+ type DecisionGapsExcluded = {
2394
+ /** Recorded before {@link DecisionGapsScope.start}. */
2395
+ byStart: number;
2396
+ /** Recorded by something other than {@link DecisionGapsScope.source}. */
2397
+ bySource: number;
2398
+ /** `kind: "track"` already resurfaced every session until closed. */
2399
+ track: number;
2400
+ /** Closed with `basou decision void`. */
2401
+ voided: number;
2327
2402
  };
2328
- /** Look up the instruction-file string table for a resolved content language. */
2329
- declare function presetStrings(language: ViewLanguage): PresetStrings;
2330
-
2331
- /** Session lifecycle states. */
2332
- declare const SessionStatusSchema: z.ZodEnum<{
2333
- initialized: "initialized";
2334
- running: "running";
2335
- waiting_approval: "waiting_approval";
2336
- completed: "completed";
2337
- failed: "failed";
2338
- interrupted: "interrupted";
2339
- imported: "imported";
2340
- archived: "archived";
2341
- }>;
2342
- /** Inferred runtime type for {@link SessionStatusSchema}. */
2343
- type SessionStatus = z.infer<typeof SessionStatusSchema>;
2344
2403
  /**
2345
- * Source kind that produced the session.
2404
+ * What the run could not read. Every field here can only make {@link
2405
+ * DecisionGapsSummary.gaps} wrong in a way the reader cannot see, so a non-zero
2406
+ * count is reported rather than absorbed.
2407
+ */
2408
+ type DecisionGapsIncomplete = {
2409
+ /**
2410
+ * Sessions this run could not read in full — a `session.yaml` that is missing
2411
+ * or does not validate, or an event log that failed to read.
2412
+ *
2413
+ * What was lost is not knowable from here: a log that fails partway through
2414
+ * has already yielded some events, so a session counted here may have
2415
+ * contributed everything, nothing, or part of what it holds. A decision of
2416
+ * theirs can therefore be missing from every count, and a void of theirs may
2417
+ * not have closed what it closes — so both a missing row and a wrongly
2418
+ * present one are possible.
2419
+ */
2420
+ sessions: number;
2421
+ /** Task files that could not be read, so any decision they carry looks uncarried. */
2422
+ tasks: number;
2423
+ /**
2424
+ * Ids named by a task that no decision in the store has. Counted, and
2425
+ * deliberately NOT treated as carrying anything: a string nobody recorded
2426
+ * must not be able to take a decision off this list.
2427
+ */
2428
+ unknownReferences: number;
2429
+ };
2430
+ type DecisionGapsSummary = {
2431
+ generatedAt: string;
2432
+ scope: DecisionGapsScope;
2433
+ /** Decisions with no task carrying them, newest first. Capped by `limit`. */
2434
+ gaps: DecisionGap[];
2435
+ /** Entries omitted from `gaps` by `limit`; 0 when nothing was cut. */
2436
+ truncated: number;
2437
+ /** Population members some task carries. */
2438
+ carried: number;
2439
+ /** `gaps.length + truncated + carried`. */
2440
+ populationCount: number;
2441
+ excluded: DecisionGapsExcluded;
2442
+ incomplete: DecisionGapsIncomplete;
2443
+ /** Task files read (live and archived). */
2444
+ tasksScanned: number;
2445
+ };
2446
+ type DecisionGapsInput = {
2447
+ paths: BasouPaths;
2448
+ nowIso: string;
2449
+ /** Defaults to {@link DECISION_GAPS_EPOCH}. */
2450
+ start?: string;
2451
+ /**
2452
+ * Maximum entries in `gaps`; the rest are counted in `truncated`. `0` yields
2453
+ * an empty `gaps` with everything in `truncated` — it is a cap of zero, not
2454
+ * "uncapped". Omit the field for uncapped. (The CLI's `--limit 0` means
2455
+ * uncapped and omits this.)
2456
+ */
2457
+ limit?: number;
2458
+ onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2459
+ onSessionSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2460
+ };
2461
+ /**
2462
+ * Find decisions in the population that no task carries.
2346
2463
  *
2347
- * - `claude-code-adapter` a live `basou run claude-code` process wrap.
2348
- * - `claude-code-import` derived after the fact from a Claude Code native
2349
- * transcript (`~/.claude/projects/*.jsonl`) by `basou import claude-code`.
2350
- * - `codex-adapter` — a live `basou run codex` process wrap.
2351
- * - `codex-import` — derived after the fact from an OpenAI Codex native
2352
- * rollout log (date-partitioned `~/.codex/sessions`) by `basou import codex`.
2353
- * - `import` — a round-trip of a Basou-format export (`basou session import`).
2354
- * - `human` / `terminal` — manually-authored / terminal-recorded sessions.
2464
+ * Ordering is `recordedAt` descending with the decision id (a ULID, so
2465
+ * monotonic) as tie-breaker, giving a stable newest-first list.
2355
2466
  */
2356
- declare const SessionSourceKindSchema: z.ZodEnum<{
2357
- "claude-code-adapter": "claude-code-adapter";
2358
- import: "import";
2359
- "claude-code-import": "claude-code-import";
2360
- "codex-adapter": "codex-adapter";
2361
- "codex-import": "codex-import";
2362
- human: "human";
2363
- terminal: "terminal";
2364
- }>;
2365
- /** Inferred runtime type for {@link SessionSourceKindSchema}. */
2366
- type SessionSourceKind = z.infer<typeof SessionSourceKindSchema>;
2467
+ declare function findDecisionGaps(input: DecisionGapsInput): Promise<DecisionGapsSummary>;
2468
+
2367
2469
  /**
2368
- * Optional per-session metrics, computed at import time from the source tool's
2369
- * native log. Two groups, both optional because not every source records them:
2470
+ * Project roster drift (the "saddle" model). A project's repos are DECLARED
2471
+ * once in the manifest's `repos` list; the capture config (`source_roots`) must
2472
+ * cover every declared repo. This computes the drift between the two so
2473
+ * `basou project check` can surface a declared repo that is NOT being captured
2474
+ * — the class of bug where a companion repo was wired into the workspace but
2475
+ * never added to `source_roots`, so its work silently fell out of capture.
2370
2476
  *
2371
- * - Model-usage rollup (`*_tokens`): the transcript carries per-message token
2372
- * usage; these are the session totals. `reasoning_output_tokens` is
2373
- * Codex-only, and live `run`/`exec` sessions carry no token usage at all.
2374
- * - Engaged-time metrics (`active_*`): the billing-oriented active time derived
2375
- * from the session's genuine engagement timestamps (conversation turns plus
2376
- * action events), with idle gaps capped. `active_intervals` are the merged
2377
- * wall-clock ranges (so cross-session totals can de-duplicate overlapping
2378
- * work by interval union); `active_time_ms` is their summed duration;
2379
- * `active_gap_cap_ms` and `active_time_method` lock the methodology so the
2380
- * stored numbers stay interpretable if the method changes later. When a
2381
- * source records explicit per-turn intervals (Codex), `active_time_method` is
2382
- * `turn-intervals` and the in-turn time is the log's real wall-clock span
2383
- * rather than a gap-capped approximation; the active semantics are unchanged.
2384
- * - `machine_active_time_ms`: model compute time — the summed duration of the
2385
- * source's per-turn spans (Codex `task_complete.duration_ms`), a SUBSET of a
2386
- * single session's engaged active time. Unlike `active_intervals` it is a
2387
- * plain sum, NOT wall-clock-deduplicated, so two concurrent sessions can sum
2388
- * past their billable (union) active wall-clock — that is intended (two models
2389
- * working at once did two machine-hours in one wall-clock hour). Captured only
2390
- * for sources that record per-turn duration (Codex); absent otherwise.
2391
- *
2392
- * Absent on sessions imported before a given field existed (re-import to
2393
- * backfill). Live sessions carry no engaged-time metrics and fall back to
2394
- * event-derived active time at stats time.
2477
+ * Pure: it compares declared relative paths against captured relative paths and
2478
+ * performs no filesystem or git I/O. Paths are compared as declared (both lists
2479
+ * use the same machine-portable relative-path form), not resolved on disk.
2395
2480
  */
2396
- declare const SessionMetricsSchema: z.ZodObject<{
2397
- output_tokens: z.ZodOptional<z.ZodNumber>;
2398
- input_tokens: z.ZodOptional<z.ZodNumber>;
2399
- cached_input_tokens: z.ZodOptional<z.ZodNumber>;
2400
- reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
2401
- active_time_ms: z.ZodOptional<z.ZodNumber>;
2402
- active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
2403
- start: z.ZodString;
2404
- end: z.ZodString;
2405
- }, z.core.$loose>>>;
2406
- active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
2407
- active_time_method: z.ZodOptional<z.ZodString>;
2408
- machine_active_time_ms: z.ZodOptional<z.ZodNumber>;
2409
- }, z.core.$loose>;
2410
- /** Inferred runtime type for {@link SessionMetricsSchema}. */
2411
- type SessionMetrics = z.infer<typeof SessionMetricsSchema>;
2481
+ type RepoVisibility = "public" | "private" | "future-public";
2412
2482
  /**
2413
- * Tamper-evidence head anchor for a session whose `events.jsonl` is hash
2414
- * chained: `head_hash` is the hex sha-256 of the last written event line
2415
- * (excluding the trailing newline), `event_count` the number of chained lines.
2416
- * Written by the import / in-place re-import writers and, for a live session
2417
- * (`exec` / `run` / ad-hoc), by the finalize once it reaches a terminal status.
2418
- * Absent on a still-live session (the anchor is stamped at finalize) and on a
2419
- * pre-feature unchained session. Additive optional => no schema_version bump.
2420
- * `.strict()` because the writers fully own the shape.
2483
+ * The audience-driven language axis. Independent of visibility: a private repo
2484
+ * can publish English content, a public repo can carry bilingual docs. `en` /
2485
+ * `ja` for a single audience, `en+ja` when both are served.
2421
2486
  */
2422
- declare const SessionIntegritySchema: z.ZodObject<{
2423
- head_hash: z.ZodString;
2424
- event_count: z.ZodNumber;
2425
- }, z.core.$strict>;
2426
- /** Inferred runtime type for {@link SessionIntegritySchema}. */
2427
- type SessionIntegrity = z.infer<typeof SessionIntegritySchema>;
2487
+ type RepoLanguage = "en" | "ja" | "en+ja";
2488
+ /** A published surface a repo emits: a deployed website or a package registry. */
2489
+ type PublishKind = "web" | "npm";
2428
2490
  /**
2429
- * Schema for `.basou/sessions/<session_id>/session.yaml`. The minimal
2430
- * session document carries the actual fields nested under the outer
2431
- * `session:` key.
2491
+ * Where a repo's agent instruction files live (the instruction-source axis),
2492
+ * independent of visibility / language / publishes. `hub` is basou's native,
2493
+ * generated hub-and-spoke topology (canonical in the anchor, gitignored symlinks
2494
+ * in each repo); `self` is the additive opt-in where the canonical AGENTS.md is a
2495
+ * regular committed file in the repo itself and basou stays hands-off about its
2496
+ * content. See {@link instructionMode} for the default (absent => `hub`).
2432
2497
  */
2433
- declare const SessionSchema: z.ZodObject<{
2434
- schema_version: z.ZodString;
2435
- session: z.ZodObject<{
2436
- id: z.ZodString & z.ZodType<`ses_${string}`, string, z.core.$ZodTypeInternals<`ses_${string}`, string>>;
2437
- label: z.ZodOptional<z.ZodString>;
2438
- task_id: z.ZodOptional<z.ZodNullable<z.ZodString & z.ZodType<`task_${string}`, string, z.core.$ZodTypeInternals<`task_${string}`, string>>>>;
2439
- workspace_id: z.ZodString & z.ZodType<`ws_${string}`, string, z.core.$ZodTypeInternals<`ws_${string}`, string>>;
2440
- source: z.ZodObject<{
2441
- kind: z.ZodEnum<{
2442
- "claude-code-adapter": "claude-code-adapter";
2443
- import: "import";
2444
- "claude-code-import": "claude-code-import";
2445
- "codex-adapter": "codex-adapter";
2446
- "codex-import": "codex-import";
2447
- human: "human";
2448
- terminal: "terminal";
2449
- }>;
2450
- version: z.ZodLiteral<"0.1.0">;
2451
- external_id: z.ZodOptional<z.ZodString>;
2452
- source_size_bytes: z.ZodOptional<z.ZodNumber>;
2453
- }, z.core.$loose>;
2454
- started_at: z.ZodString;
2455
- ended_at: z.ZodOptional<z.ZodString>;
2456
- status: z.ZodEnum<{
2457
- initialized: "initialized";
2458
- running: "running";
2459
- waiting_approval: "waiting_approval";
2460
- completed: "completed";
2461
- failed: "failed";
2462
- interrupted: "interrupted";
2463
- imported: "imported";
2464
- archived: "archived";
2465
- }>;
2466
- working_directory: z.ZodString;
2467
- invocation: z.ZodObject<{
2468
- command: z.ZodString;
2469
- args: z.ZodDefault<z.ZodArray<z.ZodString>>;
2470
- exit_code: z.ZodNullable<z.ZodNumber>;
2471
- }, z.core.$loose>;
2472
- related_files: z.ZodDefault<z.ZodArray<z.ZodString>>;
2473
- events_log: z.ZodDefault<z.ZodString>;
2474
- summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2475
- metrics: z.ZodOptional<z.ZodObject<{
2476
- output_tokens: z.ZodOptional<z.ZodNumber>;
2477
- input_tokens: z.ZodOptional<z.ZodNumber>;
2478
- cached_input_tokens: z.ZodOptional<z.ZodNumber>;
2479
- reasoning_output_tokens: z.ZodOptional<z.ZodNumber>;
2480
- active_time_ms: z.ZodOptional<z.ZodNumber>;
2481
- active_intervals: z.ZodOptional<z.ZodArray<z.ZodObject<{
2482
- start: z.ZodString;
2483
- end: z.ZodString;
2484
- }, z.core.$loose>>>;
2485
- active_gap_cap_ms: z.ZodOptional<z.ZodNumber>;
2486
- active_time_method: z.ZodOptional<z.ZodString>;
2487
- machine_active_time_ms: z.ZodOptional<z.ZodNumber>;
2488
- }, z.core.$loose>>;
2489
- integrity: z.ZodOptional<z.ZodObject<{
2490
- head_hash: z.ZodString;
2491
- event_count: z.ZodNumber;
2492
- }, z.core.$strict>>;
2493
- }, z.core.$loose>;
2494
- }, z.core.$loose>;
2495
- /** Inferred runtime type for {@link SessionSchema}. */
2496
- type Session = z.infer<typeof SessionSchema>;
2497
-
2498
+ type RepoInstructions = "hub" | "self";
2498
2499
  /**
2499
- * Threshold above which a still-`running` session with no `session_ended`
2500
- * event is flagged suspect.
2501
- *
2502
- * 24h: long enough that an active long-running session will not be flagged,
2503
- * short enough that an abandoned process is surfaced within a working day.
2504
- * Tunable via CLI option in a later step (continuation backlog #23).
2500
+ * One published surface. Its visibility and language are INDEPENDENT of the
2501
+ * source repo's: a private repo commonly publishes a public website. Both are
2502
+ * optional so a surface can be declared
2503
+ * before those facts are pinned down (mirroring how `adopt` leaves repo
2504
+ * visibility unset for the operator to fill in).
2505
2505
  */
2506
- declare const STUCK_THRESHOLD_MS: number;
2507
- type SuspectReason = "events_say_ended_but_yaml_running" | "running_no_end_event";
2508
- type SessionEntry = {
2509
- sessionId: string;
2510
- session: Session;
2511
- suspect: boolean;
2512
- suspectReason: SuspectReason | null;
2513
- /**
2514
- * The trail store this entry was read from. Its `sessions` directory locates
2515
- * the session's `events.jsonl`, so a federated caller can replay events from
2516
- * the store the session actually lives in (not the local store). For a plain
2517
- * local load this is the `paths` passed to {@link loadSessionEntries}.
2518
- */
2519
- sourceRoot: BasouPaths;
2506
+ type PublishTarget = {
2507
+ kind: PublishKind;
2508
+ visibility?: RepoVisibility | undefined;
2509
+ language?: RepoLanguage | undefined;
2510
+ };
2511
+ type RepoEntry = {
2512
+ /** Path relative to the manifest repo root (e.g. ".", "../takuhon"). */
2513
+ path: string;
2514
+ visibility?: RepoVisibility | undefined;
2515
+ /** Source language (commits/comments/code, read by contributors). Independent of visibility. */
2516
+ language?: RepoLanguage | undefined;
2517
+ /** Published surfaces this repo emits (opt-in; absent for a repo that publishes nothing). */
2518
+ publishes?: PublishTarget[] | undefined;
2520
2519
  /**
2521
- * Federation host label from the registry (`~/.basou/hosts.yaml`), or `null`
2522
- * for the local store. Surfaced by orientation so a merged, multi-host view
2523
- * can attribute the latest session / decision / next-step to its host.
2520
+ * Instruction-source mode. Absent => `hub` (basou's native generated topology),
2521
+ * so an existing roster's behavior is unchanged. `self` opts the repo out of
2522
+ * generation: its AGENTS.md is a hand-authored committed file and basou stays
2523
+ * hands-off. Resolve the effective mode with {@link instructionMode}.
2524
2524
  */
2525
- host: string | null;
2525
+ instructions?: RepoInstructions | undefined;
2526
2526
  };
2527
2527
  /**
2528
- * Per-session degradation reason emitted by {@link loadSessionEntries.onSkip}.
2529
- *
2530
- * - `session_yaml_missing` (ENOENT) and `session_yaml_invalid` (parse or schema
2531
- * failure) both omit the entry from the result.
2532
- * - `events_jsonl_unreadable` still pushes the entry with `suspect=false` so
2533
- * the session row remains visible to the caller; only the suspect check is
2534
- * degraded. Matches the existing CLI behaviour at
2535
- * `packages/cli/src/commands/session.ts` (suspect-check stderr warning).
2528
+ * The effective instruction-source mode for a repo: the declared `instructions`,
2529
+ * defaulting to `hub` when absent. The default is the single guarantee that an
2530
+ * existing roster (which has no `instructions` field) keeps basou's current
2531
+ * hub-and-spoke behavior byte-for-byte every generator branches on this, never
2532
+ * on the raw optional field, so "absent => hub" is decided in exactly one place.
2536
2533
  */
2537
- type SessionSkipReason = "session_yaml_missing" | "session_yaml_invalid" | "events_jsonl_unreadable";
2538
- type LoadSessionEntriesOptions = {
2534
+ declare function instructionMode(entry: {
2535
+ instructions?: RepoInstructions | undefined;
2536
+ }): RepoInstructions;
2537
+ type RosterDriftSummary = {
2538
+ declaredCount: number;
2539
+ capturedCount: number;
2540
+ /** Declared in `repos` but absent from `source_roots`: a capture gap. */
2541
+ gaps: RepoEntry[];
2542
+ /** In `source_roots` but not declared in `repos` (e.g. a workspace view, or a stray). */
2543
+ extra: string[];
2544
+ /** Declared paths that are also captured. */
2545
+ matched: string[];
2546
+ /** True when there is no capture gap (every declared repo is covered). */
2547
+ ok: boolean;
2548
+ };
2549
+ /**
2550
+ * Compute the {@link RosterDriftSummary} for a project. A declared repo missing
2551
+ * from the captured set is a `gap` (the surfaced suspicion); a captured path not
2552
+ * in the declared set is `extra` (commonly the workspace view, which is a
2553
+ * capture source but not itself a project repo). With no declared roster, there
2554
+ * are no gaps (nothing to check against) and every captured path is `extra`.
2555
+ */
2556
+ declare function summarizeRosterDrift(input: {
2557
+ repos?: RepoEntry[];
2558
+ sourceRoots?: string[];
2559
+ }): RosterDriftSummary;
2560
+ type SourceRootsReconcile = {
2539
2561
  /**
2540
- * Single `now` shared across every {@link classifySuspect} call so that
2541
- * sessions classified back-to-back observe the same instant. Avoids
2542
- * boundary races where a session at age 24h would flip between calls.
2562
+ * The reconciled `source_roots`: the existing entries verbatim, then every
2563
+ * declared repo path that was missing (normalized, in roster order). Existing
2564
+ * order and form are preserved so the manifest diff is minimal and reversible.
2543
2565
  */
2544
- now: Date;
2545
- onWarning?: (warning: ReplayWarning, sessionId: string) => void;
2546
- onSkip?: (sessionId: string, reason: SessionSkipReason) => void;
2566
+ next: string[];
2567
+ /** Declared repo paths (normalized) that were appended because `source_roots` did not cover them. */
2568
+ added: string[];
2569
+ /** True when `source_roots` already covers every declared repo (`next` equals the current list). */
2570
+ unchanged: boolean;
2547
2571
  };
2548
2572
  /**
2549
- * A trail store to read in a federated load, tagged with its host label.
2550
- * `host: null` denotes the local store; a non-null label comes from the host
2551
- * registry (`~/.basou/hosts.yaml`). `paths` is where that store is reachable
2552
- * as a local path on this machine (an SSHFS mount, an rsync mirror, etc.) —
2553
- * basou itself never performs any network I/O to obtain it.
2573
+ * Derive the `source_roots` a project's declared repo roster requires. The
2574
+ * roster (`repos`) is the single source of truth for which repos belong to the
2575
+ * project; this is the actuator behind `basou project sync`, computing the
2576
+ * additive reconciliation so every declared repo is captured.
2577
+ *
2578
+ * ADDITIVE ONLY: it appends declared paths that are missing and never removes
2579
+ * an existing entry. A captured-but-undeclared path (commonly the generated
2580
+ * workspace view — a legitimate capture source that is not itself a project
2581
+ * repo) is preserved; pruning strays is deferred to the slice that generates
2582
+ * the view (so basou knows which extras it owns). Existing entries are kept
2583
+ * byte-identical; only appended paths are normalized.
2584
+ *
2585
+ * Pure: no filesystem or git I/O. Paths are compared in the same normalized
2586
+ * form as {@link summarizeRosterDrift}, so a trailing-slash variant of an
2587
+ * already-captured repo is not re-appended.
2554
2588
  */
2555
- type FederatedRoot = {
2556
- paths: BasouPaths;
2557
- host: string | null;
2589
+ declare function reconcileSourceRoots(input: {
2590
+ repos?: RepoEntry[];
2591
+ sourceRoots?: string[];
2592
+ }): SourceRootsReconcile;
2593
+ /**
2594
+ * On-disk classification of a source-root candidate during adoption: a git repo
2595
+ * root (→ becomes a roster entry), a resolved-but-non-repo directory (the
2596
+ * generated workspace view, `/tmp`, a scratch dir → excluded), or a path that
2597
+ * could not be resolved on disk (→ excluded).
2598
+ */
2599
+ type AdoptCandidateKind = "repo" | "non-repo" | "unresolved";
2600
+ type AdoptCandidate = {
2601
+ /** Source-root path as declared (relative to the manifest root). */
2602
+ path: string;
2603
+ /** On-disk classification; the filesystem probing that produces it is the caller's job. */
2604
+ kind: AdoptCandidateKind;
2558
2605
  };
2559
- type LoadFederatedOptions = LoadSessionEntriesOptions & {
2560
- /**
2561
- * Called when a NON-local root cannot be enumerated (present-but-unreadable
2562
- * mount, permission error). That root is skipped best-effort so the local
2563
- * store and other roots still load. The local root (`host: null`) is never
2564
- * degraded here — its errors propagate, preserving single-store behaviour.
2565
- * (An absent root path is not an error: {@link enumerateSessionDirs} returns
2566
- * `[]` on ENOENT, so a dropped mount is simply an empty host.)
2567
- */
2568
- onRootUnavailable?: (host: string, error: unknown) => void;
2606
+ type RosterAdoptionPlan = {
2607
+ /** Proposed `repos` entries: the candidates that are git repos (visibility left unset for the operator). */
2608
+ repos: RepoEntry[];
2609
+ /** Candidates excluded from the roster, with why (a non-repo directory, or an unresolvable path). */
2610
+ excluded: {
2611
+ path: string;
2612
+ kind: Exclude<AdoptCandidateKind, "repo">;
2613
+ }[];
2614
+ };
2615
+ /**
2616
+ * Plan a `repos` roster from classified source-root candidates (the actuator
2617
+ * behind `basou project adopt`). Pure: it partitions already-classified
2618
+ * candidates — the realpath / `.git` filesystem probing that produces each
2619
+ * `kind` is the caller's job, so this stays testable without disk I/O.
2620
+ *
2621
+ * A git repo becomes a roster entry (path only; visibility is left unset because
2622
+ * it is a human judgment, kept independent of the other axes). A non-repo
2623
+ * (commonly the generated workspace view) or an unresolvable path is excluded and
2624
+ * reported, so the operator sees what was dropped and why before editing. Repo
2625
+ * paths are deduped by normalized form, preserving the first declared form and
2626
+ * order.
2627
+ */
2628
+ declare function planRosterAdoption(candidates: AdoptCandidate[]): RosterAdoptionPlan;
2629
+
2630
+ /**
2631
+ * The language of the GENERATED-VIEW chrome (headings, labels, verdict prose)
2632
+ * in handoff.md / orientation.md / decisions.md / report output.
2633
+ *
2634
+ * This is deliberately narrower than the manifest's repo `language` axis
2635
+ * (`en | ja | en+ja`): a generated view has exactly one chrome language, so
2636
+ * `en+ja` resolves to `en`. User data (decision titles, notes, labels, file
2637
+ * paths) always passes through verbatim — only the tool-generated strings are
2638
+ * localized, which is exactly the split this type exists to keep honest.
2639
+ */
2640
+ type ViewLanguage = "en" | "ja";
2641
+ /**
2642
+ * Resolve the generated-view language from a manifest: the workspace speaks
2643
+ * the language of its ANCHOR repo (the `repos[]` entry whose path is `.`).
2644
+ *
2645
+ * Rules (fixed by design):
2646
+ * - anchor declares `ja` -> `ja`
2647
+ * - anchor declares `en` / `en+ja` -> `en` (a bilingual surface renders one
2648
+ * chrome; en is the shared floor)
2649
+ * - no roster / no anchor entry / no declared language -> `en` (the default
2650
+ * for basou's English-first OSS surface)
2651
+ *
2652
+ * Binding the view to the anchor's language is a deliberate, documented
2653
+ * coupling: the anchor is the planning/trail home the views live in, so its
2654
+ * declared audience is the views' audience. Other repos' languages do not
2655
+ * participate.
2656
+ */
2657
+ declare function resolveViewLanguage(manifest: Pick<Manifest, "repos"> | null): ViewLanguage;
2658
+ /**
2659
+ * Manifest-reading convenience for the renderers: resolve the view language
2660
+ * for a workspace, defaulting to `en` when the manifest is missing or
2661
+ * unreadable (mirrors the orientation renderer's tolerant source_roots read —
2662
+ * a broken manifest must never break a view render).
2663
+ */
2664
+ declare function resolveViewLanguageFromPaths(paths: BasouPaths): Promise<ViewLanguage>;
2665
+ /**
2666
+ * Every localized string the four view renderers emit, grouped per renderer
2667
+ * with a small `common` set for lines that are byte-identical across views.
2668
+ * Parameterized lines are functions so the two languages can order their
2669
+ * parts naturally.
2670
+ *
2671
+ * This module is the SINGLE home for generated Japanese — the view chrome here
2672
+ * and the instruction-file content in {@link PresetStrings} (the E-5
2673
+ * language-lint allowlist points here, not at the renderers/generators), so
2674
+ * "user data language" and "tool-generated content language" can never blur
2675
+ * together again.
2676
+ */
2677
+ type ViewStrings = {
2678
+ /** Localized relative age for prose lines, e.g. "3日4時間前" / "3d 4h ago". */
2679
+ relativeAge: (startedAt: string | null, now: Date) => string;
2680
+ common: {
2681
+ /** "最終 session" — the latest live session pointer. */
2682
+ lastSessionLabel: string;
2683
+ /** "直近の判断" — the latest recorded decision pointer. */
2684
+ latestDecisionLabel: string;
2685
+ /** "直近の変更ファイル" — the latest session's related files. */
2686
+ recentFilesLabel: string;
2687
+ /** "理由" — a track's rationale label. */
2688
+ trackWhyLabel: string;
2689
+ /** Note that the latest decision comes from a different session. */
2690
+ decisionOtherSessionNote: (shortSessionId: string) => string;
2691
+ };
2692
+ orientation: {
2693
+ headingWhere: string;
2694
+ headingRecent: (sessionCount: number) => string;
2695
+ headingInFlight: string;
2696
+ headingForward: string;
2697
+ headingCurrency: string;
2698
+ inFlightTasksHeading: (n: number) => string;
2699
+ /**
2700
+ * Body line under the in-flight-tasks heading when NO task was ever
2701
+ * recorded here. "(none)" states that nothing is pending — a claim about
2702
+ * the work. This one claims only what it can see: that the record is
2703
+ * empty. It says nothing about whether the workspace should use tasks,
2704
+ * and names no command: the renderers report position, and a nudge that
2705
+ * cannot be silenced is noise (see `trackNudge`, which is gated).
2706
+ */
2707
+ noTasksRecorded: string;
2708
+ /**
2709
+ * Body line under the in-flight-tasks heading when tasks ARE on record
2710
+ * here and every one of them parsed, with none open. A bare "(none)" is
2711
+ * literally true and still misread: it answers "is anything in flight?"
2712
+ * with a word that sounds like "nothing is happening". Work can be under
2713
+ * way and simply not filed as a task. This line leads with what the
2714
+ * record does hold, so it cannot be skimmed as its sibling below, and it
2715
+ * claims nothing about the work.
2716
+ */
2717
+ noTasksInFlight: string;
2718
+ /**
2719
+ * Same heading, but a task file could not be read on THIS pass, so its
2720
+ * status is unknown and "none in flight" would be an assertion the
2721
+ * renderer cannot support. Says what it can see and stops.
2722
+ *
2723
+ * A standing condition, not a one-pass report: it holds every render until
2724
+ * the file is repaired or removed. It used to give way to its sibling on
2725
+ * the second render, because rebuilding the task index dropped the file it
2726
+ * could not parse and nothing enumerated it again.
2727
+ */
2728
+ tasksUnreadable: string;
2729
+ /**
2730
+ * Appended under a NON-empty in-flight list when some task file could not
2731
+ * be read.
2732
+ *
2733
+ * The list above it is true and incomplete at the same time, and a reader
2734
+ * has no way to tell from a heading count that anything is missing. The
2735
+ * zero case already says so; saying nothing here would make "unreadable" a
2736
+ * fact basou reports only when it happens to have nothing else to report.
2737
+ */
2738
+ tasksUnreadableAlongside: (n: number) => string;
2739
+ pendingApprovalsHeading: (n: number) => string;
2740
+ suspectSessionsHeading: (n: number) => string;
2741
+ openTracksHeading: (n: number) => string;
2742
+ /** Stale-decision honesty note under 直近の判断. */
2743
+ decisionStaleNote: (activityAge: string) => string;
2744
+ outOfRootWarning: (count: number, files: string) => string;
2745
+ recentEmpty: string;
2746
+ recentDecisionsLabel: string;
2747
+ recentNextStepLabel: string;
2748
+ recentChangedLabel: string;
2749
+ /** Trails the recent-files line when scratch paths were left out of it. */
2750
+ scratchOmitted: (count: number) => string;
2751
+ trackCloseInstruction: string;
2752
+ /** Forward-section pointer to `basou decision gaps`; omitted when the count is 0. */
2753
+ decisionGapsLine: (n: number) => string;
2754
+ nextStepRecordedLabel: (age: string) => string;
2755
+ noteStaleNote: (activityAge: string) => string;
2756
+ fallbackStaleDirection: string;
2757
+ fallbackStaleReferenceLabel: string;
2758
+ trackNudge: string;
2759
+ federatedFreshnessNote: string;
2760
+ bannerUnverifiable: (n: number) => string;
2761
+ bannerStale: (parts: string) => string;
2762
+ partNew: (n: number) => string;
2763
+ partUpdated: (n: number) => string;
2764
+ partsJoiner: string;
2765
+ verdictUnverifiable: (n: number) => [string, string];
2766
+ verdictStale: (parts: string) => [string, string];
2767
+ verdictUpdatedOnly: (n: number) => [string, string];
2768
+ verdictSuspectsAlso: (n: number) => string;
2769
+ verdictEmpty: [string, string];
2770
+ verdictUnprobed: (rel: string, tool: string) => [string, string];
2771
+ verdictCurrent: (rel: string, tool: string, hasHosts: boolean) => string;
2772
+ verdictSuspectsCaveat: (n: number) => string;
2773
+ verdictScopeDisclaimer: string;
2774
+ toolTerminal: string;
2775
+ toolHuman: string;
2776
+ toolImport: string;
2777
+ toolUnknown: string;
2778
+ };
2779
+ handoff: {
2780
+ headingCurrentState: string;
2781
+ headingRecentFiles: string;
2782
+ headingLatestDecision: string;
2783
+ headingOpenTracks: string;
2784
+ headingUnresolved: string;
2785
+ headingReadNext: string;
2786
+ headingNextWork: string;
2787
+ headingSessions: string;
2788
+ lastTaskLabel: string;
2789
+ /** "Work to do next" placeholder: tasks exist, none are open. */
2790
+ noPendingTasks: string;
2791
+ /** "Work to do next" placeholder: no task was ever recorded. */
2792
+ noTasksRecorded: string;
2793
+ decisionStaleNote: string;
2794
+ trackCloseInstruction: string;
2795
+ };
2796
+ decisions: {
2797
+ dateLabel: string;
2798
+ trackKindLine: string;
2799
+ decisionLabel: string;
2800
+ };
2801
+ report: {
2802
+ headingSummary: string;
2803
+ headingVolume: string;
2804
+ headingDecisions: string;
2805
+ headingApprovals: string;
2806
+ headingTasks: string;
2807
+ headingChangedFiles: string;
2808
+ headingSessions: string;
2809
+ headingIntegrity: string;
2810
+ };
2569
2811
  };
2812
+ /** Look up the string table for a resolved view language. */
2813
+ declare function viewStrings(language: ViewLanguage): ViewStrings;
2570
2814
  /**
2571
- * List session directory names under `paths.sessions`, ULID ascending.
2572
- *
2573
- * - Returns `[]` when the sessions directory does not exist (empty workspace
2574
- * or pre-init state).
2575
- * - Throws `Error("Failed to enumerate sessions", { cause })` on other I/O.
2576
- * - Only directories are returned (`.gitkeep` and other files are filtered).
2577
- *
2578
- * Sort order is `Array.prototype.sort()` default (Unicode code-point
2579
- * compare). ULIDs are Crockford base32 in uppercase, so the natural sort
2580
- * is also chronological session-start order.
2581
- */
2582
- declare function enumerateSessionDirs(paths: BasouPaths): Promise<string[]>;
2583
- /**
2584
- * Read and validate `<paths.sessions>/<sessionId>/session.yaml`.
2585
- *
2586
- * - Re-throws the yaml-store fixed-message `"YAML file not found"` for
2587
- * ENOENT so the caller can branch on it.
2588
- * - Throws `Error("Failed to read session.yaml", { cause })` for parse
2589
- * failures and schema violations (cause is either the YAML parser error
2590
- * or the zod error).
2591
- */
2592
- declare function readSessionYaml(paths: BasouPaths, sessionId: string): Promise<Session>;
2593
- /**
2594
- * Apply a terminal-status mutation to a live session's `session.yaml` AND, in
2595
- * the same locked write, stamp the tamper-evidence head anchor derived from the
2596
- * on-disk `events.jsonl` tail. Used by the `exec` / `run` orchestrators for
2597
- * BOTH terminal writers (the normal end-of-run finalize and the spawn-failure
2598
- * `failed` finalize).
2599
- *
2600
- * Why locked + anchor-from-tail: live appends chain the LOG only and leave the
2601
- * anchor for finalize. Reading the final tail under the session lock means a
2602
- * foreign line appended just before finalize (e.g. a `decision record` attached
2603
- * to a still-running session) is included in the anchor, and a foreign attach
2604
- * that arrives after the terminal status is set is rejected by the attach gate
2605
- * — so the anchor can never disagree with the at-rest log. The whole-document
2606
- * read-modify-write also preserves any field a foreign locked writer set (e.g.
2607
- * a task attach's `task_id`).
2608
- *
2609
- * The anchor is written only when the log is actually chained with at least one
2610
- * line; a legacy unchained session (and an empty log) is left with no
2611
- * `integrity` anchor, matching the import writers. The mutator receives the
2612
- * full {@link Session} document and typically sets
2613
- * `session.session.status` / `ended_at` / `invocation.exit_code` /
2614
- * `related_files`.
2615
- *
2616
- * Throws the {@link inspectChainTail} errors (torn / mixed log), the
2617
- * {@link readSessionYaml} errors, a zod error if the mutation produces an
2618
- * invalid document, or `Error("Failed to overwrite YAML file")` on a disk
2619
- * failure.
2815
+ * Resolve a GENERATED INSTRUCTION-FILE's content language from the target
2816
+ * repo's declared `language`. Unlike the views (workspace-level artifacts that
2817
+ * follow the anchor), a preset block lives inside one repo's instruction file,
2818
+ * so its audience is that repo's declared audience: `ja` renders Japanese
2819
+ * (byte-identical to the pre-i18n output), `en` / `en+ja` / undeclared render
2820
+ * English (one content language per generated block; en is the shared floor).
2620
2821
  */
2621
- declare function finalizeSessionYaml(paths: BasouPaths, sessionId: string, mutate: (session: Session) => void): Promise<void>;
2822
+ declare function resolveRepoContentLanguage(language: RepoLanguage | undefined): ViewLanguage;
2622
2823
  /**
2623
- * Classify a `running` session as suspect using one of two rules:
2624
- *
2625
- * - Rule A (`events_say_ended_but_yaml_running`): events.jsonl contains a
2626
- * `session_ended` event but the session.yaml is still `running`. The
2627
- * session ended cleanly in the event log but the YAML write was lost or
2628
- * never reached.
2629
- * - Rule B (`running_no_end_event`): no `session_ended` event and the last
2630
- * event is older than {@link STUCK_THRESHOLD_MS}. The process likely
2631
- * crashed or was killed.
2632
- *
2633
- * Sessions that are not `running` are never suspect.
2824
+ * Resolve the content language of a WORKSPACE-LEVEL instruction artifact (the
2825
+ * view's AGENTS.md block, the anchor's starter) from an already-gathered
2826
+ * roster: the entry flagged `anchor` speaks for the workspace, mirroring the
2827
+ * views' anchor-language rule. No anchor entry (or no declared language)
2828
+ * resolves to English. When more than one entry carries the flag, the first
2829
+ * wins (declared order).
2634
2830
  *
2635
- * I/O failure on events.jsonl is re-thrown unwrapped so the caller can
2636
- * degrade with a warning instead of treating the session as healthy. The
2637
- * caller is also responsible for surfacing replay warnings via `onWarning`.
2831
+ * Note the anchor is identified by the CALLER-SET flag, not by this module:
2832
+ * {@link resolveViewLanguage} keys on the manifest path being `.`, while the
2833
+ * instruction-file callers flag the anchor by resolved-path identity. For a
2834
+ * conventional manifest (anchor declared as `.`) the two agree; a roster that
2835
+ * reaches the anchor only through an aliased path is where they can diverge,
2836
+ * and the caller's flag is authoritative for the instruction files.
2638
2837
  */
2639
- declare function classifySuspect(paths: BasouPaths, sessionId: string, session: Session, now: Date, onWarning?: (warning: ReplayWarning) => void): Promise<{
2640
- suspect: boolean;
2641
- suspectReason: SuspectReason | null;
2642
- }>;
2643
- declare function loadSessionEntries(paths: BasouPaths, options: LoadSessionEntriesOptions): Promise<SessionEntry[]>;
2838
+ declare function resolveAnchorContentLanguage(repos: ReadonlyArray<{
2839
+ anchor?: boolean | undefined;
2840
+ language?: RepoLanguage | undefined;
2841
+ }>): ViewLanguage;
2644
2842
  /**
2645
- * Federated load across multiple trail stores. Each root's sessions are tagged
2646
- * with that root's host label and `sourceRoot`, so a caller replays events from
2647
- * the store the session lives in. De-duped by `sessionId` (a per-host random
2648
- * ULID), then by `source.external_id` when present first occurrence wins, so
2649
- * pass the local root FIRST to keep it authoritative (e.g. over a re-imported
2650
- * copy of the same vendor session on another host). A non-local root that
2651
- * cannot be enumerated is reported via `onRootUnavailable` and skipped; the
2652
- * local root's errors propagate, matching {@link loadSessionEntries}.
2843
+ * Every localized string the instruction-file generators emit: the per-repo
2844
+ * preset block, the workspace view's block, and the anchor's starter. Lives in
2845
+ * this module for the same reason as {@link ViewStrings}: it is the SINGLE
2846
+ * home for generated Japanese, so the language-lint E-5 allowlist stays one
2847
+ * file and "generated content language" is always a declaration-driven table
2848
+ * lookup, never a hardcode.
2653
2849
  */
2654
- declare function loadFederatedSessionEntries(roots: ReadonlyArray<FederatedRoot>, options: LoadFederatedOptions): Promise<SessionEntry[]>;
2850
+ type PresetStrings = {
2851
+ repoBlock: {
2852
+ heading: string;
2853
+ intro: string;
2854
+ /** Source git-visibility, rendered with the consequence the agent must respect. */
2855
+ visibilityLabel: (v: RepoVisibility | undefined) => string;
2856
+ /**
2857
+ * Source language (commits/comments/code), rendered with the audience it
2858
+ * serves. Invariant note: the table itself is SELECTED by this same field
2859
+ * (ja -> JA table, everything else -> EN), so the JA table's en / en+ja /
2860
+ * unset branches and the EN table's ja branch are unreachable from
2861
+ * renderPresetBlock — they exist for table completeness (and the
2862
+ * both-language sweep test), not because a render can emit them.
2863
+ */
2864
+ sourceLanguageLabel: (l: RepoLanguage | undefined) => string;
2865
+ /** Published-surface kind. */
2866
+ publishKindLabel: (k: PublishTarget["kind"]) => string;
2867
+ /** A published surface's visibility (independent of the source repo's). */
2868
+ publishVisibilityLabel: (v: RepoVisibility | undefined) => string;
2869
+ /** A published surface's content language (read by end users; may differ from source). */
2870
+ contentLanguageLabel: (l: RepoLanguage | undefined) => string;
2871
+ /** "ソース可視性" — the source-visibility line label. */
2872
+ sourceVisibilityLabel: string;
2873
+ /** "ソース言語" — the source-language line label. */
2874
+ sourceLanguageLineLabel: string;
2875
+ /** "- 配信物: なし" — no published surfaces. */
2876
+ publishesNone: string;
2877
+ /** "- 配信物:" — the published-surfaces list header. */
2878
+ publishesHeader: string;
2879
+ };
2880
+ viewBlock: {
2881
+ heading: string;
2882
+ intro: string;
2883
+ selfNote: (viewName: string) => string;
2884
+ aggregates: (repoCount: number) => string;
2885
+ reposHeading: string;
2886
+ tableHeader: string;
2887
+ /** Instruction-file ownership labels: who writes the repo's AGENTS.md. */
2888
+ instructionsAnchor: string;
2889
+ instructionsSelf: string;
2890
+ instructionsHub: string;
2891
+ /** "未設定" — the short table cell for an undeclared visibility / language. */
2892
+ unsetShort: string;
2893
+ commitHeading: string;
2894
+ commitBody: string;
2895
+ conventionsHeading: string;
2896
+ conventionsBody: string;
2897
+ /** Heading for {@link handoffPointer}. Its own, so the pointer is not filed
2898
+ * under "Required reading" while telling the reader not to read it through. */
2899
+ handoffHeading: string;
2900
+ /**
2901
+ * Where the session roster lives. One line, because the adjudication that
2902
+ * chose this destination capped session-start injection: the roster itself
2903
+ * must never be injected, only its address.
2904
+ */
2905
+ handoffPointer: string;
2906
+ principlesHeading: string;
2907
+ principleStateless: string;
2908
+ principleNoFiles: string;
2909
+ };
2910
+ anchorStarter: {
2911
+ identityLine: (title: string) => string;
2912
+ starterNote: string;
2913
+ basicsHeading: string;
2914
+ basicsTodo: string;
2915
+ commitHeading: string;
2916
+ commitPlanning: string;
2917
+ commitImplementation: string;
2918
+ commitView: string;
2919
+ conventionsHeading: string;
2920
+ conventionsBody: string;
2921
+ viewPointerLine: (viewName: string) => string;
2922
+ /** Same route as the view block's, for the anchor — which is where `.basou/` lives. */
2923
+ handoffHeading: string;
2924
+ handoffPointer: string;
2925
+ policyHeading: string;
2926
+ policyTodo: string[];
2927
+ };
2928
+ };
2929
+ /** Look up the instruction-file string table for a resolved content language. */
2930
+ declare function presetStrings(language: ViewLanguage): PresetStrings;
2655
2931
 
2656
2932
  type DecisionsRendererInput = {
2657
2933
  paths: BasouPaths;
@@ -3198,6 +3474,19 @@ declare function prefixedUlid<P extends IdPrefix>(prefix: P): PrefixedId<P>;
3198
3474
  */
3199
3475
  declare function isValidPrefixedId(value: string): boolean;
3200
3476
 
3477
+ /**
3478
+ * `schema_version` stamped on NEWLY WRITTEN `.basou/tasks/<id>.md` front matter.
3479
+ *
3480
+ * 0.2.0 requires seconds in every timestamp. That NARROWS the field's domain,
3481
+ * which §7.3 forbids except under the vacuous-narrowing rule the same section
3482
+ * states: no value basou has ever written omits seconds, so the set of
3483
+ * documents this refuses is empty. See `docs/spec/schemas.md` for the read
3484
+ * rule and the measurement. The narrowing is shared with the event format, so
3485
+ * every durable document bumps together.
3486
+ *
3487
+ * Note: basou writes this document.
3488
+ */
3489
+ declare const TASK_SCHEMA_VERSION: "0.2.0";
3201
3490
  /**
3202
3491
  * Task lifecycle states.
3203
3492
  *
@@ -3214,8 +3503,8 @@ declare function isValidPrefixedId(value: string): boolean;
3214
3503
  * Self-edges are rejected so the audit trail stays monotonic.
3215
3504
  */
3216
3505
  declare const TaskStatusSchema: z.ZodEnum<{
3217
- in_progress: "in_progress";
3218
3506
  planned: "planned";
3507
+ in_progress: "in_progress";
3219
3508
  done: "done";
3220
3509
  cancelled: "cancelled";
3221
3510
  }>;
@@ -3235,8 +3524,8 @@ declare const TaskSchema: z.ZodObject<{
3235
3524
  title: z.ZodString;
3236
3525
  label: z.ZodOptional<z.ZodString>;
3237
3526
  status: z.ZodEnum<{
3238
- in_progress: "in_progress";
3239
3527
  planned: "planned";
3528
+ in_progress: "in_progress";
3240
3529
  done: "done";
3241
3530
  cancelled: "cancelled";
3242
3531
  }>;
@@ -3425,9 +3714,15 @@ declare function writeTaskFile(paths: BasouPaths, taskId: string, doc: TaskDocum
3425
3714
  * the caller's `options.onSkip` hook in {@link loadTaskEntries} so list
3426
3715
  * commands can show a warning row.
3427
3716
  *
3428
- * Returns ids in ULID-ascending order (filename sort matches ULID order).
3717
+ * Ids come back in ULID-ascending order: the disk scan sorts by filename,
3718
+ * which matches ULID order, and {@link rebuildTaskIndex} sorts what it writes,
3719
+ * so an index basou wrote is already in that order. An index edited by hand
3720
+ * into some other order is returned in it — no caller depends on the order,
3721
+ * and re-sorting a trusted cache would hide that it was used.
3722
+ *
3429
3723
  * Empty directory or ENOENT → `[]`. Other I/O failures throw
3430
- * `"Failed to enumerate tasks"`.
3724
+ * `"Failed to enumerate tasks"`, unless a valid index is available to answer
3725
+ * from instead.
3431
3726
  */
3432
3727
  declare function enumerateTaskIds(paths: BasouPaths): Promise<string[]>;
3433
3728
  /**
@@ -3638,7 +3933,8 @@ type ReconcileFailure = {
3638
3933
  phase: TaskWriteAfterEventPhase | null;
3639
3934
  };
3640
3935
  /**
3641
- * Batch audit result. Order follows `enumerateTaskIds(paths)` (ULID-ascending).
3936
+ * Batch audit result. Order follows `enumerateTaskIds(paths)`, which is
3937
+ * ULID-ascending for any index basou wrote and for every disk scan.
3642
3938
  * `scanned` is the number of readable task.md files processed (= excludes
3643
3939
  * malformed task.md from the count so an integrity-broken file does not
3644
3940
  * pad the total).
@@ -3973,6 +4269,35 @@ type HandoffRendererResult = {
3973
4269
  */
3974
4270
  declare function renderHandoff(input: HandoffRendererInput): Promise<HandoffRendererResult>;
3975
4271
 
4272
+ /** What a build knows about itself. `commit` is `"unknown"` outside a checkout. */
4273
+ type BuildStamp = {
4274
+ readonly version: string;
4275
+ readonly commit: string;
4276
+ readonly committedAt: string;
4277
+ };
4278
+ /**
4279
+ * Parse an injected stamp. Separate from the constant below so it is reachable
4280
+ * from a test: under vitest the module loads from SOURCE, where the injected
4281
+ * identifier does not exist, so every line of the parse would otherwise be
4282
+ * unreachable -- a guarantee with no test behind it, which is the shape of
4283
+ * omission this whole feature exists to correct.
4284
+ *
4285
+ * Anything unparseable yields `undefined` rather than throwing: a malformed
4286
+ * stamp must not stop the CLI from starting.
4287
+ */
4288
+ declare function parseBuildStamp(raw: string | undefined): BuildStamp | undefined;
4289
+ /**
4290
+ * Core's own build identity.
4291
+ *
4292
+ * Core is stamped separately from the CLI because the CLI does not bundle it:
4293
+ * `cli/dist` and `core/dist` are distinct artifacts that a partial build can
4294
+ * leave at different commits. A fresh CLI in front of a stale core is the
4295
+ * dangerous half of that pair -- core is where the renderers and importers
4296
+ * live, so the behaviour would be the old one while the CLI reported the new
4297
+ * version.
4298
+ */
4299
+ declare const BASOU_CORE_BUILD: BuildStamp | undefined;
4300
+
3976
4301
  /**
3977
4302
  * Parse a unit-suffixed duration string (e.g. `30s`, `5m`, `1h`, `100ms`)
3978
4303
  * into milliseconds.
@@ -4355,6 +4680,13 @@ type OrientationSummary = {
4355
4680
  * does not sink into the flat decision list. Empty when none are open.
4356
4681
  */
4357
4682
  openTracks: TrackRecord[];
4683
+ /**
4684
+ * Decisions recorded by `basou decision capture` that are still open and that
4685
+ * no task carries — the count `basou decision gaps` lists. Surfaced here as a
4686
+ * single number because a ratified plan that became no task is invisible
4687
+ * otherwise, and a command nobody is told to run does not fix that.
4688
+ */
4689
+ openDecisionGaps: number;
4358
4690
  /**
4359
4691
  * Most recent `note_added` over non-archived sessions — the recorded next
4360
4692
  * step / handoff ("next step") surfaced in the forward section; null when none.
@@ -4391,6 +4723,14 @@ type OrientationSummary = {
4391
4723
  * zero in-flight count distinguish "all closed" from "never used here".
4392
4724
  * See {@link anyTaskEverRecorded}: a live count is NOT this. */
4393
4725
  anyTaskEverRecorded: boolean;
4726
+ /** How many task files this render could not read.
4727
+ *
4728
+ * A standing count: the task index is reconciled against the tasks directory
4729
+ * on every enumeration, so a file that cannot be parsed keeps being attempted
4730
+ * — and keeps being counted — until it is repaired or removed. It used to
4731
+ * read zero from the second render on, because rebuilding the index dropped
4732
+ * the file and nothing enumerated it again. */
4733
+ unreadableTaskCount: number;
4394
4734
  /** Tasks whose status is `planned` ("where am I heading"). */
4395
4735
  plannedTasks: PlannedTask[];
4396
4736
  pendingApprovals: PendingApproval[];
@@ -4736,8 +5076,9 @@ type ViewPresetInput = {
4736
5076
  * operator-specific string is embedded), so re-running on an unchanged manifest
4737
5077
  * produces byte-identical output. The view is a workspace-level artifact, so
4738
5078
  * its content language follows the ANCHOR entry's declared language (mirroring
4739
- * the generated views' rule); a `ja` anchor renders byte-identical to the
4740
- * pre-i18n output. The repos are listed in the order supplied. An empty roster
5079
+ * the generated views' rule). It no longer reproduces the pre-i18n output: the
5080
+ * block gained a line naming where the session roster lives, which that output
5081
+ * predates. The repos are listed in the order supplied. An empty roster
4741
5082
  * still renders cleanly (a header-only table, empty lists). Returns the block
4742
5083
  * WITHOUT a trailing newline; the marker writer adds the surrounding structure.
4743
5084
  */
@@ -5645,6 +5986,120 @@ type WorkspaceViewPlan = {
5645
5986
  */
5646
5987
  declare function planWorkspaceView(facts: ViewRepoFact[], existing?: ExistingViewLink[], rosterNames?: string[]): WorkspaceViewPlan;
5647
5988
 
5989
+ /** Prefix of the token embedded in a delivery; the content digest follows it. */
5990
+ declare const PROTOCOL_UPDATE_TOKEN_PREFIX = "basou:protocol-updated";
5991
+ /** What the block records about itself. */
5992
+ type ProtocolStamp = {
5993
+ /** When the block's rendered protocol text last CHANGED (ISO 8601, UTC). */
5994
+ changedAt: string;
5995
+ /** Truncated digest of that rendered text. */
5996
+ contentHash: string;
5997
+ };
5998
+ /** Truncated digest of the block's rendered protocol text. */
5999
+ declare function protocolBlockHash(sections: string): string;
6000
+ /**
6001
+ * The delivery's dedupe token for a given block state.
6002
+ *
6003
+ * It carries the content digest rather than being a bare literal, so "already
6004
+ * delivered" means "already delivered THIS text". A second update inside one
6005
+ * session — usually the correction of the first, after watching an agent
6006
+ * misapply it — is a different digest and still lands. A bare literal would
6007
+ * make the one edit most worth delivering the one guaranteed not to arrive.
6008
+ */
6009
+ declare function protocolUpdateToken(contentHash: string): string;
6010
+ /**
6011
+ * Render the stamp line.
6012
+ *
6013
+ * Nothing operator-authored goes on it — only a timestamp and a digest — so a
6014
+ * source path or a protocol body containing a space, a quote, or a `-->` cannot
6015
+ * break the line it is rendered into.
6016
+ */
6017
+ declare function renderProtocolStamp(stamp: ProtocolStamp): string;
6018
+ /**
6019
+ * Find and parse the stamp line in a rendered block body.
6020
+ *
6021
+ * Returns `null` when the block carries no readable stamp — a block rendered by
6022
+ * a basou older than this feature, a hand-deleted stamp, a mangled one. A null
6023
+ * means "cannot tell", and every caller treats it as "say nothing": the point
6024
+ * of the feature is to speak only when basou actually knows something changed.
6025
+ */
6026
+ declare function parseProtocolStamp(blockBody: string): ProtocolStamp | null;
6027
+ /**
6028
+ * Build the stamp for the block about to be written: keep the previous
6029
+ * `changedAt` when the rendered text is unchanged, and set it to `now`
6030
+ * otherwise.
6031
+ *
6032
+ * `sections` is the rendered protocol text exactly as it will appear under the
6033
+ * stamp — the same bytes a session reads and the same bytes a delivery carries.
6034
+ * Hashing anything else (the raw source files, say) would let bytes no session
6035
+ * ever sees move the stamp, and basou would then tell a session that text it
6036
+ * already holds supersedes what it already holds.
6037
+ *
6038
+ * `changedAt` is forced strictly forward when the text did change. Wall-clock
6039
+ * time can move backwards — an NTP correction, a laptop waking in another
6040
+ * timezone — and a stamp dated before the session start would drop the delivery
6041
+ * for the rest of that session, silently and permanently.
6042
+ */
6043
+ declare function carryForwardProtocolStamp(input: {
6044
+ sections: string;
6045
+ previous: ProtocolStamp | null;
6046
+ now: string;
6047
+ }): ProtocolStamp;
6048
+ /**
6049
+ * The rendered protocol text inside a block: everything below the stamp line.
6050
+ *
6051
+ * Anchored on the stamp rather than on "skip the leading comments" so a
6052
+ * protocol whose own body opens with an HTML comment keeps it. Returns `null`
6053
+ * when there is no stamp line to anchor on, and an empty string when the block
6054
+ * holds a stamp and nothing else.
6055
+ *
6056
+ * Takes a block BODY, but stops at a closing marker line if one is present, so
6057
+ * handing it a whole file yields the same answer instead of a digest that
6058
+ * silently includes the marker and matches nothing.
6059
+ */
6060
+ declare function protocolSectionsFrom(blockBody: string): string | null;
6061
+ /**
6062
+ * The rendered protocol text inside a block written before stamps existed:
6063
+ * everything below the managed note.
6064
+ *
6065
+ * Only for reading the block an upgrade is about to replace. Knowing what that
6066
+ * block held is what lets an upgrade stay silent — if the text is the same, no
6067
+ * running session is owed anything, and dating the new stamp at `now` would
6068
+ * announce a change to every session on the machine the first time the new
6069
+ * basou syncs. Less precise than the stamped path (a protocol body opening
6070
+ * with its own HTML comment loses that line here), which is why it is confined
6071
+ * to blocks that carry no stamp: a wrong answer costs one redundant delivery,
6072
+ * never a wrong one.
6073
+ */
6074
+ declare function unstampedProtocolSectionsFrom(blockBody: string): string;
6075
+ /**
6076
+ * Whether a session that started at `sessionStartedAt` is holding an older copy
6077
+ * of the block than the one the stamp describes.
6078
+ *
6079
+ * A change dated at or before the start is one the session already read. An
6080
+ * unparseable start yields `false` rather than "everything": without a session
6081
+ * start there is no "after".
6082
+ */
6083
+ declare function isProtocolUpdateDue(input: {
6084
+ stamp: ProtocolStamp;
6085
+ sessionStartedAt: string;
6086
+ }): boolean;
6087
+ /**
6088
+ * The text handed to the running session: the protocols themselves, not a
6089
+ * pointer to them.
6090
+ *
6091
+ * basou cannot observe whether an agent re-read a file, so a notice saying "go
6092
+ * and re-read" would put the one step that decides the outcome outside what
6093
+ * basou can see. Carrying the text makes the delivery and the reading the same
6094
+ * act. It does not make the ADOPTION observable — nothing basou can do would —
6095
+ * but it removes the step that was avoidably invisible.
6096
+ *
6097
+ * The complete current set is sent, not a diff, so the lead line's claim is
6098
+ * exactly true: after this, the set below is the whole of the standing
6099
+ * protocols, and anything read at session start that is absent here is gone.
6100
+ */
6101
+ declare function renderProtocolUpdate(sections: string, stamp: ProtocolStamp): string;
6102
+
5648
6103
  /**
5649
6104
  * `schema_version` of each on-disk format, keyed by artifact basename.
5650
6105
  *
@@ -5662,14 +6117,14 @@ declare function planWorkspaceView(facts: ViewRepoFact[], existing?: ExistingVie
5662
6117
  * changed without the envelope's own format changing.
5663
6118
  */
5664
6119
  declare const JSON_SCHEMA_VERSIONS: {
5665
- readonly manifest: "0.1.0";
5666
- readonly session: "0.1.0";
5667
- readonly event: "0.2.0";
5668
- readonly task: "0.1.0";
5669
- readonly approval: "0.1.0";
6120
+ readonly manifest: "0.2.0";
6121
+ readonly session: "0.2.0";
6122
+ readonly event: "0.3.0";
6123
+ readonly task: "0.2.0";
6124
+ readonly approval: "0.2.0";
5670
6125
  readonly status: "0.1.0";
5671
6126
  readonly "task-index": "0.1.0";
5672
- readonly "session-import": "0.1.0";
6127
+ readonly "session-import": "0.2.0";
5673
6128
  };
5674
6129
  /** One emitted JSON Schema artifact. */
5675
6130
  type JsonSchemaArtifact = {
@@ -5809,14 +6264,21 @@ declare const SchemaVersionSchema: z.ZodString;
5809
6264
  * and would need the pattern written a second time, which is a second place to
5810
6265
  * forget. {@link SchemaVersionSchema} takes the same form for the same reason.
5811
6266
  *
5812
- * It deliberately does NOT declare `format: "date-time"`. That format names
5813
- * RFC 3339, and the two sets cross rather than nest: basou takes a timestamp
5814
- * without seconds, which RFC 3339 does not, and refuses the lowercase
5815
- * designators and the leap second that RFC 3339 allows. Declaring the format
5816
- * made one artifact answer two ways from the same bytes — a validator
5817
- * asserting it rejected `2026-09-16T01:23Z`, one treating it as an annotation
5818
- * (the JSON Schema 2020-12 default) accepted it. The `pattern` is the
5819
- * contract, and `description` says so, because no format name states this set.
6267
+ * It deliberately does NOT declare `format: "date-time"`. The sets used to
6268
+ * CROSS basou took a timestamp without seconds, which RFC 3339 does not
6269
+ * and declaring the format made one artifact answer two ways from the same
6270
+ * bytes: a validator asserting it rejected `2026-09-16T01:23Z`, one treating
6271
+ * it as an annotation (the JSON Schema 2020-12 default) accepted it.
6272
+ *
6273
+ * Requiring seconds ended the crossing: this set is now a strict SUBSET of
6274
+ * RFC 3339 `date-time`, since uppercase-only and no-leap-second are
6275
+ * restrictions on it. So the format could be declared truthfully again. It is
6276
+ * still not declared, for a different reason than before — the `pattern` is
6277
+ * the whole contract and the format names a strictly larger set, so declaring
6278
+ * it would say less than the artifact already says while reintroducing a
6279
+ * keyword whose enforcement varies by validator. Reinstating it is a decision
6280
+ * about what the artifact should assert, not a correction; `description` is
6281
+ * what states this set, because no format name does.
5820
6282
  */
5821
6283
  declare const IsoTimestampSchema: z.ZodString;
5822
6284
  /** Workspace ID schema: validates `ws_<26-char ULID>`. */
@@ -5849,6 +6311,18 @@ type RiskLevel = z.infer<typeof RiskLevelSchema>;
5849
6311
  * non-empty string in v0.1; a stricter enum may be introduced post-v0.1.
5850
6312
  */
5851
6313
  declare const EventSourceSchema: z.ZodString;
6314
+ /**
6315
+ * The `source` basou stamps on an event it wrote itself, in response to a
6316
+ * `basou` command the operator (or an agent acting for them) ran — as opposed
6317
+ * to an event an importer derived from a vendor transcript.
6318
+ *
6319
+ * It is a named constant because a reader distinguishes the two: `plan-gaps`
6320
+ * admits a decision to its population only when this source recorded it, so a
6321
+ * writer drifting from this literal would silently shrink that population
6322
+ * rather than fail. Every local writer imports this rather than repeating the
6323
+ * string, so the reader's predicate and the writers have one origin.
6324
+ */
6325
+ declare const LOCAL_CLI_EVENT_SOURCE = "local-cli";
5852
6326
 
5853
6327
  /**
5854
6328
  * Schema for `.basou/status.json` — a forward-incompat cache of the current
@@ -6993,7 +7467,7 @@ type ImportSessionResult = {
6993
7467
  /**
6994
7468
  * Import a round-trip JSON payload into `.basou/sessions/<new>/`. The caller
6995
7469
  * MUST validate the payload against {@link SessionImportPayloadSchema} first
6996
- * and gate the `schema_version === "0.1.0"` literal check externally; this
7470
+ * and gate the `schema_version === SESSION_IMPORT_SCHEMA_VERSION` check externally; this
6997
7471
  * function trusts both invariants.
6998
7472
  *
6999
7473
  * On success a fresh session ID is minted and a complete
@@ -7181,4 +7655,4 @@ declare function overwriteYamlFile(filePath: string, value: unknown): Promise<vo
7181
7655
  */
7182
7656
  declare const BASOU_CORE_VERSION = "0.1.0";
7183
7657
 
7184
- export { ACTIVE_GAP_CAP_MS, AGENT_INFRA_DIRS, type ActiveTimeBasis, type AdapterOutputEvent, type AdoptCandidate, type AdoptCandidateKind, type AnchorStarterInput, type AnchorStarterRepo, type AppendBasouGitignoreOptions, type AppendBasouGitignoreResult, type AppendEventToExistingInput, type AppendEventToExistingResult, type Approval, type ApprovalApprovedEvent, type ApprovalExpiredEvent, ApprovalIdSchema, type ApprovalLocation, type ApprovalRejectedEvent, type ApprovalRequestedEvent, ApprovalSchema, type ApprovalStatus, ApprovalStatusSchema, type ArchivePlan, type ArchiveTaskInput, type ArchiveTaskResult, type AttachTaskInput, type AttachUpdateTaskStatusInput, type AttachableStatus, BASOU_CORE_VERSION, type BasouPaths, type BuildStopHookCommandOptions, type BulkChainResult, CLAUDE_IMPORT_SOURCE, CODEX_IMPORT_SOURCE, type CaptureMode, type ChainBreakReason, type ChainTailState, type ChainVerdict, type ChainVerdictStatus, type ChainedEvents, ChildProcessRunner, type CitedReview, type ClaudeSettings, type ClaudeTranscriptRecord, type ClaudeTranscriptToPayloadOptions, type CodexCommandLookup, type CodexHooksFile, type CodexRolloutRecord, type CodexRolloutToPayloadOptions, type CommandExecutedEvent, type CommandLookup, type CreateAdHocSessionInput, type CreateAdHocSessionResult, type CreateAdHocTaskInput, type CreateManifestInput, type CreateTaskInput, type CreateTaskResult, DEFAULT_STOP_HOOK_MIN_EDITS, type DayWorkStats, DecisionIdSchema, type DecisionRecordedEvent, type DecisionsRendererInput, type DecisionsRendererResult, type DeleteTaskInput, type DeleteTaskResult, type DiffResult, EVENT_SCHEMA_VERSION, type EditTaskInput, type EditTaskResult, type Event, EventIdSchema, EventSchema, EventSourceSchema, type ExistingViewLink, FailedToFinalizeError, type FederatedRoot, type FileChange, type FileChangeStatus, type FileChangedEvent, GENERATED_END, GENERATED_START, type GitSnapshot, type GitSnapshotEvent, type GitignorePlanSummary, type HandoffRendererInput, type HandoffRendererResult, ID_PREFIXES, type IdPrefix, type ImportSessionOptions, type ImportSessionResult, type IncompleteWiring, type InstructionFileFact, type InstructionSymlinkFact, type InstructionSymlinkState, IsoTimestampSchema, JSON_SCHEMA_VERSIONS, type JsonSchemaArtifact, type LoadFederatedOptions, type LoadSessionEntriesOptions, type LoadTaskEntriesOptions, type LoadedApproval, type LockHandle, type LockScope, type Manifest, ManifestSchema, type MarkerSection, type Markers, type MeasureAvailability, type MissingCanonical, type NoteAddedEvent, ORIENTATION_END, ORIENTATION_START, type OrientationRendererInput, type OrientationRendererResult, type OrientationSummary, PROTOCOL_END, PROTOCOL_START, type PrefixedId, type PresetAction, type PresetCollision, type PresetMarkerConflict, type PresetMarkerKind, type PresetPlanSummary, type PresetRepo, type PresetStrings, type ProcessRunner, type PublishKind, type PublishTarget, REVIEW_RECORD_NO_INPUT_HINT, type RechainOptions, type RechainResult, type ReconcileAllResult, type ReconcileAllTasksInput, type ReconcileAllTasksOptions, type ReconcileFailure, type ReconcileResult, type ReconcileTaskInput, type RefreshLinkageInput, type RefreshLinkageResult, type ReimportOptions, type ReimportResult, type RenamePlan, type ReplayOptions, type ReplayWarning, type RepoEntry, type RepoGitignoreFacts, type RepoGitignorePlan, type RepoInstructions, type RepoLanguage, type RepoPathProblem, type RepoPresetFacts, type RepoPresetPlan, type RepoSymlinkFacts, type RepoSymlinkPlan, type RepoVisibility, type RepoWiringFacts, type ReportApprovalItem, type ReportData, type ReportDecisionItem, type ReportRendererInput, type ReportRendererResult, type ReportSessionItem, type ReportTaskItem, type RetrofitAction, type RetrofitAgentsState, type RetrofitFacts, type RetrofitPlan, type RetrofitReason, type ReviewBlocked, type ReviewFinding, type ReviewGapRepoSummary, type ReviewGapUnit, type ReviewGapVerdict, type ReviewGapsInput, type ReviewGapsSummary, type ReviewGateResult, type ReviewGateSilentReason, type ReviewRecordBlockedInput, type ReviewRecordFindingInput, type ReviewRecordInput, type ReviewRecordedEvent, type RiskLevel, RiskLevelSchema, type RosterAdoptionPlan, type RosterDriftSummary, type RunOptions, type RunResult, SESSION_IMPORT_SCHEMA_VERSION, SESSION_START_HOOK_CONTEXT_LIMIT, SESSION_START_HOOK_MATCHER, SESSION_START_HOOK_STATUS_MESSAGE, SESSION_START_HOOK_TIMEOUT_SECONDS, STOP_HOOK_TIMEOUT_SECONDS, STUCK_THRESHOLD_MS, type SanitizePathOptions, type SanitizeRelatedFilesResult, SchemaVersionSchema, type SelfReportedReview, type Session, type SessionEndedEvent, type SessionEntry, SessionIdSchema, type SessionImportPayload, SessionImportPayloadSchema, type SessionInnerImportInput, SessionInnerImportSchema, type SessionIntegrity, SessionIntegritySchema, type SessionMetrics, SessionMetricsSchema, SessionSchema, type SessionSkipReason, type SessionSourceKind, SessionSourceKindSchema, type SessionStartHookLocation, type SessionStartHookRemoval, type SessionStartHookUpsert, type SessionStartedEvent, type SessionStatus, type SessionStatusChangedEvent, SessionStatusSchema, type SessionWorkStats, type SourceRootScope, type SourceRootsReconcile, type SourceWorkStats, type StatusCount, StatusSchema, type StatusSnapshot, type StopHookEvaluation, type StopHookEvaluationInput, type StopHookRemoval, type StopHookSilentReason, type StopHookUpsert, type SuspectReason, type SymlinkCollision, type SymlinkConflict, type SymlinkPlanSummary, type Task, type TaskArchivedEvent, type TaskCreatedEvent, type TaskDeletedEvent, type TaskDocument, TaskIdSchema, type TaskLinkageRefreshedEvent, type TaskReconciledEvent, TaskSchema, type TaskSkipReason, type TaskStatus, type TaskStatusChangedEvent, type TaskStatusCount, TaskStatusSchema, TaskWriteAfterEventError, type TaskWriteAfterEventPhase, type TokenTotals, type UnattachedSelfReports, type UnbindableRepo, type UpdateAdHocTaskStatusInput, type UpdateTaskStatusInput, type UpdateTaskStatusResult, type ViewCollision, type ViewConflict, type ViewLanguage, type ViewLinkState, type ViewPresetInput, type ViewPresetRepo, type ViewRepoFact, type ViewStrayUnknown, type ViewStrings, type ViewWiringFacts, type WiringCollision, type WiringConflict, type WiringDriftSummary, type WiringRisk, type WiringSummary, type WorkStatsInput, type WorkStatsResult, type WorkStatsTotals, WorkspaceIdSchema, type WorkspaceViewPlan, type WriteEventsBulkOptions, type WriteTaskFileMode, ZERO_DURATION_RETIRED_SINCE, acquireLock, appendBasouGitignore, appendChainedEvent, appendChainedEventLocked, appendEvent, appendEventToExistingSession, archiveTask, assertBasouRootSafe, basouPaths, buildJsonSchemas, buildReviewRecordLabel, buildReviewRecordedEvent, buildSessionStartHookCommand, buildStatusSnapshot, buildStopHookCommand, chainEvents, chainRawJsonLines, classifyFilesBySourceRoot, classifyRetrofit, classifySuspect, claudeCodeAdapterMetadata, claudeTranscriptToImportPayload, codexAdapterMetadata, codexRolloutToImportPayload, computeWorkStats, createAdHocSessionWithEvent, createManifest, createTaskWithEvent, deleteTask, editTask, ensureBasouDirectory, enumerateApprovals, enumerateArchivedTaskIds, enumerateSessionDirs, enumerateTaskIds, evaluateStopHook, finalizeSessionYaml, findBasouSessionStartHook, findBasouStopHookCommand, findErrorCode, findReviewGaps, findUnbindableRepos, formatDurationMs, genesisHash, getDiff, getSnapshot, hasRetiredZeroDuration, importSessionFromJson, inspectChainTail, instructionMode, isBasouSessionStartHookCommand, isBasouStopHookCommand, isGitNotFound, isImportDerivedSource, isLazyExpired, isRenderable, isValidPrefixedId, lineHash, linkYamlFile, loadApproval, loadFederatedSessionEntries, loadSessionEntries, loadTaskEntries, normalizeRepoKey, normalizeRepoPath, overwriteYamlFile, parseDuration, parseMarkers, parseReviewRecordInput, pathBasename, planArchive, planGitignore, planRename, planRosterAdoption, planWorkspaceView, prefixedUlid, presetStrings, readAllEvents, readManifest, readMarkdownFile, readObservedDuration, readSessionYaml, readStatus, readTaskFile, readTaskFileWithArchiveFallback, readYamlFile, rechainSessionInPlace, reconcileAllTasks, reconcileSourceRoots, reconcileTask, refreshTaskLinkedSessions, reimportPreservingId, removeMarkerSection, removeSessionStartHook, removeStopHook, renderAnchorStarter, renderDecisions, renderHandoff, renderOrientation, renderPresetBlock, renderReport, renderViewPresetBlock, renderWithMarkers, replayEvents, resolveAnchorContentLanguage, resolveBasouRepositoryRoot, resolveClaudeCodeCommand, resolveCodexCommand, resolveRepoContentLanguage, resolveRepoRoot, resolveRepositoryRoot, resolveSessionId, resolveTaskId, resolveViewLanguage, resolveViewLanguageFromPaths, safeSimpleGit, sanitizePath, sanitizeRelatedFiles, sanitizeWorkingDirectory, seedMarkers, serializeEventLine, serializeJsonSchema, sessionWorkStatsFromEvents, summarizeAdapterOutput, summarizeOrientation, summarizePresetPlan, summarizeRosterDrift, summarizeSymlinkPlan, summarizeWiring, summarizeWiringDrift, tryRemoteUrl, ulid, unknownManifestKeys, updateTaskStatusWithEvent, upsertSessionStartHook, upsertStopHook, verifyEventsChain, viewStrings, writeEventsBulk, writeManifest, writeMarkdownFile, writeObservedDuration, writeStatus, writeTaskFile, writeYamlFile };
7658
+ export { ACTIVE_GAP_CAP_MS, AGENT_INFRA_DIRS, APPROVAL_SCHEMA_VERSION, type ActiveTimeBasis, type AdapterOutputEvent, type AdoptCandidate, type AdoptCandidateKind, type AnchorStarterInput, type AnchorStarterRepo, type AppendBasouGitignoreOptions, type AppendBasouGitignoreResult, type AppendEventToExistingInput, type AppendEventToExistingResult, type Approval, type ApprovalApprovedEvent, type ApprovalExpiredEvent, ApprovalIdSchema, type ApprovalLocation, type ApprovalRejectedEvent, type ApprovalRequestedEvent, ApprovalSchema, type ApprovalStatus, ApprovalStatusSchema, type ArchivePlan, type ArchiveTaskInput, type ArchiveTaskResult, type AttachTaskInput, type AttachUpdateTaskStatusInput, type AttachableStatus, BASOU_CORE_BUILD, BASOU_CORE_VERSION, type BasouPaths, type BuildStamp, type BuildStopHookCommandOptions, type BulkChainResult, CLAUDE_IMPORT_SOURCE, CODEX_IMPORT_SOURCE, type CaptureMode, type ChainBreakReason, type ChainTailState, type ChainVerdict, type ChainVerdictStatus, type ChainedEvents, ChildProcessRunner, type CitedReview, type ClaudeSettings, type ClaudeTranscriptRecord, type ClaudeTranscriptToPayloadOptions, type CodexCommandLookup, type CodexHooksFile, type CodexRolloutRecord, type CodexRolloutToPayloadOptions, type CommandExecutedEvent, type CommandLookup, type CreateAdHocSessionInput, type CreateAdHocSessionResult, type CreateAdHocTaskInput, type CreateManifestInput, type CreateTaskInput, type CreateTaskResult, DECISION_GAPS_EPOCH, DEFAULT_STOP_HOOK_MIN_EDITS, type DayWorkStats, type DecisionGap, type DecisionGapsExcluded, type DecisionGapsIncomplete, type DecisionGapsInput, type DecisionGapsScope, type DecisionGapsSummary, DecisionIdSchema, type DecisionRecordedEvent, type DecisionsRendererInput, type DecisionsRendererResult, type DeleteTaskInput, type DeleteTaskResult, type DiffResult, EVENT_SCHEMA_VERSION, type EditTaskInput, type EditTaskResult, type Event, EventIdSchema, EventSchema, EventSourceSchema, type ExistingViewLink, FailedToFinalizeError, type FederatedRoot, type FileChange, type FileChangeStatus, type FileChangedEvent, GENERATED_END, GENERATED_START, type GitSnapshot, type GitSnapshotEvent, type GitignorePlanSummary, type HandoffRendererInput, type HandoffRendererResult, ID_PREFIXES, type IdPrefix, type ImportSessionOptions, type ImportSessionResult, type IncompleteWiring, type InstructionFileFact, type InstructionSymlinkFact, type InstructionSymlinkState, IsoTimestampSchema, JSON_SCHEMA_VERSIONS, type JsonSchemaArtifact, LOCAL_CLI_EVENT_SOURCE, type LoadFederatedOptions, type LoadSessionEntriesOptions, type LoadTaskEntriesOptions, type LoadedApproval, type LockHandle, type LockScope, MANIFEST_SCHEMA_VERSION, type Manifest, ManifestSchema, type MarkerSection, type Markers, type MeasureAvailability, type MissingCanonical, type NoteAddedEvent, ORIENTATION_END, ORIENTATION_START, type OrientationRendererInput, type OrientationRendererResult, type OrientationSummary, PROTOCOL_END, PROTOCOL_START, PROTOCOL_UPDATE_TOKEN_PREFIX, type PrefixedId, type PresetAction, type PresetCollision, type PresetMarkerConflict, type PresetMarkerKind, type PresetPlanSummary, type PresetRepo, type PresetStrings, type ProcessRunner, type ProtocolStamp, type PublishKind, type PublishTarget, REVIEW_RECORD_NO_INPUT_HINT, type RechainOptions, type RechainResult, type ReconcileAllResult, type ReconcileAllTasksInput, type ReconcileAllTasksOptions, type ReconcileFailure, type ReconcileResult, type ReconcileTaskInput, type RefreshLinkageInput, type RefreshLinkageResult, type ReimportOptions, type ReimportResult, type RenamePlan, type ReplayOptions, type ReplayWarning, type RepoEntry, type RepoGitignoreFacts, type RepoGitignorePlan, type RepoInstructions, type RepoLanguage, type RepoPathProblem, type RepoPresetFacts, type RepoPresetPlan, type RepoSymlinkFacts, type RepoSymlinkPlan, type RepoVisibility, type RepoWiringFacts, type ReportApprovalItem, type ReportData, type ReportDecisionItem, type ReportRendererInput, type ReportRendererResult, type ReportSessionItem, type ReportTaskItem, type RetrofitAction, type RetrofitAgentsState, type RetrofitFacts, type RetrofitPlan, type RetrofitReason, type ReviewBlocked, type ReviewFinding, type ReviewGapRepoSummary, type ReviewGapUnit, type ReviewGapVerdict, type ReviewGapsInput, type ReviewGapsSummary, type ReviewGateResult, type ReviewGateSilentReason, type ReviewRecordBlockedInput, type ReviewRecordFindingInput, type ReviewRecordInput, type ReviewRecordedEvent, type RiskLevel, RiskLevelSchema, type RosterAdoptionPlan, type RosterDriftSummary, type RunOptions, type RunResult, SESSION_IMPORT_SCHEMA_VERSION, SESSION_SCHEMA_VERSION, SESSION_START_HOOK_CONTEXT_LIMIT, SESSION_START_HOOK_MATCHER, SESSION_START_HOOK_STATUS_MESSAGE, SESSION_START_HOOK_TIMEOUT_SECONDS, STOP_HOOK_TIMEOUT_SECONDS, STUCK_THRESHOLD_MS, type SanitizePathOptions, type SanitizeRelatedFilesResult, SchemaVersionSchema, type SelfReportedReview, type Session, type SessionEndedEvent, type SessionEntry, SessionIdSchema, type SessionImportPayload, SessionImportPayloadSchema, type SessionInnerImportInput, SessionInnerImportSchema, type SessionIntegrity, SessionIntegritySchema, type SessionMetrics, SessionMetricsSchema, SessionSchema, type SessionSkipReason, type SessionSourceKind, SessionSourceKindSchema, type SessionStartHookLocation, type SessionStartHookRemoval, type SessionStartHookUpsert, type SessionStartedEvent, type SessionStatus, type SessionStatusChangedEvent, SessionStatusSchema, type SessionWorkStats, type SourceRootScope, type SourceRootsReconcile, type SourceWorkStats, type StatusCount, StatusSchema, type StatusSnapshot, type StopHookEvaluation, type StopHookEvaluationInput, type StopHookRemoval, type StopHookSilentReason, type StopHookUpsert, type SuspectReason, type SymlinkCollision, type SymlinkConflict, type SymlinkPlanSummary, TASK_SCHEMA_VERSION, type Task, type TaskArchivedEvent, type TaskCreatedEvent, type TaskDeletedEvent, type TaskDocument, TaskIdSchema, type TaskLinkageRefreshedEvent, type TaskReconciledEvent, TaskSchema, type TaskSkipReason, type TaskStatus, type TaskStatusChangedEvent, type TaskStatusCount, TaskStatusSchema, TaskWriteAfterEventError, type TaskWriteAfterEventPhase, type TokenTotals, type UnattachedSelfReports, type UnbindableRepo, type UpdateAdHocTaskStatusInput, type UpdateTaskStatusInput, type UpdateTaskStatusResult, type ViewCollision, type ViewConflict, type ViewLanguage, type ViewLinkState, type ViewPresetInput, type ViewPresetRepo, type ViewRepoFact, type ViewStrayUnknown, type ViewStrings, type ViewWiringFacts, type WiringCollision, type WiringConflict, type WiringDriftSummary, type WiringRisk, type WiringSummary, type WorkStatsInput, type WorkStatsResult, type WorkStatsTotals, WorkspaceIdSchema, type WorkspaceViewPlan, type WriteEventsBulkOptions, type WriteTaskFileMode, ZERO_DURATION_RETIRED_SINCE, acquireLock, appendBasouGitignore, appendChainedEvent, appendChainedEventLocked, appendEvent, appendEventToExistingSession, archiveTask, assertBasouRootSafe, basouPaths, buildJsonSchemas, buildReviewRecordLabel, buildReviewRecordedEvent, buildSessionStartHookCommand, buildStatusSnapshot, buildStopHookCommand, carryForwardProtocolStamp, chainEvents, chainRawJsonLines, classifyFilesBySourceRoot, classifyRetrofit, classifySuspect, claudeCodeAdapterMetadata, claudeTranscriptToImportPayload, codexAdapterMetadata, codexRolloutToImportPayload, computeWorkStats, createAdHocSessionWithEvent, createManifest, createTaskWithEvent, deleteTask, editTask, ensureBasouDirectory, enumerateApprovals, enumerateArchivedTaskIds, enumerateSessionDirs, enumerateTaskIds, evaluateStopHook, finalizeSessionYaml, findBasouSessionStartHook, findBasouStopHookCommand, findDecisionGaps, findErrorCode, findReviewGaps, findUnbindableRepos, formatDurationMs, genesisHash, getDiff, getSnapshot, hasRetiredZeroDuration, importSessionFromJson, inspectChainTail, instructionMode, isBasouSessionStartHookCommand, isBasouStopHookCommand, isGitNotFound, isImportDerivedSource, isLazyExpired, isProtocolUpdateDue, isRenderable, isValidPrefixedId, lineHash, linkYamlFile, loadApproval, loadFederatedSessionEntries, loadSessionEntries, loadTaskEntries, normalizeRepoKey, normalizeRepoPath, overwriteYamlFile, parseBuildStamp, parseDuration, parseMarkers, parseProtocolStamp, parseReviewRecordInput, pathBasename, planArchive, planGitignore, planRename, planRosterAdoption, planWorkspaceView, prefixedUlid, presetStrings, protocolBlockHash, protocolSectionsFrom, protocolUpdateToken, readAllEvents, readManifest, readMarkdownFile, readObservedDuration, readSessionYaml, readStatus, readTaskFile, readTaskFileWithArchiveFallback, readYamlFile, rechainSessionInPlace, reconcileAllTasks, reconcileSourceRoots, reconcileTask, refreshTaskLinkedSessions, reimportPreservingId, removeMarkerSection, removeSessionStartHook, removeStopHook, renderAnchorStarter, renderDecisions, renderHandoff, renderOrientation, renderPresetBlock, renderProtocolStamp, renderProtocolUpdate, renderReport, renderViewPresetBlock, renderWithMarkers, replayEvents, resolveAnchorContentLanguage, resolveBasouRepositoryRoot, resolveClaudeCodeCommand, resolveCodexCommand, resolveRepoContentLanguage, resolveRepoRoot, resolveRepositoryRoot, resolveSessionId, resolveTaskId, resolveViewLanguage, resolveViewLanguageFromPaths, safeSimpleGit, sanitizePath, sanitizeRelatedFiles, sanitizeWorkingDirectory, seedMarkers, serializeEventLine, serializeJsonSchema, sessionWorkStatsFromEvents, summarizeAdapterOutput, summarizeOrientation, summarizePresetPlan, summarizeRosterDrift, summarizeSymlinkPlan, summarizeWiring, summarizeWiringDrift, transcriptStartedAt, tryRemoteUrl, ulid, unknownManifestKeys, unstampedProtocolSectionsFrom, updateTaskStatusWithEvent, upsertSessionStartHook, upsertStopHook, verifyEventsChain, viewStrings, writeEventsBulk, writeManifest, writeMarkdownFile, writeObservedDuration, writeStatus, writeTaskFile, writeYamlFile };