@buildaharness/aielia 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +97 -19
  2. package/dist/action-approval-service.d.ts +34 -2
  3. package/dist/action-record.d.ts +13 -0
  4. package/dist/activity-log.d.ts +4 -0
  5. package/dist/agent-loop.d.ts +83 -1
  6. package/dist/assistant-session.d.ts +20 -1
  7. package/dist/assistant-types.d.ts +3 -1
  8. package/dist/assistant.d.ts +32 -0
  9. package/dist/{bin-CjKpxDxl.js → bin-C_hgDdSf.js} +369 -219
  10. package/dist/bin-C_hgDdSf.js.map +1 -0
  11. package/dist/cli-config.d.ts +3 -0
  12. package/dist/cli-session.d.ts +5 -4
  13. package/dist/cli.d.ts +9 -0
  14. package/dist/cli.js +1 -1
  15. package/dist/config.d.ts +3 -1
  16. package/dist/debug-log.d.ts +10 -1
  17. package/dist/email.d.ts +5 -3
  18. package/dist/file-tools-mcp-server.mjs +155 -30
  19. package/dist/file-tools.d.ts +2 -0
  20. package/dist/git-exclude.d.ts +9 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +127 -126
  23. package/dist/ink-select-prompt.d.ts +2 -0
  24. package/dist/ink-test-render.d.ts +2 -0
  25. package/dist/memory-service.d.ts +8 -0
  26. package/dist/next-step-proposer.d.ts +7 -0
  27. package/dist/plan-drafting-service.d.ts +3 -0
  28. package/dist/plan-store.d.ts +5 -0
  29. package/dist/{provider-setup-LFsaTkpf.js → provider-setup-wX2IBw2X.js} +2183 -919
  30. package/dist/provider-setup-wX2IBw2X.js.map +1 -0
  31. package/dist/reply-audit.d.ts +67 -0
  32. package/dist/side-call-policy.d.ts +30 -0
  33. package/dist/tool-step.d.ts +6 -0
  34. package/dist/trace-events.d.ts +1 -1
  35. package/dist/trust-tagging.d.ts +2 -0
  36. package/dist/{tui-app-BLR-PRFp.js → tui-app-C0J6HRzv.js} +127 -26
  37. package/dist/tui-app-C0J6HRzv.js.map +1 -0
  38. package/dist/tui-app.d.ts +35 -0
  39. package/dist/tui-mode-flag.d.ts +1 -1
  40. package/dist/turn-abort.d.ts +18 -0
  41. package/dist/turn-intent-classifier.d.ts +8 -0
  42. package/dist/turn-interpreter.d.ts +2 -0
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +3 -3
  45. package/dist/bin-CjKpxDxl.js.map +0 -1
  46. package/dist/provider-setup-LFsaTkpf.js.map +0 -1
  47. package/dist/tui-app-BLR-PRFp.js.map +0 -1
package/README.md CHANGED
@@ -7,6 +7,30 @@ what it may do, what it may believe, and what a job may cost — light enough fo
7
7
  "what's the weather", and it stages anything that can't be undone (`write_file`,
8
8
  `run_shell_command`, `send_email`) for your approval. Website: https://myaielia.com.
9
9
 
10
+ ## Install
11
+
12
+ ```bash
13
+ # macOS / Linux — standalone binary, installed to ~/.local/bin (override with AIELIA_INSTALL_DIR)
14
+ curl -fsSL https://myaielia.com/install.sh | sh
15
+ curl -fsSL https://myaielia.com/install.sh | sh -s -- --force # replace a non-aielia file at the target path
16
+
17
+ # Windows (PowerShell) — installs to %LOCALAPPDATA%\Aielia\aielia.exe and adds it to your user PATH
18
+ irm https://myaielia.com/install.ps1 | iex # set $env:AIELIA_FORCE = '1' first to force
19
+
20
+ # or from npm
21
+ npm install -g @buildaharness/aielia
22
+ ```
23
+
24
+ Both installers read the manifest at `https://myaielia.com/aielia-latest.json`, download
25
+ the binary for your platform over HTTPS only, verify its SHA-256 against the manifest and
26
+ install it; re-running upgrades in place, or says it is already up to date. They refuse
27
+ to overwrite an unrelated file (or a symlink, such as an npm-linked `aielia`) without
28
+ `--force`. `aielia update` does the same upgrade later (see "Updates and the update check"
29
+ below). On the first interactive launch with nothing configured, the CLI asks which
30
+ provider to use and for its API key (live-checked with a free read-only call) and saves
31
+ the choice to the persisted config; piped/scripted stdin skips this and keeps the
32
+ `proxy` default.
33
+
10
34
  ## Design
11
35
 
12
36
  The harness here is a governance and reliability control plane, not just a
