@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,136 @@
|
|
|
1
|
+
import { ReviewerRoundError } from '../../agents.js';
|
|
2
|
+
import { runExecuteReviewerAdjudication, synthesizeExecuteReviewerState, } from '../../adjudicator/execute.js';
|
|
3
|
+
import { assertAdjudicationTransitionSignal } from '../../adjudicator/specs.js';
|
|
4
|
+
import { getChangedFilesForRange, getCommitRange, getDiffForRange, getDiffStatForRange, getHeadCommit, } from '../../git.js';
|
|
5
|
+
import { writeDetail } from '../../diagnostic.js';
|
|
6
|
+
import { saveState } from '../../state.js';
|
|
7
|
+
import { writeExecutionArtifacts } from '../artifacts.js';
|
|
8
|
+
import { shouldNotifyFailure } from '../failures.js';
|
|
9
|
+
import { notifyBlocked } from '../notifications.js';
|
|
10
|
+
import { enterInteractiveBlockedRecovery, shouldNotifyInteractiveBlockedRecoveryEntry } from './recovery.js';
|
|
11
|
+
import { printReviewResult } from './shared.js';
|
|
12
|
+
function getExecuteReviewDispositionSignal(args) {
|
|
13
|
+
if (args.phase === 'execute_finalization') {
|
|
14
|
+
return args.meaningfulProgressAction === 'advance_parent' ? 'advance_parent' : 'accept_scope';
|
|
15
|
+
}
|
|
16
|
+
if (args.phase === 'coder_response') {
|
|
17
|
+
return 'request_revision';
|
|
18
|
+
}
|
|
19
|
+
if (args.phase === 'coder_optional_response') {
|
|
20
|
+
return 'optional_revision';
|
|
21
|
+
}
|
|
22
|
+
return args.meaningfulProgressAction === 'replace_plan' ? 'replace_plan' : 'block_for_operator';
|
|
23
|
+
}
|
|
24
|
+
export async function runReviewPhase(state, statePath, logger) {
|
|
25
|
+
await logger?.event('phase.start', { phase: 'reviewer_scope', round: state.rounds.length + 1 });
|
|
26
|
+
let claude;
|
|
27
|
+
let headCommit;
|
|
28
|
+
let changedFiles;
|
|
29
|
+
let reviewContext;
|
|
30
|
+
let reviewerSynthesis;
|
|
31
|
+
try {
|
|
32
|
+
({
|
|
33
|
+
context: reviewContext,
|
|
34
|
+
reviewerResult: claude,
|
|
35
|
+
reviewInput: { headCommit, changedFiles },
|
|
36
|
+
} = await runExecuteReviewerAdjudication({
|
|
37
|
+
state,
|
|
38
|
+
logger,
|
|
39
|
+
getHeadCommit,
|
|
40
|
+
getCommitRange,
|
|
41
|
+
getDiffStatForRange,
|
|
42
|
+
getChangedFilesForRange,
|
|
43
|
+
getDiffForRange,
|
|
44
|
+
}));
|
|
45
|
+
reviewerSynthesis = synthesizeExecuteReviewerState({
|
|
46
|
+
state,
|
|
47
|
+
context: reviewContext,
|
|
48
|
+
headCommit,
|
|
49
|
+
changedFiles,
|
|
50
|
+
reviewerResult: claude,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof ReviewerRoundError) {
|
|
55
|
+
const failedState = await saveState(statePath, {
|
|
56
|
+
...state,
|
|
57
|
+
reviewerSessionHandle: null,
|
|
58
|
+
status: 'failed',
|
|
59
|
+
});
|
|
60
|
+
await writeExecutionArtifacts(failedState);
|
|
61
|
+
await logger?.event('phase.error', {
|
|
62
|
+
phase: 'reviewer_scope',
|
|
63
|
+
round: state.rounds.length + 1,
|
|
64
|
+
sessionHandle: error.sessionHandle,
|
|
65
|
+
subtype: error.subtype,
|
|
66
|
+
message: error.message,
|
|
67
|
+
});
|
|
68
|
+
if (shouldNotifyFailure(error)) {
|
|
69
|
+
await notifyBlocked(failedState, error.message, logger);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
printReviewResult('review', claude.summary, claude.findings, logger);
|
|
75
|
+
const meaningfulProgressVerdict = reviewerSynthesis.meaningfulProgressVerdict;
|
|
76
|
+
writeDetail(`[reviewer:review] meaningful progress: ${meaningfulProgressVerdict.action} - ${meaningfulProgressVerdict.rationale}\n`, logger, {
|
|
77
|
+
phase: 'reviewer_scope',
|
|
78
|
+
role: 'reviewer',
|
|
79
|
+
});
|
|
80
|
+
assertAdjudicationTransitionSignal(reviewContext.spec, getExecuteReviewDispositionSignal({
|
|
81
|
+
phase: reviewerSynthesis.disposition.phase,
|
|
82
|
+
meaningfulProgressAction: meaningfulProgressVerdict.action,
|
|
83
|
+
}), 'orchestrator:reviewer_scope');
|
|
84
|
+
const adjudicatedState = {
|
|
85
|
+
...state,
|
|
86
|
+
reviewerSessionHandle: claude.sessionHandle,
|
|
87
|
+
phase: reviewerSynthesis.disposition.phase,
|
|
88
|
+
status: reviewerSynthesis.disposition.status,
|
|
89
|
+
rounds: [
|
|
90
|
+
...state.rounds,
|
|
91
|
+
reviewerSynthesis.roundRecord,
|
|
92
|
+
],
|
|
93
|
+
findings: reviewerSynthesis.mergedFindings,
|
|
94
|
+
blockedFromPhase: reviewerSynthesis.disposition.blockedFromPhase,
|
|
95
|
+
currentScopeMeaningfulProgressVerdict: meaningfulProgressVerdict,
|
|
96
|
+
};
|
|
97
|
+
const nextState = await saveState(statePath, adjudicatedState);
|
|
98
|
+
const parentAdvanceClassification = reviewerSynthesis.parentAdvanceClassification;
|
|
99
|
+
const parentAdvanceMessage = meaningfulProgressVerdict.action === 'advance_parent'
|
|
100
|
+
? (`advance_parent will advance parent scope ${parentAdvanceClassification?.parentScopeLabel ?? reviewContext.parentScopeLabel} ` +
|
|
101
|
+
`after empty derived scope ${parentAdvanceClassification?.currentScopeLabel ?? 'unknown'} ` +
|
|
102
|
+
'without accepting the current no-op sub-scope.')
|
|
103
|
+
: null;
|
|
104
|
+
if (meaningfulProgressVerdict.action === 'advance_parent') {
|
|
105
|
+
await logger?.event('review.meaningful_progress.advance_parent', {
|
|
106
|
+
message: parentAdvanceMessage ?? 'Advancing parent scope after an empty derived scope.',
|
|
107
|
+
parentScopeLabel: parentAdvanceClassification?.parentScopeLabel ?? reviewContext.parentScopeLabel,
|
|
108
|
+
currentScopeLabel: parentAdvanceClassification?.currentScopeLabel ?? null,
|
|
109
|
+
source: parentAdvanceClassification?.source ?? null,
|
|
110
|
+
priorEmptyCount: parentAdvanceClassification?.priorEmptyCount ?? null,
|
|
111
|
+
priorSubstantiveCount: parentAdvanceClassification?.priorSubstantiveCount ?? null,
|
|
112
|
+
aggregateChangedFileCount: parentAdvanceClassification?.aggregateChangedFiles.length ?? null,
|
|
113
|
+
failedPreconditions: parentAdvanceClassification?.failedPreconditions ?? [],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
await writeExecutionArtifacts(nextState);
|
|
117
|
+
await logger?.event('phase.complete', {
|
|
118
|
+
phase: 'reviewer_scope',
|
|
119
|
+
round: reviewContext.round,
|
|
120
|
+
sessionHandle: claude.sessionHandle,
|
|
121
|
+
findings: reviewerSynthesis.findings.length,
|
|
122
|
+
blockingFindings: reviewerSynthesis.findings.filter((finding) => finding.severity === 'blocking').length,
|
|
123
|
+
meaningfulProgressAction: meaningfulProgressVerdict.action,
|
|
124
|
+
originalMeaningfulProgressAction: claude.meaningfulProgress.action,
|
|
125
|
+
message: parentAdvanceMessage ?? undefined,
|
|
126
|
+
nextPhase: nextState.phase,
|
|
127
|
+
});
|
|
128
|
+
if (nextState.status === 'blocked' && reviewerSynthesis.blockReason) {
|
|
129
|
+
const persistedState = await enterInteractiveBlockedRecovery(nextState, statePath, reviewerSynthesis.blockReason, logger);
|
|
130
|
+
if (shouldNotifyInteractiveBlockedRecoveryEntry(persistedState)) {
|
|
131
|
+
await notifyBlocked(persistedState, reviewerSynthesis.blockReason, logger);
|
|
132
|
+
}
|
|
133
|
+
return persistedState;
|
|
134
|
+
}
|
|
135
|
+
return nextState;
|
|
136
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { writeDetail } from '../../diagnostic.js';
|
|
4
|
+
import { writeCheckpointRetrospective } from '../../retrospective.js';
|
|
5
|
+
import { getCurrentScopeLabel, getParentScopeLabel, isExecutingDerivedPlan } from '../../scopes.js';
|
|
6
|
+
import { saveState } from '../../state.js';
|
|
7
|
+
import { getDerivedPlanView } from '../../state-views.js';
|
|
8
|
+
import { writeExecutionArtifacts } from '../artifacts.js';
|
|
9
|
+
import { isCoderFreshSessionRetryableError, isCoderTimeoutError } from '../failures.js';
|
|
10
|
+
import { notifyRetry } from '../notifications.js';
|
|
11
|
+
import { appendCompletedScope } from '../transitions.js';
|
|
12
|
+
const execFile = promisify(execFileCallback);
|
|
13
|
+
function getWriterSessionRoleForPhase(phase) {
|
|
14
|
+
return phase === 'coder_plan' || phase === 'coder_plan_response' || phase === 'coder_plan_optional_response'
|
|
15
|
+
? 'planner'
|
|
16
|
+
: 'coder';
|
|
17
|
+
}
|
|
18
|
+
function getWriterSessionState(args) {
|
|
19
|
+
const role = getWriterSessionRoleForPhase(args.phase);
|
|
20
|
+
if (role === 'planner') {
|
|
21
|
+
const sessionHandle = args.errorSessionHandle ?? args.state.plannerSessionHandle;
|
|
22
|
+
return {
|
|
23
|
+
role,
|
|
24
|
+
sessionHandle,
|
|
25
|
+
protocol: sessionHandle ? args.state.plannerSessionProtocol ?? 'structured_json_v1' : null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const sessionHandle = args.errorSessionHandle ?? args.state.coderSessionHandle;
|
|
29
|
+
return {
|
|
30
|
+
role,
|
|
31
|
+
sessionHandle,
|
|
32
|
+
protocol: sessionHandle ? args.state.coderSessionProtocol ?? 'structured_json_v1' : null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function formatReviewFindings(findings) {
|
|
36
|
+
if (findings.length === 0) {
|
|
37
|
+
return ' Findings: none\n';
|
|
38
|
+
}
|
|
39
|
+
return findings
|
|
40
|
+
.map((finding, index) => {
|
|
41
|
+
const files = finding.files.length > 0 ? finding.files.join(', ') : 'n/a';
|
|
42
|
+
const source = finding.source ? ` [${finding.source}]` : '';
|
|
43
|
+
return [
|
|
44
|
+
` ${index + 1}. [${finding.severity}]${source} ${finding.claim}`,
|
|
45
|
+
` Files: ${files}`,
|
|
46
|
+
` Action: ${finding.requiredAction}`,
|
|
47
|
+
].join('\n');
|
|
48
|
+
})
|
|
49
|
+
.join('\n') + '\n';
|
|
50
|
+
}
|
|
51
|
+
export function printReviewResult(kind, summary, findings, logger) {
|
|
52
|
+
const blocking = findings.filter((finding) => finding.severity === 'blocking').length;
|
|
53
|
+
const nonBlocking = findings.length - blocking;
|
|
54
|
+
const header = kind === 'review' ? '[reviewer:review]' : '[reviewer:plan-review]';
|
|
55
|
+
const message = [
|
|
56
|
+
`${header} summary: ${summary}`,
|
|
57
|
+
`${header} findings: ${blocking} blocking, ${nonBlocking} non-blocking`,
|
|
58
|
+
formatReviewFindings(findings),
|
|
59
|
+
].join('\n');
|
|
60
|
+
writeDetail(`${message}\n`, logger, {
|
|
61
|
+
role: 'reviewer',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function persistCoderFailureState(state, statePath, phase, error, logger) {
|
|
65
|
+
const writerSession = getWriterSessionState({ state, phase, errorSessionHandle: error.sessionHandle });
|
|
66
|
+
const failedState = await saveState(statePath, {
|
|
67
|
+
...state,
|
|
68
|
+
...(writerSession.role === 'planner'
|
|
69
|
+
? {
|
|
70
|
+
plannerSessionHandle: writerSession.sessionHandle,
|
|
71
|
+
plannerSessionProtocol: writerSession.protocol,
|
|
72
|
+
}
|
|
73
|
+
: {
|
|
74
|
+
coderSessionHandle: writerSession.sessionHandle,
|
|
75
|
+
coderSessionProtocol: writerSession.protocol,
|
|
76
|
+
}),
|
|
77
|
+
status: 'failed',
|
|
78
|
+
});
|
|
79
|
+
await writeExecutionArtifacts(failedState);
|
|
80
|
+
await writeCheckpointRetrospective(failedState, 'failed');
|
|
81
|
+
await logger?.event('phase.error', {
|
|
82
|
+
phase,
|
|
83
|
+
role: writerSession.role,
|
|
84
|
+
sessionHandle: writerSession.sessionHandle,
|
|
85
|
+
message: error.message,
|
|
86
|
+
});
|
|
87
|
+
return failedState;
|
|
88
|
+
}
|
|
89
|
+
// Shared terminal-fail action for the three operator-block sites under
|
|
90
|
+
// `unattended`. Mirrors `persistCoderFailureState`: save `status:'failed'`,
|
|
91
|
+
// re-render the execution artifacts, write a `failed` checkpoint retrospective,
|
|
92
|
+
// and emit a classified log event — deliberately WITHOUT `notifyBlocked` (that
|
|
93
|
+
// is the attended wait notification). Any produced diff/plan is left in the
|
|
94
|
+
// worktree/run dir as an artifact and is not submitted, exactly as today's
|
|
95
|
+
// failed runs leave it. `phase`/`blockedFromPhase` are preserved for
|
|
96
|
+
// diagnostics (defaulting `blockedFromPhase` to the current phase).
|
|
97
|
+
export async function persistUnattendedBlockUnresolvedFailure(state, statePath, site, logger) {
|
|
98
|
+
const blockedFromPhase = state.blockedFromPhase ?? state.phase;
|
|
99
|
+
const failedState = await saveState(statePath, {
|
|
100
|
+
...state,
|
|
101
|
+
status: 'failed',
|
|
102
|
+
blockedFromPhase,
|
|
103
|
+
});
|
|
104
|
+
await writeExecutionArtifacts(failedState);
|
|
105
|
+
await writeCheckpointRetrospective(failedState, 'failed');
|
|
106
|
+
await logger?.event('unattended.block_unresolved', {
|
|
107
|
+
reason: 'unattended_block_unresolved',
|
|
108
|
+
site,
|
|
109
|
+
blockedFromPhase,
|
|
110
|
+
phase: failedState.phase,
|
|
111
|
+
scopeNumber: failedState.currentScopeNumber,
|
|
112
|
+
});
|
|
113
|
+
return failedState;
|
|
114
|
+
}
|
|
115
|
+
export async function persistBlockedScope(state, statePath, reason) {
|
|
116
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
117
|
+
if (state.completedScopes.some((scope) => scope.number === scopeLabel)) {
|
|
118
|
+
return state;
|
|
119
|
+
}
|
|
120
|
+
const derivedPlan = getDerivedPlanView(state);
|
|
121
|
+
const blockedDuringDerivedPlanReview = !derivedPlan?.executing &&
|
|
122
|
+
derivedPlan?.unexecuted === true &&
|
|
123
|
+
derivedPlan.parentScopeNumber === state.currentScopeNumber;
|
|
124
|
+
const nextState = await saveState(statePath, {
|
|
125
|
+
...state,
|
|
126
|
+
blockedFromPhase: state.blockedFromPhase ?? state.phase,
|
|
127
|
+
completedScopes: appendCompletedScope(state, 'blocked', {
|
|
128
|
+
scopeLabel: getCurrentScopeLabel(state),
|
|
129
|
+
finalCommit: null,
|
|
130
|
+
summary: null,
|
|
131
|
+
commitSubject: null,
|
|
132
|
+
archivedReviewPath: state.archivedReviewPath,
|
|
133
|
+
blocker: reason,
|
|
134
|
+
derivedFromParentScope: isExecutingDerivedPlan(state) ? getParentScopeLabel(state) : null,
|
|
135
|
+
replacedByDerivedPlanPath: blockedDuringDerivedPlanReview ? derivedPlan.path : null,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
await writeExecutionArtifacts(nextState);
|
|
139
|
+
return nextState;
|
|
140
|
+
}
|
|
141
|
+
export function shouldRetryCoderWithFreshSession(state, phase, error) {
|
|
142
|
+
if (!isCoderFreshSessionRetryableError(error) || state.coderRetryCount >= 1) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (state.topLevelMode === 'execute') {
|
|
146
|
+
return (phase === 'coder_scope' ||
|
|
147
|
+
phase === 'coder_response' ||
|
|
148
|
+
phase === 'coder_optional_response' ||
|
|
149
|
+
phase === 'interactive_blocked_recovery');
|
|
150
|
+
}
|
|
151
|
+
return phase === 'coder_plan' || phase === 'coder_plan_response' || phase === 'coder_plan_optional_response';
|
|
152
|
+
}
|
|
153
|
+
function escapeForPkillPattern(text) {
|
|
154
|
+
return text.replace(/[\\.^$|?*+()[\]{}]/g, '\\$&');
|
|
155
|
+
}
|
|
156
|
+
function parseProcessRows(output) {
|
|
157
|
+
const rows = [];
|
|
158
|
+
for (const line of output.split('\n')) {
|
|
159
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
|
|
160
|
+
if (!match) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
rows.push({
|
|
164
|
+
pid: Number(match[1]),
|
|
165
|
+
ppid: Number(match[2]),
|
|
166
|
+
command: match[3],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return rows;
|
|
170
|
+
}
|
|
171
|
+
async function findOwnCodexExecChildPids() {
|
|
172
|
+
const { stdout } = await execFile('ps', ['-axo', 'pid=,ppid=,command=']);
|
|
173
|
+
return parseProcessRows(stdout)
|
|
174
|
+
.filter((row) => row.ppid === process.pid)
|
|
175
|
+
.filter((row) => /\bcodex exec\b/.test(row.command))
|
|
176
|
+
.map((row) => row.pid);
|
|
177
|
+
}
|
|
178
|
+
async function killOwnCodexExecChildren(logger, sessionHandle) {
|
|
179
|
+
let pids;
|
|
180
|
+
try {
|
|
181
|
+
pids = await findOwnCodexExecChildPids();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
await logger?.event('coder.timeout_cleanup_child_scan_failed', {
|
|
185
|
+
sessionHandle,
|
|
186
|
+
details: error instanceof Error ? error.message : String(error),
|
|
187
|
+
});
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (pids.length === 0) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const termResults = [];
|
|
194
|
+
for (const pid of pids) {
|
|
195
|
+
try {
|
|
196
|
+
process.kill(pid, 'SIGTERM');
|
|
197
|
+
termResults.push({ pid, result: 'signaled' });
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
termResults.push({
|
|
201
|
+
pid,
|
|
202
|
+
result: 'failed',
|
|
203
|
+
details: error instanceof Error ? error.message : String(error),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
await logger?.event('coder.timeout_cleanup', {
|
|
208
|
+
sessionHandle,
|
|
209
|
+
pids,
|
|
210
|
+
result: 'killed_child_codex_exec',
|
|
211
|
+
signals: termResults,
|
|
212
|
+
});
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
export async function bestEffortCleanupTimedOutCoder(sessionHandle, logger) {
|
|
216
|
+
if (!sessionHandle) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const killedChild = await killOwnCodexExecChildren(logger, sessionHandle);
|
|
220
|
+
if (killedChild) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const pattern = `codex.*resume ${escapeForPkillPattern(sessionHandle)}`;
|
|
224
|
+
try {
|
|
225
|
+
await execFile('pkill', ['-f', pattern]);
|
|
226
|
+
await logger?.event('coder.timeout_cleanup', {
|
|
227
|
+
sessionHandle,
|
|
228
|
+
pattern,
|
|
229
|
+
result: 'killed',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
234
|
+
await logger?.event('coder.timeout_cleanup', {
|
|
235
|
+
sessionHandle,
|
|
236
|
+
pattern,
|
|
237
|
+
result: 'not_found_or_failed',
|
|
238
|
+
details,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function getCoderFreshSessionRetryMessage(state, phase, error) {
|
|
243
|
+
const reason = isCoderTimeoutError(error) ? 'timed out' : 'hit an unusable resume session';
|
|
244
|
+
const role = getWriterSessionRoleForPhase(phase);
|
|
245
|
+
return state.topLevelMode === 'plan'
|
|
246
|
+
? `planning phase ${phase} ${reason}; retrying with a fresh ${role} session`
|
|
247
|
+
: `scope ${state.currentScopeNumber} ${reason} in ${phase}; retrying with a fresh ${role} session`;
|
|
248
|
+
}
|
|
249
|
+
export async function scheduleCoderFreshSessionRetry(state, statePath, phase, error, logger) {
|
|
250
|
+
const writerSession = getWriterSessionState({ state, phase, errorSessionHandle: error.sessionHandle });
|
|
251
|
+
if (isCoderTimeoutError(error)) {
|
|
252
|
+
await bestEffortCleanupTimedOutCoder(writerSession.sessionHandle, logger);
|
|
253
|
+
}
|
|
254
|
+
const retryState = await saveState(statePath, {
|
|
255
|
+
...state,
|
|
256
|
+
...(writerSession.role === 'planner'
|
|
257
|
+
? {
|
|
258
|
+
plannerSessionHandle: null,
|
|
259
|
+
plannerSessionProtocol: null,
|
|
260
|
+
}
|
|
261
|
+
: {
|
|
262
|
+
coderSessionHandle: null,
|
|
263
|
+
coderSessionProtocol: null,
|
|
264
|
+
}),
|
|
265
|
+
coderRetryCount: state.coderRetryCount + 1,
|
|
266
|
+
status: 'running',
|
|
267
|
+
phase,
|
|
268
|
+
});
|
|
269
|
+
await writeExecutionArtifacts(retryState);
|
|
270
|
+
await logger?.event('phase.retry', {
|
|
271
|
+
phase,
|
|
272
|
+
role: writerSession.role,
|
|
273
|
+
sessionHandle: writerSession.sessionHandle,
|
|
274
|
+
retryCount: retryState.coderRetryCount,
|
|
275
|
+
message: error.message,
|
|
276
|
+
});
|
|
277
|
+
await notifyRetry(retryState, getCoderFreshSessionRetryMessage(retryState, phase, error), logger);
|
|
278
|
+
return retryState;
|
|
279
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EXECUTE_FINALIZATION_PHASE, isExecuteFinalizationPhase } from '../execute-finalization.js';
|
|
2
|
+
export const PLAN_RUNNABLE_PHASE_REGISTRY = {
|
|
3
|
+
coder_plan: 'coder_plan',
|
|
4
|
+
reviewer_plan: 'reviewer_plan',
|
|
5
|
+
coder_plan_response: 'coder_plan_response',
|
|
6
|
+
coder_plan_optional_response: 'coder_plan_response',
|
|
7
|
+
};
|
|
8
|
+
export const EXECUTE_RUNNABLE_PHASE_REGISTRY = {
|
|
9
|
+
awaiting_derived_plan_execution: 'awaiting_derived_plan_execution',
|
|
10
|
+
coder_scope: 'coder_scope',
|
|
11
|
+
reviewer_scope: 'reviewer_scope',
|
|
12
|
+
coder_response: 'coder_response',
|
|
13
|
+
coder_optional_response: 'coder_response',
|
|
14
|
+
};
|
|
15
|
+
export const RECOVERY_RUNNABLE_PHASE_REGISTRY = {
|
|
16
|
+
interactive_blocked_recovery: 'interactive_blocked_recovery',
|
|
17
|
+
};
|
|
18
|
+
export const EXECUTE_FINALIZATION_RUNNABLE_PHASE_REGISTRY = {
|
|
19
|
+
[EXECUTE_FINALIZATION_PHASE]: EXECUTE_FINALIZATION_PHASE,
|
|
20
|
+
final_completion_review: 'final_completion_review',
|
|
21
|
+
};
|
|
22
|
+
export const RUNNABLE_PHASE_PURPOSE_REGISTRIES = {
|
|
23
|
+
plan: PLAN_RUNNABLE_PHASE_REGISTRY,
|
|
24
|
+
execute: EXECUTE_RUNNABLE_PHASE_REGISTRY,
|
|
25
|
+
recovery: RECOVERY_RUNNABLE_PHASE_REGISTRY,
|
|
26
|
+
executeFinalization: EXECUTE_FINALIZATION_RUNNABLE_PHASE_REGISTRY,
|
|
27
|
+
};
|
|
28
|
+
export const RUNNABLE_PHASE_REGISTRY = {
|
|
29
|
+
...PLAN_RUNNABLE_PHASE_REGISTRY,
|
|
30
|
+
...EXECUTE_RUNNABLE_PHASE_REGISTRY,
|
|
31
|
+
...RECOVERY_RUNNABLE_PHASE_REGISTRY,
|
|
32
|
+
...EXECUTE_FINALIZATION_RUNNABLE_PHASE_REGISTRY,
|
|
33
|
+
};
|
|
34
|
+
export const RUNNABLE_PHASES_BY_TOP_LEVEL_MODE = {
|
|
35
|
+
plan: [
|
|
36
|
+
'coder_plan',
|
|
37
|
+
'reviewer_plan',
|
|
38
|
+
'coder_plan_response',
|
|
39
|
+
'coder_plan_optional_response',
|
|
40
|
+
],
|
|
41
|
+
execute: [
|
|
42
|
+
'reviewer_plan',
|
|
43
|
+
'coder_plan_response',
|
|
44
|
+
'coder_plan_optional_response',
|
|
45
|
+
'awaiting_derived_plan_execution',
|
|
46
|
+
'coder_scope',
|
|
47
|
+
'reviewer_scope',
|
|
48
|
+
'coder_response',
|
|
49
|
+
'coder_optional_response',
|
|
50
|
+
'interactive_blocked_recovery',
|
|
51
|
+
EXECUTE_FINALIZATION_PHASE,
|
|
52
|
+
'final_completion_review',
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
const RUNNABLE_PHASE_SETS_BY_TOP_LEVEL_MODE = {
|
|
56
|
+
plan: new Set(RUNNABLE_PHASES_BY_TOP_LEVEL_MODE.plan),
|
|
57
|
+
execute: new Set(RUNNABLE_PHASES_BY_TOP_LEVEL_MODE.execute),
|
|
58
|
+
};
|
|
59
|
+
export function isRunnablePhase(phase) {
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(RUNNABLE_PHASE_REGISTRY, phase);
|
|
61
|
+
}
|
|
62
|
+
export function getRunnablePhasesForTopLevelMode(topLevelMode) {
|
|
63
|
+
return RUNNABLE_PHASES_BY_TOP_LEVEL_MODE[topLevelMode];
|
|
64
|
+
}
|
|
65
|
+
export function isRunnablePhaseForTopLevelMode(phase, topLevelMode) {
|
|
66
|
+
return isRunnablePhase(phase) && RUNNABLE_PHASE_SETS_BY_TOP_LEVEL_MODE[topLevelMode].has(phase);
|
|
67
|
+
}
|
|
68
|
+
export async function runOnePass(args) {
|
|
69
|
+
const { state, logger, options, runtime, handlers } = args;
|
|
70
|
+
let currentState = state;
|
|
71
|
+
while (isRunnablePhase(currentState.phase) &&
|
|
72
|
+
(currentState.phase !== 'interactive_blocked_recovery' || runtime.hasPendingInteractiveBlockedRecoveryTurn(currentState))) {
|
|
73
|
+
const phaseStartedAt = Date.now();
|
|
74
|
+
await options?.onDisplayState?.(currentState, phaseStartedAt);
|
|
75
|
+
const stopHeartbeat = runtime.startPhaseHeartbeat(currentState.phase, () => currentState, logger);
|
|
76
|
+
try {
|
|
77
|
+
const currentPhase = currentState.phase;
|
|
78
|
+
currentState = await handlers[RUNNABLE_PHASE_REGISTRY[currentPhase]](currentState);
|
|
79
|
+
await options?.onDisplayState?.(currentState, Date.now());
|
|
80
|
+
const pausedAfterScopeBoundary = currentState.phase === 'coder_scope' &&
|
|
81
|
+
currentState.status === 'running' &&
|
|
82
|
+
(isExecuteFinalizationPhase(currentPhase) ||
|
|
83
|
+
currentPhase === 'final_completion_review' ||
|
|
84
|
+
currentPhase === 'awaiting_derived_plan_execution') &&
|
|
85
|
+
options?.shouldStopAfterCurrentScope?.();
|
|
86
|
+
if (pausedAfterScopeBoundary) {
|
|
87
|
+
await logger?.event('run.paused_after_scope', {
|
|
88
|
+
currentScopeNumber: currentState.currentScopeNumber,
|
|
89
|
+
phase: currentState.phase,
|
|
90
|
+
status: currentState.status,
|
|
91
|
+
});
|
|
92
|
+
return currentState;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
stopHeartbeat();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await logger?.event('run.complete', {
|
|
100
|
+
phase: currentState.phase,
|
|
101
|
+
status: currentState.status,
|
|
102
|
+
finalCommit: currentState.finalCommit,
|
|
103
|
+
archivedReviewPath: currentState.archivedReviewPath,
|
|
104
|
+
});
|
|
105
|
+
if (currentState.phase === 'blocked' || currentState.phase === 'done') {
|
|
106
|
+
// A terminal-failed run (e.g. unattended site-A terminal-fail lands on
|
|
107
|
+
// phase:'blocked' with status:'failed') must keep its failed retrospective
|
|
108
|
+
// rather than be overwritten as a blocked one.
|
|
109
|
+
const retrospectiveReason = currentState.status === 'failed' ? 'failed' : currentState.phase === 'blocked' ? 'blocked' : 'done';
|
|
110
|
+
await runtime.writeCheckpointRetrospective(currentState, retrospectiveReason);
|
|
111
|
+
}
|
|
112
|
+
return currentState;
|
|
113
|
+
}
|