@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,215 @@
|
|
|
1
|
+
import { copyFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { getMaxReviewRounds, getPhaseHeartbeatMs } from './config.js';
|
|
4
|
+
import { getHeadCommit, getWorktreeStatus } from './git.js';
|
|
5
|
+
import { createRunLogger } from './logger.js';
|
|
6
|
+
import { writeDetail } from './diagnostic.js';
|
|
7
|
+
import { logUserGuidanceApplied } from './prompts/guidance.js';
|
|
8
|
+
import { writeExecutionArtifacts } from './orchestrator/artifacts.js';
|
|
9
|
+
import { runFinalCompletionReviewPhase as runFinalCompletionReviewPhaseImpl, runExecuteFinalizationPhase as runExecuteFinalizationPhaseImpl, } from './orchestrator/completion.js';
|
|
10
|
+
import { runOnePass as runOnePassLoop, } from './orchestrator/run-loop.js';
|
|
11
|
+
import { runCoderScopePhase, runExecuteResponsePhase, } from './orchestrator/phases/coder.js';
|
|
12
|
+
import { hasPendingInteractiveBlockedRecoveryTurn, runInteractiveBlockedRecoveryPhase, } from './orchestrator/phases/recovery.js';
|
|
13
|
+
import { runAwaitingDerivedPlanExecutionPhase, runCoderPlanPhase, runPlanReviewPhase, runPlanningResponsePhase, } from './orchestrator/phases/planning.js';
|
|
14
|
+
import { runReviewPhase } from './orchestrator/phases/review.js';
|
|
15
|
+
import { writeCheckpointRetrospective } from './retrospective.js';
|
|
16
|
+
import { applyResumeActions, planResumeActions } from './resume-planner.js';
|
|
17
|
+
import { writeCurrentRunPointer } from './run-registry.js';
|
|
18
|
+
import { createInitialState, getRunStatePath, loadState, saveState } from './state.js';
|
|
19
|
+
import { getNealDir, getPlanDocumentBackupPath } from './storage-paths.js';
|
|
20
|
+
import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, formatDirtyWorktreeDiagnostic, } from './worktree-status.js';
|
|
21
|
+
export const runFinalCompletionReviewPhase = (state, statePath, logger) => runFinalCompletionReviewPhaseImpl(state, statePath, logger, {
|
|
22
|
+
writeExecutionArtifacts,
|
|
23
|
+
});
|
|
24
|
+
export const runExecuteFinalizationPhase = (state, statePath, logger) => runExecuteFinalizationPhaseImpl(state, statePath, logger, {
|
|
25
|
+
writeExecutionArtifacts,
|
|
26
|
+
});
|
|
27
|
+
function startPhaseHeartbeat(phase, getState, logger, intervalMs = getPhaseHeartbeatMs(getState().cwd)) {
|
|
28
|
+
if (!logger || intervalMs <= 0) {
|
|
29
|
+
return () => { };
|
|
30
|
+
}
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const timer = setInterval(() => {
|
|
33
|
+
const state = getState();
|
|
34
|
+
const elapsedMs = Date.now() - startedAt;
|
|
35
|
+
const payload = {
|
|
36
|
+
phase,
|
|
37
|
+
elapsedMs,
|
|
38
|
+
coderSessionHandle: state.coderSessionHandle,
|
|
39
|
+
reviewerSessionHandle: state.reviewerSessionHandle,
|
|
40
|
+
currentScopeNumber: state.currentScopeNumber,
|
|
41
|
+
topLevelMode: state.topLevelMode,
|
|
42
|
+
};
|
|
43
|
+
void logger.event('phase.heartbeat', payload);
|
|
44
|
+
writePhaseHeartbeatDetail({ state, phase, elapsedMs, logger });
|
|
45
|
+
}, intervalMs);
|
|
46
|
+
return () => clearInterval(timer);
|
|
47
|
+
}
|
|
48
|
+
export function writePhaseHeartbeatDetail(args) {
|
|
49
|
+
writeDetail(`[neal] heartbeat phase=${args.phase} elapsed=${Math.round(args.elapsedMs / 1000)}s` +
|
|
50
|
+
`${args.state.coderSessionHandle ? ` coder=${args.state.coderSessionHandle}` : ''}` +
|
|
51
|
+
`${args.state.reviewerSessionHandle ? ` reviewer=${args.state.reviewerSessionHandle}` : ''}\n`, args.logger, {
|
|
52
|
+
runId: basename(args.state.runDir),
|
|
53
|
+
phase: args.phase,
|
|
54
|
+
scopeNumber: args.state.currentScopeNumber ?? undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function initializeOrchestration(planDoc, cwd, agentConfig, topLevelMode = 'execute', options) {
|
|
58
|
+
const absolutePlanDoc = resolve(planDoc);
|
|
59
|
+
const stateDir = getNealDir(cwd);
|
|
60
|
+
const logger = await createRunLogger({
|
|
61
|
+
cwd,
|
|
62
|
+
stateDir,
|
|
63
|
+
planDoc: absolutePlanDoc,
|
|
64
|
+
topLevelMode,
|
|
65
|
+
runDir: options?.runDir,
|
|
66
|
+
});
|
|
67
|
+
const planDocBackupPath = topLevelMode === 'plan' ? await createPlanDocumentBackup(absolutePlanDoc, logger.runDir) : null;
|
|
68
|
+
const init = {
|
|
69
|
+
cwd,
|
|
70
|
+
planDoc: absolutePlanDoc,
|
|
71
|
+
planDocBackupPath,
|
|
72
|
+
stateDir,
|
|
73
|
+
runDir: logger.runDir,
|
|
74
|
+
topLevelMode,
|
|
75
|
+
allowedDirtyPaths: options?.allowedDirtyPaths ?? [],
|
|
76
|
+
agentConfig,
|
|
77
|
+
unattended: options?.unattended ?? false,
|
|
78
|
+
autoSquashOnCompletion: options?.autoSquashOnCompletion ?? true,
|
|
79
|
+
progressJsonPath: join(logger.runDir, 'plan-progress.json'),
|
|
80
|
+
progressMarkdownPath: join(logger.runDir, 'PLAN_PROGRESS.md'),
|
|
81
|
+
reviewMarkdownPath: join(logger.runDir, 'REVIEW.md'),
|
|
82
|
+
recoveryMarkdownPath: join(logger.runDir, 'RECOVERY.md'),
|
|
83
|
+
maxRounds: getMaxReviewRounds(cwd),
|
|
84
|
+
};
|
|
85
|
+
await mkdir(stateDir, { recursive: true });
|
|
86
|
+
const baseCommit = await getHeadCommit(cwd);
|
|
87
|
+
const initialState = await createInitialState(init, baseCommit);
|
|
88
|
+
const statePath = getRunStatePath(logger.runDir);
|
|
89
|
+
const savedState = await saveState(statePath, initialState);
|
|
90
|
+
await writeCurrentRunPointer(savedState);
|
|
91
|
+
await writeExecutionArtifacts(savedState);
|
|
92
|
+
await logger.event('run.initialized', {
|
|
93
|
+
statePath,
|
|
94
|
+
baseCommit,
|
|
95
|
+
topLevelMode,
|
|
96
|
+
planDocBackupPath,
|
|
97
|
+
agentConfig: savedState.agentConfig,
|
|
98
|
+
reviewMarkdownPath: savedState.reviewMarkdownPath,
|
|
99
|
+
progressJsonPath: savedState.progressJsonPath,
|
|
100
|
+
progressMarkdownPath: savedState.progressMarkdownPath,
|
|
101
|
+
});
|
|
102
|
+
await logUserGuidanceApplied(logger);
|
|
103
|
+
return {
|
|
104
|
+
state: savedState,
|
|
105
|
+
statePath,
|
|
106
|
+
logger,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function createPlanDocumentBackup(planDoc, runDir) {
|
|
110
|
+
const backupPath = getPlanDocumentBackupPath(runDir);
|
|
111
|
+
await mkdir(dirname(backupPath), { recursive: true });
|
|
112
|
+
await copyFile(planDoc, backupPath);
|
|
113
|
+
return backupPath;
|
|
114
|
+
}
|
|
115
|
+
export async function runOnePass(state, statePath, logger, options) {
|
|
116
|
+
const runPhaseWithCoderSessionHandle = async (runPhase) => {
|
|
117
|
+
const nextState = await runPhase();
|
|
118
|
+
options?.onCoderSessionHandle?.(nextState.coderSessionHandle);
|
|
119
|
+
return nextState;
|
|
120
|
+
};
|
|
121
|
+
const handlers = {
|
|
122
|
+
coder_plan: async (currentState) => runPhaseWithCoderSessionHandle(() => runCoderPlanPhase(currentState, statePath, logger)),
|
|
123
|
+
reviewer_plan: async (currentState) => runPlanReviewPhase(currentState, statePath, logger),
|
|
124
|
+
coder_plan_response: async (currentState) => runPhaseWithCoderSessionHandle(() => runPlanningResponsePhase(currentState, statePath, currentState.phase, logger)),
|
|
125
|
+
awaiting_derived_plan_execution: async (currentState) => runAwaitingDerivedPlanExecutionPhase(currentState, statePath, logger),
|
|
126
|
+
coder_scope: async (currentState) => runPhaseWithCoderSessionHandle(() => runCoderScopePhase(currentState, statePath, logger)),
|
|
127
|
+
reviewer_scope: async (currentState) => runReviewPhase(currentState, statePath, logger),
|
|
128
|
+
coder_response: async (currentState) => runPhaseWithCoderSessionHandle(() => runExecuteResponsePhase(currentState, statePath, currentState.phase, logger)),
|
|
129
|
+
interactive_blocked_recovery: async (currentState) => runPhaseWithCoderSessionHandle(() => runInteractiveBlockedRecoveryPhase(currentState, statePath, logger)),
|
|
130
|
+
execute_finalization: async (currentState) => runExecuteFinalizationPhase(currentState, statePath, logger),
|
|
131
|
+
final_completion_review: async (currentState) => runFinalCompletionReviewPhase(currentState, statePath, logger),
|
|
132
|
+
};
|
|
133
|
+
return runOnePassLoop({
|
|
134
|
+
state,
|
|
135
|
+
statePath,
|
|
136
|
+
logger,
|
|
137
|
+
options,
|
|
138
|
+
runtime: {
|
|
139
|
+
hasPendingInteractiveBlockedRecoveryTurn,
|
|
140
|
+
startPhaseHeartbeat,
|
|
141
|
+
writeCheckpointRetrospective: async (currentState, reason) => {
|
|
142
|
+
await writeCheckpointRetrospective(currentState, reason);
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
handlers,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export async function loadOrInitialize(planDoc, cwd, agentConfig, resumeStatePath, topLevelMode = 'execute', options) {
|
|
149
|
+
if (resumeStatePath) {
|
|
150
|
+
return loadRunForResume(resumeStatePath);
|
|
151
|
+
}
|
|
152
|
+
return initializeNewRun(planDoc, cwd, agentConfig, topLevelMode, options);
|
|
153
|
+
}
|
|
154
|
+
export async function loadRunForResume(resumeStatePath) {
|
|
155
|
+
const initialState = await loadState(resumeStatePath);
|
|
156
|
+
const logger = await createRunLogger({
|
|
157
|
+
cwd: initialState.cwd,
|
|
158
|
+
stateDir: dirname(resumeStatePath),
|
|
159
|
+
planDoc: initialState.planDoc,
|
|
160
|
+
topLevelMode: initialState.topLevelMode,
|
|
161
|
+
runDir: initialState.runDir,
|
|
162
|
+
resumedFromStatePath: resumeStatePath,
|
|
163
|
+
});
|
|
164
|
+
await logger.event('run.resumed', {
|
|
165
|
+
statePath: resumeStatePath,
|
|
166
|
+
phase: initialState.phase,
|
|
167
|
+
status: initialState.status,
|
|
168
|
+
agentConfig: initialState.agentConfig,
|
|
169
|
+
});
|
|
170
|
+
await logUserGuidanceApplied(logger);
|
|
171
|
+
const resumeActions = planResumeActions(initialState);
|
|
172
|
+
const state = await applyResumeActions(initialState, resumeStatePath, logger, resumeActions);
|
|
173
|
+
await writeCurrentRunPointer(state);
|
|
174
|
+
await writeExecutionArtifacts(state);
|
|
175
|
+
return {
|
|
176
|
+
state,
|
|
177
|
+
statePath: resumeStatePath,
|
|
178
|
+
logger,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export async function initializeNewRun(planDoc, cwd, agentConfig, topLevelMode = 'execute', options) {
|
|
182
|
+
if (!planDoc) {
|
|
183
|
+
throw new Error('planDoc is required when initializing a new orchestration');
|
|
184
|
+
}
|
|
185
|
+
if (topLevelMode === 'execute') {
|
|
186
|
+
const statusOutput = filterAllowedDirtyPathStatus(cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(cwd)), getExecuteAdmissionAllowedDirtyPaths(cwd, planDoc, options?.allowedDirtyPaths ?? []));
|
|
187
|
+
if (statusOutput) {
|
|
188
|
+
const diagnostic = formatDirtyWorktreeDiagnostic({
|
|
189
|
+
statusOutput,
|
|
190
|
+
expectedScratchDirs: ['.neal/runs/<run-id>/scratch/'],
|
|
191
|
+
});
|
|
192
|
+
throw new Error([
|
|
193
|
+
`Cannot start neal execute with a dirty worktree:\n${statusOutput}`,
|
|
194
|
+
diagnostic,
|
|
195
|
+
'Use neal resume for in-progress scope work, or start from a clean worktree.',
|
|
196
|
+
]
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join('\n\n'));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return initializeOrchestration(planDoc, cwd, agentConfig, topLevelMode, options);
|
|
202
|
+
}
|
|
203
|
+
function getExecuteAdmissionAllowedDirtyPaths(cwd, planDoc, allowedDirtyPaths) {
|
|
204
|
+
const expandedPaths = [...allowedDirtyPaths];
|
|
205
|
+
const absoluteCwd = resolve(cwd);
|
|
206
|
+
const absolutePlanDoc = resolve(planDoc);
|
|
207
|
+
const relativePlanDoc = relative(absoluteCwd, absolutePlanDoc);
|
|
208
|
+
if (relativePlanDoc && (relativePlanDoc.startsWith('..') || isAbsolute(relativePlanDoc))) {
|
|
209
|
+
const localPlanSurrogate = basename(absolutePlanDoc);
|
|
210
|
+
if (localPlanSurrogate && !expandedPaths.includes(localPlanSurrogate)) {
|
|
211
|
+
expandedPaths.push(localPlanSurrogate);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return expandedPaths;
|
|
215
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EXECUTE_FINALIZATION_PHASE, EXECUTE_FINALIZATION_PUBLIC_LABEL, } from './execute-finalization.js';
|
|
2
|
+
const PUBLIC_PHASE_LABELS = {
|
|
3
|
+
coder_plan: 'refining plan',
|
|
4
|
+
reviewer_plan: 'reviewing plan',
|
|
5
|
+
coder_plan_response: 'revising plan',
|
|
6
|
+
coder_plan_optional_response: 'optional plan revision',
|
|
7
|
+
awaiting_derived_plan_execution: 'ready to execute derived plan',
|
|
8
|
+
coder_scope: 'implementing current scope',
|
|
9
|
+
manual_gate: 'waiting for manual gate',
|
|
10
|
+
reviewer_scope: 'reviewing current scope',
|
|
11
|
+
coder_response: 'addressing review findings',
|
|
12
|
+
coder_optional_response: 'optional review follow-up',
|
|
13
|
+
interactive_blocked_recovery: 'waiting for recovery guidance',
|
|
14
|
+
[EXECUTE_FINALIZATION_PHASE]: EXECUTE_FINALIZATION_PUBLIC_LABEL,
|
|
15
|
+
final_completion_review: 'reviewing final completion',
|
|
16
|
+
done: 'done',
|
|
17
|
+
blocked: 'blocked',
|
|
18
|
+
};
|
|
19
|
+
export function formatPublicPhase(phase) {
|
|
20
|
+
return PUBLIC_PHASE_LABELS[phase];
|
|
21
|
+
}
|
|
22
|
+
export function formatMaybePublicPhase(value) {
|
|
23
|
+
if (!value) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return value in PUBLIC_PHASE_LABELS ? PUBLIC_PHASE_LABELS[value] : value;
|
|
27
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { getIgnoredPaths, getRepositoryRoot } from './git.js';
|
|
4
|
+
export async function inspectPlanDocDisposition(cwd, planDoc, options = {}) {
|
|
5
|
+
const repositoryRoot = await realpath(await getRepositoryRoot(cwd));
|
|
6
|
+
const absolutePlanDoc = resolve(cwd, planDoc);
|
|
7
|
+
const comparablePlanDoc = await resolvePathForRepositoryComparison(absolutePlanDoc);
|
|
8
|
+
const selectedPlanDoc = absolutePlanDoc;
|
|
9
|
+
if (!isPathInside(repositoryRoot, comparablePlanDoc)) {
|
|
10
|
+
const fileStatus = await getFileStatus(absolutePlanDoc);
|
|
11
|
+
return {
|
|
12
|
+
selectedPlanDoc,
|
|
13
|
+
absolutePlanDoc,
|
|
14
|
+
repositoryRoot,
|
|
15
|
+
repoRelativePath: null,
|
|
16
|
+
normalizedPlanDoc: absolutePlanDoc,
|
|
17
|
+
disposition: 'outside_repo',
|
|
18
|
+
eligibleForCommit: false,
|
|
19
|
+
exists: fileStatus !== 'missing',
|
|
20
|
+
isRegularFile: fileStatus === 'regular_file',
|
|
21
|
+
ignored: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const repoRelativePath = toRepoRelativePath(repositoryRoot, comparablePlanDoc);
|
|
25
|
+
const normalizedPlanDoc = repoRelativePath;
|
|
26
|
+
const fileStatus = await getFileStatus(absolutePlanDoc);
|
|
27
|
+
if (fileStatus === 'missing') {
|
|
28
|
+
return {
|
|
29
|
+
selectedPlanDoc,
|
|
30
|
+
absolutePlanDoc,
|
|
31
|
+
repositoryRoot,
|
|
32
|
+
repoRelativePath,
|
|
33
|
+
normalizedPlanDoc,
|
|
34
|
+
disposition: 'missing',
|
|
35
|
+
eligibleForCommit: false,
|
|
36
|
+
exists: false,
|
|
37
|
+
isRegularFile: false,
|
|
38
|
+
ignored: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (fileStatus !== 'regular_file') {
|
|
42
|
+
return {
|
|
43
|
+
selectedPlanDoc,
|
|
44
|
+
absolutePlanDoc,
|
|
45
|
+
repositoryRoot,
|
|
46
|
+
repoRelativePath,
|
|
47
|
+
normalizedPlanDoc,
|
|
48
|
+
disposition: 'not_regular_file',
|
|
49
|
+
eligibleForCommit: false,
|
|
50
|
+
exists: true,
|
|
51
|
+
isRegularFile: false,
|
|
52
|
+
ignored: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const ignoredPaths = await getIgnoredPaths(repositoryRoot, [repoRelativePath]);
|
|
56
|
+
const ignored = ignoredPaths.includes(repoRelativePath);
|
|
57
|
+
if (ignored) {
|
|
58
|
+
return {
|
|
59
|
+
selectedPlanDoc,
|
|
60
|
+
absolutePlanDoc,
|
|
61
|
+
repositoryRoot,
|
|
62
|
+
repoRelativePath,
|
|
63
|
+
normalizedPlanDoc,
|
|
64
|
+
disposition: 'ignored',
|
|
65
|
+
eligibleForCommit: false,
|
|
66
|
+
exists: true,
|
|
67
|
+
isRegularFile: true,
|
|
68
|
+
ignored: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const changedFiles = new Set((options.changedFiles ?? []).map((changedFile) => normalizeChangedFilePath(repositoryRoot, changedFile)));
|
|
72
|
+
const disposition = changedFiles.has(repoRelativePath) ? 'included' : 'metadata_only_clean';
|
|
73
|
+
return {
|
|
74
|
+
selectedPlanDoc,
|
|
75
|
+
absolutePlanDoc,
|
|
76
|
+
repositoryRoot,
|
|
77
|
+
repoRelativePath,
|
|
78
|
+
normalizedPlanDoc,
|
|
79
|
+
disposition,
|
|
80
|
+
eligibleForCommit: true,
|
|
81
|
+
exists: true,
|
|
82
|
+
isRegularFile: true,
|
|
83
|
+
ignored: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function toPlanDocMetadata(inspection) {
|
|
87
|
+
return {
|
|
88
|
+
selectedPlanDoc: inspection.selectedPlanDoc,
|
|
89
|
+
normalizedPlanDoc: inspection.normalizedPlanDoc,
|
|
90
|
+
planDocDisposition: inspection.disposition,
|
|
91
|
+
repoRelativePath: inspection.repoRelativePath,
|
|
92
|
+
eligibleForCommit: inspection.eligibleForCommit,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function isPathInside(parent, child) {
|
|
96
|
+
const relativePath = relative(parent, child);
|
|
97
|
+
return (relativePath === '' ||
|
|
98
|
+
(relativePath !== '..' &&
|
|
99
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
100
|
+
!isAbsolute(relativePath)));
|
|
101
|
+
}
|
|
102
|
+
function toRepoRelativePath(repositoryRoot, path) {
|
|
103
|
+
const relativePath = relative(repositoryRoot, path);
|
|
104
|
+
return toGitPath(relativePath === '' ? '.' : normalize(relativePath));
|
|
105
|
+
}
|
|
106
|
+
function normalizeChangedFilePath(repositoryRoot, path) {
|
|
107
|
+
const absolutePath = isAbsolute(path) ? resolve(path) : null;
|
|
108
|
+
if (absolutePath && isPathInside(repositoryRoot, absolutePath)) {
|
|
109
|
+
return toRepoRelativePath(repositoryRoot, absolutePath);
|
|
110
|
+
}
|
|
111
|
+
return toGitPath(normalize(path));
|
|
112
|
+
}
|
|
113
|
+
function toGitPath(path) {
|
|
114
|
+
return path.split(sep).join('/');
|
|
115
|
+
}
|
|
116
|
+
async function getFileStatus(path) {
|
|
117
|
+
try {
|
|
118
|
+
const fileStat = await stat(path);
|
|
119
|
+
return fileStat.isFile() ? 'regular_file' : 'not_regular_file';
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
123
|
+
return 'missing';
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function resolvePathForRepositoryComparison(path) {
|
|
129
|
+
const missingSegments = [];
|
|
130
|
+
let currentPath = path;
|
|
131
|
+
while (true) {
|
|
132
|
+
try {
|
|
133
|
+
const existingPath = await realpath(currentPath);
|
|
134
|
+
return missingSegments.length === 0 ? existingPath : join(existingPath, ...missingSegments.reverse());
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (!isNodeErrorWithCode(error, 'ENOENT')) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const parent = dirname(currentPath);
|
|
141
|
+
if (parent === currentPath) {
|
|
142
|
+
return path;
|
|
143
|
+
}
|
|
144
|
+
missingSegments.push(basename(currentPath));
|
|
145
|
+
currentPath = parent;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isNodeErrorWithCode(error, code) {
|
|
150
|
+
return (typeof error === 'object' &&
|
|
151
|
+
error !== null &&
|
|
152
|
+
'code' in error &&
|
|
153
|
+
error.code === code);
|
|
154
|
+
}
|