@buildaharness/aielia 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +832 -0
  2. package/dist/action-approval-service.d.ts +52 -0
  3. package/dist/action-snapshot.d.ts +173 -0
  4. package/dist/action-tools.d.ts +38 -0
  5. package/dist/agent-loop.d.ts +372 -0
  6. package/dist/answer-claim.d.ts +51 -0
  7. package/dist/ask-clarification-service.d.ts +70 -0
  8. package/dist/ask-mode-flag.d.ts +25 -0
  9. package/dist/ask-response-format.d.ts +8 -0
  10. package/dist/assistant-session.d.ts +267 -0
  11. package/dist/assistant-source.d.ts +14 -0
  12. package/dist/assistant-types.d.ts +158 -0
  13. package/dist/assistant.d.ts +419 -0
  14. package/dist/batch-list-detector.d.ts +15 -0
  15. package/dist/batch-research-fixtures.d.ts +50 -0
  16. package/dist/benchmark-injected-failure.d.ts +27 -0
  17. package/dist/claude-cli-llm-client.d.ts +139 -0
  18. package/dist/claude-cli-prompt.d.ts +86 -0
  19. package/dist/cli-config.d.ts +27 -0
  20. package/dist/cli-session-D0uLdT2A.js +7863 -0
  21. package/dist/cli-session-D0uLdT2A.js.map +1 -0
  22. package/dist/cli-session.d.ts +63 -0
  23. package/dist/cli.d.ts +65 -0
  24. package/dist/cli.js +1693 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/config.d.ts +175 -0
  27. package/dist/contradiction-checker.d.ts +27 -0
  28. package/dist/debug-log.d.ts +16 -0
  29. package/dist/decomposition-classifier.d.ts +38 -0
  30. package/dist/doctor-checks.d.ts +8 -0
  31. package/dist/email-smtp.d.ts +19 -0
  32. package/dist/email.d.ts +60 -0
  33. package/dist/error-classifier.d.ts +13 -0
  34. package/dist/execution-mode.d.ts +85 -0
  35. package/dist/fact-extraction.d.ts +143 -0
  36. package/dist/failure-mode-matcher.d.ts +32 -0
  37. package/dist/file-tools-mcp-server.mjs +1027 -0
  38. package/dist/file-tools.d.ts +179 -0
  39. package/dist/first-run.d.ts +22 -0
  40. package/dist/harness-bridge.d.ts +103 -0
  41. package/dist/index.d.ts +58 -0
  42. package/dist/index.js +212 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/ink-test-render.d.ts +24 -0
  45. package/dist/lexical/patterns/batch-list-markers.json +8 -0
  46. package/dist/lexical/patterns/batch-list-markers.json.d.ts +11 -0
  47. package/dist/lexical/patterns/coding-fact-markers.json +8 -0
  48. package/dist/lexical/patterns/coding-fact-markers.json.d.ts +11 -0
  49. package/dist/lexical/patterns/enumeration-markers.json +20 -0
  50. package/dist/lexical/patterns/enumeration-markers.json.d.ts +23 -0
  51. package/dist/lexical/patterns/fact-markers.json +16 -0
  52. package/dist/lexical/patterns/fact-markers.json.d.ts +19 -0
  53. package/dist/lexical/patterns/injection-patterns.json +20 -0
  54. package/dist/lexical/patterns/injection-patterns.json.d.ts +23 -0
  55. package/dist/lexical/patterns/plan-mode-markers.json +10 -0
  56. package/dist/lexical/patterns/plan-mode-markers.json.d.ts +13 -0
  57. package/dist/lexical/patterns/risk-patterns.json +122 -0
  58. package/dist/lexical/patterns/risk-patterns.json.d.ts +125 -0
  59. package/dist/lexical/patterns/task-cancel-markers.json +22 -0
  60. package/dist/lexical/patterns/task-cancel-markers.json.d.ts +25 -0
  61. package/dist/lexical/patterns/template-keywords.json +20 -0
  62. package/dist/lexical/patterns/template-keywords.json.d.ts +23 -0
  63. package/dist/lexical/patterns/tool-yield-markers.json +22 -0
  64. package/dist/lexical/patterns/tool-yield-markers.json.d.ts +25 -0
  65. package/dist/lexical/patterns.d.ts +64 -0
  66. package/dist/memory-service.d.ts +219 -0
  67. package/dist/model-pricing.d.ts +19 -0
  68. package/dist/network-containment.d.ts +21 -0
  69. package/dist/node-config-store.d.ts +29 -0
  70. package/dist/node-display-names.d.ts +26 -0
  71. package/dist/node-fs-backend.d.ts +8 -0
  72. package/dist/non-interactive-mode.d.ts +22 -0
  73. package/dist/one-loop-flag.d.ts +38 -0
  74. package/dist/plan-approval-service.d.ts +62 -0
  75. package/dist/plan-builder.d.ts +32 -0
  76. package/dist/plan-drafting-service.d.ts +87 -0
  77. package/dist/plan-drafting.d.ts +58 -0
  78. package/dist/plan-mode-flag.d.ts +34 -0
  79. package/dist/plan-service.d.ts +61 -0
  80. package/dist/plan-sketch-service.d.ts +24 -0
  81. package/dist/plan-sketch.d.ts +20 -0
  82. package/dist/plan-store.d.ts +232 -0
  83. package/dist/plan-templates/data/content_creation.json.d.ts +75 -0
  84. package/dist/plan-templates/data/decision_making.json.d.ts +66 -0
  85. package/dist/plan-templates/data/problem_solving.json.d.ts +75 -0
  86. package/dist/plan-templates/data/process_improvement.json.d.ts +57 -0
  87. package/dist/plan-templates/data/project_planning.json.d.ts +66 -0
  88. package/dist/plan-templates/data/research_analysis.json.d.ts +66 -0
  89. package/dist/plan-templates/data/trip_planning.json.d.ts +66 -0
  90. package/dist/plan-templates/index.d.ts +33 -0
  91. package/dist/plan-verification.d.ts +31 -0
  92. package/dist/reminder-tools.d.ts +13 -0
  93. package/dist/response-service.d.ts +73 -0
  94. package/dist/review-checker.d.ts +22 -0
  95. package/dist/risk-classifier.d.ts +7 -0
  96. package/dist/scripted-llm-client.d.ts +37 -0
  97. package/dist/semantic-criterion-coverage.d.ts +30 -0
  98. package/dist/shell-executor.d.ts +14 -0
  99. package/dist/shell-tools.d.ts +84 -0
  100. package/dist/spend-cap.d.ts +41 -0
  101. package/dist/spike.d.ts +1 -0
  102. package/dist/spike.js +18 -0
  103. package/dist/spike.js.map +1 -0
  104. package/dist/supervisor-decider.d.ts +3 -0
  105. package/dist/system-prompt.d.ts +8 -0
  106. package/dist/task-mapping.d.ts +42 -0
  107. package/dist/tool-control-plane.d.ts +67 -0
  108. package/dist/tool-policy.d.ts +45 -0
  109. package/dist/tool-step.d.ts +23 -0
  110. package/dist/tool-yield-classifier.d.ts +7 -0
  111. package/dist/trace-events.d.ts +126 -0
  112. package/dist/transcript-compaction.d.ts +13 -0
  113. package/dist/trust-tagging.d.ts +38 -0
  114. package/dist/tui-app-DxLPSGh2.js +481 -0
  115. package/dist/tui-app-DxLPSGh2.js.map +1 -0
  116. package/dist/tui-app.d.ts +125 -0
  117. package/dist/tui-input.d.ts +51 -0
  118. package/dist/tui-mode-flag.d.ts +36 -0
  119. package/dist/tui-output-capture.d.ts +30 -0
  120. package/dist/tui-spike.d.ts +1 -0
  121. package/dist/turn-intent-classifier.d.ts +96 -0
  122. package/dist/turn-interpreter.d.ts +71 -0
  123. package/dist/turn-policy.d.ts +45 -0
  124. package/dist/web-fetch-core.d.ts +65 -0
  125. package/dist/web-search-provider.d.ts +12 -0
  126. package/dist/web-tools.d.ts +26 -0
  127. package/package.json +69 -0
