@buildaharness/aielia 0.3.2 → 0.3.4

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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-loop.d.ts +9 -1
  3. package/dist/assistant-session.d.ts +4 -4
  4. package/dist/assistant-types.d.ts +8 -0
  5. package/dist/assistant.d.ts +39 -0
  6. package/dist/{bin-CqkqT6KK.js → bin-CdVTAe7j.js} +136 -34
  7. package/dist/bin-CdVTAe7j.js.map +1 -0
  8. package/dist/cli-session.d.ts +12 -0
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.js +1 -1
  11. package/dist/config.d.ts +22 -0
  12. package/dist/first-run.d.ts +3 -0
  13. package/dist/goal-graph-flag.d.ts +44 -0
  14. package/dist/goal-graph-reconcile.d.ts +55 -0
  15. package/dist/goal-graph-service.d.ts +48 -0
  16. package/dist/goal-graph-store.d.ts +485 -0
  17. package/dist/goal-graph-suggest-flag.d.ts +34 -0
  18. package/dist/goal-identity-matcher.d.ts +21 -0
  19. package/dist/goal-thread-identity.d.ts +32 -0
  20. package/dist/goal-thread-scheduler.d.ts +78 -0
  21. package/dist/harness-bridge.d.ts +17 -0
  22. package/dist/index.d.ts +11 -1
  23. package/dist/index.js +86 -71
  24. package/dist/live-steering-channel.d.ts +31 -0
  25. package/dist/memory-service.d.ts +10 -0
  26. package/dist/next-step-proposer.d.ts +52 -0
  27. package/dist/plan-approval-service.d.ts +1 -1
  28. package/dist/plan-drafting-service.d.ts +1 -1
  29. package/dist/{cli-session-BiX2DOni.js → provider-setup-CPbB5y_V.js} +4820 -304
  30. package/dist/provider-setup-CPbB5y_V.js.map +1 -0
  31. package/dist/provider-setup.d.ts +51 -0
  32. package/dist/response-service.d.ts +18 -2
  33. package/dist/review-checker.d.ts +10 -0
  34. package/dist/scope-urgency-classifier.d.ts +29 -0
  35. package/dist/semantic-criterion-coverage.d.ts +10 -0
  36. package/dist/supervisor-flag.d.ts +20 -0
  37. package/dist/{tui-app-4_TrzESz.js → tui-app-DeyZ4Pr9.js} +3 -3
  38. package/dist/{tui-app-4_TrzESz.js.map → tui-app-DeyZ4Pr9.js.map} +1 -1
  39. package/dist/turn-interpreter.d.ts +12 -0
  40. package/dist/version.d.ts +1 -1
  41. package/package.json +2 -2
  42. package/dist/bin-CqkqT6KK.js.map +0 -1
  43. package/dist/cli-session-BiX2DOni.js.map +0 -1
package/README.md CHANGED
@@ -99,7 +99,7 @@ silently starting over.
99
99
 
100
100
  Net effect: one real LLM call per ordinary turn, zero for a blocked one, and
101
101
  every layer of the harness touched on the ones that do run — matching what