@@ -307,8 +331,9 @@ await assistant.turn('Summarize this into notes.md', { approved: true, pendingAc
307
331
 
308
332
  Declining (`{ approved: false, pendingActionId }`) discards the staged record
309
333
  without writing. A pending action left over from a crashed/abandoned turn sits
310
- in `.pending-actions/` indefinitely — harmless (never applied without an explicit
311
- `approved: true` with the matching ID) but not currently auto-swept. This same
334
+ in `.pending-actions/` — harmless (never applied without an explicit
335
+ `approved: true` with the matching ID), and swept on the next startup unless a
336
+ session still has a resumable checkpoint (`sweepAbandonedPendingActionsOnStartup`). This same
312
337
  staging record shape (a `kind` discriminator) is shared with `run_shell_command`
313
338
  (see "Shell access via tools" below) and `send_email`.
314
339
 
@@ -348,7 +373,7 @@ Both backends enforce the same "never write inline" rule, by different
348
373
  mechanisms:
349
374
 
350
375
  - **Proxy/Anthropic backend** (`LLMClient`): `PersonalAssistant`'s tool loop
351
- (capped at 5 iterations) calls `callChatStructured` directly, executes
376
+ (bounded by `maxSteps`, default 15, minus a few steps held back for recovery) calls `callChatStructured` directly, executes
352
377
  non-mutating tool calls for real, and intercepts `write_file` itself before
353
378
  it ever reaches `file-tools.ts`'s staging code.
354
379
  - **Claude CLI backend** (`ClaudeCliLLMClient`): Claude Code's own agentic loop
@@ -365,8 +390,10 @@ mechanisms:
365
390
 
366
391
  v1 is deliberately read/list/write only — no delete/move tool (higher
367
392
  consequence than a write, no "undo" via re-approval). One workspace root per
368
- assistant instance; no multi-root or per-request override. chat-ui doesn't have
369
- a write-approval UI yet — file tools are CLI/desktop-only for now.
393
+ assistant instance; no multi-root or per-request override. File tools need a
394
+ real workspace directory, so they are CLI/desktop-only; chat-ui's approval card
395
+ (with the exact change shown) is what resolves a staged write or shell command
396
+ on the desktop build.
370
397
 
371
398
  ## Clarifying questions
372
399
 
@@ -404,8 +431,8 @@ follow-up `needs_clarification` batch if it's still unresolved once the first ba
404
431
  folded in, resolved through this exact same path.
405
432
 
406
433
  Two kinds of sites can raise a `questions` batch: the Trajectory Supervisor's `ASK_USER` directive
407
- (see ADR-005 — currently inert by default, since
408
- `HARNESS_TRAJECTORY_SUPERVISOR` stays off) and a handful of deterministic sites with a genuinely
434
+ (see ADR-005 — the supervisor is on by default in Aielia;
435
+ `HARNESS_TRAJECTORY_SUPERVISOR=0` turns it off) and a handful of deterministic sites with a genuinely
409
436
  enumerable option set, e.g. a batch-research budget running out ("continue with N more steps" /
410
437
  "stop and summarize" / "let me clarify the goal"). A site with no discrete option set keeps
411
438
  today's plain `missing_info` halt unchanged — this mechanism never forces a multiple-choice
@@ -742,8 +769,9 @@ non-functional `web_search`. The active state is shown in the startup banner
742
769
  running session with `/config set` instead of an env var — see
743
770
  "Configuration" below.
744
771
 
745
- Skip the proxy and call a provider directly with your own API key — `ASSISTANT_LLM_BACKEND=anthropic|openai|openrouter` plus
746
- `ASSISTANT_API_KEY`:
772
+ `ASSISTANT_LLM_BACKEND` also accepts `claude-cli` (shells out to the `claude` CLI already on
773
+ your PATH, no API key needed; CLI and desktop only). Skip the proxy and call a provider directly with your own API
774
+ key — `ASSISTANT_LLM_BACKEND=anthropic|openai|openrouter` plus `ASSISTANT_API_KEY`:
747
775
 
748
776
  ```bash
749
777
  ASSISTANT_LLM_BACKEND=anthropic ASSISTANT_API_KEY=sk-ant-... \
@@ -761,11 +789,12 @@ These three go straight from this process to the provider's own API
761
789
  — no proxy deployment needed, but unlike `authToken` (a self-hosted proxy's
762
790
  own bearer token), `ASSISTANT_API_KEY`/`apiKey` is a *real* provider key.
763
791
  It's stored the same way as every other secret field here — plaintext in
764
- `config.json`, not an OS keychain — so treat that file accordingly. When
792
+ `config.json` (written with mode `0600`, in a `0700` directory), not an OS
793
+ keychain — so treat that file accordingly. When
765
794
  `ASSISTANT_MODEL`/`/config set model` isn't set, each backend falls back to the
766
795
  current-generation default id exported from `@buildaharness/runtime`'s
767
796
  `model-defaults.ts` (`openai` → `gpt-5-mini`, `openrouter` →
768
- `anthropic/claude-sonnet-5`, `anthropic` and `proxy` → `claude-sonnet-5`).
797
+ `deepseek/deepseek-v4-flash`, `anthropic` and `proxy` → `claude-sonnet-5`).
769
798
 
770
799
  Transcript, learned experience, reminders, and any in-flight turn's checkpoint
771
800
  persist as real files under `~/.buildaharness/personal-assistant/`
@@ -794,9 +823,13 @@ you> /config
794
823
  model (not set)
795
824
  enableWeb false
796
825
  braveApiKey (not set)
826
+ webBackend direct
797
827
  enableShell false
798
828
  shellTimeoutMs (not set)
829
+ shellNetworkAllowlist (not set)
799
830
  workspaceRoot (not set)
831
+ enableEmail false
832
+ ... (one line per key; the full list is `CONFIG_KEYS` in config.ts)
800
833
  dangerouslySkipPermissions false
801
834
 
802
835
  you> /config set enableWeb true
@@ -812,7 +845,9 @@ you> /config reset enableWeb
812
845
  ✓ Reset enableWeb to default
813
846
  ```
814
847
 
815
- - `/config` lists every field's current (resolved) value. A field currently
848
+ - `/config` lists every field's current (resolved) value; secret fields
849
+ (`authToken`, `apiKey`, `braveApiKey`, `resendApiKey`, `smtpPass`) print as
850
+ `********`, and credentials or query strings inside `proxyUrl` are masked. A field currently
816
851
  pinned by an env var shows `(env-pinned: VAR_NAME)` and cannot be changed
817
852
  with `/config set` — unset the env var first.
818
853
  - `/config set <key> <value>` validates the change (e.g. `enableWeb true`
@@ -825,11 +860,40 @@ you> /config reset enableWeb
825
860
 
826
861
  **Precedence**: env var > persisted config > built-in default, evaluated
827
862
  independently per field. Settings persist as plain JSON at
828
- `~/.buildaharness/personal-assistant/config.json` — like the rest of this
829
- package's persistence, it's a real file, not encrypted, so `authToken` and
830
- `braveApiKey` are stored in plaintext there. This is the same trust boundary
863
+ `~/.buildaharness/personal-assistant/config.json` (created `0600` inside a
864
+ `0700` directory) — like the rest of this
865
+ package's persistence, it's a real file, not encrypted, so `authToken`,
866
+ `apiKey`, `braveApiKey` and the email credentials are stored in plaintext there. This is the same trust boundary
831
867
  the repo's root `.env` already has, not a new one.
832
868
 
869
+ Settings beyond those above (each has an env var, a `/config set` key, and a built-in default;
870
+ mode flags take `enabled`/`disabled`):
871
+
872
+ | Key (env var) | Default | What it does |
873
+ |---|---|---|
874
+ | `webBackend` | `direct` | Browser build only (see chat-ui README); the CLI always calls Brave directly |
875
+ | `shellNetworkAllowlist` (`ASSISTANT_SHELL_NETWORK_ALLOWLIST`) | none (deny all) | Hosts an approved shell command may reach |
876
+ | `enableEmail`, `emailProvider` (`resend`/`smtp`), `emailFrom`, `resendApiKey`, `smtpHost`/`smtpPort`/`smtpUser`/`smtpPass` (`ASSISTANT_ENABLE_EMAIL`, `ASSISTANT_EMAIL_*`, `ASSISTANT_RESEND_API_KEY`, `ASSISTANT_SMTP_*`) | off | Registers the staged `send_email` tool; `enableEmail` needs `emailFrom` and the provider's credentials |
877
+ | `sessionCostLimitUsd`, `sessionCallLimit` (`ASSISTANT_SESSION_COST_LIMIT_USD`, `ASSISTANT_SESSION_CALL_LIMIT`) | unbounded | Refuses a new turn once the cross-session estimated spend or completed-turn count reaches the ceiling; shown in `/status` and `/cost` |
878
+ | `memoryBudgetChars` (`ASSISTANT_MEMORY_BUDGET_CHARS`) | 4000 | Character budget for the facts block in the prompt |
879
+ | `memoryWriteMode` (`ASSISTANT_MEMORY_WRITE_MODE`) | `staged` | `auto`, `staged` or `user_only`; see "Memory tiers" |
880
+ | `oneLoopMode` (`ASSISTANT_ONE_LOOP`) | `enabled` | See "Design" |
881
+ | `askMode` (`ASSISTANT_ASK_MODE`) | `disabled` | See "Clarifying questions" |
882
+ | `planMode` (`ASSISTANT_PLAN_MODE`) | `legacy` | `gated` or `legacy` plan rollout |
883
+ | `ambiguityGuardMode` (`ASSISTANT_AMBIGUITY_GUARD`) | `disabled` | Ambiguity guard |
884
+ | `tuiMode` (`ASSISTANT_TUI`) | `enabled` | Full-screen terminal UI (Ink); only on a real TTY |
885
+ | `updateCheck` (`ASSISTANT_UPDATE_CHECK`) | `enabled` | Passive update check |
886
+ | `activeProject` (`ASSISTANT_ACTIVE_PROJECT`) | none | Project new project-scoped facts are tagged with (`/project`) |
887
+ | `goalGraphMode`, `goalGraphSuggestMode` (`ASSISTANT_GOAL_GRAPH`, `ASSISTANT_GOAL_GRAPH_SUGGEST`) | `enabled` in the CLI | Goal tree, steering and next-step options |
888
+ | `planGraphMode` (`ASSISTANT_PLAN_GRAPH`) | `disabled` | `/plan graph` |
889
+ | `layerPolicyMode` (`ASSISTANT_LAYER_POLICY`) | `static` | `static`, `shadow` or `adaptive` |
890
+ | `layers` | `{}` | Per-layer on/off, edited with `/layers`, not `/config set` |
891
+
892
+ `ASSISTANT_ACTIVITY_LOG=1` additionally appends a debug activity log to
893
+ `activity-log.jsonl` in the data directory (created `0600`). All CLI output has
894
+ terminal escape sequences stripped, so text echoed from a fetched page or file
895
+ cannot rewrite what an approval prompt shows.
896
+
833
897
  ### Lexical checks (off by default)
834
898
 
835
899
  Every lexical pass in this package (regex / keyword / phrase-list checks over natural-language text) sits
@@ -853,8 +917,10 @@ plan step saved without its own risk level is treated as HIGH.
853
917
 
854
918
  ## REPL commands
855
919
 
856
- Type `/help` inside a running CLI session for this list. All of them read or
857
- change local session/config state — none of them make an LLM call themselves.
920
+ Type `/help` inside a running CLI session for this list. Almost all of them read or
921
+ change local session/config state without an LLM call; the exceptions are `/plan sketch`
922
+ and `/memory consolidate`, each of which makes one. `/exit` (also `/quit`, `exit`, `quit`)
923
+ ends the session.
858
924
 
859
925
  | Command | What it does |
860
926
  |---|---|
@@ -863,15 +929,27 @@ change local session/config state — none of them make an LLM call themselves.
863
929
  | `/status` | Show the resolved config (model, backend, workspace, enabled capabilities — same as the startup banner) plus this session's transcript length and whether a plan is active |
864
930
  | `/export [file]` | Save this session's transcript to a markdown file (default: `assistant-transcript-<timestamp>.md` in the current directory) |
865
931
  | `/undo` | Remove the last exchange from conversation history — a completed turn drops both the user message and the reply; a turn still awaiting approval drops just the pending message. Only affects what the model remembers: a real `write_file`/`run_shell_command` effect from that turn is **not** reversed |
866
- | `/memory` | Show facts learned about you, reminders created so far, and the learning-layer `ExperienceStore`'s real content — every strategy weight, plus the 20 most recently learned decompositions/recovery sequences (newest first), not just counts |
932
+ | `/undo-action [id]` | List the real filesystem effects of approved actions that can still be reverted (see `/status`'s undo-log line), or stage a revert of one for your approval |
933
+ | `/memory` | Show facts learned about you, pending-confirmation guesses, reminders created so far, and the learning-layer `ExperienceStore`'s real content — every strategy weight, plus the 20 most recently learned decompositions/recovery sequences (newest first), not just counts |
934
+ | `/memory confirm <n\|category>` / `/memory reject <n\|category>` | Promote a pending-confirmation guess (or a whole category) to durable memory, or discard it |
935
+ | `/memory forget <n> [erase]` | Remove a learned fact by its number in "Facts I know". It stays restorable through the audit log (`/memory undo`) unless you add `erase`, which scrubs its text from the log too. `/memory forget digest [id]` erases one stored session digest, or all of them |
936
+ | `/memory history` / `/memory undo <seq>` | Show recent memory changes with their sequence numbers, and restore one change's pre-image (needs `AUDIT_MEMORY_AUDIT_LOG`) |
937
+ | `/memory status` | Store size vs budget, pending count, write mode, last consolidation |
938
+ | `/memory archive [restore <n> \| forget <n>]` | List facts set aside or replaced by a newer statement; restore one, or erase one for good |
939
+ | `/memory consolidate [accept\|dismiss <n>]` | Propose merging or retiring overlapping facts; nothing changes until you accept (needs `AUDIT_MEMORY_CONSOLIDATION`) |
940
+ | `/memory off` / `/memory on` | Stop or resume all memory writes for this install; existing facts stay readable and removable |
867
941
  | `/memory export [file]` | Write the full, unbounded `ExperienceStore` contents (every strategy weight/decomposition/recovery sequence, not the 20-entry preview `/memory` prints) plus facts/reminders to a JSON file (default: `assistant-memory-<timestamp>.json`). Read-only: there's no matching import path, so exported data can't be hand-edited and loaded back in |
868
942
  | `/search <query>` | Ranked search over past messages, across every session in this install — a hit is the one message that matched, not the whole session transcript around it. Read-only: never an LLM call or network request. Scoring is tokenized/graduated, not exact-substring-only (see "Conversation history" above), and a query matching nothing returns an explicit "no results" line |
943
+ | `/project [name]` | Show or switch the project new project-scoped facts are tagged with; `/project clear` reverts to the workspace default |
869
944
  | `/model [name]` | Show the active model, or switch it — a thin alias over `/config set model <name>` (see "Configuration" above); rejected the same way if `model` is pinned by `ASSISTANT_MODEL` |
870
945
  | `/cost` | Show token usage for the last turn and the running session total |
871
- | `/doctor` | Check proxy reachability (proxy backend), plus workspace root and data dir health — no dedicated check yet for the anthropic/openai/openrouter backends, only the backend-agnostic checks run for those |
946
+ | `/doctor` | Check proxy reachability (proxy backend) or the `claude` binary (claude-cli backend), plus workspace root and data dir health — no dedicated check yet for the anthropic/openai/openrouter backends, only the backend-agnostic checks run for those |
872
947
  | `/why` | Explain the harness path the last turn took (verification confidence + node sequence) |
948
+ | `/layers` | Show all 11 harness layers: fired or skipped, and why, for the last turn. `/layers settings` lists the reasoning layers with their cost/evidence; `/layers on\|off\|reset <id>` changes a layer's persisted on/off choice (an operator-set `AUDIT_*` env flag wins and shows as pinned) |
873
949
  | `/sources` | List files/URLs the last turn actually consulted |
874
950
  | `/plan` | Show the active structured plan's task status |
951
+ | `/plan sketch <request>` | One-shot, advisory plan sketch: no plan record, nothing staged, cannot execute |
952
+ | `/goals` | Review every known goal thread this session: status, tasks and visibility (freshly computed, carried over, done, suggested) |
875
953
  | `/plan graph [thread-id]` | Draw the active plan as a read-only dependency graph (needs `planGraphMode` enabled; full-screen pane in the terminal UI with arrows/Tab/Enter/q, one static render in the plain REPL). Glyphs, mapping and keys: `docs/plan-visualization.md` |
876
954
  | `/checkpoint [clear]` | Inspect a stuck in-progress harness checkpoint (step, node, failed-resume count so far), or `/checkpoint clear` to discard it — see "Recovering a stuck checkpoint" above. Scoped to just the checkpoint: unlike `/clear`, transcript/facts/plan are untouched |
877
955
 
@@ -4,9 +4,24 @@ import { ShellToolsContext } from './shell-tools.js';
4
4
  import { ActionToolsContext } from './action-tools.js';
5
5
  import { AssistantTurnResult } from './assistant-types.js';
6
6
  import { AssistantSession } from './assistant-session.js';
7
- import { AgentLoop } from './agent-loop.js';
7
+ import { AgentLoop, ToolLoopResult } from './agent-loop.js';
8
+ import { AssistantToolStep } from './tool-step.js';
8
9
  import { TraceEvent } from './trace-events.js';
9
10
  import { DebugLogEntry } from './debug-log.js';
11
+ /**
12
+ * Optional hooks for resuming a staged action that a flat tool loop paused on (see
13
+ * AgentLoop.resumeToolLoop): the live token/tool-step callbacks of the turn being resumed, and how
14
+ * to turn a *further* pause (another approval, or an escalation) into the turn result — the caller
15
+ * owns that because auto-approve modes live there, not here.
16
+ */
17
+ export interface ResumeOptions {
18
+ onToken?: (token: string) => void;
19
+ onToolStep?: (step: AssistantToolStep) => void;
20
+ handleLoopPause?: (result: Extract<ToolLoopResult, {
21
+ kind: 'needs_approval' | 'escalated';
22
+ }>) => Promise<AssistantTurnResult>;
23
+ }
24
+ export { actionRecordSuffix } from './action-record.js';
10
25
  /**
11
26
  * Owns the "approval-by-ID" pattern: a staged write/shell/revert/batch-research action resolved
12
27
  * by ID, never re-derived from a second LLM call (see T4 of the file-tools plan). Split out in
@@ -25,8 +40,25 @@ export declare class ActionApprovalService {
25
40
  private readonly onTrace;
26
41
  private readonly onDebugLog;
27
42
  constructor(memory: MemoryAdapter, llmClient: ILLMClient, model: () => string | undefined, fileTools: FileToolsContext | undefined, shellTools: ShellToolsContext | undefined, actionTools: ActionToolsContext | undefined, session: AssistantSession, agentLoop: AgentLoop, onTrace: ((event: TraceEvent) => void) | undefined, onDebugLog: ((entry: DebugLogEntry) => void) | undefined);
43
+ /** Every write, command and email carried out through an approval, in order (a turn's slice is what the system recorded for it). */
44
+ readonly appliedActions: string[];
45
+ /** Parallel to appliedActions (same indices): for a write, the lines it changed as a short diff, so the reply audit can tell whether a reply's account of the file is true (W1); the same clause as appliedActions for every other kind. */
46
+ readonly appliedActionDetails: string[];
47
+ /** The command and the start/end of the output of each shell command run through an approval (for the reply audit). */
48
+ readonly recentCommandOutputs: {
49
+ command: string;
50
+ output: string;
51
+ }[];
52
+ /** Sessions in which a command got the network-containment refusal. */
53
+ private readonly networkDeniedSessions;
54
+ /** Pending-action ids currently being resolved (see resolvePendingAction). */
55
+ private readonly inFlightResolutions;
56
+ networkDenied(sessionId: string): boolean;
28
57
  /** Resumes a staged action by ID instead of re-deriving *what to run* from a second LLM call — see T4 of the file-tools plan. `userMessage` is only used to synthesize an answer from a shell command's real output (see below); the command/content actually applied always comes from the staged record, never from a fresh model call. */
29
- resolvePendingAction(sessionId: string, transcriptKey: string, pendingActionId: string, approved: boolean, userMessage: string): Promise<AssistantTurnResult>;
58
+ resolvePendingAction(sessionId: string, transcriptKey: string, pendingActionId: string, approved: boolean, userMessage: string, resume?: ResumeOptions): Promise<AssistantTurnResult>;
59
+ private resolvePendingActionOnce;
60
+ /** The conversation before the current user message, for the no-tools synthesis call. The current message was already appended to the transcript when the turn paused, so it is dropped here. */
61
+ private recentHistory;
30
62
  /**
31
63
  * After resolvePendingAction resolves one staged action (approved or declined), checks whether
32
64
  * it was chained to a second approval-gated action staged from the same originating turn (see
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The "[Recorded by the system ...]" action list. Only the system writes it, into the stored transcript, from the
3
+ * approved actions it really carried out. The model sees it in earlier stored replies and was observed imitating it
4
+ * (benchmark scenarios 05 and 13: a reply listed a test file and writes that never happened), which makes a false
5
+ * claim look like a system record. Anything shaped like the marker in model-written text is therefore removed.
6
+ */
7
+ export declare const ACTION_RECORD_MARKER = "[Recorded by the system, not part of the reply";
8
+ export declare function actionRecordSuffix(actions: string[]): string;
9
+ export declare function containsActionRecord(text: string): boolean;
10
+ /** Removes every block that starts at the marker and runs to its closing bracket, with the blank lines before it. */
11
+ export declare function stripActionRecord(text: string): string;
12
+ /** Shown in place of a forged record, so the user knows why the list is gone. */
13
+ export declare const FORGED_ACTION_RECORD_NOTE = "[Note from the system: the reply listed actions as \"recorded by the system\", but the system recorded no such action this turn, so the list was removed.]";
@@ -0,0 +1,4 @@
1
+ import { DebugLogEntry } from './debug-log.js';
2
+ export declare function activityLogEnabled(env: NodeJS.ProcessEnv): boolean;
3
+ /** Returns an `onDebugLog` callback appending `{at, sessionId, kind, content}` lines to `filePath`. Never throws. */
4
+ export declare function createActivityLogger(filePath: string, now?: () => Date): (entry: DebugLogEntry) => void;
@@ -1,5 +1,5 @@
1
1
  import { ToolExecutorContext, HarnessPauseSignal, InvestigationRequestData, InvestigationFinding } from '@buildaharness/harness';
2
- import { MemoryAdapter, ILLMClient, ChatMessage, TokenUsage, ToolDefinition, ReminderStore } from '@buildaharness/runtime';
2
+ import { MemoryAdapter, ILLMClient, ChatMessage, ToolCallResult, TokenUsage, ToolDefinition, ReminderStore } from '@buildaharness/runtime';
3
3
  import { AssistantSource } from './assistant-source.js';
4
4
  import { DebugLogEntry } from './debug-log.js';
5
5
  import { TraceEvent } from './trace-events.js';
@@ -23,10 +23,54 @@ export type ToolLoopResult = {
23
23
  reason: string;
24
24
  pendingActionId: string;
25
25
  pendingActionKind: 'write' | 'shell' | 'email' | 'batch';
26
+ resume?: StagedCallResume;
26
27
  } | {
27
28
  kind: 'escalated';
28
29
  reason: string;
29
30
  };
31
+ /**
32
+ * Set on a needs_approval result when the pause came from a tool call this loop dispatched itself
33
+ * (a proxy-backend `write_file`/`run_shell_command`/`send_email` call): the exact call the model
34
+ * made, so the loop can be continued once the staged action resolves instead of ending the turn.
35
+ * Absent for a pause whose call lives inside a backend's own agentic loop (claude-cli's
36
+ * `__staged_action`) — there is no message history to continue from.
37
+ */
38
+ export interface StagedCallResume {
39
+ toolCall: ToolCallResult;
40
+ assistantContent: string;
41
+ }
42
+ /**
43
+ * Persisted under `loop-pending:<pendingActionId>` while a flat tool loop waits on an approval, so
44
+ * an approved action's result can be folded back into the same conversation (see
45
+ * AgentLoop.resumeToolLoop). `messages` is everything sent to the model so far, system prompt
46
+ * included, *excluding* the assistant message holding the staged call (rebuilt on resume).
47
+ */
48
+ export interface ToolLoopPendingState {
49
+ sessionId: string;
50
+ userMessage: string;
51
+ riskHint: TurnIntentClassification['riskLevel'];
52
+ messages: ChatMessage[];
53
+ sources: AssistantSource[];
54
+ iterationsUsed: number;
55
+ toolCall: ToolCallResult;
56
+ assistantContent: string;
57
+ /** Approved actions already carried out earlier in this same turn (see describeAppliedAction) — recorded in the transcript with the turn's final reply so later turns know what was done. */
58
+ actions?: string[];
59
+ }
60
+ /** Sent when the model returns an empty final answer, before the one retry. */
61
+ /** For a reply that held only a line shaped like the system's action record: the actions it listed were not run, so unlike the blank-reply nudge this one sends the model back to its tools. */
62
+ export declare const RECORD_ONLY_REPLY_NUDGE = "Your last reply held only a line shaped like the system's action record, no answer, and the system ran nothing it listed. Do what the user asked now with your tools if it is still wanted, then reply in plain text with what you did and found. Never write a \"Recorded by the system\" line yourself.";
63
+ export declare const EMPTY_REPLY_NUDGE = "Your last reply was empty. This check is automatic and is not from the user. Answer the user's message now in plain text: if you did work this turn, say what you did and what you found; if the message needs no work, just answer it. Do not call a tool unless something is still unfinished.";
64
+ /** The nudge with the user's message quoted, so the model answers that message and not the nudge (benchmark scenario 12 turn 1: told to "summarize", it answered a summary request the user never made). */
65
+ export declare function withUserMessage(nudge: string, userMessage: string): string;
66
+ /** Sent when a turn used all its tool steps, before the one tool-less summary call. */
67
+ export declare const OUT_OF_STEPS_NUDGE = "You have used all the tool steps available for this turn. Reply now in plain text: say what you did, what worked, and what is still unfinished. Do not call any tool.";
68
+ /**
69
+ * The reply used when the model returns nothing twice in a row: built only from what is on record, so the user
70
+ * never gets an empty message. `actions` is the turn's list of carried-out approved actions (see describeAppliedAction).
71
+ */
72
+ export declare function fallbackFinalReply(actions: string[] | undefined): string;
73
+ export declare const toolLoopPendingKey: (pendingActionId: string) => string;
30
74
  /**
31
75
  * R2 of the internal plan: thrown by a harness-driven proposer
32
76
  * (createHarnessProposer, below) when runToolIterationStep produces a 'needs_approval' or
@@ -274,6 +318,25 @@ export declare class AgentLoop {
274
318
  * those does, since `reminderStore` always exists).
275
319
  */
276
320
  runToolLoop(sessionId: string, transcript: ChatMessage[], userMessage: string, systemPrompt: string, onToken?: (token: string) => void, onToolStep?: (step: AssistantToolStep) => void, onUsage?: (usage: TokenUsage) => void, riskHint?: TurnIntentClassification['riskLevel'], controlPlaneState?: TurnControlPlaneState): Promise<ToolLoopResult>;
321
+ /** One last model call without tools after the step budget ran out; undefined if it fails or returns nothing. */
322
+ private summarizeWhenOutOfSteps;
323
+ /** Replaces an empty final answer (the model returned nothing twice) with fallbackFinalReply, shown through onToken like any answer. */
324
+ private withFallbackReply;
325
+ private flatLoopTools;
326
+ /**
327
+ * When the flat loop pauses on a staged call it dispatched itself, saves what is needed to keep
328
+ * going after the approval (see ToolLoopPendingState) and strips the in-memory `resume` handle
329
+ * from the result the caller sees. Any other result is returned untouched.
330
+ */
331
+ private persistPausedLoop;
332
+ /**
333
+ * Continues a flat tool loop that paused on an approval-gated call, after that call's staged
334
+ * action has been resolved: `toolResultText` (the command's output, a write confirmation, ...)
335
+ * goes back to the model as the tool result for the exact call it made, and the loop runs on —
336
+ * more reads, further approvals (which pause again and persist again), then a final answer —
337
+ * within whatever iteration budget the turn had left.
338
+ */
339
+ resumeToolLoop(state: ToolLoopPendingState, toolResultText: string, onToken?: (token: string) => void, onToolStep?: (step: AssistantToolStep) => void, onUsage?: (usage: TokenUsage) => void, actions?: string[] | undefined): Promise<ToolLoopResult>;
277
340
  /**
278
341
  * The actual ReAct-style tool-calling loop, factored out of runToolLoop so a batch sub-loop
279
342
  * (resolveBatchItem, below) can run the exact same iteration logic — including the
@@ -309,7 +372,21 @@ export declare class AgentLoop {
309
372
  * the result, since it's a plain accumulator shared by reference across every step call for
310
373
  * the same loop.
311
374
  */
375
+ /**
376
+ * The tool error for a write_file over an existing file the session has not read, or undefined when the write may go ahead (a new
377
+ * file, or one this session read or wrote). Benchmark scenario 13 turn 4: after a turn with no tool call at all, the model answered
378
+ * "fix that too" with a write_file that replaced `calculate_priority` with an invented function of a different signature, never having
379
+ * read the file. A write is a full replacement, so it has to be based on what the file really holds.
380
+ */
381
+ private unreadExistingFile;
312
382
  private runToolIterationStep;
383
+ /**
384
+ * A staging call (write_file / run_shell_command / send_email) was rejected before anything was staged — a path or
385
+ * cwd outside the workspace, a malformed recipient. The tool descriptions promise "rejected immediately": that is a
386
+ * tool error the model sees and can correct, exactly like a failed read, not an exception that ends the turn.
387
+ * Every tool call in the response gets a result (the provider requires one per call id).
388
+ */
389
+ private stagingRejected;
313
390
  /**
314
391
  * Resolves one batch item in its own bounded sub-loop — structurally the same
315
392
  * runToolIterations call the flat loop uses, just seeded with a single-item-focused user
@@ -367,6 +444,11 @@ export declare class AgentLoop {
367
444
  injectionDetectionGate?: () => boolean;
368
445
  /** Episodic digest source for `recall_memory` (M3); set by the assistant. The tool is offered only when this is set AND AUDIT_RECALL_TOOL is on. */
369
446
  digestReader?: DigestReader;
447
+ /**
448
+ * Set by the assistant: whether this session has already read (or itself written) a workspace file. A write over an existing file
449
+ * that the session never read is refused (see unreadExistingFile), so the model cannot replace a file with contents it guessed.
450
+ */
451
+ sessionKnowsFile?: (sessionId: string, workspaceRelativePath: string) => boolean;
370
452
  private recallTools;
371
453
  /** The opt-in layers' plan for the turn in flight (adaptive mode only), set by the assistant each turn; see resolveOptInPlan. */
372
454
  optInPlan?: EscalationPlan;
@@ -105,7 +105,10 @@ export declare class AssistantSession {
105
105
  * already correctly no-ops whenever no cap is configured, so this never enforces a cap that
106
106
  * isn't set.
107
107
  */
108
+ /** Tail of the chain serialising recordSpend's read-modify-write: the background memory reviewer records spend while a turn does, and the later write used to drop the other's cost. */
109
+ private spendChain;
108
110
  recordSpend(sessionId: string, usage: TokenUsage | undefined): Promise<void>;
111
+ private recordSpendNow;
109
112
  /** Read-only — null means plan mode has never been entered (or was already exited/cancelled) for this session (or this `threadId`, once Phase 5's per-thread rescoping applies — see `planModeKey`). */
110
113
  getPlanModeState(sessionId: string, threadId?: string): Promise<PlanModeState | null>;
111
114
  /**
@@ -140,9 +143,25 @@ export declare class AssistantSession {
140
143
  * caught and logged, never thrown — a search-indexing problem must never be able to break an
141
144
  * ordinary turn or lose the transcript message itself.
142
145
  */
143
- appendTranscriptMessage(sessionId: string, transcriptKey: string, message: {
146
+ /**
147
+ * A message the user sent while a turn was running that the turn's model call already took into account
148
+ * (a mid-turn steering note). It belongs in the conversation as its own user message, in order: held here
149
+ * and written just before the next assistant message, i.e. after the running turn's own user message and
150
+ * before the reply it shaped. Without this the note vanished from the transcript.
151
+ */
152
+ recordAbsorbedSteering(sessionId: string, message: string): void;
153
+ private readonly absorbedSteering;
154
+ /**
155
+ * Called by ResponseService the moment a turn's final reply is stored, before the slower post-reply work
156
+ * (fact recording, goal evidence, grounding check) that still has to finish inside the turn. The assistant uses it
157
+ * to put the reply in the activity log right away (benchmark issue N2).
158
+ */
159
+ replyReadyHook?: (sessionId: string, reply: string) => void;
160
+ replyReady(sessionId: string, reply: string): void;
161
+ appendTranscriptMessage(sessionId: string, transcriptKey: string, input: {
144
162
  role: 'user' | 'assistant';
145
163
  content: string;
164
+ actionRecord?: string[];
146
165
  }): Promise<void>;
147
166
  /**
148
167
  * One-off, idempotent backfill for installs that already had transcript history before the
@@ -62,7 +62,7 @@ export interface AssistantTurnResult {
62
62
  * passes through this exactly once, regardless of risk (no risk-tiered skip — see
63
63
  * PlanApprovalService's doc comment).
64
64
  */
65
- status: 'ok' | 'needs_approval' | 'escalated' | 'needs_clarification' | 'needs_plan_approval';
65
+ status: 'ok' | 'needs_approval' | 'escalated' | 'needs_clarification' | 'needs_plan_approval' | 'cancelled';
66
66
  reply: string | null;
67
67
  /**
68
68
  * Up to three concrete next steps proposed after a full (non-trivial, `ok`) turn, most confident
@@ -124,6 +124,8 @@ export interface AssistantTurnResult {
124
124
  contradictionNotice?: string;
125
125
  /** Set when the semantic change reviewer found the request conflicts with something the user stated earlier AND the answer could not be told so directly (a tool-less turn's draft already exists, so the proposer never reads the review note). Advisory only — the turn still completed. Mirrors `contradictionNotice`: a separate field, not folded into `reply`, because the CLI streams `reply` before the check runs. */
126
126
  reviewNotice?: string;
127
+ /** Set when the reply audit (reply-audit.ts) found a claim of work the system did not record, a promise of work not done, or unverified facts about outside sources. Separate from `reply` for the same reason as `reviewNotice`. */
128
+ auditNotice?: string;
127
129
  /**
128
130
  * An epistemic-honesty signal for replies that went through the harness loop (absent on the
129
131
  * triviality fast path, same "absent when unused" convention as trace/sources — a
@@ -48,6 +48,8 @@ export type MemoryPendingOutcome = {
48
48
  };
49
49
  export interface TurnOptions {
50
50
  sessionId?: string;
51
+ /** Internal: this turn is the one-time correction the reply audit asked for; it is not a user message and is never retried again. */
52
+ auditRetry?: boolean;
51
53
  /** M4: a scripted / piped / batch caller. The post-turn memory reviewer is skipped for it (it only runs for an interactive session). */
52
54
  nonInteractive?: boolean;
53
55
  approved?: boolean;
@@ -80,6 +82,12 @@ export interface TurnOptions {
80
82
  * flag is off.
81
83
  */
82
84
  askMode?: AskMode;
85
+ /**
86
+ * Stops the turn: once it fires, no further model call starts or is kept, the transcript is left as it was before
87
+ * the turn, and `turn()` resolves with `status: 'cancelled'` right away. Ignored when resuming a staged action
88
+ * (`pendingActionId`), because that applies a write/shell/email the user already approved and must not be half-done.
89
+ */
90
+ signal?: AbortSignal;
83
91
  onProgress?: (progress: AssistantProgress) => void;
84
92
  /**
85
93
  * Called with each token as the model's reply streams in. On the plain chat
@@ -254,6 +262,8 @@ export interface PersonalAssistantOptions {
254
262
  * that never asks for suggestions makes no extra call and sees no new field (INV-43).
255
263
  */
256
264
  goalGraphSuggestMode?: GoalGraphSuggestMode;
265
+ /** Run the reply audit (reply-audit.ts) after each successful turn; defaults to the AIELIA_REPLY_AUDIT env gate (on). */
266
+ replyAudit?: boolean;
257
267
  /**
258
268
  * Q2 of the internal plan — global-flag control point (tier 1 of
259
269
  * INV-29) for the ask-question mechanism. Undefined (the default) falls back to
@@ -300,6 +310,9 @@ export interface PersonalAssistantOptions {
300
310
  */
301
311
  export declare class PersonalAssistant {
302
312
  private readonly llmClient;
313
+ private readonly abortGate;
314
+ /** A stopped turn whose in-flight model call has not come back yet. Its gate stays armed until it settles, so it cannot write the transcript late. */
315
+ private stoppedWork?;
303
316
  private model?;
304
317
  private activeProject?;
305
318
  private memoryBudgetChars?;
@@ -335,6 +348,19 @@ export declare class PersonalAssistant {
335
348
  private readonly session;
336
349
  private readonly agentLoop;
337
350
  private readonly actionApproval;
351
+ /** Set when the reply was already logged by the reply-ready hook this turn (see AssistantSession.replyReadyHook). */
352
+ private replyLoggedEarly;
353
+ /** Automatic correction retries still allowed for the current user message; resumed (approval) turns share it so a retry cannot chain. */
354
+ private auditRetryBudget;
355
+ /** The session of the turn in progress, for log lines written by code that has no session of its own (the side-call retry note). */
356
+ private lastSessionId;
357
+ /** The last real (not fail-safe) risk classification of a user message, per session, for the audit retry that may correct its reply. */
358
+ private readonly lastClassifiedBySession;
359
+ /** Files, directories and pages each session's turns have read (tool and target, oldest first, capped): the reply audit needs reads from EARLIER turns too, or a correct answer about a file read last turn looks like an unverified outside fact (V1). In memory only; cleared with the session. */
360
+ private readonly sourcesReadBySession;
361
+ /** Index into the action list where the current user message's actions start; an approval resume is a separate turn() call but the same user message. */
362
+ private userMessageActionsStart;
363
+ private readonly replyAuditOn;
338
364
  private readonly planService;
339
365
  private readonly planApproval;
340
366
  private readonly planDrafting;
@@ -360,6 +386,8 @@ export declare class PersonalAssistant {
360
386
  * thread.
361
387
  */
362
388
  private nextStepContext;
389
+ /** Logs a message that arrived while a turn was running, at receipt time (its `user_message` is only logged when it is processed). */
390
+ logQueuedMessage(sessionId: string, message: string): void;
363
391
  /**
364
392
  * Thin wrapper around runTurn(): emits turn_start/turn_end/error trace events
365
393
  * around the actual logic, so every one of runTurn's return paths gets a
@@ -386,6 +414,8 @@ export declare class PersonalAssistant {
386
414
  */
387
415
  recordDeclinedRequest(sessionId: string, userMessage: string, reason: string): Promise<void>;
388
416
  /** Ends the current conversation — see AssistantSession.clearSession's doc comment for exactly what is and isn't cleared. */
417
+ /** Remembers what a turn read (tool and target) for the reply audit of later turns; oldest entries drop first. */
418
+ private noteSourcesRead;
389
419
  clearSession(sessionId: string): Promise<void>;
390
420
  /** M5: propose memory consolidation (applies nothing). `manual` forces the model pass past the cost gate. Usage is recorded against the spend state like any other call. */
391
421
  proposeMemoryConsolidation(sessionId: string, manual?: boolean): Promise<Awaited<ReturnType<MemoryService['runConsolidation']>>>;
@@ -569,6 +599,8 @@ export declare class PersonalAssistant {
569
599
  * plan-cancel bypass, the triviality fast path) actually lives now.
570
600
  */
571
601
  private runTurn;
602
+ /** What ActionApprovalService needs to continue a paused tool loop after an approval: the live callbacks, and how a further pause becomes a turn result. */
603
+ private loopResumeOptions;
572
604
  /**
573
605
  * Shared by the flag-OFF flat/batch tool loop's own needs_approval/escalated ToolLoopResult and
574
606
  * the flag-ON harness-driven proposer's equivalent OneLoopPause (R3 of