@navels/neal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,166 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { getFinalCompletionView } from '../state-views.js';
4
+ export const REVIEWER_CONTEXT_JSON = 'REVIEWER_CONTEXT.json';
5
+ export const REVIEWER_CONTEXT_MARKDOWN = 'REVIEWER_CONTEXT.md';
6
+ const COMPLETED_SCOPE_LIMIT = 12;
7
+ const FINDING_LIMIT = 24;
8
+ export async function buildAndPersistReviewerContextPacket(args) {
9
+ const packet = buildReviewerContextPacket(args);
10
+ await mkdir(args.state.runDir, { recursive: true });
11
+ await writeFile(join(args.state.runDir, REVIEWER_CONTEXT_JSON), JSON.stringify(withoutPromptMarkdown(packet), null, 2) + '\n');
12
+ await writeFile(join(args.state.runDir, REVIEWER_CONTEXT_MARKDOWN), packet.promptMarkdown + '\n');
13
+ return packet;
14
+ }
15
+ export function buildReviewerContextPacket(args) {
16
+ const { state } = args;
17
+ const createdAt = (args.now ?? new Date()).toISOString();
18
+ const completedScopeCount = state.completedScopes.length;
19
+ const findingCount = state.findings.length;
20
+ const completedScopes = state.completedScopes.slice(-COMPLETED_SCOPE_LIMIT).map((scope) => ({
21
+ number: scope.number,
22
+ result: scope.result,
23
+ marker: scope.marker,
24
+ finalCommit: scope.finalCommit,
25
+ summary: scope.summary ?? null,
26
+ reviewRounds: scope.reviewRounds,
27
+ findings: scope.findings,
28
+ residualReviewDebt: scope.residualReviewDebt?.length ?? 0,
29
+ }));
30
+ const findings = state.findings.slice(-FINDING_LIMIT).map((finding) => ({
31
+ canonicalId: finding.canonicalId,
32
+ id: finding.id,
33
+ severity: finding.severity,
34
+ status: finding.status,
35
+ source: finding.source,
36
+ files: finding.files,
37
+ claim: finding.claim,
38
+ requiredAction: finding.requiredAction,
39
+ }));
40
+ const finalCompletion = summarizeFinalCompletion(state);
41
+ const packetWithoutMarkdown = {
42
+ version: 1,
43
+ createdAt,
44
+ purpose: 'reviewer_continuity',
45
+ run: {
46
+ id: runIdFromDir(state.runDir),
47
+ cwd: state.cwd,
48
+ runDir: toDisplayPath(state.cwd, state.runDir),
49
+ planDoc: state.planDoc,
50
+ topLevelMode: state.topLevelMode,
51
+ executionShape: state.executionShape,
52
+ phase: state.phase,
53
+ status: state.status,
54
+ currentScopeNumber: state.currentScopeNumber,
55
+ },
56
+ completedScopes,
57
+ findings,
58
+ finalCompletion,
59
+ citations: buildReviewerContextCitations(state),
60
+ limits: {
61
+ completedScopeLimit: COMPLETED_SCOPE_LIMIT,
62
+ completedScopeCount,
63
+ findingLimit: FINDING_LIMIT,
64
+ findingCount,
65
+ truncatedCompletedScopes: completedScopeCount > completedScopes.length,
66
+ truncatedFindings: findingCount > findings.length,
67
+ },
68
+ };
69
+ return {
70
+ ...packetWithoutMarkdown,
71
+ promptMarkdown: renderReviewerContextMarkdown(packetWithoutMarkdown),
72
+ };
73
+ }
74
+ function withoutPromptMarkdown(packet) {
75
+ const { promptMarkdown: _promptMarkdown, ...json } = packet;
76
+ return json;
77
+ }
78
+ function summarizeFinalCompletion(state) {
79
+ const view = getFinalCompletionView(state);
80
+ if (!view) {
81
+ return null;
82
+ }
83
+ return {
84
+ state: view.state,
85
+ effectiveAction: view.effectiveAction,
86
+ hasSummary: view.hasSummary,
87
+ hasReviewVerdict: view.hasReviewVerdict,
88
+ continueExecutionCount: view.continueExecutionCount,
89
+ continueExecutionCapReached: view.continueExecutionCapReached,
90
+ };
91
+ }
92
+ function buildReviewerContextCitations(state) {
93
+ return [
94
+ { label: 'RUN_STATE.json', path: toDisplayPath(state.cwd, join(state.runDir, 'RUN_STATE.json')) },
95
+ { label: 'PLAN_PROGRESS.md', path: toDisplayPath(state.cwd, state.progressMarkdownPath) },
96
+ { label: 'plan-progress.json', path: toDisplayPath(state.cwd, state.progressJsonPath) },
97
+ { label: 'REVIEW.md', path: toDisplayPath(state.cwd, state.reviewMarkdownPath) },
98
+ { label: 'RECOVERY.md', path: toDisplayPath(state.cwd, state.recoveryMarkdownPath) },
99
+ { label: REVIEWER_CONTEXT_JSON, path: toDisplayPath(state.cwd, join(state.runDir, REVIEWER_CONTEXT_JSON)) },
100
+ { label: REVIEWER_CONTEXT_MARKDOWN, path: toDisplayPath(state.cwd, join(state.runDir, REVIEWER_CONTEXT_MARKDOWN)) },
101
+ ];
102
+ }
103
+ export function renderReviewerContextMarkdown(packet, options = {}) {
104
+ const mode = options.mode ?? 'tool-access';
105
+ const scopeLines = packet.completedScopes.length
106
+ ? packet.completedScopes.map((scope) => `- Scope ${scope.number}: ${scope.result}; marker=${scope.marker}; finalCommit=${scope.finalCommit ?? 'none'}; reviewRounds=${scope.reviewRounds}; findings=${scope.findings}; residualDebt=${scope.residualReviewDebt}; summary=${scope.summary ?? 'none'}`)
107
+ : ['- none'];
108
+ const findingLines = packet.findings.length
109
+ ? packet.findings.map((finding) => `- ${finding.canonicalId} (${finding.id}): ${finding.severity}/${finding.status}; source=${finding.source}; files=${finding.files.join(', ') || 'none'}; claim=${finding.claim}; requiredAction=${finding.requiredAction}`)
110
+ : ['- none'];
111
+ const finalCompletion = packet.finalCompletion;
112
+ return [
113
+ '# Reviewer Continuity Context',
114
+ '',
115
+ mode === 'inline'
116
+ ? 'Use this bounded Neal-owned context for continuity only. All required review context is inlined into this prompt; you have no access to the cited run artifacts or the checkout.'
117
+ : 'Use this bounded Neal-owned context for continuity only. Inspect cited artifacts or repository state directly before making findings.',
118
+ '',
119
+ '## Run',
120
+ `- id: ${packet.run.id}`,
121
+ `- plan: ${packet.run.planDoc}`,
122
+ `- mode: ${packet.run.topLevelMode}`,
123
+ `- executionShape: ${packet.run.executionShape ?? 'pending'}`,
124
+ `- phase: ${packet.run.phase}`,
125
+ `- status: ${packet.run.status}`,
126
+ `- currentScope: ${packet.run.currentScopeNumber}`,
127
+ '',
128
+ '## Completed Scopes',
129
+ ...scopeLines,
130
+ packet.limits.truncatedCompletedScopes
131
+ ? `- truncated: showing latest ${packet.completedScopes.length} of ${packet.limits.completedScopeCount}`
132
+ : '- truncated: no',
133
+ '',
134
+ '## Findings',
135
+ ...findingLines,
136
+ packet.limits.truncatedFindings
137
+ ? `- truncated: showing latest ${packet.findings.length} of ${packet.limits.findingCount}`
138
+ : '- truncated: no',
139
+ '',
140
+ '## Final Completion',
141
+ finalCompletion
142
+ ? `- state=${finalCompletion.state}; effectiveAction=${finalCompletion.effectiveAction ?? 'none'}; hasSummary=${finalCompletion.hasSummary}; hasReviewVerdict=${finalCompletion.hasReviewVerdict}; continueExecutionCount=${finalCompletion.continueExecutionCount}; capReached=${finalCompletion.continueExecutionCapReached}`
143
+ : '- not applicable',
144
+ // Citation paths are reading instructions; a no-read reviewer cannot
145
+ // follow them, so the inline rendering omits the section entirely.
146
+ ...(mode === 'inline'
147
+ ? []
148
+ : [
149
+ '',
150
+ '## Citations',
151
+ ...packet.citations.map((citation) => `- ${citation.label}: ${citation.path}`),
152
+ ]),
153
+ ].join('\n');
154
+ }
155
+ function runIdFromDir(runDir) {
156
+ const normalized = runDir.replace(/\/+$/, '');
157
+ return normalized.slice(normalized.lastIndexOf('/') + 1);
158
+ }
159
+ function toDisplayPath(cwd, path) {
160
+ const absolutePath = resolve(path);
161
+ const relativePath = relative(cwd, absolutePath);
162
+ if (!relativePath.startsWith('..') && !isAbsolute(relativePath)) {
163
+ return relativePath || '.';
164
+ }
165
+ return absolutePath;
166
+ }
@@ -0,0 +1,117 @@
1
+ export const SUPPORTED_NEAL_ACTION_TYPES = [
2
+ 'inspect_artifact',
3
+ 'start_plan_refinement',
4
+ 'start_execution',
5
+ 'pause_after_scope',
6
+ 'recover',
7
+ 'resume',
8
+ 'squash',
9
+ ];
10
+ export function isSupportedNealActionType(value) {
11
+ return SUPPORTED_NEAL_ACTION_TYPES.includes(value);
12
+ }
13
+ const RUN_SCOPED_ACTION_TYPES = new Set([
14
+ 'inspect_artifact',
15
+ 'pause_after_scope',
16
+ 'recover',
17
+ 'resume',
18
+ 'squash',
19
+ ]);
20
+ const PLAN_SCOPED_ACTION_TYPES = new Set([
21
+ 'start_plan_refinement',
22
+ 'start_execution',
23
+ ]);
24
+ const IMPLICIT_RUN_TARGETS = new Set(['latest', 'current']);
25
+ const TARGET_STRING_FIELDS = ['runDirName', 'statePath', 'planPath', 'artifactLabel'];
26
+ const ACTION_FIELDS = ['type', 'label', 'target', 'rationale'];
27
+ export function validateSuggestedNealAction(value) {
28
+ if (!value || typeof value !== 'object') {
29
+ throw new Error('suggestion must be an object');
30
+ }
31
+ if (containsForbiddenCommandKey(value)) {
32
+ throw new Error('forbidden command payload');
33
+ }
34
+ for (const key of Object.keys(value)) {
35
+ if (!ACTION_FIELDS.includes(key)) {
36
+ throw new Error(`unsupported action field "${key}"`);
37
+ }
38
+ }
39
+ const action = value;
40
+ if (typeof action.type !== 'string' ||
41
+ !isSupportedNealActionType(action.type) ||
42
+ typeof action.label !== 'string' ||
43
+ action.label.trim() === '' ||
44
+ !action.target ||
45
+ typeof action.target !== 'object') {
46
+ throw new Error('invalid action shape');
47
+ }
48
+ const target = validateNealActionTarget(action.type, action.target);
49
+ return {
50
+ type: action.type,
51
+ label: action.label.trim(),
52
+ target,
53
+ ...(typeof action.rationale === 'string' && action.rationale.trim() ? { rationale: action.rationale.trim() } : {}),
54
+ };
55
+ }
56
+ function validateNealActionTarget(type, value) {
57
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
58
+ throw new Error('target must be an object');
59
+ }
60
+ const record = value;
61
+ for (const key of Object.keys(record)) {
62
+ if (!TARGET_STRING_FIELDS.includes(key)) {
63
+ throw new Error(`unsupported target field "${key}"`);
64
+ }
65
+ }
66
+ const target = {};
67
+ for (const key of TARGET_STRING_FIELDS) {
68
+ const rawValue = record[key];
69
+ if (rawValue === undefined) {
70
+ continue;
71
+ }
72
+ if (typeof rawValue !== 'string' || rawValue.trim() === '') {
73
+ throw new Error(`target.${key} must be a non-empty string`);
74
+ }
75
+ target[key] = rawValue.trim();
76
+ }
77
+ if (target.runDirName) {
78
+ validateRunDirName(target.runDirName);
79
+ }
80
+ if (target.statePath) {
81
+ validateStatePath(target.statePath);
82
+ }
83
+ if (RUN_SCOPED_ACTION_TYPES.has(type) && !target.runDirName && !target.statePath) {
84
+ throw new Error('run-scoped action target must include runDirName or exact statePath');
85
+ }
86
+ if (PLAN_SCOPED_ACTION_TYPES.has(type) && !target.planPath) {
87
+ throw new Error('plan-scoped action target must include planPath');
88
+ }
89
+ if (type === 'inspect_artifact' && !target.artifactLabel) {
90
+ throw new Error('inspect_artifact target must include artifactLabel');
91
+ }
92
+ return target;
93
+ }
94
+ function validateRunDirName(runDirName) {
95
+ const normalized = runDirName.trim().toLowerCase();
96
+ if (IMPLICIT_RUN_TARGETS.has(normalized) || runDirName.includes('/') || runDirName.includes('\\') || runDirName.includes('..')) {
97
+ throw new Error('runDirName must name an explicit run directory');
98
+ }
99
+ }
100
+ function validateStatePath(statePath) {
101
+ const normalized = statePath.trim().replace(/\\/g, '/').toLowerCase();
102
+ if (IMPLICIT_RUN_TARGETS.has(normalized)) {
103
+ throw new Error('statePath must not target an implicit run pointer');
104
+ }
105
+ if (!normalized.endsWith('/run_state.json') && normalized !== 'run_state.json') {
106
+ throw new Error('statePath must target an exact RUN_STATE.json');
107
+ }
108
+ }
109
+ function containsForbiddenCommandKey(value) {
110
+ if (!value || typeof value !== 'object') {
111
+ return false;
112
+ }
113
+ if (Object.hasOwn(value, 'command')) {
114
+ return true;
115
+ }
116
+ return Object.values(value).some((child) => containsForbiddenCommandKey(child));
117
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,208 @@
1
+ const DETAIL_BUFFER_ENTRY_LIMIT = 400;
2
+ const DETAIL_BUFFER_BYTE_LIMIT = 128 * 1024;
3
+ const NARRATIVE_BUFFER_ENTRY_LIMIT = 400;
4
+ const NARRATIVE_BUFFER_BYTE_LIMIT = 128 * 1024;
5
+ let statusFooter = null;
6
+ let activeView = 'narrative';
7
+ let activeDetailContext = {};
8
+ let bufferedNarrativeEntries = [];
9
+ let bufferedNarrativeBytes = 0;
10
+ let droppedNarrativeEntries = 0;
11
+ let droppedNarrativeBytes = 0;
12
+ let bufferedDetailEntries = [];
13
+ let bufferedDetailBytes = 0;
14
+ let droppedDetailEntries = 0;
15
+ let droppedDetailBytes = 0;
16
+ export function configureDiagnosticFooter(footer) {
17
+ statusFooter?.dispose();
18
+ statusFooter = footer;
19
+ }
20
+ export function clearDiagnosticFooter() {
21
+ statusFooter?.dispose();
22
+ statusFooter = null;
23
+ }
24
+ export function setDiagnosticDetailContext(context) {
25
+ activeDetailContext = {
26
+ ...activeDetailContext,
27
+ ...context,
28
+ };
29
+ }
30
+ export function resetDiagnosticDetailState(context = {}) {
31
+ activeView = 'narrative';
32
+ activeDetailContext = { ...context };
33
+ bufferedNarrativeEntries = [];
34
+ bufferedNarrativeBytes = 0;
35
+ droppedNarrativeEntries = 0;
36
+ droppedNarrativeBytes = 0;
37
+ bufferedDetailEntries = [];
38
+ bufferedDetailBytes = 0;
39
+ droppedDetailEntries = 0;
40
+ droppedDetailBytes = 0;
41
+ }
42
+ export function setDiagnosticDetailVisibility(visible) {
43
+ activeView = visible ? 'detail' : 'narrative';
44
+ }
45
+ export function isDiagnosticDetailVisible() {
46
+ return activeView === 'detail';
47
+ }
48
+ export function getBufferedNarrativeSnapshot() {
49
+ return {
50
+ entries: [...bufferedNarrativeEntries],
51
+ droppedEntries: droppedNarrativeEntries,
52
+ droppedBytes: droppedNarrativeBytes,
53
+ };
54
+ }
55
+ export function getBufferedDetailSnapshot(filter = {}) {
56
+ return {
57
+ entries: bufferedDetailEntries.filter((entry) => matchesDetailFilter(entry.context, filter)),
58
+ droppedEntries: droppedDetailEntries,
59
+ droppedBytes: droppedDetailBytes,
60
+ };
61
+ }
62
+ export function writeBufferedDetail(filter = {}) {
63
+ const transcript = renderDetailTranscript(filter);
64
+ if (transcript !== '') {
65
+ writeTerminalDiagnostic(transcript);
66
+ }
67
+ }
68
+ export function showDiagnosticDetailView(filter = {}) {
69
+ activeView = 'detail';
70
+ const transcript = renderDetailTranscript(filter);
71
+ if (canReplaceManagedView()) {
72
+ statusFooter.replaceView(transcript);
73
+ return;
74
+ }
75
+ if (transcript !== '') {
76
+ writeTerminalDiagnostic(transcript);
77
+ }
78
+ }
79
+ export function showDiagnosticNarrativeView() {
80
+ activeView = 'narrative';
81
+ if (canReplaceManagedView()) {
82
+ statusFooter.replaceView(renderNarrativeTranscript());
83
+ }
84
+ }
85
+ export function writeNarrative(message, logger) {
86
+ bufferNarrative(message);
87
+ if (activeView === 'narrative' || !isManagedDiagnosticView()) {
88
+ writeTerminalDiagnostic(message);
89
+ }
90
+ void logger?.stderr(message);
91
+ }
92
+ export function writeDetail(message, logger, context = {}) {
93
+ bufferDetail(message, context);
94
+ if (activeView === 'detail') {
95
+ writeTerminalDiagnostic(message);
96
+ }
97
+ void logger?.stderr(message);
98
+ }
99
+ export function writeErrorDetail(message, logger, context = {}) {
100
+ writeDetail(message, logger, context);
101
+ }
102
+ export function writeDiagnostic(message, logger) {
103
+ writeNarrative(message, logger);
104
+ }
105
+ export function resetDiagnosticStateForTests() {
106
+ clearDiagnosticFooter();
107
+ resetDiagnosticDetailState();
108
+ }
109
+ function writeTerminalDiagnostic(message) {
110
+ if (statusFooter) {
111
+ statusFooter.write(message);
112
+ }
113
+ else {
114
+ process.stderr.write(message);
115
+ }
116
+ }
117
+ function isManagedDiagnosticView() {
118
+ return statusFooter?.isEnabled?.() === true;
119
+ }
120
+ function canReplaceManagedView() {
121
+ return isManagedDiagnosticView() && typeof statusFooter?.replaceView === 'function';
122
+ }
123
+ function renderNarrativeTranscript() {
124
+ const snapshot = getBufferedNarrativeSnapshot();
125
+ const transcript = [];
126
+ if (snapshot.droppedEntries > 0 || snapshot.droppedBytes > 0) {
127
+ transcript.push(`[neal] earlier narrative omitted from buffer (${snapshot.droppedEntries} entries, ${snapshot.droppedBytes} bytes)\n`);
128
+ }
129
+ for (const entry of snapshot.entries) {
130
+ transcript.push(entry.message);
131
+ }
132
+ return transcript.join('');
133
+ }
134
+ function renderDetailTranscript(filter) {
135
+ const snapshot = getBufferedDetailSnapshot(filter);
136
+ const transcript = [];
137
+ if (snapshot.droppedEntries > 0 || snapshot.droppedBytes > 0) {
138
+ transcript.push(`[neal:detail] earlier detail omitted from buffer (${snapshot.droppedEntries} entries, ${snapshot.droppedBytes} bytes)\n`);
139
+ }
140
+ for (const entry of snapshot.entries) {
141
+ transcript.push(entry.message);
142
+ }
143
+ return transcript.join('');
144
+ }
145
+ function bufferNarrative(message) {
146
+ const entry = {
147
+ message,
148
+ timestamp: new Date().toISOString(),
149
+ bytes: Buffer.byteLength(message, 'utf8'),
150
+ };
151
+ bufferedNarrativeEntries.push(entry);
152
+ bufferedNarrativeBytes += entry.bytes;
153
+ trimNarrativeBuffer();
154
+ }
155
+ function trimNarrativeBuffer() {
156
+ while (bufferedNarrativeEntries.length > NARRATIVE_BUFFER_ENTRY_LIMIT ||
157
+ bufferedNarrativeBytes > NARRATIVE_BUFFER_BYTE_LIMIT) {
158
+ const [dropped] = bufferedNarrativeEntries.splice(0, 1);
159
+ if (!dropped) {
160
+ return;
161
+ }
162
+ bufferedNarrativeBytes -= dropped.bytes;
163
+ droppedNarrativeEntries += 1;
164
+ droppedNarrativeBytes += dropped.bytes;
165
+ }
166
+ }
167
+ function bufferDetail(message, context) {
168
+ const entryContext = {
169
+ ...activeDetailContext,
170
+ ...context,
171
+ timestamp: context.timestamp ?? activeDetailContext.timestamp ?? new Date().toISOString(),
172
+ };
173
+ const entry = {
174
+ message,
175
+ context: entryContext,
176
+ bytes: Buffer.byteLength(message, 'utf8'),
177
+ };
178
+ bufferedDetailEntries.push(entry);
179
+ bufferedDetailBytes += entry.bytes;
180
+ trimDetailBuffer();
181
+ }
182
+ function trimDetailBuffer() {
183
+ while (bufferedDetailEntries.length > DETAIL_BUFFER_ENTRY_LIMIT ||
184
+ bufferedDetailBytes > DETAIL_BUFFER_BYTE_LIMIT) {
185
+ const [dropped] = bufferedDetailEntries.splice(0, 1);
186
+ if (!dropped) {
187
+ return;
188
+ }
189
+ bufferedDetailBytes -= dropped.bytes;
190
+ droppedDetailEntries += 1;
191
+ droppedDetailBytes += dropped.bytes;
192
+ }
193
+ }
194
+ function matchesDetailFilter(context, filter) {
195
+ if (filter.runId !== undefined && context.runId !== filter.runId) {
196
+ return false;
197
+ }
198
+ if (filter.phase !== undefined && context.phase !== filter.phase) {
199
+ return false;
200
+ }
201
+ if (filter.scopeLabel !== undefined && context.scopeLabel !== filter.scopeLabel) {
202
+ return false;
203
+ }
204
+ if (filter.scopeNumber !== undefined && String(context.scopeNumber) !== String(filter.scopeNumber)) {
205
+ return false;
206
+ }
207
+ return true;
208
+ }
@@ -0,0 +1,5 @@
1
+ export const EXECUTE_FINALIZATION_PHASE = 'execute_finalization';
2
+ export const EXECUTE_FINALIZATION_PUBLIC_LABEL = 'finalizing accepted scope';
3
+ export function isExecuteFinalizationPhase(phase) {
4
+ return phase === EXECUTE_FINALIZATION_PHASE;
5
+ }
@@ -0,0 +1,188 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { writeTextAtomic } from './atomic-write.js';
4
+ import { renderAdjudicationContractLines } from './adjudicator/artifacts.js';
5
+ import { formatPublicPhase } from './phase-display.js';
6
+ import { renderInteractiveBlockedRecoveryHistoryLines } from './recovery-artifacts.js';
7
+ import { buildScopeAccountingSummary, getCurrentScopeLabel, getExecutionPlanScopeCountForShape, renderScopeProgressSummary, } from './scopes.js';
8
+ import { getFinalCompletionView } from './state-views.js';
9
+ import { formatSquashMessage } from './squash-message.js';
10
+ export function getFinalCompletionReviewArtifactPath(runDir) {
11
+ return join(runDir, 'FINAL_COMPLETION_REVIEW.md');
12
+ }
13
+ function stringValue(value) {
14
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
15
+ }
16
+ export async function readFinalCompletionUnstructuredOutput(args) {
17
+ let content;
18
+ try {
19
+ content = await readFile(join(args.runDir, 'events.ndjson'), 'utf8');
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ const matchedTexts = [];
25
+ for (const line of content.split('\n')) {
26
+ if (!line.trim()) {
27
+ continue;
28
+ }
29
+ let event;
30
+ try {
31
+ event = JSON.parse(line);
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ const data = event.data ?? {};
37
+ if (event.type !== 'provider.assistant_text' ||
38
+ data.provider !== 'anthropic-claude' ||
39
+ data.role !== 'structured-advisor' ||
40
+ data.label !== 'final-completion') {
41
+ continue;
42
+ }
43
+ const eventSessionHandle = stringValue(data.sessionHandle);
44
+ if (args.sessionHandle && eventSessionHandle !== args.sessionHandle) {
45
+ continue;
46
+ }
47
+ const text = stringValue(data.text);
48
+ if (text) {
49
+ matchedTexts.push(text);
50
+ }
51
+ }
52
+ if (!matchedTexts.length) {
53
+ return null;
54
+ }
55
+ return {
56
+ source: args.source,
57
+ text: matchedTexts.join('\n'),
58
+ ambiguousMatch: !args.sessionHandle,
59
+ };
60
+ }
61
+ function collectResidualReviewDebt(scopes) {
62
+ return scopes.flatMap((scope) => (scope.residualReviewDebt ?? []).map((item) => ({
63
+ scope: scope.number,
64
+ ...item,
65
+ })));
66
+ }
67
+ export function renderFinalCompletionReviewMarkdown(state, options = {}) {
68
+ const finalCompletion = getFinalCompletionView(state);
69
+ const summary = finalCompletion?.summary ?? null;
70
+ const reviewVerdict = finalCompletion?.reviewVerdict ?? null;
71
+ const residualDebt = collectResidualReviewDebt(state.completedScopes.filter((scope) => scope.result === 'accepted'));
72
+ const scopeAccounting = buildScopeAccountingSummary(state.completedScopes);
73
+ const lines = [
74
+ '# Final Completion Review',
75
+ '',
76
+ '## Metadata',
77
+ `- Plan: ${state.planDoc}`,
78
+ `- Current step: ${formatPublicPhase(state.phase)}`,
79
+ `- Status: ${state.status}`,
80
+ `- Execution shape: ${state.executionShape ?? 'pending'}`,
81
+ `- Current scope: ${getCurrentScopeLabel(state)}`,
82
+ `- Scope progress: ${renderScopeProgressSummary(state, getExecutionPlanScopeCountForShape(state.executionShape))}`,
83
+ `- Final commit: ${state.finalCommit ?? 'pending'}`,
84
+ `- Last marker: ${state.lastScopeMarker ?? 'pending'}`,
85
+ `- Reviewer session: ${state.reviewerSessionHandle ?? 'pending'}`,
86
+ `- Continue-execution cycles used: ${finalCompletion?.continueExecutionCount ?? 0}`,
87
+ `- Continue-execution cap reached: ${finalCompletion?.continueExecutionCapReached ? 'yes' : 'no'}`,
88
+ ];
89
+ lines.push('', '## Coder Completion Summary');
90
+ if (!summary) {
91
+ lines.push('', 'Pending.');
92
+ }
93
+ else {
94
+ lines.push(`- Plan goal satisfied: ${summary.planGoalSatisfied ? 'yes' : 'no'}`, `- What changed overall: ${summary.whatChangedOverall}`, `- Verification summary: ${summary.verificationSummary}`);
95
+ if (summary.remainingKnownGaps.length > 0) {
96
+ lines.push('- Remaining known gaps:');
97
+ for (const gap of summary.remainingKnownGaps) {
98
+ lines.push(` - ${gap}`);
99
+ }
100
+ }
101
+ else {
102
+ lines.push('- Remaining known gaps: none');
103
+ }
104
+ }
105
+ if (options.unstructuredOutput?.source === 'coder_summary') {
106
+ lines.push('', '## Unstructured Coder Summary Output', '', 'Claude returned this prose but failed to provide SDK structured output.');
107
+ if (options.unstructuredOutput.ambiguousMatch) {
108
+ lines.push('', 'This prose was selected without a failed-session handle, so the match may be ambiguous.');
109
+ }
110
+ lines.push('', options.unstructuredOutput.text);
111
+ }
112
+ const contractLines = renderAdjudicationContractLines(state);
113
+ if (contractLines.length > 0) {
114
+ lines.push('', ...contractLines);
115
+ }
116
+ lines.push(...renderInteractiveBlockedRecoveryHistoryLines(state.interactiveBlockedRecoveryHistory));
117
+ lines.push('', '## Scope Accounting', `- ${scopeAccounting.summary}`);
118
+ if (scopeAccounting.replacedParentScopes.length > 0) {
119
+ lines.push('- Derived parent replacements:');
120
+ for (const replacement of scopeAccounting.replacedParentScopes) {
121
+ lines.push(` - Scope ${replacement.number}: ${replacement.derivedPlanPath}`);
122
+ }
123
+ }
124
+ else {
125
+ lines.push('- Derived parent replacements: none');
126
+ }
127
+ lines.push('', '## Residual Review Debt');
128
+ if (residualDebt.length === 0) {
129
+ lines.push('', 'No unresolved non-blocking review debt was recorded for accepted scopes.');
130
+ }
131
+ else {
132
+ lines.push('', 'The final reviewer should decide whether these accepted-scope leftovers are acceptable residual polish or evidence that execution exited too early.');
133
+ for (const item of residualDebt) {
134
+ const files = item.files.length > 0 ? item.files.join(', ') : 'n/a';
135
+ lines.push('', `### Scope ${item.scope} ${item.id}`, `- Status: ${item.status}`, `- Files: ${files}`, `- Claim: ${item.claim}`, `- Evidence: ${item.evidence?.trim() || 'n/a'}`, `- Required action: ${item.requiredAction}`, `- Coder disposition: ${item.coderDisposition ?? 'pending'}`, `- Coder commit: ${item.coderCommit ?? 'pending'}`);
136
+ }
137
+ }
138
+ lines.push('', '## Reviewer Verdict');
139
+ if (!reviewVerdict) {
140
+ lines.push('', 'Pending.');
141
+ }
142
+ else {
143
+ lines.push(`- Reviewer action: ${reviewVerdict.action}`, `- Resulting action: ${finalCompletion?.effectiveAction ?? reviewVerdict.action}`, `- Reviewer summary: ${reviewVerdict.summary}`, `- Reviewer rationale: ${reviewVerdict.rationale}`);
144
+ if (reviewVerdict.missingWork) {
145
+ lines.push(`- Missing work summary: ${reviewVerdict.missingWork.summary}`, `- Missing work required outcome: ${reviewVerdict.missingWork.requiredOutcome}`, `- Missing work verification: ${reviewVerdict.missingWork.verification}`);
146
+ }
147
+ else {
148
+ lines.push('- Missing work: none');
149
+ }
150
+ }
151
+ if (options.unstructuredOutput?.source === 'reviewer_verdict') {
152
+ lines.push('', '## Unstructured Reviewer Output', '', 'Claude returned this prose but failed to provide SDK structured output.');
153
+ if (options.unstructuredOutput.ambiguousMatch) {
154
+ lines.push('', 'This prose was selected without a failed-session handle, so the match may be ambiguous.');
155
+ }
156
+ lines.push('', options.unstructuredOutput.text);
157
+ }
158
+ lines.push('', '## Squash Commit Message Draft');
159
+ if (!reviewVerdict) {
160
+ lines.push('', 'Pending.');
161
+ }
162
+ else if (!reviewVerdict.squashCommitMessage && reviewVerdict.action === 'accept_complete') {
163
+ lines.push('', 'No reviewer-authored squash draft recorded. If squash is requested, Neal will derive the squash message from deterministic fallback generation.');
164
+ }
165
+ else if (!reviewVerdict.squashCommitMessage) {
166
+ lines.push('', 'None recorded.');
167
+ }
168
+ else {
169
+ lines.push('', '```text', formatSquashMessage(reviewVerdict.squashCommitMessage), '```');
170
+ }
171
+ lines.push('', '## Result');
172
+ if (!reviewVerdict) {
173
+ lines.push('', 'Final completion review has not settled yet.');
174
+ }
175
+ else if (finalCompletion?.acceptedComplete) {
176
+ lines.push('', 'Run completed cleanly.');
177
+ }
178
+ else if (finalCompletion?.continuesExecution) {
179
+ lines.push('', 'Execution reopened with one explicit follow-on scope.');
180
+ }
181
+ else {
182
+ lines.push('', 'Run blocked for operator guidance.');
183
+ }
184
+ return `${lines.join('\n')}\n`;
185
+ }
186
+ export async function writeFinalCompletionReviewMarkdown(path, state, options = {}) {
187
+ await writeTextAtomic(path, renderFinalCompletionReviewMarkdown(state, options));
188
+ }