@navels/neal 0.1.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,57 @@
1
+ import { Writable } from 'node:stream';
2
+ import { ActivityFooter } from '../activity-footer.js';
3
+ import { clearDiagnosticFooter, configureDiagnosticFooter, resetDiagnosticDetailState, setDiagnosticDetailContext, writeNarrative, } from '../diagnostic.js';
4
+ import { createInteractiveKeyController } from '../interactive-controls.js';
5
+ export async function withInteractiveActivity(options, action) {
6
+ let detailContext = {};
7
+ resetDiagnosticDetailState(detailContext);
8
+ const footer = new ActivityFooter();
9
+ configureDiagnosticFooter(footer);
10
+ let state = {
11
+ mode: options.mode,
12
+ activity: options.initialActivity,
13
+ status: options.status ?? 'running',
14
+ subject: options.subject,
15
+ startedAt: Date.now(),
16
+ };
17
+ footer.setState(state);
18
+ const controller = createInteractiveKeyController({
19
+ allowStopRequest: false,
20
+ getDetailFilter() {
21
+ return detailContext;
22
+ },
23
+ });
24
+ const reportActivity = async (update) => {
25
+ detailContext = update.detailContext ?? detailContext;
26
+ setDiagnosticDetailContext(detailContext);
27
+ state = {
28
+ ...state,
29
+ activity: update.activity,
30
+ status: update.status ?? state.status,
31
+ subject: update.subject === undefined ? state.subject : update.subject,
32
+ };
33
+ footer.setState(state);
34
+ };
35
+ try {
36
+ if (process.stdin.isTTY) {
37
+ writeNarrative('[neal] keys: v show/hide details\n');
38
+ }
39
+ return await action({
40
+ stderr: createNarrativeWritable(),
41
+ reportActivity,
42
+ });
43
+ }
44
+ finally {
45
+ controller.cleanup();
46
+ clearDiagnosticFooter();
47
+ resetDiagnosticDetailState();
48
+ }
49
+ }
50
+ function createNarrativeWritable() {
51
+ return new Writable({
52
+ write(chunk, _encoding, callback) {
53
+ writeNarrative(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk);
54
+ callback();
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,79 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import process from 'node:process';
4
+ import { parseNewRunArgs } from '../cli.js';
5
+ import { assertWriterProvidersConfigured, getConfiguredUnattended } from '../config.js';
6
+ import { assertGitRepositoryWithCommit } from '../git.js';
7
+ import { loadOrInitialize } from '../orchestrator.js';
8
+ import { assertAgentConfigSupportsWriterRun } from '../providers/registry.js';
9
+ import { executeRun, withPreparedWriterRun } from './runtime.js';
10
+ import { getExecuteRunResultExitCode, setWriterCommandExitCode } from './writer-exit-codes.js';
11
+ const PARSE_ONLY_AGENT_CONFIG = {
12
+ planner: {
13
+ provider: 'openai-codex',
14
+ model: null,
15
+ },
16
+ coder: {
17
+ provider: 'openai-codex',
18
+ model: null,
19
+ },
20
+ reviewer: {
21
+ provider: 'anthropic-claude',
22
+ model: null,
23
+ },
24
+ };
25
+ export async function runNewRunCommand(args) {
26
+ const parsedArgs = parseNewRunArgs(args, PARSE_ONLY_AGENT_CONFIG);
27
+ const cwd = process.cwd();
28
+ const agentConfig = assertWriterProvidersConfigured(cwd, {
29
+ context: `new ${parsedArgs.topLevelMode} writer run`,
30
+ });
31
+ const parsed = {
32
+ ...parsedArgs,
33
+ agentConfig,
34
+ };
35
+ assertAgentConfigSupportsWriterRun(parsed.agentConfig, { context: `new ${parsed.topLevelMode} writer run` });
36
+ const planDoc = resolve(cwd, parsed.planDoc);
37
+ // Flag overrides config: `--unattended` forces true, otherwise fall back to
38
+ // the resolved `agent.unattended` config value (default false).
39
+ const unattended = parsed.unattended || getConfiguredUnattended(cwd);
40
+ await assertGitRepositoryWithCommit(cwd, `neal ${parsed.topLevelMode}`);
41
+ await requireExistingPlanFile(planDoc, parsed.planDoc);
42
+ const result = await withPreparedWriterRun({
43
+ cwd,
44
+ topLevelMode: parsed.topLevelMode,
45
+ getLockPlanDoc: () => planDoc,
46
+ }, async (prepared, markInitialized) => {
47
+ const loaded = await loadOrInitialize(planDoc, cwd, parsed.agentConfig, undefined, parsed.topLevelMode, {
48
+ allowedDirtyPaths: parsed.topLevelMode === 'execute' ? [planDoc] : [],
49
+ runDir: prepared.runDir,
50
+ unattended,
51
+ autoSquashOnCompletion: parsed.squashOnCompletion,
52
+ });
53
+ markInitialized();
54
+ assertAgentConfigSupportsWriterRun(loaded.state.agentConfig, { context: `new ${loaded.state.topLevelMode} writer run` });
55
+ return executeRun(loaded.state, loaded.statePath, loaded.logger, {
56
+ // The `--no-squash` flag only seeds the run state; once persisted, the
57
+ // state is the single source of truth so `neal resume` sees the same
58
+ // preference this process does.
59
+ autoSquashOnCompletion: loaded.state.autoSquashOnCompletion,
60
+ unattended,
61
+ });
62
+ });
63
+ setWriterCommandExitCode(getExecuteRunResultExitCode(result));
64
+ }
65
+ async function requireExistingPlanFile(planDoc, displayPath) {
66
+ let planStat;
67
+ try {
68
+ planStat = await stat(planDoc);
69
+ }
70
+ catch (error) {
71
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
72
+ throw new Error(`Plan file does not exist: ${displayPath}`);
73
+ }
74
+ throw error;
75
+ }
76
+ if (!planStat.isFile()) {
77
+ throw new Error(`Plan path is not a file: ${displayPath}`);
78
+ }
79
+ }
@@ -0,0 +1,44 @@
1
+ import process from 'node:process';
2
+ import { parsePlanAndExecuteArgs } from '../cli.js';
3
+ import { assertWriterProvidersConfigured, getConfiguredUnattended } from '../config.js';
4
+ import { assertGitRepositoryWithCommit } from '../git.js';
5
+ import { runPlanAndExecuteQueue } from '../plan-queue.js';
6
+ import { assertAgentConfigSupportsWriterRun } from '../providers/registry.js';
7
+ import { getPlanAndExecuteQueueExitCode, setWriterCommandExitCode } from './writer-exit-codes.js';
8
+ const PARSE_ONLY_AGENT_CONFIG = {
9
+ planner: {
10
+ provider: 'openai-codex',
11
+ model: null,
12
+ },
13
+ coder: {
14
+ provider: 'openai-codex',
15
+ model: null,
16
+ },
17
+ reviewer: {
18
+ provider: 'anthropic-claude',
19
+ model: null,
20
+ },
21
+ };
22
+ export async function runPlanAndExecuteCommand(args, deps) {
23
+ const parsedArgs = parsePlanAndExecuteArgs(args, PARSE_ONLY_AGENT_CONFIG);
24
+ const cwd = process.cwd();
25
+ const agentConfig = assertWriterProvidersConfigured(cwd, { context: 'plan-and-execute queue' });
26
+ const parsed = {
27
+ ...parsedArgs,
28
+ agentConfig,
29
+ };
30
+ assertAgentConfigSupportsWriterRun(parsed.agentConfig, { context: 'plan-and-execute queue' });
31
+ await assertGitRepositoryWithCommit(cwd, 'neal run');
32
+ // Flag overrides config: `--unattended` forces true, otherwise fall back to
33
+ // the resolved `agent.unattended` config value (default false).
34
+ const unattended = parsed.unattended || getConfiguredUnattended(cwd);
35
+ const queueState = await runPlanAndExecuteQueue({
36
+ cwd,
37
+ planDocs: parsed.planDocs,
38
+ agentConfig: parsed.agentConfig,
39
+ squashOnCompletion: parsed.squashOnCompletion,
40
+ unattended,
41
+ deps,
42
+ });
43
+ setWriterCommandExitCode(getPlanAndExecuteQueueExitCode(queueState));
44
+ }
@@ -0,0 +1,217 @@
1
+ import { basename } from 'node:path';
2
+ import { RunLogger } from '../logger.js';
3
+ import { InteractiveBlockedRecoveryPendingTurnError, recordInteractiveBlockedRecoveryGuidance, } from '../orchestrator/phases/recovery.js';
4
+ import { writeExecutionArtifacts } from '../orchestrator/artifacts.js';
5
+ import { assertAgentConfigSupportsResume } from '../providers/registry.js';
6
+ import { formatPublicPhase } from '../phase-display.js';
7
+ import { logUserGuidanceApplied } from '../prompts/guidance.js';
8
+ import { decideResumeAction } from '../resume-decision.js';
9
+ import { loadState, saveState } from '../state.js';
10
+ function getSelectedRunId(state, selectedRunId) {
11
+ return selectedRunId && selectedRunId !== 'latest' ? selectedRunId : basename(state.runDir);
12
+ }
13
+ function getResumeCommand(state, target) {
14
+ return `neal resume --run ${getSelectedRunId(state, target.selectedRunId)}`;
15
+ }
16
+ function getUnsupportedModeMessage(state) {
17
+ return `neal resume --message is only supported for execute-mode recovery or blocked plan review; selected run is ${state.topLevelMode}-mode`;
18
+ }
19
+ function describeGuidanceStateProblem(state, target) {
20
+ if (state.phase === 'interactive_blocked_recovery' && !state.interactiveBlockedRecovery) {
21
+ return [
22
+ 'neal resume --message cannot record guidance because the selected run is waiting for recovery guidance but has no blocked-recovery state.',
23
+ `Run id: ${getSelectedRunId(state, target.selectedRunId)}`,
24
+ `State path: ${target.statePath}`,
25
+ ].join('\n');
26
+ }
27
+ return [
28
+ 'neal resume --message can record guidance only while the selected run is waiting in interactive blocked recovery.',
29
+ `Run id: ${getSelectedRunId(state, target.selectedRunId)}`,
30
+ `Current step: ${formatPublicPhase(state.phase)}`,
31
+ `Status: ${state.status}`,
32
+ `State path: ${target.statePath}`,
33
+ ].join('\n');
34
+ }
35
+ function describePlanReviewGuidanceStateProblem(state, target) {
36
+ return [
37
+ 'neal resume --message can record plan-review guidance only while a top-level plan run is blocked from reviewer_plan with no pending guidance.',
38
+ `Run id: ${getSelectedRunId(state, target.selectedRunId)}`,
39
+ `Current step: ${formatPublicPhase(state.phase)}`,
40
+ `Status: ${state.status}`,
41
+ `Blocked from: ${state.blockedFromPhase ?? 'none'}`,
42
+ `State path: ${target.statePath}`,
43
+ ].join('\n');
44
+ }
45
+ function getPendingRecoveryTurn(state) {
46
+ const recovery = state.interactiveBlockedRecovery;
47
+ if (!recovery) {
48
+ return 0;
49
+ }
50
+ if (recovery.pendingDirective) {
51
+ return recovery.turns.length + 1;
52
+ }
53
+ return recovery.turns.find((turn) => turn.number > recovery.lastHandledTurn)?.number ?? recovery.turns.length;
54
+ }
55
+ function formatGuidanceDecisionRejection(decision) {
56
+ switch (decision.kind) {
57
+ case 'continue':
58
+ return `Run does not need --message. Resume it with: ${decision.resumeCommand}`;
59
+ case 'already_running':
60
+ return `${decision.reason}\nInspect status with: ${decision.statusCommand}`;
61
+ case 'done':
62
+ return decision.retrospectivePath
63
+ ? `${decision.summary}\nReview latest retrospective: ${decision.retrospectivePath}`
64
+ : `${decision.summary}\nInspect status with: ${decision.statusCommand}`;
65
+ case 'cannot_resume':
66
+ return `${decision.reason}\nInspect status and artifacts with: ${decision.statusCommand}`;
67
+ }
68
+ }
69
+ function isEligiblePlanReviewGuidanceState(state) {
70
+ return (state.topLevelMode === 'plan' &&
71
+ state.status === 'blocked' &&
72
+ state.phase === 'blocked' &&
73
+ state.blockedFromPhase === 'reviewer_plan' &&
74
+ state.pendingPlanReviewGuidance === null);
75
+ }
76
+ async function recordPlanReviewGuidance(args) {
77
+ const resumeCommand = getResumeCommand(args.state, args.target);
78
+ if (args.decision.kind === 'pending_message') {
79
+ return {
80
+ kind: 'pending',
81
+ guidanceKind: 'plan_review',
82
+ statePath: args.target.statePath,
83
+ runDir: args.state.runDir,
84
+ message: args.decision.reason,
85
+ pendingTurn: null,
86
+ recoveryTurns: args.state.interactiveBlockedRecovery?.turns.length ?? 0,
87
+ resumeCommand,
88
+ };
89
+ }
90
+ if (args.decision.kind !== 'needs_message') {
91
+ throw new Error(formatGuidanceDecisionRejection(args.decision));
92
+ }
93
+ const trimmedGuidance = args.message.trim();
94
+ if (!trimmedGuidance) {
95
+ throw new Error('Plan review guidance must not be empty');
96
+ }
97
+ if (!isEligiblePlanReviewGuidanceState(args.state)) {
98
+ throw new Error(describePlanReviewGuidanceStateProblem(args.state, args.target));
99
+ }
100
+ assertAgentConfigSupportsResume(args.state.agentConfig, args.state, {
101
+ context: 'record plan-review guidance for resume',
102
+ });
103
+ const logger = new RunLogger(args.state.runDir);
104
+ await logger.event('run.resumed', {
105
+ statePath: args.target.statePath,
106
+ phase: args.state.phase,
107
+ status: args.state.status,
108
+ agentConfig: args.state.agentConfig,
109
+ });
110
+ await logUserGuidanceApplied(logger);
111
+ const nextState = await saveState(args.target.statePath, {
112
+ ...args.state,
113
+ phase: 'coder_plan_response',
114
+ status: 'running',
115
+ blockedFromPhase: null,
116
+ pendingPlanReviewGuidance: {
117
+ message: trimmedGuidance,
118
+ sourcePhase: 'reviewer_plan',
119
+ recordedAt: new Date().toISOString(),
120
+ },
121
+ });
122
+ await writeExecutionArtifacts(nextState);
123
+ await logger.event('plan_review_guidance.recorded', {
124
+ statePath: args.target.statePath,
125
+ sourcePhase: 'reviewer_plan',
126
+ guidanceBytes: Buffer.byteLength(trimmedGuidance, 'utf8'),
127
+ });
128
+ return {
129
+ kind: 'recorded',
130
+ statePath: args.target.statePath,
131
+ runDir: nextState.runDir,
132
+ nextState,
133
+ logger,
134
+ recoveryTurns: nextState.interactiveBlockedRecovery?.turns.length ?? 0,
135
+ terminalDirectivePending: false,
136
+ resumeCommand,
137
+ };
138
+ }
139
+ export async function recordRecoveryGuidanceForResolvedRun(args) {
140
+ const state = await loadState(args.target.statePath);
141
+ const selectedRunId = getSelectedRunId(state, args.target.selectedRunId);
142
+ const currentStateDecision = decideResumeAction({
143
+ state,
144
+ selectedRunId,
145
+ statePath: args.target.statePath,
146
+ });
147
+ const decision = args.decision?.kind === 'needs_message'
148
+ ? currentStateDecision
149
+ : args.decision ?? currentStateDecision;
150
+ if (state.topLevelMode === 'plan') {
151
+ return recordPlanReviewGuidance({
152
+ state,
153
+ target: args.target,
154
+ message: args.message,
155
+ decision,
156
+ });
157
+ }
158
+ if (state.topLevelMode !== 'execute') {
159
+ throw new Error(getUnsupportedModeMessage(state));
160
+ }
161
+ if (decision.kind === 'pending_message') {
162
+ return {
163
+ kind: 'pending',
164
+ guidanceKind: 'interactive_blocked_recovery',
165
+ statePath: args.target.statePath,
166
+ runDir: state.runDir,
167
+ message: decision.reason,
168
+ pendingTurn: getPendingRecoveryTurn(state),
169
+ recoveryTurns: state.interactiveBlockedRecovery?.turns.length ?? 0,
170
+ resumeCommand: decision.resumeCommand,
171
+ };
172
+ }
173
+ if (decision.kind !== 'needs_message') {
174
+ throw new Error(formatGuidanceDecisionRejection(decision));
175
+ }
176
+ if (state.phase !== 'interactive_blocked_recovery' || !state.interactiveBlockedRecovery) {
177
+ throw new Error(describeGuidanceStateProblem(state, args.target));
178
+ }
179
+ const resumeCommand = getResumeCommand(state, args.target);
180
+ assertAgentConfigSupportsResume(state.agentConfig, state, { context: 'record recovery guidance for resume' });
181
+ const logger = new RunLogger(state.runDir);
182
+ await logger.event('run.resumed', {
183
+ statePath: args.target.statePath,
184
+ phase: state.phase,
185
+ status: state.status,
186
+ agentConfig: state.agentConfig,
187
+ });
188
+ await logUserGuidanceApplied(logger);
189
+ try {
190
+ const nextState = await recordInteractiveBlockedRecoveryGuidance(args.target.statePath, args.message, logger);
191
+ return {
192
+ kind: 'recorded',
193
+ statePath: args.target.statePath,
194
+ runDir: nextState.runDir,
195
+ nextState,
196
+ logger,
197
+ recoveryTurns: nextState.interactiveBlockedRecovery?.turns.length ?? 0,
198
+ terminalDirectivePending: nextState.interactiveBlockedRecovery?.pendingDirective?.terminalOnly ?? false,
199
+ resumeCommand,
200
+ };
201
+ }
202
+ catch (error) {
203
+ if (error instanceof InteractiveBlockedRecoveryPendingTurnError) {
204
+ return {
205
+ kind: 'pending',
206
+ guidanceKind: 'interactive_blocked_recovery',
207
+ statePath: args.target.statePath,
208
+ runDir: state.runDir,
209
+ message: error.message,
210
+ pendingTurn: error.pendingTurn,
211
+ recoveryTurns: state.interactiveBlockedRecovery.turns.length,
212
+ resumeCommand,
213
+ };
214
+ }
215
+ throw error;
216
+ }
217
+ }