diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..24af5e1 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,42 @@ +# PLAN — Issue #127 slice: `src/engine/loop.ts` slim-down (tests-first) + +## Goal +Reduce `src/engine/loop.ts:1-6567` (6567 lines, single `HeadlessSession` class at `src/engine/loop.ts:1363`) toward [`docs/typescript-standards.md`](docs/typescript-standards.md:17-24) (250 file / 200 class / 20 function lines) with **no behavior change**. Land a coherent partial split with tests + green gates if the full split does not fit; do not leave a sprawling WIP. + +## Scope boundaries (hard) +- Owns ONLY `src/engine/loop.ts`. Do NOT touch `src/orchestrator/cli.ts`, `resume.ts`, or anything a sibling worker owns per the issue brief. +- No behavior change: pure moves + thin delegation. Keep all 27 existing importers compiling (importers include [`src/cli.ts`](src/cli.ts:1), [`src/index.ts`](src/index.ts:1), [`src/orchestrator/pipeline.ts`](src/orchestrator/pipeline.ts:1)); keep `src/engine/loop.ts` as a re-export facade during the split. +- Tests-first is mandatory: test must exist and pass BEFORE the unit it covers is reshaped. + +## Starting state (verified) +- Monolith: `src/engine/loop.ts:1363` `HeadlessSession` + config interfaces `src/engine/loop.ts:744` (`HeadlessSessionConfig`), `src/engine/loop.ts:1218` (`ResolvedSessionConfig`), `src/engine/loop.ts:1208` (`SessionRunState`). +- Largest method: [`runIterations()`](src/engine/loop.ts:3416) spans ~3416–5487 (~2000 lines) — the endgame; do it last, and accept a partial win. +- Condensation cluster (extract as a group): [`maybeCondenseHistory()`](src/engine/loop.ts:2584), [`fireBackgroundCondense()`](src/engine/loop.ts:2862), [`applyBackgroundCondense()`](src/engine/loop.ts:2985), [`recordCondensationUsage()`](src/engine/loop.ts:3103), [`recordHandoffCondensationUsage()`](src/engine/loop.ts:3135), [`writeIterationCapHandoff()`](src/engine/loop.ts:3162), [`recordAuxLlmUsage()`](src/engine/loop.ts:3241), [`condensationClient()`](src/engine/loop.ts:3378). Prior art already extracted: [`src/engine/condense.ts:1-60`](src/engine/condense.ts:1), [`src/engine/handoff.ts:42-52`](src/engine/handoff.ts:42). +- Handler cluster: [`handleNewTask()`](src/engine/loop.ts:5662), [`handleSwitchMode()`](src/engine/loop.ts:5842), [`handleExtensionManage()`](src/engine/loop.ts:5595), [`handleExtensionCall()`](src/engine/loop.ts:5641), [`handleActivateSkill()`](src/engine/loop.ts:6060), [`handleReadSkillReference()`](src/engine/loop.ts:6083), [`handleRequestTool()`](src/engine/loop.ts:6105), [`handleListTools()`](src/engine/loop.ts:6056). +- Pure free functions (lowest-risk first extraction): [`combineAbortSignals()`](src/engine/loop.ts:173), [`isReadOnlyDirectCall()`](src/engine/loop.ts:361), [`isReadOnlyTurnCall()`](src/engine/loop.ts:379), [`artifactRejectionNudgeMessage()`](src/engine/loop.ts:524), [`stripSupersededReasoning()`](src/engine/loop.ts:572), [`countVerifiedCitations()`](src/engine/loop.ts:613), [`matchingArtifactFileStatus()`](src/engine/loop.ts:649), [`hasUncommittedTrackedChanges()`](src/engine/loop.ts:709), [`estimateTokenSize()`](src/engine/loop.ts:6283), [`summarizeToolArg()`](src/engine/loop.ts:6300), [`truncateHistory()`](src/engine/loop.ts:6474), [`sameToolNames()`](src/engine/loop.ts:6387), [`messagesPrefixEquals()`](src/engine/loop.ts:6415), `env*` helpers [`src/engine/loop.ts:6493-6560`](src/engine/loop.ts:6493). +- Existing test style: plain `node:assert/strict` scripts via `tsx`, auto-discovered (`scripts/run-tests.mjs:41-55`). Main suite [`src/engine/__tests__/loop.test.ts:1-80`](src/engine/__tests__/loop.test.ts:1) (4402 lines, FakeLlmClient DI pattern at `src/engine/__tests__/loop.test.ts:44-65`). Focused suites exist for condense/pause/inject/switch-mode — mirror them. +- Check tooling: [`scripts/check-code-standards.mjs:18`](scripts/check-code-standards.mjs:18) limits, [`scripts/check-architecture.mjs:19`](scripts/check-architecture.mjs:19) limits; baselines `scripts/code-standards-baseline.json`, `scripts/architecture-baseline.json` (content-anchored; regenerate with `--write-baseline` in the SAME change, never by hand). + +## Steps +1. **Measure (record numbers for the closing report).** Run: `npm run check-standards -- src/engine/loop.ts`, `npm run check-architecture -- src/engine/loop.ts` (or full runs), `npm run coverage -- --filter engine`. Save outputs; also `wc -l src/engine/loop.ts`. +2. **Tests FIRST — pure helpers.** Add `src/engine/__tests__/loop-helpers.test.ts` covering every pure function to be moved first (`combineAbortSignals`, `isReadOnlyDirectCall/TurnCall`, `artifactRejectionNudgeMessage`, `stripSupersededReasoning`, `countVerifiedCitations` + `matchingArtifactFileStatus` with temp dirs, `estimateTokenSize`, `summarizeToolArg`, `truncateHistory`, `sameToolNames`, `messagesPrefixEquals`, env helpers). Keep each test file <250 lines — split into `loop-helpers-*.test.ts` if needed. Run `npx tsx ` then `npm test -- loop-helpers` until green. Do not move code until these pass. +3. **Extract pure helpers.** Create `src/engine/loop-helpers.ts` (or two files if >250 lines: `loop-pure.ts` + `loop-history.ts` for truncate/messages fns). Move the step-2 functions verbatim, fix imports, re-export from `loop.ts` so existing `import { … } from "../loop.js"` in tests (`src/engine/__tests__/loop.test.ts:17-30`) keeps working. Verify `npx tsc --noEmit`. +4. **Tests FIRST — condensation.** Add `src/engine/__tests__/loop-condense.test.ts` using the FakeLlmClient pattern for `maybeCondenseHistory` / `fireBackgroundCondense` / `applyBackgroundCondense` / usage recorders. Reuse fixtures from `src/engine/__tests__/condense.test.ts`, not new harness machinery. +5. **Extract condensation.** Create `src/engine/loop-condense.ts` taking explicit params (messages, budgetTracker, contextWindowTokens) rather than reaching into private session fields; `HeadlessSession` methods at `src/engine/loop.ts:2584-3415` become thin delegates. Follow the `src/engine/condense.ts` / `src/engine/handoff.ts` module idiom (one layout/concern per file). Verify typecheck. +6. **Tests FIRST + extract handlers.** Add `src/engine/__tests__/loop-handlers.test.ts` for `handleSwitchMode` / `handleNewTask` / extension + skill handlers with fake executor/registry (see existing `switch-mode.test.ts`, `new-task.test.ts`). Then create `src/engine/loop-handlers.ts`; pass a minimal session-context interface (not the whole `HeadlessSession`) to avoid an import cycle (`cycle` rule allows zero cycles). +7. **runIterations — partial, coherent only.** Do NOT attempt to split the whole `src/engine/loop.ts:3416-5487` in one go. If time permits, extract ONE seam (e.g. pause/inject polling `src/engine/loop.ts:2439-2583` → `loop-poll.ts`, or checkpoint helpers `src/engine/loop.ts:5488-5575` → `loop-checkpoints.ts`). Prefer landing steps 2–6 green over a half-split runIterations. +8. **Baselines + gates (all green, same change).** `npx tsc --noEmit`; `npm test` (full); `npm run check-standards`; `npm run check-architecture`; regenerate both baselines in the same commit(s); `npm run coverage -- --min 24` must not regress vs step 1. +9. **Commit / PR.** One logical commit per extraction (`git add` + `git commit` with `#127` in message). Push branch, open PR (ready, not draft) against `master` linking `#127`. Close with the ORCHESTRATOR_TASK.md template (Files changed / Baseline before+after / Boot check / Residual risk / PR link). + +## Verification +- `npx tsc --noEmit` clean. +- `npm test` full passes (runner: `scripts/run-tests.mjs:105-177`, fail-fast, sorted). +- `npm run check-standards` and `npm run check-architecture` pass with regenerated baselines in the same change. +- `npm run coverage -- --min 24` passes; `npm run coverage -- --filter engine` recorded before/after (must not regress). +- Boot check: N/A (library refactor, no CLI behavior change) — state that explicitly in the closing report unless a touched path warrants `npx tsx src/cli.ts --help`. + +## Risks / notes +- Content-anchored baselines: editing a flagged line creates a NEW violation key — expect to fix (not baseline) touched lines; keep lines ≤80 chars and functions ≤20 lines in all new files. +- Cycle risk: new `loop-*.ts` modules must not import `loop.ts` back (zero `cycle` tolerance). Inject a narrow context interface instead. +- `runIterations` statefulness (locals at `src/engine/loop.ts:3419-3456` like `completionNudged`, `lastExecuteCommandFailed`) makes extraction error-prone — default to leaving it intact and claiming the helpers+condensation+handlers win. +- Scratch only under `.headlesscode/scratch/`; never `/tmp` (outside-workspace writes fail). diff --git a/scripts/architecture-baseline.json b/scripts/architecture-baseline.json index a286f38..c836ce8 100644 --- a/scripts/architecture-baseline.json +++ b/scripts/architecture-baseline.json @@ -1,9 +1,9 @@ { "version": 1, "violations": [ - "clone 00803486e758 src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", + "clone 00803486e758 src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", "clone 0081a7d462ab src/orchestrator/__tests__/cost-history-cli.test.ts,src/orchestrator/__tests__/cost-history.test.ts", - "clone 0152e2318e4e src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts", + "clone 0152e2318e4e src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts", "clone 01a56a433e82 src/orchestrator/task-files.ts", "clone 01e5839d0892 src/permissions/commands.ts", "clone 0288db0ddefd src/orchestrator/cli.ts", @@ -18,7 +18,7 @@ "clone 05d4573a34dd src/codesearch/embedder.ts,src/memory/embed.ts", "clone 05f9940a16d5 src/decision-proxy/cli.ts,src/orchestrator/cli.ts,src/orchestrator/resume.ts", "clone 0654c838248c src/llm/muse.ts,src/llm/openrouter.ts", - "clone 0665ce33abe0 src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts", + "clone 0665ce33abe0 src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts", "clone 06b27f6a49e2 src/orchestrator/__tests__/analyze-cli.test.ts", "clone 06c2ac64ec1e src/engine/__tests__/condense.test.ts", "clone 070bf0935a24 src/engine/loop.ts", @@ -76,7 +76,7 @@ "clone 14ae7a9a5f1d src/codesearch/__tests__/backend-selection.test.ts,src/codesearch/__tests__/index.test.ts,src/codesearch/__tests__/search.test.ts,src/config/__tests__/mode-models.test.ts,src/dashboard/__tests__/file-browser-permissions.test.ts,src/dashboard/__tests__/settings.test.ts,src/orchestrator/__tests__/cli.test.ts,src/orchestrator/__tests__/model-resolution.test.ts,src/orchestrator/__tests__/resume.test.ts,src/orchestrator/__tests__/spawn-model.test.ts,src/orchestrator/__tests__/watch.test.ts,src/permissions/__tests__/protected-files.test.ts,src/tools/__tests__/codebase_search.test.ts", "clone 14d7e81b5443 src/checkpoints/__tests__/cli.test.ts,src/codemap/__tests__/cli.test.ts,src/dashboard/__tests__/cli.test.ts", "clone 15440c983d57 src/checkpoints/cli.ts,src/decision-proxy/cli.ts", - "clone 157b83b06b3d src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", + "clone 157b83b06b3d src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", "clone 15d1e6928f43 src/dashboard/server.ts", "clone 16271d25ae1f src/llm/preflight.ts", "clone 162af6dd3851 src/decision-proxy/__tests__/proxy.test.ts,src/engine/__tests__/switch-mode.test.ts,src/tools/__tests__/codebase_search.test.ts,src/tools/__tests__/execute_command.test.ts", @@ -342,9 +342,9 @@ "clone 60c4fdaf31bd src/codesearch/airunner-embedder.ts,src/codesearch/ollama-embedder.ts", "clone 61046dbb6410 src/budget/__tests__/budget.test.ts,src/budget/__tests__/concurrency.test.ts,src/budget/__tests__/cost.test.ts,src/checkpoints/__tests__/service.test.ts,src/cloud/__tests__/provider.test.ts,src/codeintel/__tests__/call-graph.test.ts,src/codeintel/__tests__/call-hierarchy.test.ts,src/codeintel/__tests__/find-references.test.ts,src/codeintel/__tests__/go-to-definition.test.ts,src/codeintel/__tests__/hover.test.ts,src/codeintel/__tests__/import-graph.test.ts,src/codeintel/__tests__/outline.test.ts,src/codeintel/__tests__/rename-symbol.test.ts,src/codemap/__tests__/build.test.ts,src/codesearch/__tests__/airunner-embedder.test.ts,src/codesearch/__tests__/chunk.test.ts,src/codesearch/__tests__/files.test.ts,src/codesearch/__tests__/ollama-embedder.test.ts,src/dashboard/__tests__/aggregate.test.ts,src/dashboard/__tests__/chat-thread.test.ts,src/dashboard/__tests__/checkpoints-routes.test.ts,src/dashboard/__tests__/codemap-routes.test.ts,src/dashboard/__tests__/cost-history-routes.test.ts,src/dashboard/__tests__/events.test.ts,src/dashboard/__tests__/projects-routes.test.ts,src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts,src/dashboard/__tests__/timeline.test.ts,src/deploy/__tests__/gate.test.ts,src/engine/__tests__/condense-async.test.ts,src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts,src/engine/__tests__/inject-message.test.ts,src/engine/__tests__/local-explore.test.ts,src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/pause.test.ts,src/engine/__tests__/switch-mode.test.ts,src/github/__tests__/app-auth.test.ts,src/github/__tests__/cli.test.ts,src/github/__tests__/pr.test.ts,src/github/__tests__/provision.test.ts,src/github/__tests__/push.test.ts,src/index-util.test.ts,src/init/__tests__/cli.test.ts,src/init/__tests__/gitignore.test.ts,src/memory/__tests__/local.test.ts,src/memory/__tests__/memory-wiring.test.ts,src/memory/__tests__/uwuchat.test.ts,src/migrate/cli.test.ts,src/orchestrator/__tests__/arg-cursor.test.ts,src/orchestrator/__tests__/auto-split.test.ts,src/orchestrator/__tests__/cleanup.test.ts,src/orchestrator/__tests__/git-sync.test.ts,src/orchestrator/__tests__/merge-check.test.ts,src/orchestrator/__tests__/pipeline.test.ts,src/orchestrator/__tests__/reviewer.test.ts,src/orchestrator/__tests__/split.test.ts,src/orchestrator/__tests__/state.test.ts,src/orchestrator/__tests__/status-cli.test.ts,src/orchestrator/__tests__/stop-cli.test.ts,src/permissions/__tests__/commands.test.ts,src/permissions/__tests__/store-protection.test.ts,src/project-store.test.ts,src/projects/__tests__/cli.test.ts,src/qa/__tests__/qa.test.ts,src/tools/__tests__/apply_diff.test.ts,src/tools/__tests__/browser-action.test.ts,src/tools/__tests__/decision.test.ts,src/tools/__tests__/describe_image.test.ts,src/tools/__tests__/edit_file.test.ts,src/tools/__tests__/execute_command.test.ts,src/tools/__tests__/language-detect.test.ts,src/tools/__tests__/list-files.test.ts,src/tools/__tests__/local-summarization-gate.test.ts,src/tools/__tests__/output-summarizer.test.ts,src/tools/__tests__/path-safety.test.ts,src/tools/__tests__/read-cache.test.ts,src/tools/__tests__/run-tests.test.ts,src/tools/__tests__/search_replace.test.ts,src/tools/__tests__/update_todo_list.test.ts,src/tools/__tests__/write_to_file.test.ts,src/vision/__tests__/describe.test.ts,src/watcher/__tests__/github.test.ts,src/watcher/__tests__/state.test.ts,src/watcher/__tests__/watch.test.ts", "clone 6255fbea917d src/orchestrator/__tests__/cost-history-cli.test.ts,src/orchestrator/__tests__/cost-history.test.ts", - "clone 6287c1413aa4 src/budget/__tests__/budget.test.ts,src/budget/__tests__/concurrency.test.ts,src/budget/__tests__/cost.test.ts,src/checkpoints/__tests__/service.test.ts,src/cloud/__tests__/provider.test.ts,src/codeintel/__tests__/call-graph.test.ts,src/codeintel/__tests__/call-hierarchy.test.ts,src/codeintel/__tests__/find-references.test.ts,src/codeintel/__tests__/go-to-definition.test.ts,src/codeintel/__tests__/hover.test.ts,src/codeintel/__tests__/import-graph.test.ts,src/codeintel/__tests__/outline.test.ts,src/codeintel/__tests__/rename-symbol.test.ts,src/codemap/__tests__/build.test.ts,src/codesearch/__tests__/airunner-embedder.test.ts,src/codesearch/__tests__/chunk.test.ts,src/codesearch/__tests__/files.test.ts,src/codesearch/__tests__/ollama-embedder.test.ts,src/dashboard/__tests__/aggregate.test.ts,src/dashboard/__tests__/chat-thread.test.ts,src/dashboard/__tests__/checkpoints-routes.test.ts,src/dashboard/__tests__/codemap-routes.test.ts,src/dashboard/__tests__/cost-history-routes.test.ts,src/dashboard/__tests__/events.test.ts,src/dashboard/__tests__/projects-routes.test.ts,src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts,src/dashboard/__tests__/timeline.test.ts,src/deploy/__tests__/gate.test.ts,src/engine/__tests__/condense-async.test.ts,src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts,src/engine/__tests__/inject-message.test.ts,src/engine/__tests__/local-explore.test.ts,src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/pause.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts,src/github/__tests__/app-auth.test.ts,src/github/__tests__/cli.test.ts,src/github/__tests__/pr.test.ts,src/github/__tests__/provision.test.ts,src/github/__tests__/push.test.ts,src/index-util.test.ts,src/init/__tests__/cli.test.ts,src/init/__tests__/gitignore.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/local.test.ts,src/memory/__tests__/memory-wiring.test.ts,src/memory/__tests__/summarizer.test.ts,src/memory/__tests__/uwuchat.test.ts,src/migrate/cli.test.ts,src/orchestrator/__tests__/arg-cursor.test.ts,src/orchestrator/__tests__/auto-split.test.ts,src/orchestrator/__tests__/cleanup.test.ts,src/orchestrator/__tests__/git-sync.test.ts,src/orchestrator/__tests__/merge-check.test.ts,src/orchestrator/__tests__/pipeline.test.ts,src/orchestrator/__tests__/reviewer.test.ts,src/orchestrator/__tests__/split.test.ts,src/orchestrator/__tests__/state.test.ts,src/orchestrator/__tests__/status-cli.test.ts,src/orchestrator/__tests__/stop-cli.test.ts,src/permissions/__tests__/commands.test.ts,src/permissions/__tests__/store-protection.test.ts,src/project-store.test.ts,src/projects/__tests__/cli.test.ts,src/qa/__tests__/qa.test.ts,src/tools/__tests__/apply_diff.test.ts,src/tools/__tests__/browser-action.test.ts,src/tools/__tests__/decision.test.ts,src/tools/__tests__/describe_image.test.ts,src/tools/__tests__/edit_file.test.ts,src/tools/__tests__/execute_command.test.ts,src/tools/__tests__/language-detect.test.ts,src/tools/__tests__/list-files.test.ts,src/tools/__tests__/local-summarization-gate.test.ts,src/tools/__tests__/output-summarizer.test.ts,src/tools/__tests__/read-cache.test.ts,src/tools/__tests__/run-tests.test.ts,src/tools/__tests__/search_replace.test.ts,src/tools/__tests__/update_todo_list.test.ts,src/tools/__tests__/write_to_file.test.ts,src/vision/__tests__/describe.test.ts,src/watcher/__tests__/github.test.ts,src/watcher/__tests__/state.test.ts,src/watcher/__tests__/watch.test.ts", + "clone 6287c1413aa4 src/budget/__tests__/budget.test.ts,src/budget/__tests__/concurrency.test.ts,src/budget/__tests__/cost.test.ts,src/checkpoints/__tests__/service.test.ts,src/cloud/__tests__/provider.test.ts,src/codeintel/__tests__/call-graph.test.ts,src/codeintel/__tests__/call-hierarchy.test.ts,src/codeintel/__tests__/find-references.test.ts,src/codeintel/__tests__/go-to-definition.test.ts,src/codeintel/__tests__/hover.test.ts,src/codeintel/__tests__/import-graph.test.ts,src/codeintel/__tests__/outline.test.ts,src/codeintel/__tests__/rename-symbol.test.ts,src/codemap/__tests__/build.test.ts,src/codesearch/__tests__/airunner-embedder.test.ts,src/codesearch/__tests__/chunk.test.ts,src/codesearch/__tests__/files.test.ts,src/codesearch/__tests__/ollama-embedder.test.ts,src/dashboard/__tests__/aggregate.test.ts,src/dashboard/__tests__/chat-thread.test.ts,src/dashboard/__tests__/checkpoints-routes.test.ts,src/dashboard/__tests__/codemap-routes.test.ts,src/dashboard/__tests__/cost-history-routes.test.ts,src/dashboard/__tests__/events.test.ts,src/dashboard/__tests__/projects-routes.test.ts,src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts,src/dashboard/__tests__/timeline.test.ts,src/deploy/__tests__/gate.test.ts,src/engine/__tests__/condense-async.test.ts,src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts,src/engine/__tests__/inject-message.test.ts,src/engine/__tests__/local-explore.test.ts,src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/pause.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts,src/github/__tests__/app-auth.test.ts,src/github/__tests__/cli.test.ts,src/github/__tests__/pr.test.ts,src/github/__tests__/provision.test.ts,src/github/__tests__/push.test.ts,src/index-util.test.ts,src/init/__tests__/cli.test.ts,src/init/__tests__/gitignore.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/local.test.ts,src/memory/__tests__/memory-wiring.test.ts,src/memory/__tests__/summarizer.test.ts,src/memory/__tests__/uwuchat.test.ts,src/migrate/cli.test.ts,src/orchestrator/__tests__/arg-cursor.test.ts,src/orchestrator/__tests__/auto-split.test.ts,src/orchestrator/__tests__/cleanup.test.ts,src/orchestrator/__tests__/git-sync.test.ts,src/orchestrator/__tests__/merge-check.test.ts,src/orchestrator/__tests__/pipeline.test.ts,src/orchestrator/__tests__/reviewer.test.ts,src/orchestrator/__tests__/split.test.ts,src/orchestrator/__tests__/state.test.ts,src/orchestrator/__tests__/status-cli.test.ts,src/orchestrator/__tests__/stop-cli.test.ts,src/permissions/__tests__/commands.test.ts,src/permissions/__tests__/store-protection.test.ts,src/project-store.test.ts,src/projects/__tests__/cli.test.ts,src/qa/__tests__/qa.test.ts,src/tools/__tests__/apply_diff.test.ts,src/tools/__tests__/browser-action.test.ts,src/tools/__tests__/decision.test.ts,src/tools/__tests__/describe_image.test.ts,src/tools/__tests__/edit_file.test.ts,src/tools/__tests__/execute_command.test.ts,src/tools/__tests__/language-detect.test.ts,src/tools/__tests__/list-files.test.ts,src/tools/__tests__/local-summarization-gate.test.ts,src/tools/__tests__/output-summarizer.test.ts,src/tools/__tests__/read-cache.test.ts,src/tools/__tests__/run-tests.test.ts,src/tools/__tests__/search_replace.test.ts,src/tools/__tests__/update_todo_list.test.ts,src/tools/__tests__/write_to_file.test.ts,src/vision/__tests__/describe.test.ts,src/watcher/__tests__/github.test.ts,src/watcher/__tests__/state.test.ts,src/watcher/__tests__/watch.test.ts", "clone 62d28c84ccfe src/orchestrator/cli.ts", - "clone 62ff58c84315 src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts,src/stack-rules-content.test.ts", + "clone 62ff58c84315 src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts,src/stack-rules-content.test.ts", "clone 6376e4c9bfd3 src/github/__tests__/provision.test.ts", "clone 63fb58a6271f src/orchestrator/__tests__/cli.test.ts", "clone 6433777a2a28 src/orchestrator/__tests__/spawn-model.test.ts,src/orchestrator/__tests__/spawn-state-reset.test.ts", @@ -453,7 +453,7 @@ "clone 7d9fa9503333 src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts", "clone 7ddc99484482 src/dashboard/server.ts", "clone 7dfc9cbdc2b8 src/orchestrator/task-files.ts", - "clone 7e1bcf45c7b2 src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", + "clone 7e1bcf45c7b2 src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", "clone 7e604775e29f src/orchestrator/cli.ts", "clone 7e9f9c4012ef src/orchestrator/__tests__/analyze-cli.test.ts", "clone 7f13e4c18807 src/orchestrator/__tests__/cli.test.ts", @@ -522,7 +522,7 @@ "clone 8fbaf4437025 src/dashboard/__tests__/cost-history-routes.test.ts,src/orchestrator/__tests__/cost-history-cli.test.ts", "clone 8fe789c46bc4 src/orchestrator/__tests__/reviewer.test.ts,src/qa/__tests__/qa.test.ts", "clone 9003da86dcf3 src/codesearch/files.ts,src/engine/stacks.ts", - "clone 902284a95450 src/budget/__tests__/budget.test.ts,src/budget/__tests__/concurrency.test.ts,src/budget/__tests__/cost.test.ts,src/checkpoints/__tests__/service.test.ts,src/cloud/__tests__/provider.test.ts,src/codeintel/__tests__/call-graph.test.ts,src/codeintel/__tests__/call-hierarchy.test.ts,src/codeintel/__tests__/find-references.test.ts,src/codeintel/__tests__/go-to-definition.test.ts,src/codeintel/__tests__/hover.test.ts,src/codeintel/__tests__/import-graph.test.ts,src/codeintel/__tests__/outline.test.ts,src/codeintel/__tests__/rename-symbol.test.ts,src/codemap/__tests__/build.test.ts,src/codesearch/__tests__/airunner-embedder.test.ts,src/codesearch/__tests__/chunk.test.ts,src/codesearch/__tests__/files.test.ts,src/codesearch/__tests__/ollama-embedder.test.ts,src/dashboard/__tests__/aggregate.test.ts,src/dashboard/__tests__/chat-thread.test.ts,src/dashboard/__tests__/checkpoints-routes.test.ts,src/dashboard/__tests__/codemap-routes.test.ts,src/dashboard/__tests__/cost-history-routes.test.ts,src/dashboard/__tests__/events.test.ts,src/dashboard/__tests__/projects-routes.test.ts,src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts,src/dashboard/__tests__/timeline.test.ts,src/deploy/__tests__/gate.test.ts,src/engine/__tests__/condense-async.test.ts,src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts,src/engine/__tests__/inject-message.test.ts,src/engine/__tests__/local-explore.test.ts,src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/pause.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts,src/github/__tests__/app-auth.test.ts,src/github/__tests__/cli.test.ts,src/github/__tests__/pr.test.ts,src/github/__tests__/provision.test.ts,src/github/__tests__/push.test.ts,src/index-util.test.ts,src/init/__tests__/cli.test.ts,src/init/__tests__/gitignore.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/local.test.ts,src/memory/__tests__/memory-wiring.test.ts,src/memory/__tests__/summarizer.test.ts,src/memory/__tests__/uwuchat.test.ts,src/migrate/cli.test.ts,src/orchestrator/__tests__/arg-cursor.test.ts,src/orchestrator/__tests__/auto-split.test.ts,src/orchestrator/__tests__/cleanup.test.ts,src/orchestrator/__tests__/git-sync.test.ts,src/orchestrator/__tests__/merge-check.test.ts,src/orchestrator/__tests__/pipeline.test.ts,src/orchestrator/__tests__/reviewer.test.ts,src/orchestrator/__tests__/split.test.ts,src/orchestrator/__tests__/state.test.ts,src/orchestrator/__tests__/status-cli.test.ts,src/orchestrator/__tests__/stop-cli.test.ts,src/permissions/__tests__/commands.test.ts,src/permissions/__tests__/store-protection.test.ts,src/project-store.test.ts,src/projects/__tests__/cli.test.ts,src/qa/__tests__/qa.test.ts,src/tools/__tests__/apply_diff.test.ts,src/tools/__tests__/browser-action.test.ts,src/tools/__tests__/decision.test.ts,src/tools/__tests__/describe_image.test.ts,src/tools/__tests__/edit_file.test.ts,src/tools/__tests__/execute_command.test.ts,src/tools/__tests__/language-detect.test.ts,src/tools/__tests__/list-files.test.ts,src/tools/__tests__/local-summarization-gate.test.ts,src/tools/__tests__/output-summarizer.test.ts,src/tools/__tests__/read-cache.test.ts,src/tools/__tests__/run-tests.test.ts,src/tools/__tests__/search_replace.test.ts,src/tools/__tests__/update_todo_list.test.ts,src/tools/__tests__/write_to_file.test.ts,src/vision/__tests__/describe.test.ts,src/watcher/__tests__/github.test.ts,src/watcher/__tests__/state.test.ts,src/watcher/__tests__/watch.test.ts", + "clone 902284a95450 src/budget/__tests__/budget.test.ts,src/budget/__tests__/concurrency.test.ts,src/budget/__tests__/cost.test.ts,src/checkpoints/__tests__/service.test.ts,src/cloud/__tests__/provider.test.ts,src/codeintel/__tests__/call-graph.test.ts,src/codeintel/__tests__/call-hierarchy.test.ts,src/codeintel/__tests__/find-references.test.ts,src/codeintel/__tests__/go-to-definition.test.ts,src/codeintel/__tests__/hover.test.ts,src/codeintel/__tests__/import-graph.test.ts,src/codeintel/__tests__/outline.test.ts,src/codeintel/__tests__/rename-symbol.test.ts,src/codemap/__tests__/build.test.ts,src/codesearch/__tests__/airunner-embedder.test.ts,src/codesearch/__tests__/chunk.test.ts,src/codesearch/__tests__/files.test.ts,src/codesearch/__tests__/ollama-embedder.test.ts,src/dashboard/__tests__/aggregate.test.ts,src/dashboard/__tests__/chat-thread.test.ts,src/dashboard/__tests__/checkpoints-routes.test.ts,src/dashboard/__tests__/codemap-routes.test.ts,src/dashboard/__tests__/cost-history-routes.test.ts,src/dashboard/__tests__/events.test.ts,src/dashboard/__tests__/projects-routes.test.ts,src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts,src/dashboard/__tests__/timeline.test.ts,src/deploy/__tests__/gate.test.ts,src/engine/__tests__/condense-async.test.ts,src/engine/__tests__/condense.test.ts,src/engine/__tests__/events.test.ts,src/engine/__tests__/inject-message.test.ts,src/engine/__tests__/local-explore.test.ts,src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/pause.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts,src/github/__tests__/app-auth.test.ts,src/github/__tests__/cli.test.ts,src/github/__tests__/pr.test.ts,src/github/__tests__/provision.test.ts,src/github/__tests__/push.test.ts,src/index-util.test.ts,src/init/__tests__/cli.test.ts,src/init/__tests__/gitignore.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/local.test.ts,src/memory/__tests__/memory-wiring.test.ts,src/memory/__tests__/summarizer.test.ts,src/memory/__tests__/uwuchat.test.ts,src/migrate/cli.test.ts,src/orchestrator/__tests__/arg-cursor.test.ts,src/orchestrator/__tests__/auto-split.test.ts,src/orchestrator/__tests__/cleanup.test.ts,src/orchestrator/__tests__/git-sync.test.ts,src/orchestrator/__tests__/merge-check.test.ts,src/orchestrator/__tests__/pipeline.test.ts,src/orchestrator/__tests__/reviewer.test.ts,src/orchestrator/__tests__/split.test.ts,src/orchestrator/__tests__/state.test.ts,src/orchestrator/__tests__/status-cli.test.ts,src/orchestrator/__tests__/stop-cli.test.ts,src/permissions/__tests__/commands.test.ts,src/permissions/__tests__/store-protection.test.ts,src/project-store.test.ts,src/projects/__tests__/cli.test.ts,src/qa/__tests__/qa.test.ts,src/tools/__tests__/apply_diff.test.ts,src/tools/__tests__/browser-action.test.ts,src/tools/__tests__/decision.test.ts,src/tools/__tests__/describe_image.test.ts,src/tools/__tests__/edit_file.test.ts,src/tools/__tests__/execute_command.test.ts,src/tools/__tests__/language-detect.test.ts,src/tools/__tests__/list-files.test.ts,src/tools/__tests__/local-summarization-gate.test.ts,src/tools/__tests__/output-summarizer.test.ts,src/tools/__tests__/read-cache.test.ts,src/tools/__tests__/run-tests.test.ts,src/tools/__tests__/search_replace.test.ts,src/tools/__tests__/update_todo_list.test.ts,src/tools/__tests__/write_to_file.test.ts,src/vision/__tests__/describe.test.ts,src/watcher/__tests__/github.test.ts,src/watcher/__tests__/state.test.ts,src/watcher/__tests__/watch.test.ts", "clone 90309d08a06e src/engine/local-explore.ts,src/llm/ollama.ts", "clone 90919b289545 src/engine/__tests__/loop.test.ts,src/tools/__tests__/execute_command.test.ts", "clone 90ad7e210e7a src/engine/__tests__/condense.test.ts,src/engine/__tests__/loop.test.ts", @@ -543,7 +543,7 @@ "clone 95c346543423 src/orchestrator/task-files.ts", "clone 95e9585718ac src/engine/loop.ts", "clone 96160c6998c6 src/dashboard/__tests__/server.test.ts,src/dashboard/__tests__/session-launch.test.ts", - "clone 9624c77068ad src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", + "clone 9624c77068ad src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", "clone 9653e98afda5 src/codesearch/__tests__/airunner-embedder.test.ts,src/codesearch/__tests__/ollama-embedder.test.ts", "clone 966fcd888ef7 src/orchestrator/cli.ts,src/orchestrator/resume.ts", "clone 9674b1ab1696 src/engine/loop.ts", @@ -705,7 +705,7 @@ "clone be4b5da32f15 src/orchestrator/cli.ts", "clone bf4577eed8ec src/engine/__tests__/loop.test.ts", "clone bf96094425f1 src/orchestrator/__tests__/cli.test.ts,src/orchestrator/__tests__/resume.test.ts", - "clone bfda8ae55db7 src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts", + "clone bfda8ae55db7 src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/loop.test.ts,src/engine/__tests__/new-task.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/engine/__tests__/switch-mode.test.ts", "clone c1b7cee2cf47 src/codesearch/files.ts,src/engine/stacks.ts", "clone c1e1b41c0b87 src/orchestrator/__tests__/spawn-model.test.ts,src/orchestrator/__tests__/spawn-state-reset.test.ts", "clone c1f158e49080 src/orchestrator/reviewer.ts,src/qa/qa.ts", @@ -724,7 +724,7 @@ "clone c5206c729975 src/dashboard/__tests__/cost-history-routes.test.ts,src/orchestrator/__tests__/cost-history-cli.test.ts", "clone c5f99dad818d src/engine/__tests__/new-task.test.ts,src/engine/__tests__/switch-mode.test.ts", "clone c5fae546f069 src/watcher/watch.ts", - "clone c620cb9d14ba src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", + "clone c620cb9d14ba src/engine/__tests__/loop-helpers-history.test.ts,src/engine/__tests__/loop-helpers-readonly.test.ts,src/engine/__tests__/parser.test.ts,src/engine/__tests__/summarize-tool-arg.test.ts,src/memory/__tests__/embed.test.ts,src/memory/__tests__/summarizer.test.ts", "clone c68a90c711b3 src/codesearch/files.ts,src/engine/stacks.ts", "clone c6af3683a9b2 src/rsi/__tests__/worker.test.ts", "clone c6b95dabb537 src/engine/local-explore.ts,src/llm/ollama.ts", @@ -1050,7 +1050,7 @@ "src/engine/loop.ts complexity 214b8e849a5b", "src/engine/loop.ts complexity 5f3c07fedbeb", "src/engine/loop.ts complexity 67a65c1b3515", - "src/engine/loop.ts complexity 7401a4a1090e", + "src/engine/loop.ts complexity 821721586672", "src/engine/loop.ts complexity 906b9934f22d", "src/engine/loop.ts complexity 961afaf5a2dc", "src/engine/loop.ts complexity a27a56ef4199", diff --git a/scripts/code-standards-baseline.json b/scripts/code-standards-baseline.json index 6a18bae..d5d4405 100644 --- a/scripts/code-standards-baseline.json +++ b/scripts/code-standards-baseline.json @@ -5789,6 +5789,8 @@ "src/engine/logger.ts function-lines beec0121cd12", "src/engine/logger.ts line-length 70c3b81c1d49", "src/engine/logger.ts line-length beec0121cd12", + "src/engine/loop-helpers.ts function-lines 9043e7e0e420", + "src/engine/loop-helpers.ts function-lines fab59c019929", "src/engine/loop.ts class-lines 496b65652f93", "src/engine/loop.ts file-lines", "src/engine/loop.ts function-lines 072059d63067", @@ -5797,33 +5799,33 @@ "src/engine/loop.ts function-lines 214b8e849a5b", "src/engine/loop.ts function-lines 2629b2fd9415", "src/engine/loop.ts function-lines 31c410909812", + "src/engine/loop.ts function-lines 369668345e47", "src/engine/loop.ts function-lines 3a30452a3906", - "src/engine/loop.ts function-lines 3d2ffb9ed78d", "src/engine/loop.ts function-lines 57f92880a896", "src/engine/loop.ts function-lines 5f3c07fedbeb", "src/engine/loop.ts function-lines 662156214600", "src/engine/loop.ts function-lines 671012f3f150", "src/engine/loop.ts function-lines 67a65c1b3515", "src/engine/loop.ts function-lines 6955ee0a0f8f", - "src/engine/loop.ts function-lines 7401a4a1090e", "src/engine/loop.ts function-lines 802c109934b9", + "src/engine/loop.ts function-lines 821721586672", "src/engine/loop.ts function-lines 94acf87a9f49", "src/engine/loop.ts function-lines 961afaf5a2dc", "src/engine/loop.ts function-lines 9a2168b24ac6", "src/engine/loop.ts function-lines 9bd42684dd58", - "src/engine/loop.ts function-lines a249517592a1", "src/engine/loop.ts function-lines a27a56ef4199", "src/engine/loop.ts function-lines a3b9c8fa6f2c", "src/engine/loop.ts function-lines a71245b1fd88", - "src/engine/loop.ts function-lines aa27c6b1efa5", "src/engine/loop.ts function-lines b4ab1604b46f", "src/engine/loop.ts function-lines b94ac5f967f5", "src/engine/loop.ts function-lines c34d14b72496", + "src/engine/loop.ts function-lines c4e520c5240b", "src/engine/loop.ts function-lines cfbb056451f4", "src/engine/loop.ts function-lines d3b914c18e78", "src/engine/loop.ts function-lines dc8cff131278", "src/engine/loop.ts function-lines dee88a349844", "src/engine/loop.ts function-lines e5af64dc88c1", + "src/engine/loop.ts function-lines eb067ad4b16c", "src/engine/loop.ts function-lines f3ab8bbb8f3c", "src/engine/loop.ts function-lines f85331ca0776", "src/engine/loop.ts line-length 0018968da493", @@ -5856,6 +5858,7 @@ "src/engine/loop.ts line-length 12cfb7d0e091", "src/engine/loop.ts line-length 13e1d1d803dd", "src/engine/loop.ts line-length 14b90d4fe263", + "src/engine/loop.ts line-length 14eb8db19c71", "src/engine/loop.ts line-length 1526334a2e4d", "src/engine/loop.ts line-length 158a429011e6", "src/engine/loop.ts line-length 15e555ff1994", @@ -5886,7 +5889,6 @@ "src/engine/loop.ts line-length 2520aa744448", "src/engine/loop.ts line-length 25fbea00dd2f", "src/engine/loop.ts line-length 260421a85197", - "src/engine/loop.ts line-length 268d938d6910", "src/engine/loop.ts line-length 26fa37a95351", "src/engine/loop.ts line-length 277e2be2ecbf", "src/engine/loop.ts line-length 279234cdebeb", @@ -5925,6 +5927,7 @@ "src/engine/loop.ts line-length 35a275010065", "src/engine/loop.ts line-length 35d3a8309bf3", "src/engine/loop.ts line-length 35e9daf71e1a", + "src/engine/loop.ts line-length 369668345e47", "src/engine/loop.ts line-length 36eae49735b2", "src/engine/loop.ts line-length 36f4950d8f4c", "src/engine/loop.ts line-length 370109abea60", @@ -5979,7 +5982,6 @@ "src/engine/loop.ts line-length 5535cb66cd5b", "src/engine/loop.ts line-length 559d1b7ae9dc", "src/engine/loop.ts line-length 56712955d1c8", - "src/engine/loop.ts line-length 576768ec8bdc", "src/engine/loop.ts line-length 57973955fe58", "src/engine/loop.ts line-length 58ae6fcc04ad", "src/engine/loop.ts line-length 58ed34c5a3a1", @@ -6033,7 +6035,6 @@ "src/engine/loop.ts line-length 70acca1c6abf", "src/engine/loop.ts line-length 714500010fb4", "src/engine/loop.ts line-length 729a7b7b232b", - "src/engine/loop.ts line-length 7401a4a1090e", "src/engine/loop.ts line-length 7414371560b8", "src/engine/loop.ts line-length 741d312b12bb", "src/engine/loop.ts line-length 7608bc6e2c84", @@ -6044,6 +6045,7 @@ "src/engine/loop.ts line-length 7b9b01db9458", "src/engine/loop.ts line-length 7bcb30e055ed", "src/engine/loop.ts line-length 7ca52fc81b41", + "src/engine/loop.ts line-length 7cfdfa4acd46", "src/engine/loop.ts line-length 7f03d34ec0cd", "src/engine/loop.ts line-length 7f2b6160b1aa", "src/engine/loop.ts line-length 7f457d5844f1", @@ -6051,6 +6053,7 @@ "src/engine/loop.ts line-length 80d0db899bb9", "src/engine/loop.ts line-length 814a267f842f", "src/engine/loop.ts line-length 82120a95b35e", + "src/engine/loop.ts line-length 821721586672", "src/engine/loop.ts line-length 8282ac15f6f5", "src/engine/loop.ts line-length 82af17c72759", "src/engine/loop.ts line-length 836c10c3d0ca", @@ -6096,6 +6099,7 @@ "src/engine/loop.ts line-length 95ea61c9ee45", "src/engine/loop.ts line-length 961afaf5a2dc", "src/engine/loop.ts line-length 971604c99ff2", + "src/engine/loop.ts line-length 97a5b3ff8fe5", "src/engine/loop.ts line-length 98ec9e82dc1d", "src/engine/loop.ts line-length 994fd659e030", "src/engine/loop.ts line-length 995c48d31301", @@ -6116,7 +6120,6 @@ "src/engine/loop.ts line-length a19df91269f6", "src/engine/loop.ts line-length a1ee36ae2427", "src/engine/loop.ts line-length a24243dc1ee6", - "src/engine/loop.ts line-length a249517592a1", "src/engine/loop.ts line-length a2a60d4d6dc9", "src/engine/loop.ts line-length a2e7b84359cb", "src/engine/loop.ts line-length a36b0efee72a", @@ -6124,6 +6127,7 @@ "src/engine/loop.ts line-length a38d5245a07e", "src/engine/loop.ts line-length a3a23cd06d4f", "src/engine/loop.ts line-length a3ad15831c9b", + "src/engine/loop.ts line-length a3fb133b7a6a", "src/engine/loop.ts line-length a47feb7f175d", "src/engine/loop.ts line-length a5479cec9989", "src/engine/loop.ts line-length a5ba9287307f", @@ -6158,13 +6162,13 @@ "src/engine/loop.ts line-length b782210eae8b", "src/engine/loop.ts line-length b7b2b1237458", "src/engine/loop.ts line-length b7ce05e941dd", - "src/engine/loop.ts line-length b89ae8da6acc", "src/engine/loop.ts line-length b8bc3232e8b1", "src/engine/loop.ts line-length b9a90e6117e9", "src/engine/loop.ts line-length b9d284122ceb", "src/engine/loop.ts line-length ba16322419b3", "src/engine/loop.ts line-length ba5ca552477a", "src/engine/loop.ts line-length ba5e78f170df", + "src/engine/loop.ts line-length bea357dae2e8", "src/engine/loop.ts line-length bf21f82a85ae", "src/engine/loop.ts line-length c0d7f28e3b30", "src/engine/loop.ts line-length c153e8e7dc84", @@ -6242,7 +6246,6 @@ "src/engine/loop.ts line-length df9e29e1f1f3", "src/engine/loop.ts line-length e17b5ba069fc", "src/engine/loop.ts line-length e1c86f624d86", - "src/engine/loop.ts line-length e2139950f190", "src/engine/loop.ts line-length e25ec4705a8d", "src/engine/loop.ts line-length e265d91c6076", "src/engine/loop.ts line-length e3161f88100d", @@ -6317,7 +6320,6 @@ "src/engine/loop.ts line-length fd9d27068ffb", "src/engine/loop.ts line-length fdd8b3a449c1", "src/engine/loop.ts line-length fdf0f88b9591", - "src/engine/loop.ts line-length feb10f7da988", "src/engine/loop.ts line-length fed3d0288d6e", "src/engine/loop.ts line-length ff37c912bfaa", "src/engine/loop.ts line-length ff6eb7f51a30", @@ -9975,7 +9977,6 @@ "src/orchestrator/__tests__/muse-executor.test.ts line-length b233eec97607", "src/orchestrator/__tests__/muse-executor.test.ts line-length b3268889eb14", "src/orchestrator/__tests__/muse-executor.test.ts line-length b797c8ffe6a6", - "src/orchestrator/__tests__/muse-executor.test.ts line-length bc24b01e49ad", "src/orchestrator/__tests__/muse-executor.test.ts line-length bee9d01134de", "src/orchestrator/__tests__/muse-executor.test.ts line-length c3c8c4513f2e", "src/orchestrator/__tests__/muse-executor.test.ts line-length c5168913ccae", @@ -20055,17 +20056,12 @@ "src/run-tests-script.test.ts line-length 1cf58f72574e", "src/run-tests-script.test.ts line-length 2038ad170e41", "src/run-tests-script.test.ts line-length 22dc3843a0f2", - "src/run-tests-script.test.ts line-length 26f1bec9d982", "src/run-tests-script.test.ts line-length 2780a8ed82b7", - "src/run-tests-script.test.ts line-length 3474f983b517", "src/run-tests-script.test.ts line-length 39319cbcff44", - "src/run-tests-script.test.ts line-length 5a4b2fe9fa9f", "src/run-tests-script.test.ts line-length 6102d38c67d6", "src/run-tests-script.test.ts line-length 7709f634c6f6", - "src/run-tests-script.test.ts line-length 7daac9336bfe", "src/run-tests-script.test.ts line-length 9ecc8b198c06", "src/run-tests-script.test.ts line-length d7702b0ba27f", - "src/run-tests-script.test.ts line-length d7aef0a9c23d", "src/run-tests-script.test.ts line-length ec0f99139ed4", "src/run-tests-script.test.ts line-length ec1027840bb4", "src/security/__tests__/profile.test.ts function-lines 64d20085f3f3", diff --git a/src/engine/__tests__/loop-helpers-history.test.ts b/src/engine/__tests__/loop-helpers-history.test.ts new file mode 100644 index 0000000..5006527 --- /dev/null +++ b/src/engine/__tests__/loop-helpers-history.test.ts @@ -0,0 +1,116 @@ +/** + * Tests for loop history/comparison helpers (src/engine/loop.ts). + * Tests-first for the loop-helpers extraction (#127 slice). + * Plain assert script run via `npm test` -> tsx (no framework). + */ + +import assert from "node:assert/strict" + +import { + estimateTokenSize, + messagesPrefixEquals, + sameToolNames, + stripSupersededReasoning, + truncateHistory, +} from "../loop.js" +import type { ChatMessage, ChatTool } from "../types.js" + +function msg(role: ChatMessage["role"], content: string | null): ChatMessage { + return { role, content } +} + +function tool(name: string): ChatTool { + return { type: "function", function: { name } } +} + +function testStripKeepsMostRecent(): void { + const a: ChatMessage = { role: "assistant", content: "a", reasoning: "r1" } + const b: ChatMessage = { role: "assistant", content: "b", reasoning: "r2" } + const messages = [a, b] + stripSupersededReasoning(messages) + assert.equal(messages[0].reasoning, undefined) + assert.equal(messages[1].reasoning, "r2") +} + +function testStripIgnoresNonAssistant(): void { + const messages = [msg("system", "s"), msg("user", "u")] + stripSupersededReasoning(messages) + assert.equal(messages.length, 2) +} + +function testTruncateNoOpBelowWindow(): void { + const messages = [msg("system", "s"), msg("user", "u"), msg("user", "v")] + const result = truncateHistory(messages, 10) + assert.equal(result.length, 3) +} + +function testTruncateKeepsSystemAndFirstUser(): void { + const messages = [msg("system", "s"), msg("user", "first")] + for (let i = 0; i < 25; i++) messages.push(msg("user", `m${i}`)) + const result = truncateHistory(messages, 10) + assert.equal(result[0], messages[0]) + assert.equal(result[1], messages[1]) + assert.ok(result.length <= 20) +} + +function testEstimateTokenSize(): void { + assert.equal(estimateTokenSize(0), 0) + assert.equal(estimateTokenSize(4), 1) + assert.equal(estimateTokenSize(5), 2) + assert.equal(estimateTokenSize(400), 100) +} + +function testSameToolNames(): void { + const ab = [tool("a"), tool("b")] + const ba = [tool("b"), tool("a")] + assert.equal(sameToolNames(ab, ba), true) + assert.equal(sameToolNames([tool("a")], [tool("a"), tool("b")]), false) + assert.equal(sameToolNames([tool("a")], [tool("b")]), false) + assert.equal(sameToolNames([], []), true) +} + +function testPrefixEquals(): void { + const a = msg("user", "a") + const b = msg("user", "b") + assert.equal(messagesPrefixEquals([a, b], [a]), true) + assert.equal(messagesPrefixEquals([a], [a, b]), false) + assert.equal(messagesPrefixEquals([a, msg("user", "x")], [a, b]), false) + assert.equal(messagesPrefixEquals([], []), true) +} + +function testPrefixEqualsEmptySnapshot(): void { + const a = msg("user", "a") + assert.equal(messagesPrefixEquals([a], []), true) +} + +const tests: Array<[string, () => void]> = [ + ["strip: keeps most recent", testStripKeepsMostRecent], + ["strip: ignores non-assistant", testStripIgnoresNonAssistant], + ["truncate: no-op below window", testTruncateNoOpBelowWindow], + ["truncate: keeps system+first-user", testTruncateKeepsSystemAndFirstUser], + ["estimateTokenSize heuristic", testEstimateTokenSize], + ["sameToolNames comparison", testSameToolNames], + ["messagesPrefixEquals", testPrefixEquals], + ["messagesPrefixEquals empty snapshot", testPrefixEqualsEmptySnapshot], +] + +function main(): void { + let failed = 0 + for (const [name, fn] of tests) { + try { + fn() + console.log(` ok ${name}`) + } catch (err) { + failed++ + console.error(` FAIL ${name}`) + console.error(err instanceof Error ? err.stack ?? err.message : String(err)) + } + } + if (failed > 0) { + console.error(`\n${failed} test(s) failed`) + process.exit(1) + } + console.log(`\nAll ${tests.length} tests passed`) +} + +main() diff --git a/src/engine/__tests__/loop-helpers-readonly.test.ts b/src/engine/__tests__/loop-helpers-readonly.test.ts new file mode 100644 index 0000000..9ac65bd --- /dev/null +++ b/src/engine/__tests__/loop-helpers-readonly.test.ts @@ -0,0 +1,169 @@ +/** + * Tests for loop read-only/guardrail helpers (src/engine/loop.ts). + * Tests-first for the loop-helpers extraction (#127 slice). + * Plain assert script run via `npm test` -> tsx (no framework). + */ + +import assert from "node:assert/strict" + +import { + artifactRejectionNudgeMessage, + combineAbortSignals, + immutableBoundaryArguments, + isReadOnlyDirectCall, + isReadOnlyTurnCall, +} from "../loop.js" + +function testCombineAbortedWhenInputAborted(): void { + const c = new AbortController() + c.abort("boom") + const { signal, dispose } = combineAbortSignals([c.signal]) + assert.equal(signal.aborted, true) + dispose() +} + +function testCombineAbortsOnLaterAbort(): void { + const c = new AbortController() + const { signal, dispose } = combineAbortSignals([c.signal]) + assert.equal(signal.aborted, false) + c.abort("stop") + assert.equal(signal.aborted, true) + dispose() +} + +function testCombineDisposeRemovesListeners(): void { + const c = new AbortController() + const { signal, dispose } = combineAbortSignals([c.signal]) + dispose() + c.abort("late") + assert.equal(signal.aborted, false) +} + +function testCombineSkipsUndefined(): void { + const { signal, dispose } = combineAbortSignals([undefined]) + assert.equal(signal.aborted, false) + dispose() +} + +function testImmutableFreezesNested(): void { + const args = { a: { b: [1, 2] }, c: "x" } + const frozen = immutableBoundaryArguments(args) + assert.deepEqual(frozen, args) + assert.equal(Object.isFrozen(frozen), true) + assert.equal(Object.isFrozen(frozen["a"]), true) +} + +function testImmutableCopiesWithoutSharing(): void { + const inner = { b: 1 } + const frozen = immutableBoundaryArguments({ inner }) + assert.notEqual(frozen["inner"], inner) + inner.b = 2 + assert.equal((frozen["inner"] as { b: number }).b, 1) +} + +function testDirectReadOnlyTools(): void { + const args = { path: "src/app.ts" } + assert.equal(isReadOnlyDirectCall({ name: "read_file", args }), true) + assert.equal(isReadOnlyDirectCall({ name: "list_files", args }), true) + assert.equal(isReadOnlyDirectCall({ name: "write_to_file", args }), false) +} + +function testDirectReadOnlyShell(): void { + const read = { command: "ls -la" } + const readCall = { name: "execute_command", args: read } + assert.equal(isReadOnlyDirectCall(readCall), true) + const write = { command: "rm -rf build" } + const writeCall = { name: "execute_command", args: write } + assert.equal(isReadOnlyDirectCall(writeCall), false) +} + +function testTurnNonBatchDelegates(): void { + const read = { name: "read_file", args: { path: "x" } } + assert.equal(isReadOnlyTurnCall(read), true) + const edit = { name: "edit_file", args: { path: "x" } } + assert.equal(isReadOnlyTurnCall(edit), false) +} + +function testTurnBatchAllReadOnly(): void { + const call = { + name: "batch", + args: { calls: [{ tool: "read_file", args: { path: "a" } }] }, + } + assert.equal(isReadOnlyTurnCall(call), true) +} + +function testTurnBatchMixedIsProductive(): void { + const call = { + name: "batch", + args: { + calls: [ + { tool: "read_file", args: { path: "a" } }, + { tool: "write_to_file", args: { path: "b" } }, + ], + }, + } + assert.equal(isReadOnlyTurnCall(call), false) +} + +function testTurnBatchMalformedIsProductive(): void { + assert.equal(isReadOnlyTurnCall({ name: "batch", args: { calls: [] } }), false) + assert.equal(isReadOnlyTurnCall({ name: "batch", args: {} }), false) + const nested = { + name: "batch", + args: { calls: [{ tool: "batch", args: { calls: [] } }] }, + } + assert.equal(isReadOnlyTurnCall(nested), false) +} + +function testTurnBatchOversizedIsProductive(): void { + const calls = Array.from({ length: 11 }, () => ({ + tool: "read_file", + args: { path: "a" }, + })) + assert.equal(isReadOnlyTurnCall({ name: "batch", args: { calls } }), false) +} + +function testArtifactNudgeNamesStreakAndPath(): void { + const msg = artifactRejectionNudgeMessage(3, "plans/x.md") + assert.ok(msg.includes("3")) + assert.ok(msg.includes("plans/x.md")) + assert.ok(msg.includes("read_file")) +} + +const tests: Array<[string, () => void]> = [ + ["combine: already-aborted input", testCombineAbortedWhenInputAborted], + ["combine: later abort propagates", testCombineAbortsOnLaterAbort], + ["combine: dispose removes listeners", testCombineDisposeRemovesListeners], + ["combine: undefined skipped", testCombineSkipsUndefined], + ["immutable: nested frozen", testImmutableFreezesNested], + ["immutable: no shared refs", testImmutableCopiesWithoutSharing], + ["direct: read-only tools", testDirectReadOnlyTools], + ["direct: read-only shell", testDirectReadOnlyShell], + ["turn: non-batch delegates", testTurnNonBatchDelegates], + ["turn: all-read-only batch", testTurnBatchAllReadOnly], + ["turn: mixed batch productive", testTurnBatchMixedIsProductive], + ["turn: malformed batch productive", testTurnBatchMalformedIsProductive], + ["turn: oversized batch productive", testTurnBatchOversizedIsProductive], + ["artifact nudge names streak+path", testArtifactNudgeNamesStreakAndPath], +] + +function main(): void { + let failed = 0 + for (const [name, fn] of tests) { + try { + fn() + console.log(` ok ${name}`) + } catch (err) { + failed++ + console.error(` FAIL ${name}`) + console.error(err instanceof Error ? err.stack ?? err.message : String(err)) + } + } + if (failed > 0) { + console.error(`\n${failed} test(s) failed`) + process.exit(1) + } + console.log(`\nAll ${tests.length} tests passed`) +} + +main() diff --git a/src/engine/loop-helpers.ts b/src/engine/loop-helpers.ts new file mode 100644 index 0000000..6e62926 --- /dev/null +++ b/src/engine/loop-helpers.ts @@ -0,0 +1,238 @@ +/** + * Pure sync helpers moved from src/engine/loop.ts (#127 slice). + * Behavior-identical moves; loop.ts re-exports them. + * Leaf imports only (never loop.ts): no import cycle. + */ + +import { isReadOnlyEquivalentShellCommand } from "./tool-classes.js" +import type { ChatMessage, ChatTool } from "./types.js" +import type { ToolExecutor } from "../tools/executor.js" +import type { + ExtensionRevision, + ExtensionSummary, +} from "../extensions/registry.js" +import { MAX_BATCH_CALLS } from "../tools/batch.js" + +export function combineAbortSignals( + signals: readonly (AbortSignal | undefined)[], +): { signal: AbortSignal; dispose: () => void } { + const controller = new AbortController() + type Entry = { signal: AbortSignal; listener: () => void } + const listeners: Entry[] = [] + for (const signal of signals) { + if (!signal) continue + if (signal.aborted) { + controller.abort(signal.reason) + break + } + const listener = (): void => { + controller.abort(signal.reason) + } + signal.addEventListener("abort", listener, { once: true }) + listeners.push({ signal, listener }) + } + const dispose = (): void => { + for (const e of listeners) { + e.signal.removeEventListener("abort", e.listener) + } + } + return { signal: controller.signal, dispose } +} + +/** Monitor observations cannot mutate nested tool args. */ +export function immutableBoundaryArguments( + args: Record, +): Readonly> { + const copy = structuredClone(args) + const pending: unknown[] = [copy] + while (pending.length > 0) { + const value = pending.pop() + if (value === null || typeof value !== "object") continue + for (const child of Object.values(value)) { + pending.push(child) + } + Object.freeze(value) + } + return copy +} + +/** Tool names that count as "blind tree-walking". */ +const READ_ONLY_TOOL_NAMES = new Set([ + "list_files", + "read_file", + "search_files", +]) + +/** Direct-call read-only test for top-level and batch calls. */ +export function isReadOnlyDirectCall(call: { + name: string + args: Record +}): boolean { + if (READ_ONLY_TOOL_NAMES.has(call.name)) return true + if (call.name !== "execute_command") return false + if (typeof call.args.command !== "string") return false + return isReadOnlyEquivalentShellCommand(call.args.command) +} + +function asRecord(value: unknown): Record | undefined { + if (typeof value !== "object" || value === null) return undefined + if (Array.isArray(value)) return undefined + return value as Record +} + +function isBatchChildReadOnly(item: unknown): boolean { + const record = asRecord(item) + if (!record) return false + const tool = record["tool"] + if (typeof tool !== "string" || tool.trim() === "") return false + if (tool === "batch") return false + const subArgs = asRecord(record["args"] ?? {}) + if (!subArgs) return false + return isReadOnlyDirectCall({ + name: tool, + args: subArgs as Record, + }) +} + +/** Whether one top-level turn call counts as read-only. */ +export function isReadOnlyTurnCall(call: { + name: string + args: Record +}): boolean { + if (call.name !== "batch") { + return isReadOnlyDirectCall(call) + } + const raw = call.args.calls + if (!Array.isArray(raw) || raw.length === 0) return false + if (raw.length > MAX_BATCH_CALLS) return false + return raw.every(isBatchChildReadOnly) +} + +/** Artifact-gate rejection guardrail nudge (issue #152). */ +export function artifactRejectionNudgeMessage( + streak: number, + relativePath: string, +): string { + const parts = [ + `Your last ${streak} attempts to complete this `, + "task were rejected for the same reason, and ", + `each time you edited '${relativePath}' without `, + "making any read_file call in between. Adding a ", + "citation-shaped string or a section heading does ", + "NOT satisfy this requirement unless it reflects ", + "something real you actually verified — a ", + "citation with a real line number but a wrong ", + "description of what's there, or a section ", + "heading with placeholder content, is worse ", + "than not having it at all. Before your next ", + "edit: use read_file on the real source file(s) ", + "you intend to cite, confirm exactly what's ", + "at that line, and only then write content that ", + "genuinely reflects it. Do not just patch in ", + "the missing surface feature again.", + ] + return parts.join("") +} + +/** Keep reasoning only on the most-recent assistant message. */ +export function stripSupersededReasoning( + messages: ChatMessage[], +): void { + let lastAssistant = -1 + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === "assistant") { + lastAssistant = i + break + } + } + for (let i = 0; i < messages.length; i++) { + const stale = i !== lastAssistant + const isAssistant = messages[i].role === "assistant" + const hasReasoning = messages[i].reasoning !== undefined + if (stale && isAssistant && hasReasoning) { + messages[i].reasoning = undefined + } + } +} + +/** Approx token count from chars (~4 chars/token). */ +export function estimateTokenSize(chars: number): number { + return Math.ceil(chars / 4) +} + +/** True when two tool lists advertise the same name set. */ +export function sameToolNames(a: ChatTool[], b: ChatTool[]): boolean { + const names = (tools: ChatTool[]): string[] => + tools + .filter((t) => t.type === "function") + .map((t) => t.function.name) + .sort() + const na = names(a) + const nb = names(b) + if (na.length !== nb.length) return false + return na.every((n, i) => n === nb[i]) +} + +/** Stable-prefix check for background condense snapshots. */ +export function messagesPrefixEquals( + live: ChatMessage[], + snapshot: ChatMessage[], +): boolean { + if (live.length < snapshot.length) return false + return snapshot.every((m, i) => live[i] === m) +} + +const WORKSPACE_WRITE_TOOLS = new Set([ + "write_to_file", + "apply_diff", + "search_replace", + "edit_file", + "set_indentation", +]) + +export function hasWorkspaceWriteTool( + tools: readonly ChatTool[], +): boolean { + return tools.some( + (tool) => + tool.type === "function" && + WORKSPACE_WRITE_TOOLS.has(tool.function.name), + ) +} + +export function hasExecutorWorkspaceWriteTool( + executor: ToolExecutor, +): boolean { + return [...WORKSPACE_WRITE_TOOLS].some((name) => + executor.has(name), + ) +} + +export function extensionIdentity( + revision: ExtensionRevision, +): { kind: "skill" | "tool" | "mcp"; id: string; digest: string } { + return { + kind: revision.manifest.kind, + id: revision.manifest.id, + digest: revision.digest, + } +} + +export function extensionSummaryData( + summary: ExtensionSummary, +): { manifest: ExtensionSummary["manifest"]; digest: string } { + return { manifest: summary.manifest, digest: summary.digest } +} + +export function boundedExtensionJson(value: unknown): string { + const serialized = JSON.stringify(value) + const tooLarge = + typeof serialized !== "string" || + Buffer.byteLength(serialized, "utf8") > 512 * 1024 + if (tooLarge) { + throw new Error( + "extension result exceeds the 512 KiB session result limit", + ) + } + return serialized +} diff --git a/src/engine/loop.ts b/src/engine/loop.ts index ac5d9e2..690c70d 100644 --- a/src/engine/loop.ts +++ b/src/engine/loop.ts @@ -170,7 +170,7 @@ import { writeHandoffSummary } from "./handoff.js" import type { EvidenceInput, EvidenceJournal } from "../evidence/types.js" import { canonicalJson, sha256 } from "../evidence/canonical.js" -function combineAbortSignals(signals: readonly (AbortSignal | undefined)[]): { signal: AbortSignal; dispose: () => void } { +export function combineAbortSignals(signals: readonly (AbortSignal | undefined)[]): { signal: AbortSignal; dispose: () => void } { const controller = new AbortController() const listeners: Array<{ signal: AbortSignal; listener: () => void }> = [] for (const signal of signals) { @@ -205,7 +205,7 @@ export const DEFAULT_MAX_PAUSE_MS = 2 * 60 * 60 * 1000 export const DEFAULT_PAUSE_POLL_INTERVAL_MS = 5_000 /** Monitor observations cannot mutate nested tool arguments or share their references. */ -function immutableBoundaryArguments(args: Record): Readonly> { +export function immutableBoundaryArguments(args: Record): Readonly> { const copy = structuredClone(args) const pending: unknown[] = [copy] while (pending.length > 0) { @@ -358,7 +358,7 @@ export const DEFAULT_MAX_TOKENS = 32_768 const READ_ONLY_TOOL_NAMES = new Set(["list_files", "read_file", "search_files"]) /** Direct-call read-only test shared by top-level calls and batch children. */ -function isReadOnlyDirectCall(call: { name: string; args: Record }): boolean { +export function isReadOnlyDirectCall(call: { name: string; args: Record }): boolean { return ( READ_ONLY_TOOL_NAMES.has(call.name) || (call.name === "execute_command" && @@ -376,7 +376,7 @@ function isReadOnlyDirectCall(call: { name: string; args: Record }): boolean { +export function isReadOnlyTurnCall(call: { name: string; args: Record }): boolean { if (call.name !== "batch") { return isReadOnlyDirectCall(call) } @@ -521,7 +521,7 @@ const DAEMON_EMPTY_REPLY_FALLBACK_TEXTS = [ * DEFAULT_ARTIFACT_REJECTION_NUDGE_THRESHOLD's doc comment for the real * failure shape this addresses. */ -function artifactRejectionNudgeMessage(streak: number, relativePath: string): string { +export function artifactRejectionNudgeMessage(streak: number, relativePath: string): string { return ( `Your last ${streak} attempts to complete this task were rejected for the same reason, and each time you ` + `edited '${relativePath}' without making any read_file call in between. Adding a citation-shaped string or a ` + @@ -610,7 +610,7 @@ const CITATION_PATTERN = /\b[\w.-]+(?:\/[\w.-]+)+\.\w+:\d+(?:-\d+)?\b/g * all — a real, if partial, improvement: previously ANY citation-shaped * text counted, even a wholly invented file path. */ -async function countVerifiedCitations(content: string, workspaceRoot: string): Promise { +export async function countVerifiedCitations(content: string, workspaceRoot: string): Promise { const matches = content.match(CITATION_PATTERN) ?? [] let verified = 0 for (const citation of matches) { @@ -646,7 +646,7 @@ async function countVerifiedCitations(content: string, workspaceRoot: string): P * (via CITATION_PATTERN) so the caller can also enforce * requireArtifactMinCitations without a second filesystem pass. */ -async function matchingArtifactFileStatus( +export async function matchingArtifactFileStatus( workspaceRoot: string, pattern: string, ): Promise<{ found: boolean; citationCount: number; relativePath?: string; content?: string }> { @@ -706,7 +706,7 @@ const execFileP = promisify(execFile) * guardrail's structural backstop — independent of whether the prompt * instruction alone is followed. */ -async function hasUncommittedTrackedChanges( +export async function hasUncommittedTrackedChanges( workspaceRoot: string, sessionWrittenPaths: ReadonlySet, ): Promise { @@ -6384,7 +6384,7 @@ function boundedExtensionJson(value: unknown): string { return serialized } -function sameToolNames(a: ChatTool[], b: ChatTool[]): boolean { +export function sameToolNames(a: ChatTool[], b: ChatTool[]): boolean { const names = (tools: ChatTool[]): string[] => tools .filter((t) => t.type === "function") @@ -6412,7 +6412,7 @@ function sameToolNames(a: ChatTool[], b: ChatTool[]): boolean { * REPLACES whole prefix entries), so reference equality per index is * authoritative and cheap — no deep compare needed. */ -function messagesPrefixEquals(live: ChatMessage[], snapshot: ChatMessage[]): boolean { +export function messagesPrefixEquals(live: ChatMessage[], snapshot: ChatMessage[]): boolean { if (live.length < snapshot.length) { return false } @@ -6490,7 +6490,7 @@ export function truncateHistory(messages: ChatMessage[], windowSize: number): Ch * the caller's flag/default wins). Mirrors the other env fallbacks in * src/cli.ts (envNumber) — invalid values are ignored, not fatal. */ -function envMaxPauseMs(): number | undefined { +export function envMaxPauseMs(): number | undefined { const raw = process.env.HEADLESSCODE_MAX_PAUSE_MS if (raw === undefined || raw === "") { return undefined @@ -6506,7 +6506,7 @@ function envMaxPauseMs(): number | undefined { * values fall back to the default — a 0 or negative threshold would inject * the nudge on the very first read, which is noise, not guidance. */ -function envReadOnlyNudgeThreshold(): number { +export function envReadOnlyNudgeThreshold(): number { const raw = process.env.HEADLESSCODE_READ_ONLY_NUDGE_THRESHOLD if (raw === undefined || raw === "") { return DEFAULT_READ_ONLY_NUDGE_THRESHOLD @@ -6515,7 +6515,7 @@ function envReadOnlyNudgeThreshold(): number { return Number.isFinite(n) && n >= 1 ? Math.floor(n) : DEFAULT_READ_ONLY_NUDGE_THRESHOLD } -function envReadOnlyStallLimit(): number { +export function envReadOnlyStallLimit(): number { const raw = process.env.HEADLESSCODE_READ_ONLY_STALL_LIMIT if (raw === undefined || raw === "") { return DEFAULT_READ_ONLY_STALL_LIMIT @@ -6524,7 +6524,7 @@ function envReadOnlyStallLimit(): number { return Number.isFinite(n) && n >= 1 ? Math.floor(n) : DEFAULT_READ_ONLY_STALL_LIMIT } -function envToolFailureNudgeThreshold(): number { +export function envToolFailureNudgeThreshold(): number { const raw = process.env.HEADLESSCODE_TOOL_FAILURE_NUDGE_THRESHOLD if (raw === undefined || raw === "") { return DEFAULT_TOOL_FAILURE_NUDGE_THRESHOLD @@ -6533,7 +6533,7 @@ function envToolFailureNudgeThreshold(): number { return Number.isFinite(n) && n >= 1 ? Math.floor(n) : DEFAULT_TOOL_FAILURE_NUDGE_THRESHOLD } -function envArtifactRejectionNudgeThreshold(): number { +export function envArtifactRejectionNudgeThreshold(): number { const raw = process.env.HEADLESSCODE_ARTIFACT_REJECTION_NUDGE_THRESHOLD if (raw === undefined || raw === "") { return DEFAULT_ARTIFACT_REJECTION_NUDGE_THRESHOLD @@ -6542,7 +6542,7 @@ function envArtifactRejectionNudgeThreshold(): number { return Number.isFinite(n) && n >= 1 ? Math.floor(n) : DEFAULT_ARTIFACT_REJECTION_NUDGE_THRESHOLD } -function envIdenticalCallRepeatPenaltyBoost(): number { +export function envIdenticalCallRepeatPenaltyBoost(): number { const raw = process.env.HEADLESSCODE_IDENTICAL_CALL_REPEAT_PENALTY_BOOST if (raw === undefined || raw === "") { return DEFAULT_IDENTICAL_CALL_REPEAT_PENALTY_BOOST @@ -6557,7 +6557,7 @@ function envIdenticalCallRepeatPenaltyBoost(): number { * — including unset — means OFF). Default OFF deliberately: the blocking * request path (which the e2e fixtures and most tests assume) is unchanged. */ -function envStreaming(): boolean { +export function envStreaming(): boolean { const raw = process.env.HEADLESSCODE_STREAM if (raw === undefined || raw === "") { return false