diff --git a/AGENTS.md b/AGENTS.md index 0a75a0800..9cbc3e971 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ - `PLAN_AUTHORING_TOOL_NAMES` in `src/constants/loop.ts` is the single list of plan-authoring tools; the `code`, `auditor`, and `feature-splitter` tool-exclude lists and both permission rulesets derive their deny entries from it. - `LoopService.resolveActiveLoopForSession` is the only correct "is this session inside a running loop" check. `resolveLoopName` matches terminated loops too, so using it as an activity guard blocks a session forever after its loop ends. - `resolveForgeDbPath` in `src/utils/opencode-paths.ts` is the only place `/forge.db` is built; every entry point must route through it so a configured `dataDir` is honoured uniformly. -- `buildAuditorModelChain`/`resolveLoopAuditorChoice` in `src/utils/loop-helpers.ts` are the only auditor model/variant resolution point (the chain index lives in `loops.auditor_fallback_index`), and `handleAuditorProviderLimit` in `src/loop/runtime.ts` is the only place that decides whether an auditor provider limit falls back or terminates. +- `buildAuditorModelChain`/`resolveLoopAuditorChoice` in `src/utils/loop-helpers.ts` are the only auditor model/variant resolution point (the chain index lives in `loops.auditor_fallback_index`), and `handleAuditorProviderLimit` in `src/loop/runtime.ts` is the only place that decides whether an auditor provider limit falls back or terminates. Every detection path routes through it, including the watchdog's `retry` status poll, which must call it via `withStateLock` because the handler assumes the caller already holds the per-loop lock. The index only moves forward through `advanceAuditorFallbackIndex`; it resets to `0` on a successful audit (both auditor phases) and on loop restart, so a transient limit never permanently drops `auditorVariant`. ## Sandbox single sources diff --git a/docs/api/_media/configuration.md b/docs/api/_media/configuration.md index 8fe184f79..cd8416900 100644 --- a/docs/api/_media/configuration.md +++ b/docs/api/_media/configuration.md @@ -14,7 +14,7 @@ See also: [Tools](tools.md), [Agents and Slash Commands](agents-and-commands.md) | `auditorModel` | `""` | Fallback model override for auditor sessions. Format: `provider/model`. | | `executionVariant` | `""` | Default reasoning/thinking variant for the execution model (e.g. `high`, `max`). | | `auditorVariant` | `""` | Default reasoning/thinking variant for the auditor model. Independent — does not inherit `executionVariant`. | -| `auditorFallbackModels` | `[]` | Ordered fallback auditor models tried when the current auditor model hits a provider usage/auth limit mid-loop. Entries use `provider/model` format and are tried in order; variants are NOT inherited. Applies only to `auditing`/`final_auditing`. The fallback index resets to `0` on loop restart. Empty/omitted means a limited auditor terminates the loop. | +| `auditorFallbackModels` | `[]` | Ordered fallback auditor models tried when the current auditor model hits a provider usage/auth limit mid-loop. Entries use `provider/model` format and are tried in order; variants are NOT inherited. Applies only to `auditing`/`final_auditing`. The fallback index resets to `0` after any successful audit (so the preferred model and its variant are retried on the next audit) and on loop restart. Empty/omitted means a limited auditor terminates the loop. | | `agents` | unset | Per-agent overrides keyed by display name, currently supporting `temperature`. | | `remotes` | unset | Remote opencode servers available as loop launch targets in the TUI execution dialog. See [Remotes](#remotes). | | `dashboard` | unset | Dashboard HTTP server bind host and port. Defaults to loopback only. See [Dashboard](#dashboard). | diff --git a/docs/api/functions/createForgePlugin.md b/docs/api/functions/createForgePlugin.md index 8535a2ffd..89e3cdd5d 100644 --- a/docs/api/functions/createForgePlugin.md +++ b/docs/api/functions/createForgePlugin.md @@ -8,7 +8,7 @@ > **createForgePlugin**(`config`): `Plugin` -Defined in: [index.ts:202](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L202) +Defined in: [index.ts:202](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L202) Creates an OpenCode plugin instance with loop management and sandboxing. diff --git a/docs/api/functions/createParentSessionLookup.md b/docs/api/functions/createParentSessionLookup.md index 3bdd7be9b..dee015d81 100644 --- a/docs/api/functions/createParentSessionLookup.md +++ b/docs/api/functions/createParentSessionLookup.md @@ -8,7 +8,7 @@ > **createParentSessionLookup**(`__namedParameters`): (`sessionId`) => `Promise`\<`string` \| `null`\> -Defined in: [index.ts:56](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L56) +Defined in: [index.ts:56](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L56) ## Parameters diff --git a/docs/api/functions/createSessionDirectoryLookup.md b/docs/api/functions/createSessionDirectoryLookup.md index e90b1ffdf..5327f2211 100644 --- a/docs/api/functions/createSessionDirectoryLookup.md +++ b/docs/api/functions/createSessionDirectoryLookup.md @@ -8,7 +8,7 @@ > **createSessionDirectoryLookup**(`__namedParameters`): (`sessionId`) => `Promise`\<`string` \| `null`\> -Defined in: [index.ts:137](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L137) +Defined in: [index.ts:137](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L137) ## Parameters diff --git a/docs/api/interfaces/CompactionConfig.md b/docs/api/interfaces/CompactionConfig.md index 222993f49..87748fbf5 100644 --- a/docs/api/interfaces/CompactionConfig.md +++ b/docs/api/interfaces/CompactionConfig.md @@ -6,7 +6,7 @@ # Interface: CompactionConfig -Defined in: [types.ts:145](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L145) +Defined in: [types.ts:145](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L145) Configuration for session compaction behavior. @@ -16,7 +16,7 @@ Configuration for session compaction behavior. > `optional` **customPrompt?**: `boolean` -Defined in: [types.ts:147](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L147) +Defined in: [types.ts:147](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L147) Use a custom compaction prompt. @@ -26,6 +26,6 @@ Use a custom compaction prompt. > `optional` **maxContextTokens?**: `number` -Defined in: [types.ts:149](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L149) +Defined in: [types.ts:149](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L149) Maximum context tokens for compaction. diff --git a/docs/api/interfaces/CreateParentSessionLookupOptions.md b/docs/api/interfaces/CreateParentSessionLookupOptions.md index 1868f4c95..b81fdc08f 100644 --- a/docs/api/interfaces/CreateParentSessionLookupOptions.md +++ b/docs/api/interfaces/CreateParentSessionLookupOptions.md @@ -6,7 +6,7 @@ # Interface: CreateParentSessionLookupOptions -Defined in: [index.ts:46](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L46) +Defined in: [index.ts:46](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L46) ## Properties @@ -14,7 +14,7 @@ Defined in: [index.ts:46](https://github.com/chriswritescode-dev/opencode-forge/ > **client**: `ForgeClient` -Defined in: [index.ts:47](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L47) +Defined in: [index.ts:47](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L47) *** @@ -22,7 +22,7 @@ Defined in: [index.ts:47](https://github.com/chriswritescode-dev/opencode-forge/ > **directory**: `string` -Defined in: [index.ts:48](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L48) +Defined in: [index.ts:48](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L48) *** @@ -30,7 +30,7 @@ Defined in: [index.ts:48](https://github.com/chriswritescode-dev/opencode-forge/ > **logger**: `object` -Defined in: [index.ts:50](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L50) +Defined in: [index.ts:50](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L50) #### debug @@ -92,7 +92,7 @@ Defined in: [index.ts:50](https://github.com/chriswritescode-dev/opencode-forge/ > **loop**: `Loop` -Defined in: [index.ts:49](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L49) +Defined in: [index.ts:49](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L49) *** @@ -100,4 +100,4 @@ Defined in: [index.ts:49](https://github.com/chriswritescode-dev/opencode-forge/ > `optional` **negativeTtlMs?**: `number` -Defined in: [index.ts:51](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L51) +Defined in: [index.ts:51](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L51) diff --git a/docs/api/interfaces/CreateSessionDirectoryLookupOptions.md b/docs/api/interfaces/CreateSessionDirectoryLookupOptions.md index b3a94cfa2..49e82320c 100644 --- a/docs/api/interfaces/CreateSessionDirectoryLookupOptions.md +++ b/docs/api/interfaces/CreateSessionDirectoryLookupOptions.md @@ -6,7 +6,7 @@ # Interface: CreateSessionDirectoryLookupOptions -Defined in: [index.ts:131](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L131) +Defined in: [index.ts:131](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L131) ## Properties @@ -14,7 +14,7 @@ Defined in: [index.ts:131](https://github.com/chriswritescode-dev/opencode-forge > **client**: `ForgeClient` -Defined in: [index.ts:132](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L132) +Defined in: [index.ts:132](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L132) *** @@ -22,7 +22,7 @@ Defined in: [index.ts:132](https://github.com/chriswritescode-dev/opencode-forge > **directory**: `string` -Defined in: [index.ts:133](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L133) +Defined in: [index.ts:133](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L133) *** @@ -30,4 +30,4 @@ Defined in: [index.ts:133](https://github.com/chriswritescode-dev/opencode-forge > **loop**: `Loop` -Defined in: [index.ts:134](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L134) +Defined in: [index.ts:134](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L134) diff --git a/docs/api/interfaces/DashboardConfig.md b/docs/api/interfaces/DashboardConfig.md index 3aa7efc4f..97cdc8888 100644 --- a/docs/api/interfaces/DashboardConfig.md +++ b/docs/api/interfaces/DashboardConfig.md @@ -6,7 +6,7 @@ # Interface: DashboardConfig -Defined in: [types.ts:181](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L181) +Defined in: [types.ts:181](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L181) Configuration for the read-only observability dashboard HTTP server. The dashboard is unauthenticated: binding to a non-loopback address exposes @@ -20,7 +20,7 @@ for the canonical warning text rendered by launch surfaces. > `optional` **host?**: `string` -Defined in: [types.ts:183](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L183) +Defined in: [types.ts:183](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L183) Bind hostname or IP. Defaults to "localhost". Use "0.0.0.0" to listen on all interfaces. @@ -30,6 +30,6 @@ Bind hostname or IP. Defaults to "localhost". Use "0.0.0.0" to listen on all int > `optional` **port?**: `number` -Defined in: [types.ts:185](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L185) +Defined in: [types.ts:185](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L185) Base bind port. Defaults to 4747. Consecutive ports are tried when busy. diff --git a/docs/api/interfaces/PluginConfig.md b/docs/api/interfaces/PluginConfig.md index 3e08a45bb..4fcd838f6 100644 --- a/docs/api/interfaces/PluginConfig.md +++ b/docs/api/interfaces/PluginConfig.md @@ -6,7 +6,7 @@ # Interface: PluginConfig -Defined in: [types.ts:223](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L223) +Defined in: [types.ts:223](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L223) Complete plugin configuration for opencode-forge. @@ -16,7 +16,7 @@ Complete plugin configuration for opencode-forge. > `optional` **agents?**: `Record`\<`string`, `AgentOverrideConfig`\> -Defined in: [types.ts:255](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L255) +Defined in: [types.ts:255](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L255) Per-agent configuration overrides. @@ -26,7 +26,7 @@ Per-agent configuration overrides. > `optional` **auditorFallbackModels?**: `string`[] -Defined in: [types.ts:241](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L241) +Defined in: [types.ts:241](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L241) Ordered "provider/model" entries tried, in order, when the current auditor model hits a provider usage/auth limit mid-loop; variants are **not** inherited by fallback entries. @@ -36,7 +36,7 @@ Ordered "provider/model" entries tried, in order, when the current auditor model > `optional` **auditorModel?**: `string` -Defined in: [types.ts:235](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L235) +Defined in: [types.ts:235](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L235) Model to use for code auditing. @@ -46,7 +46,7 @@ Model to use for code auditing. > `optional` **auditorVariant?**: `string` -Defined in: [types.ts:239](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L239) +Defined in: [types.ts:239](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L239) Default reasoning/thinking variant for the auditor model. @@ -56,7 +56,7 @@ Default reasoning/thinking variant for the auditor model. > `optional` **compaction?**: [`CompactionConfig`](CompactionConfig.md) -Defined in: [types.ts:229](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L229) +Defined in: [types.ts:229](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L229) Compaction behavior configuration. @@ -66,7 +66,7 @@ Compaction behavior configuration. > `optional` **completedLoopTtlMs?**: `number` -Defined in: [types.ts:249](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L249) +Defined in: [types.ts:249](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L249) TTL for completed/cancelled/errored/stalled loops before sweep. Default 7 days. @@ -76,7 +76,7 @@ TTL for completed/cancelled/errored/stalled loops before sweep. Default 7 days. > `optional` **dashboard?**: [`DashboardConfig`](DashboardConfig.md) -Defined in: [types.ts:253](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L253) +Defined in: [types.ts:253](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L253) Dashboard HTTP server bind configuration. @@ -86,7 +86,7 @@ Dashboard HTTP server bind configuration. > `optional` **dataDir?**: `string` -Defined in: [types.ts:225](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L225) +Defined in: [types.ts:225](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L225) Custom data directory for plugin storage. Defaults to platform data dir. @@ -96,7 +96,7 @@ Custom data directory for plugin storage. Defaults to platform data dir. > `optional` **executionModel?**: `string` -Defined in: [types.ts:233](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L233) +Defined in: [types.ts:233](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L233) Model to use for code execution. @@ -106,7 +106,7 @@ Model to use for code execution. > `optional` **executionVariant?**: `string` -Defined in: [types.ts:237](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L237) +Defined in: [types.ts:237](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L237) Default reasoning/thinking variant for the execution model. @@ -116,7 +116,7 @@ Default reasoning/thinking variant for the execution model. > `optional` **groupLaunch?**: `GroupLaunchConfig` -Defined in: [types.ts:245](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L245) +Defined in: [types.ts:245](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L245) Group launch configuration. @@ -126,7 +126,7 @@ Group launch configuration. > `optional` **logging?**: `LoggingConfig` -Defined in: [types.ts:227](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L227) +Defined in: [types.ts:227](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L227) Logging configuration. @@ -136,7 +136,7 @@ Logging configuration. > `optional` **loop?**: `LoopConfig` -Defined in: [types.ts:243](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L243) +Defined in: [types.ts:243](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L243) Loop behavior configuration. @@ -146,7 +146,7 @@ Loop behavior configuration. > `optional` **messagesTransform?**: `MessagesTransformConfig` -Defined in: [types.ts:231](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L231) +Defined in: [types.ts:231](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L231) Message transformation for architect agent. @@ -156,7 +156,7 @@ Message transformation for architect agent. > `optional` **remotes?**: `RemoteServerConfig`[] -Defined in: [types.ts:247](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L247) +Defined in: [types.ts:247](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L247) Remote opencode servers available as loop launch targets. @@ -166,7 +166,7 @@ Remote opencode servers available as loop launch targets. > `optional` **sandbox?**: `SandboxConfig` -Defined in: [types.ts:257](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L257) +Defined in: [types.ts:257](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L257) Sandbox execution configuration. @@ -176,6 +176,6 @@ Sandbox execution configuration. > `optional` **tui?**: `TuiConfig` -Defined in: [types.ts:251](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/types.ts#L251) +Defined in: [types.ts:251](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/types.ts#L251) TUI display configuration. diff --git a/docs/api/variables/VERSION.md b/docs/api/variables/VERSION.md index d016500d0..b3434cade 100644 --- a/docs/api/variables/VERSION.md +++ b/docs/api/variables/VERSION.md @@ -8,4 +8,4 @@ > `const` **VERSION**: `"0.7.7"` = `'0.7.7'` -Defined in: [version.ts:1](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/version.ts#L1) +Defined in: [version.ts:1](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/version.ts#L1) diff --git a/docs/api/variables/default.md b/docs/api/variables/default.md index 7246d58a2..e54dfec50 100644 --- a/docs/api/variables/default.md +++ b/docs/api/variables/default.md @@ -8,7 +8,7 @@ > `const` **default**: `object` -Defined in: [index.ts:826](https://github.com/chriswritescode-dev/opencode-forge/blob/69298c7174999585054d057751054e338fb60412/src/index.ts#L826) +Defined in: [index.ts:826](https://github.com/chriswritescode-dev/opencode-forge/blob/ebdaf6f48681f30bd723c30b2316902cb1c5fcda/src/index.ts#L826) ## Type Declaration diff --git a/docs/configuration.md b/docs/configuration.md index 8fe184f79..cd8416900 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,7 +14,7 @@ See also: [Tools](tools.md), [Agents and Slash Commands](agents-and-commands.md) | `auditorModel` | `""` | Fallback model override for auditor sessions. Format: `provider/model`. | | `executionVariant` | `""` | Default reasoning/thinking variant for the execution model (e.g. `high`, `max`). | | `auditorVariant` | `""` | Default reasoning/thinking variant for the auditor model. Independent — does not inherit `executionVariant`. | -| `auditorFallbackModels` | `[]` | Ordered fallback auditor models tried when the current auditor model hits a provider usage/auth limit mid-loop. Entries use `provider/model` format and are tried in order; variants are NOT inherited. Applies only to `auditing`/`final_auditing`. The fallback index resets to `0` on loop restart. Empty/omitted means a limited auditor terminates the loop. | +| `auditorFallbackModels` | `[]` | Ordered fallback auditor models tried when the current auditor model hits a provider usage/auth limit mid-loop. Entries use `provider/model` format and are tried in order; variants are NOT inherited. Applies only to `auditing`/`final_auditing`. The fallback index resets to `0` after any successful audit (so the preferred model and its variant are retried on the next audit) and on loop restart. Empty/omitted means a limited auditor terminates the loop. | | `agents` | unset | Per-agent overrides keyed by display name, currently supporting `temperature`. | | `remotes` | unset | Remote opencode servers available as loop launch targets in the TUI execution dialog. See [Remotes](#remotes). | | `dashboard` | unset | Dashboard HTTP server bind host and port. Defaults to loopback only. See [Dashboard](#dashboard). | diff --git a/src/dashboard/app-bundle.ts b/src/dashboard/app-bundle.ts index b3049ac80..69aa243ac 100644 --- a/src/dashboard/app-bundle.ts +++ b/src/dashboard/app-bundle.ts @@ -1,3 +1,3 @@ // Auto-generated from src/dashboard/app. Do not edit. -export const DASHBOARD_APP_BUNDLE: string = "var v={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return nt(this.context.count)},getNextContextId(){return nt(this.context.count++)}};function nt(e){let n=String(e),t=n.length-1;return v.context.id+(t?String.fromCharCode(96+t):\"\")+n}function sn(e){v.context=e}function cf(){return{...v.context,id:v.getNextContextId(),count:0}}var lf=!0,sf=(e,n)=>e===n,J=Symbol(\"solid-proxy\"),df=typeof Proxy===\"function\",hn=Symbol(\"solid-track\"),ct=Symbol(\"solid-dev-component\"),Ue={equals:sf},tt=null,lt=$t,D=1,Ce=2,hf={};var k=null,O=null,Ae=null,re=null,W=null,_=null,T=null,Ze=0,Q={afterUpdate:null,afterCreateOwner:null,afterCreateSignal:null,afterRegisterGraph:null};function st(e,n){let t=W,i=k,f=e.length===0,u=n===void 0?i:n,r=f?{owned:null,cleanups:null,context:null,owner:null}:{owned:null,cleanups:null,context:u?u.context:null,owner:u},c=f?()=>e(()=>{throw Error(\"Dispose method must be an explicit argument to createRoot function\")}):()=>e(()=>H(()=>ne(r)));Q.afterCreateOwner&&Q.afterCreateOwner(r),k=r,W=null;try{return B(c,!0)}finally{W=t,k=i}}function K(e,n){n=n?Object.assign({},Ue,n):Ue;let t={value:e,observers:null,observerSlots:null,comparator:n.equals||void 0};{if(n.name)t.name=n.name;if(n.internal)t.internal=!0;else if(ot(t),Q.afterCreateSignal)Q.afterCreateSignal(t)}let i=(f)=>{if(typeof f===\"function\")if(O&&O.running&&O.sources.has(t))f=f(t.tValue);else f=f(t.value);return yn(t,f)};return[yt.bind(t),i]}function M(e,n,t){let i=Xe(e,n,!1,D,t);if(Ae&&O&&O.running)_.push(i);else le(i)}function R(e,n,t){lt=gf;let i=Xe(e,n,!1,D,t),f=dn&&yf(dn);if(f)i.suspense=f;if(!t||!t.render)i.user=!0;T?T.push(i):le(i)}function g(e,n,t){t=t?Object.assign({},Ue,t):Ue;let i=Xe(e,n,!0,0,t);if(i.observers=null,i.observerSlots=null,i.comparator=t.equals||void 0,Ae&&O&&O.running)i.tState=D,_.push(i);else le(i);return yt.bind(i)}function Ne(e){return B(e,!1)}function H(e){if(!re&&W===null)return e();let n=W;W=null;try{if(re)return re.untrack(e);return e()}finally{W=n}}function dt(e,n,t){let i=Array.isArray(e),f,u=t&&t.defer;return(r)=>{let c;if(i){c=Array(e.length);for(let s=0;sn(c,f,r));return f=c,l}}function ht(e){R(()=>H(e))}function ce(e){if(k===null)console.warn(\"cleanups created outside a `createRoot` or `render` will never be run\");else if(k.cleanups===null)k.cleanups=[e];else k.cleanups.push(e);return e}function qe(){return W}function De(){return k}function on(e,n){let t=k,i=W;k=e,W=null;try{return B(n,!0)}catch(f){Je(f)}finally{k=t,W=i}}function of(e){if(O&&O.running)return e(),O.done;let n=W,t=k;return Promise.resolve().then(()=>{W=n,k=t;let i;if(Ae||dn)i=O||(O={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),i.done||(i.done=new Promise((f)=>i.resolve=f)),i.running=!0;return B(e,!1),W=k=null,i?i.done:void 0})}var[_u,it]=K(!1);function ft(e,n){let t=Xe(()=>H(()=>{return Object.assign(e,{[ct]:!0}),e(n)}),void 0,!0,0);return t.props=n,t.observers=null,t.observerSlots=null,t.name=e.name,t.component=e,le(t),t.tValue!==void 0?t.tValue:t.value}function ot(e){if(k){if(k.sourceMap)k.sourceMap.push(e);else k.sourceMap=[e];e.graph=k}if(Q.afterRegisterGraph)Q.afterRegisterGraph(e)}function yf(e){let n;return k&&k.context&&(n=k.context[e.id])!==void 0?n:e.defaultValue}var dn;function yt(){let e=O&&O.running;if(this.sources&&(e?this.tState:this.state))if((e?this.tState:this.state)===D)le(this);else{let n=_;_=null,B(()=>je(this),!1),_=n}if(W){let n=this.observers?this.observers.length:0;if(!W.sources)W.sources=[this],W.sourceSlots=[n];else W.sources.push(this),W.sourceSlots.push(n);if(!this.observers)this.observers=[W],this.observerSlots=[W.sources.length-1];else this.observers.push(W),this.observerSlots.push(W.sources.length-1)}if(e&&O.sources.has(this))return this.tValue;return this.value}function yn(e,n,t){let i=O&&O.running&&O.sources.has(e)?e.tValue:e.value;if(!e.comparator||!e.comparator(i,n)){if(O){let f=O.running;if(f||!t&&O.sources.has(e))O.sources.add(e),e.tValue=n;if(!f)e.value=n}else e.value=n;if(e.observers&&e.observers.length)B(()=>{for(let f=0;f1e6){if(_=[],lf)throw Error(\"Potential Infinite Loop Detected.\");throw Error()}},!1)}return n}function le(e){if(!e.fn)return;ne(e);let n=Ze;if(ut(e,O&&O.running&&O.sources.has(e)?e.tValue:e.value,n),O&&!O.running&&O.sources.has(e))queueMicrotask(()=>{B(()=>{O&&(O.running=!0),W=k=e,ut(e,e.tValue,n),W=k=null},!1)})}function ut(e,n,t){let i,f=k,u=W;W=k=e;try{i=e.fn(n)}catch(r){if(e.pure)if(O&&O.running)e.tState=D,e.tOwned&&e.tOwned.forEach(ne),e.tOwned=void 0;else e.state=D,e.owned&&e.owned.forEach(ne),e.owned=null;return e.updatedAt=t+1,Je(r)}finally{W=u,k=f}if(!e.updatedAt||e.updatedAt<=t){if(e.updatedAt!=null&&\"observers\"in e)yn(e,i,!0);else if(O&&O.running&&e.pure){if(!O.sources.has(e))e.value=i;O.sources.add(e),e.tValue=i}else e.value=i;e.updatedAt=t}}function Xe(e,n,t,i=D,f){let u={fn:e,state:i,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:n,owner:k,context:k?k.context:null,pure:t};if(O&&O.running)u.state=0,u.tState=i;if(k===null)console.warn(\"computations created outside a `createRoot` or `render` will never be disposed\");else if(k!==hf)if(O&&O.running&&k.pure)if(!k.tOwned)k.tOwned=[u];else k.tOwned.push(u);else if(!k.owned)k.owned=[u];else k.owned.push(u);if(f&&f.name)u.name=f.name;if(re&&u.fn){let r=u.fn,[c,l]=K(void 0,{equals:!1}),s=re.factory(r,l);ce(()=>s.dispose());let h,x=()=>of(l).then(()=>{if(h)h.dispose(),h=void 0});u.fn=(E)=>{if(c(),O&&O.running){if(!h)h=re.factory(r,x);return h.track(E)}return s.track(E)}}return Q.afterCreateOwner&&Q.afterCreateOwner(u),u}function Se(e){let n=O&&O.running;if((n?e.tState:e.state)===0)return;if((n?e.tState:e.state)===Ce)return je(e);if(e.suspense&&H(e.suspense.inFallback))return e.suspense.effects.push(e);let t=[e];while((e=e.owner)&&(!e.updatedAt||e.updatedAt=0;i--){if(e=t[i],n){let f=e,u=t[i+1];while((f=f.owner)&&f!==u)if(O.disposed.has(f))return}if((n?e.tState:e.state)===D)le(e);else if((n?e.tState:e.state)===Ce){let f=_;_=null,B(()=>je(e,t[0]),!1),_=f}}}function B(e,n){if(_)return e();let t=!1;if(!n)_=[];if(T)t=!0;else T=[];Ze++;try{let i=e();return $f(t),i}catch(i){if(!t)T=null;_=null,Je(i)}}function $f(e){if(_){if(Ae&&O&&O.running)wf(_);else $t(_);_=null}if(e)return;let n;if(O){if(!O.promises.size&&!O.queue.size){let{sources:i,disposed:f}=O;T.push.apply(T,O.effects),n=O.resolve;for(let u of T)\"tState\"in u&&(u.state=u.tState),delete u.tState;O=null,B(()=>{for(let u of f)ne(u);for(let u of i){if(u.value=u.tValue,u.owned)for(let r=0,c=u.owned.length;rlt(t),!1);else Q.afterUpdate&&Q.afterUpdate();if(n)n()}function $t(e){for(let n=0;n{i.delete(t),B(()=>{O.running=!0,Se(t)},!1),O&&(O.running=!1)})}}function gf(e){let n,t=0;for(n=0;n=0;n--)ne(e.tOwned[n]);delete e.tOwned}if(O&&O.running&&e.pure)gt(e,!0);else if(e.owned){for(n=e.owned.length-1;n>=0;n--)ne(e.owned[n]);e.owned=null}if(e.cleanups){for(n=e.cleanups.length-1;n>=0;n--)e.cleanups[n]();e.cleanups=null}if(O&&O.running)e.tState=0;else e.state=0;delete e.sourceMap}function gt(e,n){if(!n)e.tState=0,O.disposed.add(e);if(e.owned)for(let t=0;t=0;c--){let l=ln(e[c])[r];if(l!==void 0)return l}},has(r){for(let c=e.length-1;c>=0;c--)if(r in ln(e[c]))return!0;return!1},keys(){let r=[];for(let c=0;c=0;r--){let c=e[r];if(!c)continue;let l=Object.getOwnPropertyNames(c);for(let s=l.length-1;s>=0;s--){let h=l[s];if(h===\"__proto__\"||h===\"constructor\")continue;let x=Object.getOwnPropertyDescriptor(c,h);if(!i[h])i[h]=x.get?{enumerable:!0,configurable:!0,get:Of.bind(t[h]=[x.get.bind(c)])}:x.value!==void 0?x:void 0;else{let E=t[h];if(E){if(x.get)E.push(x.get.bind(c));else if(x.value!==void 0)E.push(()=>x.value)}}}}let f={},u=Object.keys(i);for(let r=u.length-1;r>=0;r--){let c=u[r],l=i[c];if(l&&l.get)Object.defineProperty(f,c,l);else f[c]=l?l.value:void 0}return f}var bt={hooks:Q,writeSignal:yn,registerGraph:ot};if(globalThis)if(!globalThis.Solid$$)globalThis.Solid$$=!0;else console.warn(\"You appear to have multiple instances of Solid. This can lead to unexpected behavior.\");var Ef=[\"allowfullscreen\",\"async\",\"alpha\",\"autofocus\",\"autoplay\",\"checked\",\"controls\",\"default\",\"disabled\",\"formnovalidate\",\"hidden\",\"indeterminate\",\"inert\",\"ismap\",\"loop\",\"multiple\",\"muted\",\"nomodule\",\"novalidate\",\"open\",\"playsinline\",\"readonly\",\"required\",\"reversed\",\"seamless\",\"selected\",\"adauctionheaders\",\"browsingtopics\",\"credentialless\",\"defaultchecked\",\"defaultmuted\",\"defaultselected\",\"defer\",\"disablepictureinpicture\",\"disableremoteplayback\",\"preservespitch\",\"shadowrootclonable\",\"shadowrootcustomelementregistry\",\"shadowrootdelegatesfocus\",\"shadowrootserializable\",\"sharedstoragewritable\"],bn=new Set([\"className\",\"value\",\"readOnly\",\"noValidate\",\"formNoValidate\",\"isMap\",\"noModule\",\"playsInline\",\"adAuctionHeaders\",\"allowFullscreen\",\"browsingTopics\",\"defaultChecked\",\"defaultMuted\",\"defaultSelected\",\"disablePictureInPicture\",\"disableRemotePlayback\",\"preservesPitch\",\"shadowRootClonable\",\"shadowRootCustomElementRegistry\",\"shadowRootDelegatesFocus\",\"shadowRootSerializable\",\"sharedStorageWritable\",...Ef]),xn=new Set([\"innerHTML\",\"textContent\",\"innerText\",\"children\"]),an=Object.assign(Object.create(null),{className:\"class\",htmlFor:\"for\"}),Cf=Object.assign(Object.create(null),{class:\"className\",novalidate:{$:\"noValidate\",FORM:1},formnovalidate:{$:\"formNoValidate\",BUTTON:1,INPUT:1},ismap:{$:\"isMap\",IMG:1},nomodule:{$:\"noModule\",SCRIPT:1},playsinline:{$:\"playsInline\",VIDEO:1},readonly:{$:\"readOnly\",INPUT:1,TEXTAREA:1},adauctionheaders:{$:\"adAuctionHeaders\",IFRAME:1},allowfullscreen:{$:\"allowFullscreen\",IFRAME:1},browsingtopics:{$:\"browsingTopics\",IMG:1},defaultchecked:{$:\"defaultChecked\",INPUT:1},defaultmuted:{$:\"defaultMuted\",AUDIO:1,VIDEO:1},defaultselected:{$:\"defaultSelected\",OPTION:1},disablepictureinpicture:{$:\"disablePictureInPicture\",VIDEO:1},disableremoteplayback:{$:\"disableRemotePlayback\",AUDIO:1,VIDEO:1},preservespitch:{$:\"preservesPitch\",AUDIO:1,VIDEO:1},shadowrootclonable:{$:\"shadowRootClonable\",TEMPLATE:1},shadowrootdelegatesfocus:{$:\"shadowRootDelegatesFocus\",TEMPLATE:1},shadowrootserializable:{$:\"shadowRootSerializable\",TEMPLATE:1},sharedstoragewritable:{$:\"sharedStorageWritable\",IFRAME:1,IMG:1}});function On(e,n){let t=Cf[e];return typeof t===\"object\"?t[n]?t.$:void 0:t}var En=new Set([\"beforeinput\",\"click\",\"dblclick\",\"contextmenu\",\"focusin\",\"focusout\",\"input\",\"keydown\",\"keyup\",\"mousedown\",\"mousemove\",\"mouseout\",\"mouseover\",\"mouseup\",\"pointerdown\",\"pointermove\",\"pointerout\",\"pointerover\",\"pointerup\",\"touchend\",\"touchmove\",\"touchstart\"]),Ct=new Set([\"altGlyph\",\"altGlyphDef\",\"altGlyphItem\",\"animate\",\"animateColor\",\"animateMotion\",\"animateTransform\",\"circle\",\"clipPath\",\"color-profile\",\"cursor\",\"defs\",\"desc\",\"ellipse\",\"feBlend\",\"feColorMatrix\",\"feComponentTransfer\",\"feComposite\",\"feConvolveMatrix\",\"feDiffuseLighting\",\"feDisplacementMap\",\"feDistantLight\",\"feDropShadow\",\"feFlood\",\"feFuncA\",\"feFuncB\",\"feFuncG\",\"feFuncR\",\"feGaussianBlur\",\"feImage\",\"feMerge\",\"feMergeNode\",\"feMorphology\",\"feOffset\",\"fePointLight\",\"feSpecularLighting\",\"feSpotLight\",\"feTile\",\"feTurbulence\",\"filter\",\"font\",\"font-face\",\"font-face-format\",\"font-face-name\",\"font-face-src\",\"font-face-uri\",\"foreignObject\",\"g\",\"glyph\",\"glyphRef\",\"hkern\",\"image\",\"line\",\"linearGradient\",\"marker\",\"mask\",\"metadata\",\"missing-glyph\",\"mpath\",\"path\",\"pattern\",\"polygon\",\"polyline\",\"radialGradient\",\"rect\",\"set\",\"stop\",\"svg\",\"switch\",\"symbol\",\"text\",\"textPath\",\"tref\",\"tspan\",\"use\",\"view\",\"vkern\"]),Cn={xlink:\"http://www.w3.org/1999/xlink\",xml:\"http://www.w3.org/XML/1998/namespace\"};function Sf(e,n,t){let i=t.length,f=n.length,u=i,r=0,c=0,l=n[f-1].nextSibling,s=null;while(rh-c){let d=n[r];while(c{f=u,n===document?e():kn(n,e(),n.firstChild?null:void 0,t)},i.owner),()=>{f(),n.textContent=\"\"}}function Sn(e,n=window.document){let t=n[xt]||(n[xt]=new Set);for(let i=0,f=e.length;if.call(e,t[1],u))}else e.addEventListener(n,t,typeof t!==\"function\"&&t)}function Pn(e,n,t={}){let i=Object.keys(n||{}),f=Object.keys(t),u,r;for(u=0,r=f.length;uf.children=de(e,n.children,f.children));return M(()=>typeof n.ref===\"function\"&&Pf(n.ref,e)),M(()=>Vf(e,n,t,!0,f,!0)),f}function Nt(e,n){let t=e[n];return Object.defineProperty(e,n,{get(){return t()},enumerable:!0}),e}function Pf(e,n,t){return H(()=>e(n,t))}function kn(e,n,t,i){if(t!==void 0&&!i)i=[];if(typeof n!==\"function\")return de(e,n,i,t);M((f)=>de(e,n(),f,t),i)}function Vf(e,n,t,i,f={},u=!1){n||(n={});for(let r in f)if(!(r in n)){if(r===\"children\")continue;f[r]=Ot(e,r,null,f[r],t,u,n)}for(let r in n){if(r===\"children\"){if(!i)de(e,n.children);continue}let c=n[r];f[r]=Ot(e,r,c,f[r],t,u,n)}}function he(e){return!!v.context&&!v.done&&(!e||e.isConnected)}function kf(e){return e.toLowerCase().replace(/-([a-z])/g,(n,t)=>t.toUpperCase())}function at(e,n,t){let i=n.trim().split(/\\s+/);for(let f=0,u=i.length;f-1&&Cn[n.split(\":\")[0]];if(E)An(e,E,n,t);else Pe(e,an[n]||n,t)}return t}function If(e){if(v.registry&&v.events){if(v.events.find(([l,s])=>s===e))return}let n=e.target,t=`$$${e.type}`,i=e.target,f=e.currentTarget,u=(l)=>Object.defineProperty(e,\"target\",{configurable:!0,value:l}),r=()=>{let l=n[t];if(l&&!n.disabled){let s=n[`${t}Data`];if(s!==void 0?l.call(n,s,e):l.call(n,e),e.cancelBubble)return}return n.host&&typeof n.host!==\"string\"&&!n.host._$host&&n.contains(e.target)&&u(n.host),!0},c=()=>{while(r()&&(n=n._$host||n.parentNode||n.host));};if(Object.defineProperty(e,\"currentTarget\",{configurable:!0,get(){return n||document}}),v.registry&&!v.done)v.done=_$HY.done=!0;if(e.composedPath){let l=e.composedPath();u(l[0]);for(let s=0;s{let l=n();while(typeof l===\"function\")l=l();t=de(e,l,t,i)}),()=>t;else if(Array.isArray(n)){let l=[],s=t&&Array.isArray(t);if(gn(l,n,t,f))return M(()=>t=de(e,l,t,i,!0)),()=>t;if(u){if(!l.length)return t;if(i===void 0)return t=[...e.childNodes];let h=l[0];if(h.parentNode!==e)return t;let x=[h];while((h=h.nextSibling)!==i)x.push(h);return t=x}if(l.length===0){if(t=se(e,t,i),c)return t}else if(s)if(t.length===0)Et(e,l,i);else Sf(e,t,l);else t&&se(e),Et(e,l);t=l}else if(n.nodeType){if(u&&n.parentNode)return t=c?[n]:n;if(Array.isArray(t)){if(c)return t=se(e,t,i,n);se(e,t,null,n)}else if(t==null||t===\"\"||!e.firstChild)e.appendChild(n);else e.replaceChild(n,e.firstChild);t=n}else console.warn(\"Unrecognized value. Skipped inserting\",n);return t}function gn(e,n,t,i){let f=!1;for(let u=0,r=n.length;u=0;r--){let c=n[r];if(f!==c){let l=c.parentNode===e;if(!u&&!r)l?e.replaceChild(f,c):e.insertBefore(f,t);else l&&c.remove()}else u=!0}}else e.insertBefore(f,t);return[f]}var Lu=Symbol();var In=Symbol(\"store-raw\"),ye=Symbol(\"store-node\"),ee=Symbol(\"store-has\"),Vt=Symbol(\"store-self\"),Pt={onStoreNodeUpdate:null};function kt(e){let n=e[J];if(!n){if(Object.defineProperty(e,J,{value:n=new Proxy(e,Wf)}),!Array.isArray(e)){let t=Object.keys(e),i=Object.getOwnPropertyDescriptors(e);for(let f=0,u=t.length;fe[J][n],t}function It(e){qe()&&ke(Qe(e,ye),Vt)()}function zf(e){return It(e),Reflect.ownKeys(e)}var Wf={get(e,n,t){if(n===In)return e;if(n===J)return t;if(n===hn)return It(e),t;let i=Qe(e,ye),f=i[n],u=f?f():e[n];if(n===ye||n===ee||n===\"__proto__\")return u;if(!f){let r=Object.getOwnPropertyDescriptor(e,n);if(qe()&&(typeof u!==\"function\"||e.hasOwnProperty(n))&&!(r&&r.get))u=ke(i,n,u)()}return te(u)?kt(u):u},has(e,n){if(n===In||n===J||n===hn||n===ye||n===ee||n===\"__proto__\")return!0;return qe()&&ke(Qe(e,ee),n)(),n in e},set(){return console.warn(\"Cannot mutate a Store directly\"),!0},deleteProperty(){return console.warn(\"Cannot mutate a Store directly\"),!0},ownKeys:zf,getOwnPropertyDescriptor:Kf};function X(e,n,t,i=!1){if(!i&&e[n]===t)return;let f=e[n],u=e.length;if(Pt.onStoreNodeUpdate&&Pt.onStoreNodeUpdate(e,n,t,f),t===void 0){if(delete e[n],e[ee]&&e[ee][n]&&f!==void 0)e[ee][n].$()}else if(e[n]=t,e[ee]&&e[ee][n]&&f===void 0)e[ee][n].$();let r=Qe(e,ye),c;if(c=ke(r,n,f))c.$(()=>t);if(Array.isArray(e)&&e.length!==u){for(let l=e.length;l1){i=n.shift();let r=typeof i,c=Array.isArray(e);if(Array.isArray(i)){for(let l=0;l1){Ve(e[i],n,[i].concat(t));return}f=e[i],t=[i].concat(t)}let u=n[0];if(typeof u===\"function\"){if(u=u(f,t),u===f)return}if(i===void 0&&u==null)return;if(u=$e(u),i===void 0||te(f)&&te(u)&&!Array.isArray(u))Kt(f,u);else X(e,i,u)}function zt(...[e,n]){let t=$e(e||{}),i=Array.isArray(t);if(typeof t!==\"object\"&&typeof t!==\"function\")throw Error(`Unexpected type ${typeof t} received when initializing 'createStore'. Expected an object.`);let f=kt(t);bt.registerGraph({value:t,name:n&&n.name});function u(...r){Ne(()=>{i&&r.length===1?Yf(t,r[0]):Ve(t,r)})}return[f,u]}var Kn=Symbol(\"store-root\");function oe(e,n,t,i,f){let u=n[t];if(e===u)return;let r=Array.isArray(e);if(t!==Kn&&(!te(e)||!te(u)||r!==Array.isArray(u)||f&&e[f]!==u[f])){X(n,t,e);return}if(r){if(e.length&&u.length&&(!i||f&&e[0]&&e[0][f]!=null)){let s,h,x,E,w,d,y,b;for(x=0,E=Math.min(u.length,e.length);x=x&&w>=x&&(u[E]===e[w]||f&&u[E]&&e[w]&&u[E][f]&&u[E][f]===e[w][f]);E--,w--)C[w]=u[E];if(x>w||x>E){for(h=x;h<=w;h++)X(u,h,e[h]);for(;he.length)X(u,\"length\",e.length);return}y=Array(w+1);for(h=w;h>=x;h--)d=e[h],b=f&&d?d[f]:d,s=V.get(b),y[h]=s===void 0?-1:s,V.set(b,h);for(s=x;s<=E;s++)if(d=u[s],b=f&&d?d[f]:d,h=V.get(b),h!==void 0&&h!==-1)C[h]=u[s],h=y[h],V.set(b,h);for(h=x;he.length)X(u,\"length\",e.length);return}let c=Object.keys(e);for(let s=0,h=c.length;s{if(!te(u)||!te(f))return f;let r=oe(f,{[Kn]:u},Kn,t,i);return r===void 0?u:r}}var vf=/(?:|<(?:\"[^\"]*\"['\"]*|'[^']*'['\"]*|[^'\">])+>)/g,Ff=/(?:\\s(?[^/\\s><=]+?)(?=[\\s/>]))|(?:(?\\S+?)(?:\\s*=\\s*(?:(['\"])(?[\\s\\S]*?)\\3|(?[^\\s>]+))))/g,_f={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,menuitem:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0};function Tf(e){let n={type:\"tag\",name:\"\",voidElement:!1,attrs:[],children:[]},t=e.match(/<\\/?([^\\s]+?)[/\\s>]/);if(t){if(n.name=t[1],_f[t[1].toLowerCase()]||e.charAt(e.length-2)===\"/\")n.voidElement=!0;if(n.name.startsWith(\"!--\")){let f=e.indexOf(\"-->\");return{type:\"comment\",comment:f!==-1?e.slice(4,f):\"\"}}}let i=new RegExp(Ff);for(let f of e.matchAll(i))if((f[1]||f[2]).startsWith(\"use:\"))n.attrs.push({type:\"directive\",name:f[1]||f[2],value:f[4]||f[5]||\"\"});else n.attrs.push({type:\"attr\",name:f[1]||f[2],value:f[4]||f[5]||\"\"});return n}function Yt(e,n,t){let i=n.indexOf(\"<\",t),f=n.slice(t,i===-1?void 0:i);if(!/^\\s*$/.test(f))e.push({type:\"text\",content:f})}function vt(e,n){let t=n.replace(\"\",\"\");if(!/^\\s*$/.test(t))e.push({type:\"comment\",content:t})}function Hf(e){let n=[],t=void 0,i=-1,f=[],u={};return e.replace(vf,(r,c)=>{let l=r.charAt(1)!==\"/\",s=r.slice(0,4)===\"\"}}function Uf(e){return e.reduce(function(n,t){return n+Tt(\"\",t)},\"\")}var Ft=new Map,jf=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,Be=\" \\\\f\\\\n\\\\r\\\\t\",Ht=\"[^\"+Be+`\\\\/>\"'=]+`,Wn=\"[ \"+Be+\"]+(?:use:|\"+Ht+\")\",Gt=\"<([A-Za-z$#]+[A-Za-z0-9:_-]*)((?:\",Ut=`(?:\\\\s*=\\\\s*(?:'[^']*?'|\"[^\"]*?\"|\\\\([^)]*?\\\\)|<[^>]*?>|`+Ht+\"))?)\",Zf=new RegExp(Gt+Wn+Ut+\"+)([ \"+Be+\"]*/?>)\",\"g\"),qf=new RegExp(\"(\"+Wn+`\\\\s*=\\\\s*)(|['\"(]([\\\\w\\\\s]*[\\\\w\\\\s]*)*['\")])`,\"gi\"),Df=new RegExp(Gt+Wn+Ut+\"*)([ \"+Be+\"]*/>)\",\"g\"),_t=\"\",Xf=new Set([\"class\",\"on\",\"oncapture\",\"style\",\"use\",\"prop\",\"attr\"]);function Jf(e,n,t,i){return\"<\"+n+t.replace(qf,Qf)+i}function Qf(e,n,t){return n.replace(//g,\"###\")+(t[0]==='\"'||t[0]===\"'\"?t.replace(//g,\"###\"):'\"###\"')}function Bf(e,n,t){return jf.test(n)?e:\"<\"+n+t+\">\"}function Rf(e){return e.toLowerCase().replace(/-([a-z])/g,(n,t)=>t.toUpperCase())}function zn(e,n,t,i){if(e===\"use:###\"&&n===\"###\"){let f=i.counter++;i.exprs.push(`typeof exprs[${f}] === \"function\" ? r.use(exprs[${f}], ${t}, exprs[${i.counter++}]) : (()=>{throw new Error(\"use:### must be a function\")})()`)}else throw Error(`Not support syntax ${e} must be use:{function}`)}function Lf(e,{delegateEvents:n=!0,functionBuilder:t=(...i)=>Function(...i)}={}){let i=1;e.wrapProps=(w)=>{let d=Object.getOwnPropertyDescriptors(w);for(let y in d)if(typeof d[y].value===\"function\"&&!d[y].value.length)e.dynamicProperty(w,y);return w};function f(w,d){let y=0,b=\"\";for(;y\";b=b+w[y],b=[[Df,Bf],[/<()/g,\"<###\"],[/\\.\\.\\.()/g,\"###\"],[Zf,Jf],[/>\\n+\\s*/g,\">\"],[/\\n+\\s*\\s+/g,\"> \"]].reduce((I,z)=>{return I.replace(z[0],z[1])},b);let V=Hf(b),[A,S]=x(V,d.funcBuilder),P=[];for(let I=0;I-1){let m=F.split(_t).reduce((p,ve,ae)=>{return ae&&p.push(\"\"),p.push(ve),p},[]);z[I].firstChild.replaceWith(...m)}}}return P[0].create=S,Ft.set(w,P),P}function u(w,d,y,b,C,V,A){let S=b===\"###\"?`!doNotWrap ? exprs[${A.counter}]() : exprs[${A.counter++}]`:b.split(\"###\").map((F,m)=>m?` + (typeof exprs[${A.counter}] === \"function\" ? exprs[${A.counter}]() : exprs[${A.counter++}]) + \"${F}\"`:`\"${F}\"`).join(\"\"),P,I;if((P=y.split(\":\"))&&P[1]&&Xf.has(P[0]))y=P[1],I=P[0];let z=e.ChildProperties.has(y),G=e.Properties.has(y);if(y===\"style\"){let F=`_$v${i++}`;A.decl.push(`${F}={}`),A.exprs.push(`r.style(${d},${S},${F})`)}else if(y===\"classList\"){let F=`_$v${i++}`;A.decl.push(`${F}={}`),A.exprs.push(`r.classList(${d},${S},${F})`)}else if(I!==\"attr\"&&(z||!C&&(e.getPropAlias(y,w.name.toUpperCase())||G)||V||I===\"prop\")){if(V&&!z&&!G&&I!==\"prop\")y=Rf(y);A.exprs.push(`${d}.${e.getPropAlias(y,w.name.toUpperCase())||y} = ${S}`)}else{let F=C&&y.indexOf(\":\")>-1&&e.SVGNamespace[y.split(\":\")[0]];if(F)A.exprs.push(`r.setAttributeNS(${d},\"${F}\",\"${y}\",${S})`);else A.exprs.push(`r.setAttribute(${d},\"${e.Aliases[y]||y}\",${S})`)}}function r(w,d,y,b,C,V,A){if(y.slice(0,2)===\"on\")if(!y.includes(\":\")){let S=y.slice(2).toLowerCase(),P=n&&e.DelegatedEvents.has(S);A.exprs.push(`r.addEventListener(${d},\"${S}\",exprs[${A.counter++}],${P})`),P&&A.delegatedEvents.add(S)}else{let S=y.startsWith(\"oncapture:\");A.exprs.push(`${d}.addEventListener(\"${y.slice(S?10:3)}\",exprs[${A.counter++}]${S?\",true\":\"\"})`)}else if(y===\"ref\")A.exprs.push(`exprs[${A.counter++}](${d})`);else{let S=Object.assign({},A,{exprs:[]}),P=A.counter;if(u(w,d,y,b,C,V,S),A.decl.push(`_fn${P} = (${b===\"###\"?\"doNotWrap\":\"\"}) => {\n${S.exprs.join(`;\n`)};\n}`),b===\"###\")A.exprs.push(`typeof exprs[${P}] === \"function\" ? r.effect(_fn${P}) : _fn${P}(true)`);else{let I=\"\";for(let z=P;z1)for(let C=0;C1?`r.mergeProps(${d.join(\",\")})`:d[0]}function s(w,d){let y=[],b=Object.keys(w.attrs),C=[y],V=d.counter++;for(let S=0;S exprs[${d.counter++}]`);else if(w.children.length){let S={type:\"fragment\",children:w.children},P=Object.assign({},d,{first:!0,decl:[],exprs:[],parent:!1});h(S,P),y.push(`children: () => { ${P.exprs.join(`;\n`)}}`),d.templateId=P.templateId,d.counter=P.counter}let A;if(d.multi)A=`_$el${i++}`,d.decl.push(`${A} = ${d.path}.${d.first?\"firstChild\":\"nextSibling\"}`);if(d.parent)d.exprs.push(`r.insert(${d.parent}, r.createComponent(exprs[${V}],${l(C)})${A?`, ${A}`:\"\"})`);else d.exprs.push(`${d.fragment?\"\":\"return \"}r.createComponent(exprs[${V}],${l(C)})`);d.path=A,d.first=!1}function h(w,d){if(w.type===\"fragment\"){let y=[];w.children.forEach((b)=>{if(b.type===\"tag\"){if(b.name===\"###\"){let A=Object.assign({},d,{first:!0,fragment:!0,decl:[],exprs:[]});s(b,A),y.push(A.exprs[0]),d.counter=A.counter,d.templateId=A.templateId;return}d.templateId++;let C=i,V=Object.assign({},d,{first:!0,decl:[],exprs:[]});d.templateNodes.push([b]),h(b,V),y.push(`function() { ${V.decl.join(`,\n`)+`;\n`+V.exprs.join(`;\n`)+`;\nreturn _$el${C};\n`}}()`),d.counter=V.counter,d.templateId=V.templateId}else if(b.type===\"text\")y.push(`\"${b.content}\"`);else if(b.type===\"comment\"){if(b.content===\"#\")y.push(`exprs[${d.counter++}]`);else if(b.content)for(let C=0;CS.name===\"is\");if(d.hasCustomElement=A,d.isImportNode=(w.name===\"img\"||w.name===\"iframe\")&&w.attrs.some((S)=>S.name===\"loading\"&&S.value===\"lazy\"),w.attrs.some((S)=>S.name===\"###\")){let S=[],P=\"\",I=[];for(let z=0;z({${P}})`),P=\"\";S.push(`exprs[${d.counter++}]`)}else I.push(w.attrs[z]);else if(G===\"directive\")zn(F,m,y,d)}if(w.attrs=I,P.length)S.push(`()=>({${P}})`);d.exprs.push(`r.spread(${y},${S.length===1?`typeof ${S[0]} === \"function\" ? r.mergeProps(${S[0]}) : ${S[0]}`:`r.mergeProps(${S.join(\",\")})`},${V},${!!w.children.length})`)}else for(let S=0;S document.importNode(tmpls[${C}].content.firstChild, true))`:`const ${y} = tmpls[${C}].content.firstChild.cloneNode(true)`}else if(w.type===\"text\"){let y=`_$el${i++}`;d.decl.push(`${y} = ${d.path}.${d.first?\"firstChild\":\"nextSibling\"}`),d.path=y,d.first=!1}else if(w.type===\"comment\"){let y=`_$el${i++}`;if(d.decl.push(`${y} = ${d.path}.${d.first?\"firstChild\":\"nextSibling\"}`),w.content===\"#\")if(d.multi)d.exprs.push(`r.insert(${d.parent}, exprs[${d.counter++}], ${y})`);else d.exprs.push(`r.insert(${d.parent}, exprs[${d.counter++}])`);d.path=y,d.first=!1}}function x(w,d){let y={path:\"\",decl:[],exprs:[],delegatedEvents:new Set,counter:0,first:!0,multi:!1,templateId:0,templateNodes:[]},b=i,C=w,V;if(w.length>1)w=[{type:\"fragment\",children:w}];if(w[0].name===\"###\")V=!0,s(w[0],y);else h(w[0],y);return e.delegateEvents(Array.from(y.delegatedEvents)),[[C].concat(y.templateNodes).map((S)=>Uf(S)),d(\"tmpls\",\"exprs\",\"r\",y.decl.join(`,\n`)+`;\n`+y.exprs.join(`;\n`)+(V?\"\":`;\nreturn _$el${b};\n`))]}function E(w,...d){let y=Ft.get(w)||f(w,{funcBuilder:t});return y[0].create(y,d,e)}return E}var o=Lf({effect:M,style:Vn,insert:kn,untrack:H,spread:At,createComponent:$n,delegateEvents:Sn,classList:Pn,mergeProps:wn,dynamicProperty:Nt,setAttribute:Pe,setAttributeNS:An,addEventListener:Nn,Aliases:an,getPropAlias:On,Properties:bn,ChildProperties:xn,DelegatedEvents:En,SVGElements:Ct,SVGNamespace:Cn});function Ie(e){let n=Math.floor(e/3600),t=Math.floor(e%3600/60),i=e%60;if(n>0)return`${n}h ${t}m`;return t>0?`${t}m ${i}s`:`${i}s`}function Re(e,n){if(!e)return 0;let t=new Date(e).getTime(),i=n?new Date(n).getTime():Date.now();return Math.round((i-t)/1000)}function jt(e){return e.toLowerCase().replace(/[^\\w\\s-]/g,\"\").trim().replace(/\\s+/g,\"-\").replace(/-+/g,\"-\")}var Zt=[\"code\",\"auditor\",\"unknown\"];var vn=[\"overview\",\"timeline\",\"sections\",\"findings\",\"plan\",\"usage\"],Fn=200,qt=300,Dt=100;function Le(e,n,t){if(t||e.length<=n)return{rows:e,total:e.length,capped:!1};return{rows:e.slice(0,n),total:e.length,capped:!0}}function _n(e,n){return e.length===n.length&&e.every((t,i)=>t===n[i])}var Mf=new Set([\"loops\",\"groups\",\"findings\",\"plans\"]),mf=new Set(vn);function ie(e){try{return decodeURIComponent(e)}catch{return e}}function pf(){return{projectId:null,section:\"loops\",loopName:null,tab:\"overview\",groupId:null,statuses:[],query:\"\"}}function Yn(e,n){if(!n)return;for(let t of n.split(\"&\")){if(!t)continue;let i=t.indexOf(\"=\");if(i===-1)continue;let f=ie(t.slice(0,i)),u=ie(t.slice(i+1));if(f===\"status\")e.statuses=u.split(\",\").map((r)=>r.trim()).filter((r)=>r!==\"\");else if(f===\"q\")e.query=u}}function Me(e){let n=pf(),t=(e||\"\").replace(/^#/,\"\");if(!t)return n;let i=t.indexOf(\"?\"),f=i===-1?t:t.slice(0,i),u=i===-1?\"\":t.slice(i+1),r=f.indexOf(\"/\"),c,l;if(r===-1)c=f,l=\"\";else c=f.slice(0,r),l=f.slice(r+1);if(c)n.projectId=ie(c);if(!l)return Yn(n,u),n;let s=l.split(\"/\").filter((x)=>x!==\"\");if(s.length===0)return Yn(n,u),n;let h=ie(s[0]);if(h===\"loop\"){if(s.length>=2){if(n.loopName=ie(s[1]),s.length>=3){let x=ie(s[2]);if(mf.has(x))n.tab=x}}}else if(Mf.has(h)){if(n.section=h,h===\"groups\"&&s.length>=2)n.groupId=ie(s[1])}else n.loopName=h;return Yn(n,u),n}function me(e){if(!e.projectId)return\"\";let n=\"#\"+encodeURIComponent(e.projectId);if(e.section===\"loops\"){if(e.loopName){if(n+=\"/loop/\"+encodeURIComponent(e.loopName),e.tab!==\"overview\")n+=\"/\"+e.tab}}else if(e.section===\"groups\"){if(n+=\"/groups\",e.groupId)n+=\"/\"+encodeURIComponent(e.groupId)}else n+=\"/\"+e.section;let t=[];if(e.statuses.length>0)t.push(\"status=\"+e.statuses.map((i)=>encodeURIComponent(i)).join(\",\"));if(e.query)t.push(\"q=\"+encodeURIComponent(e.query));if(t.length>0)n+=\"?\"+t.join(\"&\");return n}function Xt(e,n){let i=\"#\"+(location.hash||\"\").replace(/^#/,\"\"),f=\"#\"+e.replace(/^#/,\"\");if(i!==f)n.current=!0,location.hash=e}function j(e){if(!e||e===0)return\"\";let n=new Date(e),t=(l)=>l<10?\"0\"+l:String(l),i=t(n.getMonth()+1),f=t(n.getDate()),u=n.getFullYear(),r=n.getHours(),c=r>=12?\"PM\":\"AM\";if(r=r%12,r===0)r=12;return i+\"-\"+f+\"-\"+u+\" \"+r+\":\"+t(n.getMinutes())+\" \"+c}function fe(e){return\"status-badge status-\"+e}function Tn(e){return\"section-status section-\"+e}function Jt(e){return\"feature-stage feature-stage-\"+e}function Qt(e,n,t,i){if(!(n.size===0||n.has(e.status)))return!1;if(!t)return!0;let u=t.toLowerCase(),r=e.worktreeBranch??\"\";return((e.loopName||\"\")+\" \"+r+\" \"+(i||\"\")).toLowerCase().indexOf(u)!==-1}function Bt(e,n){if(n===\"recent\")return[...e];let t=[...e];if(n===\"cost\")t.sort((i,f)=>{let u=i.usage?.totalCost??null,r=f.usage?.totalCost??null;if(u===null&&r===null)return 0;if(u===null)return 1;if(r===null)return-1;return r-u});else if(n===\"duration\")t.sort((i,f)=>{let u=Re(i.loop.startedAt,i.loop.completedAt??void 0);return Re(f.loop.startedAt,f.loop.completedAt??void 0)-u});else if(n===\"findings\")t.sort((i,f)=>f.findings.length-i.findings.length);return t}function Rt(e){return JSON.stringify(e.projects)}function we(e){return e.projectDir||e.projectId||\"\"}function ge(e,n){let t=we(n);return e.get(t)??t}function ze(e){return e.completedAt||e.startedAt||0}function Lt(e){let n=new Map;for(let f of e){if(f===\"\"){n.set(f,[]);continue}let u=f.split(\"/\"),r=u.length;while(r>0&&u[r-1]===\"\")r--;n.set(f,r>0?u.slice(0,r):[f])}let t=new Map;for(let f of e){let u=n.get(f);t.set(f,u.length?u[u.length-1]:f)}let i=new Map;for(let f of e){let u=t.get(f),r=i.get(u);if(r)r.push(f);else i.set(u,[f])}for(let[,f]of i){let u=[...new Set(f)];if(u.length<2)continue;let r=1;while(r<1024){let c=new Set,l=!0,s=!0;for(let h of u){let x=n.get(h),E=Math.max(0,x.length-r),w=x.slice(E).join(\"/\");if(c.has(w))l=!1;else c.add(w);if(E>0)s=!1}if(l||s)break;r++}for(let c of u){let l=n.get(c),s=Math.max(0,l.length-r);t.set(c,l.slice(s).join(\"/\"))}}return t}function Hn(e){let n=[],t=[];for(let i of e)if(i.severity===\"bug\")n.push(i);else t.push(i);return{bugs:n,warnings:t}}function Mt(e){if(e.bugs.length>0)return\"bug\";if(e.warnings.length>0)return\"warn\";return\"clean\"}function be(e,n){return e+\" \"+n+(e===1?\"\":\"s\")}function Gn(e,n){if(!n||n<=0)return 0;return Math.max(0,Math.min(100,e/n*100))}function mt(e){let n=e.file+\":\"+e.line+\" — \"+e.description;if(e.scenario)n+=\" (\"+e.scenario+\")\";return n}function pt(e,n){return\" \"+e+\": $\"+n.cost.toFixed(6)+\", \"+n.inputTokens+\" in / \"+n.outputTokens+\" out (reasoning: \"+n.reasoningTokens+\", cache R: \"+n.cacheReadTokens+\" W: \"+n.cacheWriteTokens+\"), messages: \"+n.messageCount}function We(e){if(!Number.isFinite(e)||e<=0)return\"0\";if(e>=1e6)return(e/1e6).toFixed(1).replace(/\\.0$/,\"\")+\"M\";if(e>=1000)return(e/1000).toFixed(1).replace(/\\.0$/,\"\")+\"k\";return String(e)}function pe(e){if(!Number.isFinite(e)||e<=0)return\"$0\";return\"$\"+(e>=1?e.toFixed(2):e.toFixed(4))}function ei(e){let n=[{label:\"Input\",value:e.totalInputTokens,color:\"var(--seg-input)\"},{label:\"Output\",value:e.totalOutputTokens,color:\"var(--seg-output)\"},{label:\"Reasoning\",value:e.totalReasoningTokens,color:\"var(--seg-reasoning)\"},{label:\"Cache R\",value:e.totalCacheReadTokens,color:\"var(--seg-cache-read)\"},{label:\"Cache W\",value:e.totalCacheWriteTokens,color:\"var(--seg-cache-write)\"}],t=n.reduce((i,f)=>i+Math.max(0,f.value),0);return n.map((i)=>({...i,pct:t>0?Math.max(0,i.value)/t*100:0}))}var eu={code:\"execution\",auditor:\"auditor\",unknown:\"other\"};function ni(e){let t=Zt.map((f)=>{let u={role:eu[f],cost:0,messageCount:0,tokens:0,pct:0},r=e.byRole[f];if(!r)return u;return u.cost=r.cost,u.messageCount=r.messageCount,u.tokens=r.inputTokens+r.outputTokens+r.reasoningTokens,u}).filter((f)=>f.role!==\"other\"||f.cost>0||f.messageCount>0),i=t.reduce((f,u)=>Math.max(f,u.cost),0);for(let f of t)f.pct=i>0?f.cost/i*100:0;return t}function ti(e){let n=Object.keys(e.byModel).map((i)=>({model:i,...e.byModel[i]})),t=n.reduce((i,f)=>Math.max(i,f.cost),0);return n.sort((i,f)=>f.cost-i.cost).map((i)=>({model:i.model,cost:i.cost,inputTokens:i.inputTokens,outputTokens:i.outputTokens,messageCount:i.messageCount,pct:t>0?i.cost/t*100:0}))}function ii(e,n){if(!e)return\"\";let t=Re(e,n??void 0);return t>0?Ie(t):\"\"}function xe(e){if(!e||e===0)return\"\";let n=Date.now()-e,t=Math.floor(n/1000);if(t<60)return t+\"s ago\";let i=Math.floor(t/60);if(i<60)return i+\"m ago\";let f=Math.floor(i/60);if(f<24)return f+\"h ago\";let u=Math.floor(f/24);if(u<30)return u+\"d ago\";return j(e)}var nu=/([\\s\\S]*?)<\\/h\\1>/g;function fi(e){return e.replace(/<[^>]+>/g,\"\").replace(/&[a-z#0-9]+;/gi,\"\").trim()}function tu(e){return jt(fi(e)).replace(/[_-]+/g,\"-\").replace(/^-+|-+$/g,\"\")||\"heading\"}function iu(e){if(!//.test(e))return{html:e,outline:[]};let n=[];return{html:e.replace(nu,(i,f,u)=>{let r=Number(f),c=tu(u);return n.push({depth:r,id:c,text:fi(u)}),`${u}`}),outline:n}}function ui(e){if(!e)return{html:\"\",outline:[]};let n=Ke.get(e);if(n!==void 0)return n;let t=globalThis.marked;if(!t)return{html:\"\",outline:[]};let{html:i,outline:f}=iu(t.parse(e)),u={html:i,outline:f};if(Ke.size>=fu){let r=Ke.keys().next().value;if(r!==void 0)Ke.delete(r)}return Ke.set(e,u),u}function ri(e){return ui(e).html}function ci(e){return ui(e)}var Ke=new Map,fu=200;function en(e){let n=[\"overview\",\"timeline\"];if(e.sectionCount>0)n.push(\"sections\");if(n.push(\"findings\"),e.hasPlan)n.push(\"plan\");return n.push(\"usage\"),n}function li(e,n,t,i,f=\"coding\"){let u=[],r=e.findIndex((x)=>x.createdAt>=n),c=r===-1?[]:e.slice(r);if(t!==null)c=c.filter((x)=>x.createdAt<=t);let l;if(c.length>0&&c[0].eventType===\"restart\"&&c[0].toPhase!==null)l=c[0].toPhase;else if(c.length>0&&c[0].eventType!==\"restart\")l=c[0].fromPhase;else l=f;let s=n;for(let x of c){if(x.eventType===\"restart\"){if(x.toPhase!==null)l=x.toPhase;continue}if(l===\"\")l=x.fromPhase,s=Math.max(s,x.createdAt);let E=x.createdAt,w=Math.max(s,E);if(u.push({phase:l,startedAt:s,endedAt:w,durationMs:w-s,open:!1}),x.toPhase===null)l=\"\",s=w;else l=x.toPhase,s=w}if(l!==\"\"){let E=Math.max(s,t??i);u.push({phase:l,startedAt:s,endedAt:t,durationMs:E-s,open:t===null})}let h=e.length>0&&e[0].createdAt>=n&&e[0].createdAt>n+1000;if(h&&u.length>0)u[0]={...u[0],phase:\"\"};return{spans:u,truncated:h}}function si(e){let n={};for(let t of e){if(!t.phase)continue;n[t.phase]=(n[t.phase]??0)+t.durationMs}return n}function di(e,n,t){let i=[];for(let f=0;f=n)r=Math.max(0,u.createdAt-n);else if(u.createdAt=n)r=Math.max(0,u.createdAt-n);else r=null}i.push({transition:u,elapsedMs:r})}return i.reverse(),i}var uu=[{phase:\"coding\",label:\"coding\",cx:70,cy:60},{phase:\"auditing\",label:\"auditing\",cx:220,cy:60},{phase:\"final_auditing\",label:\"final_auditing\",cx:370,cy:60},{phase:\"post_action\",label:\"post_action\",cx:520,cy:60},{phase:\"final_audit_fix\",label:\"final_audit_fix\",cx:370,cy:160}],nn=110,tn=36,hi=655,oi=160,ru=[{key:\"coding→auditing\",d:\"M 125 60 L 165 60\",labelX:145,labelY:52},{key:\"auditing→final_auditing\",d:\"M 275 60 L 315 60\",labelX:295,labelY:52},{key:\"final_auditing→post_action\",d:\"M 425 60 L 465 60\",labelX:445,labelY:52},{key:\"auditing→coding\",d:\"M 220 42 Q 145 14 70 42\",labelX:145,labelY:18},{key:\"auditing→post_action\",d:\"M 220 42 Q 370 8 520 42\",labelX:370,labelY:16},{key:\"final_auditing→auditing\",d:\"M 315 42 Q 295 24 275 42\",labelX:295,labelY:30},{key:\"final_auditing→coding\",d:\"M 370 78 C 370 110 145 110 70 78\",labelX:220,labelY:102},{key:\"final_auditing→final_audit_fix\",d:\"M 370 78 L 370 142\",labelX:358,labelY:112},{key:\"final_audit_fix→final_auditing\",d:\"M 415 142 L 415 78\",labelX:442,labelY:116},{key:\"coding→final_audit_fix\",d:\"M 70 78 C 70 95 200 115 315 142\",labelX:175,labelY:118},{key:\"final_audit_fix→coding\",d:\"M 315 178 C 250 210 100 210 70 78\",labelX:175,labelY:205},{key:\"coding→terminal\",d:\"M 125 78 L 600 142\",labelX:240,labelY:90},{key:\"auditing→terminal\",d:\"M 275 78 L 600 142\",labelX:395,labelY:90},{key:\"final_auditing→terminal\",d:\"M 425 78 L 600 142\",labelX:505,labelY:90},{key:\"post_action→terminal\",d:\"M 520 78 L 600 142\",labelX:565,labelY:95},{key:\"final_audit_fix→terminal\",d:\"M 425 160 L 600 160\",labelX:512,labelY:152}];function cu(e){if(e.toPhase===null)return e.fromPhase+\"→terminal\";return e.fromPhase+\"→\"+e.toPhase}function lu(e){return e.toPhase??e.status??\"\"}function Un(e){let n=Array.isArray(e)?e[0]:e,t=n instanceof SVGSVGElement?n.firstElementChild:null;if(!(t instanceof SVGElement))throw Error(\"Expected an SVG child\");return t}function yi(e){let n=()=>e.loop(),t=g(()=>n().status===\"running\"),i=g(()=>{let c=new Map;for(let l of e.transitions()){let s=cu(l);c.set(s,(c.get(s)??0)+1)}return c}),f=ru.map((c)=>Un(o`\n \n \n ${()=>{let l=i().get(c.key);return l?String(l):\"\"}}\n \n `)),u=uu.map((c)=>{let l=c.cx-nn/2,s=c.cy-tn/2;return Un(o`\n \"mg-node\"+(n().phase===c.phase&&t()?\" mg-node-active\":\"\")}>\n \n ${c.label}\n \n `)}),r=Un(o`\n \n \n ${()=>n().status}\n \n `);return o`
\n \n \n \n \n \n \n\n ${f}\n ${u}\n ${()=>t()?\"\":r}\n \n\n
\n
Transition history
\n
\n ${()=>e.transitions().slice(-20).reverse().map((c)=>o`
\n ${c.eventType}\n ${c.fromPhase} → ${lu(c)}\n ${xe(c.createdAt)}\n
`)}\n
\n
\n
`}function bi(e){let[n,t]=K(e.query()),i=null,f=(r)=>{let c=r.target.value;if(t(c),i)clearTimeout(i);i=setTimeout(()=>{i=null,e.onChangeQuery(c)},250)};ce(()=>{if(i)clearTimeout(i),i=null}),R(()=>{e.externalNav();let r=H(()=>e.query());if(i!==null)clearTimeout(i),i=null;t(r)});let u=g(()=>{let r=e.counts();return[{label:\"Running\",key:\"running\",value:r.running},{label:\"Completed\",key:\"completed\",value:r.completed},{label:\"Cancelled\",key:\"cancelled\",value:r.cancelled},{label:\"Errored\",key:\"errored\",value:r.errored},{label:\"Stalled\",key:\"stalled\",value:r.stalled}]});return o`
\n ${()=>u().map(({label:r,key:c,value:l})=>{let h=`badge badge-filter${e.statuses().has(c)?\" badge-active\":\"\"}`;return o`e.onToggleStatus(c)}>${r}: ${l}`})}\n \n \n
`}function xi(e){return o`
Last updated: ${new Date(e.generatedAt).toLocaleString()}
`}function ai(e){let n=g(()=>e.entries().map((t)=>{let i=t.loops.some((r)=>r.loop.status===\"running\"),f=we(t.proj),u=ge(e.labels(),t.proj);return o`
e.onSelect(t.proj.projectId)}>\n ${i?o``:\"\"}\n ${u}\n ${t.loops.length}\n
`}));return o`
${()=>n()}
`}function su(e,n){let t=0,i=0,f=0,u=[],r=[];for(let c of e){let l=ge(n,c.proj);for(let s of c.loops){i++,f+=s.bugCount;let h=s.loop.status,x=ze(s.loop);if(h===\"running\")t++,u.push({projectId:c.proj.projectId,label:l,loopName:s.loop.loopName,phase:s.loop.phase||\"\"});r.push({projectId:c.proj.projectId,label:l,loopName:s.loop.loopName,status:h,when:x})}}return r.sort((c,l)=>l.when-c.when),{runningCount:t,loopCount:i,bugCount:f,runningCards:u,recentRows:r.slice(0,8)}}function Oi(e){let n=g(()=>su(e.entries(),e.labels())),t=g(()=>n().runningCount),i=g(()=>n().loopCount),f=g(()=>n().bugCount),u=g(()=>n().runningCards),r=g(()=>n().recentRows);return o`
\n
\n

All repositories

\n
${()=>`${t()} running · ${i()} loops · ${f()} open bugs`}
\n
\n ${()=>u().length>0?o`
\n

Running

\n
\n ${u().map((c)=>o`
e.onOpenLoop(c.projectId,c.loopName)}>\n ${c.label}\n ${c.loopName}\n ${c.phase}\n
`)}\n
\n
`:\"\"}\n ${()=>r().length>0?o`
\n

Recent activity

\n
\n ${r().map((c)=>o`
e.onOpenLoop(c.projectId,c.loopName)}>\n fe(c.status)}>${c.status}\n ${c.label}\n ${c.loopName}\n ${j(c.when)}\n
`)}\n
\n
`:\"\"}\n
`}function du(e){let[n,t]=K(!1),[i,f]=K(\"\"),[u,r]=K(0),c=g(()=>{let b=i().trim().toLowerCase(),C=e.loops();return b?C.filter((V)=>V.name.toLowerCase().indexOf(b)!==-1):C}),l=g(()=>Le(c(),Dt,!1)),s=()=>{if(n())return;f(\"\");let b=l().rows.findIndex((C)=>C.name===e.loopName);r(b<0?0:b),t(!0)},h=()=>{t(!1),f(\"\"),r(0)},x=(b)=>{if(h(),y.blur(),b!==e.loopName)e.onSelect(b)},E=o`n()?\"display:block\":\"display:none\"}\n onmousedown=${(b)=>b.preventDefault()}\n >\n ${()=>l().rows.map((b,C)=>o`\"loop-picker-option\"+(C===u()?\" loop-picker-option-active\":\"\")+(b.name===e.loopName?\" loop-picker-option-current\":\"\")}\n onclick=${()=>x(b.name)}\n >\n ${b.name}\n ${b.whenLabel}\n `)}\n ${()=>l().total===0?o`
No matching loops
`:\"\"}\n ${()=>l().capped?o`
${()=>\"Showing \"+l().rows.length+\" of \"+l().total+\" — type to filter\"}
`:\"\"}\n `,w=()=>{E.querySelector(\".loop-picker-option-active\")?.scrollIntoView?.({block:\"nearest\"})},d=(b)=>{if(b.key===\"ArrowDown\"||b.key===\"ArrowUp\"){if(b.preventDefault(),!n()){s();return}let C=l().rows.length;if(C===0)return;let V=b.key===\"ArrowDown\"?1:-1;r((A)=>(A+V+C)%C),w();return}if(b.key===\"Enter\"){b.preventDefault();let C=l().rows[u()];if(C)x(C.name);return}if(b.key===\"Escape\")b.preventDefault(),h(),y.blur()},y=o`n()?i():e.loopName}\n onfocus=${s}\n oninput=${(b)=>{f(b.target.value),r(0),t(!0)}}\n onkeydown=${d}\n onblur=${h}\n />`;return o`\n ${y}\n {if(b.preventDefault(),n())h(),y.blur();else y.focus()}}>▾\n ${E}\n `}function Ei(e){if(e.loopName)return o`
\n ← ${e.repoLabel}\n /\n ${du({loopName:e.loopName,loops:e.loops,onSelect:e.onSelectLoop})}\n ${()=>{let n=e.loopNav();return n?o`\n \n ${n.index+1} of ${n.total}\n \n `:\"\"}}\n
`;return o`
\n ← Repositories\n /\n ${e.repoLabel}\n ${e.projectDir}\n
`}function Ci(e){let n=[{key:\"loops\",label:\"Loops\",count:e.counts.loops},{key:\"groups\",label:\"Groups\",count:e.counts.groups},{key:\"findings\",label:\"Findings\",count:e.counts.findings},{key:\"plans\",label:\"Plans\",count:e.counts.plans}];return o`
\n ${n.map((t)=>{let f=`section-nav-item${t.key===e.section?\" section-nav-active\":\"\"}`;return o`
e.onNavigate(t.key)}>\n ${t.label}\n ${t.count}\n
`})}\n
`}function qn(e){return o`
\n ${()=>\"Showing \"+e.shown()+\" of \"+e.total()+\" \"+e.noun}\n ${e.onShowAll?o``:\"\"}\n
`}function Zn(e){let n=()=>Gn(e.current(),e.total());return o`\n \"width:\"+n()+\"%\"}>\n ${()=>e.current()}/${()=>e.total()}\n `}var hu={sm:4,md:14,lg:22};function Dn(e){let n=hu[e.variant],t=g(()=>e.spans().filter((u)=>u.phase!==\"\")),i=g(()=>Math.max(0,t().reduce((u,r)=>u+r.durationMs,0))),f=g(()=>e.spans().some((u)=>u.phase===\"\"));return o`
\"phase-bar phase-bar-\"+e.variant+(f()?\" phase-bar-trunc\":\"\")} data-truncated=${()=>f()?\"true\":\"false\"} style=${()=>\"height:\"+n+\"px\"}>\n ${()=>f()?o`\\u22ef`:\"\"}\n ${()=>{let u=t(),r=i();return u.map((c)=>{let l=r>0?c.durationMs/r*100:0,s=\"phase-seg\"+(c.open?\" phase-seg-open\":\"\");return o`
`})}}\n
`}function Xn(e,n){let t=e.loop;return li(e.transitions??[],t.startedAt,t.completedAt,t.completedAt===null?n():0,t.phase)}function Si(e){let[n,t]=K(!1),i=g(()=>Le(e.loops(),Fn,n()));return o`
\n \n \n \n \n \n \n ${()=>i().rows.map((f)=>ou({dashLoop:f,now:e.now,onOpen:e.onOpen}))}\n \n
StatusLoopPhasePhase spanIterSectionsFindingsCostDurationUpdated
\n ${()=>i().capped?qn({shown:()=>i().rows.length,total:()=>i().total,noun:\"loops\",onShowAll:()=>t(!0)}):\"\"}\n
`}function ou(e){let n=()=>e.dashLoop.loop,t=()=>e.dashLoop,i=g(()=>Hn(t().findings)),f=g(()=>Xn(t(),e.now).spans);return o`e.onOpen(e.dashLoop.loop.loopName)}>\n fe(n().status)}>${()=>n().status}\n ${()=>n().loopName}\n ${()=>n().phase}\n ${()=>Dn({spans:f,variant:\"sm\"})}\n ${Zn({current:()=>n().iteration,total:()=>n().maxIterations})}\n ${()=>n().totalSections>0?Zn({current:()=>n().currentSectionIndex,total:()=>n().totalSections}):o``}\n ${()=>{let u=i();if(u.bugs.length===0&&u.warnings.length===0)return o``;return o`\n ${u.bugs.length>0?o`${be(u.bugs.length,\"bug\")}`:\"\"}\n ${u.bugs.length>0&&u.warnings.length>0?\" · \":\"\"}\n ${u.warnings.length>0?o`${be(u.warnings.length,\"warn\")}`:\"\"}\n `}}\n ${()=>t().usage?pe(t().usage.totalCost):o``}\n ${()=>t().duration||\"\"}\n ${()=>j(ze(n()))}\n `}function Ye(e){let n=()=>typeof e.label===\"function\"?e.label():e.label,[t,i]=K(!1),f=()=>i((c)=>!c),u=g(()=>ci(e.src()||\"\")),r=g(()=>u().outline.filter((c)=>c.depth<=3));return o`
\n
\n t()?\"false\":\"true\"}\n onclick=${f}\n onkeydown=${(c)=>{if(c.key!==\"Enter\"&&c.key!==\" \")return;c.preventDefault(),f()}}\n >\n ${()=>t()?\"▸\":\"▾\"}\n

${()=>n()}

\n
\n \"Copy \"+n()+\" as markdown\"}\n onclick=${(c)=>{c.stopPropagation();let l=c.target,s=\"Copy\";navigator.clipboard.writeText(e.src()||\"\").then(()=>{l.textContent=\"Copied!\"},()=>{l.textContent=\"Failed\"}).then(()=>{setTimeout(()=>{l.textContent=s},2000)})}}\n >Copy\n
\n
t()?\"display:none\":\"display:block\"}>\n ${()=>{let c=r();return c.length>=2?yu({items:()=>c}):null}}\n
u().html}>
\n
\n `}function yu(e){let n=(t,i)=>{t.preventDefault();let f=t.currentTarget.closest(\".markdown-body\"),u=f?Array.from(f.querySelectorAll(\"h1,h2,h3,h4,h5,h6\")).find((r)=>r.id===i):document.getElementById(i);if(u&&typeof u.scrollIntoView===\"function\")u.scrollIntoView({behavior:\"smooth\",block:\"start\"})};return o``}function jn(e){return o`
\n
${e.label}
\n
ri(e.value()||\"\")}>
\n
`}function $u(e){let n=e.sec,t=g(()=>!!n.startedAt||!!n.completedAt),i=g(()=>!!n.summaryDone),f=g(()=>!!n.summaryDeviations),u=g(()=>!!n.summaryFollowUps),r=g(()=>!!n.content),c=g(()=>t()||i()||f()||u()||r());return o`
\n ${()=>t()?o`
\n ${()=>n.startedAt?\"Started \"+j(n.startedAt):\"Not started\"}\n ${()=>n.completedAt?o` → Completed ${()=>j(n.completedAt)}`:\"\"}\n
`:\"\"}\n ${()=>i()?jn({label:\"Done\",value:()=>n.summaryDone}):\"\"}\n ${()=>f()?jn({label:\"Deviations\",value:()=>n.summaryDeviations}):\"\"}\n ${()=>u()?jn({label:\"Follow-ups\",value:()=>n.summaryFollowUps}):\"\"}\n ${()=>r()?Ye({label:\"Section Plan\",src:()=>n.content}):\"\"}\n ${()=>c()?\"\":o`
No details captured for this section yet.
`}\n
`}function wu(e){let n=e.sec,t=g(()=>ii(n.startedAt,n.completedAt));return o`
\"section-list-row section-item-\"+n.status} onclick=${e.onOpen}>\n #${n.sectionIndex}\n ${()=>n.title}\n Tn(n.status)}>${()=>n.status}\n ${()=>t()?o`${()=>t()}`:\"\"}\n ${()=>n.attempts>0?o`${()=>n.attempts} attempts`:\"\"}\n \n
`}function gu(e){let[n,t]=K(null),i=g(()=>{let f=n();if(f===null)return null;return e.sections().find((u)=>u.sectionIndex===f)??null});return o`
\n

Sections

\n ${()=>{let f=i();if(f)return o`
\n
t(null)}>← Back to sections
\n
\n #${f.sectionIndex}\n ${()=>f.title}\n Tn(f.status)}>${()=>f.status}\n
\n ${$u({sec:f})}\n
`;return o`
\n ${e.sections().map((u)=>wu({sec:u,onOpen:()=>t(u.sectionIndex)}))}\n
`}}\n
`}function Z(e){return o`
\n ${e.label}\n ${()=>e.value()}\n
`}function $i(e){let n=()=>Gn(e.current(),e.total());return o`
\n
\n ${e.label}\n ${()=>e.current()} / ${()=>e.total()}\n
\n
\n
\"width:\"+n()+\"%\"}>
\n
\n
`}function bu(e){let n=()=>e.dashLoop.loop,t=()=>e.dashLoop,i=g(()=>!!n().completedAt),f=g(()=>!!t().duration),u=g(()=>n().totalSections>0),r=g(()=>!!n().terminationReason),c=g(()=>Mt(e.split())),l=g(()=>!!e.dashLoop.usage);return o`
\n
\"ldh-findings ldh-findings-\"+c()}>\n ${()=>{let s=e.split();if(s.bugs.length===0&&s.warnings.length===0)return\"No findings\";let h=[];if(s.bugs.length>0)h.push(be(s.bugs.length,\"bug\"));if(s.warnings.length>0)h.push(be(s.warnings.length,\"warning\"));return h.join(\" · \")}}\n
\n
\n fe(n().status)}>${()=>n().status}\n

${()=>n().loopName}

\n ${()=>n().phase}\n
\n\n
\n ${Z({label:\"Started\",value:()=>j(n().startedAt)})}\n ${()=>i()?Z({label:\"Completed\",value:()=>j(n().completedAt)}):\"\"}\n ${()=>f()?Z({label:\"Duration\",value:()=>t().duration||\"\"}):\"\"}\n ${Z({label:\"Iteration\",value:()=>n().iteration+\" / \"+n().maxIterations})}\n ${()=>u()?Z({label:\"Section\",value:()=>n().currentSectionIndex+\" / \"+n().totalSections}):\"\"}\n ${()=>l()?Z({label:\"Messages\",value:()=>String(e.dashLoop.usage.totalMessageCount)}):\"\"}\n ${Z({label:\"Execution model\",value:()=>n().executionModel??\"—\"})}\n ${Z({label:\"Auditor model\",value:()=>n().auditorModel??\"—\"})}\n ${Z({label:\"Branch\",value:()=>n().worktreeBranch??\"—\"})}\n ${Z({label:\"Sandbox\",value:()=>n().sandbox?\"on\":\"off\"})}\n ${Z({label:\"Kind\",value:()=>n().kind??\"—\"})}\n ${Z({label:\"Audits\",value:()=>String(n().auditCount)})}\n ${Z({label:\"Errors\",value:()=>String(n().errorCount)})}\n
\n\n
\n ${$i({label:\"Iterations\",current:()=>n().iteration,total:()=>n().maxIterations})}\n ${()=>u()?$i({label:\"Sections\",current:()=>n().currentSectionIndex,total:()=>n().totalSections}):\"\"}\n
\n\n ${()=>r()?o`
\"ldh-banner ldh-banner-\"+n().status}>${()=>n().terminationReason}
`:\"\"}\n
`}function xu(e){let n=()=>e.usage(),t=g(()=>ei(n())),i=g(()=>ti(n())),f=g(()=>i().length>0),u=g(()=>ni(n()));return o`
\n

Usage

\n\n
\n
Role split
\n
\n ${()=>u().map((r)=>o`
r.role}>\n
\n r.role}>${r.role}\n ${()=>pe(r.cost)}\n
\n
\n
\"width:\"+r.pct+\"%\"}>
\n
\n
${()=>r.messageCount+\" msg · \"+We(r.tokens)+\" tok\"}
\n
`)}\n
\n
\n\n
\n
Token composition
\n
\n ${()=>t().map((r)=>r.pct>0?o`
`:\"\")}\n
\n
\n ${()=>t().map((r)=>o`
\n \n ${r.label}\n ${We(r.value)}\n
`)}\n
\n
\n\n ${()=>f()?o`
\n
Cost by model
\n
\n ${()=>i().map((r)=>o`
\n
\n ${r.model}\n ${pe(r.cost)}\n
\n
\n
\n
\n
\n ${We(r.inputTokens)} in / ${We(r.outputTokens)} out · ${r.messageCount} msg\n
\n
`)}\n
\n
`:\"\"}\n `}function wi(e){try{return JSON.parse(e)}catch{return[]}}function gi(e){return\"#\"+e.index+\" \"+e.title}function au(e){let n=e.amendment,t=g(()=>wi(n.sectionsBefore)),i=g(()=>wi(n.sectionsAfter));return o`
\n
\n ${()=>xe(n.createdAt)}\n applied @ section ${()=>n.appliedAtSection}\n ${()=>n.source}\n ${()=>n.rationale}\n ${()=>e.expanded()?\"▾\":\"▸\"}\n
\n
e.expanded()?\"display:block\":\"display:none\"}>\n
\n ${()=>t().length>0?o`
\n
Before
\n ${()=>t().map((f)=>o`
${()=>gi(f)}
`)}\n
`:\"\"}\n ${()=>i().length>0?o`
\n
After
\n ${()=>i().map((f)=>o`
${()=>gi(f)}
`)}\n
`:\"\"}\n
\n
\n
`}function Ou(e){let n=()=>e.amendments(),[t,i]=K(null);return o`
\n

Plan amendments

\n
\n ${()=>n().map((f)=>au({amendment:f,expanded:()=>t()===f.id,onToggle:()=>i(t()===f.id?null:f.id)}))}\n
\n
`}var Eu={overview:\"Overview\",timeline:\"Timeline\",sections:\"Sections\",findings:\"Findings\",plan:\"Plan\",usage:\"Usage\"};function Cu(e){return o`
\n ${()=>e.tabs().map((n)=>{let t=Eu[n];return o`\"tab-item\"+(e.activeTab()===n?\" tab-active\":\"\")} data-tab=\"${n}\" onclick=${()=>e.onSelect(n)}>${t}`})}\n
`}function Su(e){let n=()=>e.dashLoop,t=()=>n().loop,i=g(()=>!!n().goal),f=g(()=>n().postActionReport??t().completionSummary??null),u=g(()=>n().postActionReport?\"Post-Action Report\":\"Completion Summary\"),r=g(()=>!!f()),c=g(()=>!!n().lastAuditResult),l=g(()=>Xn(n(),e.now));return o`
\n ${()=>Dn({spans:()=>l().spans,variant:\"md\"})}\n ${()=>i()?Ye({label:\"Goal\",src:()=>n().goal}):\"\"}\n ${bu({dashLoop:e.dashLoop,split:e.split})}\n ${()=>r()?Ye({label:u,src:()=>f()}):\"\"}\n ${()=>c()?Ye({label:\"Last Audit Result\",src:()=>n().lastAuditResult}):\"\"}\n
`}function Au(e){let n=e.ev.transition,t=e.ev.elapsedMs,i=t===null?null:Math.floor(t/1000),f=n.toPhase===null?n.status??\"\":n.toPhase;return o`
\n ${xe(n.createdAt)}\n ${n.fromPhase} → ${f}\n ${n.transitionKind}\n iter ${n.iteration}\n ${()=>n.sectionIndex===null?\"—\":\"sect \"+(n.sectionIndex+1)}\n ${()=>i===null?\"—\":i>0?Ie(i):\"0s\"}\n
`}function Nu(e){let n=()=>e.dashLoop,t=()=>n().loop,i=()=>n().transitions??[],f=g(()=>Xn(n(),e.now)),u=g(()=>f().spans),r=g(()=>f().truncated),c=g(()=>si(u())),l=g(()=>u().filter((y)=>y.phase!==\"\").reduce((y,b)=>y+b.durationMs,0)),s=g(()=>{let y=c();return Object.keys(y).filter((b)=>b!==\"\").sort((b,C)=>y[C]-y[b])}),h=g(()=>di(i(),t().startedAt,r())),[x,E]=K(!1),w=g(()=>{let y=h();return x()?y:y.slice(0,20)}),d=g(()=>x()?0:Math.max(0,h().length-20));return o`
\n ${yi({loop:()=>t(),transitions:i})}\n ${()=>Dn({spans:u,variant:\"lg\"})}\n ${()=>r()?o`
Earlier history was truncated by the 100-row fetch window.
`:\"\"}\n\n
\n ${()=>s().map((y)=>{let b=c()[y],C=l()>0?Math.round(b/l()*100):0;return o`
\n \n ${y}\n \n ${()=>b>=1000?Ie(Math.floor(b/1000)):b+\"ms\"} · ${C}%\n
`})}\n
\n\n
\n ${()=>w().map((y)=>Au({ev:y}))}\n ${()=>d()>0?o`
E(!0)}>[ ${()=>d()} earlier events ]
`:\"\"}\n
\n
`}function Pu(e){return gu({sections:()=>e.dashLoop.sections})}function Vu(e){let n=[],t=new Map;for(let i of e){let f=i.sectionIndex??null;if(!t.has(f))t.set(f,[]),n.push(f);t.get(f).push(i)}return n.sort((i,f)=>{if(i===null&&f===null)return 0;if(i===null)return 1;if(f===null)return-1;return i-f}),n.map((i)=>({key:i,label:i===null?\"cross-section\":\"sect \"+(i+1),rows:t.get(i)}))}function Ai(e){return o`
\n ${e.rows.map((n)=>o`
\"finding finding-\"+n.severity}>\n ${()=>xe(n.createdAt)}\n ${()=>mt(n)}\n
`)}\n
`}function Ni(e){let n=()=>me({projectId:e.projectId(),section:\"loops\",loopName:e.loopName,tab:\"overview\",groupId:null,statuses:[],query:\"\"});return o`{if(t.defaultPrevented||t.button!==0||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;t.preventDefault(),e.onOpen(e.loopName)}}>${e.loopName}`}function ku(e){let n=()=>e.dashLoop,t=g(()=>n().findings.length>0),i=g(()=>Vu(n().findings));return o`
\n ${()=>t()?i().map((f)=>o`
f.key===null?\"cross\":String(f.key)}>\n
${f.label}
\n ${Ai({rows:f.rows})}\n
`):o`
No findings recorded for this loop.
`}\n
`}function Iu(e){let n=()=>e.dashLoop,t=g(()=>n().hasPlan),i=g(()=>!!n().amendments&&n().amendments.length>0);return o`
\n ${()=>t()?Ye({label:\"Plan\",src:()=>n().plan}):o`
No plan recorded for this loop.
`}\n ${()=>i()?Ou({amendments:()=>n().amendments}):\"\"}\n
`}function Ku(e){let n=()=>e.dashLoop,t=g(()=>!!n().usage);return o`
\n ${()=>t()?xu({usage:()=>n().usage}):o`
No usage recorded for this loop.
`}\n
`}function zu(e){switch(e.tab){case\"overview\":return Su({dashLoop:e.dashLoop,split:e.split,now:e.now});case\"timeline\":return Nu({dashLoop:e.dashLoop,now:e.now});case\"sections\":return Pu({dashLoop:e.dashLoop});case\"findings\":return ku({dashLoop:e.dashLoop});case\"plan\":return Iu({dashLoop:e.dashLoop});case\"usage\":return Ku({dashLoop:e.dashLoop})}}function Wu(e){let n=g(()=>e.tabs().includes(e.tab)&&e.activeTab()===e.tab),t=De(),i=o`
n()?\"display:block\":\"display:none\"}>
`,f=!1;return R(()=>{if(!n()||f)return;f=!0;let u=on(t,()=>zu(e));if(u)i.appendChild(u)}),i}function Yu(e){return o`
\n ${vn.map((n)=>Wu({tab:n,dashLoop:e.dashLoop,split:e.split,now:e.now,tabs:e.tabs,activeTab:e.activeTab}))}\n
`}function Pi(e){let n=()=>e.dashLoop,t=g(()=>Hn(n().findings)),i=g(()=>en(n())),f=g(()=>{let u=e.routeTab();return i().includes(u)?u:\"overview\"});return o`
\n
\n ${Cu({tabs:i,activeTab:f,onSelect:e.onSelectTab})}\n ${Yu({dashLoop:e.dashLoop,split:t,now:e.now,tabs:i,activeTab:f})}\n
\n
`}function Jn(){return o`
No loops match the current filters.
`}var vu=new Set([\"completed\",\"cancelled\",\"errored\"]);function Vi(e){let n=e.length;return{completed:e.filter((i)=>i.stage===\"completed\").length,total:n}}function ki(e,n){let t=vu.has(n);return e+(t?` ${e}-terminal`:` ${e}-active`)}function Ii(e){return o`
\n

Groups

\n ${()=>{let n=e.groups();if(n.length===0)return o`
No feature groups for this repo.
`;return o`
\n ${n.map((t)=>{let i=t.group,f=g(()=>Vi(t.features));return o`
ki(\"group-row\",i.status)} data-group-status=${()=>i.status} onclick=${()=>e.onOpen(i.groupId)}>\n fe(i.status)}>${()=>i.status}\n ${()=>i.title}\n ${Zn({current:()=>f().completed,total:()=>f().total})}\n max ${()=>i.maxConcurrent}\n \n Created ${()=>j(i.createdAt)}\n ${()=>i.completedAt?o`Completed ${()=>j(i.completedAt)}`:\"\"}\n \n
`})}\n
`}}\n
`}function Ki(e){return o`
\n
← Back to groups
\n ${()=>{let n=e.group();if(!n)return o`
Group not found.
`;let t=n.group,i=g(()=>Vi(n.features));return o`
\n
ki(\"group-header\",t.status)} data-group-status=${()=>t.status}>\n
\n fe(t.status)}>${()=>t.status}\n

${()=>t.title}

\n max ${()=>t.maxConcurrent}\n
\n
\n ${()=>\"Features \"+i().completed+\"/\"+i().total}\n Created ${()=>j(t.createdAt)}\n ${()=>t.completedAt?o`Completed ${()=>j(t.completedAt)}`:\"\"}\n ${()=>t.error?o`${()=>t.error}`:\"\"}\n
\n ${()=>t.prdPreview?o`
\n

PRD preview

\n
${()=>t.prdPreview||\"\"}
\n
`:\"\"}\n
\n
\n ${n.features.map((f)=>o`
f.stage}>\n #${()=>f.featureIndex}\n ${()=>f.title}\n Jt(f.stage)}>${()=>f.stage}\n ${()=>f.attempts} attempts\n ${()=>!!f.loopName&&e.loopNames().has(f.loopName)?Ni({className:\"feature-loop-link\",projectId:e.projectId,loopName:f.loopName,onOpen:e.onOpenLoop}):\"\"}\n ${()=>f.error?o`${()=>f.error}`:\"\"}\n
`)}\n
\n
`}}\n
`}function Fu(e,n,t){let i=[],f=0,u=t?1/0:n;for(let r of e){if(r.findings.length===0)continue;if(f+=r.findings.length,u<=0)continue;let c=Math.min(u,r.findings.length);i.push({loopName:r.loop.loopName,rows:r.findings.slice(0,c)}),u-=c}return{groups:i,total:f,capped:!t&&f>n}}function zi(e){let[n,t]=K(!1),i=g(()=>Fu(e.loops(),qt,n()));return o`
\n

Findings ${()=>i().total}

\n ${()=>i().capped?qn({shown:()=>i().groups.reduce((f,u)=>f+u.rows.length,0),total:()=>i().total,noun:\"findings\",onShowAll:()=>t(!0)}):\"\"}\n ${()=>{let f=i().groups;if(f.length===0)return o`
No findings recorded for this repo.
`;return o`
\n ${f.map((u)=>o`
u.loopName}>\n
\n ${Ni({className:\"findings-loop-link\",projectId:e.projectId,loopName:u.loopName,onOpen:e.onOpenLoop})}\n ${()=>u.rows.length}\n
\n ${Ai({rows:u.rows})}\n
`)}\n
`}}\n
`}function Wi(e){let[n,t]=K(!1),i=g(()=>e.loops().filter((u)=>u.hasPlan)),f=g(()=>Le(i(),Fn,n()));return o`
\n

Plans ${()=>f().total}

\n ${()=>f().capped?qn({shown:()=>f().rows.length,total:()=>f().total,noun:\"plans\",onShowAll:()=>t(!0)}):\"\"}\n ${()=>{let u=f().rows;if(u.length===0)return o`
No plans recorded for this repo.
`;return o`
\n ${u.map((r)=>o`
e.onOpenLoop(r.loop.loopName)}>\n fe(r.loop.status)}>${()=>r.loop.status}\n ${()=>r.loop.loopName}\n \n ${()=>be(r.findings.length,\"finding\")}\n \n iter ${()=>r.loop.iteration}/${()=>r.loop.maxIterations}\n
`)}\n
`}}\n
`}function Yi(){let[e,n]=zt({generatedAt:0,projects:[]}),[t,i]=K(!1),f=Me(typeof location<\"u\"?location.hash:\"\"),[u,r]=K(f.projectId),[c,l]=K(f.section),[s,h]=K(f.loopName),[x,E]=K(f.tab),[w,d]=K(f.groupId),[y,b]=K(f.statuses,{equals:_n}),[C,V]=K(f.query),A=g(()=>({projectId:u(),section:c(),loopName:s(),tab:x(),groupId:w(),statuses:y(),query:C()})),[S,P]=K(null),[I,z]=K(\"recent\"),[G,F]=K(0),[m,p]=K(Date.now()),ve={current:\"\"},ae={current:!1},Fi=()=>{let $=H(A),a=new URLSearchParams;if($.projectId)a.set(\"project\",$.projectId);if($.loopName)a.set(\"loop\",$.loopName);let N=a.toString();return N?\"/api/data?\"+N:\"/api/data\"},Fe=0,ue={url:null,gen:0},fn=async()=>{let $=Fi();if(ue.url===$)return;let a=++Fe;ue.url=$,ue.gen=a;try{let N=await fetch($,{cache:\"no-store\"});if(a!==Fe)return;let Y=await N.json();if(a!==Fe)return;let q=Rt(Y);if(q!==ve.current)ve.current=q,n(Wt(Y,{key:\"id\"})),i(!0),P(null);p(Date.now())}catch(N){if(a!==Fe)return;let Y=N instanceof Error?N.message:String(N);P(\"Failed to load dashboard data: \"+Y)}finally{if(ue.gen===a)ue.url=null,ue.gen=0}},Qn=($)=>{Ne(()=>{r($.projectId),l($.section),h($.loopName),E($.tab),d($.groupId),b($.statuses),V($.query)})},U=($)=>{Ne(()=>{let a=$.projectId!==void 0&&$.projectId!==u();if($.projectId!==void 0)r($.projectId);if($.section!==void 0)l($.section);if($.loopName!==void 0)h($.loopName);if($.tab!==void 0)E($.tab);if($.groupId!==void 0)d($.groupId);if($.statuses!==void 0)b($.statuses);if($.query!==void 0)V($.query);if(a)b([]),V(\"\")})},_i=($)=>{U({tab:$})},_e=($)=>{U({loopName:$,section:\"loops\",groupId:null,statuses:[],query:\"\",tab:\"overview\"})},Ti=($)=>{let a=new Set(y());if(a.has($))a.delete($);else a.add($);U({statuses:[...a]})},Hi=($)=>{U({query:$})},Gi=($)=>{z($)},Bn=g(()=>new Set(y())),Te=g(()=>Lt(e.projects.map(we))),He=g(()=>{if(!t()||e.projects.length===0)return[];let $=Bn(),a=C(),N=Te(),Y=[],q=a.trim().toLowerCase();for(let Ee of e.projects){let cn=[],pn=ge(N,Ee);for(let et of Ee.loops)if(Qt(et.loop,$,a,pn))cn.push(et);let rf=(Ee.groups?.length??0)>0;if(cn.length>0)Y.push({proj:Ee,loops:cn});else if(rf&&(!q||pn.toLowerCase().indexOf(q)!==-1))Y.push({proj:Ee,loops:[]})}return Y}),Rn=g(()=>u()===null),Ui=g(()=>c()===\"groups\"),Ln=g(()=>c()===\"loops\"),ji=g(()=>c()===\"findings\"),Zi=g(()=>c()===\"plans\"),Mn=g(()=>{let $=He(),a=u();if(!a)return null;return $.find((N)=>N.proj.projectId===a)??null}),L=g(()=>{let $=u();if(!$)return null;return e.projects.find((a)=>a.projectId===$)??null}),un=g(()=>{let $=u(),a=s();if(!$||!a)return null;let N=L();if(!N)return null;return N.loops.find((Y)=>Y.loop.loopName===a)??null}),qi=g(()=>{let $=L(),a=$?.groups?.length??0;if(!$)return{loops:0,groups:a,findings:0,plans:0};let N=0,Y=0;for(let q of $.loops)if(N+=q.findings.length,q.hasPlan)Y++;return{loops:$.loops.length,groups:a,findings:N,plans:Y}}),Oe=g(()=>{let $=L();if(!$)return[];return $.loops.map((a)=>{let N=ze(a.loop);return{name:a.loop.loopName,when:N,whenLabel:j(N)}}).sort((a,N)=>N.when-a.when||a.name.localeCompare(N.name))}),rn=g(()=>{let $=s();if(!$)return-1;return Oe().findIndex((a)=>a.name===$)}),Di=g(()=>{let $=rn();if($<0)return null;let a=Oe();if(a.length<=1)return null;return{index:$,total:a.length,onPrev:()=>{let N=rn(),Y=Oe();if(N>0)U({loopName:Y[N-1].name})},onNext:()=>{let N=rn(),Y=Oe();if(N$===a||!!$&&!!a&&$.index===a.index&&$.total===a.total}),Xi=g(()=>{let $=L(),a={running:0,completed:0,cancelled:0,errored:0,stalled:0};if(!$)return a;for(let N of $.loops){let Y=N.loop.status;if(Y===\"running\")a.running++;else if(Y===\"completed\")a.completed++;else if(Y===\"cancelled\")a.cancelled++;else if(Y===\"errored\")a.errored++;else if(Y===\"stalled\")a.stalled++}return a}),Ji=g(()=>{let $=s();if(!u())return\"\";let a=L();if(!a)return\"\";let N=we(a),Y=ge(Te(),a);return H(()=>Ei({repoLabel:Y,projectDir:N,loopName:$,loops:Oe,loopNav:Di,onSelectLoop:(q)=>U({loopName:q}),onBackToRepos:()=>U({projectId:null,loopName:null,section:\"loops\"}),onBackToRepo:()=>U({loopName:null})}))}),Qi=g(()=>{let $=u(),a=s();if(!$||a)return\"\";return Ci({section:c(),counts:qi(),onNavigate:(N)=>U({section:N,loopName:null,groupId:null})})}),Bi=g(()=>{return u()!==null&&s()===null&&c()===\"loops\"}),Ri=g(()=>{if(!Bi())return\"\";return H(()=>bi({counts:Xi,statuses:Bn,query:C,sortMode:I,onToggleStatus:Ti,onChangeQuery:Hi,onChangeSort:Gi,externalNav:G}))}),Li=g(()=>{if(!Ln())return\"\";let $=un();return $?Pi({dashLoop:$,now:m,routeTab:x,onSelectTab:_i}):\"\"}),Mi=g(()=>{let $=Mn();return $?Bt($.loops,I()):[]},[],{equals:_n}),mi=g(()=>{if(!Ln())return\"\";if(un())return\"\";if(!u())return\"\";return o`
\n ${Si({loops:Mi,now:m,onOpen:($)=>_e($)})}\n ${()=>Mn()?\"\":Jn()}\n
`}),pi=g(()=>{if(c()!==\"groups\"||!w())return null;let $=L();if(!$)return null;return $.groups.find((a)=>a.group.groupId===w())??null}),ef=g(()=>{let $=L();if(!$)return new Set;return new Set($.loops.map((a)=>a.loop.loopName))}),nf=g(()=>{if(!Ui())return\"\";if(!u())return\"\";if(w())return Ki({group:pi,loopNames:ef,projectId:()=>u(),onBack:()=>U({groupId:null}),onOpenLoop:_e});return Ii({groups:()=>L()?.groups??[],onOpen:(a)=>U({groupId:a})})}),mn=g(()=>{let $=L();return $?$.loops:[]}),tf=g(()=>{if(!ji())return\"\";if(!u())return\"\";return zi({loops:mn,projectId:()=>u(),onOpenLoop:_e})}),ff=g(()=>{if(!Zi())return\"\";if(!u())return\"\";return Wi({loops:mn,onOpenLoop:_e})});R(()=>{if(!t())return;let $=u();if($===null)return;if(!e.projects.some((N)=>N.projectId===$))r(null),h(null),l(\"loops\"),b([]),V(\"\")}),R(()=>{let $=u(),a=s();if(!$||!a)return;let N=L();if(!N)return;if(!N.loops.some((q)=>q.loop.loopName===a))h(null)}),R(()=>{let $=s(),a=x();if(!u()||!$||a===\"overview\")return;let N=un();if(!N)return;if(!en(N).includes(a))E(\"overview\")}),R(()=>{if(!t())return;Xt(me(A()),ae)});let uf=g(()=>(u()??\"\")+\"\\x00\"+(s()??\"\"));return R(dt(uf,()=>{fn()},{defer:!0})),ht(()=>{Qn(Me(location.hash)),fn();let $=setInterval(()=>{fn()},5000),a=()=>{if(ae.current){ae.current=!1;return}Qn(Me(location.hash)),F((N)=>N+1)};window.addEventListener(\"hashchange\",a),ce(()=>{clearInterval($),window.removeEventListener(\"hashchange\",a)})}),o`
\n

Forge Dashboard

\n\n ${()=>{let $=S();return $?o`
${$}
`:\"\"}}\n\n ${()=>{if(!t())return\"\";return o`
\n ${xi({generatedAt:e.generatedAt})}\n
`}}\n\n ${()=>{if(!t())return\"\";if(Rn()&&He().length===0)return Jn();return o`
\n ${()=>{if(Rn())return o`
\n ${ai({entries:()=>He(),labels:()=>Te(),onSelect:($)=>U({projectId:$,loopName:null,section:\"loops\",groupId:null})})}\n ${Oi({entries:()=>He(),labels:()=>Te(),onOpenLoop:($,a)=>U({projectId:$,loopName:a,section:\"loops\",groupId:null,statuses:[],query:\"\",tab:\"overview\"})})}\n
`;return o`
\n ${Ji}\n ${Ri}\n ${Qi}\n ${mi}\n ${Li}\n ${nf}\n ${tf}\n ${ff}\n
`}}\n
`}}\n
`}var vi=document.getElementById(\"forge-app-root\");if(vi)St(Yi,vi);\n"; -export const DASHBOARD_APP_SOURCE_HASH: string = "e5bd222ee43b0b17441e696a223b68bae3e1571cae17c38ddfb81ff718292863"; +export const DASHBOARD_APP_BUNDLE: string = "var v={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return nt(this.context.count)},getNextContextId(){return nt(this.context.count++)}};function nt(e){let n=String(e),t=n.length-1;return v.context.id+(t?String.fromCharCode(96+t):\"\")+n}function sn(e){v.context=e}function cf(){return{...v.context,id:v.getNextContextId(),count:0}}var lf=!0,sf=(e,n)=>e===n,J=Symbol(\"solid-proxy\"),df=typeof Proxy===\"function\",hn=Symbol(\"solid-track\"),ct=Symbol(\"solid-dev-component\"),Ue={equals:sf},tt=null,lt=$t,D=1,Ce=2,hf={};var k=null,O=null,Ae=null,re=null,W=null,_=null,T=null,Ze=0,Q={afterUpdate:null,afterCreateOwner:null,afterCreateSignal:null,afterRegisterGraph:null};function st(e,n){let t=W,i=k,f=e.length===0,u=n===void 0?i:n,r=f?{owned:null,cleanups:null,context:null,owner:null}:{owned:null,cleanups:null,context:u?u.context:null,owner:u},c=f?()=>e(()=>{throw Error(\"Dispose method must be an explicit argument to createRoot function\")}):()=>e(()=>H(()=>ne(r)));Q.afterCreateOwner&&Q.afterCreateOwner(r),k=r,W=null;try{return B(c,!0)}finally{W=t,k=i}}function K(e,n){n=n?Object.assign({},Ue,n):Ue;let t={value:e,observers:null,observerSlots:null,comparator:n.equals||void 0};{if(n.name)t.name=n.name;if(n.internal)t.internal=!0;else if(ot(t),Q.afterCreateSignal)Q.afterCreateSignal(t)}let i=(f)=>{if(typeof f===\"function\")if(O&&O.running&&O.sources.has(t))f=f(t.tValue);else f=f(t.value);return yn(t,f)};return[yt.bind(t),i]}function M(e,n,t){let i=Xe(e,n,!1,D,t);if(Ae&&O&&O.running)_.push(i);else le(i)}function R(e,n,t){lt=gf;let i=Xe(e,n,!1,D,t),f=dn&&yf(dn);if(f)i.suspense=f;if(!t||!t.render)i.user=!0;T?T.push(i):le(i)}function g(e,n,t){t=t?Object.assign({},Ue,t):Ue;let i=Xe(e,n,!0,0,t);if(i.observers=null,i.observerSlots=null,i.comparator=t.equals||void 0,Ae&&O&&O.running)i.tState=D,_.push(i);else le(i);return yt.bind(i)}function Ne(e){return B(e,!1)}function H(e){if(!re&&W===null)return e();let n=W;W=null;try{if(re)return re.untrack(e);return e()}finally{W=n}}function dt(e,n,t){let i=Array.isArray(e),f,u=t&&t.defer;return(r)=>{let c;if(i){c=Array(e.length);for(let s=0;sn(c,f,r));return f=c,l}}function ht(e){R(()=>H(e))}function ce(e){if(k===null)console.warn(\"cleanups created outside a `createRoot` or `render` will never be run\");else if(k.cleanups===null)k.cleanups=[e];else k.cleanups.push(e);return e}function qe(){return W}function De(){return k}function on(e,n){let t=k,i=W;k=e,W=null;try{return B(n,!0)}catch(f){Je(f)}finally{k=t,W=i}}function of(e){if(O&&O.running)return e(),O.done;let n=W,t=k;return Promise.resolve().then(()=>{W=n,k=t;let i;if(Ae||dn)i=O||(O={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),i.done||(i.done=new Promise((f)=>i.resolve=f)),i.running=!0;return B(e,!1),W=k=null,i?i.done:void 0})}var[_u,it]=K(!1);function ft(e,n){let t=Xe(()=>H(()=>{return Object.assign(e,{[ct]:!0}),e(n)}),void 0,!0,0);return t.props=n,t.observers=null,t.observerSlots=null,t.name=e.name,t.component=e,le(t),t.tValue!==void 0?t.tValue:t.value}function ot(e){if(k){if(k.sourceMap)k.sourceMap.push(e);else k.sourceMap=[e];e.graph=k}if(Q.afterRegisterGraph)Q.afterRegisterGraph(e)}function yf(e){let n;return k&&k.context&&(n=k.context[e.id])!==void 0?n:e.defaultValue}var dn;function yt(){let e=O&&O.running;if(this.sources&&(e?this.tState:this.state))if((e?this.tState:this.state)===D)le(this);else{let n=_;_=null,B(()=>je(this),!1),_=n}if(W){let n=this.observers?this.observers.length:0;if(!W.sources)W.sources=[this],W.sourceSlots=[n];else W.sources.push(this),W.sourceSlots.push(n);if(!this.observers)this.observers=[W],this.observerSlots=[W.sources.length-1];else this.observers.push(W),this.observerSlots.push(W.sources.length-1)}if(e&&O.sources.has(this))return this.tValue;return this.value}function yn(e,n,t){let i=O&&O.running&&O.sources.has(e)?e.tValue:e.value;if(!e.comparator||!e.comparator(i,n)){if(O){let f=O.running;if(f||!t&&O.sources.has(e))O.sources.add(e),e.tValue=n;if(!f)e.value=n}else e.value=n;if(e.observers&&e.observers.length)B(()=>{for(let f=0;f1e6){if(_=[],lf)throw Error(\"Potential Infinite Loop Detected.\");throw Error()}},!1)}return n}function le(e){if(!e.fn)return;ne(e);let n=Ze;if(ut(e,O&&O.running&&O.sources.has(e)?e.tValue:e.value,n),O&&!O.running&&O.sources.has(e))queueMicrotask(()=>{B(()=>{O&&(O.running=!0),W=k=e,ut(e,e.tValue,n),W=k=null},!1)})}function ut(e,n,t){let i,f=k,u=W;W=k=e;try{i=e.fn(n)}catch(r){if(e.pure)if(O&&O.running)e.tState=D,e.tOwned&&e.tOwned.forEach(ne),e.tOwned=void 0;else e.state=D,e.owned&&e.owned.forEach(ne),e.owned=null;return e.updatedAt=t+1,Je(r)}finally{W=u,k=f}if(!e.updatedAt||e.updatedAt<=t){if(e.updatedAt!=null&&\"observers\"in e)yn(e,i,!0);else if(O&&O.running&&e.pure){if(!O.sources.has(e))e.value=i;O.sources.add(e),e.tValue=i}else e.value=i;e.updatedAt=t}}function Xe(e,n,t,i=D,f){let u={fn:e,state:i,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:n,owner:k,context:k?k.context:null,pure:t};if(O&&O.running)u.state=0,u.tState=i;if(k===null)console.warn(\"computations created outside a `createRoot` or `render` will never be disposed\");else if(k!==hf)if(O&&O.running&&k.pure)if(!k.tOwned)k.tOwned=[u];else k.tOwned.push(u);else if(!k.owned)k.owned=[u];else k.owned.push(u);if(f&&f.name)u.name=f.name;if(re&&u.fn){let r=u.fn,[c,l]=K(void 0,{equals:!1}),s=re.factory(r,l);ce(()=>s.dispose());let d,x=()=>of(l).then(()=>{if(d)d.dispose(),d=void 0});u.fn=(E)=>{if(c(),O&&O.running){if(!d)d=re.factory(r,x);return d.track(E)}return s.track(E)}}return Q.afterCreateOwner&&Q.afterCreateOwner(u),u}function Se(e){let n=O&&O.running;if((n?e.tState:e.state)===0)return;if((n?e.tState:e.state)===Ce)return je(e);if(e.suspense&&H(e.suspense.inFallback))return e.suspense.effects.push(e);let t=[e];while((e=e.owner)&&(!e.updatedAt||e.updatedAt=0;i--){if(e=t[i],n){let f=e,u=t[i+1];while((f=f.owner)&&f!==u)if(O.disposed.has(f))return}if((n?e.tState:e.state)===D)le(e);else if((n?e.tState:e.state)===Ce){let f=_;_=null,B(()=>je(e,t[0]),!1),_=f}}}function B(e,n){if(_)return e();let t=!1;if(!n)_=[];if(T)t=!0;else T=[];Ze++;try{let i=e();return $f(t),i}catch(i){if(!t)T=null;_=null,Je(i)}}function $f(e){if(_){if(Ae&&O&&O.running)wf(_);else $t(_);_=null}if(e)return;let n;if(O){if(!O.promises.size&&!O.queue.size){let{sources:i,disposed:f}=O;T.push.apply(T,O.effects),n=O.resolve;for(let u of T)\"tState\"in u&&(u.state=u.tState),delete u.tState;O=null,B(()=>{for(let u of f)ne(u);for(let u of i){if(u.value=u.tValue,u.owned)for(let r=0,c=u.owned.length;rlt(t),!1);else Q.afterUpdate&&Q.afterUpdate();if(n)n()}function $t(e){for(let n=0;n{i.delete(t),B(()=>{O.running=!0,Se(t)},!1),O&&(O.running=!1)})}}function gf(e){let n,t=0;for(n=0;n=0;n--)ne(e.tOwned[n]);delete e.tOwned}if(O&&O.running&&e.pure)gt(e,!0);else if(e.owned){for(n=e.owned.length-1;n>=0;n--)ne(e.owned[n]);e.owned=null}if(e.cleanups){for(n=e.cleanups.length-1;n>=0;n--)e.cleanups[n]();e.cleanups=null}if(O&&O.running)e.tState=0;else e.state=0;delete e.sourceMap}function gt(e,n){if(!n)e.tState=0,O.disposed.add(e);if(e.owned)for(let t=0;t=0;c--){let l=ln(e[c])[r];if(l!==void 0)return l}},has(r){for(let c=e.length-1;c>=0;c--)if(r in ln(e[c]))return!0;return!1},keys(){let r=[];for(let c=0;c=0;r--){let c=e[r];if(!c)continue;let l=Object.getOwnPropertyNames(c);for(let s=l.length-1;s>=0;s--){let d=l[s];if(d===\"__proto__\"||d===\"constructor\")continue;let x=Object.getOwnPropertyDescriptor(c,d);if(!i[d])i[d]=x.get?{enumerable:!0,configurable:!0,get:Of.bind(t[d]=[x.get.bind(c)])}:x.value!==void 0?x:void 0;else{let E=t[d];if(E){if(x.get)E.push(x.get.bind(c));else if(x.value!==void 0)E.push(()=>x.value)}}}}let f={},u=Object.keys(i);for(let r=u.length-1;r>=0;r--){let c=u[r],l=i[c];if(l&&l.get)Object.defineProperty(f,c,l);else f[c]=l?l.value:void 0}return f}var bt={hooks:Q,writeSignal:yn,registerGraph:ot};if(globalThis)if(!globalThis.Solid$$)globalThis.Solid$$=!0;else console.warn(\"You appear to have multiple instances of Solid. This can lead to unexpected behavior.\");var Ef=[\"allowfullscreen\",\"async\",\"alpha\",\"autofocus\",\"autoplay\",\"checked\",\"controls\",\"default\",\"disabled\",\"formnovalidate\",\"hidden\",\"indeterminate\",\"inert\",\"ismap\",\"loop\",\"multiple\",\"muted\",\"nomodule\",\"novalidate\",\"open\",\"playsinline\",\"readonly\",\"required\",\"reversed\",\"seamless\",\"selected\",\"adauctionheaders\",\"browsingtopics\",\"credentialless\",\"defaultchecked\",\"defaultmuted\",\"defaultselected\",\"defer\",\"disablepictureinpicture\",\"disableremoteplayback\",\"preservespitch\",\"shadowrootclonable\",\"shadowrootcustomelementregistry\",\"shadowrootdelegatesfocus\",\"shadowrootserializable\",\"sharedstoragewritable\"],bn=new Set([\"className\",\"value\",\"readOnly\",\"noValidate\",\"formNoValidate\",\"isMap\",\"noModule\",\"playsInline\",\"adAuctionHeaders\",\"allowFullscreen\",\"browsingTopics\",\"defaultChecked\",\"defaultMuted\",\"defaultSelected\",\"disablePictureInPicture\",\"disableRemotePlayback\",\"preservesPitch\",\"shadowRootClonable\",\"shadowRootCustomElementRegistry\",\"shadowRootDelegatesFocus\",\"shadowRootSerializable\",\"sharedStorageWritable\",...Ef]),xn=new Set([\"innerHTML\",\"textContent\",\"innerText\",\"children\"]),an=Object.assign(Object.create(null),{className:\"class\",htmlFor:\"for\"}),Cf=Object.assign(Object.create(null),{class:\"className\",novalidate:{$:\"noValidate\",FORM:1},formnovalidate:{$:\"formNoValidate\",BUTTON:1,INPUT:1},ismap:{$:\"isMap\",IMG:1},nomodule:{$:\"noModule\",SCRIPT:1},playsinline:{$:\"playsInline\",VIDEO:1},readonly:{$:\"readOnly\",INPUT:1,TEXTAREA:1},adauctionheaders:{$:\"adAuctionHeaders\",IFRAME:1},allowfullscreen:{$:\"allowFullscreen\",IFRAME:1},browsingtopics:{$:\"browsingTopics\",IMG:1},defaultchecked:{$:\"defaultChecked\",INPUT:1},defaultmuted:{$:\"defaultMuted\",AUDIO:1,VIDEO:1},defaultselected:{$:\"defaultSelected\",OPTION:1},disablepictureinpicture:{$:\"disablePictureInPicture\",VIDEO:1},disableremoteplayback:{$:\"disableRemotePlayback\",AUDIO:1,VIDEO:1},preservespitch:{$:\"preservesPitch\",AUDIO:1,VIDEO:1},shadowrootclonable:{$:\"shadowRootClonable\",TEMPLATE:1},shadowrootdelegatesfocus:{$:\"shadowRootDelegatesFocus\",TEMPLATE:1},shadowrootserializable:{$:\"shadowRootSerializable\",TEMPLATE:1},sharedstoragewritable:{$:\"sharedStorageWritable\",IFRAME:1,IMG:1}});function On(e,n){let t=Cf[e];return typeof t===\"object\"?t[n]?t.$:void 0:t}var En=new Set([\"beforeinput\",\"click\",\"dblclick\",\"contextmenu\",\"focusin\",\"focusout\",\"input\",\"keydown\",\"keyup\",\"mousedown\",\"mousemove\",\"mouseout\",\"mouseover\",\"mouseup\",\"pointerdown\",\"pointermove\",\"pointerout\",\"pointerover\",\"pointerup\",\"touchend\",\"touchmove\",\"touchstart\"]),Ct=new Set([\"altGlyph\",\"altGlyphDef\",\"altGlyphItem\",\"animate\",\"animateColor\",\"animateMotion\",\"animateTransform\",\"circle\",\"clipPath\",\"color-profile\",\"cursor\",\"defs\",\"desc\",\"ellipse\",\"feBlend\",\"feColorMatrix\",\"feComponentTransfer\",\"feComposite\",\"feConvolveMatrix\",\"feDiffuseLighting\",\"feDisplacementMap\",\"feDistantLight\",\"feDropShadow\",\"feFlood\",\"feFuncA\",\"feFuncB\",\"feFuncG\",\"feFuncR\",\"feGaussianBlur\",\"feImage\",\"feMerge\",\"feMergeNode\",\"feMorphology\",\"feOffset\",\"fePointLight\",\"feSpecularLighting\",\"feSpotLight\",\"feTile\",\"feTurbulence\",\"filter\",\"font\",\"font-face\",\"font-face-format\",\"font-face-name\",\"font-face-src\",\"font-face-uri\",\"foreignObject\",\"g\",\"glyph\",\"glyphRef\",\"hkern\",\"image\",\"line\",\"linearGradient\",\"marker\",\"mask\",\"metadata\",\"missing-glyph\",\"mpath\",\"path\",\"pattern\",\"polygon\",\"polyline\",\"radialGradient\",\"rect\",\"set\",\"stop\",\"svg\",\"switch\",\"symbol\",\"text\",\"textPath\",\"tref\",\"tspan\",\"use\",\"view\",\"vkern\"]),Cn={xlink:\"http://www.w3.org/1999/xlink\",xml:\"http://www.w3.org/XML/1998/namespace\"};function Sf(e,n,t){let i=t.length,f=n.length,u=i,r=0,c=0,l=n[f-1].nextSibling,s=null;while(rd-c){let h=n[r];while(c{f=u,n===document?e():kn(n,e(),n.firstChild?null:void 0,t)},i.owner),()=>{f(),n.textContent=\"\"}}function Sn(e,n=window.document){let t=n[xt]||(n[xt]=new Set);for(let i=0,f=e.length;if.call(e,t[1],u))}else e.addEventListener(n,t,typeof t!==\"function\"&&t)}function Pn(e,n,t={}){let i=Object.keys(n||{}),f=Object.keys(t),u,r;for(u=0,r=f.length;uf.children=de(e,n.children,f.children));return M(()=>typeof n.ref===\"function\"&&Pf(n.ref,e)),M(()=>Vf(e,n,t,!0,f,!0)),f}function Nt(e,n){let t=e[n];return Object.defineProperty(e,n,{get(){return t()},enumerable:!0}),e}function Pf(e,n,t){return H(()=>e(n,t))}function kn(e,n,t,i){if(t!==void 0&&!i)i=[];if(typeof n!==\"function\")return de(e,n,i,t);M((f)=>de(e,n(),f,t),i)}function Vf(e,n,t,i,f={},u=!1){n||(n={});for(let r in f)if(!(r in n)){if(r===\"children\")continue;f[r]=Ot(e,r,null,f[r],t,u,n)}for(let r in n){if(r===\"children\"){if(!i)de(e,n.children);continue}let c=n[r];f[r]=Ot(e,r,c,f[r],t,u,n)}}function he(e){return!!v.context&&!v.done&&(!e||e.isConnected)}function kf(e){return e.toLowerCase().replace(/-([a-z])/g,(n,t)=>t.toUpperCase())}function at(e,n,t){let i=n.trim().split(/\\s+/);for(let f=0,u=i.length;f-1&&Cn[n.split(\":\")[0]];if(E)An(e,E,n,t);else Pe(e,an[n]||n,t)}return t}function If(e){if(v.registry&&v.events){if(v.events.find(([l,s])=>s===e))return}let n=e.target,t=`$$${e.type}`,i=e.target,f=e.currentTarget,u=(l)=>Object.defineProperty(e,\"target\",{configurable:!0,value:l}),r=()=>{let l=n[t];if(l&&!n.disabled){let s=n[`${t}Data`];if(s!==void 0?l.call(n,s,e):l.call(n,e),e.cancelBubble)return}return n.host&&typeof n.host!==\"string\"&&!n.host._$host&&n.contains(e.target)&&u(n.host),!0},c=()=>{while(r()&&(n=n._$host||n.parentNode||n.host));};if(Object.defineProperty(e,\"currentTarget\",{configurable:!0,get(){return n||document}}),v.registry&&!v.done)v.done=_$HY.done=!0;if(e.composedPath){let l=e.composedPath();u(l[0]);for(let s=0;s{let l=n();while(typeof l===\"function\")l=l();t=de(e,l,t,i)}),()=>t;else if(Array.isArray(n)){let l=[],s=t&&Array.isArray(t);if(gn(l,n,t,f))return M(()=>t=de(e,l,t,i,!0)),()=>t;if(u){if(!l.length)return t;if(i===void 0)return t=[...e.childNodes];let d=l[0];if(d.parentNode!==e)return t;let x=[d];while((d=d.nextSibling)!==i)x.push(d);return t=x}if(l.length===0){if(t=se(e,t,i),c)return t}else if(s)if(t.length===0)Et(e,l,i);else Sf(e,t,l);else t&&se(e),Et(e,l);t=l}else if(n.nodeType){if(u&&n.parentNode)return t=c?[n]:n;if(Array.isArray(t)){if(c)return t=se(e,t,i,n);se(e,t,null,n)}else if(t==null||t===\"\"||!e.firstChild)e.appendChild(n);else e.replaceChild(n,e.firstChild);t=n}else console.warn(\"Unrecognized value. Skipped inserting\",n);return t}function gn(e,n,t,i){let f=!1;for(let u=0,r=n.length;u=0;r--){let c=n[r];if(f!==c){let l=c.parentNode===e;if(!u&&!r)l?e.replaceChild(f,c):e.insertBefore(f,t);else l&&c.remove()}else u=!0}}else e.insertBefore(f,t);return[f]}var Lu=Symbol();var In=Symbol(\"store-raw\"),ye=Symbol(\"store-node\"),ee=Symbol(\"store-has\"),Vt=Symbol(\"store-self\"),Pt={onStoreNodeUpdate:null};function kt(e){let n=e[J];if(!n){if(Object.defineProperty(e,J,{value:n=new Proxy(e,Wf)}),!Array.isArray(e)){let t=Object.keys(e),i=Object.getOwnPropertyDescriptors(e);for(let f=0,u=t.length;fe[J][n],t}function It(e){qe()&&ke(Qe(e,ye),Vt)()}function zf(e){return It(e),Reflect.ownKeys(e)}var Wf={get(e,n,t){if(n===In)return e;if(n===J)return t;if(n===hn)return It(e),t;let i=Qe(e,ye),f=i[n],u=f?f():e[n];if(n===ye||n===ee||n===\"__proto__\")return u;if(!f){let r=Object.getOwnPropertyDescriptor(e,n);if(qe()&&(typeof u!==\"function\"||e.hasOwnProperty(n))&&!(r&&r.get))u=ke(i,n,u)()}return te(u)?kt(u):u},has(e,n){if(n===In||n===J||n===hn||n===ye||n===ee||n===\"__proto__\")return!0;return qe()&&ke(Qe(e,ee),n)(),n in e},set(){return console.warn(\"Cannot mutate a Store directly\"),!0},deleteProperty(){return console.warn(\"Cannot mutate a Store directly\"),!0},ownKeys:zf,getOwnPropertyDescriptor:Kf};function X(e,n,t,i=!1){if(!i&&e[n]===t)return;let f=e[n],u=e.length;if(Pt.onStoreNodeUpdate&&Pt.onStoreNodeUpdate(e,n,t,f),t===void 0){if(delete e[n],e[ee]&&e[ee][n]&&f!==void 0)e[ee][n].$()}else if(e[n]=t,e[ee]&&e[ee][n]&&f===void 0)e[ee][n].$();let r=Qe(e,ye),c;if(c=ke(r,n,f))c.$(()=>t);if(Array.isArray(e)&&e.length!==u){for(let l=e.length;l1){i=n.shift();let r=typeof i,c=Array.isArray(e);if(Array.isArray(i)){for(let l=0;l1){Ve(e[i],n,[i].concat(t));return}f=e[i],t=[i].concat(t)}let u=n[0];if(typeof u===\"function\"){if(u=u(f,t),u===f)return}if(i===void 0&&u==null)return;if(u=$e(u),i===void 0||te(f)&&te(u)&&!Array.isArray(u))Kt(f,u);else X(e,i,u)}function zt(...[e,n]){let t=$e(e||{}),i=Array.isArray(t);if(typeof t!==\"object\"&&typeof t!==\"function\")throw Error(`Unexpected type ${typeof t} received when initializing 'createStore'. Expected an object.`);let f=kt(t);bt.registerGraph({value:t,name:n&&n.name});function u(...r){Ne(()=>{i&&r.length===1?Yf(t,r[0]):Ve(t,r)})}return[f,u]}var Kn=Symbol(\"store-root\");function oe(e,n,t,i,f){let u=n[t];if(e===u)return;let r=Array.isArray(e);if(t!==Kn&&(!te(e)||!te(u)||r!==Array.isArray(u)||f&&e[f]!==u[f])){X(n,t,e);return}if(r){if(e.length&&u.length&&(!i||f&&e[0]&&e[0][f]!=null)){let s,d,x,E,w,h,y,b;for(x=0,E=Math.min(u.length,e.length);x=x&&w>=x&&(u[E]===e[w]||f&&u[E]&&e[w]&&u[E][f]&&u[E][f]===e[w][f]);E--,w--)C[w]=u[E];if(x>w||x>E){for(d=x;d<=w;d++)X(u,d,e[d]);for(;de.length)X(u,\"length\",e.length);return}y=Array(w+1);for(d=w;d>=x;d--)h=e[d],b=f&&h?h[f]:h,s=V.get(b),y[d]=s===void 0?-1:s,V.set(b,d);for(s=x;s<=E;s++)if(h=u[s],b=f&&h?h[f]:h,d=V.get(b),d!==void 0&&d!==-1)C[d]=u[s],d=y[d],V.set(b,d);for(d=x;de.length)X(u,\"length\",e.length);return}let c=Object.keys(e);for(let s=0,d=c.length;s{if(!te(u)||!te(f))return f;let r=oe(f,{[Kn]:u},Kn,t,i);return r===void 0?u:r}}var vf=/(?:|<(?:\"[^\"]*\"['\"]*|'[^']*'['\"]*|[^'\">])+>)/g,Ff=/(?:\\s(?[^/\\s><=]+?)(?=[\\s/>]))|(?:(?\\S+?)(?:\\s*=\\s*(?:(['\"])(?[\\s\\S]*?)\\3|(?[^\\s>]+))))/g,_f={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,menuitem:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0};function Tf(e){let n={type:\"tag\",name:\"\",voidElement:!1,attrs:[],children:[]},t=e.match(/<\\/?([^\\s]+?)[/\\s>]/);if(t){if(n.name=t[1],_f[t[1].toLowerCase()]||e.charAt(e.length-2)===\"/\")n.voidElement=!0;if(n.name.startsWith(\"!--\")){let f=e.indexOf(\"-->\");return{type:\"comment\",comment:f!==-1?e.slice(4,f):\"\"}}}let i=new RegExp(Ff);for(let f of e.matchAll(i))if((f[1]||f[2]).startsWith(\"use:\"))n.attrs.push({type:\"directive\",name:f[1]||f[2],value:f[4]||f[5]||\"\"});else n.attrs.push({type:\"attr\",name:f[1]||f[2],value:f[4]||f[5]||\"\"});return n}function Yt(e,n,t){let i=n.indexOf(\"<\",t),f=n.slice(t,i===-1?void 0:i);if(!/^\\s*$/.test(f))e.push({type:\"text\",content:f})}function vt(e,n){let t=n.replace(\"\",\"\");if(!/^\\s*$/.test(t))e.push({type:\"comment\",content:t})}function Hf(e){let n=[],t=void 0,i=-1,f=[],u={};return e.replace(vf,(r,c)=>{let l=r.charAt(1)!==\"/\",s=r.slice(0,4)===\"\"}}function Uf(e){return e.reduce(function(n,t){return n+Tt(\"\",t)},\"\")}var Ft=new Map,jf=/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i,Be=\" \\\\f\\\\n\\\\r\\\\t\",Ht=\"[^\"+Be+`\\\\/>\"'=]+`,Wn=\"[ \"+Be+\"]+(?:use:|\"+Ht+\")\",Gt=\"<([A-Za-z$#]+[A-Za-z0-9:_-]*)((?:\",Ut=`(?:\\\\s*=\\\\s*(?:'[^']*?'|\"[^\"]*?\"|\\\\([^)]*?\\\\)|<[^>]*?>|`+Ht+\"))?)\",Zf=new RegExp(Gt+Wn+Ut+\"+)([ \"+Be+\"]*/?>)\",\"g\"),qf=new RegExp(\"(\"+Wn+`\\\\s*=\\\\s*)(|['\"(]([\\\\w\\\\s]*[\\\\w\\\\s]*)*['\")])`,\"gi\"),Df=new RegExp(Gt+Wn+Ut+\"*)([ \"+Be+\"]*/>)\",\"g\"),_t=\"\",Xf=new Set([\"class\",\"on\",\"oncapture\",\"style\",\"use\",\"prop\",\"attr\"]);function Jf(e,n,t,i){return\"<\"+n+t.replace(qf,Qf)+i}function Qf(e,n,t){return n.replace(//g,\"###\")+(t[0]==='\"'||t[0]===\"'\"?t.replace(//g,\"###\"):'\"###\"')}function Bf(e,n,t){return jf.test(n)?e:\"<\"+n+t+\">\"}function Rf(e){return e.toLowerCase().replace(/-([a-z])/g,(n,t)=>t.toUpperCase())}function zn(e,n,t,i){if(e===\"use:###\"&&n===\"###\"){let f=i.counter++;i.exprs.push(`typeof exprs[${f}] === \"function\" ? r.use(exprs[${f}], ${t}, exprs[${i.counter++}]) : (()=>{throw new Error(\"use:### must be a function\")})()`)}else throw Error(`Not support syntax ${e} must be use:{function}`)}function Lf(e,{delegateEvents:n=!0,functionBuilder:t=(...i)=>Function(...i)}={}){let i=1;e.wrapProps=(w)=>{let h=Object.getOwnPropertyDescriptors(w);for(let y in h)if(typeof h[y].value===\"function\"&&!h[y].value.length)e.dynamicProperty(w,y);return w};function f(w,h){let y=0,b=\"\";for(;y\";b=b+w[y],b=[[Df,Bf],[/<()/g,\"<###\"],[/\\.\\.\\.()/g,\"###\"],[Zf,Jf],[/>\\n+\\s*/g,\">\"],[/\\n+\\s*\\s+/g,\"> \"]].reduce((I,z)=>{return I.replace(z[0],z[1])},b);let V=Hf(b),[A,S]=x(V,h.funcBuilder),P=[];for(let I=0;I-1){let m=F.split(_t).reduce((p,ve,ae)=>{return ae&&p.push(\"\"),p.push(ve),p},[]);z[I].firstChild.replaceWith(...m)}}}return P[0].create=S,Ft.set(w,P),P}function u(w,h,y,b,C,V,A){let S=b===\"###\"?`!doNotWrap ? exprs[${A.counter}]() : exprs[${A.counter++}]`:b.split(\"###\").map((F,m)=>m?` + (typeof exprs[${A.counter}] === \"function\" ? exprs[${A.counter}]() : exprs[${A.counter++}]) + \"${F}\"`:`\"${F}\"`).join(\"\"),P,I;if((P=y.split(\":\"))&&P[1]&&Xf.has(P[0]))y=P[1],I=P[0];let z=e.ChildProperties.has(y),G=e.Properties.has(y);if(y===\"style\"){let F=`_$v${i++}`;A.decl.push(`${F}={}`),A.exprs.push(`r.style(${h},${S},${F})`)}else if(y===\"classList\"){let F=`_$v${i++}`;A.decl.push(`${F}={}`),A.exprs.push(`r.classList(${h},${S},${F})`)}else if(I!==\"attr\"&&(z||!C&&(e.getPropAlias(y,w.name.toUpperCase())||G)||V||I===\"prop\")){if(V&&!z&&!G&&I!==\"prop\")y=Rf(y);A.exprs.push(`${h}.${e.getPropAlias(y,w.name.toUpperCase())||y} = ${S}`)}else{let F=C&&y.indexOf(\":\")>-1&&e.SVGNamespace[y.split(\":\")[0]];if(F)A.exprs.push(`r.setAttributeNS(${h},\"${F}\",\"${y}\",${S})`);else A.exprs.push(`r.setAttribute(${h},\"${e.Aliases[y]||y}\",${S})`)}}function r(w,h,y,b,C,V,A){if(y.slice(0,2)===\"on\")if(!y.includes(\":\")){let S=y.slice(2).toLowerCase(),P=n&&e.DelegatedEvents.has(S);A.exprs.push(`r.addEventListener(${h},\"${S}\",exprs[${A.counter++}],${P})`),P&&A.delegatedEvents.add(S)}else{let S=y.startsWith(\"oncapture:\");A.exprs.push(`${h}.addEventListener(\"${y.slice(S?10:3)}\",exprs[${A.counter++}]${S?\",true\":\"\"})`)}else if(y===\"ref\")A.exprs.push(`exprs[${A.counter++}](${h})`);else{let S=Object.assign({},A,{exprs:[]}),P=A.counter;if(u(w,h,y,b,C,V,S),A.decl.push(`_fn${P} = (${b===\"###\"?\"doNotWrap\":\"\"}) => {\n${S.exprs.join(`;\n`)};\n}`),b===\"###\")A.exprs.push(`typeof exprs[${P}] === \"function\" ? r.effect(_fn${P}) : _fn${P}(true)`);else{let I=\"\";for(let z=P;z1)for(let C=0;C1?`r.mergeProps(${h.join(\",\")})`:h[0]}function s(w,h){let y=[],b=Object.keys(w.attrs),C=[y],V=h.counter++;for(let S=0;S exprs[${h.counter++}]`);else if(w.children.length){let S={type:\"fragment\",children:w.children},P=Object.assign({},h,{first:!0,decl:[],exprs:[],parent:!1});d(S,P),y.push(`children: () => { ${P.exprs.join(`;\n`)}}`),h.templateId=P.templateId,h.counter=P.counter}let A;if(h.multi)A=`_$el${i++}`,h.decl.push(`${A} = ${h.path}.${h.first?\"firstChild\":\"nextSibling\"}`);if(h.parent)h.exprs.push(`r.insert(${h.parent}, r.createComponent(exprs[${V}],${l(C)})${A?`, ${A}`:\"\"})`);else h.exprs.push(`${h.fragment?\"\":\"return \"}r.createComponent(exprs[${V}],${l(C)})`);h.path=A,h.first=!1}function d(w,h){if(w.type===\"fragment\"){let y=[];w.children.forEach((b)=>{if(b.type===\"tag\"){if(b.name===\"###\"){let A=Object.assign({},h,{first:!0,fragment:!0,decl:[],exprs:[]});s(b,A),y.push(A.exprs[0]),h.counter=A.counter,h.templateId=A.templateId;return}h.templateId++;let C=i,V=Object.assign({},h,{first:!0,decl:[],exprs:[]});h.templateNodes.push([b]),d(b,V),y.push(`function() { ${V.decl.join(`,\n`)+`;\n`+V.exprs.join(`;\n`)+`;\nreturn _$el${C};\n`}}()`),h.counter=V.counter,h.templateId=V.templateId}else if(b.type===\"text\")y.push(`\"${b.content}\"`);else if(b.type===\"comment\"){if(b.content===\"#\")y.push(`exprs[${h.counter++}]`);else if(b.content)for(let C=0;CS.name===\"is\");if(h.hasCustomElement=A,h.isImportNode=(w.name===\"img\"||w.name===\"iframe\")&&w.attrs.some((S)=>S.name===\"loading\"&&S.value===\"lazy\"),w.attrs.some((S)=>S.name===\"###\")){let S=[],P=\"\",I=[];for(let z=0;z({${P}})`),P=\"\";S.push(`exprs[${h.counter++}]`)}else I.push(w.attrs[z]);else if(G===\"directive\")zn(F,m,y,h)}if(w.attrs=I,P.length)S.push(`()=>({${P}})`);h.exprs.push(`r.spread(${y},${S.length===1?`typeof ${S[0]} === \"function\" ? r.mergeProps(${S[0]}) : ${S[0]}`:`r.mergeProps(${S.join(\",\")})`},${V},${!!w.children.length})`)}else for(let S=0;S document.importNode(tmpls[${C}].content.firstChild, true))`:`const ${y} = tmpls[${C}].content.firstChild.cloneNode(true)`}else if(w.type===\"text\"){let y=`_$el${i++}`;h.decl.push(`${y} = ${h.path}.${h.first?\"firstChild\":\"nextSibling\"}`),h.path=y,h.first=!1}else if(w.type===\"comment\"){let y=`_$el${i++}`;if(h.decl.push(`${y} = ${h.path}.${h.first?\"firstChild\":\"nextSibling\"}`),w.content===\"#\")if(h.multi)h.exprs.push(`r.insert(${h.parent}, exprs[${h.counter++}], ${y})`);else h.exprs.push(`r.insert(${h.parent}, exprs[${h.counter++}])`);h.path=y,h.first=!1}}function x(w,h){let y={path:\"\",decl:[],exprs:[],delegatedEvents:new Set,counter:0,first:!0,multi:!1,templateId:0,templateNodes:[]},b=i,C=w,V;if(w.length>1)w=[{type:\"fragment\",children:w}];if(w[0].name===\"###\")V=!0,s(w[0],y);else d(w[0],y);return e.delegateEvents(Array.from(y.delegatedEvents)),[[C].concat(y.templateNodes).map((S)=>Uf(S)),h(\"tmpls\",\"exprs\",\"r\",y.decl.join(`,\n`)+`;\n`+y.exprs.join(`;\n`)+(V?\"\":`;\nreturn _$el${b};\n`))]}function E(w,...h){let y=Ft.get(w)||f(w,{funcBuilder:t});return y[0].create(y,h,e)}return E}var o=Lf({effect:M,style:Vn,insert:kn,untrack:H,spread:At,createComponent:$n,delegateEvents:Sn,classList:Pn,mergeProps:wn,dynamicProperty:Nt,setAttribute:Pe,setAttributeNS:An,addEventListener:Nn,Aliases:an,getPropAlias:On,Properties:bn,ChildProperties:xn,DelegatedEvents:En,SVGElements:Ct,SVGNamespace:Cn});function Ie(e){let n=Math.floor(e/3600),t=Math.floor(e%3600/60),i=e%60;if(n>0)return`${n}h ${t}m`;return t>0?`${t}m ${i}s`:`${i}s`}function Re(e,n){if(!e)return 0;let t=new Date(e).getTime(),i=n?new Date(n).getTime():Date.now();return Math.round((i-t)/1000)}function jt(e){return e.toLowerCase().replace(/[^\\w\\s-]/g,\"\").trim().replace(/\\s+/g,\"-\").replace(/-+/g,\"-\")}var Zt=[\"code\",\"auditor\",\"unknown\"];var vn=[\"overview\",\"timeline\",\"sections\",\"findings\",\"plan\",\"usage\"],Fn=200,qt=300,Dt=100;function Le(e,n,t){if(t||e.length<=n)return{rows:e,total:e.length,capped:!1};return{rows:e.slice(0,n),total:e.length,capped:!0}}function _n(e,n){return e.length===n.length&&e.every((t,i)=>t===n[i])}var Mf=new Set([\"loops\",\"groups\",\"findings\",\"plans\"]),mf=new Set(vn);function ie(e){try{return decodeURIComponent(e)}catch{return e}}function pf(){return{projectId:null,section:\"loops\",loopName:null,tab:\"overview\",groupId:null,statuses:[],query:\"\"}}function Yn(e,n){if(!n)return;for(let t of n.split(\"&\")){if(!t)continue;let i=t.indexOf(\"=\");if(i===-1)continue;let f=ie(t.slice(0,i)),u=ie(t.slice(i+1));if(f===\"status\")e.statuses=u.split(\",\").map((r)=>r.trim()).filter((r)=>r!==\"\");else if(f===\"q\")e.query=u}}function Me(e){let n=pf(),t=(e||\"\").replace(/^#/,\"\");if(!t)return n;let i=t.indexOf(\"?\"),f=i===-1?t:t.slice(0,i),u=i===-1?\"\":t.slice(i+1),r=f.indexOf(\"/\"),c,l;if(r===-1)c=f,l=\"\";else c=f.slice(0,r),l=f.slice(r+1);if(c)n.projectId=ie(c);if(!l)return Yn(n,u),n;let s=l.split(\"/\").filter((x)=>x!==\"\");if(s.length===0)return Yn(n,u),n;let d=ie(s[0]);if(d===\"loop\"){if(s.length>=2){if(n.loopName=ie(s[1]),s.length>=3){let x=ie(s[2]);if(mf.has(x))n.tab=x}}}else if(Mf.has(d)){if(n.section=d,d===\"groups\"&&s.length>=2)n.groupId=ie(s[1])}else n.loopName=d;return Yn(n,u),n}function me(e){if(!e.projectId)return\"\";let n=\"#\"+encodeURIComponent(e.projectId);if(e.section===\"loops\"){if(e.loopName){if(n+=\"/loop/\"+encodeURIComponent(e.loopName),e.tab!==\"overview\")n+=\"/\"+e.tab}}else if(e.section===\"groups\"){if(n+=\"/groups\",e.groupId)n+=\"/\"+encodeURIComponent(e.groupId)}else n+=\"/\"+e.section;let t=[];if(e.statuses.length>0)t.push(\"status=\"+e.statuses.map((i)=>encodeURIComponent(i)).join(\",\"));if(e.query)t.push(\"q=\"+encodeURIComponent(e.query));if(t.length>0)n+=\"?\"+t.join(\"&\");return n}function Xt(e,n){let i=\"#\"+(location.hash||\"\").replace(/^#/,\"\"),f=\"#\"+e.replace(/^#/,\"\");if(i!==f)n.current=!0,location.hash=e}function j(e){if(!e||e===0)return\"\";let n=new Date(e),t=(l)=>l<10?\"0\"+l:String(l),i=t(n.getMonth()+1),f=t(n.getDate()),u=n.getFullYear(),r=n.getHours(),c=r>=12?\"PM\":\"AM\";if(r=r%12,r===0)r=12;return i+\"-\"+f+\"-\"+u+\" \"+r+\":\"+t(n.getMinutes())+\" \"+c}function fe(e){return\"status-badge status-\"+e}function Tn(e){return\"section-status section-\"+e}function Jt(e){return\"feature-stage feature-stage-\"+e}function Qt(e,n,t,i){if(!(n.size===0||n.has(e.status)))return!1;if(!t)return!0;let u=t.toLowerCase(),r=e.worktreeBranch??\"\";return((e.loopName||\"\")+\" \"+r+\" \"+(i||\"\")).toLowerCase().indexOf(u)!==-1}function Bt(e,n){if(n===\"recent\")return[...e];let t=[...e];if(n===\"cost\")t.sort((i,f)=>{let u=i.usage?.totalCost??null,r=f.usage?.totalCost??null;if(u===null&&r===null)return 0;if(u===null)return 1;if(r===null)return-1;return r-u});else if(n===\"duration\")t.sort((i,f)=>{let u=Re(i.loop.startedAt,i.loop.completedAt??void 0);return Re(f.loop.startedAt,f.loop.completedAt??void 0)-u});else if(n===\"findings\")t.sort((i,f)=>f.findings.length-i.findings.length);return t}function Rt(e){return JSON.stringify(e.projects)}function we(e){return e.projectDir||e.projectId||\"\"}function ge(e,n){let t=we(n);return e.get(t)??t}function ze(e){return e.completedAt||e.startedAt||0}function Lt(e){let n=new Map;for(let f of e){if(f===\"\"){n.set(f,[]);continue}let u=f.split(\"/\"),r=u.length;while(r>0&&u[r-1]===\"\")r--;n.set(f,r>0?u.slice(0,r):[f])}let t=new Map;for(let f of e){let u=n.get(f);t.set(f,u.length?u[u.length-1]:f)}let i=new Map;for(let f of e){let u=t.get(f),r=i.get(u);if(r)r.push(f);else i.set(u,[f])}for(let[,f]of i){let u=[...new Set(f)];if(u.length<2)continue;let r=1;while(r<1024){let c=new Set,l=!0,s=!0;for(let d of u){let x=n.get(d),E=Math.max(0,x.length-r),w=x.slice(E).join(\"/\");if(c.has(w))l=!1;else c.add(w);if(E>0)s=!1}if(l||s)break;r++}for(let c of u){let l=n.get(c),s=Math.max(0,l.length-r);t.set(c,l.slice(s).join(\"/\"))}}return t}function Hn(e){let n=[],t=[];for(let i of e)if(i.severity===\"bug\")n.push(i);else t.push(i);return{bugs:n,warnings:t}}function Mt(e){if(e.bugs.length>0)return\"bug\";if(e.warnings.length>0)return\"warn\";return\"clean\"}function be(e,n){return e+\" \"+n+(e===1?\"\":\"s\")}function Gn(e,n){if(!n||n<=0)return 0;return Math.max(0,Math.min(100,e/n*100))}function mt(e){let n=e.file+\":\"+e.line+\" — \"+e.description;if(e.scenario)n+=\" (\"+e.scenario+\")\";return n}function pt(e,n){return\" \"+e+\": $\"+n.cost.toFixed(6)+\", \"+n.inputTokens+\" in / \"+n.outputTokens+\" out (reasoning: \"+n.reasoningTokens+\", cache R: \"+n.cacheReadTokens+\" W: \"+n.cacheWriteTokens+\"), messages: \"+n.messageCount}function We(e){if(!Number.isFinite(e)||e<=0)return\"0\";if(e>=1e6)return(e/1e6).toFixed(1).replace(/\\.0$/,\"\")+\"M\";if(e>=1000)return(e/1000).toFixed(1).replace(/\\.0$/,\"\")+\"k\";return String(e)}function pe(e){if(!Number.isFinite(e)||e<=0)return\"$0\";return\"$\"+(e>=1?e.toFixed(2):e.toFixed(4))}function ei(e){let n=[{label:\"Input\",value:e.totalInputTokens,color:\"var(--seg-input)\"},{label:\"Output\",value:e.totalOutputTokens,color:\"var(--seg-output)\"},{label:\"Reasoning\",value:e.totalReasoningTokens,color:\"var(--seg-reasoning)\"},{label:\"Cache R\",value:e.totalCacheReadTokens,color:\"var(--seg-cache-read)\"},{label:\"Cache W\",value:e.totalCacheWriteTokens,color:\"var(--seg-cache-write)\"}],t=n.reduce((i,f)=>i+Math.max(0,f.value),0);return n.map((i)=>({...i,pct:t>0?Math.max(0,i.value)/t*100:0}))}var eu={code:\"execution\",auditor:\"auditor\",unknown:\"other\"};function ni(e){let t=Zt.map((f)=>{let u={role:eu[f],cost:0,messageCount:0,tokens:0,pct:0},r=e.byRole[f];if(!r)return u;return u.cost=r.cost,u.messageCount=r.messageCount,u.tokens=r.inputTokens+r.outputTokens+r.reasoningTokens,u}).filter((f)=>f.role!==\"other\"||f.cost>0||f.messageCount>0),i=t.reduce((f,u)=>Math.max(f,u.cost),0);for(let f of t)f.pct=i>0?f.cost/i*100:0;return t}function ti(e){let n=Object.keys(e.byModel).map((i)=>({model:i,...e.byModel[i]})),t=n.reduce((i,f)=>Math.max(i,f.cost),0);return n.sort((i,f)=>f.cost-i.cost).map((i)=>({model:i.model,cost:i.cost,inputTokens:i.inputTokens,outputTokens:i.outputTokens,messageCount:i.messageCount,pct:t>0?i.cost/t*100:0}))}function ii(e,n){if(!e)return\"\";let t=Re(e,n??void 0);return t>0?Ie(t):\"\"}function xe(e){if(!e||e===0)return\"\";let n=Date.now()-e,t=Math.floor(n/1000);if(t<60)return t+\"s ago\";let i=Math.floor(t/60);if(i<60)return i+\"m ago\";let f=Math.floor(i/60);if(f<24)return f+\"h ago\";let u=Math.floor(f/24);if(u<30)return u+\"d ago\";return j(e)}var nu=/([\\s\\S]*?)<\\/h\\1>/g;function fi(e){return e.replace(/<[^>]+>/g,\"\").replace(/&[a-z#0-9]+;/gi,\"\").trim()}function tu(e){return jt(fi(e)).replace(/[_-]+/g,\"-\").replace(/^-+|-+$/g,\"\")||\"heading\"}function iu(e){if(!//.test(e))return{html:e,outline:[]};let n=[];return{html:e.replace(nu,(i,f,u)=>{let r=Number(f),c=tu(u);return n.push({depth:r,id:c,text:fi(u)}),`${u}`}),outline:n}}function ui(e){if(!e)return{html:\"\",outline:[]};let n=Ke.get(e);if(n!==void 0)return n;let t=globalThis.marked;if(!t)return{html:\"\",outline:[]};let{html:i,outline:f}=iu(t.parse(e)),u={html:i,outline:f};if(Ke.size>=fu){let r=Ke.keys().next().value;if(r!==void 0)Ke.delete(r)}return Ke.set(e,u),u}function ri(e){return ui(e).html}function ci(e){return ui(e)}var Ke=new Map,fu=200;function en(e){let n=[\"overview\",\"timeline\"];if(e.sectionCount>0)n.push(\"sections\");if(n.push(\"findings\"),e.hasPlan)n.push(\"plan\");return n.push(\"usage\"),n}function li(e,n,t,i,f=\"coding\"){let u=[],r=e.findIndex((x)=>x.createdAt>=n),c=r===-1?[]:e.slice(r);if(t!==null)c=c.filter((x)=>x.createdAt<=t);let l;if(c.length>0&&c[0].eventType===\"restart\"&&c[0].toPhase!==null)l=c[0].toPhase;else if(c.length>0&&c[0].eventType!==\"restart\")l=c[0].fromPhase;else l=f;let s=n;for(let x of c){if(x.eventType===\"restart\"){if(x.toPhase!==null)l=x.toPhase;continue}if(l===\"\")l=x.fromPhase,s=Math.max(s,x.createdAt);let E=x.createdAt,w=Math.max(s,E);if(u.push({phase:l,startedAt:s,endedAt:w,durationMs:w-s,open:!1}),x.toPhase===null)l=\"\",s=w;else l=x.toPhase,s=w}if(l!==\"\"){let E=Math.max(s,t??i);u.push({phase:l,startedAt:s,endedAt:t,durationMs:E-s,open:t===null})}let d=e.length>0&&e[0].createdAt>=n&&e[0].createdAt>n+1000;if(d&&u.length>0)u[0]={...u[0],phase:\"\"};return{spans:u,truncated:d}}function si(e){let n={};for(let t of e){if(!t.phase)continue;n[t.phase]=(n[t.phase]??0)+t.durationMs}return n}function di(e,n,t){let i=[];for(let f=0;f=n)r=Math.max(0,u.createdAt-n);else if(u.createdAt=n)r=Math.max(0,u.createdAt-n);else r=null}i.push({transition:u,elapsedMs:r})}return i.reverse(),i}var uu=[{phase:\"coding\",label:\"coding\",cx:70,cy:60},{phase:\"auditing\",label:\"auditing\",cx:220,cy:60},{phase:\"final_auditing\",label:\"final_auditing\",cx:370,cy:60},{phase:\"post_action\",label:\"post_action\",cx:520,cy:60},{phase:\"final_audit_fix\",label:\"final_audit_fix\",cx:370,cy:160}],nn=110,tn=36,hi=655,oi=160,ru=[{key:\"coding→auditing\",d:\"M 125 60 L 165 60\",labelX:145,labelY:52},{key:\"auditing→final_auditing\",d:\"M 275 60 L 315 60\",labelX:295,labelY:52},{key:\"final_auditing→post_action\",d:\"M 425 60 L 465 60\",labelX:445,labelY:52},{key:\"auditing→coding\",d:\"M 220 42 Q 145 14 70 42\",labelX:145,labelY:18},{key:\"auditing→post_action\",d:\"M 220 42 Q 370 8 520 42\",labelX:370,labelY:16},{key:\"final_auditing→auditing\",d:\"M 315 42 Q 295 24 275 42\",labelX:295,labelY:30},{key:\"final_auditing→coding\",d:\"M 370 78 C 370 110 145 110 70 78\",labelX:220,labelY:102},{key:\"final_auditing→final_audit_fix\",d:\"M 370 78 L 370 142\",labelX:358,labelY:112},{key:\"final_audit_fix→final_auditing\",d:\"M 415 142 L 415 78\",labelX:442,labelY:116},{key:\"coding→final_audit_fix\",d:\"M 70 78 C 70 95 200 115 315 142\",labelX:175,labelY:118},{key:\"final_audit_fix→coding\",d:\"M 315 178 C 250 210 100 210 70 78\",labelX:175,labelY:205},{key:\"coding→terminal\",d:\"M 125 78 L 600 142\",labelX:240,labelY:90},{key:\"auditing→terminal\",d:\"M 275 78 L 600 142\",labelX:395,labelY:90},{key:\"final_auditing→terminal\",d:\"M 425 78 L 600 142\",labelX:505,labelY:90},{key:\"post_action→terminal\",d:\"M 520 78 L 600 142\",labelX:565,labelY:95},{key:\"final_audit_fix→terminal\",d:\"M 425 160 L 600 160\",labelX:512,labelY:152}];function cu(e){if(e.toPhase===null)return e.fromPhase+\"→terminal\";return e.fromPhase+\"→\"+e.toPhase}function lu(e){return e.toPhase??e.status??\"\"}function Un(e){let n=Array.isArray(e)?e[0]:e,t=n instanceof SVGSVGElement?n.firstElementChild:null;if(!(t instanceof SVGElement))throw Error(\"Expected an SVG child\");return t}function yi(e){let n=()=>e.loop(),t=g(()=>n().status===\"running\"),i=g(()=>{let c=new Map;for(let l of e.transitions()){let s=cu(l);c.set(s,(c.get(s)??0)+1)}return c}),f=ru.map((c)=>Un(o`\n \n \n ${()=>{let l=i().get(c.key);return l?String(l):\"\"}}\n \n `)),u=uu.map((c)=>{let l=c.cx-nn/2,s=c.cy-tn/2;return Un(o`\n \"mg-node\"+(n().phase===c.phase&&t()?\" mg-node-active\":\"\")}>\n \n ${c.label}\n \n `)}),r=Un(o`\n \n \n ${()=>n().status}\n \n `);return o`
\n \n \n \n \n \n \n\n ${f}\n ${u}\n ${()=>t()?\"\":r}\n \n\n
\n
Transition history
\n
\n ${()=>e.transitions().slice(-20).reverse().map((c)=>o`
\n ${c.eventType}\n ${c.fromPhase} → ${lu(c)}\n ${xe(c.createdAt)}\n
`)}\n
\n
\n
`}function bi(e){let[n,t]=K(e.query()),i=null,f=(r)=>{let c=r.target.value;if(t(c),i)clearTimeout(i);i=setTimeout(()=>{i=null,e.onChangeQuery(c)},250)};ce(()=>{if(i)clearTimeout(i),i=null}),R(()=>{e.externalNav();let r=H(()=>e.query());if(i!==null)clearTimeout(i),i=null;t(r)});let u=g(()=>{let r=e.counts();return[{label:\"Running\",key:\"running\",value:r.running},{label:\"Completed\",key:\"completed\",value:r.completed},{label:\"Cancelled\",key:\"cancelled\",value:r.cancelled},{label:\"Errored\",key:\"errored\",value:r.errored},{label:\"Stalled\",key:\"stalled\",value:r.stalled}]});return o`
\n ${()=>u().map(({label:r,key:c,value:l})=>{let d=`badge badge-filter${e.statuses().has(c)?\" badge-active\":\"\"}`;return o`e.onToggleStatus(c)}>${r}: ${l}`})}\n \n \n
`}function xi(e){return o`
Last updated: ${new Date(e.generatedAt).toLocaleString()}
`}function ai(e){let n=g(()=>e.entries().map((t)=>{let i=t.loops.some((r)=>r.loop.status===\"running\"),f=we(t.proj),u=ge(e.labels(),t.proj);return o`
e.onSelect(t.proj.projectId)}>\n ${i?o``:\"\"}\n ${u}\n ${t.loops.length}\n
`}));return o`
${()=>n()}
`}function su(e,n){let t=0,i=0,f=0,u=[],r=[];for(let c of e){let l=ge(n,c.proj);for(let s of c.loops){i++,f+=s.bugCount;let d=s.loop.status,x=ze(s.loop);if(d===\"running\")t++,u.push({projectId:c.proj.projectId,label:l,loopName:s.loop.loopName,phase:s.loop.phase||\"\"});r.push({projectId:c.proj.projectId,label:l,loopName:s.loop.loopName,status:d,when:x})}}return r.sort((c,l)=>l.when-c.when),{runningCount:t,loopCount:i,bugCount:f,runningCards:u,recentRows:r.slice(0,8)}}function Oi(e){let n=g(()=>su(e.entries(),e.labels())),t=g(()=>n().runningCount),i=g(()=>n().loopCount),f=g(()=>n().bugCount),u=g(()=>n().runningCards),r=g(()=>n().recentRows);return o`
\n
\n

All repositories

\n
${()=>`${t()} running · ${i()} loops · ${f()} open bugs`}
\n
\n ${()=>u().length>0?o`
\n

Running

\n
\n ${u().map((c)=>o`
e.onOpenLoop(c.projectId,c.loopName)}>\n ${c.label}\n ${c.loopName}\n ${c.phase}\n
`)}\n
\n
`:\"\"}\n ${()=>r().length>0?o`
\n

Recent activity

\n
\n ${r().map((c)=>o`
e.onOpenLoop(c.projectId,c.loopName)}>\n fe(c.status)}>${c.status}\n ${c.label}\n ${c.loopName}\n ${j(c.when)}\n
`)}\n
\n
`:\"\"}\n
`}function du(e){let[n,t]=K(!1),[i,f]=K(\"\"),[u,r]=K(0),c=g(()=>{let b=i().trim().toLowerCase(),C=e.loops();return b?C.filter((V)=>V.name.toLowerCase().indexOf(b)!==-1):C}),l=g(()=>Le(c(),Dt,!1)),s=()=>{if(n())return;f(\"\");let b=l().rows.findIndex((C)=>C.name===e.loopName);r(b<0?0:b),t(!0)},d=()=>{t(!1),f(\"\"),r(0)},x=(b)=>{if(d(),y.blur(),b!==e.loopName)e.onSelect(b)},E=o`n()?\"display:block\":\"display:none\"}\n onmousedown=${(b)=>b.preventDefault()}\n >\n ${()=>l().rows.map((b,C)=>o`\"loop-picker-option\"+(C===u()?\" loop-picker-option-active\":\"\")+(b.name===e.loopName?\" loop-picker-option-current\":\"\")}\n onclick=${()=>x(b.name)}\n >\n ${b.name}\n ${b.whenLabel}\n `)}\n ${()=>l().total===0?o`
No matching loops
`:\"\"}\n ${()=>l().capped?o`
${()=>\"Showing \"+l().rows.length+\" of \"+l().total+\" — type to filter\"}
`:\"\"}\n `,w=()=>{E.querySelector(\".loop-picker-option-active\")?.scrollIntoView?.({block:\"nearest\"})},h=(b)=>{if(b.key===\"ArrowDown\"||b.key===\"ArrowUp\"){if(b.preventDefault(),!n()){s();return}let C=l().rows.length;if(C===0)return;let V=b.key===\"ArrowDown\"?1:-1;r((A)=>(A+V+C)%C),w();return}if(b.key===\"Enter\"){b.preventDefault();let C=l().rows[u()];if(C)x(C.name);return}if(b.key===\"Escape\")b.preventDefault(),d(),y.blur()},y=o`n()?i():e.loopName}\n onfocus=${s}\n oninput=${(b)=>{f(b.target.value),r(0),t(!0)}}\n onkeydown=${h}\n onblur=${d}\n />`;return o`\n ${y}\n {if(b.preventDefault(),n())d(),y.blur();else y.focus()}}>▾\n ${E}\n `}function Ei(e){if(e.loopName)return o`
\n ← ${e.repoLabel}\n /\n ${du({loopName:e.loopName,loops:e.loops,onSelect:e.onSelectLoop})}\n ${()=>{let n=e.loopNav();return n?o`\n \n ${n.index+1} of ${n.total}\n \n `:\"\"}}\n
`;return o`
\n ← Repositories\n /\n ${e.repoLabel}\n ${e.projectDir}\n
`}function Ci(e){let n=[{key:\"loops\",label:\"Loops\",count:e.counts.loops},{key:\"groups\",label:\"Groups\",count:e.counts.groups},{key:\"findings\",label:\"Findings\",count:e.counts.findings},{key:\"plans\",label:\"Plans\",count:e.counts.plans}];return o`
\n ${n.map((t)=>{let f=`section-nav-item${t.key===e.section?\" section-nav-active\":\"\"}`;return o`
e.onNavigate(t.key)}>\n ${t.label}\n ${t.count}\n
`})}\n
`}function qn(e){return o`
\n ${()=>\"Showing \"+e.shown()+\" of \"+e.total()+\" \"+e.noun}\n ${e.onShowAll?o``:\"\"}\n
`}function Zn(e){let n=()=>Gn(e.current(),e.total());return o`\n \"width:\"+n()+\"%\"}>\n ${()=>e.current()}/${()=>e.total()}\n `}var hu={sm:4,md:14,lg:22};function Dn(e){let n=hu[e.variant],t=g(()=>e.spans().filter((u)=>u.phase!==\"\")),i=g(()=>Math.max(0,t().reduce((u,r)=>u+r.durationMs,0))),f=g(()=>e.spans().some((u)=>u.phase===\"\"));return o`
\"phase-bar phase-bar-\"+e.variant+(f()?\" phase-bar-trunc\":\"\")} data-truncated=${()=>f()?\"true\":\"false\"} style=${()=>\"height:\"+n+\"px\"}>\n ${()=>f()?o`\\u22ef`:\"\"}\n ${()=>{let u=t(),r=i();return u.map((c)=>{let l=r>0?c.durationMs/r*100:0,s=\"phase-seg\"+(c.open?\" phase-seg-open\":\"\");return o`
`})}}\n
`}function Xn(e,n){let t=e.loop;return li(e.transitions??[],t.startedAt,t.completedAt,t.completedAt===null?n():0,t.phase)}function Si(e){let[n,t]=K(!1),i=g(()=>Le(e.loops(),Fn,n()));return o`
\n \n \n \n \n \n \n ${()=>i().rows.map((f)=>ou({dashLoop:f,now:e.now,onOpen:e.onOpen}))}\n \n
StatusLoopPhasePhase spanIterSectionsFindingsCostDurationUpdated
\n ${()=>i().capped?qn({shown:()=>i().rows.length,total:()=>i().total,noun:\"loops\",onShowAll:()=>t(!0)}):\"\"}\n
`}function ou(e){let n=()=>e.dashLoop.loop,t=()=>e.dashLoop,i=g(()=>Hn(t().findings)),f=g(()=>Xn(t(),e.now).spans);return o`e.onOpen(e.dashLoop.loop.loopName)}>\n fe(n().status)}>${()=>n().status}\n ${()=>n().loopName}\n ${()=>n().phase}\n ${()=>Dn({spans:f,variant:\"sm\"})}\n ${Zn({current:()=>n().iteration,total:()=>n().maxIterations})}\n ${()=>n().totalSections>0?Zn({current:()=>n().currentSectionIndex,total:()=>n().totalSections}):o``}\n ${()=>{let u=i();if(u.bugs.length===0&&u.warnings.length===0)return o``;return o`\n ${u.bugs.length>0?o`${be(u.bugs.length,\"bug\")}`:\"\"}\n ${u.bugs.length>0&&u.warnings.length>0?\" · \":\"\"}\n ${u.warnings.length>0?o`${be(u.warnings.length,\"warn\")}`:\"\"}\n `}}\n ${()=>t().usage?pe(t().usage.totalCost):o``}\n ${()=>t().duration||\"\"}\n ${()=>j(ze(n()))}\n `}function Ye(e){let n=()=>typeof e.label===\"function\"?e.label():e.label,[t,i]=K(!1),f=()=>i((c)=>!c),u=g(()=>ci(e.src()||\"\")),r=g(()=>u().outline.filter((c)=>c.depth<=3));return o`
\n
\n t()?\"false\":\"true\"}\n onclick=${f}\n onkeydown=${(c)=>{if(c.key!==\"Enter\"&&c.key!==\" \")return;c.preventDefault(),f()}}\n >\n ${()=>t()?\"▸\":\"▾\"}\n

${()=>n()}

\n
\n \"Copy \"+n()+\" as markdown\"}\n onclick=${(c)=>{c.stopPropagation();let l=c.target,s=\"Copy\";navigator.clipboard.writeText(e.src()||\"\").then(()=>{l.textContent=\"Copied!\"},()=>{l.textContent=\"Failed\"}).then(()=>{setTimeout(()=>{l.textContent=s},2000)})}}\n >Copy\n
\n
t()?\"display:none\":\"display:block\"}>\n ${()=>{let c=r();return c.length>=2?yu({items:()=>c}):null}}\n
u().html}>
\n
\n `}function yu(e){let n=(t,i)=>{t.preventDefault();let f=t.currentTarget.closest(\".markdown-body\"),u=f?Array.from(f.querySelectorAll(\"h1,h2,h3,h4,h5,h6\")).find((r)=>r.id===i):document.getElementById(i);if(u&&typeof u.scrollIntoView===\"function\")u.scrollIntoView({behavior:\"smooth\",block:\"start\"})};return o``}function jn(e){return o`
\n
${e.label}
\n
ri(e.value()||\"\")}>
\n
`}function $u(e){let n=e.sec,t=g(()=>!!n.startedAt||!!n.completedAt),i=g(()=>!!n.summaryDone),f=g(()=>!!n.summaryDeviations),u=g(()=>!!n.summaryFollowUps),r=g(()=>!!n.content),c=g(()=>t()||i()||f()||u()||r());return o`
\n ${()=>t()?o`
\n ${()=>n.startedAt?\"Started \"+j(n.startedAt):\"Not started\"}\n ${()=>n.completedAt?o` → Completed ${()=>j(n.completedAt)}`:\"\"}\n
`:\"\"}\n ${()=>i()?jn({label:\"Done\",value:()=>n.summaryDone}):\"\"}\n ${()=>f()?jn({label:\"Deviations\",value:()=>n.summaryDeviations}):\"\"}\n ${()=>u()?jn({label:\"Follow-ups\",value:()=>n.summaryFollowUps}):\"\"}\n ${()=>r()?Ye({label:\"Section Plan\",src:()=>n.content}):\"\"}\n ${()=>c()?\"\":o`
No details captured for this section yet.
`}\n
`}function wu(e){let n=e.sec,t=g(()=>ii(n.startedAt,n.completedAt));return o`
\"section-list-row section-item-\"+n.status} onclick=${e.onOpen}>\n #${n.sectionIndex}\n ${()=>n.title}\n Tn(n.status)}>${()=>n.status}\n ${()=>t()?o`${()=>t()}`:\"\"}\n ${()=>n.attempts>0?o`${()=>n.attempts} attempts`:\"\"}\n \n
`}function gu(e){let[n,t]=K(null),i=g(()=>{let f=n();if(f===null)return null;return e.sections().find((u)=>u.sectionIndex===f)??null});return o`
\n

Sections

\n ${()=>{let f=i();if(f)return o`
\n
t(null)}>← Back to sections
\n
\n #${f.sectionIndex}\n ${()=>f.title}\n Tn(f.status)}>${()=>f.status}\n
\n ${$u({sec:f})}\n
`;return o`
\n ${e.sections().map((u)=>wu({sec:u,onOpen:()=>t(u.sectionIndex)}))}\n
`}}\n
`}function Z(e){return o`
\n ${e.label}\n ${()=>e.value()}\n
`}function $i(e){let n=()=>Gn(e.current(),e.total());return o`
\n
\n ${e.label}\n ${()=>e.current()} / ${()=>e.total()}\n
\n
\n
\"width:\"+n()+\"%\"}>
\n
\n
`}function bu(e){let n=()=>e.dashLoop.loop,t=()=>e.dashLoop,i=g(()=>!!n().completedAt),f=g(()=>!!t().duration),u=g(()=>n().totalSections>0),r=g(()=>!!n().terminationReason),c=g(()=>Mt(e.split())),l=g(()=>!!e.dashLoop.usage);return o`
\n
\"ldh-findings ldh-findings-\"+c()}>\n ${()=>{let s=e.split();if(s.bugs.length===0&&s.warnings.length===0)return\"No findings\";let d=[];if(s.bugs.length>0)d.push(be(s.bugs.length,\"bug\"));if(s.warnings.length>0)d.push(be(s.warnings.length,\"warning\"));return d.join(\" · \")}}\n
\n
\n fe(n().status)}>${()=>n().status}\n

${()=>n().loopName}

\n ${()=>n().phase}\n
\n\n
\n ${Z({label:\"Started\",value:()=>j(n().startedAt)})}\n ${()=>i()?Z({label:\"Completed\",value:()=>j(n().completedAt)}):\"\"}\n ${()=>f()?Z({label:\"Duration\",value:()=>t().duration||\"\"}):\"\"}\n ${Z({label:\"Iteration\",value:()=>n().iteration+\" / \"+n().maxIterations})}\n ${()=>u()?Z({label:\"Section\",value:()=>n().currentSectionIndex+\" / \"+n().totalSections}):\"\"}\n ${()=>l()?Z({label:\"Messages\",value:()=>String(e.dashLoop.usage.totalMessageCount)}):\"\"}\n ${Z({label:\"Execution model\",value:()=>n().executionModel??\"—\"})}\n ${Z({label:\"Auditor model\",value:()=>{let s=n().auditorModel??\"—\",d=n().auditorFallbackIndex??0;return d>0?`${s} (fallback ${d})`:s}})}\n ${Z({label:\"Branch\",value:()=>n().worktreeBranch??\"—\"})}\n ${Z({label:\"Sandbox\",value:()=>n().sandbox?\"on\":\"off\"})}\n ${Z({label:\"Kind\",value:()=>n().kind??\"—\"})}\n ${Z({label:\"Audits\",value:()=>String(n().auditCount)})}\n ${Z({label:\"Errors\",value:()=>String(n().errorCount)})}\n
\n\n
\n ${$i({label:\"Iterations\",current:()=>n().iteration,total:()=>n().maxIterations})}\n ${()=>u()?$i({label:\"Sections\",current:()=>n().currentSectionIndex,total:()=>n().totalSections}):\"\"}\n
\n\n ${()=>r()?o`
\"ldh-banner ldh-banner-\"+n().status}>${()=>n().terminationReason}
`:\"\"}\n
`}function xu(e){let n=()=>e.usage(),t=g(()=>ei(n())),i=g(()=>ti(n())),f=g(()=>i().length>0),u=g(()=>ni(n()));return o`
\n

Usage

\n\n
\n
Role split
\n
\n ${()=>u().map((r)=>o`
r.role}>\n
\n r.role}>${r.role}\n ${()=>pe(r.cost)}\n
\n
\n
\"width:\"+r.pct+\"%\"}>
\n
\n
${()=>r.messageCount+\" msg · \"+We(r.tokens)+\" tok\"}
\n
`)}\n
\n
\n\n
\n
Token composition
\n
\n ${()=>t().map((r)=>r.pct>0?o`
`:\"\")}\n
\n
\n ${()=>t().map((r)=>o`
\n \n ${r.label}\n ${We(r.value)}\n
`)}\n
\n
\n\n ${()=>f()?o`
\n
Cost by model
\n
\n ${()=>i().map((r)=>o`
\n
\n ${r.model}\n ${pe(r.cost)}\n
\n
\n
\n
\n
\n ${We(r.inputTokens)} in / ${We(r.outputTokens)} out · ${r.messageCount} msg\n
\n
`)}\n
\n
`:\"\"}\n `}function wi(e){try{return JSON.parse(e)}catch{return[]}}function gi(e){return\"#\"+e.index+\" \"+e.title}function au(e){let n=e.amendment,t=g(()=>wi(n.sectionsBefore)),i=g(()=>wi(n.sectionsAfter));return o`
\n
\n ${()=>xe(n.createdAt)}\n applied @ section ${()=>n.appliedAtSection}\n ${()=>n.source}\n ${()=>n.rationale}\n ${()=>e.expanded()?\"▾\":\"▸\"}\n
\n
e.expanded()?\"display:block\":\"display:none\"}>\n
\n ${()=>t().length>0?o`
\n
Before
\n ${()=>t().map((f)=>o`
${()=>gi(f)}
`)}\n
`:\"\"}\n ${()=>i().length>0?o`
\n
After
\n ${()=>i().map((f)=>o`
${()=>gi(f)}
`)}\n
`:\"\"}\n
\n
\n
`}function Ou(e){let n=()=>e.amendments(),[t,i]=K(null);return o`
\n

Plan amendments

\n
\n ${()=>n().map((f)=>au({amendment:f,expanded:()=>t()===f.id,onToggle:()=>i(t()===f.id?null:f.id)}))}\n
\n
`}var Eu={overview:\"Overview\",timeline:\"Timeline\",sections:\"Sections\",findings:\"Findings\",plan:\"Plan\",usage:\"Usage\"};function Cu(e){return o`
\n ${()=>e.tabs().map((n)=>{let t=Eu[n];return o`\"tab-item\"+(e.activeTab()===n?\" tab-active\":\"\")} data-tab=\"${n}\" onclick=${()=>e.onSelect(n)}>${t}`})}\n
`}function Su(e){let n=()=>e.dashLoop,t=()=>n().loop,i=g(()=>!!n().goal),f=g(()=>n().postActionReport??t().completionSummary??null),u=g(()=>n().postActionReport?\"Post-Action Report\":\"Completion Summary\"),r=g(()=>!!f()),c=g(()=>!!n().lastAuditResult),l=g(()=>Xn(n(),e.now));return o`
\n ${()=>Dn({spans:()=>l().spans,variant:\"md\"})}\n ${()=>i()?Ye({label:\"Goal\",src:()=>n().goal}):\"\"}\n ${bu({dashLoop:e.dashLoop,split:e.split})}\n ${()=>r()?Ye({label:u,src:()=>f()}):\"\"}\n ${()=>c()?Ye({label:\"Last Audit Result\",src:()=>n().lastAuditResult}):\"\"}\n
`}function Au(e){let n=e.ev.transition,t=e.ev.elapsedMs,i=t===null?null:Math.floor(t/1000),f=n.toPhase===null?n.status??\"\":n.toPhase;return o`
\n ${xe(n.createdAt)}\n ${n.fromPhase} → ${f}\n ${n.transitionKind}\n iter ${n.iteration}\n ${()=>n.sectionIndex===null?\"—\":\"sect \"+(n.sectionIndex+1)}\n ${()=>i===null?\"—\":i>0?Ie(i):\"0s\"}\n
`}function Nu(e){let n=()=>e.dashLoop,t=()=>n().loop,i=()=>n().transitions??[],f=g(()=>Xn(n(),e.now)),u=g(()=>f().spans),r=g(()=>f().truncated),c=g(()=>si(u())),l=g(()=>u().filter((y)=>y.phase!==\"\").reduce((y,b)=>y+b.durationMs,0)),s=g(()=>{let y=c();return Object.keys(y).filter((b)=>b!==\"\").sort((b,C)=>y[C]-y[b])}),d=g(()=>di(i(),t().startedAt,r())),[x,E]=K(!1),w=g(()=>{let y=d();return x()?y:y.slice(0,20)}),h=g(()=>x()?0:Math.max(0,d().length-20));return o`
\n ${yi({loop:()=>t(),transitions:i})}\n ${()=>Dn({spans:u,variant:\"lg\"})}\n ${()=>r()?o`
Earlier history was truncated by the 100-row fetch window.
`:\"\"}\n\n
\n ${()=>s().map((y)=>{let b=c()[y],C=l()>0?Math.round(b/l()*100):0;return o`
\n \n ${y}\n \n ${()=>b>=1000?Ie(Math.floor(b/1000)):b+\"ms\"} · ${C}%\n
`})}\n
\n\n
\n ${()=>w().map((y)=>Au({ev:y}))}\n ${()=>h()>0?o`
E(!0)}>[ ${()=>h()} earlier events ]
`:\"\"}\n
\n
`}function Pu(e){return gu({sections:()=>e.dashLoop.sections})}function Vu(e){let n=[],t=new Map;for(let i of e){let f=i.sectionIndex??null;if(!t.has(f))t.set(f,[]),n.push(f);t.get(f).push(i)}return n.sort((i,f)=>{if(i===null&&f===null)return 0;if(i===null)return 1;if(f===null)return-1;return i-f}),n.map((i)=>({key:i,label:i===null?\"cross-section\":\"sect \"+(i+1),rows:t.get(i)}))}function Ai(e){return o`
\n ${e.rows.map((n)=>o`
\"finding finding-\"+n.severity}>\n ${()=>xe(n.createdAt)}\n ${()=>mt(n)}\n
`)}\n
`}function Ni(e){let n=()=>me({projectId:e.projectId(),section:\"loops\",loopName:e.loopName,tab:\"overview\",groupId:null,statuses:[],query:\"\"});return o`{if(t.defaultPrevented||t.button!==0||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;t.preventDefault(),e.onOpen(e.loopName)}}>${e.loopName}`}function ku(e){let n=()=>e.dashLoop,t=g(()=>n().findings.length>0),i=g(()=>Vu(n().findings));return o`
\n ${()=>t()?i().map((f)=>o`
f.key===null?\"cross\":String(f.key)}>\n
${f.label}
\n ${Ai({rows:f.rows})}\n
`):o`
No findings recorded for this loop.
`}\n
`}function Iu(e){let n=()=>e.dashLoop,t=g(()=>n().hasPlan),i=g(()=>!!n().amendments&&n().amendments.length>0);return o`
\n ${()=>t()?Ye({label:\"Plan\",src:()=>n().plan}):o`
No plan recorded for this loop.
`}\n ${()=>i()?Ou({amendments:()=>n().amendments}):\"\"}\n
`}function Ku(e){let n=()=>e.dashLoop,t=g(()=>!!n().usage);return o`
\n ${()=>t()?xu({usage:()=>n().usage}):o`
No usage recorded for this loop.
`}\n
`}function zu(e){switch(e.tab){case\"overview\":return Su({dashLoop:e.dashLoop,split:e.split,now:e.now});case\"timeline\":return Nu({dashLoop:e.dashLoop,now:e.now});case\"sections\":return Pu({dashLoop:e.dashLoop});case\"findings\":return ku({dashLoop:e.dashLoop});case\"plan\":return Iu({dashLoop:e.dashLoop});case\"usage\":return Ku({dashLoop:e.dashLoop})}}function Wu(e){let n=g(()=>e.tabs().includes(e.tab)&&e.activeTab()===e.tab),t=De(),i=o`
n()?\"display:block\":\"display:none\"}>
`,f=!1;return R(()=>{if(!n()||f)return;f=!0;let u=on(t,()=>zu(e));if(u)i.appendChild(u)}),i}function Yu(e){return o`
\n ${vn.map((n)=>Wu({tab:n,dashLoop:e.dashLoop,split:e.split,now:e.now,tabs:e.tabs,activeTab:e.activeTab}))}\n
`}function Pi(e){let n=()=>e.dashLoop,t=g(()=>Hn(n().findings)),i=g(()=>en(n())),f=g(()=>{let u=e.routeTab();return i().includes(u)?u:\"overview\"});return o`
\n
\n ${Cu({tabs:i,activeTab:f,onSelect:e.onSelectTab})}\n ${Yu({dashLoop:e.dashLoop,split:t,now:e.now,tabs:i,activeTab:f})}\n
\n
`}function Jn(){return o`
No loops match the current filters.
`}var vu=new Set([\"completed\",\"cancelled\",\"errored\"]);function Vi(e){let n=e.length;return{completed:e.filter((i)=>i.stage===\"completed\").length,total:n}}function ki(e,n){let t=vu.has(n);return e+(t?` ${e}-terminal`:` ${e}-active`)}function Ii(e){return o`
\n

Groups

\n ${()=>{let n=e.groups();if(n.length===0)return o`
No feature groups for this repo.
`;return o`
\n ${n.map((t)=>{let i=t.group,f=g(()=>Vi(t.features));return o`
ki(\"group-row\",i.status)} data-group-status=${()=>i.status} onclick=${()=>e.onOpen(i.groupId)}>\n fe(i.status)}>${()=>i.status}\n ${()=>i.title}\n ${Zn({current:()=>f().completed,total:()=>f().total})}\n max ${()=>i.maxConcurrent}\n \n Created ${()=>j(i.createdAt)}\n ${()=>i.completedAt?o`Completed ${()=>j(i.completedAt)}`:\"\"}\n \n
`})}\n
`}}\n
`}function Ki(e){return o`
\n
← Back to groups
\n ${()=>{let n=e.group();if(!n)return o`
Group not found.
`;let t=n.group,i=g(()=>Vi(n.features));return o`
\n
ki(\"group-header\",t.status)} data-group-status=${()=>t.status}>\n
\n fe(t.status)}>${()=>t.status}\n

${()=>t.title}

\n max ${()=>t.maxConcurrent}\n
\n
\n ${()=>\"Features \"+i().completed+\"/\"+i().total}\n Created ${()=>j(t.createdAt)}\n ${()=>t.completedAt?o`Completed ${()=>j(t.completedAt)}`:\"\"}\n ${()=>t.error?o`${()=>t.error}`:\"\"}\n
\n ${()=>t.prdPreview?o`
\n

PRD preview

\n
${()=>t.prdPreview||\"\"}
\n
`:\"\"}\n
\n
\n ${n.features.map((f)=>o`
f.stage}>\n #${()=>f.featureIndex}\n ${()=>f.title}\n Jt(f.stage)}>${()=>f.stage}\n ${()=>f.attempts} attempts\n ${()=>!!f.loopName&&e.loopNames().has(f.loopName)?Ni({className:\"feature-loop-link\",projectId:e.projectId,loopName:f.loopName,onOpen:e.onOpenLoop}):\"\"}\n ${()=>f.error?o`${()=>f.error}`:\"\"}\n
`)}\n
\n
`}}\n
`}function Fu(e,n,t){let i=[],f=0,u=t?1/0:n;for(let r of e){if(r.findings.length===0)continue;if(f+=r.findings.length,u<=0)continue;let c=Math.min(u,r.findings.length);i.push({loopName:r.loop.loopName,rows:r.findings.slice(0,c)}),u-=c}return{groups:i,total:f,capped:!t&&f>n}}function zi(e){let[n,t]=K(!1),i=g(()=>Fu(e.loops(),qt,n()));return o`
\n

Findings ${()=>i().total}

\n ${()=>i().capped?qn({shown:()=>i().groups.reduce((f,u)=>f+u.rows.length,0),total:()=>i().total,noun:\"findings\",onShowAll:()=>t(!0)}):\"\"}\n ${()=>{let f=i().groups;if(f.length===0)return o`
No findings recorded for this repo.
`;return o`
\n ${f.map((u)=>o`
u.loopName}>\n
\n ${Ni({className:\"findings-loop-link\",projectId:e.projectId,loopName:u.loopName,onOpen:e.onOpenLoop})}\n ${()=>u.rows.length}\n
\n ${Ai({rows:u.rows})}\n
`)}\n
`}}\n
`}function Wi(e){let[n,t]=K(!1),i=g(()=>e.loops().filter((u)=>u.hasPlan)),f=g(()=>Le(i(),Fn,n()));return o`
\n

Plans ${()=>f().total}

\n ${()=>f().capped?qn({shown:()=>f().rows.length,total:()=>f().total,noun:\"plans\",onShowAll:()=>t(!0)}):\"\"}\n ${()=>{let u=f().rows;if(u.length===0)return o`
No plans recorded for this repo.
`;return o`
\n ${u.map((r)=>o`
e.onOpenLoop(r.loop.loopName)}>\n fe(r.loop.status)}>${()=>r.loop.status}\n ${()=>r.loop.loopName}\n \n ${()=>be(r.findings.length,\"finding\")}\n \n iter ${()=>r.loop.iteration}/${()=>r.loop.maxIterations}\n
`)}\n
`}}\n
`}function Yi(){let[e,n]=zt({generatedAt:0,projects:[]}),[t,i]=K(!1),f=Me(typeof location<\"u\"?location.hash:\"\"),[u,r]=K(f.projectId),[c,l]=K(f.section),[s,d]=K(f.loopName),[x,E]=K(f.tab),[w,h]=K(f.groupId),[y,b]=K(f.statuses,{equals:_n}),[C,V]=K(f.query),A=g(()=>({projectId:u(),section:c(),loopName:s(),tab:x(),groupId:w(),statuses:y(),query:C()})),[S,P]=K(null),[I,z]=K(\"recent\"),[G,F]=K(0),[m,p]=K(Date.now()),ve={current:\"\"},ae={current:!1},Fi=()=>{let $=H(A),a=new URLSearchParams;if($.projectId)a.set(\"project\",$.projectId);if($.loopName)a.set(\"loop\",$.loopName);let N=a.toString();return N?\"/api/data?\"+N:\"/api/data\"},Fe=0,ue={url:null,gen:0},fn=async()=>{let $=Fi();if(ue.url===$)return;let a=++Fe;ue.url=$,ue.gen=a;try{let N=await fetch($,{cache:\"no-store\"});if(a!==Fe)return;let Y=await N.json();if(a!==Fe)return;let q=Rt(Y);if(q!==ve.current)ve.current=q,n(Wt(Y,{key:\"id\"})),i(!0),P(null);p(Date.now())}catch(N){if(a!==Fe)return;let Y=N instanceof Error?N.message:String(N);P(\"Failed to load dashboard data: \"+Y)}finally{if(ue.gen===a)ue.url=null,ue.gen=0}},Qn=($)=>{Ne(()=>{r($.projectId),l($.section),d($.loopName),E($.tab),h($.groupId),b($.statuses),V($.query)})},U=($)=>{Ne(()=>{let a=$.projectId!==void 0&&$.projectId!==u();if($.projectId!==void 0)r($.projectId);if($.section!==void 0)l($.section);if($.loopName!==void 0)d($.loopName);if($.tab!==void 0)E($.tab);if($.groupId!==void 0)h($.groupId);if($.statuses!==void 0)b($.statuses);if($.query!==void 0)V($.query);if(a)b([]),V(\"\")})},_i=($)=>{U({tab:$})},_e=($)=>{U({loopName:$,section:\"loops\",groupId:null,statuses:[],query:\"\",tab:\"overview\"})},Ti=($)=>{let a=new Set(y());if(a.has($))a.delete($);else a.add($);U({statuses:[...a]})},Hi=($)=>{U({query:$})},Gi=($)=>{z($)},Bn=g(()=>new Set(y())),Te=g(()=>Lt(e.projects.map(we))),He=g(()=>{if(!t()||e.projects.length===0)return[];let $=Bn(),a=C(),N=Te(),Y=[],q=a.trim().toLowerCase();for(let Ee of e.projects){let cn=[],pn=ge(N,Ee);for(let et of Ee.loops)if(Qt(et.loop,$,a,pn))cn.push(et);let rf=(Ee.groups?.length??0)>0;if(cn.length>0)Y.push({proj:Ee,loops:cn});else if(rf&&(!q||pn.toLowerCase().indexOf(q)!==-1))Y.push({proj:Ee,loops:[]})}return Y}),Rn=g(()=>u()===null),Ui=g(()=>c()===\"groups\"),Ln=g(()=>c()===\"loops\"),ji=g(()=>c()===\"findings\"),Zi=g(()=>c()===\"plans\"),Mn=g(()=>{let $=He(),a=u();if(!a)return null;return $.find((N)=>N.proj.projectId===a)??null}),L=g(()=>{let $=u();if(!$)return null;return e.projects.find((a)=>a.projectId===$)??null}),un=g(()=>{let $=u(),a=s();if(!$||!a)return null;let N=L();if(!N)return null;return N.loops.find((Y)=>Y.loop.loopName===a)??null}),qi=g(()=>{let $=L(),a=$?.groups?.length??0;if(!$)return{loops:0,groups:a,findings:0,plans:0};let N=0,Y=0;for(let q of $.loops)if(N+=q.findings.length,q.hasPlan)Y++;return{loops:$.loops.length,groups:a,findings:N,plans:Y}}),Oe=g(()=>{let $=L();if(!$)return[];return $.loops.map((a)=>{let N=ze(a.loop);return{name:a.loop.loopName,when:N,whenLabel:j(N)}}).sort((a,N)=>N.when-a.when||a.name.localeCompare(N.name))}),rn=g(()=>{let $=s();if(!$)return-1;return Oe().findIndex((a)=>a.name===$)}),Di=g(()=>{let $=rn();if($<0)return null;let a=Oe();if(a.length<=1)return null;return{index:$,total:a.length,onPrev:()=>{let N=rn(),Y=Oe();if(N>0)U({loopName:Y[N-1].name})},onNext:()=>{let N=rn(),Y=Oe();if(N$===a||!!$&&!!a&&$.index===a.index&&$.total===a.total}),Xi=g(()=>{let $=L(),a={running:0,completed:0,cancelled:0,errored:0,stalled:0};if(!$)return a;for(let N of $.loops){let Y=N.loop.status;if(Y===\"running\")a.running++;else if(Y===\"completed\")a.completed++;else if(Y===\"cancelled\")a.cancelled++;else if(Y===\"errored\")a.errored++;else if(Y===\"stalled\")a.stalled++}return a}),Ji=g(()=>{let $=s();if(!u())return\"\";let a=L();if(!a)return\"\";let N=we(a),Y=ge(Te(),a);return H(()=>Ei({repoLabel:Y,projectDir:N,loopName:$,loops:Oe,loopNav:Di,onSelectLoop:(q)=>U({loopName:q}),onBackToRepos:()=>U({projectId:null,loopName:null,section:\"loops\"}),onBackToRepo:()=>U({loopName:null})}))}),Qi=g(()=>{let $=u(),a=s();if(!$||a)return\"\";return Ci({section:c(),counts:qi(),onNavigate:(N)=>U({section:N,loopName:null,groupId:null})})}),Bi=g(()=>{return u()!==null&&s()===null&&c()===\"loops\"}),Ri=g(()=>{if(!Bi())return\"\";return H(()=>bi({counts:Xi,statuses:Bn,query:C,sortMode:I,onToggleStatus:Ti,onChangeQuery:Hi,onChangeSort:Gi,externalNav:G}))}),Li=g(()=>{if(!Ln())return\"\";let $=un();return $?Pi({dashLoop:$,now:m,routeTab:x,onSelectTab:_i}):\"\"}),Mi=g(()=>{let $=Mn();return $?Bt($.loops,I()):[]},[],{equals:_n}),mi=g(()=>{if(!Ln())return\"\";if(un())return\"\";if(!u())return\"\";return o`
\n ${Si({loops:Mi,now:m,onOpen:($)=>_e($)})}\n ${()=>Mn()?\"\":Jn()}\n
`}),pi=g(()=>{if(c()!==\"groups\"||!w())return null;let $=L();if(!$)return null;return $.groups.find((a)=>a.group.groupId===w())??null}),ef=g(()=>{let $=L();if(!$)return new Set;return new Set($.loops.map((a)=>a.loop.loopName))}),nf=g(()=>{if(!Ui())return\"\";if(!u())return\"\";if(w())return Ki({group:pi,loopNames:ef,projectId:()=>u(),onBack:()=>U({groupId:null}),onOpenLoop:_e});return Ii({groups:()=>L()?.groups??[],onOpen:(a)=>U({groupId:a})})}),mn=g(()=>{let $=L();return $?$.loops:[]}),tf=g(()=>{if(!ji())return\"\";if(!u())return\"\";return zi({loops:mn,projectId:()=>u(),onOpenLoop:_e})}),ff=g(()=>{if(!Zi())return\"\";if(!u())return\"\";return Wi({loops:mn,onOpenLoop:_e})});R(()=>{if(!t())return;let $=u();if($===null)return;if(!e.projects.some((N)=>N.projectId===$))r(null),d(null),l(\"loops\"),b([]),V(\"\")}),R(()=>{let $=u(),a=s();if(!$||!a)return;let N=L();if(!N)return;if(!N.loops.some((q)=>q.loop.loopName===a))d(null)}),R(()=>{let $=s(),a=x();if(!u()||!$||a===\"overview\")return;let N=un();if(!N)return;if(!en(N).includes(a))E(\"overview\")}),R(()=>{if(!t())return;Xt(me(A()),ae)});let uf=g(()=>(u()??\"\")+\"\\x00\"+(s()??\"\"));return R(dt(uf,()=>{fn()},{defer:!0})),ht(()=>{Qn(Me(location.hash)),fn();let $=setInterval(()=>{fn()},5000),a=()=>{if(ae.current){ae.current=!1;return}Qn(Me(location.hash)),F((N)=>N+1)};window.addEventListener(\"hashchange\",a),ce(()=>{clearInterval($),window.removeEventListener(\"hashchange\",a)})}),o`
\n

Forge Dashboard

\n\n ${()=>{let $=S();return $?o`
${$}
`:\"\"}}\n\n ${()=>{if(!t())return\"\";return o`
\n ${xi({generatedAt:e.generatedAt})}\n
`}}\n\n ${()=>{if(!t())return\"\";if(Rn()&&He().length===0)return Jn();return o`
\n ${()=>{if(Rn())return o`
\n ${ai({entries:()=>He(),labels:()=>Te(),onSelect:($)=>U({projectId:$,loopName:null,section:\"loops\",groupId:null})})}\n ${Oi({entries:()=>He(),labels:()=>Te(),onOpenLoop:($,a)=>U({projectId:$,loopName:a,section:\"loops\",groupId:null,statuses:[],query:\"\",tab:\"overview\"})})}\n
`;return o`
\n ${Ji}\n ${Ri}\n ${Qi}\n ${mi}\n ${Li}\n ${nf}\n ${tf}\n ${ff}\n
`}}\n
`}}\n
`}var vi=document.getElementById(\"forge-app-root\");if(vi)St(Yi,vi);\n"; +export const DASHBOARD_APP_SOURCE_HASH: string = "beccf5e5f1adcd49650bb48fc86e208ce841e03af55b16f402440e54e5b1d293"; diff --git a/src/dashboard/app/components.ts b/src/dashboard/app/components.ts index ac4656a32..29b10e62a 100644 --- a/src/dashboard/app/components.ts +++ b/src/dashboard/app/components.ts @@ -768,7 +768,11 @@ function LoopDetailHeader(props: { ${() => (hasSectionsTotal() ? LoopDetailStat({ label: 'Section', value: () => lp().currentSectionIndex + ' / ' + lp().totalSections }) : '')} ${() => (hasUsage() ? LoopDetailStat({ label: 'Messages', value: () => String(props.dashLoop.usage!.totalMessageCount) }) : '')} ${LoopDetailStat({ label: 'Execution model', value: () => lp().executionModel ?? '—' })} - ${LoopDetailStat({ label: 'Auditor model', value: () => lp().auditorModel ?? '—' })} + ${LoopDetailStat({ label: 'Auditor model', value: () => { + const base = lp().auditorModel ?? '—' + const fallbackIndex = lp().auditorFallbackIndex ?? 0 + return fallbackIndex > 0 ? `${base} (fallback ${fallbackIndex})` : base + }})} ${LoopDetailStat({ label: 'Branch', value: () => lp().worktreeBranch ?? '—' })} ${LoopDetailStat({ label: 'Sandbox', value: () => (lp().sandbox ? 'on' : 'off') })} ${LoopDetailStat({ label: 'Kind', value: () => lp().kind ?? '—' })} diff --git a/src/hooks/watchdog.ts b/src/hooks/watchdog.ts index 1dd661455..81c874b21 100644 --- a/src/hooks/watchdog.ts +++ b/src/hooks/watchdog.ts @@ -1,5 +1,6 @@ import type { LoopService, LoopState, TerminationReason } from '../loop' import { classifyProviderLimit } from '../loop/provider-limit' +import { isAuditorPhase } from '../utils/loop-helpers' import type { Logger } from '../types' import type { ForgeClient } from '../client/port' @@ -57,6 +58,8 @@ export function createLoopWatchdog(input: { logger: Logger recover(loopName: string, state: LoopState, context: LoopWatchdogRecoveryContext): Promise terminate(loopName: string, state: LoopState, reason: TerminationReason): Promise + /** Routes an auditor provider limit into the fallback chain. Returns true when absorbed (do not terminate), false when the caller must terminate. */ + handleAuditorProviderLimit?: (loopName: string, limitReason: string) => Promise /** Ancestor-aware session→loop resolver for child/subagent sessions. Falls back to loopService.resolveLoopName when absent. */ resolveSessionLoopName?: (sessionId: string) => Promise statusRetryAttempts?: number @@ -216,6 +219,14 @@ export function createLoopWatchdog(input: { // async status poll and ancestor-resolution window. const freshState = input.loopService.getActiveState(loopName) if (!freshState?.active) return + if (input.handleAuditorProviderLimit && isAuditorPhase(freshState.phase)) { + const absorbed = await input.handleAuditorProviderLimit(loopName, limitReason) + if (absorbed) { + resetActivity(loopName, 'status:retry') + input.logger.debug(`Loop watchdog: auditor provider limit absorbed via fallback for ${loopName}, resetting timer`) + return + } + } await input.terminate(loopName, freshState, { kind: 'provider_limit', message: limitReason }) return } diff --git a/src/loop/runtime.ts b/src/loop/runtime.ts index 058ab360f..3bf9d2284 100644 --- a/src/loop/runtime.ts +++ b/src/loop/runtime.ts @@ -13,7 +13,7 @@ import type { LoopSessionUsageRepo } from '../storage/repos/loop-session-usage-r import type { LoopTransitionsRepo } from '../storage/repos/loop-transitions-repo' import type { PlanAmendmentsRepo } from '../storage/repos/plan-amendments-repo' import { createLoopWatchdog, type LoopWatchdogStallInfo, type LoopWatchdogRecoveryContext } from '../hooks/watchdog' -import { resolveLoopModel, resolveLoopAuditorChoice, buildAuditorModelChain, nextAuditorFallbackIndex } from '../utils/loop-helpers' +import { resolveLoopModel, resolveLoopAuditorChoice, buildAuditorModelChain, nextAuditorFallbackIndex, isAuditorPhase, usageRoleForPhase } from '../utils/loop-helpers' import { parseModelString } from '../utils/model-fallback' import type { createSandboxManager } from '../sandbox/manager' // worktree-completion imports moved to hooks/loop.ts (termination side-effects) @@ -412,8 +412,8 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { phase: string, errorSignal?: { name?: string; message?: string; statusCode?: number } | null, ): Promise<{ assistantErrorDetected: boolean; currentState: LoopState } | null> { - // `null` means the caller must `return` without continuing: either the loop - // was terminated or the audit was re-dispatched on a fallback auditor model. + // `null` means the caller must `return` without continuing: the loop was + // terminated. if (!assistantError) { return { assistantErrorDetected: false, currentState } } @@ -422,7 +422,6 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { const limitReason = classifyProviderLimit(errorSignal ?? {}) if (limitReason) { - if (await handleAuditorProviderLimit(loopName, limitReason)) return null logger.error(`Loop: provider limit detected in ${phase} assistant error for ${loopName}: ${limitReason}, terminating`) await terminateLoop(loopName, currentState, { kind: 'provider_limit', message: limitReason }) return null @@ -798,7 +797,7 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { if (queue.some(entry => entry.sessionId === sessionId)) return // Determine role and fallback model at queue time - const role: 'code' | 'auditor' = phase && (phase === 'auditing' || phase === 'final_auditing') ? 'auditor' : 'code' + const role = usageRoleForPhase(phase) const fallbackModel = phase && state ? getFallbackModelForSession(state, phase) : undefined queue.push({ sessionId, role, fallbackModel, directory }) @@ -868,6 +867,7 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { } idleRetryAttempts.delete(loopName) codingLaunchRecoveryAttempts.delete(loopName) + coalescedLimitSessions.delete(loopName) clearPromptPending(loopName, logger) clearPromptInFlight(loopName) @@ -894,13 +894,17 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { // Clean up session→loop reverse index for this loop for (const [sid, ln] of sessionToLoop) { - if (ln === loopName) sessionToLoop.delete(sid) + if (ln === loopName) { + sessionToLoop.delete(sid) + internalFallbackAborts.delete(sid) + } } + internalFallbackAborts.delete(sessionId) sessionToLoop.delete(sessionId) // Capture usage for the final active session before termination const fallbackModel = getFallbackModelForSession(state, state.phase) - const role: 'code' | 'auditor' = state.phase === 'auditing' || state.phase === 'final_auditing' ? 'auditor' : 'code' + const role = usageRoleForPhase(state.phase) await captureLoopSessionUsage({ loopName, sessionId: state.sessionId, @@ -948,10 +952,6 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { } } - function isAuditorPhase(phase: LoopState['phase']): boolean { - return phase === 'auditing' || phase === 'final_auditing' - } - /** * Abort the current audit session (it may be mid-retry when the provider limit * arrives via `session.status`), settle, then re-dispatch the audit prompt on the @@ -1043,7 +1043,7 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { // failed prompt are absorbed above rather than advancing again. coalescedLimitSessions.set(loopName, state.sessionId) - state = loopService.getActiveState(loopName) + state = { ...state, auditorFallbackIndex: committed } if (!state?.active || !isAuditorPhase(state.phase)) return true logger.log(`Loop: auditor provider limit for ${loopName} (${limitReason}) → auditor fallback #${committed}`) @@ -1244,6 +1244,8 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { logger, recover: recoverWatchdogStall, terminate: tryTerminateLoop, + handleAuditorProviderLimit: (loopName: string, limitReason: string) => + withStateLock(loopName, () => handleAuditorProviderLimit(loopName, limitReason)), resolveSessionLoopName, }) @@ -1755,6 +1757,8 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { currentState = resetErrorCountIfNeeded(loopName, currentState, assistantErrorDetected, 'auditing') if (!assistantErrorDetected) { + loopService.resetAuditorFallbackIndex(loopName) + currentState = { ...currentState, auditorFallbackIndex: 0 } const newAuditCount = (currentState.auditCount ?? 0) + 1 logger.log(`Loop audit ${newAuditCount} at iteration ${currentState.iteration ?? 0}`) @@ -2042,6 +2046,8 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { currentState = resetErrorCountIfNeeded(loopName, currentState, assistantErrorDetected, 'final_auditing') if (!assistantErrorDetected) { + loopService.resetAuditorFallbackIndex(loopName) + currentState = { ...currentState, auditorFallbackIndex: 0 } const hasOutstandingBugs = loopService.hasOutstandingFindings(loopName, 'bug') const finalAuditEvent: TransitionEvent = { type: hasOutstandingBugs ? 'final-audit-dirty' : 'final-audit-clean' } @@ -2422,29 +2428,30 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { // its pre-lock gate and be suppressed, stalling the loop with no phase // run. Busy proves the prompt started, so its idle is always legitimate. // - // The in-flight and coalesced-limit markers are still cleared under the - // lock: the internal-abort counter and duplicate provider-limit signals - // for the same failed prompt are queued with `withStateLock`, so an - // unlocked cleanup would clear the guard before the queued handler - // consumed it — treating the intentional abort as an audit failure or - // re-advancing the fallback index. Serializing that boundary here means - // the earlier-queued lifecycle event is always processed first. + // The in-flight marker is cleared synchronously as well: it is a + // membership check, not a guard against a queued lifecycle event, so + // clearing it immediately on this busy is always safe. + // + // The state lock is taken only when a coalesced provider-limit marker is + // present for this exact session. `busy` is the highest-frequency loop + // event and awaits the shared event pipeline, so we avoid paying the lock + // on every occurrence. The lock is required only to serialize the + // coalesced-limit clear against the queued limit handler: duplicate + // provider-limit signals for the same failed prompt are queued with + // `withStateLock`, so an unlocked cleanup would clear the guard before + // the queued handler consumed it, re-advancing the fallback index. if (loopName && isAwaitingBusy(loopName, sessionId)) { logger.debug(`[idle-gate] busy observed for ses=${sessionId} loop=${loopName}, clearing pending`) clearPromptPending(loopName, logger) } - const clearBusyMarkers = async (): Promise => { - if (loopName) { - clearPromptInFlightBySession(loopName, sessionId) - } - if (loopName && coalescedLimitSessions.get(loopName) === sessionId) { - coalescedLimitSessions.delete(loopName) - } - } if (loopName) { - await withStateLock(loopName, clearBusyMarkers) - } else { - clearBusyMarkers() + const coalesced = coalescedLimitSessions.get(loopName) === sessionId + clearPromptInFlightBySession(loopName, sessionId) + if (coalesced) { + await withStateLock(loopName, async () => { + coalescedLimitSessions.delete(loopName) + }) + } } return } @@ -2693,6 +2700,8 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { } idleRetryAttempts.clear() codingLaunchRecoveryAttempts.clear() + internalFallbackAborts.clear() + coalescedLimitSessions.clear() loopRetainedSessions.clear() sessionToLoop.clear() terminatingLoops.clear() @@ -2750,6 +2759,7 @@ export function createLoop(deps: LoopRuntimeDeps): Loop { } idleRetryAttempts.delete(loopName) codingLaunchRecoveryAttempts.delete(loopName) + coalescedLimitSessions.delete(loopName) const retained = loopRetainedSessions.get(loopName) if (retained) { diff --git a/src/loop/service.ts b/src/loop/service.ts index 3cc0ee0a3..cae584808 100644 --- a/src/loop/service.ts +++ b/src/loop/service.ts @@ -72,6 +72,8 @@ export interface LoopService { resetError(name: string): void /** Compare-and-swap advance of the auditor fallback index; returns the new index on success or null when the loop is absent/non-running/index mismatch. */ advanceAuditorFallbackIndex(name: string, fromIndex: number, toIndex: number): number | null + /** Reset the auditor fallback index to 0 after a successful audit; returns true when a running row was actually changed. */ + resetAuditorFallbackIndex(name: string): boolean setPhase(name: string, phase: LoopState['phase']): void setPhaseAndResetError(name: string, phase: LoopState['phase']): void setModelFailed(name: string, failed: boolean): void @@ -411,14 +413,23 @@ export function createLoopService( } function advanceAuditorFallbackIndex(name: string, fromIndex: number, toIndex: number): number | null { - const state = getAnyState(name) const result = loopsRepo.advanceAuditorFallbackIndex(projectId, name, fromIndex, toIndex) if (result !== null) { + const state = getAnyState(name) notifyLoopChange('auditor-fallback', name, state ? { projectDir: state.projectDir, worktreeDir: state.worktreeDir } : undefined) } return result } + function resetAuditorFallbackIndex(name: string): boolean { + const changed = loopsRepo.resetAuditorFallbackIndex(projectId, name) + if (changed) { + const state = getAnyState(name) + notifyLoopChange('auditor-fallback', name, state ? { projectDir: state.projectDir, worktreeDir: state.worktreeDir } : undefined) + } + return changed + } + function setPhase(name: string, phase: LoopState['phase']): void { const state = getAnyState(name) loopsRepo.updatePhase(projectId, name, phase) @@ -855,6 +866,7 @@ export function createLoopService( incrementError, resetError, advanceAuditorFallbackIndex, + resetAuditorFallbackIndex, setPhase, setPhaseAndResetError, setModelFailed, diff --git a/src/services/execution.ts b/src/services/execution.ts index d6bba7c4b..6f77e73e1 100644 --- a/src/services/execution.ts +++ b/src/services/execution.ts @@ -452,6 +452,11 @@ export interface ForgeExecutionServiceDeps { // Helper Functions // ============================================================================ +function normalizeModelString(value: string | undefined): string | undefined { + const trimmed = value?.trim() + return trimmed ? trimmed : undefined +} + function ok(data: T, warnings?: ForgeExecutionWarning[]): ForgeExecutionResponse { return { ok: true, data, warnings } } @@ -2003,7 +2008,7 @@ export function createForgeExecutionService(deps: ForgeExecutionServiceDeps): Fo const restartAuditorState = { ...stoppedState, - auditorModel: stoppedState.auditorModel ?? deps.config.auditorModel, + auditorModel: normalizeModelString(stoppedState.auditorModel ?? deps.config.auditorModel), modelFailed: false, auditorFallbackIndex: 0, } @@ -2244,6 +2249,34 @@ export function createForgeExecutionService(deps: ForgeExecutionServiceDeps): Fo return { ok: true, newSessionId: effectiveSessionId, previousSessionId, sandbox: restartSandbox, bindFailed } }) + const restartSucceeded = ( + sessionId: string, + previousSession: string, + bindFailedValue: boolean, + ): ForgeExecutionResponse => { + if (bindFailedValue) { + publishWorkspaceDetachedToast({ + client: deps.client, + directory: stoppedState.projectDir ?? stoppedState.worktreeDir, + loopName: stoppedState.loopName, + logger: deps.logger, + context: 'on restart', + }) + } + return ok({ + operation: 'loop.restart', + loopName: stoppedState.loopName, + sessionId, + previousSessionId: previousSession, + worktreeDir: stoppedState.worktreeDir, + worktreeBranch: stoppedState.worktreeBranch, + worktree: !!stoppedState.worktree, + sandbox: restartSandbox, + bindFailed: bindFailedValue, + iteration: stoppedState.iteration, + }) + } + // Provider-limit termination deferred from inside the runExclusive // callback. The callback cannot call deps.loop.terminate itself because // that reacquires the non-reentrant per-loop state lock held by @@ -2293,18 +2326,7 @@ export function createForgeExecutionService(deps: ForgeExecutionServiceDeps): Fo }) if (absorbed) { deps.loopHandler!.startWatchdog(stoppedState.loopName) - return ok({ - operation: 'loop.restart', - loopName: stoppedState.loopName, - sessionId: restartedSessionId!, - previousSessionId, - worktreeDir: stoppedState.worktreeDir, - worktreeBranch: stoppedState.worktreeBranch, - worktree: !!stoppedState.worktree, - sandbox: restartSandbox, - bindFailed, - iteration: stoppedState.iteration, - }) + return restartSucceeded(restartedSessionId!, previousSessionId, bindFailed) } } await deps.loop.terminate(stoppedState.loopName, { kind: 'provider_limit', message: outcome.providerLimitMessage }) @@ -2316,28 +2338,7 @@ export function createForgeExecutionService(deps: ForgeExecutionServiceDeps): Fo if (!outcome.ok) return fail('internal_error', 500, outcome.error) - if (outcome.bindFailed) { - publishWorkspaceDetachedToast({ - client: deps.client, - directory: stoppedState.projectDir ?? stoppedState.worktreeDir, - loopName: stoppedState.loopName, - logger: deps.logger, - context: 'on restart', - }) - } - - return ok({ - operation: 'loop.restart', - loopName: stoppedState.loopName, - sessionId: outcome.newSessionId, - previousSessionId: outcome.previousSessionId, - worktreeDir: stoppedState.worktreeDir, - worktreeBranch: stoppedState.worktreeBranch, - worktree: !!stoppedState.worktree, - sandbox: outcome.sandbox, - bindFailed: outcome.bindFailed, - iteration: stoppedState.iteration, - }) + return restartSucceeded(outcome.newSessionId, outcome.previousSessionId, outcome.bindFailed) } async function dispatch( diff --git a/src/storage/repos/loops-repo.ts b/src/storage/repos/loops-repo.ts index ebcaccd03..6fcef6ac1 100644 --- a/src/storage/repos/loops-repo.ts +++ b/src/storage/repos/loops-repo.ts @@ -61,6 +61,8 @@ export interface LoopsRepo { resetError(projectId: string, loopName: string): void /** Compare-and-swap advance of the auditor fallback index; returns the new index on success or null when the row is absent/non-running/index mismatch. */ advanceAuditorFallbackIndex(projectId: string, loopName: string, fromIndex: number, toIndex: number): number | null + /** Reset the auditor fallback index to 0; returns true when a running row was actually changed, false when it was already 0 / non-running / absent. */ + resetAuditorFallbackIndex(projectId: string, loopName: string): boolean setCurrentSessionId(projectId: string, loopName: string, sessionId: string): void setWorkspaceId(projectId: string, loopName: string, workspaceId: string): void clearWorkspaceId(projectId: string, loopName: string): void @@ -268,6 +270,11 @@ export function createLoopsRepo(db: Database): LoopsRepo { RETURNING auditor_fallback_index `) + const resetAuditorFallbackIndexStmt = db.prepare(` + UPDATE loops SET auditor_fallback_index = 0 + WHERE project_id = ? AND loop_name = ? AND status = 'running' AND auditor_fallback_index != 0 + `) + const setCurrentSessionIdStmt = db.prepare(` UPDATE loops SET current_session_id = ? WHERE project_id = ? AND loop_name = ? @@ -527,6 +534,11 @@ export function createLoopsRepo(db: Database): LoopsRepo { return result?.auditor_fallback_index ?? null }, + resetAuditorFallbackIndex(projectId: string, loopName: string): boolean { + const result = resetAuditorFallbackIndexStmt.run(projectId, loopName) as unknown as { changes: number } + return result.changes > 0 + }, + setCurrentSessionId(projectId: string, loopName: string, sessionId: string): void { setCurrentSessionIdStmt.run(sessionId, projectId, loopName) }, diff --git a/src/tools/loop.ts b/src/tools/loop.ts index 1930bded7..7fc30d4ab 100644 --- a/src/tools/loop.ts +++ b/src/tools/loop.ts @@ -3,8 +3,8 @@ import type { ToolContext } from './types' import { slugify } from '../utils/logger' import { formatSessionOutput, formatAuditResult, formatCompletionSummary, formatPostActionReport } from '../utils/loop-format' -import { fetchSessionOutput, type LoopSessionOutput, MAX_RETRIES } from '../loop' -import { formatDuration, computeElapsedSeconds, resolveUsageFallbackModelLabel } from '../utils/loop-helpers' +import { fetchSessionOutput, type LoopSessionOutput, MAX_RETRIES, type LoopState } from '../loop' +import { formatDuration, computeElapsedSeconds, resolveUsageFallbackModelLabel, buildAuditorModelChain, auditorModelChoiceAt, usageRoleForPhase } from '../utils/loop-helpers' import { buildStartLoopCommand, createForgeExecutionService, type ForgeExecutionRequestContext, type PlanSource } from '../services/execution' import { resolveSessionPlanOfRecord } from '../services/plan-capture' import { formatLoopSessionTitle, formatPlanSessionTitle } from '../utils/session-titles' @@ -16,6 +16,14 @@ const z = tool.schema export function createLoopTools(ctx: ToolContext): Record> { const { loopHandler, config, logger } = ctx + function auditorModelStatusLabel(state: LoopState): string { + const chain = buildAuditorModelChain(config, state) + const index = Math.min(Math.max(state.auditorFallbackIndex ?? 0, 0), chain.length - 1) + const choice = auditorModelChoiceAt(chain, index) + const base = choice.model ? `${choice.model.providerID}/${choice.model.modelID}` : 'default' + return index > 0 ? `${base} (fallback ${index}/${chain.length - 1})` : base + } + /** * Detects when a newly created loop session resolved to a different opencode * project than the one this plugin/TUI instance is scoped to. This happens @@ -436,7 +444,7 @@ export function createLoopTools(ctx: ToolContext): Record 0 ? [`Stalls: ${stallCount}`] : []), ...(stallReason ? [`Last stall reason: ${stallReason}`] : []), ...(stallStatus ? [`Last stall status: ${stallStatus}`] : []), diff --git a/src/utils/loop-helpers.ts b/src/utils/loop-helpers.ts index 9ef11378d..95cf3cae1 100644 --- a/src/utils/loop-helpers.ts +++ b/src/utils/loop-helpers.ts @@ -122,13 +122,12 @@ export function resolveLoopAuditorChoice( return choice } -export function resolveLoopAuditorModel( - config: PluginConfig, - loopService: LoopService, - loopName: string, - logger?: Logger, -): { providerID: string; modelID: string } | undefined { - return resolveLoopAuditorChoice(config, loopService, loopName, logger).model +export function isAuditorPhase(phase: LoopState['phase'] | undefined): boolean { + return phase === 'auditing' || phase === 'final_auditing' +} + +export function usageRoleForPhase(phase: LoopState['phase'] | undefined): 'code' | 'auditor' { + return isAuditorPhase(phase) ? 'auditor' : 'code' } // Resolves the label used to attribute assistant usage when a message's own @@ -140,7 +139,7 @@ export function resolveUsageFallbackModelLabel( state: LoopState, phase: LoopState['phase'], ): string | undefined { - if (phase === 'auditing' || phase === 'final_auditing') { + if (isAuditorPhase(phase)) { const choice = auditorModelChoiceAt(buildAuditorModelChain(config, state), state.auditorFallbackIndex ?? 0) if (choice.model) { return `${choice.model.providerID}/${choice.model.modelID}` diff --git a/test/hooks/loop-section-advancement.test.ts b/test/hooks/loop-section-advancement.test.ts index 571d31730..478593e44 100644 --- a/test/hooks/loop-section-advancement.test.ts +++ b/test/hooks/loop-section-advancement.test.ts @@ -13,6 +13,7 @@ import type { LoopState } from '../../src/loop/state' import { createLoopEventHandler } from '../../src/hooks/loop' import type { Logger, PluginConfig } from '../../src/types' import { createFakeForgeClient } from '../helpers/fake-client' +import { setupLoopsTestDb } from '../helpers/loops-test-db' const mockLogger: Logger = { log: () => {}, @@ -62,126 +63,7 @@ describe('Loop Section Advancement', () => { const dbPath = join(tempDir, 'test.db') db = new Database(dbPath) - db.run(` - CREATE TABLE loops ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - status TEXT NOT NULL, - current_session_id TEXT NOT NULL, - worktree INTEGER NOT NULL, - worktree_dir TEXT NOT NULL, - session_directory TEXT, - worktree_branch TEXT, - project_dir TEXT NOT NULL, - max_iterations INTEGER NOT NULL, - iteration INTEGER NOT NULL DEFAULT 0, - audit_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - phase TEXT NOT NULL, - execution_model TEXT, - auditor_model TEXT, - model_failed INTEGER NOT NULL DEFAULT 0, - sandbox INTEGER NOT NULL DEFAULT 0, - sandbox_container TEXT, - started_at INTEGER NOT NULL, - completed_at INTEGER, - termination_reason TEXT, - completion_summary TEXT, - workspace_id TEXT, - host_session_id TEXT, - audit_session_id TEXT, - current_section_index INTEGER NOT NULL DEFAULT 0, - total_sections INTEGER NOT NULL DEFAULT 0, - final_audit_done INTEGER NOT NULL DEFAULT 0, - final_audit_attempts INTEGER NOT NULL DEFAULT 0, - execution_variant TEXT, - auditor_variant TEXT, - loop_kind TEXT NOT NULL DEFAULT 'plan', - executor_session_id TEXT, - auditor_fallback_index INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (project_id, loop_name) - ) - `) - - db.run(` - CREATE TABLE loop_large_fields ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - last_audit_result TEXT, - post_action_report TEXT, - goal TEXT, - PRIMARY KEY (project_id, loop_name), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - - db.run(` - CREATE TABLE plans ( - project_id TEXT NOT NULL, - loop_name TEXT, - session_id TEXT, - content TEXT NOT NULL, - updated_at INTEGER NOT NULL, - CHECK (loop_name IS NOT NULL OR session_id IS NOT NULL), - CHECK (NOT (loop_name IS NOT NULL AND session_id IS NOT NULL)), - UNIQUE (project_id, loop_name), - UNIQUE (project_id, session_id) - ) - `) - - db.run(` - CREATE TABLE review_findings ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL DEFAULT '', - file TEXT NOT NULL, - line INTEGER NOT NULL, - severity TEXT NOT NULL, - description TEXT NOT NULL, - scenario TEXT, - created_at INTEGER NOT NULL, - section_index INTEGER, - PRIMARY KEY (project_id, loop_name, file, line, section_index) - ) - `) - - db.run(` - CREATE TABLE section_plans ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - section_index INTEGER NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','in_progress','completed','failed')), - attempts INTEGER NOT NULL DEFAULT 0, - started_at INTEGER, - completed_at INTEGER, - summary_done TEXT, - summary_deviations TEXT, - summary_follow_ups TEXT, - created_at INTEGER NOT NULL, - PRIMARY KEY (project_id, loop_name, section_index), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - - db.run(` - CREATE TABLE loop_transitions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - event_type TEXT NOT NULL, - transition_kind TEXT NOT NULL, - from_phase TEXT NOT NULL, - to_phase TEXT, - status TEXT, - reason TEXT, - iteration INTEGER NOT NULL DEFAULT 0, - section_index INTEGER, - created_at INTEGER NOT NULL, - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - db.run(`CREATE INDEX IF NOT EXISTS idx_loop_transitions_loop ON loop_transitions (project_id, loop_name, id)`) + setupLoopsTestDb(db) loopsRepo = createLoopsRepo(db) plansRepo = createPlansRepo(db) @@ -214,7 +96,6 @@ describe('Loop Section Advancement', () => { current_section_index: 0, total_sections: 3, final_audit_done: 0, - final_audit_attempts: 0, } const values = { ...defaults, ...overrides } db.run( diff --git a/test/loop-helpers.test.ts b/test/loop-helpers.test.ts index 4e0190e3c..676dbcb19 100644 --- a/test/loop-helpers.test.ts +++ b/test/loop-helpers.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { resolveLoopModel, resolveLoopAuditorModel, resolveLoopAuditorChoice, buildAuditorModelChain, auditorModelChoiceAt, nextAuditorFallbackIndex, resolveUsageFallbackModelLabel, formatDuration, computeElapsedSeconds } from '../src/utils/loop-helpers' +import { resolveLoopModel, resolveLoopAuditorChoice, buildAuditorModelChain, auditorModelChoiceAt, nextAuditorFallbackIndex, resolveUsageFallbackModelLabel, formatDuration, computeElapsedSeconds } from '../src/utils/loop-helpers' import type { PluginConfig } from '../src/types' describe('resolveLoopModel', () => { @@ -66,7 +66,7 @@ describe('resolveLoopAuditorModel', () => { auditorModel: 'provider/config-auditor', executionModel: 'provider/exec-model', } as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toEqual({ providerID: 'provider', modelID: 'state-auditor' }) }) @@ -79,7 +79,7 @@ describe('resolveLoopAuditorModel', () => { auditorModel: 'provider/config-auditor', executionModel: 'provider/exec-model', } as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toEqual({ providerID: 'provider', modelID: 'state-exec' }) }) @@ -91,7 +91,7 @@ describe('resolveLoopAuditorModel', () => { auditorModel: 'provider/config-auditor', executionModel: 'provider/exec-model', } as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toEqual({ providerID: 'provider', modelID: 'exec-model' }) }) @@ -102,7 +102,7 @@ describe('resolveLoopAuditorModel', () => { const config = { auditorModel: 'provider/config-auditor', } as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toBeUndefined() }) @@ -113,7 +113,7 @@ describe('resolveLoopAuditorModel', () => { const config = { executionModel: 'provider/exec-model', } as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toEqual({ providerID: 'provider', modelID: 'exec-model' }) }) @@ -122,7 +122,7 @@ describe('resolveLoopAuditorModel', () => { active: true, }) const config = {} as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toBeUndefined() }) @@ -138,7 +138,7 @@ describe('resolveLoopAuditorModel', () => { loop: { model: 'provider/loop-model' }, executionModel: 'provider/exec-model', } as PluginConfig - const result = resolveLoopAuditorModel(config, mockLoopService, 'test-loop') + const result = resolveLoopAuditorChoice(config, mockLoopService, 'test-loop').model expect(result).toBeUndefined() }) }) @@ -213,7 +213,7 @@ describe('buildAuditorModelChain', () => { variant: undefined, source: 'state.auditorModel=provider/state-auditor', }) - expect(chain[0].model).toEqual(resolveLoopAuditorModel(config, { getActiveState: () => state } as any, 'test-loop')) + expect(chain[0].model).toEqual(resolveLoopAuditorChoice(config, { getActiveState: () => state } as any, 'test-loop').model) }) it('preserves order, drops unparseable and duplicate entries', () => { diff --git a/test/loop-status-tool.test.ts b/test/loop-status-tool.test.ts index 12ccfbef9..707e17f0c 100644 --- a/test/loop-status-tool.test.ts +++ b/test/loop-status-tool.test.ts @@ -610,6 +610,72 @@ describe('loop-status tool restart path', () => { expect(callWithPermission).toBeDefined() expect(callWithPermission![0].permission).toEqual(buildAuditSessionPermissionRuleset({ sandbox: false, allowDirectories: resolveLoopAllowedDirectories({}) })) }) + + test('auditor model label clamps persisted fallback index to the chain', async () => { + const { client: forgeClient } = createFakeForgeClient() + const logger = createLogger({ enabled: false, file: '' }) + + const loopsRepo = createLoopsRepo(db) + const plansRepo = createPlansRepo(db) + const reviewFindingsRepo = createReviewFindingsRepo(db) + const loopService = createLoopService(loopsRepo, plansRepo, reviewFindingsRepo, projectId, logger) + + const worktreeDir = `${TEST_DIR}/worktree-clamped-index` + mkdirSync(worktreeDir, { recursive: true }) + + loopService.setState(loopName, { + active: false, + sessionId: 'session-clamped-index', + loopName, + worktreeDir, + projectDir: TEST_DIR, + worktreeBranch: 'opencode/loop-test-clamped-index', + iteration: 2, + maxIterations: 5, + startedAt: new Date().toISOString(), + prompt: 'Test prompt clamped index', + phase: 'auditing', + errorCount: 0, + auditCount: 1, + status: 'completed', + worktree: true, + sandbox: false, + executionModel: 'test-model', + auditorModel: 'prov/primary', + auditorFallbackIndex: 3, + workspaceId: 'ws-clamped', + hostSessionId: 'host-clamped', + currentSectionIndex: 0, + totalSections: 0, + finalAuditDone: false, + terminationReason: 'completed', + completedAt: new Date().toISOString(), + } as any) + + const loopHandler = createLoopEventHandler(loopsRepo, plansRepo, reviewFindingsRepo, projectId, forgeClient, logger, () => ({}), undefined, dbPath) + const tools = createLoopTools({ + client: forgeClient, + workspaceStatusRegistry: createNoWaitWorkspaceStatusRegistry(), + pendingTeardowns: createPendingTeardownRegistry(), + directory: TEST_DIR, + config: { auditorFallbackModels: ['prov/fb'] }, + loopService, + loopHandler, + logger, + plansRepo, + loopsRepo, + projectId, + dataDir: dbPath, + loop: loopHandler.loop, + } as any) + + const result = await tools['loop-status'].execute({ + name: loopName, + }, { sessionID: 'test-session' } as any) + + expect(result).toContain('prov/fb (fallback 1/1)') + expect(result).not.toContain('(fallback 3/1)') + }) }) describe('loop-status cumulative usage', () => { diff --git a/test/loop/runtime-service-seam.test.ts b/test/loop/runtime-service-seam.test.ts index b571a7289..88a6c89e0 100644 --- a/test/loop/runtime-service-seam.test.ts +++ b/test/loop/runtime-service-seam.test.ts @@ -36,6 +36,7 @@ function makeFakeLoopService(): LoopService { getPlanText: vi.fn(() => null), incrementError: vi.fn(() => 1), resetError: vi.fn(), + resetAuditorFallbackIndex: vi.fn(() => false), setPhase: vi.fn(), setPhaseAndResetError: vi.fn(), setModelFailed: vi.fn(), diff --git a/test/loop/runtime.test.ts b/test/loop/runtime.test.ts index 6507086d2..40415cee8 100644 --- a/test/loop/runtime.test.ts +++ b/test/loop/runtime.test.ts @@ -2600,9 +2600,10 @@ describe('stall handling terminates with stall timeout when configured cap is re expect(finalAuditPromptText).toContain('Coder decisions & verification notes') expect(finalAuditPromptText).toContain('Chose approach X') expect(finalAuditPromptText).toContain('FOO=bar pnpm test') - // The final-audit prompt must honour the persisted fallback index: fb/auditor-2, no variant. - expect(finalAuditPrompt?.model).toEqual({ providerID: 'fb', modelID: 'auditor-2' }) - expect(finalAuditPrompt?.variant).toBeUndefined() + // The first dirty final-audit succeeded, so the persisted fallback index was + // reset to 0: the subsequent final audit uses the primary model with its variant. + expect(finalAuditPrompt?.model).toEqual({ providerID: 'test', modelID: 'auditor' }) + expect(finalAuditPrompt?.variant).toBe('audit-high') }) }) @@ -3732,7 +3733,7 @@ describe('stall handling terminates with stall timeout when configured cap is re expect(afterState).not.toBeNull() expect(afterState!.active).toBe(false) expect(afterState!.terminationReason).toBe('completed') - expect(afterState!.auditorFallbackIndex).toBe(1) + expect(afterState!.auditorFallbackIndex).toBe(0) const auditorCalls = calls.filter(c => c.method === 'session.promptAsync' && (c.params as any)?.agent === 'auditor-loop') expect(auditorCalls).toHaveLength(1) @@ -4105,6 +4106,71 @@ describe('stall handling terminates with stall timeout when configured cap is re expect(afterState!.phase).toBe('coding') expect(afterState!.auditorFallbackIndex).toBe(1) }) + + test('a provider-limit fallback advance is reset to 0 by the next successful audit, restoring primary model and variant', async () => { + const { client, calls } = createFakeForgeClient({ + session: { + messages: async () => makeCodingIdleMessages(), + }, + }) + const { loop } = createRuntime({ client, loopConfig: { auditorFallbackModels: ['fb/auditor-2'] } }) + + // A persistent bug finding keeps audits dirty so a successful audit rotates + // back to coding instead of terminating, giving us a second audit dispatch. + reviewFindingsRepo.write({ + projectId: PROJECT_ID, + loopName: 'test-loop', + file: 'src/fallback-reset.ts', + line: 1, + severity: 'bug', + description: 'persistent issue', + }) + + const state = makeState({ phase: 'auditing', totalSections: 0, auditCount: 0, auditorVariant: 'audit-high' }) + loopService.setState(state.loopName, state) + loopService.registerLoopSession(state.sessionId, state.loopName) + + // First audit: the primary hits a provider limit (advances 0→1) and + // re-dispatches on the fallback. + await loop.tick({ + type: 'session.status', + properties: { sessionID: state.sessionId, status: { type: 'retry', attempt: 2, message: 'You have reached your usage limit', next: 60000 } }, + }) + + let afterState = loopService.getActiveState(state.loopName) + expect(afterState!.active).toBe(true) + expect(afterState!.phase).toBe('auditing') + expect(afterState!.auditorFallbackIndex).toBe(1) + + // The fallback prompt completes with a dirty audit result, which rotates + // back to coding and must reset the persisted index to 0. + await loop.tick({ type: 'session.status', properties: { sessionID: state.sessionId, status: { type: 'busy' } } }) + await loop.tick({ type: 'session.status', properties: { sessionID: state.sessionId, status: { type: 'idle' } } }) + + afterState = loopService.getActiveState(state.loopName) + expect(afterState).not.toBeNull() + expect(afterState!.active).toBe(true) + expect(afterState!.phase).toBe('coding') + expect(afterState!.auditorFallbackIndex).toBe(0) + + const firstAuditorCalls = calls.filter(c => c.method === 'session.promptAsync' && (c.params as any)?.agent === 'auditor-loop') + expect(firstAuditorCalls).toHaveLength(1) + expect(firstAuditorCalls[0].params).toMatchObject({ model: { providerID: 'fb', modelID: 'auditor-2' } }) + + // The dirty audit rotated to a fresh coding session. + const newSessionId = afterState!.sessionId + expect(newSessionId).not.toBe(state.sessionId) + loopService.registerLoopSession(newSessionId, state.loopName) + + // Second audit: coding idles again and must dispatch on the PRIMARY auditor + // model with the configured auditorVariant restored (index is back to 0). + await loop.tick({ type: 'session.status', properties: { sessionID: newSessionId, status: { type: 'busy' } } }) + await loop.tick({ type: 'session.status', properties: { sessionID: newSessionId, status: { type: 'idle' } } }) + + const allAuditorCalls = calls.filter(c => c.method === 'session.promptAsync' && (c.params as any)?.agent === 'auditor-loop') + const lastAuditor = allAuditorCalls[allAuditorCalls.length - 1] + expect(lastAuditor.params).toMatchObject({ model: { providerID: 'test', modelID: 'auditor' }, variant: 'audit-high' }) + }) }) describe('child session error resolution via ancestor chain', () => { diff --git a/test/loops-repo.test.ts b/test/loops-repo.test.ts index 77830f801..67872b80a 100644 --- a/test/loops-repo.test.ts +++ b/test/loops-repo.test.ts @@ -502,6 +502,37 @@ describe('LoopsRepo', () => { expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(0) }) + + test('reset returns the index to 0 and reports a change', () => { + repo.insert(testRow, testLarge) + repo.advanceAuditorFallbackIndex(testRow.projectId, testRow.loopName, 0, 1) + expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(1) + + const changed = repo.resetAuditorFallbackIndex(testRow.projectId, testRow.loopName) + expect(changed).toBe(true) + expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(0) + }) + + test('reset is a no-op when the index is already 0', () => { + repo.insert(testRow, testLarge) + expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(0) + + const changed = repo.resetAuditorFallbackIndex(testRow.projectId, testRow.loopName) + expect(changed).toBe(false) + expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(0) + }) + + test('reset does not touch a non-running row', () => { + repo.insert(testRow, testLarge) + repo.advanceAuditorFallbackIndex(testRow.projectId, testRow.loopName, 0, 1) + expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(1) + // Flip the row to non-running after the index was advanced while running. + repo.setStatus(testRow.projectId, testRow.loopName, 'completed') + + const changed = repo.resetAuditorFallbackIndex(testRow.projectId, testRow.loopName) + expect(changed).toBe(false) + expect(repo.get(testRow.projectId, testRow.loopName)!.auditorFallbackIndex).toBe(1) + }) }) describe('setLastAuditResult', () => { diff --git a/test/services/attach-loop.test.ts b/test/services/attach-loop.test.ts index 7f4041bc9..b3e83fba8 100644 --- a/test/services/attach-loop.test.ts +++ b/test/services/attach-loop.test.ts @@ -10,110 +10,10 @@ import { createSectionPlansRepo } from '../../src/storage/repos/section-plans-re import { createLoopService } from '../../src/loop/service' import type { Logger } from '../../src/types' import { createFakeForgeClient } from '../helpers/fake-client' +import { setupLoopsTestDb } from '../helpers/loops-test-db' const noopFn = () => {} -const DB_SCHEMA = ` -CREATE TABLE loops ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - status TEXT NOT NULL, - current_session_id TEXT NOT NULL, - worktree INTEGER NOT NULL, - worktree_dir TEXT NOT NULL, - session_directory TEXT, - worktree_branch TEXT, - project_dir TEXT NOT NULL, - max_iterations INTEGER NOT NULL, - iteration INTEGER NOT NULL DEFAULT 0, - audit_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - phase TEXT NOT NULL, - execution_model TEXT, - auditor_model TEXT, - model_failed INTEGER NOT NULL DEFAULT 0, - sandbox INTEGER NOT NULL DEFAULT 0, - sandbox_container TEXT, - started_at INTEGER NOT NULL, - completed_at INTEGER, - termination_reason TEXT, - completion_summary TEXT, - workspace_id TEXT, - host_session_id TEXT, - audit_session_id TEXT, - current_section_index INTEGER NOT NULL DEFAULT 0, - total_sections INTEGER NOT NULL DEFAULT 0, - final_audit_done INTEGER NOT NULL DEFAULT 0, - final_audit_attempts INTEGER NOT NULL DEFAULT 0, - execution_variant TEXT, - auditor_variant TEXT, - loop_kind TEXT NOT NULL DEFAULT 'plan', - executor_session_id TEXT, - auditor_fallback_index INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (project_id, loop_name) -) -` - -const LOOP_LARGE_FIELDS_SCHEMA = ` -CREATE TABLE loop_large_fields ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - last_audit_result TEXT, - post_action_report TEXT, - goal TEXT, - PRIMARY KEY (project_id, loop_name), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE -) -` - -const PLANS_SCHEMA = ` -CREATE TABLE plans ( - project_id TEXT NOT NULL, - loop_name TEXT, - session_id TEXT, - content TEXT NOT NULL, - updated_at INTEGER NOT NULL, - CHECK (loop_name IS NOT NULL OR session_id IS NOT NULL), - CHECK (NOT (loop_name IS NOT NULL AND session_id IS NOT NULL)), - UNIQUE (project_id, loop_name), - UNIQUE (project_id, session_id) -) -` - -const REVIEW_FINDINGS_SCHEMA = ` -CREATE TABLE review_findings ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL DEFAULT '', - file TEXT NOT NULL, - line INTEGER NOT NULL, - severity TEXT NOT NULL, - description TEXT NOT NULL, - scenario TEXT, - created_at INTEGER NOT NULL, - section_index INTEGER, - PRIMARY KEY (project_id, loop_name, file, line, section_index) -) -` - -const SECTION_PLANS_SCHEMA = ` -CREATE TABLE section_plans ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - section_index INTEGER NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','in_progress','completed','failed')), - attempts INTEGER NOT NULL DEFAULT 0, - started_at INTEGER, - completed_at INTEGER, - summary_done TEXT, - summary_deviations TEXT, - summary_follow_ups TEXT, - created_at INTEGER NOT NULL, - PRIMARY KEY (project_id, loop_name, section_index) -) -` - const PROJECT_ID = 'test-project' describe('attachLoopToSession', () => { @@ -123,11 +23,7 @@ describe('attachLoopToSession', () => { beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'attach-loop-test-')) db = new Database(join(tempDir, 'test.db')) - db.exec(DB_SCHEMA) - db.exec(LOOP_LARGE_FIELDS_SCHEMA) - db.exec(PLANS_SCHEMA) - db.exec(REVIEW_FINDINGS_SCHEMA) - db.exec(SECTION_PLANS_SCHEMA) + setupLoopsTestDb(db) }) afterEach(() => { diff --git a/test/services/execution-attach-cleanup.test.ts b/test/services/execution-attach-cleanup.test.ts index 22b33c260..f3db97f69 100644 --- a/test/services/execution-attach-cleanup.test.ts +++ b/test/services/execution-attach-cleanup.test.ts @@ -73,6 +73,7 @@ describe('attachLoopToSession', () => { findMatchByName: (...a: any[]) => loopService.findMatchByName(...a as any), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: { runExclusive: async (name: string, fn: () => Promise) => fn(), diff --git a/test/services/execution-in-flight-guard.test.ts b/test/services/execution-in-flight-guard.test.ts index b2889f937..ee272b475 100644 --- a/test/services/execution-in-flight-guard.test.ts +++ b/test/services/execution-in-flight-guard.test.ts @@ -148,6 +148,7 @@ describe('execution in-flight guard', () => { findMatchByName: (...a: any[]) => loopService.findMatchByName(...a as any), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -269,6 +270,7 @@ describe('execution in-flight guard', () => { findMatchByName: (...a: any[]) => loopService.findMatchByName(...a as any), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -374,6 +376,7 @@ describe('execution in-flight guard', () => { findMatchByName: (...a: any[]) => loopService.findMatchByName(...a as any), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, diff --git a/test/services/execution-restart.test.ts b/test/services/execution-restart.test.ts index 467cfbaaa..6d2dbfcf5 100644 --- a/test/services/execution-restart.test.ts +++ b/test/services/execution-restart.test.ts @@ -1692,7 +1692,9 @@ describe('handleLoopRestart restartability rules', () => { // Default auditor-limit handler mimicking the runtime choke point so the // restart path's routing to it can be exercised without a full runtime. Uses // the real shared chain helpers and the real compare-and-swap advance. + const handleAuditorProviderLimitCalls: Array<{ name: string; message: string }> = [] const defaultHandleAuditorProviderLimit = async (name: string, message: string): Promise => { + handleAuditorProviderLimitCalls.push({ name, message }) const state = loopService.getActiveState(name) if (!state?.active || (state.phase !== 'auditing' && state.phase !== 'final_auditing')) return false const cfg = { @@ -1774,6 +1776,7 @@ describe('handleLoopRestart restartability rules', () => { return { service, client, + handleAuditorProviderLimitCalls, } } @@ -2171,7 +2174,7 @@ describe('handleLoopRestart restartability rules', () => { }) const terminateSpy = vi.fn(async () => true) - const { service, client } = await createMockService({ + const { service, client, handleAuditorProviderLimitCalls } = await createMockService({ terminate: terminateSpy, config: { auditorFallbackModels: ['prov/fb'] }, }) @@ -2191,9 +2194,14 @@ describe('handleLoopRestart restartability rules', () => { ) // The fallback absorbed the limit: the restart reports success and does NOT - // terminate, and the chain advanced to index 1 with a re-dispatch on prov/fb. + // terminate, the deferred handler was invoked with the failing loop and the + // provider-limit reason, and the chain advanced to index 1 with a + // re-dispatch on prov/fb. expect(result.ok).toBe(true) expect(terminateSpy).not.toHaveBeenCalled() + expect(handleAuditorProviderLimitCalls).toContainEqual( + { name: loopName, message: expect.stringContaining('usage limit') }, + ) const newState = loopService.getAnyState(loopName) expect(newState).not.toBeNull() @@ -2207,6 +2215,61 @@ describe('handleLoopRestart restartability rules', () => { expect(fallbackPrompt).toBeDefined() }) + test('absorbed auditor fallback restart still publishes the workspace-detached toast when the bind failed', async () => { + const loopName = 'final-audit-bind-fail-fallback-restart-loop' + insertLoop({ + loopName, + status: 'stalled', + terminationReason: 'stall_timeout', + phase: 'final_auditing', + workspaceId: 'ws_old', + }) + + const terminateSpy = vi.fn(async () => true) + const { service, client, handleAuditorProviderLimitCalls } = await createMockService({ + terminate: terminateSpy, + config: { auditorFallbackModels: ['prov/fb'] }, + }) + + // Force the workspace bind to fail so bindFailed is set on the restart. + ;(client.workspace.warp as any).mockImplementation(async () => { + throw new Error('bind failed') + }) + + // Record toast publications instead of the default no-op. + const toastPublish = vi.fn(async () => {}) + ;(client.tui.publish as any).mockImplementation(toastPublish) + + // The restarted final-audit prompt is directly rejected on a provider limit; + // the fallback re-dispatch on prov/fb succeeds and absorbs the limit. + ;(client.session.promptAsync as any).mockImplementation(async (params: any) => { + if (params?.model?.providerID === 'prov' && params?.model?.modelID === 'fb') { + return + } + throw { name: 'APIError', data: { message: 'You have reached your usage limit', statusCode: 429 } } + }) + + const result = await service.dispatch( + { surface: 'api', projectId: PROJECT_ID, directory: '/tmp/test' }, + { type: 'loop.restart' as const, selector: { kind: 'exact' as const, name: loopName } }, + ) + + // Absorbed by the fallback: the restart reports success and does not terminate. + expect(result.ok).toBe(true) + expect(terminateSpy).not.toHaveBeenCalled() + expect(handleAuditorProviderLimitCalls).toContainEqual( + { name: loopName, message: expect.stringContaining('usage limit') }, + ) + + // Even though the limit was absorbed, the failed workspace bind must still + // surface the workspace-detached toast. + expect(toastPublish).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ type: 'tui.toast.show' }), + }), + ) + }) + test('restart auditor fallback holds the per-loop lock so a concurrent abort lifecycle event is deferred (abort-emits-before-resolving regression)', async () => { // Regression guard: the deferred final-audit restart fallback // (deps.loop.handleAuditorProviderLimit) must run inside the per-loop diff --git a/test/services/execution.start-loop.test.ts b/test/services/execution.start-loop.test.ts index 81c40d2e8..cfe62790f 100644 --- a/test/services/execution.start-loop.test.ts +++ b/test/services/execution.start-loop.test.ts @@ -135,6 +135,7 @@ describe('handleStartLoop builtin worktree workspace', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -244,6 +245,7 @@ describe('handleStartLoop builtin worktree workspace', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -332,6 +334,7 @@ describe('handleStartLoop builtin worktree workspace', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -415,6 +418,7 @@ describe('handleStartLoop builtin worktree workspace', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -482,6 +486,7 @@ describe('handleStartLoop builtin worktree workspace', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -585,6 +590,7 @@ describe('handleStartLoop concurrent-start dedupe', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mocks.mockLoopHandler as any, sectionPlansRepo, sandboxManager: mocks.mockSandboxManager as any, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -648,6 +654,7 @@ describe('handleStartLoop concurrent-start dedupe', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mocks.mockLoopHandler as any, sectionPlansRepo, sandboxManager: mocks.mockSandboxManager as any, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -711,6 +718,7 @@ describe('handleStartLoop concurrent-start dedupe', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mocks.mockLoopHandler as any, sectionPlansRepo, sandboxManager: mocks.mockSandboxManager as any, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -808,6 +816,7 @@ describe('handleStartLoop select-session ordering', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mocks.mockLoopHandler as any, sectionPlansRepo, sandboxManager: mocks.mockSandboxManager as any, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -871,6 +880,7 @@ describe('handleStartLoop select-session ordering', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mocks.mockLoopHandler as any, sectionPlansRepo, sandboxManager: mocks.mockSandboxManager as any, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -930,6 +940,7 @@ describe('handleStartLoop select-session ordering', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mocks.mockLoopHandler as any, sectionPlansRepo, sandboxManager: mocks.mockSandboxManager as any, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -1056,6 +1067,7 @@ describe('handleStartLoop selectSessionBestEffort retry on connection errors', ( findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -1166,6 +1178,7 @@ describe('handleStartLoop selectSessionBestEffort retry on connection errors', ( findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, @@ -1254,6 +1267,7 @@ describe('handleStartLoop variant config fallback', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -1331,6 +1345,7 @@ describe('handleStartLoop variant config fallback', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, workspaceStatusRegistry: mockWorkspaceStatusRegistry, @@ -1438,6 +1453,7 @@ describe('handleStartGoal creates dedicated code session', () => { findMatchByName: (...args: any[]) => loopService.findMatchByName(...args), registerSessionReverseIndex: () => {}, unregisterSessionReverseIndex: () => {}, + handleAuditorProviderLimit: async () => false, } as any, loopHandler: mockLoopHandler as any, sectionPlansRepo, diff --git a/test/tools/plan-adjust.test.ts b/test/tools/plan-adjust.test.ts index 2f20656a4..5698af50b 100644 --- a/test/tools/plan-adjust.test.ts +++ b/test/tools/plan-adjust.test.ts @@ -11,6 +11,7 @@ import { createPlanAmendmentsRepo } from '../../src/storage/repos/plan-amendment import { createLoopService } from '../../src/loop/service' import { createPlanAdjustTool } from '../../src/tools/plan-adjust' import type { Logger } from '../../src/types' +import { setupLoopsTestDb } from '../helpers/loops-test-db' const mockLogger: Logger = { log: () => {}, @@ -35,120 +36,7 @@ describe('plan-adjust tool', () => { dbPath = join(tempDir, 'test.db') db = new Database(dbPath) - db.run(` - CREATE TABLE loops ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - status TEXT NOT NULL, - current_session_id TEXT NOT NULL, - worktree INTEGER NOT NULL, - worktree_dir TEXT NOT NULL, - worktree_branch TEXT, - project_dir TEXT NOT NULL, - max_iterations INTEGER NOT NULL, - iteration INTEGER NOT NULL DEFAULT 0, - audit_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - phase TEXT NOT NULL, - execution_model TEXT, - auditor_model TEXT, - model_failed INTEGER NOT NULL DEFAULT 0, - sandbox INTEGER NOT NULL DEFAULT 0, - sandbox_container TEXT, - started_at INTEGER NOT NULL, - completed_at INTEGER, - termination_reason TEXT, - completion_summary TEXT, - workspace_id TEXT, - host_session_id TEXT, - session_directory TEXT, - current_section_index INTEGER NOT NULL DEFAULT 0, - total_sections INTEGER NOT NULL DEFAULT 0, - final_audit_done INTEGER NOT NULL DEFAULT 0, - final_audit_attempts INTEGER NOT NULL DEFAULT 0, - execution_variant TEXT, - auditor_variant TEXT, - loop_kind TEXT NOT NULL DEFAULT 'plan', - executor_session_id TEXT, - auditor_fallback_index INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (project_id, loop_name) - ) - `) - - db.run(` - CREATE TABLE loop_large_fields ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - last_audit_result TEXT, - post_action_report TEXT, - goal TEXT, - PRIMARY KEY (project_id, loop_name), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - - db.run(` - CREATE TABLE plans ( - project_id TEXT NOT NULL, - loop_name TEXT, - session_id TEXT, - content TEXT NOT NULL, - updated_at INTEGER NOT NULL, - CHECK (loop_name IS NOT NULL OR session_id IS NOT NULL), - CHECK (NOT (loop_name IS NOT NULL AND session_id IS NOT NULL)), - UNIQUE (project_id, loop_name), - UNIQUE (project_id, session_id) - ) - `) - - db.run(` - CREATE TABLE review_findings ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL DEFAULT '', - file TEXT NOT NULL, - line INTEGER NOT NULL, - severity TEXT NOT NULL, - description TEXT NOT NULL, - scenario TEXT, - created_at INTEGER NOT NULL, - section_index INTEGER, - PRIMARY KEY (project_id, loop_name, file, line, section_index) - ) - `) - - db.run(` - CREATE TABLE section_plans ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - section_index INTEGER NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - attempts INTEGER NOT NULL DEFAULT 0, - summary_done TEXT, - summary_deviations TEXT, - summary_follow_ups TEXT, - started_at INTEGER, - completed_at INTEGER, - created_at INTEGER NOT NULL, - PRIMARY KEY (project_id, loop_name, section_index), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - - db.run(` - CREATE TABLE plan_amendments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - source TEXT NOT NULL DEFAULT 'auditor', - rationale TEXT NOT NULL, - applied_at_section INTEGER NOT NULL, - sections_before TEXT NOT NULL, - sections_after TEXT NOT NULL, - created_at INTEGER NOT NULL - ) - `) + setupLoopsTestDb(db) loopsRepo = createLoopsRepo(db) plansRepo = createPlansRepo(db) diff --git a/test/tools/review-section-scope.test.ts b/test/tools/review-section-scope.test.ts index d33e26d36..663a0abc3 100644 --- a/test/tools/review-section-scope.test.ts +++ b/test/tools/review-section-scope.test.ts @@ -11,6 +11,7 @@ import { createLoopService } from '../../src/loop/service' import { createReviewTools } from '../../src/tools/review' import { createSessionLoopResolver } from '../../src/services/session-loop-resolver' import type { Logger } from '../../src/types' +import { setupLoopsTestDb } from '../helpers/loops-test-db' const mockLogger: Logger = { log: () => {}, @@ -37,109 +38,9 @@ describe('review section scoping', () => { dbPath = join(tempDir, 'test.db') db = new Database(dbPath) - db.run(` - CREATE TABLE loops ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - status TEXT NOT NULL, - current_session_id TEXT NOT NULL, - worktree INTEGER NOT NULL, - worktree_dir TEXT NOT NULL, - session_directory TEXT, - worktree_branch TEXT, - project_dir TEXT NOT NULL, - max_iterations INTEGER NOT NULL, - iteration INTEGER NOT NULL DEFAULT 0, - audit_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - phase TEXT NOT NULL, - execution_model TEXT, - auditor_model TEXT, - model_failed INTEGER NOT NULL DEFAULT 0, - sandbox INTEGER NOT NULL DEFAULT 0, - sandbox_container TEXT, - started_at INTEGER NOT NULL, - completed_at INTEGER, - termination_reason TEXT, - completion_summary TEXT, - workspace_id TEXT, - host_session_id TEXT, - audit_session_id TEXT, - current_section_index INTEGER NOT NULL DEFAULT 0, - total_sections INTEGER NOT NULL DEFAULT 0, - final_audit_done INTEGER NOT NULL DEFAULT 0, - final_audit_attempts INTEGER NOT NULL DEFAULT 0, - execution_variant TEXT, - auditor_variant TEXT, - loop_kind TEXT NOT NULL DEFAULT 'plan', - executor_session_id TEXT, - auditor_fallback_index INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (project_id, loop_name) - ) - `) - - db.run(` - CREATE TABLE loop_large_fields ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - last_audit_result TEXT, - post_action_report TEXT, - goal TEXT, - PRIMARY KEY (project_id, loop_name), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - - db.run(` - CREATE TABLE plans ( - project_id TEXT NOT NULL, - loop_name TEXT, - session_id TEXT, - content TEXT NOT NULL, - updated_at INTEGER NOT NULL, - CHECK (loop_name IS NOT NULL OR session_id IS NOT NULL), - CHECK (NOT (loop_name IS NOT NULL AND session_id IS NOT NULL)), - UNIQUE (project_id, loop_name), - UNIQUE (project_id, session_id) - ) - `) - - db.run(` - CREATE TABLE review_findings ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL DEFAULT '', - file TEXT NOT NULL, - line INTEGER NOT NULL, - severity TEXT NOT NULL, - description TEXT NOT NULL, - scenario TEXT, - created_at INTEGER NOT NULL, - section_index INTEGER, - PRIMARY KEY (project_id, loop_name, file, line, section_index) - ) - `) + setupLoopsTestDb(db) db.run(`CREATE INDEX IF NOT EXISTS idx_review_findings_loop_name ON review_findings(project_id, loop_name)`) - db.run(` - CREATE TABLE section_plans ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - section_index INTEGER NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','in_progress','completed','failed')), - attempts INTEGER NOT NULL DEFAULT 0, - started_at INTEGER, - completed_at INTEGER, - created_at INTEGER NOT NULL, - summary_done TEXT, - summary_deviations TEXT, - summary_follow_ups TEXT, - PRIMARY KEY (project_id, loop_name, section_index), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - loopsRepo = createLoopsRepo(db) plansRepo = createPlansRepo(db) reviewFindingsRepo = createReviewFindingsRepo(db) diff --git a/test/tools/section-read.test.ts b/test/tools/section-read.test.ts index eb3d51916..50f9597e6 100644 --- a/test/tools/section-read.test.ts +++ b/test/tools/section-read.test.ts @@ -10,6 +10,7 @@ import { createSectionPlansRepo } from '../../src/storage/repos/section-plans-re import { createLoopService } from '../../src/loop/service' import { createSectionReadTool } from '../../src/tools/section-read' import type { Logger } from '../../src/types' +import { setupLoopsTestDb } from '../helpers/loops-test-db' const mockLogger: Logger = { log: () => {}, @@ -33,106 +34,7 @@ describe('section-read tool', () => { dbPath = join(tempDir, 'test.db') db = new Database(dbPath) - db.run(` - CREATE TABLE loops ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - status TEXT NOT NULL, - current_session_id TEXT NOT NULL, - worktree INTEGER NOT NULL, - worktree_dir TEXT NOT NULL, - worktree_branch TEXT, - project_dir TEXT NOT NULL, - max_iterations INTEGER NOT NULL, - iteration INTEGER NOT NULL DEFAULT 0, - audit_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - phase TEXT NOT NULL, - execution_model TEXT, - auditor_model TEXT, - model_failed INTEGER NOT NULL DEFAULT 0, - sandbox INTEGER NOT NULL DEFAULT 0, - sandbox_container TEXT, - started_at INTEGER NOT NULL, - completed_at INTEGER, - termination_reason TEXT, - completion_summary TEXT, - workspace_id TEXT, - host_session_id TEXT, - session_directory TEXT, - current_section_index INTEGER NOT NULL DEFAULT 0, - total_sections INTEGER NOT NULL DEFAULT 0, - final_audit_done INTEGER NOT NULL DEFAULT 0, - final_audit_attempts INTEGER NOT NULL DEFAULT 0, - execution_variant TEXT, - auditor_variant TEXT, - loop_kind TEXT NOT NULL DEFAULT 'plan', - executor_session_id TEXT, - auditor_fallback_index INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (project_id, loop_name) - ) - `) - - db.run(` - CREATE TABLE loop_large_fields ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - last_audit_result TEXT, - post_action_report TEXT, - goal TEXT, - PRIMARY KEY (project_id, loop_name), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) - - db.run(` - CREATE TABLE plans ( - project_id TEXT NOT NULL, - loop_name TEXT, - session_id TEXT, - content TEXT NOT NULL, - updated_at INTEGER NOT NULL, - CHECK (loop_name IS NOT NULL OR session_id IS NOT NULL), - CHECK (NOT (loop_name IS NOT NULL AND session_id IS NOT NULL)), - UNIQUE (project_id, loop_name), - UNIQUE (project_id, session_id) - ) - `) - - db.run(` - CREATE TABLE review_findings ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL DEFAULT '', - file TEXT NOT NULL, - line INTEGER NOT NULL, - severity TEXT NOT NULL, - description TEXT NOT NULL, - scenario TEXT, - created_at INTEGER NOT NULL, - section_index INTEGER, - PRIMARY KEY (project_id, loop_name, file, line, section_index) - ) - `) - - db.run(` - CREATE TABLE section_plans ( - project_id TEXT NOT NULL, - loop_name TEXT NOT NULL, - section_index INTEGER NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - attempts INTEGER NOT NULL DEFAULT 0, - summary_done TEXT, - summary_deviations TEXT, - summary_follow_ups TEXT, - started_at INTEGER, - completed_at INTEGER, - created_at INTEGER NOT NULL, - PRIMARY KEY (project_id, loop_name, section_index), - FOREIGN KEY (project_id, loop_name) REFERENCES loops(project_id, loop_name) ON DELETE CASCADE - ) - `) + setupLoopsTestDb(db) loopsRepo = createLoopsRepo(db) plansRepo = createPlansRepo(db) diff --git a/test/watchdog.test.ts b/test/watchdog.test.ts index 56d491eed..462d9eae0 100644 --- a/test/watchdog.test.ts +++ b/test/watchdog.test.ts @@ -477,6 +477,163 @@ describe('createLoopWatchdog', () => { watchdog.stop(loopName) }) + it('absorbs provider-limit retry in final_auditing when fallback returns true and resets activity', async () => { + const stateRef = { current: createState({ phase: 'final_auditing', sessionId: 'audit-session' }) } + const terminateCalls: unknown[] = [] + const fallbackCalls: unknown[] = [] + + const logger = createLogger() + const watchdog = createLoopWatchdog({ + loopService: { + ...createMockLoopService({ + getActiveState: () => stateRef.current, + }), + }, + client: createMockClient(async () => ({ + 'audit-session': { type: 'retry', attempt: 1, message: 'usage limit reached' }, + })), + logger, + recover: async () => {}, + terminate: async (ln, _s, reason) => { + terminateCalls.push(reason) + watchdog.stop(ln) + }, + handleAuditorProviderLimit: async (ln, reason) => { + fallbackCalls.push({ ln, reason }) + return true + }, + }) + + const loopName = 'test-loop' + watchdog.start(loopName) + await wait(60) + + expect(fallbackCalls.length).toBeGreaterThanOrEqual(1) + expect((fallbackCalls[0] as { ln: string }).ln).toBe(loopName) + expect(String((fallbackCalls[0] as { reason: unknown }).reason)).toContain('usage limit') + expect(terminateCalls.length).toBe(0) + expect(watchdog.getStallInfo(loopName)?.consecutiveStalls).toBe(0) + + watchdog.stop(loopName) + }) + + it('terminates with provider_limit in final_auditing when fallback returns false', async () => { + const stateRef = { current: createState({ phase: 'final_auditing', sessionId: 'audit-session' }) } + const terminateCalls: unknown[] = [] + const fallbackCalls: unknown[] = [] + + const logger = createLogger() + const watchdog = createLoopWatchdog({ + loopService: { + ...createMockLoopService({ + getActiveState: () => stateRef.current, + }), + }, + client: createMockClient(async () => ({ + 'audit-session': { type: 'retry', attempt: 1, message: 'usage limit reached' }, + })), + logger, + recover: async () => {}, + terminate: async (ln, _s, reason) => { + terminateCalls.push(reason) + watchdog.stop(ln) + }, + handleAuditorProviderLimit: async (ln, reason) => { + fallbackCalls.push({ ln, reason }) + return false + }, + }) + + const loopName = 'test-loop' + watchdog.start(loopName) + await wait(60) + + expect(fallbackCalls.length).toBe(1) + expect(terminateCalls.length).toBe(1) + expect(terminateCalls[0]).toEqual({ + kind: 'provider_limit', + message: expect.stringContaining('usage limit'), + }) + + watchdog.stop(loopName) + }) + + it('terminates with provider_limit when fallback absent (backwards compatible)', async () => { + const stateRef = { current: createState({ phase: 'final_auditing', sessionId: 'audit-session' }) } + const terminateCalls: unknown[] = [] + + const logger = createLogger() + const watchdog = createLoopWatchdog({ + loopService: { + ...createMockLoopService({ + getActiveState: () => stateRef.current, + }), + }, + client: createMockClient(async () => ({ + 'audit-session': { type: 'retry', attempt: 1, message: 'usage limit reached' }, + })), + logger, + recover: async () => {}, + terminate: async (ln, _s, reason) => { + terminateCalls.push(reason) + watchdog.stop(ln) + }, + }) + + const loopName = 'test-loop' + watchdog.start(loopName) + await wait(60) + + expect(terminateCalls.length).toBe(1) + expect(terminateCalls[0]).toEqual({ + kind: 'provider_limit', + message: expect.stringContaining('usage limit'), + }) + + watchdog.stop(loopName) + }) + + it('does not absorb provider-limit retry in a non-auditor phase', async () => { + const stateRef = { current: createState({ phase: 'coding' }) } + const terminateCalls: unknown[] = [] + let fallbackCalls = 0 + + const logger = createLogger() + const watchdog = createLoopWatchdog({ + loopService: { + ...createMockLoopService({ + getActiveState: () => stateRef.current, + }), + }, + client: createMockClient(async () => ({ + 'coding-session': { type: 'retry', attempt: 1, message: 'usage limit reached' }, + })), + logger, + recover: async () => {}, + terminate: async (ln, _s, reason) => { + terminateCalls.push(reason) + watchdog.stop(ln) + }, + handleAuditorProviderLimit: async () => { + fallbackCalls += 1 + return true + }, + }) + + const loopName = 'test-loop' + watchdog.start(loopName) + await wait(60) + + expect(fallbackCalls).toBe(0) + expect(terminateCalls.length).toBe(1) + expect(terminateCalls[0]).toEqual({ + kind: 'provider_limit', + message: expect.stringContaining('usage limit'), + }) + + watchdog.stop(loopName) + }) + it('terminates when child session has provider-limit retry via resolveSessionLoopName', async () => { const stateRef = { current: createState() } const terminateCalls: unknown[] = []