@buildaharness/aielia 0.3.1 → 0.3.3

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 (71) hide show
  1. package/README.md +31 -8
  2. package/dist/agent-loop.d.ts +8 -8
  3. package/dist/ask-clarification-service.d.ts +1 -1
  4. package/dist/ask-mode-flag.d.ts +1 -1
  5. package/dist/assistant-session.d.ts +1 -1
  6. package/dist/assistant-types.d.ts +3 -3
  7. package/dist/assistant.d.ts +12 -12
  8. package/dist/batch-research-fixtures.d.ts +1 -1
  9. package/dist/benchmark-injected-failure.d.ts +1 -1
  10. package/dist/bin-Btzhiyzi.js +2108 -0
  11. package/dist/bin-Btzhiyzi.js.map +1 -0
  12. package/dist/bin-sea-bootstrap.d.ts +1 -0
  13. package/dist/bin.d.ts +1 -0
  14. package/dist/claude-cli-llm-client.d.ts +2 -2
  15. package/dist/cli-args.d.ts +20 -0
  16. package/dist/cli-icons.d.ts +28 -0
  17. package/dist/{cli-session-D0uLdT2A.js → cli-session-DnSc6Hhx.js} +607 -127
  18. package/dist/cli-session-DnSc6Hhx.js.map +1 -0
  19. package/dist/cli.d.ts +31 -1
  20. package/dist/cli.js +1 -1691
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config.d.ts +14 -6
  23. package/dist/contradiction-checker.d.ts +1 -1
  24. package/dist/diff-format.d.ts +16 -0
  25. package/dist/execution-mode.d.ts +1 -1
  26. package/dist/fact-extraction.d.ts +4 -4
  27. package/dist/failure-mode-matcher.d.ts +1 -1
  28. package/dist/file-tools-mcp-server.mjs +2 -2
  29. package/dist/file-tools.d.ts +10 -2
  30. package/dist/harness-bridge.d.ts +21 -4
  31. package/dist/index.js +68 -68
  32. package/dist/index.js.map +1 -1
  33. package/dist/ink-select-prompt.d.ts +21 -0
  34. package/dist/markdown-line.d.ts +42 -0
  35. package/dist/mcp-server-asset.d.ts +24 -0
  36. package/dist/memory-service.d.ts +12 -2
  37. package/dist/network-containment.d.ts +1 -1
  38. package/dist/node-display-names.d.ts +1 -1
  39. package/dist/one-loop-flag.d.ts +1 -1
  40. package/dist/plan-drafting-service.d.ts +1 -1
  41. package/dist/plan-drafting.d.ts +4 -4
  42. package/dist/plan-mode-flag.d.ts +1 -1
  43. package/dist/plan-sketch-service.d.ts +1 -1
  44. package/dist/plan-sketch.d.ts +1 -1
  45. package/dist/plan-store.d.ts +2 -2
  46. package/dist/plan-templates/index.d.ts +1 -1
  47. package/dist/plan-verification.d.ts +1 -1
  48. package/dist/review-checker.d.ts +10 -0
  49. package/dist/scripted-llm-client.d.ts +1 -1
  50. package/dist/sea-cache.d.ts +9 -0
  51. package/dist/self-update.d.ts +101 -0
  52. package/dist/semantic-criterion-coverage.d.ts +10 -0
  53. package/dist/shell-tools.d.ts +1 -1
  54. package/dist/spike.js.map +1 -1
  55. package/dist/tool-control-plane.d.ts +1 -1
  56. package/dist/tool-policy.d.ts +1 -1
  57. package/dist/tool-step.d.ts +10 -0
  58. package/dist/trace-events.d.ts +5 -5
  59. package/dist/trust-tagging.d.ts +1 -1
  60. package/dist/{tui-app-DxLPSGh2.js → tui-app-CFfOP2DC.js} +176 -20
  61. package/dist/tui-app-CFfOP2DC.js.map +1 -0
  62. package/dist/tui-app.d.ts +34 -9
  63. package/dist/tui-mode-flag.d.ts +1 -1
  64. package/dist/turn-intent-classifier.d.ts +5 -5
  65. package/dist/turn-interpreter.d.ts +12 -0
  66. package/dist/turn-policy.d.ts +1 -1
  67. package/dist/update-check-flag.d.ts +11 -0
  68. package/dist/version.d.ts +6 -0
  69. package/package.json +7 -3
  70. package/dist/cli-session-D0uLdT2A.js.map +0 -1
  71. package/dist/tui-app-DxLPSGh2.js.map +0 -1
@@ -1,5 +1,15 @@
1
1
  import { ILLMClient, TokenUsage } from '@buildaharness/runtime';
2
2
  import { BeliefCandidate } from './contradiction-checker.js';
