@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,218 @@
|
|
|
1
|
+
import { access, readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { writeJsonAtomic } from './atomic-write.js';
|
|
4
|
+
import { getRunStatePath, loadState } from './state.js';
|
|
5
|
+
import { getCurrentRunPointerPath as getStorageCurrentRunPointerPath, getRunDir, getRunsDir, } from './storage-paths.js';
|
|
6
|
+
const RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
7
|
+
export function getRunIdFromRunDir(runDir) {
|
|
8
|
+
const runId = basename(runDir);
|
|
9
|
+
assertValidRunId(runId);
|
|
10
|
+
return runId;
|
|
11
|
+
}
|
|
12
|
+
export function getCurrentRunPointerPath(cwd) {
|
|
13
|
+
return getStorageCurrentRunPointerPath(cwd);
|
|
14
|
+
}
|
|
15
|
+
export function getRunStatePathForRunId(cwd, runId) {
|
|
16
|
+
assertValidRunId(runId);
|
|
17
|
+
return getRunStatePath(getRunDir(cwd, runId));
|
|
18
|
+
}
|
|
19
|
+
export async function writeCurrentRunPointer(state) {
|
|
20
|
+
const pointerPath = getCurrentRunPointerPath(state.cwd);
|
|
21
|
+
const pointer = {
|
|
22
|
+
version: 1,
|
|
23
|
+
runId: getRunIdFromRunDir(state.runDir),
|
|
24
|
+
runStatePath: toRepoRelativePath(state.cwd, getRunStatePath(state.runDir)),
|
|
25
|
+
planDoc: state.planDoc,
|
|
26
|
+
topLevelMode: state.topLevelMode,
|
|
27
|
+
updatedAt: state.updatedAt,
|
|
28
|
+
};
|
|
29
|
+
await writeJsonAtomic(pointerPath, pointer);
|
|
30
|
+
}
|
|
31
|
+
export async function updateRunPointersAfterStateSave(savedPath, state) {
|
|
32
|
+
const savedAbsolutePath = resolvePath(state.cwd, savedPath);
|
|
33
|
+
const runStatePath = getRunStatePath(state.runDir);
|
|
34
|
+
if (samePath(savedAbsolutePath, runStatePath) && (await shouldRefreshCurrentPointer(state))) {
|
|
35
|
+
await writeCurrentRunPointer(state);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function resolveRunStatePath(args) {
|
|
39
|
+
const cwd = resolve(args.cwd);
|
|
40
|
+
const runId = args.runId?.trim() || null;
|
|
41
|
+
const statePath = args.statePath?.trim() || null;
|
|
42
|
+
if (runId && statePath) {
|
|
43
|
+
throw new Error('runId and statePath are mutually exclusive');
|
|
44
|
+
}
|
|
45
|
+
if (statePath) {
|
|
46
|
+
return {
|
|
47
|
+
statePath: resolvePath(cwd, statePath),
|
|
48
|
+
runId: null,
|
|
49
|
+
source: 'explicit_state_path',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (runId) {
|
|
53
|
+
if (runId === 'latest') {
|
|
54
|
+
return resolveCurrentPointer(cwd);
|
|
55
|
+
}
|
|
56
|
+
const exactStatePath = getRunStatePathForRunId(cwd, runId);
|
|
57
|
+
await requireReadableStatePath(exactStatePath, `No run state found for --run ${runId}`);
|
|
58
|
+
return {
|
|
59
|
+
statePath: exactStatePath,
|
|
60
|
+
runId,
|
|
61
|
+
source: 'explicit_run',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return resolveDefaultRunStatePath(cwd);
|
|
65
|
+
}
|
|
66
|
+
export async function listRuns(cwd) {
|
|
67
|
+
const runsDir = getRunsDir(cwd);
|
|
68
|
+
const entries = await readdir(runsDir, { withFileTypes: true }).catch((error) => {
|
|
69
|
+
if (isNotFoundError(error)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
});
|
|
74
|
+
const summaries = [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory() || !isValidRunId(entry.name)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const statePath = getRunStatePathForRunId(cwd, entry.name);
|
|
80
|
+
let state;
|
|
81
|
+
try {
|
|
82
|
+
state = await loadState(statePath);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
summaries.push({
|
|
88
|
+
runId: entry.name,
|
|
89
|
+
runDir: join(runsDir, entry.name),
|
|
90
|
+
statePath,
|
|
91
|
+
planDoc: state.planDoc,
|
|
92
|
+
topLevelMode: state.topLevelMode,
|
|
93
|
+
status: state.status,
|
|
94
|
+
phase: state.phase,
|
|
95
|
+
createdAt: state.createdAt,
|
|
96
|
+
updatedAt: state.updatedAt,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return summaries.sort(compareRunSummariesNewestFirst);
|
|
100
|
+
}
|
|
101
|
+
function compareRunSummariesNewestFirst(left, right) {
|
|
102
|
+
return (compareIsoTimestampDescending(left.updatedAt, right.updatedAt) ||
|
|
103
|
+
compareIsoTimestampDescending(left.createdAt, right.createdAt) ||
|
|
104
|
+
right.runId.localeCompare(left.runId));
|
|
105
|
+
}
|
|
106
|
+
function compareIsoTimestampDescending(left, right) {
|
|
107
|
+
const leftMs = Date.parse(left);
|
|
108
|
+
const rightMs = Date.parse(right);
|
|
109
|
+
if (Number.isFinite(leftMs) && Number.isFinite(rightMs) && leftMs !== rightMs) {
|
|
110
|
+
return rightMs - leftMs;
|
|
111
|
+
}
|
|
112
|
+
if (left !== right) {
|
|
113
|
+
return right.localeCompare(left);
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
async function resolveDefaultRunStatePath(cwd) {
|
|
118
|
+
const current = await tryReadCurrentRunPointer(cwd);
|
|
119
|
+
if (!current) {
|
|
120
|
+
throw new Error('No .neal/current.json pointer found; run neal status --all to choose an explicit run id');
|
|
121
|
+
}
|
|
122
|
+
const statePath = resolvePath(cwd, current.runStatePath);
|
|
123
|
+
await requireReadableStatePath(statePath, 'Current Neal run state is missing');
|
|
124
|
+
return {
|
|
125
|
+
statePath,
|
|
126
|
+
runId: current.runId,
|
|
127
|
+
source: 'current_pointer',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async function resolveCurrentPointer(cwd) {
|
|
131
|
+
const pointer = await tryReadCurrentRunPointer(cwd);
|
|
132
|
+
if (!pointer) {
|
|
133
|
+
throw new Error('No .neal/current.json pointer found for --run latest; use neal status --all to choose an explicit run id');
|
|
134
|
+
}
|
|
135
|
+
const statePath = resolvePath(cwd, pointer.runStatePath);
|
|
136
|
+
await requireReadableStatePath(statePath, `Current Neal run state is missing for --run ${pointer.runId}`);
|
|
137
|
+
return {
|
|
138
|
+
statePath,
|
|
139
|
+
runId: pointer.runId,
|
|
140
|
+
source: 'current_pointer',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function shouldRefreshCurrentPointer(state) {
|
|
144
|
+
const current = await tryReadCurrentRunPointer(state.cwd);
|
|
145
|
+
if (!current) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return current.runId === getRunIdFromRunDir(state.runDir);
|
|
149
|
+
}
|
|
150
|
+
async function tryReadCurrentRunPointer(cwd) {
|
|
151
|
+
const pointerPath = getCurrentRunPointerPath(cwd);
|
|
152
|
+
let content;
|
|
153
|
+
try {
|
|
154
|
+
content = await readFile(pointerPath, 'utf8');
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (isNotFoundError(error)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
return parseCurrentRunPointer(JSON.parse(content));
|
|
163
|
+
}
|
|
164
|
+
function parseCurrentRunPointer(value) {
|
|
165
|
+
if (!value || typeof value !== 'object') {
|
|
166
|
+
throw new Error('Invalid .neal/current.json: expected object');
|
|
167
|
+
}
|
|
168
|
+
const pointer = value;
|
|
169
|
+
if (pointer.version !== 1 ||
|
|
170
|
+
typeof pointer.runId !== 'string' ||
|
|
171
|
+
!isValidRunId(pointer.runId) ||
|
|
172
|
+
typeof pointer.runStatePath !== 'string' ||
|
|
173
|
+
typeof pointer.planDoc !== 'string' ||
|
|
174
|
+
(pointer.topLevelMode !== 'plan' && pointer.topLevelMode !== 'execute') ||
|
|
175
|
+
typeof pointer.updatedAt !== 'string') {
|
|
176
|
+
throw new Error('Invalid .neal/current.json: malformed current run pointer');
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
version: 1,
|
|
180
|
+
runId: pointer.runId,
|
|
181
|
+
runStatePath: pointer.runStatePath,
|
|
182
|
+
planDoc: pointer.planDoc,
|
|
183
|
+
topLevelMode: pointer.topLevelMode,
|
|
184
|
+
updatedAt: pointer.updatedAt,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function assertValidRunId(runId) {
|
|
188
|
+
if (!isValidRunId(runId)) {
|
|
189
|
+
throw new Error(`Invalid Neal run id: ${runId}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function isValidRunId(runId) {
|
|
193
|
+
return RUN_ID_PATTERN.test(runId) && runId !== '.' && runId !== '..' && !runId.includes('..');
|
|
194
|
+
}
|
|
195
|
+
async function requireReadableStatePath(statePath, message) {
|
|
196
|
+
try {
|
|
197
|
+
await access(statePath);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (isNotFoundError(error)) {
|
|
201
|
+
throw new Error(`${message}: ${statePath}`);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function resolvePath(cwd, path) {
|
|
207
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
208
|
+
}
|
|
209
|
+
function samePath(left, right) {
|
|
210
|
+
return resolve(left) === resolve(right);
|
|
211
|
+
}
|
|
212
|
+
function toRepoRelativePath(cwd, path) {
|
|
213
|
+
const relativePath = relative(cwd, path);
|
|
214
|
+
return relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath) ? relativePath : path;
|
|
215
|
+
}
|
|
216
|
+
function isNotFoundError(error) {
|
|
217
|
+
return Boolean(error && typeof error === 'object' && error.code === 'ENOENT');
|
|
218
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getPublicLifecycleView, hasPendingOperatorGuidance as hasPendingOperatorGuidanceFromStateView, } from './state-views.js';
|
|
2
|
+
export function hasPendingOperatorGuidance(state) {
|
|
3
|
+
return hasPendingOperatorGuidanceFromStateView(state);
|
|
4
|
+
}
|
|
5
|
+
export function getRunDisplayStatus(state) {
|
|
6
|
+
const lifecycle = getPublicLifecycleView(state);
|
|
7
|
+
return {
|
|
8
|
+
effectiveStatus: lifecycle.lifecycle === 'waiting_for_manual_gate'
|
|
9
|
+
? 'waiting_for_manual_gate'
|
|
10
|
+
: lifecycle.lifecycle === 'waiting_for_guidance'
|
|
11
|
+
? 'waiting_for_operator'
|
|
12
|
+
: state.status,
|
|
13
|
+
waitingForOperatorGuidance: lifecycle.waitingForOperatorGuidance,
|
|
14
|
+
pendingOperatorGuidance: lifecycle.pendingOperatorGuidance,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function formatPublicRunStatus(displayStatus) {
|
|
18
|
+
if (displayStatus.effectiveStatus === 'waiting_for_manual_gate') {
|
|
19
|
+
return 'waiting_for_manual_gate';
|
|
20
|
+
}
|
|
21
|
+
if (displayStatus.waitingForOperatorGuidance) {
|
|
22
|
+
return 'waiting_for_guidance';
|
|
23
|
+
}
|
|
24
|
+
return displayStatus.effectiveStatus;
|
|
25
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { validatePlanDocument } from './plan-validation.js';
|
|
3
|
+
import { getDerivedPlanIdentityView } from './state-views.js';
|
|
4
|
+
export const DEFAULT_PARENT_OBJECTIVE_HISTORY_WINDOW = 5;
|
|
5
|
+
const EXECUTION_PLAN_SCOPE_DESCRIPTOR_CACHE_LIMIT = 32;
|
|
6
|
+
const SCOPE_HEADING_TITLE_PATTERN = /^### Scope (\d+):(.*)$/gm;
|
|
7
|
+
const executionPlanDescriptorCache = new Map();
|
|
8
|
+
export function isExecutingDerivedPlan(state) {
|
|
9
|
+
return getDerivedPlanIdentityView(state)?.executing ?? false;
|
|
10
|
+
}
|
|
11
|
+
export function getParentScopeLabel(state) {
|
|
12
|
+
return String(state.derivedFromScopeNumber ?? state.currentScopeNumber);
|
|
13
|
+
}
|
|
14
|
+
export function getCurrentScopeLabel(state) {
|
|
15
|
+
const derivedPlan = getDerivedPlanIdentityView(state);
|
|
16
|
+
if (derivedPlan?.executing) {
|
|
17
|
+
return `${getParentScopeLabel(state)}.${derivedPlan.scopeIndex}`;
|
|
18
|
+
}
|
|
19
|
+
return String(state.currentScopeNumber);
|
|
20
|
+
}
|
|
21
|
+
export function getExecutionPlanPath(state) {
|
|
22
|
+
const derivedPlan = getDerivedPlanIdentityView(state);
|
|
23
|
+
return derivedPlan?.executing ? derivedPlan.path : state.planDoc;
|
|
24
|
+
}
|
|
25
|
+
export function getCompletedScopeParentObjective(scope) {
|
|
26
|
+
return scope.derivedFromParentScope ?? scope.number;
|
|
27
|
+
}
|
|
28
|
+
export function shouldAdvanceTopLevelScopeNumber(state) {
|
|
29
|
+
return state.executionShape !== 'one_shot';
|
|
30
|
+
}
|
|
31
|
+
export function shouldContinueTopLevelExecutionAfterAcceptedScope(state) {
|
|
32
|
+
if (state.lastScopeMarker === 'AUTONOMY_DONE' || state.lastScopeMarker === 'AUTONOMY_BLOCKED') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return shouldAdvanceTopLevelScopeNumber(state);
|
|
36
|
+
}
|
|
37
|
+
export function getExecutionPlanScopeCountForShape(executionShape, options) {
|
|
38
|
+
if (executionShape === 'one_shot') {
|
|
39
|
+
return { kind: 'known', total: 1 };
|
|
40
|
+
}
|
|
41
|
+
if (executionShape === 'multi_scope_unknown') {
|
|
42
|
+
return { kind: 'unknown_by_contract' };
|
|
43
|
+
}
|
|
44
|
+
if (executionShape === 'multi_scope') {
|
|
45
|
+
const knownTotal = options?.knownTotal ?? null;
|
|
46
|
+
if (typeof knownTotal === 'number' && Number.isFinite(knownTotal) && knownTotal > 0) {
|
|
47
|
+
return { kind: 'known', total: knownTotal };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { kind: 'unavailable' };
|
|
51
|
+
}
|
|
52
|
+
export async function getExecutionPlanScopeCount(planPath) {
|
|
53
|
+
const data = await getExecutionPlanDescriptorData(planPath);
|
|
54
|
+
return data?.scopeCount ?? { kind: 'unavailable' };
|
|
55
|
+
}
|
|
56
|
+
export async function getCurrentExecutionScopeDescriptor(state) {
|
|
57
|
+
const planPath = getExecutionPlanPath(state);
|
|
58
|
+
const data = await getExecutionPlanDescriptorData(planPath);
|
|
59
|
+
const executionShape = data?.executionShape ?? state.executionShape;
|
|
60
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
61
|
+
const planScopeNumber = getCurrentPlanScopeNumber(state, executionShape);
|
|
62
|
+
const scopeCount = data?.scopeCount ?? getExecutionPlanScopeCountForShape(executionShape);
|
|
63
|
+
const title = getExecutionPlanScopeTitle(data, planScopeNumber);
|
|
64
|
+
return {
|
|
65
|
+
planPath,
|
|
66
|
+
scopeLabel,
|
|
67
|
+
planScopeNumber,
|
|
68
|
+
scopeCount,
|
|
69
|
+
title,
|
|
70
|
+
display: formatExecutionPlanScopeDisplay({
|
|
71
|
+
scopeLabel,
|
|
72
|
+
scopeCount,
|
|
73
|
+
title,
|
|
74
|
+
includeScopeCount: Boolean(title) && !isExecutingDerivedPlan(state),
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function getCurrentPlanScopeNumber(state, executionShape) {
|
|
79
|
+
const derivedPlan = getDerivedPlanIdentityView(state);
|
|
80
|
+
if (derivedPlan?.executing) {
|
|
81
|
+
return derivedPlan.scopeIndex ?? 1;
|
|
82
|
+
}
|
|
83
|
+
if (executionShape === 'one_shot') {
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
return state.currentScopeNumber;
|
|
87
|
+
}
|
|
88
|
+
function getExecutionPlanScopeTitle(data, planScopeNumber) {
|
|
89
|
+
if (!data) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (data.executionShape === 'multi_scope_unknown') {
|
|
93
|
+
return 'Recurring Scope';
|
|
94
|
+
}
|
|
95
|
+
return data.scopeTitles.get(planScopeNumber) ?? null;
|
|
96
|
+
}
|
|
97
|
+
function formatExecutionPlanScopeDisplay(args) {
|
|
98
|
+
if (!args.title) {
|
|
99
|
+
return `scope ${args.scopeLabel}`;
|
|
100
|
+
}
|
|
101
|
+
const scopeSuffix = args.includeScopeCount && args.scopeCount.kind === 'known' ? `/${args.scopeCount.total}` : '';
|
|
102
|
+
return `scope ${args.scopeLabel}${scopeSuffix}: ${args.title}`;
|
|
103
|
+
}
|
|
104
|
+
async function getExecutionPlanDescriptorData(planPath) {
|
|
105
|
+
try {
|
|
106
|
+
const planStat = await stat(planPath);
|
|
107
|
+
const cached = executionPlanDescriptorCache.get(planPath);
|
|
108
|
+
if (cached && cached.mtimeMs === planStat.mtimeMs && cached.size === planStat.size) {
|
|
109
|
+
return cached.data;
|
|
110
|
+
}
|
|
111
|
+
const planDocument = await readFile(planPath, 'utf8');
|
|
112
|
+
const data = parseExecutionPlanDescriptorData(planDocument);
|
|
113
|
+
cacheExecutionPlanDescriptorData(planPath, {
|
|
114
|
+
mtimeMs: planStat.mtimeMs,
|
|
115
|
+
size: planStat.size,
|
|
116
|
+
data,
|
|
117
|
+
});
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function cacheExecutionPlanDescriptorData(planPath, data) {
|
|
125
|
+
if (executionPlanDescriptorCache.has(planPath)) {
|
|
126
|
+
executionPlanDescriptorCache.delete(planPath);
|
|
127
|
+
}
|
|
128
|
+
executionPlanDescriptorCache.set(planPath, data);
|
|
129
|
+
while (executionPlanDescriptorCache.size > EXECUTION_PLAN_SCOPE_DESCRIPTOR_CACHE_LIMIT) {
|
|
130
|
+
const oldestKey = executionPlanDescriptorCache.keys().next().value;
|
|
131
|
+
if (!oldestKey) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
executionPlanDescriptorCache.delete(oldestKey);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function parseExecutionPlanDescriptorData(planDocument) {
|
|
138
|
+
const validation = validatePlanDocument(planDocument);
|
|
139
|
+
if (!validation.ok) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const normalizedDocument = validation.normalization.normalizedDocument;
|
|
143
|
+
if (validation.executionShape === 'one_shot') {
|
|
144
|
+
const oneShotTitle = parseScopeHeadings(normalizedDocument).titles.get(1) ??
|
|
145
|
+
getFirstLevelOneMarkdownTitle(normalizedDocument) ??
|
|
146
|
+
'single-scope plan';
|
|
147
|
+
return {
|
|
148
|
+
executionShape: validation.executionShape,
|
|
149
|
+
scopeCount: { kind: 'known', total: 1 },
|
|
150
|
+
scopeTitles: new Map([[1, oneShotTitle]]),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (validation.executionShape === 'multi_scope_unknown') {
|
|
154
|
+
return {
|
|
155
|
+
executionShape: validation.executionShape,
|
|
156
|
+
scopeCount: { kind: 'unknown_by_contract' },
|
|
157
|
+
scopeTitles: new Map(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const scopeHeadings = parseScopeHeadings(normalizedDocument);
|
|
161
|
+
if (scopeHeadings.count === 0) {
|
|
162
|
+
return {
|
|
163
|
+
executionShape: validation.executionShape,
|
|
164
|
+
scopeCount: { kind: 'unavailable' },
|
|
165
|
+
scopeTitles: scopeHeadings.titles,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
executionShape: validation.executionShape,
|
|
170
|
+
scopeCount: { kind: 'known', total: scopeHeadings.count },
|
|
171
|
+
scopeTitles: scopeHeadings.titles,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function parseScopeHeadings(planDocument) {
|
|
175
|
+
const titles = new Map();
|
|
176
|
+
let count = 0;
|
|
177
|
+
for (const match of planDocument.matchAll(SCOPE_HEADING_TITLE_PATTERN)) {
|
|
178
|
+
const scopeNumber = Number(match[1]);
|
|
179
|
+
const title = match[2]?.trim() ?? '';
|
|
180
|
+
if (Number.isSafeInteger(scopeNumber) && scopeNumber > 0) {
|
|
181
|
+
count += 1;
|
|
182
|
+
if (title !== '') {
|
|
183
|
+
titles.set(scopeNumber, title);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { count, titles };
|
|
188
|
+
}
|
|
189
|
+
function getFirstLevelOneMarkdownTitle(planDocument) {
|
|
190
|
+
for (const line of planDocument.split(/\r?\n/)) {
|
|
191
|
+
const match = line.trim().match(/^#\s+(.+)$/);
|
|
192
|
+
const title = match?.[1]?.trim() ?? '';
|
|
193
|
+
if (title !== '') {
|
|
194
|
+
return title;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
export function renderScopeProgressSegments(state, scopeCount) {
|
|
200
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
201
|
+
const scopeSuffix = scopeCount.kind === 'known' ? `/${scopeCount.total}` : '';
|
|
202
|
+
const scopeSegment = `scope ${scopeLabel}${scopeSuffix}`;
|
|
203
|
+
const derivedPlan = getDerivedPlanIdentityView(state);
|
|
204
|
+
if (!derivedPlan?.executing) {
|
|
205
|
+
return {
|
|
206
|
+
scopeSegment,
|
|
207
|
+
derivedSegment: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const derivedIndex = derivedPlan.scopeIndex ?? 1;
|
|
211
|
+
const derivedSuffix = scopeCount.kind === 'known' ? `/${scopeCount.total}` : '';
|
|
212
|
+
return {
|
|
213
|
+
scopeSegment: `scope ${scopeLabel}`,
|
|
214
|
+
derivedSegment: `derived ${derivedIndex}${derivedSuffix}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function renderScopeProgressSummary(state, scopeCount) {
|
|
218
|
+
const { scopeSegment, derivedSegment } = renderScopeProgressSegments(state, scopeCount);
|
|
219
|
+
return derivedSegment ? `${scopeSegment} | ${derivedSegment}` : scopeSegment;
|
|
220
|
+
}
|
|
221
|
+
export function getRecentAcceptedScopesForParentObjective(state, parentScopeLabel, window = DEFAULT_PARENT_OBJECTIVE_HISTORY_WINDOW) {
|
|
222
|
+
return state.completedScopes
|
|
223
|
+
.filter((scope) => scope.result === 'accepted' && getCompletedScopeParentObjective(scope) === parentScopeLabel)
|
|
224
|
+
.filter((scope) => !(scope.derivedFromParentScope === null && scope.replacedByDerivedPlanPath))
|
|
225
|
+
.slice(-Math.max(window, 0));
|
|
226
|
+
}
|
|
227
|
+
export function getAcceptedDerivedScopesForParentObjective(state, parentScopeLabel) {
|
|
228
|
+
return state.completedScopes.filter((scope) => scope.result === 'accepted' &&
|
|
229
|
+
scope.derivedFromParentScope === parentScopeLabel);
|
|
230
|
+
}
|
|
231
|
+
export function getAcceptedParentScopeForObjective(state, parentScopeLabel) {
|
|
232
|
+
return state.completedScopes.find((scope) => scope.result === 'accepted' &&
|
|
233
|
+
scope.number === parentScopeLabel &&
|
|
234
|
+
scope.derivedFromParentScope === null) ?? null;
|
|
235
|
+
}
|
|
236
|
+
export function getSubstantiveAcceptedDerivedScopesForParentObjective(state, parentScopeLabel) {
|
|
237
|
+
return getAcceptedDerivedScopesForParentObjective(state, parentScopeLabel)
|
|
238
|
+
.filter((scope) => scope.changedFiles.length > 0);
|
|
239
|
+
}
|
|
240
|
+
export function getEmptyAcceptedDerivedScopesForParentObjective(state, parentScopeLabel) {
|
|
241
|
+
return getAcceptedDerivedScopesForParentObjective(state, parentScopeLabel)
|
|
242
|
+
.filter((scope) => scope.changedFiles.length === 0);
|
|
243
|
+
}
|
|
244
|
+
export function aggregateChangedFilesForAcceptedDerivedScopes(scopes) {
|
|
245
|
+
return [...new Set(scopes.flatMap((scope) => scope.changedFiles))];
|
|
246
|
+
}
|
|
247
|
+
export function classifyEmptyDerivedParentAdvance(args) {
|
|
248
|
+
const parentScopeLabel = getParentScopeLabel(args.state);
|
|
249
|
+
const currentScopeLabel = getCurrentScopeLabel(args.state);
|
|
250
|
+
const substantiveScopes = getSubstantiveAcceptedDerivedScopesForParentObjective(args.state, parentScopeLabel);
|
|
251
|
+
const emptyScopes = getEmptyAcceptedDerivedScopesForParentObjective(args.state, parentScopeLabel);
|
|
252
|
+
const aggregateChangedFiles = aggregateChangedFilesForAcceptedDerivedScopes(substantiveScopes);
|
|
253
|
+
const acceptedParentScope = getAcceptedParentScopeForObjective(args.state, parentScopeLabel);
|
|
254
|
+
const failedPreconditions = [];
|
|
255
|
+
const derivedPlan = getDerivedPlanIdentityView(args.state);
|
|
256
|
+
if (args.source === 'explicit' && args.reviewerAction !== 'advance_parent') {
|
|
257
|
+
failedPreconditions.push('reviewer action is not advance_parent for explicit parent advancement');
|
|
258
|
+
}
|
|
259
|
+
if (args.source === 'fallback' && args.reviewerAction !== 'block_for_operator') {
|
|
260
|
+
failedPreconditions.push('reviewer action is not block_for_operator for fallback parent advancement');
|
|
261
|
+
}
|
|
262
|
+
if (args.state.topLevelMode !== 'execute') {
|
|
263
|
+
failedPreconditions.push('run is not execute mode');
|
|
264
|
+
}
|
|
265
|
+
if (!derivedPlan?.executing) {
|
|
266
|
+
failedPreconditions.push('accepted derived plan is not actively executing');
|
|
267
|
+
}
|
|
268
|
+
if (args.state.executionShape === 'one_shot') {
|
|
269
|
+
failedPreconditions.push('one-shot execution shape cannot advance a parent objective through this path');
|
|
270
|
+
}
|
|
271
|
+
else if (args.state.executionShape !== 'multi_scope' && args.state.executionShape !== 'multi_scope_unknown') {
|
|
272
|
+
failedPreconditions.push('execution shape is not multi_scope or multi_scope_unknown');
|
|
273
|
+
}
|
|
274
|
+
if (args.currentChangedFiles.length > 0) {
|
|
275
|
+
failedPreconditions.push(`current scope changed-file list is not empty (${args.currentChangedFiles.length})`);
|
|
276
|
+
}
|
|
277
|
+
if (args.currentReviewerFindings.length > 0) {
|
|
278
|
+
failedPreconditions.push(`current reviewer result has findings (${args.currentReviewerFindings.length})`);
|
|
279
|
+
}
|
|
280
|
+
if (args.mergedFindings.some((finding) => finding.status === 'open')) {
|
|
281
|
+
failedPreconditions.push('merged review findings still contain open findings');
|
|
282
|
+
}
|
|
283
|
+
if (substantiveScopes.length === 0) {
|
|
284
|
+
failedPreconditions.push('parent objective has no prior substantive accepted derived sub-scope');
|
|
285
|
+
}
|
|
286
|
+
if (args.source === 'fallback' && emptyScopes.length < 2) {
|
|
287
|
+
failedPreconditions.push('fallback requires at least two prior empty accepted derived sub-scopes');
|
|
288
|
+
}
|
|
289
|
+
if (acceptedParentScope) {
|
|
290
|
+
failedPreconditions.push(`parent objective ${parentScopeLabel} already has an accepted top-level record`);
|
|
291
|
+
}
|
|
292
|
+
if (args.source === 'explicit' && args.reviewerRationale.trim() === '') {
|
|
293
|
+
failedPreconditions.push('reviewer rationale is empty for explicit parent advancement');
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
eligible: failedPreconditions.length === 0,
|
|
297
|
+
failedPreconditions,
|
|
298
|
+
parentScopeLabel,
|
|
299
|
+
currentScopeLabel,
|
|
300
|
+
priorSubstantiveCount: substantiveScopes.length,
|
|
301
|
+
priorEmptyCount: emptyScopes.length,
|
|
302
|
+
aggregateChangedFiles,
|
|
303
|
+
source: args.source,
|
|
304
|
+
reviewerAction: args.reviewerAction,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const EXECUTE_PROGRESS_JUSTIFICATION_FIELDS = [
|
|
308
|
+
'milestoneTargeted',
|
|
309
|
+
'newEvidence',
|
|
310
|
+
'whyNotRedundant',
|
|
311
|
+
'nextStepUnlocked',
|
|
312
|
+
];
|
|
313
|
+
export function classifyAlreadySatisfiedTopLevelScopeAcceptance(args) {
|
|
314
|
+
const scopeLabel = getCurrentScopeLabel(args.state);
|
|
315
|
+
const parentScopeLabel = getParentScopeLabel(args.state);
|
|
316
|
+
const derivedPlan = getDerivedPlanIdentityView(args.state);
|
|
317
|
+
const acceptedParentScope = getAcceptedParentScopeForObjective(args.state, parentScopeLabel);
|
|
318
|
+
const priorAcceptedScopeLabels = args.state.completedScopes
|
|
319
|
+
.filter((scope) => scope.result === 'accepted')
|
|
320
|
+
.filter((scope) => !(scope.number === parentScopeLabel && scope.derivedFromParentScope === null))
|
|
321
|
+
.map((scope) => scope.number);
|
|
322
|
+
const failedPreconditions = [];
|
|
323
|
+
if (args.reviewerAction !== 'advance_parent') {
|
|
324
|
+
failedPreconditions.push('reviewer action is not advance_parent for top-level already-satisfied acceptance');
|
|
325
|
+
}
|
|
326
|
+
if (args.state.topLevelMode !== 'execute') {
|
|
327
|
+
failedPreconditions.push('run is not execute mode');
|
|
328
|
+
}
|
|
329
|
+
if (scopeLabel !== parentScopeLabel) {
|
|
330
|
+
failedPreconditions.push(`current scope ${scopeLabel} is not top-level parent scope ${parentScopeLabel}`);
|
|
331
|
+
}
|
|
332
|
+
if (derivedPlan?.executing) {
|
|
333
|
+
failedPreconditions.push('accepted derived plan is actively executing');
|
|
334
|
+
}
|
|
335
|
+
if (args.state.executionShape === 'one_shot') {
|
|
336
|
+
failedPreconditions.push('one-shot execution shape cannot use top-level already-satisfied acceptance');
|
|
337
|
+
}
|
|
338
|
+
else if (args.state.executionShape !== 'multi_scope' && args.state.executionShape !== 'multi_scope_unknown') {
|
|
339
|
+
failedPreconditions.push('execution shape is not multi_scope or multi_scope_unknown');
|
|
340
|
+
}
|
|
341
|
+
if (!args.currentChangedFiles) {
|
|
342
|
+
failedPreconditions.push('current scope changed-file list is unavailable');
|
|
343
|
+
}
|
|
344
|
+
else if (args.currentChangedFiles.length > 0) {
|
|
345
|
+
failedPreconditions.push(`current scope changed-file list is not empty (${args.currentChangedFiles.length})`);
|
|
346
|
+
}
|
|
347
|
+
if (args.currentReviewerFindings.length > 0) {
|
|
348
|
+
failedPreconditions.push(`current reviewer result has findings (${args.currentReviewerFindings.length})`);
|
|
349
|
+
}
|
|
350
|
+
if (args.mergedFindings.some((finding) => finding.status === 'open')) {
|
|
351
|
+
failedPreconditions.push('merged review findings still contain open findings');
|
|
352
|
+
}
|
|
353
|
+
if (args.reviewerRationale.trim() === '') {
|
|
354
|
+
failedPreconditions.push('reviewer rationale is empty for top-level already-satisfied acceptance');
|
|
355
|
+
}
|
|
356
|
+
const progressJustification = args.state.currentScopeProgressJustification;
|
|
357
|
+
if (!progressJustification) {
|
|
358
|
+
failedPreconditions.push('current scope progress justification is missing');
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
const emptyProgressFields = EXECUTE_PROGRESS_JUSTIFICATION_FIELDS.filter((field) => progressJustification[field].trim() === '');
|
|
362
|
+
if (emptyProgressFields.length > 0) {
|
|
363
|
+
failedPreconditions.push(`current scope progress justification has empty field(s): ${emptyProgressFields.join(', ')}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (priorAcceptedScopeLabels.length === 0) {
|
|
367
|
+
failedPreconditions.push('no prior accepted completed scope exists before the current top-level scope');
|
|
368
|
+
}
|
|
369
|
+
if (acceptedParentScope) {
|
|
370
|
+
failedPreconditions.push(`top-level scope ${parentScopeLabel} already has an accepted completed-scope record`);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
eligible: failedPreconditions.length === 0,
|
|
374
|
+
failedPreconditions,
|
|
375
|
+
scopeLabel,
|
|
376
|
+
priorAcceptedScopeLabels,
|
|
377
|
+
reviewerAction: args.reviewerAction,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
export function renderRecentAcceptedScopesSummary(state, parentScopeLabel, window = DEFAULT_PARENT_OBJECTIVE_HISTORY_WINDOW) {
|
|
381
|
+
const recentScopes = getRecentAcceptedScopesForParentObjective(state, parentScopeLabel, window);
|
|
382
|
+
if (recentScopes.length === 0) {
|
|
383
|
+
return `No accepted scopes have been recorded yet for parent objective ${parentScopeLabel}.`;
|
|
384
|
+
}
|
|
385
|
+
const touchedFileCounts = new Map();
|
|
386
|
+
for (const scope of recentScopes) {
|
|
387
|
+
for (const file of scope.changedFiles) {
|
|
388
|
+
touchedFileCounts.set(file, (touchedFileCounts.get(file) ?? 0) + 1);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const concentrationSummary = touchedFileCounts.size === 0
|
|
392
|
+
? '(no changed files recorded)'
|
|
393
|
+
: [...touchedFileCounts.entries()]
|
|
394
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
395
|
+
.map(([file, touches]) => `${file} (${touches}/${recentScopes.length} scopes)`)
|
|
396
|
+
.join(', ');
|
|
397
|
+
return [
|
|
398
|
+
`Accepted scope history for parent objective ${parentScopeLabel} (oldest to newest, last ${window} max):`,
|
|
399
|
+
...recentScopes.map((scope) => {
|
|
400
|
+
const changedFiles = scope.changedFiles.length > 0 ? scope.changedFiles.join(', ') : '(no changed files)';
|
|
401
|
+
return [
|
|
402
|
+
`- Scope ${scope.number}`,
|
|
403
|
+
` commit: ${scope.finalCommit ?? 'pending'}`,
|
|
404
|
+
` subject: ${scope.commitSubject ?? 'pending'}`,
|
|
405
|
+
` parentScope: ${scope.derivedFromParentScope ?? 'none'}`,
|
|
406
|
+
` changedFiles: ${changedFiles}`,
|
|
407
|
+
].join('\n');
|
|
408
|
+
}),
|
|
409
|
+
`Touched-file concentration: ${concentrationSummary}`,
|
|
410
|
+
].join('\n');
|
|
411
|
+
}
|
|
412
|
+
export function buildScopeAccountingSummary(scopes) {
|
|
413
|
+
const acceptedScopes = scopes.filter((scope) => scope.result === 'accepted');
|
|
414
|
+
const blockedScopes = scopes.filter((scope) => scope.result === 'blocked');
|
|
415
|
+
const acceptedTopLevelScopes = acceptedScopes.filter((scope) => scope.derivedFromParentScope === null);
|
|
416
|
+
const acceptedDerivedSubScopes = acceptedScopes.filter((scope) => scope.derivedFromParentScope !== null);
|
|
417
|
+
const blockedTopLevelScopes = blockedScopes.filter((scope) => scope.derivedFromParentScope === null);
|
|
418
|
+
const blockedDerivedSubScopes = blockedScopes.filter((scope) => scope.derivedFromParentScope !== null);
|
|
419
|
+
const replacedParentScopes = scopes.flatMap((scope) => {
|
|
420
|
+
if (!scope.replacedByDerivedPlanPath) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
return [
|
|
424
|
+
{
|
|
425
|
+
number: scope.number,
|
|
426
|
+
derivedPlanPath: scope.replacedByDerivedPlanPath,
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
});
|
|
430
|
+
const replacementSummary = replacedParentScopes.length === 0
|
|
431
|
+
? 'no parent scopes were replaced by derived plans'
|
|
432
|
+
: `parent scope replacement(s): ${replacedParentScopes
|
|
433
|
+
.map((scope) => `${scope.number} -> ${scope.derivedPlanPath}`)
|
|
434
|
+
.join('; ')}`;
|
|
435
|
+
return {
|
|
436
|
+
acceptedScopeRecords: acceptedScopes.length,
|
|
437
|
+
acceptedTopLevelScopeRecords: acceptedTopLevelScopes.length,
|
|
438
|
+
acceptedDerivedSubScopeRecords: acceptedDerivedSubScopes.length,
|
|
439
|
+
blockedScopeRecords: blockedScopes.length,
|
|
440
|
+
blockedTopLevelScopeRecords: blockedTopLevelScopes.length,
|
|
441
|
+
blockedDerivedSubScopeRecords: blockedDerivedSubScopes.length,
|
|
442
|
+
replacedParentScopes,
|
|
443
|
+
summary: `Accepted scope records: ${acceptedScopes.length} total ` +
|
|
444
|
+
`(${acceptedTopLevelScopes.length} top-level parent/objective record(s), ` +
|
|
445
|
+
`${acceptedDerivedSubScopes.length} derived sub-scope record(s)). ` +
|
|
446
|
+
`Blocked scope records: ${blockedScopes.length} total ` +
|
|
447
|
+
`(${blockedTopLevelScopes.length} top-level, ${blockedDerivedSubScopes.length} derived). ` +
|
|
448
|
+
`Top-level records are the comparable count against the original plan; accepted scope records include derived sub-scopes. ` +
|
|
449
|
+
replacementSummary,
|
|
450
|
+
};
|
|
451
|
+
}
|