@bastani/atomic 0.8.26-alpha.1 → 0.8.26-alpha.2

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 (41) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  5. package/dist/builtin/mcp/package.json +1 -1
  6. package/dist/builtin/subagents/CHANGELOG.md +7 -0
  7. package/dist/builtin/subagents/package.json +1 -1
  8. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +8 -3
  9. package/dist/builtin/subagents/src/runs/foreground/execution.ts +42 -4
  10. package/dist/builtin/subagents/src/runs/shared/acceptance.ts +2 -1
  11. package/dist/builtin/subagents/src/runs/shared/worktree.ts +2 -2
  12. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  13. package/dist/builtin/web-access/package.json +1 -1
  14. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  15. package/dist/builtin/workflows/package.json +1 -1
  16. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +28 -9
  17. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +6 -1
  18. package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -2
  19. package/dist/builtin/workflows/src/shared/store.ts +61 -7
  20. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +37 -2
  21. package/dist/core/footer-data-provider.d.ts.map +1 -1
  22. package/dist/core/footer-data-provider.js +3 -0
  23. package/dist/core/footer-data-provider.js.map +1 -1
  24. package/dist/core/package-manager.d.ts.map +1 -1
  25. package/dist/core/package-manager.js +14 -7
  26. package/dist/core/package-manager.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/footer.js +4 -1
  33. package/dist/modes/interactive/components/footer.js.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.js +3 -2
  36. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  37. package/dist/utils/git-env.d.ts +10 -0
  38. package/dist/utils/git-env.d.ts.map +1 -0
  39. package/dist/utils/git-env.js +33 -0
  40. package/dist/utils/git-env.js.map +1 -0
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.26-alpha.2] - 2026-06-05
6
+
7
+ ### Fixed
8
+
9
+ - Clarified overflow auto-compaction warnings in the TUI footer so automatic transcript compaction is reported distinctly from user-triggered compaction ([#1250](https://github.com/bastani-inc/atomic/issues/1250)).
10
+ - Fixed internal Git subprocesses to strip ambient repository-local Git environment variables before package-manager and footer branch lookups inspect a targeted working tree.
11
+
5
12
  ## [0.8.26-alpha.1] - 2026-06-05
6
13
 
7
14
  ### Fixed
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.26-alpha.2] - 2026-06-05
8
+
9
+ ### Changed
10
+
11
+ - Bumped package version for the Atomic 0.8.26-alpha.2 prerelease.
12
+
7
13
  ## [0.8.26-alpha.1] - 2026-06-05
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.8.26-alpha.1",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.26-alpha.2] - 2026-06-05
11
+
12
+ ### Changed
13
+
14
+ - Bumped package version for the Atomic 0.8.26-alpha.2 prerelease.
15
+
10
16
  ## [0.8.26-alpha.1] - 2026-06-05
11
17
 
12
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.8.26-alpha.1",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.26-alpha.2] - 2026-06-05
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the `no-staged-files` acceptance runtime check and subagent worktree Git commands to ignore ambient Git repository environment variables, so subagents inspect the intended working tree instead of a parent hook or unrelated worktree.
10
+ - Suppressed intermediate model fallback failure notes and live foreground failure updates from successful subagent runs while preserving final failures and raw per-attempt diagnostics ([#1226](https://github.com/bastani-inc/atomic/issues/1226)).
11
+
5
12
  ## [0.8.26-alpha.1] - 2026-06-05
6
13
 
7
14
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.8.26-alpha.1",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -650,6 +650,7 @@ async function runSingleStep(
650
650
  const attemptedModels: string[] = [];
651
651
  const modelAttempts: ModelAttempt[] = [];
652
652
  const attemptNotes: string[] = [];
653
+ const pendingAttemptNotes: string[] = [];
653
654
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
654
655
  let finalResult: RunPiStreamingResult | undefined;
655
656
  let finalFastMode: boolean | undefined;
@@ -768,9 +769,13 @@ async function runSingleStep(
768
769
  finalFastMode = attemptFastMode;
769
770
  finalOutputSnapshot = outputSnapshot;
770
771
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
771
- if (attempt.success || completionGuardTriggered) break;
772
- if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
773
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
772
+ if (attempt.success) break;
773
+ if (!completionGuardTriggered && isRetryableModelFailure(error) && index < candidates.length - 1) {
774
+ pendingAttemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
775
+ continue;
776
+ }
777
+ attemptNotes.push(...pendingAttemptNotes);
778
+ break;
774
779
  }
775
780
 
776
781
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -17,6 +17,7 @@ import {
17
17
  type AgentProgress,
18
18
  type ArtifactPaths,
19
19
  type ControlEvent,
20
+ type Details,
20
21
  type ModelAttempt,
21
22
  type RunSyncOptions,
22
23
  type SingleResult,
@@ -139,6 +140,29 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
139
140
  };
140
141
  }
141
142
 
143
+ type RunSyncUpdate = import("@earendil-works/pi-agent-core").AgentToolResult<Details>;
144
+
145
+ function extractUpdateText(update: RunSyncUpdate): string | undefined {
146
+ const text = update.content
147
+ .map((item) => item.type === "text" ? item.text : undefined)
148
+ .filter((item): item is string => Boolean(item?.trim()))
149
+ .join("\n");
150
+ return text || undefined;
151
+ }
152
+
153
+ export function shouldSuppressIntermediateRetryableFailureUpdate(update: RunSyncUpdate): boolean {
154
+ const result = update.details?.results?.[0];
155
+ if (!result) return false;
156
+ const progress = update.details?.progress?.[0];
157
+ const status = result.progress?.status ?? progress?.status;
158
+ if (status !== "failed") return false;
159
+ const failureText = result.error
160
+ ?? result.progress?.error
161
+ ?? progress?.error
162
+ ?? extractUpdateText(update);
163
+ return isRetryableModelFailure(failureText);
164
+ }
165
+
142
166
  async function runSingleAttempt(
143
167
  runtimeCwd: string,
144
168
  agent: AgentConfig,
@@ -875,6 +899,7 @@ export async function runSync(
875
899
  const modelAttempts: ModelAttempt[] = [];
876
900
  const aggregateUsage = emptyUsage();
877
901
  const attemptNotes: string[] = [];
902
+ const pendingAttemptNotes: string[] = [];
878
903
  let totalToolCount = 0;
879
904
  let totalDurationMs = 0;
880
905
 
@@ -897,7 +922,18 @@ export async function runSync(
897
922
  const candidate = modelsToTry[i];
898
923
  if (candidate) attemptedModels.push(candidate);
899
924
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
900
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
925
+ let attemptOptions = options;
926
+ if (i < modelsToTry.length - 1 && options.onUpdate) {
927
+ const forwardUpdate = options.onUpdate;
928
+ attemptOptions = {
929
+ ...options,
930
+ onUpdate: (update) => {
931
+ if (shouldSuppressIntermediateRetryableFailureUpdate(update)) return;
932
+ forwardUpdate(update);
933
+ },
934
+ };
935
+ }
936
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
901
937
  sessionEnabled,
902
938
  systemPrompt,
903
939
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -928,10 +964,12 @@ export async function runSync(
928
964
  if (attemptSucceeded) {
929
965
  break;
930
966
  }
931
- if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
932
- break;
967
+ if (isRetryableModelFailure(result.error) && i < modelsToTry.length - 1) {
968
+ pendingAttemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
969
+ continue;
933
970
  }
934
- attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
971
+ attemptNotes.push(...pendingAttemptNotes);
972
+ break;
935
973
  }
936
974
 
937
975
  const result = lastResult ?? {
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import * as path from "node:path";
4
+ import { createGitEnvironment } from "@bastani/atomic";
4
5
  import type {
5
6
  AcceptanceConfig,
6
7
  AcceptanceEvidenceKind,
@@ -399,7 +400,7 @@ function reportEvidencePresent(report: AcceptanceReport, kind: AcceptanceEvidenc
399
400
  }
400
401
 
401
402
  function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
402
- const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
403
+ const result = spawnSync("git", ["status", "--short"], { cwd, env: createGitEnvironment(), encoding: "utf-8" });
403
404
  if (result.status !== 0) {
404
405
  return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
405
406
  }
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { APP_NAME } from "@bastani/atomic";
5
+ import { APP_NAME, createGitEnvironment } from "@bastani/atomic";
6
6
 
7
7
  export interface WorktreeSetup {
8
8
  cwd: string;
@@ -82,7 +82,7 @@ interface RepoState {
82
82
  const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
83
83
 
84
84
  function runGit(cwd: string, args: string[]): GitResult {
85
- const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
85
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", env: createGitEnvironment() });
86
86
  return {
87
87
  stdout: result.stdout ?? "",
88
88
  stderr: result.stderr ?? "",
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.26-alpha.2] - 2026-06-05
8
+
9
+ ### Changed
10
+
11
+ - Bumped package version for the Atomic 0.8.26-alpha.2 prerelease.
12
+
7
13
  ## [0.8.26-alpha.1] - 2026-06-05
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.8.26-alpha.1",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.26-alpha.2] - 2026-06-05
10
+
11
+ ### Changed
12
+
13
+ - Updated the `research-codebase` skill to capture a `breaking_changes_allowed` compatibility posture before research fanout, carry it through sub-agent prompts, and record it in research documents so downstream specs and workflows do not preserve legacy APIs by default when breaking changes are allowed ([#1225](https://github.com/bastani-inc/atomic/issues/1225)).
14
+
15
+ ### Fixed
16
+
17
+ - Fixed stage-local workflow HIL `input` and `editor` prompts losing draft text across Ctrl+D detach/reattach; drafts are kept live-only in memory and cleared when the prompt or run/stage exits ([#1179](https://github.com/bastani-inc/atomic/issues/1179)).
18
+ - Fixed workflow worktree Git commands to strip ambient repository-local Git environment variables before inspecting or creating targeted worktrees.
19
+ - Suppressed intermediate model fallback failure warnings from successful workflow stages while preserving final failures and raw per-attempt diagnostics ([#1226](https://github.com/bastani-inc/atomic/issues/1226)).
20
+
9
21
  ## [0.8.26-alpha.1] - 2026-06-05
10
22
 
11
23
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.26-alpha.1",
3
+ "version": "0.8.26-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -24,14 +24,24 @@ The user's research question/request is: **$ARGUMENTS**
24
24
  - **CRITICAL**: Read these files yourself in the main context before spawning any sub-tasks
25
25
  - This ensures you have full context before decomposing the research
26
26
 
27
- 2. **Analyze and decompose the research question:**
27
+ 2. **Determine the compatibility posture:**
28
+ - Before decomposing the research request, identify whether this project must preserve backward compatibility for real downstream users.
29
+ - If the user explicitly allows breaking changes, public API changes, cleanup, or says there are no real users/downstream dependencies, set `breaking_changes_allowed: true`.
30
+ - If the user mentions production users, published APIs, downstream consumers, migration safety, or compatibility requirements, set `breaking_changes_allowed: false`.
31
+ - If the posture is not inferable from the request, ask the user once before continuing, using the available structured question tool when possible.
32
+ - Carry this posture into the research plan, every sub-agent prompt, the final research document frontmatter, and the `## Compatibility Context` section.
33
+ - When `breaking_changes_allowed: true`, document existing legacy behavior, compatibility shims, optional flags, and public APIs as current state, not as constraints future specs must preserve unless the user explicitly asks for preservation.
34
+ - When `breaking_changes_allowed: false`, document public APIs, compatibility-sensitive surfaces, downstream callers, migration constraints, and behavior that future work must preserve.
35
+
36
+ 3. **Analyze and decompose the research question:**
28
37
  - Break the research question down into composable research areas
29
38
  - Take time to ultrathink about the underlying patterns, connections, and architectural implications the user might be seeking
30
39
  - Identify specific components, patterns, or concepts to investigate
31
40
  - Create a research plan using TodoWrite to track all subtasks
41
+ - Include the compatibility posture in the plan so later synthesis and spec creation inherit the same constraint.
32
42
  - Consider which directories, files, or architectural patterns are relevant
33
43
 
34
- 3. **Spawn parallel sub-agent tasks:**
44
+ 4. **Spawn parallel sub-agent tasks:**
35
45
  - Create multiple Task agents to research different aspects concurrently
36
46
  - We now have specialized agents that know how to do specific research tasks:
37
47
 
@@ -67,8 +77,9 @@ The user's research question/request is: **$ARGUMENTS**
67
77
  - Each agent knows its job - just tell it what you're looking for
68
78
  - Don't write detailed prompts about HOW to search - the agents already know
69
79
  - Remind agents they are documenting, not evaluating or improving
80
+ - Include `breaking_changes_allowed: true` or `breaking_changes_allowed: false` in each sub-agent prompt so compatibility-sensitive findings are documented with the right posture.
70
81
 
71
- 4. **Wait for all sub-agents to complete and synthesize:**
82
+ 5. **Wait for all sub-agents to complete and synthesize:**
72
83
  - IMPORTANT: Wait for ALL sub-agent tasks to complete before proceeding
73
84
  - Compile all sub-agent results (both codebase and research findings)
74
85
  - Prioritize live codebase findings as primary source of truth
@@ -79,7 +90,7 @@ The user's research question/request is: **$ARGUMENTS**
79
90
  - Answer the user's research question with concrete evidence
80
91
  - **If findings reveal the original question was misframed** (e.g., the system works differently than assumed, or the components don't exist where expected), flag this to the user before finalizing the document. This is valuable signal — don't bury it.
81
92
 
82
- 5. **Generate research document:**
93
+ 6. **Generate research document:**
83
94
  - Follow the directory structure for research documents:
84
95
 
85
96
  ```
@@ -117,6 +128,8 @@ research/
117
128
  status: complete
118
129
  last_updated: !`date '+%Y-%m-%d'`
119
130
  last_updated_by: [Researcher name]
131
+ breaking_changes_allowed: [true or false]
132
+ compatibility_context: "[Short explanation of downstream-user/API compatibility posture]"
120
133
  ---
121
134
 
122
135
  # Research
@@ -125,6 +138,10 @@ research/
125
138
 
126
139
  [Original user query]
127
140
 
141
+ ## Compatibility Context
142
+
143
+ [State whether breaking changes are allowed. If true, note that existing compatibility shims, optional flags, legacy APIs, and public APIs are documented as current state rather than preservation constraints. If false, summarize compatibility-sensitive surfaces, downstream users/callers, migration constraints, and behavior future work must preserve.]
144
+
128
145
  ## Summary
129
146
 
130
147
  [High-level documentation of what was found, answering the user's question by describing what exists]
@@ -167,19 +184,19 @@ research/
167
184
  [Any areas that need further investigation]
168
185
  ```
169
186
 
170
- 1. **Add GitHub permalinks (if applicable):**
187
+ 7. **Add GitHub permalinks (if applicable):**
171
188
  - Check if on main branch or if commit is pushed: `git branch --show-current` and `git status`
172
189
  - If on main/master or pushed, generate GitHub permalinks:
173
190
  - Get repo info: `gh repo view --json owner,name`
174
191
  - Create permalinks: `https://github.com/{owner}/{repo}/blob/{commit}/{file}#L{line}`
175
192
  - Replace local file references with permalinks in the document
176
193
 
177
- 2. **Present findings:**
194
+ 8. **Present findings:**
178
195
  - Present a concise summary of findings to the user
179
196
  - Include key file references for easy navigation
180
197
  - Ask if they have follow-up questions or need clarification
181
198
 
182
- 3. **Handle follow-up questions:**
199
+ 9. **Handle follow-up questions:**
183
200
 
184
201
  - If the user has follow-up questions, append to the same research document
185
202
  - Update the frontmatter fields `last_updated` and `last_updated_by` to reflect the update
@@ -207,10 +224,12 @@ research/
207
224
  - **REMEMBER**: Document what IS, not what SHOULD BE
208
225
  - **NO RECOMMENDATIONS**: Only describe the current state of the codebase
209
226
  - **File reading**: Always read mentioned files FULLY (no limit/offset) before spawning sub-tasks
227
+ - **Compatibility posture**: Always determine `breaking_changes_allowed` before decomposing the question. This is a single project/research posture, not a request to add compatibility flags. Use it to document whether old APIs and shims are constraints for future work.
210
228
  - **Critical ordering**: Follow the numbered steps exactly
211
229
  - ALWAYS read mentioned files first before spawning sub-tasks (step 1)
212
- - ALWAYS wait for all sub-agents to complete before synthesizing (step 4)
213
- - ALWAYS gather metadata before writing the document (step 5 before step 6)
230
+ - ALWAYS determine compatibility posture before decomposing the question (step 2)
231
+ - ALWAYS wait for all sub-agents to complete before synthesizing (step 5)
232
+ - ALWAYS gather metadata before writing the document (as part of step 6)
214
233
  - NEVER write the research document with placeholder values
215
234
 
216
235
  - **Frontmatter consistency**:
@@ -586,6 +586,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
586
586
  let selectedModel: string | undefined;
587
587
  const modelAttempts: WorkflowModelAttempt[] = [];
588
588
  const modelWarnings: string[] = [];
589
+ const pendingFallbackWarnings: string[] = [];
589
590
  const modelCatalog = opts.models === undefined
590
591
  ? undefined
591
592
  : {
@@ -796,15 +797,19 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
796
797
  try {
797
798
  await promptWithPauseResume(activeSession, text, sdkOptions);
798
799
  modelAttempts.push({ model: candidate.id, success: true, ...modelAttemptReasoning(candidate) });
800
+ pendingFallbackWarnings.length = 0;
799
801
  return;
800
802
  } catch (err) {
801
803
  const message = errorMessage(err);
802
804
  modelAttempts.push({ model: candidate.id, success: false, ...modelAttemptReasoning(candidate), error: message });
803
805
  if (signal?.aborted || !isRetryableModelFailure(message) || index === candidates.length - 1) {
806
+ modelWarnings.push(...pendingFallbackWarnings);
807
+ pendingFallbackWarnings.length = 0;
808
+ notifyModelFallbackMetaChange();
804
809
  throw err;
805
810
  }
806
811
  const nextCandidate = candidates[index + 1]!;
807
- modelWarnings.push(`[fallback] ${candidateLabel(candidate)} failed: ${message}. Retrying with ${candidateLabel(nextCandidate)}.`);
812
+ pendingFallbackWarnings.push(`[fallback] ${candidateLabel(candidate)} failed: ${message}. Retrying with ${candidateLabel(nextCandidate)}.`);
808
813
  await disposeCurrentSession();
809
814
  index += 1;
810
815
  }
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { APP_NAME } from "@bastani/atomic";
5
+ import { APP_NAME, createGitEnvironment } from "@bastani/atomic";
6
6
 
7
7
  export interface WorktreeSetup {
8
8
  cwd: string;
@@ -110,7 +110,7 @@ function runGit(cwd: string, args: string[]): GitResult {
110
110
  ], {
111
111
  cwd,
112
112
  encoding: "utf-8",
113
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
113
+ env: createGitEnvironment({ GIT_OPTIONAL_LOCKS: "0" }),
114
114
  timeout: 5000,
115
115
  });
116
116
  return {
@@ -151,6 +151,16 @@ export interface Store {
151
151
  ): boolean;
152
152
  /** Wait for a stage/node-scoped HIL prompt to resolve. */
153
153
  awaitStagePendingPrompt(runId: string, stageId: string, promptId: string): Promise<unknown>;
154
+ /**
155
+ * Record a live-only draft for an active stage-local input/editor prompt.
156
+ * Draft text may contain secrets and must never be copied into snapshots,
157
+ * status output, logs, notifications, or persisted metadata.
158
+ */
159
+ recordStagePromptDraft(runId: string, stageId: string, promptId: string, text: string): boolean;
160
+ /** Return a live-only draft for an active stage-local input/editor prompt, if present. */
161
+ getStagePromptDraft(runId: string, stageId: string, promptId: string): string | undefined;
162
+ /** Clear a live-only draft for a stage-local prompt. */
163
+ clearStagePromptDraft(runId: string, stageId: string, promptId: string): boolean;
154
164
  /**
155
165
  * Return the live-only prompt answer record for a completed prompt stage, if
156
166
  * still available. The returned value may contain secrets and must never be
@@ -239,6 +249,7 @@ export function createStore(): Store {
239
249
  const _notices: WorkflowNotice[] = [];
240
250
  const _listeners: Set<(snap: StoreSnapshot) => void> = new Set();
241
251
  const _stagePromptAnswers = new Map<string, PromptAnswerRecord>();
252
+ const _stagePromptDrafts = new Map<string, string>();
242
253
  let _version = 0;
243
254
 
244
255
  /**
@@ -285,16 +296,36 @@ export function createStore(): Store {
285
296
  return JSON.stringify([runId, stageId]);
286
297
  }
287
298
 
288
- function rejectStagePrompt(stage: StageSnapshot, reason: string): void {
299
+ function stagePromptDraftKey(runId: string, stageId: string, promptId: string): string {
300
+ return JSON.stringify([runId, stageId, promptId]);
301
+ }
302
+
303
+ function stageHasActiveTextPrompt(
304
+ runId: string,
305
+ stageId: string,
306
+ promptId: string,
307
+ ): { prompt: PendingPrompt } | undefined {
308
+ const run = findRun(runId);
309
+ if (!run || TERMINAL_STATUSES.has(run.status)) return undefined;
310
+ const stage = findStage(run, stageId);
311
+ if (!stage || isTerminalStageStatus(stage.status)) return undefined;
312
+ const prompt = stage.pendingPrompt;
313
+ if (!prompt || prompt.id !== promptId) return undefined;
314
+ if (prompt.kind !== "input" && prompt.kind !== "editor") return undefined;
315
+ return { prompt };
316
+ }
317
+
318
+ function rejectStagePrompt(runId: string, stage: StageSnapshot, reason: string): void {
289
319
  const prompt = stage.pendingPrompt;
290
320
  if (!prompt) return;
291
321
  stage.pendingPrompt = undefined;
322
+ _stagePromptDrafts.delete(stagePromptDraftKey(runId, stage.id, prompt.id));
292
323
  rejectPrompt(prompt.id, reason);
293
324
  }
294
325
 
295
- function rejectAllStagePrompts(run: RunSnapshot, reason: string): void {
326
+ function rejectAllStagePrompts(runId: string, run: RunSnapshot, reason: string): void {
296
327
  for (const stage of run.stages) {
297
- rejectStagePrompt(stage, reason);
328
+ rejectStagePrompt(runId, stage, reason);
298
329
  }
299
330
  }
300
331
 
@@ -427,7 +458,7 @@ export function createStore(): Store {
427
458
  if (stage.workflowChild !== undefined) existing.workflowChild = structuredClone(stage.workflowChild);
428
459
  delete existing.awaitingInputSince;
429
460
  delete existing.inputRequest;
430
- rejectStagePrompt(existing, `atomic-workflows: stage ${stage.id} ended before prompt resolved`);
461
+ rejectStagePrompt(runId, existing, `atomic-workflows: stage ${stage.id} ended before prompt resolved`);
431
462
  _version++;
432
463
  notify();
433
464
  },
@@ -474,7 +505,7 @@ export function createStore(): Store {
474
505
  run.pendingPrompt = undefined;
475
506
  rejectPrompt(pending.id, `atomic-workflows: run ${runId} ended before prompt resolved`);
476
507
  }
477
- rejectAllStagePrompts(run, `atomic-workflows: run ${runId} ended before prompt resolved`);
508
+ rejectAllStagePrompts(runId, run, `atomic-workflows: run ${runId} ended before prompt resolved`);
478
509
  _version++;
479
510
  notify();
480
511
  return true;
@@ -488,7 +519,7 @@ export function createStore(): Store {
488
519
  if (pending) {
489
520
  rejectPrompt(pending.id, `atomic-workflows: run ${runId} was removed before prompt resolved`);
490
521
  }
491
- rejectAllStagePrompts(run, `atomic-workflows: run ${runId} was removed before prompt resolved`);
522
+ rejectAllStagePrompts(runId, run, `atomic-workflows: run ${runId} was removed before prompt resolved`);
492
523
  for (const stage of run.stages) {
493
524
  _stagePromptAnswers.delete(stagePromptAnswerKey(runId, stage.id));
494
525
  }
@@ -599,6 +630,7 @@ export function createStore(): Store {
599
630
  if (!stage) return false;
600
631
  const pending = stage.pendingPrompt;
601
632
  if (!pending || pending.id !== promptId) return false;
633
+ _stagePromptDrafts.delete(stagePromptDraftKey(runId, stageId, promptId));
602
634
  if (options.recordAnswer !== false) {
603
635
  _stagePromptAnswers.set(stagePromptAnswerKey(runId, stageId), {
604
636
  runId,
@@ -654,6 +686,21 @@ export function createStore(): Store {
654
686
  });
655
687
  },
656
688
 
689
+ recordStagePromptDraft(runId: string, stageId: string, promptId: string, text: string): boolean {
690
+ if (stageHasActiveTextPrompt(runId, stageId, promptId) === undefined) return false;
691
+ _stagePromptDrafts.set(stagePromptDraftKey(runId, stageId, promptId), text);
692
+ return true;
693
+ },
694
+
695
+ getStagePromptDraft(runId: string, stageId: string, promptId: string): string | undefined {
696
+ if (stageHasActiveTextPrompt(runId, stageId, promptId) === undefined) return undefined;
697
+ return _stagePromptDrafts.get(stagePromptDraftKey(runId, stageId, promptId));
698
+ },
699
+
700
+ clearStagePromptDraft(runId: string, stageId: string, promptId: string): boolean {
701
+ return _stagePromptDrafts.delete(stagePromptDraftKey(runId, stageId, promptId));
702
+ },
703
+
657
704
  getStagePromptAnswer(runId: string, stageId: string): PromptAnswerRecord | undefined {
658
705
  return _stagePromptAnswers.get(stagePromptAnswerKey(runId, stageId));
659
706
  },
@@ -896,7 +943,13 @@ export function createStore(): Store {
896
943
  },
897
944
 
898
945
  clear(): void {
899
- if (_runs.length === 0 && _notices.length === 0 && _resolvers.size === 0 && _stagePromptAnswers.size === 0) return;
946
+ if (
947
+ _runs.length === 0 &&
948
+ _notices.length === 0 &&
949
+ _resolvers.size === 0 &&
950
+ _stagePromptAnswers.size === 0 &&
951
+ _stagePromptDrafts.size === 0
952
+ ) return;
900
953
  _runs.length = 0;
901
954
  _notices.length = 0;
902
955
  // Reject any outstanding HIL waiters so background promises terminate
@@ -907,6 +960,7 @@ export function createStore(): Store {
907
960
  }
908
961
  _resolvers.clear();
909
962
  _stagePromptAnswers.clear();
963
+ _stagePromptDrafts.clear();
910
964
  _version++;
911
965
  notify();
912
966
  },
@@ -521,6 +521,7 @@ export class StageChatView implements Component, Focusable {
521
521
  }
522
522
  if (!this.promptState || this.promptState.prompt.id !== prompt.id) {
523
523
  this.promptState = createPromptCardState(prompt);
524
+ this._seedPromptTextState(prompt);
524
525
  this._resetPromptEditor(prompt);
525
526
  this._resetPromptScroll();
526
527
  return true;
@@ -533,19 +534,49 @@ export class StageChatView implements Component, Focusable {
533
534
  this.promptMaxScroll = 0;
534
535
  }
535
536
 
537
+ private _promptSeedText(prompt: PendingPrompt): string {
538
+ const draft = this.store.getStagePromptDraft(this.runId, this.stageId, prompt.id);
539
+ if (draft !== undefined) return draft;
540
+ return typeof prompt.initial === "string" ? prompt.initial : "";
541
+ }
542
+
543
+ private _seedPromptTextState(prompt: PendingPrompt): void {
544
+ if (prompt.kind !== "input" && prompt.kind !== "editor") return;
545
+ if (!this.promptState || this.promptState.prompt.id !== prompt.id) return;
546
+ const seed = this._promptSeedText(prompt);
547
+ this.promptState.rawText = seed;
548
+ this.promptState.caret = seed.length;
549
+ }
550
+
551
+ private _recordPromptDraft(promptId: string, text: string): void {
552
+ this.store.recordStagePromptDraft(this.runId, this.stageId, promptId, text);
553
+ }
554
+
555
+ private _recordCurrentPromptDraft(): void {
556
+ const state = this.promptState;
557
+ if (!state) return;
558
+ const prompt = state.prompt;
559
+ if (prompt.kind !== "input" && prompt.kind !== "editor") return;
560
+ const text = this.promptEditor && this.promptEditorPromptId === prompt.id
561
+ ? this.promptEditor.getText()
562
+ : state.rawText;
563
+ this._recordPromptDraft(prompt.id, text);
564
+ }
565
+
536
566
  private _resetPromptEditor(prompt: PendingPrompt): void {
537
567
  this._disposePromptEditor();
538
568
  if ((prompt.kind !== "input" && prompt.kind !== "editor") || !this.piTui) return;
539
569
  const editor = this.piEditorFactory
540
570
  ? this.piEditorFactory(this.piTui, editorThemeFromGraphTheme(this.theme), this.piKeybindings)
541
571
  : new Editor(this.piTui, editorThemeFromGraphTheme(this.theme), { paddingX: 0 });
542
- editor.setText(typeof prompt.initial === "string" ? prompt.initial : "");
572
+ editor.setText(this.promptState?.prompt.id === prompt.id ? this.promptState.rawText : this._promptSeedText(prompt));
543
573
  setEditorPlaceholder(editor, "Type your response…");
544
574
  setEditorBorderColor(editor, (text) => hexToAnsi(this.theme.accent) + text + RESET);
545
575
  editor.onChange = (text: string) => {
546
576
  if (this.promptState?.prompt.id !== prompt.id) return;
547
577
  this.promptState.rawText = text;
548
578
  this.promptState.caret = text.length;
579
+ this._recordPromptDraft(prompt.id, text);
549
580
  this.requestRender?.();
550
581
  };
551
582
  editor.onSubmit = (text: string) => {
@@ -1248,11 +1279,14 @@ export class StageChatView implements Component, Focusable {
1248
1279
  return;
1249
1280
  }
1250
1281
  const action = handlePromptCardInput(data, state, this._promptKeybindings());
1282
+ const prompt = state.prompt;
1283
+ if (prompt.kind === "input" || prompt.kind === "editor") {
1284
+ this._recordPromptDraft(prompt.id, state.rawText);
1285
+ }
1251
1286
  if (action.kind === "noop") {
1252
1287
  this.requestRender?.();
1253
1288
  return;
1254
1289
  }
1255
- const prompt = state.prompt;
1256
1290
  const response = action.kind === "submit"
1257
1291
  ? action.response
1258
1292
  : defaultResponseFor(prompt);
@@ -1306,6 +1340,7 @@ export class StageChatView implements Component, Focusable {
1306
1340
  const readOnlyPromptArchive = readOnlyArchive && stage?.promptFootprint !== undefined;
1307
1341
  if (matchesKey(data, Key.ctrl("d"))) {
1308
1342
  if (!this.promptState && this.chatHost.hasInputText()) return this.chatHost.handleInput(data);
1343
+ this._recordCurrentPromptDraft();
1309
1344
  this.onDetach();
1310
1345
  return true;
1311
1346
  }