3
+ /**
4
+ * `AUDIT_SEMANTIC_CHANGE_REVIEW` gate — feature-value audit (Phase C2 of the internal plan).
5
+ * Default **ON**: the semantic change-reviewer LLM call ships enabled, so an unset / empty / truthy
6
+ * value keeps today's behaviour. Set to a falsy value (`0` / `false` / `off` / `no` / `disabled`)
7
+ * to skip the hook entirely, leaving `reviewProposedChange`'s lexical `isNegation` check as the
8
+ * only conflict check. Read at exactly one call site — `harness-bridge.ts`, where the
9
+ * `semanticChangeReviewer` host hook is wired. Same shape as `semanticContradictionEnabled()`
10
+ * (contradiction-checker.ts).
11
+ */
12
+ export declare function semanticChangeReviewEnabled(env?: Record<string, string | undefined>): boolean;
3
13
  /**
4
14
  * One LLM call checking a proposed change against everything relevant at once (never one call
5
15
  * per belief/prediction) — layered on top of review-proposed-change.ts's lexical `isNegation`
@@ -2,7 +2,7 @@ import { ILLMClient, LLMStructuredResponse } from '@buildaharness/runtime';
2
2
  /**
3
3
  * A deterministic, network-free {@link ILLMClient} for tests and demos — the one publicly
4
4
  * supported way to drive a real `PersonalAssistant` turn end to end without a proxy, an API key,
5
- * or a live model. Built for plans/chat_ui_browser_e2e_plan.html phase B1 (the browser-e2e lane
5
+ * or a live model. Built for the internal plan phase B1 (the browser-e2e lane
6
6
  * that unblocks the `ASSISTANT_ONE_LOOP` default-flip), and reusable by chat-ui's `demo-seed.ts`.
7
7
  *
8
8
  * It mirrors the shape of the internal `ScriptedToolLLMClient` that `assistant.test.ts` has used
@@ -0,0 +1,9 @@
1
+ /** `~/.buildaharness/personal-assistant/sea-cache/<version>` — same data dir as cli.ts's `defaultDataDir`. */
2
+ export declare function seaCacheDir(version: string, home?: string): string;
3
+ /**
4
+ * Writes `read()`'s bytes to `<dir>/<fileName>` unless that exact path already exists, and returns
5
+ * the path. Version-gated by construction: `dir` embeds the CLI version, so a new release never
6
+ * reuses a stale extraction. Written via a temp file + rename so a crash or a concurrent second
7
+ * launch can never observe (and then import) a half-written file.
8
+ */
9
+ export declare function extractAssetOnce(dir: string, fileName: string, read: () => string | ArrayBuffer): string;
@@ -0,0 +1,101 @@
1
+ export declare const MANIFEST_URL = "https://myaielia.com/aielia-latest.json";
2
+ export declare const RELEASES_API_URL = "https://api.github.com/repos/3IVIS/buildaharness/releases?per_page=50";
3
+ export declare const RELEASE_TAG_PREFIX = "aielia-v";
4
+ export declare const CHECK_INTERVAL_MS: number;
5
+ export interface ReleaseAsset {
6
+ url: string;
7
+ /** From the manifest — the trusted hash (a different origin than the binary's github.com URL). Absent on the GitHub-API fallback. */
8
+ sha256?: string;
9
+ size?: number;
10
+ }
11
+ export interface ReleaseInfo {
12
+ tag: string;
13
+ version: string;
14
+ /** Keyed `<process.platform>-<process.arch>`, e.g. `linux-x64`, `darwin-arm64`, `win32-x64`. */
15
+ assets: Record<string, ReleaseAsset>;
16
+ source: 'manifest' | 'github';
17
+ }
18
+ export interface UpdateCheckCache {
19
+ checkedAt: number;
20
+ latestVersion?: string;
21
+ latestTag?: string;
22
+ /** ETag of the GitHub Releases API response, for conditional re-requests. */
23
+ etag?: string;
24
+ }
25
+ export interface SelfUpdateOptions {
26
+ fetchFn?: typeof fetch;
27
+ manifestUrl?: string;
28
+ releasesApiUrl?: string;
29
+ /** Test-only: permit http:// manifest/asset URLs (a local mock server). Production never sets this. */
30
+ allowInsecureHttp?: boolean;
31
+ now?: () => number;
32
+ /** Defaults to `~/.buildaharness/personal-assistant`. */
33
+ dataDir?: string;
34
+ currentVersion?: string;
35
+ platform?: NodeJS.Platform;
36
+ arch?: string;
37
+ /** Path of the running binary. Defaults to `process.execPath` (which, in a SEA, is the binary itself). */
38
+ execPath?: string;
39
+ /** Defaults to whether `node:sea` reports a SEA. */
40
+ isSea?: boolean;
41
+ log?: (line: string) => void;
42
+ }
43
+ /** Numeric `major.minor.patch` comparison; a `-prerelease` suffix sorts below the same release. Returns <0, 0, >0. */
44
+ export declare function compareVersions(a: string, b: string): number;
45
+ /** Throws on anything but https:// (unless the test-only override is set) — applied to the manifest URL and every asset URL. */
46
+ export declare function assertHttps(url: string, allowInsecureHttp?: boolean): void;
47
+ /**
48
+ * Whether the passive check may make its unsolicited outbound request at all. Off for scripted/CI
49
+ * runs (stdin not a TTY, or `ASSISTANT_NON_INTERACTIVE_APPROVAL` set) and when opted out via
50
+ * `ASSISTANT_UPDATE_CHECK=disabled` / `/config set updateCheck disabled`.
51
+ */
52
+ export declare function shouldRunPassiveUpdateCheck(input: {
53
+ updateCheck: 'enabled' | 'disabled' | undefined;
54
+ env: NodeJS.ProcessEnv;
55
+ stdinIsTty: boolean;
56
+ }): boolean;
57
+ /**
58
+ * Resolves the latest release: the product-scoped manifest first, the GitHub Releases API (filtered
59
+ * to `aielia-v*` tags, ETag-cached) as fallback. A plain GET each — no query string, no
60
+ * identifiers, default User-Agent only. Returns `notModified` when the API answered 304 to the
61
+ * cached ETag; the caller then keeps the cached version.
62
+ */
63
+ export declare function fetchLatestRelease(options: SelfUpdateOptions, etag?: string): Promise<{
64
+ release: ReleaseInfo;
65
+ etag?: string;
66
+ } | {
67
+ notModified: true;
68
+ }>;
69
+ export interface UpdateCheckResult {
70
+ latestVersion: string;
71
+ latestTag?: string;
72
+ updateAvailable: boolean;
73
+ }
74
+ /**
75
+ * The passive check: 24h-throttled via `<dataDir>/update-check.json`. Never throws — any failure
76
+ * (offline, malformed manifest, …) resolves to `null`, since an unsolicited background check must
77
+ * never disturb a session. Callers are responsible for gating it with `shouldRunPassiveUpdateCheck`
78
+ * first and for not awaiting it before the prompt renders.
79
+ */
80
+ export declare function checkForUpdate(options?: SelfUpdateOptions): Promise<UpdateCheckResult | null>;
81
+ /** One-line notice; the instruction branches on how this copy was installed. */
82
+ export declare function updateAvailableNotice(latestVersion: string, currentVersion: string, isSea: boolean): string;
83
+ /** `isSea` the way the rest of the CLI decides it — false under npm/tsx, where nothing can be self-replaced. */
84
+ export declare function isRunningAsSea(): boolean;
85
+ /** Removes leftovers of a previous update (the Windows `.old` rename-aside copy, an interrupted temp download). Never throws. */
86
+ export declare function cleanupStaleUpdateFiles(execPath?: string): void;
87
+ /**
88
+ * Replaces the binary at `execPath` with the already-verified file at `newFile` (same directory).
89
+ * Unix: `rename` over the running binary — POSIX lets a still-open inode be replaced. Windows can't
90
+ * overwrite a running exe, so the current one is renamed aside to `.old` first (cleaned up on the
91
+ * next launch by `cleanupStaleUpdateFiles`), rolling back if the second rename fails.
92
+ */
93
+ export declare function replaceBinary(execPath: string, newFile: string, platform?: NodeJS.Platform): void;
94
+ /**
95
+ * `aielia update [--dry-run]`. Returns the process exit code (0 = up to date / updated / dry-run
96
+ * reported; 1 = refused or failed) and reports through `options.log`. Always does a fresh check —
97
+ * never the throttled cache — and works regardless of the `updateCheck` opt-out.
98
+ */
99
+ export declare function runUpdateCommand(input?: {
100
+ dryRun?: boolean;
101
+ } & SelfUpdateOptions): Promise<number>;
@@ -15,6 +15,16 @@ import { Belief } from '@buildaharness/harness';
15
15
  * NOT skipped.
