@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.
- package/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { parseResumeArgs } from '../cli.js';
|
|
5
|
+
import { assertWriterProvidersConfigured } from '../config.js';
|
|
6
|
+
import { writeDiagnostic } from '../diagnostic.js';
|
|
7
|
+
import { assertGitRepositoryWithCommit } from '../git.js';
|
|
8
|
+
import { loadOrInitialize } from '../orchestrator.js';
|
|
9
|
+
import { continuePlanAndExecuteQueueFromChildRun, inspectQueueChildResumeEvidence, } from '../plan-queue.js';
|
|
10
|
+
import { assertAgentConfigSupportsResume } from '../providers/registry.js';
|
|
11
|
+
import { decideResumeAction } from '../resume-decision.js';
|
|
12
|
+
import { runManualGateResumeChecks } from '../manual-gates.js';
|
|
13
|
+
import { writeExecutionArtifacts } from '../orchestrator/artifacts.js';
|
|
14
|
+
import { ActiveRunLockError, acquireActiveRunLock, clearStaleActiveRunLockForResume, inspectActiveRunLock, } from '../run-lock.js';
|
|
15
|
+
import { getDefaultAgentConfig, saveState } from '../state.js';
|
|
16
|
+
import { recordRecoveryGuidanceForResolvedRun } from './recovery-guidance.js';
|
|
17
|
+
import { executeRun, resolveWriterRunSelection, withActiveRunLock, } from './runtime.js';
|
|
18
|
+
import { getExecuteRunResultExitCode, getPlanAndExecuteQueueExitCode, setWriterCommandExitCode, } from './writer-exit-codes.js';
|
|
19
|
+
export async function runResumeRunCommand(args, deps = {}) {
|
|
20
|
+
const parsed = parseResumeArgs(args);
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const selection = await resolveResumeSelectionOrSetupGuidance(parsed.runId, cwd);
|
|
23
|
+
await assertGitRepositoryWithCommit(selection.state.cwd, 'neal resume');
|
|
24
|
+
const lock = await inspectActiveRunLock(cwd, selection.selectedRunId);
|
|
25
|
+
const decision = decideResumeAction({
|
|
26
|
+
state: selection.state,
|
|
27
|
+
selectedRunId: selection.selectedRunId,
|
|
28
|
+
statePath: selection.statePath,
|
|
29
|
+
lock,
|
|
30
|
+
queue: await inspectQueueChildResumeEvidence({ cwd: selection.state.cwd, runDir: selection.state.runDir }),
|
|
31
|
+
retrospectivePath: await findLatestRetrospectiveMarkdownPath(selection.state.runDir),
|
|
32
|
+
});
|
|
33
|
+
if (parsed.message !== null && isWaitingForManualGate(selection.state)) {
|
|
34
|
+
throw new Error([
|
|
35
|
+
`Run is waiting for manual gate ${selection.state.manualGate.id}: ${selection.state.manualGate.title}`,
|
|
36
|
+
`Complete the manual work and resume without --message: neal resume --run ${selection.selectedRunId}`,
|
|
37
|
+
`Instructions: ${selection.state.manualGate.instructionsPath}`,
|
|
38
|
+
].join('\n'));
|
|
39
|
+
}
|
|
40
|
+
const result = parsed.message === null
|
|
41
|
+
? await runPlainResume({ selection, decision, lock })
|
|
42
|
+
: await runMessageResume({ selection, decision, lock, message: parsed.message });
|
|
43
|
+
await applyResumeRunExitCode(result, deps);
|
|
44
|
+
}
|
|
45
|
+
async function resolveResumeSelectionOrSetupGuidance(runId, cwd) {
|
|
46
|
+
try {
|
|
47
|
+
return await resolveWriterRunSelection({
|
|
48
|
+
runId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (runId === null || runId === 'latest') {
|
|
53
|
+
assertWriterProvidersConfigured(cwd, { context: 'resume writer run' });
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function runPlainResume(args) {
|
|
59
|
+
if (isWaitingForManualGate(args.selection.state)) {
|
|
60
|
+
switch (args.decision.kind) {
|
|
61
|
+
case 'continue':
|
|
62
|
+
case 'pending_message':
|
|
63
|
+
return resumeManualGateRun(args.selection, args.lock);
|
|
64
|
+
case 'already_running':
|
|
65
|
+
process.stdout.write(formatAlreadyRunningMessage(args.decision));
|
|
66
|
+
return { kind: 'already_running' };
|
|
67
|
+
case 'done':
|
|
68
|
+
process.stdout.write(formatDoneMessage(args.decision));
|
|
69
|
+
return { kind: 'already_done' };
|
|
70
|
+
case 'needs_message':
|
|
71
|
+
process.stdout.write(`${formatNeedsMessageResumeRejection(args.decision)}\n`);
|
|
72
|
+
return { kind: 'waiting_for_operator_guidance' };
|
|
73
|
+
case 'cannot_resume':
|
|
74
|
+
throw new Error(formatCannotResumeMessage(args.decision));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
switch (args.decision.kind) {
|
|
78
|
+
case 'continue':
|
|
79
|
+
case 'pending_message':
|
|
80
|
+
return resumeExecutableRun(args.selection, args.lock);
|
|
81
|
+
case 'needs_message':
|
|
82
|
+
process.stdout.write(`${formatNeedsMessageResumeRejection(args.decision)}\n`);
|
|
83
|
+
return { kind: 'waiting_for_operator_guidance' };
|
|
84
|
+
case 'already_running':
|
|
85
|
+
process.stdout.write(formatAlreadyRunningMessage(args.decision));
|
|
86
|
+
return { kind: 'already_running' };
|
|
87
|
+
case 'done':
|
|
88
|
+
process.stdout.write(formatDoneMessage(args.decision));
|
|
89
|
+
return { kind: 'already_done' };
|
|
90
|
+
case 'cannot_resume':
|
|
91
|
+
throw new Error(formatCannotResumeMessage(args.decision));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function runMessageResume(args) {
|
|
95
|
+
switch (args.decision.kind) {
|
|
96
|
+
case 'needs_message':
|
|
97
|
+
return resumeAfterRecordingGuidance(args.selection, args.lock, args.decision, args.message);
|
|
98
|
+
case 'pending_message':
|
|
99
|
+
throw new Error([
|
|
100
|
+
args.decision.reason,
|
|
101
|
+
`Run \`${args.decision.resumeCommand}\` without --message to let Neal handle it before recording more guidance.`,
|
|
102
|
+
].join('\n'));
|
|
103
|
+
case 'continue':
|
|
104
|
+
throw new Error(`Run does not need --message. Resume it with: ${args.decision.resumeCommand}`);
|
|
105
|
+
case 'already_running':
|
|
106
|
+
throw new Error(formatAlreadyRunningMessage(args.decision).trimEnd());
|
|
107
|
+
case 'done':
|
|
108
|
+
throw new Error(formatDoneMessage(args.decision).trimEnd());
|
|
109
|
+
case 'cannot_resume':
|
|
110
|
+
throw new Error(formatCannotResumeMessage(args.decision));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function resumeAfterRecordingGuidance(selection, lock, decision, message) {
|
|
114
|
+
return withResumeWriterLock(selection, lock, async () => {
|
|
115
|
+
const guidanceResult = await recordRecoveryGuidanceForResolvedRun({
|
|
116
|
+
target: {
|
|
117
|
+
statePath: selection.statePath,
|
|
118
|
+
selectedRunId: selection.selectedRunId,
|
|
119
|
+
},
|
|
120
|
+
message,
|
|
121
|
+
decision,
|
|
122
|
+
});
|
|
123
|
+
if (guidanceResult.kind === 'pending') {
|
|
124
|
+
throw new Error(formatPendingGuidanceMessage(guidanceResult));
|
|
125
|
+
}
|
|
126
|
+
writeDiagnostic(`[neal] recovery guidance recorded; resuming: ${guidanceResult.resumeCommand}\n`);
|
|
127
|
+
const loaded = {
|
|
128
|
+
state: guidanceResult.nextState,
|
|
129
|
+
statePath: guidanceResult.statePath,
|
|
130
|
+
logger: guidanceResult.logger,
|
|
131
|
+
};
|
|
132
|
+
assertAgentConfigSupportsResume(loaded.state.agentConfig, loaded.state, {
|
|
133
|
+
context: 'resume after recording recovery guidance',
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
kind: 'resumed_child',
|
|
137
|
+
result: await executeResumedRun(loaded.state, loaded.statePath, loaded.logger),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function formatPendingGuidanceMessage(guidanceResult) {
|
|
142
|
+
if (guidanceResult.kind !== 'pending') {
|
|
143
|
+
throw new Error('Expected pending guidance result');
|
|
144
|
+
}
|
|
145
|
+
const firstLine = guidanceResult.guidanceKind === 'plan_review'
|
|
146
|
+
? '[neal] plan-review guidance is already pending.'
|
|
147
|
+
: `[neal] recovery guidance is already pending for turn ${guidanceResult.pendingTurn}.`;
|
|
148
|
+
return [
|
|
149
|
+
firstLine,
|
|
150
|
+
`Run \`${guidanceResult.resumeCommand}\` without --message to let Neal handle it before recording more guidance.`,
|
|
151
|
+
].join('\n');
|
|
152
|
+
}
|
|
153
|
+
async function resumeExecutableRun(selection, lock) {
|
|
154
|
+
return withResumeWriterLock(selection, lock, async () => {
|
|
155
|
+
const loaded = await loadOrInitialize(null, selection.state.cwd, getDefaultAgentConfig(selection.state.cwd), selection.statePath, selection.state.topLevelMode);
|
|
156
|
+
assertAgentConfigSupportsResume(loaded.state.agentConfig, loaded.state, { context: 'resume writer run' });
|
|
157
|
+
return {
|
|
158
|
+
kind: 'resumed_child',
|
|
159
|
+
result: await executeResumedRun(loaded.state, loaded.statePath, loaded.logger),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function resumeManualGateRun(selection, lock) {
|
|
164
|
+
return withResumeWriterLock(selection, lock, async () => {
|
|
165
|
+
const loaded = await loadOrInitialize(null, selection.state.cwd, getDefaultAgentConfig(selection.state.cwd), selection.statePath, selection.state.topLevelMode);
|
|
166
|
+
if (!isWaitingForManualGate(loaded.state)) {
|
|
167
|
+
return {
|
|
168
|
+
kind: 'resumed_child',
|
|
169
|
+
result: await executeResumedRun(loaded.state, loaded.statePath, loaded.logger),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const checkResult = await runManualGateResumeChecks(loaded.state);
|
|
173
|
+
const checkedAt = new Date().toISOString();
|
|
174
|
+
if (!checkResult.ok) {
|
|
175
|
+
const failedState = await saveState(loaded.statePath, {
|
|
176
|
+
...loaded.state,
|
|
177
|
+
manualGate: {
|
|
178
|
+
...loaded.state.manualGate,
|
|
179
|
+
updatedAt: checkedAt,
|
|
180
|
+
lastCheckedAt: checkedAt,
|
|
181
|
+
lastFailure: toPersistedManualGateFailure(checkResult.failure),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
await writeExecutionArtifacts(failedState);
|
|
185
|
+
await loaded.logger.event('manual_gate.check_failed', {
|
|
186
|
+
gateId: failedState.manualGate?.id ?? null,
|
|
187
|
+
checkName: checkResult.failure.checkName,
|
|
188
|
+
exitCode: checkResult.failure.exitCode,
|
|
189
|
+
signal: checkResult.failure.signal,
|
|
190
|
+
instructionsPath: failedState.manualGate?.instructionsPath ?? null,
|
|
191
|
+
});
|
|
192
|
+
process.stdout.write(formatManualGateCheckFailure(failedState, checkResult.failure));
|
|
193
|
+
return { kind: 'manual_gate_check_failed' };
|
|
194
|
+
}
|
|
195
|
+
const gate = loaded.state.manualGate;
|
|
196
|
+
const resumedState = await saveState(loaded.statePath, {
|
|
197
|
+
...loaded.state,
|
|
198
|
+
phase: gate.resumePhase,
|
|
199
|
+
status: 'running',
|
|
200
|
+
blockedFromPhase: null,
|
|
201
|
+
manualGate: null,
|
|
202
|
+
});
|
|
203
|
+
await writeExecutionArtifacts(resumedState);
|
|
204
|
+
await loaded.logger.event('manual_gate.checks_passed', {
|
|
205
|
+
gateId: gate.id,
|
|
206
|
+
checkNames: checkResult.results.map((result) => result.checkName),
|
|
207
|
+
resumePhase: resumedState.phase,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
kind: 'resumed_child',
|
|
211
|
+
result: await executeResumedRun(resumedState, loaded.statePath, loaded.logger),
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function executeResumedRun(state, statePath, logger) {
|
|
216
|
+
return executeRun(state, statePath, logger, {
|
|
217
|
+
// Honor the squash preference persisted at run creation (`--no-squash`)
|
|
218
|
+
// instead of re-deriving it, exactly as `unattended` is. Legacy states
|
|
219
|
+
// missing the field hydrate to true, keeping the historical behavior.
|
|
220
|
+
autoSquashOnCompletion: state.autoSquashOnCompletion,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function isWaitingForManualGate(state) {
|
|
224
|
+
return state.phase === 'manual_gate' && state.manualGate !== null;
|
|
225
|
+
}
|
|
226
|
+
function toPersistedManualGateFailure(failure) {
|
|
227
|
+
return {
|
|
228
|
+
checkName: failure.checkName,
|
|
229
|
+
exitCode: failure.exitCode,
|
|
230
|
+
signal: failure.signal,
|
|
231
|
+
stdoutTail: failure.stdoutTail,
|
|
232
|
+
stderrTail: failure.stderrTail,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function formatManualGateCheckFailure(state, failure) {
|
|
236
|
+
const gate = state.manualGate;
|
|
237
|
+
if (!gate) {
|
|
238
|
+
throw new Error('Expected manual gate state while formatting failed resume check.');
|
|
239
|
+
}
|
|
240
|
+
const runId = basename(state.runDir);
|
|
241
|
+
const status = failure.signal ? `signal ${failure.signal}` : `exit code ${failure.exitCode ?? 'unknown'}`;
|
|
242
|
+
const lines = [
|
|
243
|
+
`[neal] Manual gate ${gate.id} is still waiting: check ${failure.checkName} failed with ${status}.`,
|
|
244
|
+
`[neal] Instructions: ${gate.instructionsPath}`,
|
|
245
|
+
`[neal] Resume after completing the manual work: neal resume --run ${runId}`,
|
|
246
|
+
];
|
|
247
|
+
if (failure.stdoutTail) {
|
|
248
|
+
lines.push('[neal] stdout tail:', failure.stdoutTail.trimEnd());
|
|
249
|
+
}
|
|
250
|
+
if (failure.stderrTail) {
|
|
251
|
+
lines.push('[neal] stderr tail:', failure.stderrTail.trimEnd());
|
|
252
|
+
}
|
|
253
|
+
return `${lines.join('\n')}\n`;
|
|
254
|
+
}
|
|
255
|
+
function emitAlreadyRunningOutcome(selection) {
|
|
256
|
+
process.stdout.write(formatAlreadyRunningMessage({
|
|
257
|
+
kind: 'already_running',
|
|
258
|
+
reason: `Run ${selection.selectedRunId} appears to already be running under the active writer lock.`,
|
|
259
|
+
statusCommand: `neal status --run ${selection.selectedRunId}`,
|
|
260
|
+
}));
|
|
261
|
+
return { kind: 'already_running' };
|
|
262
|
+
}
|
|
263
|
+
async function withResumeWriterLock(selection, evidence, action) {
|
|
264
|
+
let lock;
|
|
265
|
+
try {
|
|
266
|
+
lock = await acquireResumeWriterLock(selection, evidence);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
// A live same-run lock owned by a different process means a concurrent
|
|
270
|
+
// `neal resume` won the acquisition race after this process passed the
|
|
271
|
+
// read-only decision layer. That is the same operator situation as the
|
|
272
|
+
// shared-handle path below, so it gets the same graceful already-running
|
|
273
|
+
// outcome (exit 2) instead of a hard failure.
|
|
274
|
+
if (error instanceof ActiveRunLockError && error.kind === 'active_same_run') {
|
|
275
|
+
return emitAlreadyRunningOutcome(selection);
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (!lock.acquired) {
|
|
280
|
+
await lock.release();
|
|
281
|
+
return emitAlreadyRunningOutcome(selection);
|
|
282
|
+
}
|
|
283
|
+
return withActiveRunLock(lock, action);
|
|
284
|
+
}
|
|
285
|
+
async function applyResumeRunExitCode(outcome, deps) {
|
|
286
|
+
switch (outcome.kind) {
|
|
287
|
+
case 'already_done':
|
|
288
|
+
setWriterCommandExitCode(0);
|
|
289
|
+
return;
|
|
290
|
+
case 'already_running':
|
|
291
|
+
case 'waiting_for_operator_guidance':
|
|
292
|
+
case 'manual_gate_check_failed':
|
|
293
|
+
setWriterCommandExitCode(2);
|
|
294
|
+
return;
|
|
295
|
+
case 'resumed_child': {
|
|
296
|
+
const continueQueueFromChildRun = deps.continueQueueFromChildRun ?? continuePlanAndExecuteQueueFromChildRun;
|
|
297
|
+
const queueState = await continueQueueFromChildRun({
|
|
298
|
+
childResult: outcome.result,
|
|
299
|
+
agentConfig: outcome.result.finalState.agentConfig,
|
|
300
|
+
// Source unattended from the resumed child's persisted state, exactly as
|
|
301
|
+
// agentConfig is, so the queue continuation keeps headless runs headless.
|
|
302
|
+
unattended: outcome.result.finalState.unattended,
|
|
303
|
+
// Likewise source the squash preference from the resumed child's
|
|
304
|
+
// persisted state so remaining queue items keep honoring --no-squash.
|
|
305
|
+
squashOnCompletion: outcome.result.finalState.autoSquashOnCompletion,
|
|
306
|
+
});
|
|
307
|
+
setWriterCommandExitCode(queueState
|
|
308
|
+
? getPlanAndExecuteQueueExitCode(queueState)
|
|
309
|
+
: getExecuteRunResultExitCode(outcome.result));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function acquireResumeWriterLock(selection, evidence) {
|
|
314
|
+
const cwd = process.cwd();
|
|
315
|
+
await clearStaleActiveRunLockForResume(cwd, evidence);
|
|
316
|
+
try {
|
|
317
|
+
return await acquireActiveRunLock({
|
|
318
|
+
cwd,
|
|
319
|
+
runId: selection.selectedRunId,
|
|
320
|
+
runStatePath: selection.statePath,
|
|
321
|
+
planDoc: selection.state.planDoc,
|
|
322
|
+
topLevelMode: selection.state.topLevelMode,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (!(error instanceof ActiveRunLockError) || error.kind !== 'stale_same_host') {
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
const refreshedEvidence = await inspectActiveRunLock(cwd, selection.selectedRunId);
|
|
330
|
+
await clearStaleActiveRunLockForResume(cwd, refreshedEvidence);
|
|
331
|
+
return acquireActiveRunLock({
|
|
332
|
+
cwd,
|
|
333
|
+
runId: selection.selectedRunId,
|
|
334
|
+
runStatePath: selection.statePath,
|
|
335
|
+
planDoc: selection.state.planDoc,
|
|
336
|
+
topLevelMode: selection.state.topLevelMode,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function formatNeedsMessageResumeRejection(decision) {
|
|
341
|
+
return [
|
|
342
|
+
`Run is waiting for operator guidance: ${decision.blocker}`,
|
|
343
|
+
`Record guidance and resume with: ${decision.messageCommand}`,
|
|
344
|
+
`Inspect status with: ${decision.statusCommand}`,
|
|
345
|
+
].join('\n');
|
|
346
|
+
}
|
|
347
|
+
function formatAlreadyRunningMessage(decision) {
|
|
348
|
+
return [`[neal] ${decision.reason}`, `[neal] Inspect status with: ${decision.statusCommand}`].join('\n') + '\n';
|
|
349
|
+
}
|
|
350
|
+
function formatDoneMessage(decision) {
|
|
351
|
+
const lines = [`[neal] ${decision.summary}`];
|
|
352
|
+
if (decision.retrospectivePath) {
|
|
353
|
+
lines.push(`[neal] Retrospective: ${decision.retrospectivePath}`);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
lines.push(`[neal] Inspect status with: ${decision.statusCommand}`);
|
|
357
|
+
}
|
|
358
|
+
return lines.join('\n') + '\n';
|
|
359
|
+
}
|
|
360
|
+
function formatCannotResumeMessage(decision) {
|
|
361
|
+
return [decision.reason, `Inspect status and artifacts with: ${decision.statusCommand}`].join('\n');
|
|
362
|
+
}
|
|
363
|
+
async function findLatestRetrospectiveMarkdownPath(runDir) {
|
|
364
|
+
const defaultPath = join(runDir, 'RETROSPECTIVE.md');
|
|
365
|
+
try {
|
|
366
|
+
const defaultStat = await stat(defaultPath);
|
|
367
|
+
if (defaultStat.isFile()) {
|
|
368
|
+
return defaultPath;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (!isNotFoundError(error)) {
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
let entries;
|
|
377
|
+
try {
|
|
378
|
+
entries = await readdir(runDir, { withFileTypes: true });
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (isNotFoundError(error)) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
const latestName = entries
|
|
387
|
+
.filter((entry) => entry.isFile() && /^RETROSPECTIVE-.+\.md$/.test(entry.name))
|
|
388
|
+
.map((entry) => entry.name)
|
|
389
|
+
.sort()
|
|
390
|
+
.at(-1);
|
|
391
|
+
return latestName ? join(runDir, latestName) : null;
|
|
392
|
+
}
|
|
393
|
+
function isNotFoundError(error) {
|
|
394
|
+
return typeof error === 'object' && error !== null && error.code === 'ENOENT';
|
|
395
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { parseReviewArgs } from '../cli.js';
|
|
3
|
+
import { assertGitRepositoryWithCommit } from '../git.js';
|
|
4
|
+
import { runNealReviewCli } from '../review-findings/run.js';
|
|
5
|
+
import { withInteractiveActivity } from './interactive-activity.js';
|
|
6
|
+
export async function runReviewCommand(args) {
|
|
7
|
+
const parsed = parseReviewArgs(args);
|
|
8
|
+
await assertGitRepositoryWithCommit(process.cwd(), 'neal review');
|
|
9
|
+
await withInteractiveActivity({
|
|
10
|
+
mode: 'review',
|
|
11
|
+
initialActivity: 'preparing review',
|
|
12
|
+
}, async ({ stderr, reportActivity }) => {
|
|
13
|
+
await runNealReviewCli({
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
parsed,
|
|
16
|
+
stdout: process.stdout,
|
|
17
|
+
stderr,
|
|
18
|
+
onActivity: reportActivity,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|