@@ -0,0 +1,125 @@
1
+ import { RunCliOptions } from './cli.js';
2
+ import { CaptureEvent } from './tui-output-capture.js';
3
+ /**
4
+ * A minimal pub/sub store, one per piece of state the Ink tree needs — deliberately not React
5
+ * state, because the values that drive it arrive from outside React entirely: `startCapture`
6
+ * (Phase 1) calls its `onEvent` callback synchronously from a monkey-patched `console.log`/
7
+ * `process.stdout.write`, and `askYesNo`/`askLine` (Decision 1's override seam) are plain
8
+ * functions handed to `runCli()` before any component has mounted. `useSyncExternalStore` is the
9
+ * React-correct way to subscribe a component to state that's mutated from outside React's own
10
+ * render cycle, without the chicken-and-egg problem of needing a `setState` that only exists
11
+ * after the first render.
12
+ */
13
+ interface Store<T> {
14
+ subscribe: (listener: () => void) => () => void;
15
+ getSnapshot: () => T;
16
+ }
17
+ /**
18
+ * Which visual style a committed scrollback line gets (this task's ask: "different formats for
19
+ * user text vs aielia response vs tool call details"). `'margin'` is a blank spacer line between
20
+ * turns, not a real writer's output — kept as its own kind rather than reusing `'system'` so
21
+ * rendering never has to guess a blank line's intent from empty text alone.
22
+ */
23
+ export type LineKind = 'user' | 'assistant' | 'tool' | 'error' | 'system' | 'margin';
24
+ export interface LogLine {
25
+ text: string;
26
+ kind: LineKind;
27
+ }
28
+ export interface TuiLogState {
29
+ /** Every permanently-committed scrollback line, oldest first — fed straight into `<Static>`. */
30
+ lines: LogLine[];
31
+ /** `writeProgress`'s current `\r`-overwritten line — shown in the transient region, never committed. */
32
+ progressText: string;
33
+ /** `writeToken`'s not-yet-newline-terminated streamed reply text — shown in the transient region until the next committed-line event closes it out. */
34
+ transientText: string;
35
+ }
36
+ /**
37
+ * Classifies and accumulates `CaptureEvent`s (Phase 1) into the three regions Phase 3's layout
38
+ * needs: permanent scrollback, the transient progress line, and the transient in-progress
39
+ * streamed reply. A `'line'` event closes out whatever streamed-token text is currently pending
40
+ * (see `cli.ts`'s `writeToken`/`handleTurn`: a streamed reply is one or more raw `'token'` writes
41
+ * followed by exactly one final `'line'` write for the trailing hint text) — merging the two
42
+ * before committing is what turns "Hi the|re!\n\n" (streamed "Hi the" + committed "re!\n\n") into
43
+ * one correct committed line instead of two garbled ones.
44
+ */
45
+ export declare class EventLogBridge implements Store<TuiLogState> {
46
+ private lines;
47
+ private progressText;
48
+ private transientText;
49
+ private snapshot;
50
+ private listeners;
51
+ subscribe: (listener: () => void) => (() => void);
52
+ getSnapshot: () => TuiLogState;
53
+ private commit;
54
+ /**
55
+ * A blank spacer line ("some margins between different conversations", later extended to "at
56
+ * least one line spacing between the user and system messages") — called from three places in
57
+ * `handleSubmitChat` (never from a resolved approval/clarification prompt answer, which stays
58
+ * visually grouped with the turn it belongs to): once before a new turn's echo (separating it
59
+ * from the previous turn's reply) and once right after it (separating the echo from whatever
60
+ * system/tool/assistant output follows). A no-op on an empty log or if the last line is already
61
+ * a margin, so calling it from both of those spots back-to-back across turns can't stack blank
62
+ * lines — the "before next turn" and "after this turn" calls collapse into the same one line.
63
+ */
64
+ pushTurnMargin(): void;
65
+ /** 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. */
66
+ pushEchoLine(prefix: string, text: string): void;
67
+ handleEvent(event: CaptureEvent): void;
68
+ }
69
+ export type PendingPrompt = {
70
+ question: string;
71
+ } | undefined;
72
+ /**
73
+ * Bridges `runCli()`'s `askYesNo`/`askLine` override seam (Decision 1) to Ink: each call parks
74
+ * a resolver and exposes the pending question to the component tree via `getSnapshot()`, instead
75
+ * of the original `rl.question`-based implementation. `TuiInput`'s own `promptLabel` prop
76
+ * (Phase 2) already knows how to render this state and route Enter to `onSubmitPrompt` instead
77
+ * of `onSubmitChat` — this class only needs to supply the question text and resolve the answer.
78
+ */
79
+ export declare class PromptBridge implements Store<PendingPrompt> {
80
+ private pending;
81
+ private resolve;
82
+ private listeners;
83
+ subscribe: (listener: () => void) => (() => void);
84
+ getSnapshot: () => PendingPrompt;
85
+ private ask;
86
+ /** 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`. */
87
+ askYesNo: (question: string) => Promise<boolean>;
88
+ askLine: (question: string) => Promise<string>;
89
+ submit(answer: string): void;
90
+ }
91
+ /** The status line's short indicator list (Phase 3 step 5) — polled from `CliInstance.getStatusIndicators()` after every dispatched line, since plan/spend-cap state only changes as a side effect of a turn. */
92
+ export declare class StatusBridge implements Store<string[]> {
93
+ private indicators;
94
+ private listeners;
95
+ subscribe: (listener: () => void) => (() => void);
96
+ getSnapshot: () => string[];
97
+ set(indicators: string[]): void;
98
+ }
99
+ export interface TuiAppProps {
100
+ eventLog: EventLogBridge;
101
+ prompt: PromptBridge;
102
+ status: StatusBridge;
103
+ onSubmitChat: (line: string) => void;
104
+ onExit: () => void;
105
+ /** Overrides the live terminal width for tests — same seam `TuiInput` already accepts. */
106
+ columns?: number;
107
+ }
108
+ /**
109
+ * The top-level Ink app (Phase 3): a `<Static>` scrollback of every committed line, a transient
110
+ * region for the in-progress progress line/streamed reply, a one-row separator, the Phase 2
111
+ * multiline input box pinned to the bottom, and a reserved status line under that. Composition
112
+ * only — command parsing/dispatch stays entirely inside `cli.ts`'s `dispatchLine()` (Decision 1),
113
+ * and this component never re-implements it.
114
+ */
115
+ export declare function TuiApp(props: TuiAppProps): React.JSX.Element;
116
+ export type RunTuiAppOptions = Omit<RunCliOptions, 'askYesNo' | 'askLine'>;
117
+ /**
118
+ * Constructs `runCli()` wired to this file's Ink shell (Phase 4 calls this once, behind the
119
+ * `tuiMode` rollout flag) and renders it. Resolves once the user exits (Ctrl+C — see `TuiApp`'s
120
+ * `onExit`), after the same graceful-exit steps `cli.ts`'s own `rl.on('SIGINT', ...)` handler
121
+ * performs: restore the captured `console`/`process.stdout`, close the underlying `CliInstance`,
122
+ * then exit the process.
123
+ */
124
+ export declare function runTuiApp(options?: RunTuiAppOptions): Promise<void>;
125
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One word-wrapped visual row of the draft. `wraps: true` means the next row is a continuation
3
+ * of the same logical (`\n`-delimited) line — no real newline between `end` and the next row's
4
+ * `start` — which is what lets {@link offsetToRowCol} tell a wrap boundary (cursor snaps to the
5
+ * start of the next row) apart from a real end-of-line (cursor stays put at this row's end).
6
+ */
7
+ export interface DraftRow {
8
+ text: string;
9
+ start: number;
10
+ end: number;
11
+ wraps: boolean;
12
+ }
13
+ /**
14
+ * Splits `value` into visual rows at `\n` boundaries and, within each logical line, at every
15
+ * `columns`-wide chunk — plain character wrapping (not word-boundary wrapping): a real terminal
16
+ * hard-wraps by character when a row overflows, so this matches what the terminal will actually
17
+ * show rather than reflowing at word boundaries the way a GUI text area would.
18
+ */
19
+ export declare function wrapDraftIntoRows(value: string, columns: number): DraftRow[];
20
+ /** Maps a character offset into `value` to its (row, col) among `rows` — see {@link DraftRow.wraps} for the wrap-vs-newline distinction this leans on. */
21
+ export declare function offsetToRowCol(rows: DraftRow[], cursor: number): {
22
+ row: number;
23
+ col: number;
24
+ };
25
+ /** Inverse of {@link offsetToRowCol}, clamping both row and column into range — used to preserve column when moving the cursor a visual row up/down. */
26
+ export declare function rowColToOffset(rows: DraftRow[], row: number, col: number): number;
27
+ /** Home = start of the current logical (`\n`-delimited) line, not the current visual row. */
28
+ export declare function homeOffset(value: string, cursor: number): number;
29
+ /** End = end of the current logical (`\n`-delimited) line, not the current visual row. */
30
+ export declare function endOffset(value: string, cursor: number): number;
31
+ /**
32
+ * Keeps the cursor's row inside a `maxRows`-tall window, scrolling the minimum amount needed —
33
+ * matches an ordinary scrolling textarea rather than re-centering on every keystroke.
34
+ */
35
+ export declare function computeViewportStart(prevStart: number, cursorRow: number, totalRows: number, maxRows: number): number;
36
+ export interface TuiInputProps {
37
+ /** Question text for a pending askYesNo/askLine prompt (Decision 1's override seam) — undefined means ordinary chat mode. Shown as a label above the box, since it's real information (the question being asked), unlike chat mode's old static "you" label, which was dropped in favor of a placeholder inside the box (see `placeholder` below). */
38
+ promptLabel?: string;
39
+ /** Faint placeholder shown inside the (empty) box in chat mode, replacing the old "you" label above it. */
40
+ placeholder?: string;
41
+ /** Called when Enter submits a non-empty draft in chat mode. Also appended to in-session history. */
42
+ onSubmitChat: (line: string) => void;
43
+ /** Called when Enter resolves a pending prompt (promptLabel set). Not added to chat history. */
44
+ onSubmitPrompt: (line: string) => void;
45
+ /** Terminal width; defaults to the live width from Ink's useWindowSize. Pass explicitly for a deterministic wrap width in tests. */
46
+ columns?: number;
47
+ /** Max input box height in rows before internal scrolling kicks in. */
48
+ maxRows?: number;
49
+ isActive?: boolean;
50
+ }
51
+ export declare function TuiInput(props: TuiInputProps): React.JSX.Element;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure logic backing PersonalAssistant's ASSISTANT_TUI handling — split out so it's
3
+ * unit-testable in isolation, mirroring one-loop-flag.ts/ask-mode-flag.ts/plan-mode-flag.ts's
4
+ * exact shape (module-level default, an injectable resolver so tests never touch real
5
+ * process.env).
6
+ *
7
+ * Phase 4 of plans/personal_assistant_cli_pinned_input_plan.html — the rollout flag gating
8
+ * whether `cli.ts`'s `main()` launches the Phase 3 Ink shell (`runTuiApp()`) instead of
9
+ * today's plain `readline`-based `runCli()` loop. 'disabled' (the default, for this plan's
10
+ * whole rollout window, per `DEFAULT_TUI_MODE`) means `main()` behaves exactly as it does
11
+ * today, byte-for-byte — the Ink shell is never constructed. 'enabled' only takes effect when
12
+ * `main()` also confirms a real interactive TTY on both stdio streams (see cli.ts); it never
13
+ * affects `runCli()` when called directly with test-only options, since that call site is
14
+ * `cli.test.ts`/`non-interactive-mode.ts`'s own, not `main()`'s.
15
+ */
16
+ export type TuiMode = 'enabled' | 'disabled';
17
+ export declare const DEFAULT_TUI_MODE: TuiMode;
18
+ /**
19
+ * Value-level resolver: `resolveTuiMode` (CLI, reads `process.env.ASSISTANT_TUI`). Unlike
20
+ * `oneLoopMode`/`askMode`/`planMode`, this flag has no browser-facing analog (chat-ui/desktop
21
+ * have no terminal to pin input to), so there's no `envOverridesFromImportMetaEnv` counterpart.
22
+ * An unset or empty value falls back to the default silently; an unrecognized non-empty value
23
+ * falls back with a startup warning naming `varName` — same typo-tolerance-with-a-warning
24
+ * convention `normalizeOneLoopMode`/`normalizeAskMode`/`normalizePlanMode` use.
25
+ */
26
+ export declare function normalizeTuiMode(raw: string | undefined, varName?: string): TuiMode;
27
+ export declare function resolveTuiMode(env: NodeJS.ProcessEnv): TuiMode;
28
+ /**
29
+ * The full gate `cli.ts`'s `main()` applies before ever constructing the Ink shell — split out
30
+ * as a pure function so the decision is unit-testable without a real pty. `tuiMode === 'enabled'`
31
+ * alone isn't sufficient: a piped/scripted invocation (`printf 'msg\nexit\n' | node dist/cli.js`,
32
+ * this repo's own documented headless workflow, or a real CI/non-interactive shell) must always
33
+ * fall through to the plain `readline` loop regardless of the flag, since Ink's raw-mode input
34
+ * has nothing to attach to without a real TTY on both streams.
35
+ */
36
+ export declare function shouldLaunchTuiApp(tuiMode: TuiMode, stdoutIsTty: boolean, stdinIsTty: boolean): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One classified write, mirroring the three writer patterns `cli.ts` already uses (see that
3
+ * file's `writeProgress`/`clearProgress`, `writeToolStep`, `writeToken`) — not a new convention,
4
+ * just naming what's already there so Phase 3's Ink shell can route each kind to the right
5
+ * region (transient status line, transient streamed-reply buffer, permanent scrollback).
6
+ */
7
+ export type CaptureEvent = {
8
+ /** `writeProgress`'s `\r`-overwritten step counter, or `clearProgress`'s blank-out of it. */
9
+ type: 'progress';
10
+ /** The current progress text with the `\r` control character and `padEnd` filler removed — `''` once cleared. */
11
+ text: string;
12
+ } | {
13
+ /** `writeToken`'s raw incremental streamed characters — no trailing newline until the turn ends. */
14
+ type: 'token';
15
+ text: string;
16
+ } | {
17
+ /** A `console.log`/`console.error` call, or any write that completes with a trailing newline — one or more committed, permanently-scrolled lines. */
18
+ type: 'line';
19
+ lines: string[];
20
+ /** Which global function produced this — `'stderr'` only for `console.error`, everything else (`console.log`, raw `process.stdout.write`) is `'stdout'`. Lets a consumer (Phase 3's `EventLogBridge`) style error output distinctly without guessing from content. */
21
+ stream: 'stdout' | 'stderr';
22
+ };
23
+ /**
24
+ * Monkey-patches `console.log`, `console.error`, and `process.stdout.write` for the duration of
25
+ * the interactive Ink session, classifying every write via {@link classify} and routing it to
26
+ * `onEvent` instead of the real terminal — Ink owns the frame once this is active (Decision 1:
27
+ * every command handler still calls the same global functions unmodified). Returns a restore
28
+ * function that puts the originals back; call it on exit and in every test's `afterEach`.
29
+ */
30
+ export declare function startCapture(onEvent: (event: CaptureEvent) => void): () => void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { ILLMClient, TokenUsage } from '@buildaharness/runtime';
2
+ import { DecomposedTaskSpec } from './decomposition-classifier.js';
3
+ /**
4
+ * 'UNKNOWN' is never produced by a successful classification (TURN_INTENT_SCHEMA's riskLevel enum
5
+ * only ever allows LOW/MEDIUM/HIGH from the model) — it exists solely as failSafeClassification's
6
+ * fail-safe value, so callers can route a classifier failure to their most conservative branch
7
+ * instead of silently treating it as LOW risk. See failSafeClassification's doc comment.
8
+ */
9
+ export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'UNKNOWN';
10
+ /** Only meaningful for `source: 'model_inferred'` facts — see fact-extraction.ts's `UserFact`. */
11
+ export type FactConfidence = 'high' | 'medium' | 'low';
12
+ /**
13
+ * Fixed enum, not free text, so the pending-confirmation queue (Phase 3 of
14
+ * plans/personal_assistant_fact_extraction_llm_confidence_plan.html) can group entries stably
15
+ * instead of drifting between synonyms ("job"/"occupation"/"work") for the same underlying topic.
16
+ */
17
+ export type FactCategory = 'identity' | 'health' | 'preference' | 'location' | 'occupation' | 'relationships' | 'project' | 'other';
18
+ export interface StatedFact {
19
+ text: string;
20
+ durable: boolean;
21
+ confidence: FactConfidence;
22
+ category: FactCategory;
23
+ }
24
+ export interface TurnIntentContext {
25
+ /** Whether an active durable plan exists for this session — gates whether the abandon
26
+ * judgment means anything and whether plan-template matching should even be attempted
27
+ * (mirrors assistant.ts's own `if (activePlan) { ... } else { match a template } ` split). */
28
+ hasActivePlan: boolean;
29
+ }
30
+ export interface TurnIntentClassification {
31
+ riskLevel: RiskLevel;
32
+ riskReason: string;
33
+ /** Computed here from riskLevel/isBulkReminderRequest for trace/display purposes and any
34
+ * caller that only wants the raw classifier verdict. Phase D3: no gating call site reads this
35
+ * field directly anymore — turn-interpreter.ts's approval gate and assistant.ts's
36
+ * execution-mode classification both recompute the decision from riskLevel/isBulkReminderRequest
37
+ * via turn-policy.ts's evaluateTurnPolicy() instead, so a future bug here can't silently change
38
+ * what those care about (INV-14). */
39
+ requiresApproval: boolean;
40
+ /** Only meaningful when riskLevel === 'LOW' — same precondition classifyTriviality had. */
41
+ isTrivial: boolean;
42
+ decomposedTasks: DecomposedTaskSpec[] | null;
43
+ /** True when the request asks to create a calendar/reminder entry (regardless of how many). */
44
+ isReminderRequest: boolean;
45
+ /** True when isReminderRequest is true AND the request looks like it may create more than one
46
+ * reminder in a single turn — same signal risk-classifier.ts's BULK_REMINDER_REASON gated on,
47
+ * folded into requiresApproval the same way. Always false when isReminderRequest is false. */
48
+ isBulkReminderRequest: boolean;
49
+ /** Only meaningful when context.hasActivePlan is true. */
50
+ isAbandonRequest: boolean;
51
+ /** One of listTemplateNames()'s names, or null. Only ever set when context.hasActivePlan is false. */
52
+ matchedPlanTemplate: string | null;
53
+ /**
54
+ * P3 of plans/ask_question_and_plan_mode_plan.html — the generalized, domain-general
55
+ * counterpart to matchedPlanTemplate: true when the request warrants a durable, tracked,
56
+ * user-approved plan even though it doesn't match one of the 7 named template kinds (e.g. a
57
+ * code-implementation request spanning several files/tests). Always false when
58
+ * matchedPlanTemplate is already set (a template match is itself one flavor of "needs a
59
+ * plan" — callers should treat `matchedPlanTemplate !== null || needsMultiStepPlan` as the
60
+ * combined trigger) and always false when context.hasActivePlan is true, same gating
61
+ * matchedPlanTemplate already uses.
62
+ */
63
+ needsMultiStepPlan: boolean;
64
+ /**
65
+ * Every durable/session fact the message states about the user (name, preference,
66
+ * health/dietary, current location/job, ...) — the LLM-backed primary extraction path this
67
+ * plan builds (plans/personal_assistant_fact_extraction_llm_confidence_plan.html Phase 1),
68
+ * superseding the old single-fact `statesDurableFact` fallback so a turn stating more than one
69
+ * fact ("I'm Priya, I'm vegetarian, and I live in Austin") isn't truncated to one. `confidence`
70
+ * is anchored to an observable criterion, not a bare self-report: `high` = stated directly and
71
+ * unhedged in first person; `medium` = stated about the user but hedged, indirect, or inferred
72
+ * from context; `low` = a weak inference, or primarily about a third party and only tangentially
73
+ * about the user. `durable`'s contract is unchanged from the old field: true only for
74
+ * identity/safety-relevant facts meant to persist indefinitely, false for something expected to
75
+ * change. Empty array (not null) when nothing was stated, including on classifier failure — see
76
+ * failSafeClassification.
77
+ */
78
+ statesDurableFacts: StatedFact[];
79
+ }
80
+ /**
81
+ * Runs the single consolidated LLM call every turn (replacing the old lexical-gate-then-maybe-
82
+ * LLM-call chain) and derives all seven downstream judgments from one structured response. Falls
83
+ * back to failSafeClassification's conservative defaults on any parse failure or LLM error:
84
+ * UNKNOWN risk requiring approval / not trivial / no decomposition / no abandon / no template
85
+ * match / no stated fact — i.e. "do the careful thing" (run the full harness, require approval
86
+ * rather than guessing LOW, don't auto-abandon, return no stated facts when the call failed).
87
+ * Per-task riskLevel (Phase 1 of plans/lexical_functions_hardening_plan.html) is this call's
88
+ * LLM-backed backstop for what used to be a pure-lexical, no-fallback judgment —
89
+ * risk-classifier.ts's standalone per-task classifyRisk — which stays as the free, zero-latency
90
+ * first check; this call is only trusted when it finds nothing (see assistant.ts's call site).
91
+ * statesDurableFacts (Phase 1 of
92
+ * plans/personal_assistant_fact_extraction_llm_confidence_plan.html) is, as of that plan, the
93
+ * *primary* fact-extraction path rather than a fallback — see that plan for how memory-service.ts
94
+ * merges it with fact-extraction.ts's regex backstop.
95
+ */
96
+ export declare function classifyTurnIntent(message: string, llmClient: ILLMClient, context: TurnIntentContext, model?: string, onUsage?: (usage: TokenUsage) => void): Promise<TurnIntentClassification>;
@@ -0,0 +1,71 @@
1
+ import { Task } from '@buildaharness/harness';
2
+ import { ILLMClient, TokenUsage, ReminderStore } from '@buildaharness/runtime';
3
+ import { TurnIntentClassification } from './turn-intent-classifier.js';
4
+ import { PlanRecord } from './plan-store.js';
5
+ import { PlanService } from './plan-service.js';
6
+ import { AssistantTurnResult } from './assistant-types.js';
7
+ /**
8
+ * The front portion of runTurn — plan-cancel-bypass detection, the classifyTurnIntent call, the
9
+ * message-level risk-gate decision, and (via `resolveTasks`) decomposition/plan-template
10
+ * resolution — as a pure decision-maker. Deliberately no MemoryAdapter/onTrace dependency beyond
11
+ * what's passed as plain arguments/collaborators (PlanService, ReminderStore), so this stays
12
+ * unit-testable without heavy mocks. Does NOT append to the transcript or call onTrace itself —
13
+ * every variant below carries enough data for the sequencer (assistant.ts) to do both, in the
14
+ * exact order the pre-split code did.
15
+ */
16
+ export type TurnInterpretation = {
17
+ kind: 'bypass';
18
+ result: AssistantTurnResult;
19
+ transcriptAppend: {
20
+ user: string;
21
+ assistant: string;
22
+ };
23
+ planUpdatedTrace: {
24
+ templateName: string | null;
25
+ completionPct: number;
26
+ };
27
+ } | {
28
+ kind: 'needs_approval';
29
+ classification: TurnIntentClassification;
30
+ result: AssistantTurnResult;
31
+ } | {
32
+ kind: 'proceed';
33
+ classification: TurnIntentClassification;
34
+ planForCancelCheck: PlanRecord | null;
35
+ };
36
+ export interface ResolvedTasks {
37
+ initialTasks: Task[];
38
+ activePlan: PlanRecord | null;
39
+ /** Only set when a NEW plan match was attempted this turn (i.e. no active plan was already being resumed) — mirrors the original code's `else` branch, which is the only place this trace fired. */
40
+ planClassifiedTrace?: {
41
+ isCandidate: boolean;
42
+ matchedTemplate: string | null;
43
+ };
44
+ }
45
+ export declare class TurnInterpreter {
46
+ private readonly llmClient;
47
+ private readonly model;
48
+ private readonly planService;
49
+ private readonly reminderStore;
50
+ constructor(llmClient: ILLMClient, model: () => string | undefined, planService: PlanService, reminderStore: ReminderStore);
51
+ interpretIntent(params: {
52
+ userMessage: string;
53
+ sessionId: string;
54
+ toolLoopWillRun: boolean;
55
+ approved: boolean;
56
+ dangerouslySkipPermissions: boolean;
57
+ onUsage: (usage: TokenUsage) => void;
58
+ }): Promise<TurnInterpretation>;
59
+ /**
60
+ * Decomposition/plan-template resolution — only meaningful (and only ever called by the
61
+ * sequencer) once a turn is known NOT to be trivial, since a trivial turn returns before any
62
+ * of this would matter and must not spend buildPlanFromTemplate's LLM call for nothing.
63
+ */
64
+ resolveTasks(params: {
65
+ userMessage: string;
66
+ sessionId: string;
67
+ classification: TurnIntentClassification;
68
+ planForCancelCheck: PlanRecord | null;
69
+ onUsage: (usage: TokenUsage) => void;
70
+ }): Promise<ResolvedTasks>;
71
+ }
@@ -0,0 +1,45 @@
1
+ import { RiskLevel } from './turn-intent-classifier.js';
2
+ /**
3
+ * Phase D3 (plans/harness_consolidation_and_control_plane_plan.html, A3): the deterministic,
4
+ * authoritative decision point for whether a turn's message-level risk requires human approval
5
+ * before the turn proceeds — the message-level analog of tool-policy.ts's evaluateToolPolicy().
6
+ *
7
+ * turn-intent-classifier.ts's own `requiresApproval` field is computed from the same
8
+ * riskLevel/isBulkReminderRequest signals and still exists for trace/display purposes, but no
9
+ * gating call site should read that boolean directly — TurnPolicyInput deliberately has no
10
+ * `requiresApproval` field to read, so a gate built on this module always recomputes the decision
11
+ * from the underlying signals rather than trusting a pass-through boolean (INV-14: a consequential
12
+ * decision cannot be set by LLM output alone). See turn-interpreter.ts and assistant.ts's
13
+ * classifyAndTraceExecutionMode call sites for where this replaces a direct
14
+ * `classification.requiresApproval` read.
15
+ */
16
+ export type TurnPolicyDecision = 'ALLOW' | 'REQUIRE_APPROVAL';
17
+ export interface TurnPolicyInput {
18
+ /** turn-intent-classifier.ts's RiskLevel — a signal, never itself the gate (same contract
19
+ * tool-policy.ts's ToolPolicyInput.riskHint documents). */
20
+ riskHint: RiskLevel;
21
+ /** turn-intent-classifier.ts's own isBulkReminderRequest verdict — only meaningful alongside a
22
+ * MEDIUM riskHint (see turn-intent-classifier.ts), but read here unconditionally since a
23
+ * MEDIUM-only precondition on this field is itself already enforced upstream. */
24
+ isBulkReminderRequest: boolean;
25
+ }
26
+ export interface TurnPolicyResult {
27
+ decision: TurnPolicyDecision;
28
+ reason: string;
29
+ }
30
+ export declare function evaluateTurnPolicy(input: TurnPolicyInput): TurnPolicyResult;
31
+ export interface AbandonPolicyInput {
32
+ /** Structural fact — an active durable plan record actually exists for this session,
33
+ * independent of anything the LLM said this turn. */
34
+ hasActivePlan: boolean;
35
+ /** turn-intent-classifier.ts's isAbandonRequest verdict — a signal, never itself sufficient:
36
+ * an LLM claiming "abandon the plan" means nothing when no plan is actually active. */
37
+ abandonHint: boolean;
38
+ }
39
+ /**
40
+ * The deterministic gate for plan abandonment: `abandonHint` alone is never enough — it only
41
+ * takes effect when `hasActivePlan` (independent of the LLM call) is also true. Centralizes the
42
+ * same check turn-intent-classifier.ts and turn-interpreter.ts each already performed inline, so
43
+ * it's asserted once, not maintained in two places that could drift apart.
44
+ */
45
+ export declare function evaluateAbandonPolicy(input: AbandonPolicyInput): boolean;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Runtime-agnostic SSRF guard + safe-fetch core used by web-tools.ts (desktop/CLI path). Split
3
+ * out of web-tools.ts so the guard/redirect/byte-cap logic is one focused module instead of
4
+ * living inline in the tool executor.
5
+ *
6
+ * packages/proxy/src/web-fetch.ts (POST /web/fetch, W2) intentionally carries its own copy of
7
+ * this guard rather than importing this module as a workspace dependency — same call W1's
8
+ * web-search.ts already made for the search parsing code: this package's only `exports` entry is
9
+ * the whole `dist/index.js` bundle (nodemailer, the MCP SDK, the CLI), and this repo's proxy CI
10
+ * job (and this plan's own suggested gate script) typechecks/tests packages/proxy without a
11
+ * `build:personal-assistant` step first, so a real cross-package import isn't actually wired up
12
+ * to build here. Keep the two guards in sync by hand if one changes — same discipline the ported
13
+ * DDG/Brave search code in web-search.ts already requires.
14
+ */
15
+ /**
16
+ * Thrown by assertPublicHttpUrl instead of returning a falsy value, so callers
17
+ * can't accidentally proceed past a rejected URL.
18
+ */
19
+ export declare class PrivateNetworkTargetError extends Error {
20
+ readonly requestedUrl: string;
21
+ readonly detail: string;
22
+ constructor(requestedUrl: string, detail: string);
23
+ }
24
+ /** Thrown when the fetched body's content-type (header and/or sniffed bytes) isn't on the allowlist. */
25
+ export declare class UnsupportedContentTypeError extends Error {
26
+ readonly requestedUrl: string;
27
+ readonly detail: string;
28
+ constructor(requestedUrl: string, detail: string);
29
+ }
30
+ /** Resolves a hostname to its IP addresses. Injected so assertPublicHttpUrl stays unit-testable without real DNS/network access. */
31
+ export type DnsResolver = (hostname: string) => Promise<string[]>;
32
+ /**
33
+ * Parses `url`, rejects non-http(s) schemes / credentialed URLs / non-80/443 ports / raw IP
34
+ * literals outright (all before any DNS call), then resolves the hostname and throws
35
+ * PrivateNetworkTargetError if any resolved address is loopback, RFC1918 private, link-local,
36
+ * or a well-known cloud metadata address. Must be called again on every redirect hop — a public
37
+ * URL can 302 to a private one — which is exactly what fetchTextSafely's redirect loop below does.
38
+ */
39
+ export declare function assertPublicHttpUrl(url: string, dns?: DnsResolver): Promise<void>;
40
+ export declare const MAX_REDIRECTS = 5;
41
+ export declare const MAX_FETCH_CHARS = 15000;
42
+ export interface FetchTextSafelyOptions {
43
+ url: string;
44
+ dns?: DnsResolver;
45
+ fetchImpl?: typeof fetch;
46
+ maxBytes?: number;
47
+ maxRedirects?: number;
48
+ allowedContentTypes?: string[];
49
+ timeoutMs?: number;
50
+ }
51
+ export interface FetchTextSafelyResult {
52
+ text: string;
53
+ finalUrl: string;
54
+ /** True if either the streamed byte cap or the char-level cap truncated the returned text. */
55
+ truncated: boolean;
56
+ }
57
+ /**
58
+ * Fetches `options.url`, following redirects manually (not via fetch's automatic redirect-follow)
59
+ * so every hop gets its own assertPublicHttpUrl check — a public URL that 302s to a private
60
+ * target is rejected mid-fetch, not silently followed. Enforces a streamed byte cap (a
61
+ * Content-Length header can lie), a content-type allowlist (checked against both the header and
62
+ * the sniffed body bytes), and a connect+read timeout. Sends a fixed User-Agent and nothing else
63
+ * client-supplied — zero ambient authority, safe to run on a caller's behalf server-side.
64
+ */
65
+ export declare function fetchTextSafely(options: FetchTextSafelyOptions): Promise<FetchTextSafelyResult>;
@@ -0,0 +1,12 @@
1
+ import { WebSearchResult } from './web-tools.js';
2
+ export interface BraveSearchOptions {
3
+ maxResults?: number;
4
+ fetchImpl?: typeof fetch;
5
+ }
6
+ /**
7
+ * Queries the Brave Search API and returns a bounded, parsed result list. Matches
8
+ * WebToolsContext['search']'s signature. Requires a Brave Search API key — see
9
+ * https://api.search.brave.com/app/keys — passed as `apiKey`, sent as the
10
+ * `X-Subscription-Token` header Brave's API expects.
11
+ */
12
+ export declare function braveSearch(query: string, apiKey: string, options?: BraveSearchOptions): Promise<WebSearchResult[]>;
@@ -0,0 +1,26 @@
1
+ import { ToolDefinition } from '@buildaharness/runtime';
2
+ import { DnsResolver } from './web-fetch-core.js';
3
+ export interface WebSearchResult {
4
+ title: string;
5
+ url: string;
6
+ snippet: string;
7
+ }
8
+ export { PrivateNetworkTargetError, assertPublicHttpUrl } from './web-fetch-core.js';
9
+ export type { DnsResolver } from './web-fetch-core.js';
10
+ export interface WebToolsContext {
11
+ /** No default implementation — the caller supplies a real search backend (an API client, etc.), same way FileToolsContext's `backend` is injected rather than defaulted to real disk. See `braveSearch` in web-search-provider.ts for a ready-made one. */
12
+ search(query: string): Promise<WebSearchResult[]>;
13
+ /** Overrides the HTTP client fetch_url uses — defaults to the global `fetch`. Lets tests and non-browser runtimes inject their own. */
14
+ fetchImpl?: typeof fetch;
15
+ /** Injected DNS resolver for the SSRF guard below — defaults to a lazily-imported node:dns/promises. Tests inject a fake to avoid real network access. */
16
+ dns?: DnsResolver;
17
+ }
18
+ export declare const WEB_SEARCH_TOOL: ToolDefinition;
19
+ export declare const FETCH_URL_TOOL: ToolDefinition;
20
+ export declare const WEB_TOOLS: ToolDefinition[];
21
+ export type WebToolResult = {
22
+ kind: 'text';
23
+ text: string;
24
+ };
25
+ /** Executes web_search/fetch_url. Both return raw, untagged text — trust-tagging is applied by the caller (assistant.ts), not here, so this stays a plain I/O layer like executeFileTool. */
26
+ export declare function executeWebTool(ctx: WebToolsContext, toolName: string, input: Record<string, unknown>): Promise<WebToolResult>;
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@buildaharness/aielia",
3
+ "version": "0.3.0",
4
+ "description": "An open-source AI chat assistant that runs the full 11-layer Build A Harness every turn — light for a fact lookup, and it stops for approval before it sends an email or runs a shell command. CLI, browser, and desktop.",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://buildaharness.com/personal-assistant",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/3IVIS/buildaharness.git",
10
+ "directory": "packages/aielia"
11
+ },
12
+ "keywords": [
13
+ "aielia",
14
+ "ai-assistant",
15
+ "personal-assistant",
16
+ "chat-agent",
17
+ "agent-harness",
18
+ "human-in-the-loop",
19
+ "tool-approval",
20
+ "self-hosted",
21
+ "llm",
22
+ "openclaw-alternative",
23
+ "open-source"
24
+ ],
25
+ "type": "module",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/index.js",
32
+ "types": "./dist/index.d.ts"
33
+ }
34
+ },
35
+ "bin": {
36
+ "aielia": "./dist/cli.js"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "vite build --config vite.config.lib.ts && cp src/file-tools-mcp-server.mjs dist/file-tools-mcp-server.mjs && mkdir -p dist/lexical/patterns && cp src/lexical/patterns/*.json dist/lexical/patterns/ && node -e \"const f='dist/cli.js',s=require('fs'),c=s.readFileSync(f,'utf8');if(!c.startsWith('#!'))s.writeFileSync(f,'#!/usr/bin/env node\\n'+c)\"",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit",
45
+ "cli": "tsx src/cli.ts",
46
+ "eval:turn-intent": "tsx scripts/eval-turn-intent.ts",
47
+ "eval:contradiction-checker": "tsx scripts/eval-contradiction-checker.ts",
48
+ "eval:harness-benchmark": "tsx scripts/run-harness-benchmark.ts"
49
+ },
50
+ "dependencies": {
51
+ "@buildaharness/harness": "^0.2.0",
52
+ "@buildaharness/runtime": "^0.2.0",
53
+ "@modelcontextprotocol/sdk": "^1.29.0",
54
+ "ink": "^7.1.1",
55
+ "nodemailer": "^6.9.14",
56
+ "react": "^19.3.0",
57
+ "zod": "^3.25.76"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.14.0",
61
+ "@types/nodemailer": "^6.4.15",
62
+ "@types/react": "^19.3.0",
63
+ "tsx": "^4.16.0",
64
+ "typescript": "^5.4.5",
65
+ "vite": "^5.3.4",
66
+ "vite-plugin-dts": "^4.5.4",
67
+ "vitest": "^1.6.0"
68
+ }
69
+ }