16
16
  */
17
17
  export declare const NON_CHECKABLE_DEFAULT_CRITERION = "Respond helpfully, accurately, and safely to the user request.";
18
+ /**
19
+ * `AUDIT_SEMANTIC_CRITERION_COVERAGE` gate — feature-value audit (Phase C1 of
20
+ * the internal plan). Default **ON**: the semantic criterion-coverage LLM call ships enabled, so an
21
+ * unset / empty / truthy value keeps today's behaviour. Set to a falsy value (`0` / `false` /
22
+ * `off` / `no` / `disabled`) to skip the hook entirely and leave the reviewer's implementer lens
23
+ * with its `.includes()` substring check alone. Read at exactly one call site — `harness-bridge.ts`,
24
+ * where the `semanticCriterionCoverage` host hook is wired. Same shape as
25
+ * `semanticContradictionEnabled()` (contradiction-checker.ts).
26
+ */
27
+ export declare function semanticCriterionCoverageEnabled(env?: Record<string, string | undefined>): boolean;
18
28
  /**
19
29
  * One LLM call per uncovered criterion, checking it against the whole belief set at once — layered
20
30
  * on top of reviewerPass's implementerLens (packages/harness/src/nodes/reviewer-pass.ts), which
@@ -60,7 +60,7 @@ export interface ShellToolsContext extends ShellStagingContext {
60
60
  * Hostnames an approved command's HTTP(S)_PROXY traffic may reach (exact match or subdomain) —
61
61
  * see network-containment.ts and shell-executor.ts's networkContainmentEnv. Passed through to
62
62
  * executeCommand at apply time, same as timeoutMs. Undefined/empty denies all network access,
63
- * the safe default (Decision 6, plans/lexical_functions_hardening_plan.html Phase 4).
63
+ * the safe default (Decision 6, the internal plan Phase 4).
64
64
  */
65
65
  networkAllowlist?: string[];
66
66
  executeCommand: ShellCommandExecutor;