102
- [buildaharness.com/harness-comparison](https://buildaharness.com/harness-comparison)
102
+ [myaielia.com/harness-comparison](https://myaielia.com/harness-comparison)
103
103
  calls out as missing from Hermes Agent, Kilo Code, and OpenClaw: none of them
104
104
  ships a formal Control State resolver *and* a Reviewer/output gate together.
105
105
  <!--
@@ -196,6 +196,14 @@ export declare class AgentLoop {
196
196
  onToolStep?: (step: AssistantToolStep) => void;
197
197
  onUsage?: (usage: TokenUsage) => void;
198
198
  riskHint?: TurnIntentClassification['riskLevel'];
199
+ /**
200
+ * Mid-turn steering (goalGraphMode): messages the user sent while this turn runs that the
201
+ * scope×urgency classifier judged apply to the answer being produced right now. Called once
202
+ * per iteration, immediately before the LLM call; anything returned is spliced in as a user
203
+ * turn so the model itself applies it — no lexical matching, unlike the harness's
204
+ * caller-constraint output contract.
205
+ */
206
+ takeSteeringNotes?: () => string[];
199
207
  }): (toolCtx: ToolExecutorContext) => Promise<unknown>;
200
208
  /**
201
209
  * R3 of the internal plan: the wiring-level counterpart to
@@ -208,7 +216,7 @@ export declare class AgentLoop {
208
216
  * back whatever got pushed to it once the harness run using this proposer has finished (mirrors
209
217
  * `ToolLoopResult`'s `sources` field on the flag-OFF path).
210
218
  */
211
- createOneLoopProposer(sessionId: string, transcript: ChatMessage[], userMessage: string, systemPrompt: string, onToken?: (token: string) => void, onToolStep?: (step: AssistantToolStep) => void, onUsage?: (usage: TokenUsage) => void, riskHint?: TurnIntentClassification['riskLevel']): {
219
+ createOneLoopProposer(sessionId: string, transcript: ChatMessage[], userMessage: string, systemPrompt: string, onToken?: (token: string) => void, onToolStep?: (step: AssistantToolStep) => void, onUsage?: (usage: TokenUsage) => void, riskHint?: TurnIntentClassification['riskLevel'], takeSteeringNotes?: () => string[]): {
212
220
  proposer: (toolCtx: ToolExecutorContext) => Promise<unknown>;
213
221
  sources: AssistantSource[];
214
222
  };
@@ -99,8 +99,8 @@ export declare class AssistantSession {
99
99
  * isn't set.
100
100
  */
101
101
  recordSpend(sessionId: string, usage: TokenUsage | undefined): Promise<void>;
102
- /** Read-only — null means plan mode has never been entered (or was already exited/cancelled) for this session. */
103
- getPlanModeState(sessionId: string): Promise<PlanModeState | null>;
102
+ /** 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`). */
103
+ getPlanModeState(sessionId: string, threadId?: string): Promise<PlanModeState | null>;
104
104
  /**
105
105
  * The one place `planMode.active` ever flips true — a fresh `draftId` per entry, even if a
106
106
  * prior drafting session was cancelled without ever reaching approval, so a stale draftId can
@@ -108,9 +108,9 @@ export declare class AssistantSession {
108
108
  * a new state, same as a fresh entry) — callers that only ever call this once per drafting
109
109
  * session (P3's future auto-trigger) never observe that.
110
110
  */
111
- enterPlanMode(sessionId: string): Promise<PlanModeState>;
111
+ enterPlanMode(sessionId: string, threadId?: string): Promise<PlanModeState>;
112
112
  /** Clears `planMode.active` — the only two designed callers are an explicit cancel phrase (PlanDraftingService) and P2's future approval resolution. */
113
- exitPlanMode(sessionId: string): Promise<void>;
113
+ exitPlanMode(sessionId: string, threadId?: string): Promise<void>;
114
114
  /** The session's conversation transcript, oldest first — same array `turn()` reads/appends to. Used by `/export`. */
115
115
  getTranscript(sessionId: string): Promise<ChatMessage[]>;
116
116
  /**
@@ -1,3 +1,4 @@
1
+ import { NextStepSuggestion } from './next-step-proposer.js';
1
2
  import { RiskState, LayerActivityEvent, AskQuestion } from '@buildaharness/harness';
2
3
  import { TokenUsage } from '@buildaharness/runtime';
3
4
  import { AnswerClaim } from './answer-claim.js';
@@ -63,6 +64,13 @@ export interface AssistantTurnResult {
63
64
  */
64
65
  status: 'ok' | 'needs_approval' | 'escalated' | 'needs_clarification' | 'needs_plan_approval';
65
66
  reply: string | null;
67
+ /**
68
+ * Up to three concrete next steps proposed after a full (non-trivial, `ok`) turn, most confident
69
+ * first — for the front end to show under the reply. Advisory only: never executed, never
70
+ * persisted from here. Present only when the caller enabled `goalGraphSuggestMode`
71
+ * (PersonalAssistantOptions) and the proposer returned at least one; absent otherwise.
72
+ */
73
+ nextSteps?: NextStepSuggestion[];
66
74
  reason?: string;
67
75
  /** See ProposerKind. Always set (defaults to 'posthoc'), on every status. */
68
76
  proposerKind?: ProposerKind;
@@ -16,11 +16,15 @@ import { PlanRecord } from './plan-store.js';
16
16
  import { OneLoopMode } from './one-loop-flag.js';
17
17
  import { AskMode } from './ask-mode-flag.js';
18
18
  import { PlanRolloutMode } from './plan-mode-flag.js';
19
+ import { GoalGraphSuggestMode } from './goal-graph-suggest-flag.js';
20
+ import { GoalGraphState } from './goal-graph-service.js';
21
+ import { LiveSteeringChannel } from './live-steering-channel.js';
19
22
  import { DebugLogEntry } from './debug-log.js';
20
23
  import { AssistantTurnResult, AssistantProgress } from './assistant-types.js';
21
24
  export type { MemorySummary, MemoryExport, PendingFact } from './memory-service.js';
22
25
  export type { FactCategory } from './turn-intent-classifier.js';
23
26
  export type { IndexedMessage, TranscriptSearchHit } from './assistant-session.js';
27
+ export type { GoalGraphState, GoalThreadView, GoalThreadVisibility, GoalTaskSummary } from './goal-graph-service.js';
24
28
  export type { BatchBudgetState } from './agent-loop.js';
25
29
  export { trimmedAverage, nextItemBudget } from './agent-loop.js';
26
30
  export type { AssistantSource } from './assistant-source.js';
@@ -105,6 +109,21 @@ export interface TurnOptions {
105
109
  seedFailures: number;
106
110
  onInjected?: () => void;
107
111
  };
112
+ /**
113
+ * Phase 4 of plans/hierarchical_goal_tree_and_steering_plan.html (R1/R4) — the session-scoped
114
+ * LiveSteeringChannel a caller (cli.ts/App.tsx, Phase 3) built for mid-task steering. When
115
+ * present, `turn()` wraps it in a scope×urgency-classifying UpdateChannel
116
+ * (goal-graph-reconcile.ts) and threads it into harnessBridge.run() so queued messages are
117
+ * absorbed at the harness's own checkCallerUpdates iteration boundary instead of waiting for
118
+ * the turn to finish. PersonalAssistant never reads goalGraphMode itself (same convention as
119
+ * oneLoopMode/askMode/planMode) — the caller decides whether to pass this at all; when absent,
120
+ * behavior is exactly today's (INV-43). Whatever this turn's harness run doesn't get around to
121
+ * classifying (it ends before the queue drains, or never calls harnessBridge.run() at all — a
122
+ * trivial turn) is pushed back onto this same channel in a `finally`, so the caller's own
123
+ * post-turn fallback drain (built in Phase 3) still picks it up — R4's "never silently drop"
124
+ * holds either way.
125
+ */
126
+ steeringChannel?: LiveSteeringChannel;
108
127
  }
109
128
  export interface PersonalAssistantOptions {
110
129
  llmClient: ILLMClient;
@@ -207,6 +226,16 @@ export interface PersonalAssistantOptions {
207
226
  * call resolveOneLoopMode(process.env) and pass the result here.
208
227
  */
209
228
  oneLoopMode?: OneLoopMode;
229
+ /**
230
+ * R7 (Tier 1.5) — next-step suggestions, one bounded LLM call each: after every full
231
+ * (non-trivial, `ok`) turn, up to three options are attached to the result as `nextSteps` for
232
+ * the front end to show under the reply; and when a GoalThread finishes its last task, its own
233
+ * suggestions are persisted on the thread for `/goals`. Defaults to 'disabled' — like
234
+ * oneLoopMode/askMode/planMode, the front end resolves the flag (cli.ts / App.tsx pass
235
+ * `config.goalGraphSuggestMode`, which defaults to enabled), so a library or benchmark caller
236
+ * that never asks for suggestions makes no extra call and sees no new field (INV-43).
237
+ */
238
+ goalGraphSuggestMode?: GoalGraphSuggestMode;
210
239
  /**
211
240
  * Q2 of the internal plan — global-flag control point (tier 1 of
212
241
  * INV-29) for the ask-question mechanism. Undefined (the default) falls back to
@@ -269,6 +298,8 @@ export declare class PersonalAssistant {
269
298
  * the internal plan phase B1.
270
299
  */
271
300
  private lastProposerKind;
301
+ /** See PersonalAssistantOptions.goalGraphSuggestMode. */
302
+ private readonly goalGraphSuggestMode;
272
303
  private readonly memoryService;
273
304
  private readonly session;
274
305
  private readonly agentLoop;
@@ -330,6 +361,14 @@ export declare class PersonalAssistant {
330
361
  * all). Returns `null` when no plan record exists for this session yet.
331
362
  */
332
363
  getPlanState(sessionId: string): Promise<PlanRecord | null>;
364
+ /**
365
+ * Phase 7 of plans/hierarchical_goal_tree_and_steering_plan.html (R5, "Visibility") — the shared
366
+ * read surface behind the CLI's `/goals` and chat-ui's GoalsPanel, analogous to `searchTranscript`
367
+ * above. A pure query over whatever Phases 4-5 already populate in this session's
368
+ * `GoalGraphRecord`; never mints or mutates one. Returns the empty state for a session with no
369
+ * goal graph yet, same "nothing to show" convention `getGoalGraphState` itself documents.
370
+ */
371
+ getGoalGraphState(sessionId: string): Promise<GoalGraphState>;
333
372
  /**
334
373
  * P9 of the internal plan — the lightweight plan-sketch delegate: a
335
374
  * cheaper "just go research and draft an approach" path than plan mode's durable task-graph
@@ -8,7 +8,7 @@ import { homedir, tmpdir } from "node:os";
8
8
  import { join, dirname, resolve } from "node:path";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  import { OPENROUTER_DEFAULT_MODEL, OPENAI_DEFAULT_MODEL, ANTHROPIC_DEFAULT_MODEL, InMemoryReminderStore, FileSystemAdapter, FileSystemExperienceStore, LLMClient, OpenAICompatibleLLMClient, OPENROUTER_EXTRA_HEADERS, OPENROUTER_BASE_URL, OPENAI_BASE_URL, AnthropicLLMClient } from "@buildaharness/runtime";
11
- import { x as buildClaudePrompt, aE as stripJsonCodeFence, a as ALREADY_STAGED_ACTION_TOOL, aD as stagedActionInput, ar as parseClaudeCliOutput, aF as stripMcpToolPrefix, V as envOverridesFromProcessEnv, aw as resolveConfig, aB as shouldLaunchTuiApp, aI as validateConfig, c as ConfigValidationError, aa as formatSpendCapStatus, n as PersonalAssistant, w as braveSearch, N as createSmtpSender, M as createResendSender, z as classifyError, an as nodeToLayer, L as LAYER_DISPLAY_NAME, am as nodeDisplayName, X as estimateCostUsd, a0 as formatConfigListing, ae as isConfigKey, C as CONFIG_KEYS, E as ENV_VAR_FOR_CONFIG_KEY, as as parseConfigValue, d as ConfigValueParseError, a2 as formatDoctorReport, a1 as formatCostSummary, a9 as formatSearchResults, ad as formatUndoLogListing, O as defaultExportFilename, ac as formatTranscriptMarkdown, ab as formatStatus, a4 as formatHelp, k as LAYER_ORDER, y as buildWhyChain, l as LAYER_SHORT_CODE, Q as defaultMemoryExportFilename, a5 as formatMemoryExport, a6 as formatMemoryPendingOutcome, a7 as formatMemorySummary } from "./cli-session-BiX2DOni.js";
11
+ import { G as buildClaudePrompt, aT as stripJsonCodeFence, a as ALREADY_STAGED_ACTION_TOOL, aS as stagedActionInput, aE as parseClaudeCliOutput, aU as stripMcpToolPrefix, P as PROVIDER_SETUP, Q as cleanApiKey, J as checkApiKeyFormat, a0 as envOverridesFromProcessEnv, aJ as resolveConfig, aQ as shouldLaunchTuiApp, aW as testApiKey, aY as validateConfig, c as ConfigValidationError, ai as formatSpendCapStatus, r as PersonalAssistant, ap as isGoalGraphSuggestEnabled, ao as isGoalGraphEnabled, B as braveSearch, X as createSmtpSender, V as createResendSender, p as LiveSteeringChannel, af as formatNextSteps, K as classifyError, ay as nodeToLayer, L as LAYER_DISPLAY_NAME, ax as nodeDisplayName, a1 as estimateCostUsd, a6 as formatConfigListing, an as isConfigKey, C as CONFIG_KEYS, E as ENV_VAR_FOR_CONFIG_KEY, aF as parseConfigValue, d as ConfigValueParseError, a8 as formatDoctorReport, a7 as formatCostSummary, aa as formatGoalGraphState, ah as formatSearchResults, al as formatUndoLogListing, Y as defaultExportFilename, ak as formatTranscriptMarkdown, aj as formatStatus, ab as formatHelp, m as LAYER_ORDER, H as buildWhyChain, n as LAYER_SHORT_CODE, Z as defaultMemoryExportFilename, ac as formatMemoryExport, ad as formatMemoryPendingOutcome, ae as formatMemorySummary } from "./provider-setup-CPbB5y_V.js";
12
12
  import { spawn } from "node:child_process";
13
13
  import { createServer, connect } from "node:net";
14
14
  import { createRequire } from "node:module";
@@ -66,7 +66,7 @@ function createNodeFsBackend() {
66
66
  }
67
67
  };
68
68
  }
69
- const CLI_VERSION = "0.3.2";
69
+ const CLI_VERSION = "0.3.4";
70
70
  function seaCacheDir(version, home = homedir()) {
71
71
  return join(home, ".buildaharness", "personal-assistant", "sea-cache", version);
72
72
  }
@@ -650,6 +650,7 @@ function resolveNonInteractiveApprovalMode(env) {
650
650
  console.error(`[warning] ASSISTANT_NON_INTERACTIVE_APPROVAL="${raw}" is not "decline" or "require-tty" — ignoring.`);
651
651
  return void 0;
652
652
  }
653
+ const MAX_KEY_ATTEMPTS = 3;
653
654
  function alreadyConfigured(deps) {
654
655
  const { persisted, overriddenKeys } = deps;
655
656
  if (persisted.llmBackend !== void 0) return true;
@@ -659,50 +660,94 @@ function alreadyConfigured(deps) {
659
660
  }
660
661
  return false;
661
662
  }
662
- const PROVIDER_CHOICES = {
663
- "1": { backend: "anthropic", label: "Anthropic (Claude) — needs an sk-ant-… key" },
664
- "2": { backend: "openai", label: "OpenAI — needs an sk-… key" },
665
- "3": { backend: "openrouter", label: "OpenRouter — needs an sk-or-… key" }
666
- };
667
663
  async function maybeRunFirstRunSetup(deps) {
668
- const { configStore, persisted, isInteractive, ask, detectClaudeCli, log } = deps;
664
+ const { configStore, persisted, isInteractive, ask, detectClaudeCli, log, testKey } = deps;
669
665
  if (alreadyConfigured(deps)) return persisted;
670
666
  if (!isInteractive) return persisted;
671
667
  log("");
672
- log("Welcome to Aielia — this looks like your first run. Let's pick how to reach a model.");
673
- log("(You can change any of this later with /config, or skip with Ctrl+C.)");
668
+ log("Welcome to Aielia! 👋 Let’s get you set up — it takes about a minute.");
669
+ log("");
670
+ log("Aielia needs an AI model to think with. Pick where that comes from below.");
671
+ log("(You can change this any time with /config, or press Ctrl+C to skip.)");
674
672
  log("");
675
673
  const patch = {};
676
674
  if (await detectClaudeCli()) {
677
- const useClaude = await ask(
678
- "Found an authenticated `claude` CLI on your PATH. Use it? No API key needed. (Y/n) "
679
- );
675
+ log("✓ Claude is already running on this computer (Claude Code is installed and signed in).");
676
+ log(" That means you can start right now — no API key, no extra account, nothing to paste.");
677
+ log("");
678
+ const useClaude = await ask("Use your Claude login? (Y/n — Enter means yes) ");
680
679
  if (useClaude === "" || useClaude.toLowerCase().startsWith("y")) {
681
680
  patch.llmBackend = "claude-cli";
682
681
  await configStore.save(patch);
683
682
  log("");
684
- log('✓ Using the claude-cli backend. Try "what time zone is Tokyo in?", then');
683
+ log('✓ All set — using your Claude login. Try "what time zone is Tokyo in?", then');
685
684
  log(' "send an email to my boss saying I quit" to see the approval gate.');
686
685
  log("");
687
686
  return { ...persisted, ...patch };
688
687
  }
688
+ log("");
689
+ } else {
690
+ log("Tip: if you already use Claude Code, install it and sign in (run `claude` once), then");
691
+ log("start Aielia again — you can use it with no API key. Otherwise, pick a provider below.");
692
+ log("");
689
693
  }
690
- log("Pick a provider to use with your own API key:");
691
- for (const [key2, { label }] of Object.entries(PROVIDER_CHOICES)) log(` ${key2}) ${label}`);
692
- const choice = (await ask("Provider [1-3, or Enter to skip]: ")).trim();
693
- const picked = PROVIDER_CHOICES[choice];
694
+ log("Which AI provider do you have (or want to use)?");
695
+ log("");
696
+ PROVIDER_SETUP.forEach((p, i) => {
697
+ log(` ${i + 1}) ${p.name}`);
698
+ log(` ${p.blurb}`);
699
+ });
700
+ log("");
701
+ const choice = (await ask(`Type 1-${PROVIDER_SETUP.length} and press Enter (or just Enter to skip): `)).trim();
702
+ const picked = PROVIDER_SETUP[Number(choice) - 1];
694
703
  if (!picked) {
695
704
  log("");
696
- log('Skipped. The assistant will start on the "proxy" backend (needs @buildaharness/proxy');
697
- log("on :8787). Run /config set llmBackend <anthropic|openai|openrouter> and");
698
- log("/config set apiKey <key> when you are ready, or set ASSISTANT_LLM_BACKEND + ASSISTANT_API_KEY.");
705
+ log('Skipped. Until you set a provider, Aielia will try the "proxy" backend (needs');
706
+ log("@buildaharness/proxy running on :8787). To set one up later, run:");
707
+ log(" /config set llmBackend <anthropic|openai|openrouter>");
708
+ log(" /config set apiKey <your key>");
699
709
  log("");
700
710
  return persisted;
701
711
  }
702
- const key = (await ask(`Paste your ${picked.backend} API key: `)).trim();
712
+ log("");
713
+ log(`To connect ${picked.name}, you need an “API key” — a password-like code that lets Aielia use your account.`);
714
+ log("Here’s how to get one:");
715
+ log("");
716
+ picked.steps.forEach((step, i) => log(` ${i + 1}. ${step}`));
717
+ if (picked.safety) {
718
+ log("");
719
+ log(` ${picked.safety.title}:`);
720
+ picked.safety.steps.forEach((step, i) => log(` ${i + 1}. ${step}`));
721
+ log(` (${picked.safety.urlLabel})`);
722
+ }
723
+ log("");
724
+ log(`The key starts with "${picked.keyPrefix}". Paste it below when you have it.`);
725
+ log("");
726
+ let key = "";
727
+ for (let attempt = 1; attempt <= MAX_KEY_ATTEMPTS; attempt++) {
728
+ const candidate = cleanApiKey(await ask("Paste your API key (or just Enter to skip): "));
729
+ if (candidate === "") break;
730
+ const formatProblem = checkApiKeyFormat(picked.backend, candidate);
731
+ if (formatProblem) {
732
+ log(` ✗ ${formatProblem}`);
733
+ continue;
734
+ }
735
+ if (testKey) {
736
+ log(" Checking your key…");
737
+ const result = await testKey(picked.backend, candidate);
738
+ if (result.status === "invalid") {
739
+ log(` ✗ ${result.message}`);
740
+ continue;
741
+ }
742
+ if (result.status === "unverified") log(` ! ${result.message}`);
743
+ }
744
+ key = candidate;
745
+ break;
746
+ }
703
747
  if (!key) {
704
748
  log("");
705
- log("No key entered — skipping. Set it later with /config set apiKey <key>.");
749
+ log("No working key entered, so nothing was saved. Run Aielia again to retry, or use");
750
+ log("/config set llmBackend " + picked.backend + " then /config set apiKey <key>");
706
751
  log("");
707
752
  return persisted;
708
753
  }
@@ -710,8 +755,9 @@ async function maybeRunFirstRunSetup(deps) {
710
755
  patch.apiKey = key;
711
756
  await configStore.save(patch);
712
757
  log("");
713
- log(`✓ Saved. Using ${picked.backend}. The key is stored in plain text in your config file`);
714
- log(" (~/.buildaharness/personal-assistant/config.json) — the same trust boundary as a .env.");
758
+ log(`✓ You’re all set — using ${picked.name}. Say hello!`);
759
+ log(" Your key is saved on this computer in ~/.buildaharness/personal-assistant/config.json");
760
+ log(" (plain text, like a .env file) — don’t share that file.");
715
761
  log("");
716
762
  return { ...persisted, ...patch };
717
763
  }
@@ -1088,6 +1134,10 @@ async function buildAssistant(config, { backend, dataDir, remindersFile }) {
1088
1134
  // chat-ui, and the Tauri desktop build all decide this the same way. Undefined here means
1089
1135
  // PersonalAssistant falls back to DEFAULT_ONE_LOOP_MODE.
1090
1136
  oneLoopMode: config.oneLoopMode,
1137
+ // R7: turn-end next-step options and DONE-thread suggestions. Undefined config means the
1138
+ // package default (enabled); PersonalAssistant itself defaults to disabled, so this is where
1139
+ // the front end opts in.
1140
+ goalGraphSuggestMode: isGoalGraphSuggestEnabled(config.goalGraphSuggestMode) ? "enabled" : "disabled",
1091
1141
  // P11 of the internal plan — resolved through the same shared config
1092
1142
  // seam as oneLoopMode above (ASSISTANT_PLAN_MODE / `/config set planMode`). Undefined here
1093
1143
  // means PersonalAssistant falls back to DEFAULT_PLAN_MODE ('legacy').
@@ -1095,6 +1145,7 @@ async function buildAssistant(config, { backend, dataDir, remindersFile }) {
1095
1145
  });
1096
1146
  }
1097
1147
  async function runCli(options = {}) {
1148
+ console.log("Aielia is alpha software — expect rough edges and breaking changes. It uses AI models and can make mistakes; verify anything important.\n");
1098
1149
  const dataDir = options.dataDir ?? defaultDataDir;
1099
1150
  const configStore = options.configStore ?? defaultConfigStore;
1100
1151
  const backend = options.backend ?? defaultBackend;
@@ -1123,7 +1174,8 @@ async function runCli(options = {}) {
1123
1174
  ask: firstRunAsk,
1124
1175
  detectClaudeCli: options.detectClaudeCli ?? (async () => (await checkClaudeCli(process.env.CLAUDE_PATH ?? "claude")).ok),
1125
1176
  log: (line) => outputStream.write(`${line}
1126
- `)
1177
+ `),
1178
+ testKey: options.firstRunTestKey ?? ((backend2, key) => testApiKey(backend2, key))
1127
1179
  });
1128
1180
  } finally {
1129
1181
  setupRl == null ? void 0 : setupRl.close();
@@ -1495,6 +1547,12 @@ ${formatMemoryPendingOutcome("forgotten", outcome)}
1495
1547
  const hits = await assistant.searchTranscript(query);
1496
1548
  console.log(`
1497
1549
  ${formatSearchResults(hits, query)}
1550
+ `);
1551
+ }
1552
+ async function handleGoals() {
1553
+ const state = await assistant.getGoalGraphState("cli");
1554
+ console.log(`
1555
+ ${formatGoalGraphState(state)}
1498
1556
  `);
1499
1557
  }
1500
1558
  async function printCost() {
@@ -1733,7 +1791,15 @@ Unrecognized input "${token}".
1733
1791
  if (!streamedAnyTokens) writeProgress(progress);
1734
1792
  },
1735
1793
  onToken: writeToken,
1736
- onToolStep: writeToolStep
1794
+ onToolStep: writeToolStep,
1795
+ // Phase 4 — real intra-turn absorption via checkCallerUpdates, replacing Phase 3's
1796
+ // drain-as-follow-up-turns fallback in dispatchOne's `finally` below (kept there, but
1797
+ // only as a backstop: turn() re-enqueues anything it didn't get around to classifying
1798
+ // this turn — e.g. a trivial turn that never calls harnessBridge.run() at all — back onto
1799
+ // this same channel, see TurnOptions.steeringChannel's doc comment). Passed unconditionally:
1800
+ // with goalGraphMode off, routeMessage (below) never enqueues into steeringChannel, so it's
1801
+ // always empty and this resolves to exactly today's behavior (INV-43).
1802
+ steeringChannel
1737
1803
  });
1738
1804
  if (!streamedAnyTokens) clearProgress();
1739
1805
  if (result.status === "needs_approval" && result.pendingActionId) {
@@ -1803,16 +1869,20 @@ Unrecognized input "${token}".
1803
1869
  const contradictionNotice = result.contradictionNotice ? `
1804
1870
 
1805
1871
  ${result.contradictionNotice}` : "";
1872
+ lastNextSteps = result.nextSteps && result.nextSteps.length > 0 ? result.nextSteps : void 0;
1873
+ const nextStepsBlock = lastNextSteps ? `
1874
+
1875
+ ${formatNextSteps(lastNextSteps)}` : "";
1806
1876
  if (streamedAnyTokens) {
1807
1877
  const pausedNoteText = result.pausedNote ? `
1808
1878
 
1809
1879
  ${result.pausedNote}` : "";
1810
- process.stdout.write(`${pausedNoteText}${riskSuffix}${sourcesHint}${planHint}${contradictionNotice}
1880
+ process.stdout.write(`${pausedNoteText}${riskSuffix}${sourcesHint}${planHint}${contradictionNotice}${nextStepsBlock}
1811
1881
 
1812
1882
  `);
1813
1883
  } else {
1814
1884
  console.log(`
1815
- Aielia>${riskSuffix} ${result.reply}${sourcesHint}${planHint}${contradictionNotice}
1885
+ Aielia>${riskSuffix} ${result.reply}${sourcesHint}${planHint}${contradictionNotice}${nextStepsBlock}
1816
1886
  `);
1817
1887
  }
1818
1888
  } catch (err) {
@@ -1986,6 +2056,9 @@ ${assistant.getActiveProject() || "(none)"}
1986
2056
  }
1987
2057
  await handleConfigCommand(["set", "activeProject", args.join(" ")]);
1988
2058
  }
2059
+ let turnInProgress = false;
2060
+ let lastNextSteps;
2061
+ const steeringChannel = new LiveSteeringChannel();
1989
2062
  const commands = {
1990
2063
  "/why": () => printWhy(),
1991
2064
  "/layers": () => printLayers(),
@@ -2000,6 +2073,7 @@ ${assistant.getActiveProject() || "(none)"}
2000
2073
  "/undo-action": (args) => handleUndoAction(args),
2001
2074
  "/memory": (args) => handleMemory(args),
2002
2075
  "/search": (args) => handleSearch(args),
2076
+ "/goals": () => handleGoals(),
2003
2077
  "/model": (args) => handleModel(args),
2004
2078
  "/project": (args) => handleProject(args),
2005
2079
  "/cost": () => printCost(),
@@ -2008,13 +2082,29 @@ ${assistant.getActiveProject() || "(none)"}
2008
2082
  "/checkpoint": (args) => handleCheckpoint(args)
2009
2083
  };
2010
2084
  async function dispatchOne(message) {
2085
+ const pick = /^[1-3]$/.test(message) && lastNextSteps ? lastNextSteps[Number(message) - 1] : void 0;
2086
+ lastNextSteps = void 0;
2087
+ if (pick) {
2088
+ console.log(` → ${pick.description}`);
2089
+ message = pick.description;
2090
+ }
2011
2091
  const [token, ...args] = message.split(/\s+/);
2012
2092
  const handler = commands[token];
2013
2093
  if (handler) {
2014
2094
  await handler(args);
2015
2095
  return;
2016
2096
  }
2017
- await handleTurn(message);
2097
+ turnInProgress = true;
2098
+ try {
2099
+ await handleTurn(message);
2100
+ } finally {
2101
+ turnInProgress = false;
2102
+ if (isGoalGraphEnabled(config.goalGraphMode)) {
2103
+ for (const event of steeringChannel.poll()) {
2104
+ void enqueue(event.message);
2105
+ }
2106
+ }
2107
+ }
2018
2108
  }
2019
2109
  let dispatchQueue = Promise.resolve();
2020
2110
  function enqueue(message) {
@@ -2024,20 +2114,32 @@ ${assistant.getActiveProject() || "(none)"}
2024
2114
  dispatchQueue = result;
2025
2115
  return result;
2026
2116
  }
2117
+ function isKnownCommand(message) {
2118
+ const [token] = message.split(/\s+/);
2119
+ return token in commands;
2120
+ }
2121
+ function routeMessage(message) {
2122
+ if (isGoalGraphEnabled(config.goalGraphMode) && turnInProgress && !isKnownCommand(message)) {
2123
+ steeringChannel.enqueue(message);
2124
+ console.log("\n[queued — the current turn is still running; this will be taken into account once it finishes]\n");
2125
+ return Promise.resolve();
2126
+ }
2127
+ return enqueue(message);
2128
+ }
2027
2129
  rl.on("line", (line) => {
2028
2130
  const message = line.trim();
2029
2131
  if (!message) {
2030
2132
  rl.prompt();
2031
2133
  return;
2032
2134
  }
2033
- void enqueue(message).finally(() => rl.prompt());
2135
+ void routeMessage(message).finally(() => rl.prompt());
2034
2136
  });
2035
2137
  rl.resume();
2036
2138
  return {
2037
2139
  dispatchLine: async (line) => {
2038
2140
  const message = line.trim();
2039
2141
  if (!message) return;
2040
- await enqueue(message);
2142
+ await routeMessage(message);
2041
2143
  },
2042
2144
  close: () => rl.close(),
2043
2145
  getStatusIndicators: async () => {
@@ -2080,7 +2182,7 @@ async function main(argv = process.argv.slice(2)) {
2080
2182
  });
2081
2183
  }
2082
2184
  if (shouldLaunchTuiApp(config.tuiMode ?? "disabled", Boolean(process.stdout.isTTY), Boolean(process.stdin.isTTY))) {
2083
- const { runTuiApp } = await import("./tui-app-4_TrzESz.js");
2185
+ const { runTuiApp } = await import("./tui-app-DeyZ4Pr9.js");
2084
2186
  await runTuiApp();
2085
2187
  return;
2086
2188
  }
@@ -2104,4 +2206,4 @@ export {
2104
2206
  PLAN_LINE_PREFIX as P,
2105
2207
  runCli as r
2106
2208
  };
2107
- //# sourceMappingURL=bin-CqkqT6KK.js.map
2209
+ //# sourceMappingURL=bin-CdVTAe7j.js.map