package/dist/spike.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"spike.js","sources":["../src/tui-spike.tsx"],"sourcesContent":["// Phase 0 feasibility spike for plans/personal_assistant_cli_pinned_input_plan.html.\n// Throwaway — not part of the shipped feature. Delete once Phase 5 confirms Ink\n// works over a real live terminal (raw-mode input + redraw), along with its\n// temporary build entry in vite.config.lib.ts.\nimport React, { useState } from 'react'\nimport { Box, Text, render, useInput } from 'ink'\n\nfunction Spike() {\n const [lastKey, setLastKey] = useState('(none yet)')\n\n useInput((input, key) => {\n if (key.escape || (key.ctrl && input === 'c')) {\n process.exit(0)\n }\n setLastKey(key.return ? '<enter>' : input || JSON.stringify(key))\n })\n\n return (\n <Box borderStyle=\"round\" padding={1}>\n <Text>Ink spike — hello world. Last key: {lastKey}</Text>\n </Box>\n )\n}\n\nrender(<Spike />)\n"],"names":[],"mappings":";;;AAOA,SAAS,QAAQ;AACf,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,YAAY;AAEnD,WAAS,CAAC,OAAO,QAAQ;AACvB,QAAI,IAAI,UAAW,IAAI,QAAQ,UAAU,KAAM;AAC7C,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,eAAW,IAAI,SAAS,YAAY,SAAS,KAAK,UAAU,GAAG,CAAC;AAAA,EAClE,CAAC;AAED,6BACG,KAAA,EAAI,aAAY,SAAQ,SAAS,GAChC,+BAAC,MAAA,EAAK,UAAA;AAAA,IAAA;AAAA,IAAoC;AAAA,EAAA,EAAA,CAAQ,EAAA,CACpD;AAEJ;AAEA,OAAO,oBAAC,SAAM,CAAE;"}
1
+ {"version":3,"file":"spike.js","sources":["../src/tui-spike.tsx"],"sourcesContent":["// Phase 0 feasibility spike for the internal plan.\n// Throwaway — not part of the shipped feature. Delete once Phase 5 confirms Ink\n// works over a real live terminal (raw-mode input + redraw), along with its\n// temporary build entry in vite.config.lib.ts.\nimport React, { useState } from 'react'\nimport { Box, Text, render, useInput } from 'ink'\n\nfunction Spike() {\n const [lastKey, setLastKey] = useState('(none yet)')\n\n useInput((input, key) => {\n if (key.escape || (key.ctrl && input === 'c')) {\n process.exit(0)\n }\n setLastKey(key.return ? '<enter>' : input || JSON.stringify(key))\n })\n\n return (\n <Box borderStyle=\"round\" padding={1}>\n <Text>Ink spike — hello world. Last key: {lastKey}</Text>\n </Box>\n )\n}\n\nrender(<Spike />)\n"],"names":[],"mappings":";;;AAOA,SAAS,QAAQ;AACf,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,YAAY;AAEnD,WAAS,CAAC,OAAO,QAAQ;AACvB,QAAI,IAAI,UAAW,IAAI,QAAQ,UAAU,KAAM;AAC7C,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,eAAW,IAAI,SAAS,YAAY,SAAS,KAAK,UAAU,GAAG,CAAC;AAAA,EAClE,CAAC;AAED,6BACG,KAAA,EAAI,aAAY,SAAQ,SAAS,GAChC,+BAAC,MAAA,EAAK,UAAA;AAAA,IAAA;AAAA,IAAoC;AAAA,EAAA,EAAA,CAAQ,EAAA,CACpD;AAEJ;AAEA,OAAO,oBAAC,SAAM,CAAE;"}
@@ -1,6 +1,6 @@
1
1
  import { WorldModel, EvidenceStore, Diagnostics, FailureDiagnostics, ControlState, ToolAvailability } from '@buildaharness/harness';
2
2
  /**
3
- * Phase 4c of plans/harness_and_assistant_architecture_remediation_plan.html — a live, per-turn
3
+ * Phase 4c of the internal plan — a live, per-turn
4
4
  * harness ControlState, so tool-policy.ts's evaluateToolPolicy() can be given a real
5
5
  * controlState instead of the pre-evidence-baseline `undefined` every call site passed before
6
6
  * this phase. Deliberately builds on the fallback control-plane design (no packages/harness core
@@ -1,6 +1,6 @@
1
1
  import { RiskLevel } from './turn-intent-classifier.js';
2
2
  /**
3
- * Phase 4 of plans/harness_and_assistant_architecture_remediation_plan.html: the deterministic,
3
+ * Phase 4 of the internal plan: the deterministic,
4
4
  * authoritative decision point for whether a specific tool call may proceed — replacing "advisory
5
5
  * LLM classification, checked after the fact" with "deterministic policy informed by real harness
6
6
  * state, checked before execution" (the substantive fix both external architecture critiques
@@ -8,6 +8,16 @@ import { ToolStepEvent } from '@buildaharness/runtime';
8
8
  */
9
9
  export interface AssistantToolStep extends ToolStepEvent {
10
10
  summary: string;
11
+ /**
12
+ * Set only on a second, follow-up `onToolStep` call fired immediately after `tool-policy.ts`'s
13
+ * `evaluateToolPolicy()` denies this exact call (see `agent-loop.ts`'s `checkToolPolicy`) — the
14
+ * first call (this field absent) already reported the proposal itself. Without this, a denied
15
+ * read-only tool call was invisible to the user: the model saw a "Denied by tool policy: …"
16
+ * result, but nothing ever told the person watching the transcript that the call didn't happen
17
+ * (report finding: "no confirmation, no denial message, nothing to distinguish 'the policy
18
+ * allowed this' from 'there was no policy at all'").
19
+ */
20
+ deniedReason?: string;
11
21
  }
12
22
  /**
13
23
  * Strips an MCP-qualified tool name (`mcp__<server>__<tool>`, how Claude Code CLI surfaces
@@ -47,7 +47,7 @@ export type TraceEvent = {
47
47
  }
48
48
  /**
49
49
  * One of the harness's 11 layers did (or explicitly skipped) real work this step —
50
- * see plans/harness_layer_activation_plan.html Phase 2/3.1. `layer` is a stable slug
50
+ * see the internal plan Phase 2/3.1. `layer` is a stable slug
51
51
  * ('world_model' | 'evidence_reasoning' | 'hypothesis' | 'contradiction' | 'diagnostics' |
52
52
  * 'control_state' | 'planning' | 'execution' | 'verification' | 'recovery' | 'reviewer_pass'),
53
53
  * not a free-text name, so a "Why?"/`/layers` renderer can key off it directly.
@@ -71,7 +71,7 @@ export type TraceEvent = {
71
71
  failedAttempts: number;
72
72
  }
73
73
  /**
74
- * Phase 4 of plans/harness_and_assistant_architecture_remediation_plan.html — see
74
+ * Phase 4 of the internal plan — see
75
75
  * execution-mode.ts's own doc comment for each mode's guarantee level.
76
76
  */
77
77
  | {
@@ -93,14 +93,14 @@ export type TraceEvent = {
93
93
  * Which proposer drove this turn — 'posthoc' (flag OFF, or a trivial / no-tool turn),
94
94
  * 'flat-oneloop', or 'batch-oneloop' (both flag ON). Emitted once per turn from runTurn, right
95
95
  * after `useOneLoop` is resolved. See AssistantTurnResult.proposerKind and
96
- * plans/chat_ui_browser_e2e_plan.html phase B1 for why this exists.
96
+ * the internal plan phase B1 for why this exists.
97
97
  */
98
98
  | {
99
99
  kind: 'proposer_selected';
100
100
  proposerKind: ProposerKind;
101
101
  }
102
102
  /**
103
- * P9 of plans/ask_question_and_plan_mode_plan.html — the lightweight plan-sketch delegate
103
+ * P9 of the internal plan — the lightweight plan-sketch delegate
104
104
  * (PlanSketchService) ran. `requestPreview` is truncated to stay name/status-only per this
105
105
  * type's own doc comment, not the full request text. Distinct from `plan_classified`/
106
106
  * `plan_updated`, which are about the stateful drafting/approval machinery (PlanDraftingService)
@@ -111,7 +111,7 @@ export type TraceEvent = {
111
111
  requestPreview: string;
112
112
  }
113
113
  /**
114
- * P10 of plans/ask_question_and_plan_mode_plan.html — a write_file/run_shell_command/send_email
114
+ * P10 of the internal plan — a write_file/run_shell_command/send_email
115
115
  * proposal was auto-applied instead of staged for approval because it was proposed while
116
116
  * `taskId` (part of a plan approved with `'approve_trusted'`) was the currently RUNNING task —
117
117
  * see PlanRecord.trustApprovedSteps and INV-36. The action itself still went through
@@ -15,7 +15,7 @@ export interface InjectionDetection {
15
15
  export declare function detectInjectionLikely(text: string): InjectionDetection;
16
16
  /**
17
17
  * `AUDIT_LLM_INJECTION_DETECT` gate — feature-value audit (Phase A5 of
18
- * plans/feature_audit_automation_plan.html). Default **ON**: the semantic injection-detection LLM
18
+ * the internal plan). Default **ON**: the semantic injection-detection LLM
19
19
  * call ships enabled, so an unset / empty / truthy value keeps today's behaviour. Set to a falsy
20
20
  * value (`0` / `false` / `off` / `no` / `disabled`) to skip the LLM escalation entirely and fall
21
21
  * back to the always-on deterministic regex/pattern pass only. Read at exactly one place —
@@ -3,10 +3,11 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsxs, jsx } from "react/jsx-runtime";
5
5
  import { Readable, Writable } from "node:stream";
6
- import { useState, useRef, useCallback, useSyncExternalStore } from "react";
6
+ import { useState, useRef, useCallback, useSyncExternalStore, useEffect } from "react";
7
7
  import { useWindowSize, usePaste, useInput, Box, Text, render, Static } from "ink";
8
- import { runCli } from "./cli.js";
8
+ import { r as runCli, P as PLAN_LINE_PREFIX, I as ICONS } from "./bin-Btzhiyzi.js";
9
9
  import { format } from "node:util";
10
+ import { h as DIFF_INDENT } from "./cli-session-DnSc6Hhx.js";
10
11
  function classify(raw, stream) {
11
12
  if (raw.startsWith("\r")) {
12
13
  return { type: "progress", text: raw.replace(/\r/g, "").trimEnd() };
@@ -210,18 +211,131 @@ function TuiInput(props) {
210
211
  }) })
211
212
  ] });
212
213
  }
214
+ function SelectPrompt(props) {
215
+ const { question, options, onSubmit, isActive = true } = props;
216
+ const [highlighted, setHighlighted] = useState(0);
217
+ useInput(
218
+ (input, key) => {
219
+ if (key.upArrow) {
220
+ setHighlighted((h) => (h - 1 + options.length) % options.length);
221
+ return;
222
+ }
223
+ if (key.downArrow) {
224
+ setHighlighted((h) => (h + 1) % options.length);
225
+ return;
226
+ }
227
+ if (key.return) {
228
+ onSubmit(options[highlighted].key);
229
+ return;
230
+ }
231
+ const matched = options.find((option) => option.key.toLowerCase() === input.toLowerCase());
232
+ if (matched) onSubmit(matched.key);
233
+ },
234
+ { isActive }
235
+ );
236
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
237
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: question }),
238
+ /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: options.map((option, i) => /* @__PURE__ */ jsxs(Text, { inverse: i === highlighted, children: [
239
+ i === highlighted ? "› " : " ",
240
+ "[",
241
+ option.key,
242
+ "] ",
243
+ option.label
244
+ ] }, option.key)) })
245
+ ] });
246
+ }
247
+ const HEADER_RE = /^(#{1,6})\s+(.*)$/;
248
+ const CHECKBOX_RE = /^(\s*[-*]\s)\[([ xX])\]\s+(.*)$/;
249
+ const DIFF_LINE_RE = /^(\s*\d+) ([+\- ])(.*)$/;
250
+ const INLINE_TOKEN_RE = /\*\*(.+?)\*\*|`([^`]+)`/g;
251
+ const ASSISTANT_BULLET = "- ";
252
+ function matchDiffLine(text) {
253
+ if (!text.startsWith(DIFF_INDENT)) return void 0;
254
+ const rest = text.slice(DIFF_INDENT.length);
255
+ const match = DIFF_LINE_RE.exec(rest);
256
+ if (!match) return void 0;
257
+ const sign = match[2];
258
+ return { content: rest, color: sign === "+" ? "green" : sign === "-" ? "red" : void 0 };
259
+ }
260
+ function renderDiffLine(text, key) {
261
+ const diffLine = matchDiffLine(text);
262
+ if (!diffLine) return void 0;
263
+ return /* @__PURE__ */ jsxs(Text, { color: diffLine.color, children: [
264
+ DIFF_INDENT,
265
+ diffLine.content
266
+ ] }, key);
267
+ }
268
+ const NEEDS_APPROVAL_RE = /^(\[needs approval[^\]]*\])(.*)$/;
269
+ function renderNeedsApprovalLine(text, key) {
270
+ const match = NEEDS_APPROVAL_RE.exec(text);
271
+ if (!match) return void 0;
272
+ return /* @__PURE__ */ jsxs(Text, { children: [
273
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: match[1] }),
274
+ match[2]
275
+ ] }, key);
276
+ }
277
+ function renderInlineSegments(text) {
278
+ const segments = [];
279
+ let lastIndex = 0;
280
+ let key = 0;
281
+ INLINE_TOKEN_RE.lastIndex = 0;
282
+ let match;
283
+ while ((match = INLINE_TOKEN_RE.exec(text)) !== null) {
284
+ if (match.index > lastIndex) segments.push(text.slice(lastIndex, match.index));
285
+ if (match[1] !== void 0) {
286
+ segments.push(
287
+ /* @__PURE__ */ jsx(Text, { bold: true, children: match[1] }, key++)
288
+ );
289
+ } else if (match[2] !== void 0) {
290
+ segments.push(
291
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: match[2] }, key++)
292
+ );
293
+ }
294
+ lastIndex = INLINE_TOKEN_RE.lastIndex;
295
+ }
296
+ if (lastIndex < text.length || segments.length === 0) segments.push(text.slice(lastIndex));
297
+ return segments;
298
+ }
299
+ function renderMarkdownLine(text, key, continuation = false) {
300
+ const diffElement = renderDiffLine(text, key);
301
+ if (diffElement) return diffElement;
302
+ const headerMatch = HEADER_RE.exec(text);
303
+ if (headerMatch) {
304
+ return /* @__PURE__ */ jsx(Text, { bold: true, underline: true, children: renderInlineSegments(headerMatch[2]) }, key);
305
+ }
306
+ const checkboxMatch = CHECKBOX_RE.exec(text);
307
+ if (checkboxMatch) {
308
+ const checked = checkboxMatch[2].toLowerCase() === "x";
309
+ return /* @__PURE__ */ jsxs(Text, { children: [
310
+ checkboxMatch[1],
311
+ checked ? "☑" : "☐",
312
+ " ",
313
+ renderInlineSegments(checkboxMatch[3])
314
+ ] }, key);
315
+ }
316
+ if (text.length === 0) return /* @__PURE__ */ jsx(Text, { children: " " }, key);
317
+ return /* @__PURE__ */ jsxs(Text, { children: [
318
+ continuation ? "" : ASSISTANT_BULLET,
319
+ renderInlineSegments(text)
320
+ ] }, key);
321
+ }
322
+ const ASSISTANT_INDENT_WIDTH = 2;
213
323
  function useStore(store) {
214
324
  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
215
325
  }
216
- const EMPTY_LOG = { lines: [], progressText: "", transientText: "" };
217
- const TOOL_STEP_PREFIX = "⚙";
326
+ const EMPTY_LOG = { lines: [], progressText: "", transientText: "", waitingForOutput: false };
327
+ const TOOL_STEP_PREFIXES = [ICONS.toolStep, ICONS.proposalStep, ICONS.deniedStep];
218
328
  const ASSISTANT_REPLY_PREFIX = "Aielia>";
219
329
  function classifyLineKind(mergedText, streamedReplyWasOpen, stream) {
220
330
  if (stream === "stderr") return "error";
331
+ if (mergedText.trimStart().startsWith(PLAN_LINE_PREFIX)) return "plan";
221
332
  if (streamedReplyWasOpen || mergedText.trimStart().startsWith(ASSISTANT_REPLY_PREFIX)) return "assistant";
222
- if (mergedText.trimStart().startsWith(TOOL_STEP_PREFIX)) return "tool";
333
+ if (TOOL_STEP_PREFIXES.some((prefix) => mergedText.trimStart().startsWith(prefix))) return "tool";
223
334
  return "system";
224
335
  }
336
+ function stripPlanLabel(text) {
337
+ return text.replace(new RegExp(`^\\s*${PLAN_LINE_PREFIX}\\s*`), "");
338
+ }
225
339
  function stripAssistantLabel(text) {
226
340
  return text.replace(/^\s*Aielia>\s?/, "");
227
341
  }
@@ -230,6 +344,7 @@ class EventLogBridge {
230
344
  __publicField(this, "lines", []);
231
345
  __publicField(this, "progressText", "");
232
346
  __publicField(this, "transientText", "");
347
+ __publicField(this, "waitingForOutput", false);
233
348
  __publicField(this, "snapshot", EMPTY_LOG);
234
349
  __publicField(this, "listeners", /* @__PURE__ */ new Set());
235
350
  __publicField(this, "subscribe", (listener) => {
@@ -241,9 +356,14 @@ class EventLogBridge {
241
356
  __publicField(this, "getSnapshot", () => this.snapshot);
242
357
  }
243
358
  commit() {
244
- this.snapshot = { lines: this.lines, progressText: this.progressText, transientText: this.transientText };
359
+ this.snapshot = { lines: this.lines, progressText: this.progressText, transientText: this.transientText, waitingForOutput: this.waitingForOutput };
245
360
  for (const listener of this.listeners) listener();
246
361
  }
362
+ /** Marks the start of a turn's wait for its first output (see `TuiLogState.waitingForOutput`'s doc comment) — called by `TuiApp` right after a chat line or resolved prompt answer is submitted. */
363
+ beginTurn() {
364
+ this.waitingForOutput = true;
365
+ this.commit();
366
+ }
247
367
  /**
248
368
  * A blank spacer line ("some margins between different conversations", later extended to "at
249
369
  * least one line spacing between the user and system messages") — called from three places in
@@ -260,12 +380,13 @@ class EventLogBridge {
260
380
  this.lines = [...this.lines, { text: "", kind: "margin" }];
261
381
  this.commit();
262
382
  }
263
- /** Records a line the user just submitted (chat or a resolved prompt answer) into permanent scrollback — mirrors a real terminal's own line-echo, which the inert (non-TTY) input stream this shell uses never produces on its own. `prefix` is `''` for an ordinary chat line (no "you>" label — see `LogLineText`'s doc comment) and `'> '` for a resolved prompt answer, which keeps its prefix since it's answering a question printed just above it, not identifying whose turn it is. */
383
+ /** Records a line the user just submitted (chat or a resolved prompt answer) into permanent scrollback — mirrors a real terminal's own line-echo, which the inert (non-TTY) input stream this shell uses never produces on its own. `prefix` is `''` for an ordinary chat line (no "you>" label — see `LogLineText`'s doc comment, kind `'user'`) and `'> '` for a resolved prompt answer (kind `'approval'`, its own box color — see `LogLineText` — distinct from a real chat message even though both are the user's own input), which keeps its prefix since it's answering a question printed just above it, not identifying whose turn it is. */
264
384
  pushEchoLine(prefix, text) {
265
- this.lines = [...this.lines, { text: `${prefix}${text}`, kind: "user" }];
385
+ this.lines = [...this.lines, { text: `${prefix}${text}`, kind: prefix === "" ? "user" : "approval" }];
266
386
  this.commit();
267
387
  }
268
388
  handleEvent(event) {
389
+ this.waitingForOutput = false;
269
390
  if (event.type === "progress") {
270
391
  this.progressText = event.text;
271
392
  this.commit();
@@ -279,8 +400,13 @@ class EventLogBridge {
279
400
  const streamedReplyWasOpen = this.transientText.length > 0;
280
401
  const merged = this.transientText + event.lines.join("\n");
281
402
  const kind = classifyLineKind(merged, streamedReplyWasOpen, event.stream);
282
- const displayText = kind === "assistant" ? stripAssistantLabel(merged) : merged;
283
- this.lines = [...this.lines, ...displayText.split("\n").map((text) => ({ text, kind }))];
403
+ const displayText = kind === "assistant" ? stripAssistantLabel(merged) : kind === "plan" ? stripPlanLabel(merged) : merged;
404
+ this.lines = kind === "plan" ? [...this.lines, { text: displayText, kind }] : (
405
+ // continuation is only meaningful (and only set) for 'assistant' — every other kind's
406
+ // LogLine stays a bare { text, kind } so existing exact-shape equality checks elsewhere
407
+ // (classifyLineKind's own test coverage) don't have to account for an unused field.
408
+ [...this.lines, ...displayText.split("\n").map((text, i) => kind === "assistant" ? { text, kind, continuation: i > 0 } : { text, kind })]
409
+ );
284
410
  this.transientText = "";
285
411
  this.progressText = "";
286
412
  this.commit();
@@ -302,10 +428,12 @@ class PromptBridge {
302
428
  /** Matches `RunCliOptions.askYesNo`'s exact parsing convention (`answer.trim().toLowerCase().startsWith('y')`) — same fail-closed shape, just answered via the Ink prompt instead of `rl.question`. */
303
429
  __publicField(this, "askYesNo", (question) => this.ask(question).then((answer) => answer.trim().toLowerCase().startsWith("y")));
304
430
  __publicField(this, "askLine", (question) => this.ask(question));
431
+ /** Matches `RunCliOptions.askSelect`'s contract exactly — resolves with the chosen `SelectOption.key`, answered via `SelectPrompt` instead of `rl.question`. */
432
+ __publicField(this, "askSelect", (question, options) => this.ask(question, options));
305
433
  }
306
- ask(question) {
434
+ ask(question, options) {
307
435
  return new Promise((resolve) => {
308
- this.pending = { question };
436
+ this.pending = { question, options };
309
437
  this.resolve = resolve;
310
438
  for (const listener of this.listeners) listener();
311
439
  });
@@ -335,16 +463,24 @@ class StatusBridge {
335
463
  for (const listener of this.listeners) listener();
336
464
  }
337
465
  }
338
- function UserMessageBox({ text, width }) {
339
- return /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, width, children: text.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { children: line.length > 0 ? line : " " }, i)) });
466
+ function UserMessageBox({ text, width, borderColor = "gray" }) {
467
+ return /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor, flexDirection: "column", paddingX: 1, width, children: text.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { children: line.length > 0 ? line : " " }, i)) });
468
+ }
469
+ function PlanBox({ text, width }) {
470
+ return /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, width, children: text.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { bold: i === 0, children: line.length > 0 ? line : " " }, i)) });
340
471
  }
341
472
  function LogLineText({ line, width }) {
342
473
  switch (line.kind) {
343
474
  case "user":
344
475
  return /* @__PURE__ */ jsx(UserMessageBox, { text: line.text, width });
476
+ case "approval":
477
+ return /* @__PURE__ */ jsx(UserMessageBox, { text: line.text, width, borderColor: "greenBright" });
345
478
  case "assistant":
479
+ return /* @__PURE__ */ jsx(Box, { paddingLeft: ASSISTANT_INDENT_WIDTH, children: renderMarkdownLine(line.text, 0, line.continuation) });
480
+ case "plan":
481
+ return /* @__PURE__ */ jsx(PlanBox, { text: line.text, width });
346
482
  case "system":
347
- return /* @__PURE__ */ jsx(Text, { children: line.text });
483
+ return renderDiffLine(line.text, 0) ?? renderNeedsApprovalLine(line.text, 0) ?? /* @__PURE__ */ jsx(Text, { children: line.text });
348
484
  case "tool":
349
485
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: line.text });
350
486
  case "error":
@@ -353,6 +489,19 @@ function LogLineText({ line, width }) {
353
489
  return /* @__PURE__ */ jsx(Text, { children: " " });
354
490
  }
355
491
  }
492
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
493
+ const SPINNER_INTERVAL_MS = 80;
494
+ function Spinner() {
495
+ const [frame, setFrame] = useState(0);
496
+ useEffect(() => {
497
+ const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
498
+ return () => clearInterval(id);
499
+ }, []);
500
+ return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
501
+ SPINNER_FRAMES[frame],
502
+ " Thinking…"
503
+ ] });
504
+ }
356
505
  function StatusLine({ indicators }) {
357
506
  return /* @__PURE__ */ jsx(Text, { children: indicators.map((indicator, index) => /* @__PURE__ */ jsxs(Text, { children: [
358
507
  index > 0 ? " " : "",
@@ -374,26 +523,32 @@ function TuiApp(props) {
374
523
  eventLog.pushTurnMargin();
375
524
  eventLog.pushEchoLine("", line);
376
525
  eventLog.pushTurnMargin();
526
+ eventLog.beginTurn();
377
527
  onSubmitChat(line);
378
528
  },
379
529
  [eventLog, onSubmitChat]
380
530
  );
381
531
  const handleSubmitPrompt = useCallback(
382
532
  (line) => {
383
- eventLog.pushEchoLine("> ", line);
533
+ var _a, _b;
534
+ const displayAnswer = ((_b = (_a = pending == null ? void 0 : pending.options) == null ? void 0 : _a.find((option) => option.key === line)) == null ? void 0 : _b.label) ?? line;
535
+ eventLog.pushEchoLine("> ", (pending == null ? void 0 : pending.question) !== void 0 ? `${pending.question.trimEnd()} → ${displayAnswer}` : line);
536
+ eventLog.beginTurn();
384
537
  prompt.submit(line);
385
538
  },
386
- [eventLog, prompt]
539
+ [eventLog, prompt, pending]
387
540
  );
388
541
  const hasTransient = log.progressText.length > 0 || log.transientText.length > 0;
542
+ const showSpinner = log.waitingForOutput && !hasTransient;
389
543
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
390
544
  /* @__PURE__ */ jsx(Static, { items: log.lines, children: (line, index) => /* @__PURE__ */ jsx(LogLineText, { line, width }, index) }),
545
+ showSpinner && /* @__PURE__ */ jsx(Spinner, {}),
391
546
  hasTransient && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
392
547
  log.progressText.length > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: log.progressText }),
393
548
  log.transientText.length > 0 && /* @__PURE__ */ jsx(Text, { children: stripAssistantLabel(log.transientText) })
394
549
  ] }),
395
550
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "─".repeat(Math.max(1, width)) }),
396
- /* @__PURE__ */ jsx(
551
+ (pending == null ? void 0 : pending.options) ? /* @__PURE__ */ jsx(SelectPrompt, { question: pending.question, options: pending.options, onSubmit: handleSubmitPrompt }) : /* @__PURE__ */ jsx(
397
552
  TuiInput,
398
553
  {
399
554
  promptLabel: pending == null ? void 0 : pending.question,
@@ -436,7 +591,8 @@ async function runTuiApp(options = {}) {
436
591
  input: options.input ?? inert.input,
437
592
  output: options.output ?? inert.output,
438
593
  askYesNo: prompt.askYesNo,
439
- askLine: prompt.askLine
594
+ askLine: prompt.askLine,
595
+ askSelect: prompt.askSelect
440
596
  });
441
597
  } catch (err) {
442
598
  restoreCapture();
@@ -478,4 +634,4 @@ export {
478
634
  TuiApp,
479
635
  runTuiApp
480
636
  };
481
- //# sourceMappingURL=tui-app-DxLPSGh2.js.map
637
+ //# sourceMappingURL=tui-app-CFfOP2DC.js.map