@navels/neal 0.1.0 → 0.2.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.
@@ -2,6 +2,31 @@ import { aggregateChangedFilesForAcceptedDerivedScopes, getAcceptedDerivedScopes
2
2
  import { toResidualReviewDebt } from '../review-debt.js';
3
3
  import { getDerivedPlanView, getFinalCompletionView } from '../state-views.js';
4
4
  const ALREADY_SATISFIED_ACCEPTANCE_RATIONALE_PREFIX = 'Accepted top-level already-satisfied scope';
5
+ export function createScopeBoundaryReset() {
6
+ return {
7
+ currentScopeProgressJustification: null,
8
+ currentScopeMeaningfulProgressVerdict: null,
9
+ rounds: [],
10
+ recentBlocks: [],
11
+ reviewStuckArbiterCount: 0,
12
+ findings: [],
13
+ createdCommits: [],
14
+ };
15
+ }
16
+ export function createNextScopeEntryReset(baseCommit) {
17
+ return {
18
+ ...createScopeBoundaryReset(),
19
+ baseCommit,
20
+ finalCommit: null,
21
+ archivedReviewPath: null,
22
+ coderSessionHandle: null,
23
+ coderSessionProtocol: null,
24
+ lastScopeMarker: null,
25
+ finalCompletionSummary: null,
26
+ phase: 'coder_scope',
27
+ status: 'running',
28
+ };
29
+ }
5
30
  export function appendCompletedScope(state, result, details) {
6
31
  const scopeLabel = details.scopeLabel ?? getCurrentScopeLabel(state);
7
32
  const marker = details.marker ?? (state.lastScopeMarker ?? 'AUTONOMY_BLOCKED');
@@ -57,23 +82,17 @@ export function adoptAcceptedDerivedPlan(state) {
57
82
  }
58
83
  return {
59
84
  ...state,
85
+ ...createScopeBoundaryReset(),
60
86
  phase: 'coder_scope',
61
87
  status: 'running',
62
88
  derivedScopeIndex: derivedPlan.scopeIndex ?? 1,
63
89
  coderSessionHandle: null,
64
90
  coderSessionProtocol: null,
65
91
  coderRetryCount: 0,
66
- currentScopeProgressJustification: null,
67
- currentScopeMeaningfulProgressVerdict: null,
68
92
  finalCompletionSummary: null,
69
93
  finalCompletionReviewVerdict: null,
70
94
  finalCompletionResolvedAction: null,
71
95
  finalCompletionContinueExecutionCapReached: false,
72
- rounds: [],
73
- recentBlocks: [],
74
- reviewStuckArbiterCount: 0,
75
- findings: [],
76
- createdCommits: [],
77
96
  blockedFromPhase: null,
78
97
  };
79
98
  }
@@ -89,15 +108,8 @@ export function computeNextScopeStateAfterExecuteFinalization({ state, finalComm
89
108
  if (derivedExecution && derivedPlanCompleted) {
90
109
  return {
91
110
  ...state,
92
- baseCommit: finalCommit,
93
- finalCommit: null,
94
- coderSessionHandle: null,
95
- coderSessionProtocol: null,
111
+ ...createNextScopeEntryReset(finalCommit),
96
112
  currentScopeNumber: nextTopLevelScopeNumber,
97
- lastScopeMarker: null,
98
- currentScopeProgressJustification: null,
99
- currentScopeMeaningfulProgressVerdict: null,
100
- finalCompletionSummary: null,
101
113
  finalCompletionReviewVerdict: null,
102
114
  finalCompletionResolvedAction: null,
103
115
  finalCompletionContinueExecutionCapReached: false,
@@ -109,28 +121,13 @@ export function computeNextScopeStateAfterExecuteFinalization({ state, finalComm
109
121
  derivedPlanAcceptedNotified: false,
110
122
  splitPlanBlockedNotified: false,
111
123
  splitPlanCountForCurrentScope: 0,
112
- rounds: [],
113
- recentBlocks: [],
114
- reviewStuckArbiterCount: 0,
115
- findings: [],
116
- createdCommits: [],
117
124
  completedScopes,
118
- archivedReviewPath: null,
119
- phase: 'coder_scope',
120
- status: 'running',
121
125
  };
122
126
  }
123
127
  if (derivedExecution) {
124
128
  return {
125
129
  ...state,
126
- baseCommit: finalCommit,
127
- finalCommit: null,
128
- coderSessionHandle: null,
129
- coderSessionProtocol: null,
130
- lastScopeMarker: null,
131
- currentScopeProgressJustification: null,
132
- currentScopeMeaningfulProgressVerdict: null,
133
- finalCompletionSummary: null,
130
+ ...createNextScopeEntryReset(finalCommit),
134
131
  finalCompletionReviewVerdict: null,
135
132
  finalCompletionResolvedAction: null,
136
133
  finalCompletionContinueExecutionCapReached: false,
@@ -138,29 +135,14 @@ export function computeNextScopeStateAfterExecuteFinalization({ state, finalComm
138
135
  splitPlanStartedNotified: false,
139
136
  derivedPlanAcceptedNotified: false,
140
137
  splitPlanBlockedNotified: false,
141
- rounds: [],
142
- recentBlocks: [],
143
- reviewStuckArbiterCount: 0,
144
- findings: [],
145
- createdCommits: [],
146
138
  completedScopes,
147
- archivedReviewPath: null,
148
- phase: 'coder_scope',
149
- status: 'running',
150
139
  };
151
140
  }
152
141
  if (continueScopes) {
153
142
  return {
154
143
  ...state,
155
- baseCommit: finalCommit,
156
- finalCommit: null,
157
- coderSessionHandle: null,
158
- coderSessionProtocol: null,
144
+ ...createNextScopeEntryReset(finalCommit),
159
145
  currentScopeNumber: nextTopLevelScopeNumber,
160
- lastScopeMarker: null,
161
- currentScopeProgressJustification: null,
162
- currentScopeMeaningfulProgressVerdict: null,
163
- finalCompletionSummary: null,
164
146
  finalCompletionReviewVerdict: null,
165
147
  finalCompletionResolvedAction: null,
166
148
  finalCompletionContinueExecutionCapReached: false,
@@ -172,15 +154,7 @@ export function computeNextScopeStateAfterExecuteFinalization({ state, finalComm
172
154
  derivedPlanAcceptedNotified: false,
173
155
  splitPlanBlockedNotified: false,
174
156
  splitPlanCountForCurrentScope: 0,
175
- rounds: [],
176
- recentBlocks: [],
177
- reviewStuckArbiterCount: 0,
178
- findings: [],
179
- createdCommits: [],
180
157
  completedScopes,
181
- archivedReviewPath: null,
182
- phase: 'coder_scope',
183
- status: 'running',
184
158
  };
185
159
  }
186
160
  return {
@@ -223,19 +197,11 @@ export function computeNextScopeStateAfterParentAdvance(args) {
223
197
  : args.state.currentScopeNumber;
224
198
  return {
225
199
  ...args.state,
226
- baseCommit: args.finalCommit,
227
- finalCommit: null,
228
- archivedReviewPath: null,
229
- coderSessionHandle: null,
230
- coderSessionProtocol: null,
200
+ ...createNextScopeEntryReset(args.finalCommit),
231
201
  reviewerSessionHandle: null,
232
202
  coderRetryCount: 0,
233
203
  currentScopeNumber: nextTopLevelScopeNumber,
234
- lastScopeMarker: null,
235
- currentScopeProgressJustification: null,
236
- currentScopeMeaningfulProgressVerdict: null,
237
204
  manualGate: null,
238
- finalCompletionSummary: null,
239
205
  finalCompletionReviewVerdict: null,
240
206
  finalCompletionResolvedAction: null,
241
207
  finalCompletionContinueExecutionCapReached: false,
@@ -247,16 +213,9 @@ export function computeNextScopeStateAfterParentAdvance(args) {
247
213
  derivedPlanAcceptedNotified: false,
248
214
  splitPlanBlockedNotified: false,
249
215
  splitPlanCountForCurrentScope: 0,
250
- rounds: [],
251
- recentBlocks: [],
252
- reviewStuckArbiterCount: 0,
253
- findings: [],
254
- createdCommits: [],
255
216
  completedScopes: args.completedScopes,
256
217
  blockedFromPhase: null,
257
218
  interactiveBlockedRecovery: null,
258
- phase: 'coder_scope',
259
- status: 'running',
260
219
  };
261
220
  }
262
221
  function getAcceptedScopeSummary(state) {
@@ -1,6 +1,29 @@
1
- import { realpath, stat } from 'node:fs/promises';
1
+ import { readFile, realpath, stat, writeFile } from 'node:fs/promises';
2
2
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
3
3
  import { getIgnoredPaths, getRepositoryRoot } from './git.js';
4
+ /**
5
+ * Run a destructive worktree action (reset --hard / clean) while preserving
6
+ * the plan document's byte content across it. The plan doc is a wrapper-owned
7
+ * overlay by contract: it may be an uncommitted modification of a TRACKED file
8
+ * (reset --hard would silently restore the committed content — a different
9
+ * plan — mid-run) or an untracked file (clean would delete it). Either way the
10
+ * run's plan must survive worktree hygiene. Restores nothing if the plan doc
11
+ * did not exist before the action.
12
+ */
13
+ export async function withPlanDocPreserved(planDoc, action) {
14
+ let content = null;
15
+ try {
16
+ content = await readFile(planDoc);
17
+ }
18
+ catch {
19
+ content = null;
20
+ }
21
+ const result = await action();
22
+ if (content !== null) {
23
+ await writeFile(planDoc, content);
24
+ }
25
+ return result;
26
+ }
4
27
  export async function inspectPlanDocDisposition(cwd, planDoc, options = {}) {
5
28
  const repositoryRoot = await realpath(await getRepositoryRoot(cwd));
6
29
  const absolutePlanDoc = resolve(cwd, planDoc);
@@ -1,4 +1,4 @@
1
- import { getPromptSpec } from './specs.js';
1
+ import { getPromptSpec, } from './specs.js';
2
2
  export function assertPromptBuilder(id, exportName, modulePath) {
3
3
  const spec = getPromptSpec(id);
4
4
  const allowedBuilders = [spec.baseInstructions, ...spec.variants.map((variant) => variant.baseInstructions)];
@@ -11,3 +11,10 @@ export function assertPromptBuilder(id, exportName, modulePath) {
11
11
  }
12
12
  return spec;
13
13
  }
14
+ export function resolvePrimaryVariant(spec, specLabel) {
15
+ const primary = spec.variants.find((variant) => variant.kind === 'primary');
16
+ if (!primary) {
17
+ throw new Error(`Prompt spec ${specLabel} is missing a primary variant`);
18
+ }
19
+ return primary;
20
+ }
@@ -54,6 +54,7 @@ export function buildScopePrompt(planDoc, progressText, options) {
54
54
  CODER_REGRESSION_PRESERVATION_LINE,
55
55
  ...CODER_PREEXISTING_FAILURE_LINES,
56
56
  'Verify the relevant work before you finish.',
57
+ 'Before claiming a step is done or a verification passed, confirm the claim against an actual tool or command result from this session, and state plainly when a step is not yet verified.',
57
58
  'Create real git commit(s) for completed work.',
58
59
  'Do not edit or stage wrapper-owned artifacts such as review files under .neal/runs/, PLAN_PROGRESS.md, plan-progress.json, or .neal/*.',
59
60
  'Treat action=`blocked` as a last resort, not an early exit.',
@@ -257,8 +258,11 @@ export function buildCoderResponsePrompt(args) {
257
258
  mode === 'blocking'
258
259
  ? 'Make code changes if needed, run the most relevant verification for the fixes you make, and create a real git commit if you changed code.'
259
260
  : 'Fix local, concrete, low-expansion non-blocking findings by default. Make the smallest justified code changes, run the most relevant verification for those changes, and create a real git commit if you changed code.',
261
+ // model-calibrated: caps post-commit tool use once the fix is committed and verified; tuned for a model that otherwise keeps issuing redundant shell commands after it already has enough evidence.
260
262
  'After you have made the necessary commit and run the relevant verification, stop using tools. Your next response must return the structured outcome with no additional shell commands such as git show, git status, or extra exploratory tests.',
263
+ // model-calibrated: stops repeated re-verification of an already-evidenced fix; tuned for a model that over-proves rather than summarizing the evidence it already gathered.
261
264
  'Do not keep proving the same fix after you have enough evidence to answer each finding. Summarize the evidence in the structured response instead.',
265
+ 'Before claiming a step is done or a verification passed, confirm the claim against an actual tool or command result from this session, and state plainly when a finding is not yet verified.',
262
266
  CODER_REGRESSION_PRESERVATION_LINE,
263
267
  ...CODER_PREEXISTING_FAILURE_LINES,
264
268
  'Use `fixed` only when you actually changed the code or verification in a way that resolves the finding.',
@@ -1,3 +1,4 @@
1
+ import { guardStructuredJsonOutputFormatLines } from '../agents/structured-json.js';
1
2
  import { assertNoReadPromptInstructionText, renderInlineReviewerContext, renderInlinedRangeDiffSection, } from '../context/inline-review-context.js';
2
3
  import { renderReviewerContextMarkdown } from '../context/reviewer-context.js';
3
4
  import { assertPromptBuilder } from './assert-builder.js';
@@ -5,6 +6,24 @@ import { getUserGuidanceLines } from './guidance.js';
5
6
  import { getUnattendedAutonomyLines } from './shared.js';
6
7
  import { getAdversarialReviewDoctrineLines, getCodeReviewFalsificationLines, getFindingQualityLines, getPreexistingFailureContractLines, getRegressionPreservationLines, getVerificationSkepticismLines, resolveReviewerPromptAccessMode, } from './review-doctrine.js';
7
8
  const PROMPT_MODULE_PATH = 'src/neal/prompts/specialized.ts';
9
+ // Guarded output-format instruction block shared by the two structured-JSON
10
+ // completion base prompts. It emits only JSON-only framing and a
11
+ // transport-deferring line (replacing the old fence-prohibition that
12
+ // contradicted the neal-json transport), and routes the assembled lines through
13
+ // guardStructuredJsonOutputFormatLines so a reintroduced conflicting phrasing
14
+ // throws at render. Both completion builders emit their output-format
15
+ // instructions exclusively through this helper. The transport-deferring line is
16
+ // provider-neutral: the neal-json protocol block is appended below the base
17
+ // prompt only on the wrapper path (anthropic-claude and the repair loop), while
18
+ // other transports (e.g. generic-agentic) send the base prompt with no protocol
19
+ // below it, so the line must not claim instructions appear "below".
20
+ export function completionJsonOutputFormatLines(label) {
21
+ return guardStructuredJsonOutputFormatLines([
22
+ 'Return only JSON that matches the required schema.',
23
+ 'Return the final answer only as the required structured output object, not as prose or markdown.',
24
+ 'Defer exact output framing to the active structured-output transport; do not add your own output-format constraints.',
25
+ ], label);
26
+ }
8
27
  export function buildFinalCompletionSummaryPrompt(args) {
9
28
  const spec = assertPromptBuilder('completion_coder', 'buildFinalCompletionSummaryPrompt', PROMPT_MODULE_PATH);
10
29
  const finalCompletionVariant = spec.variants.find((variant) => variant.kind === 'final_completion');
@@ -18,12 +37,12 @@ export function buildFinalCompletionSummaryPrompt(args) {
18
37
  `Summarize whether the execute-mode plan at ${args.planDoc} is complete as a whole.`,
19
38
  '',
20
39
  'Before writing the summary, review the current repository state, the plan document, and the completion packet below.',
21
- 'Return only JSON that matches the required schema.',
22
- 'Return the final answer only as the required structured output object, not as prose or markdown.',
40
+ ...completionJsonOutputFormatLines('buildFinalCompletionSummaryPrompt'),
23
41
  'Keep the response compact and auditable rather than essay-style.',
24
42
  'Use `planGoalSatisfied` to state whether the plan goal is satisfied overall.',
25
43
  'Use `whatChangedOverall` to summarize the completed work across the whole plan, not just the last scope.',
26
44
  'Use `verificationSummary` to summarize the completion evidence that actually ran.',
45
+ 'Before claiming a step is done or a verification passed, confirm the claim against an actual tool or command result from this session, and do not claim verification that did not actually run.',
27
46
  'Use `remainingKnownGaps` for any known missing work, regressions, quality concerns, testing gaps, risks, or omissions that would make the plan not fully complete.',
28
47
  'Do not contradict yourself:',
29
48
  '- if `planGoalSatisfied` is `true`, `remainingKnownGaps` must be empty',
@@ -49,7 +68,6 @@ export function buildFinalCompletionSummaryPrompt(args) {
49
68
  }, null, 2),
50
69
  '',
51
70
  'If the completion is verification-only, say so directly in `whatChangedOverall` or `remainingKnownGaps` instead of pretending there was a terminal implementation diff.',
52
- 'Do not include markdown fences or prose outside the JSON object.',
53
71
  ...getUserGuidanceLines('coder'),
54
72
  '',
55
73
  'Last non-empty implementation scope reference:',
@@ -141,8 +159,7 @@ export function buildFinalCompletionReviewerPrompt(args) {
141
159
  'Review the whole-plan result for correctness and completeness against the plan objectives, regressions or missing behavior, cross-scope integration issues that may not have been visible in individual scope reviews, code quality, maintainability, and consistency of the final implementation, and adequacy of test coverage and verification for the total change.',
142
160
  ...getReviewerContextLines(args.reviewerContext, noRead ? 'inline' : 'tool-access'),
143
161
  'Do not treat prior per-scope acceptance as sufficient evidence that the whole plan is complete or that the aggregate code quality is acceptable.',
144
- 'Return only JSON that matches the required schema.',
145
- 'Return the final answer only as the required structured output object, not as prose or markdown.',
162
+ ...completionJsonOutputFormatLines('buildFinalCompletionReviewerPrompt'),
146
163
  'Use `accept_complete` only when the full plan objectives are satisfied and the aggregate implementation is acceptable under ordinary code review standards.',
147
164
  'Use `continue_execution` only when the remaining work is concrete, bounded, and suitable for one explicit follow-on scope.',
148
165
  'Use `block_for_operator` when the remaining gap is ambiguous, externally constrained, or needs human direction.',
@@ -183,7 +200,6 @@ export function buildFinalCompletionReviewerPrompt(args) {
183
200
  }, null, 2),
184
201
  '',
185
202
  'If this was a verification-only terminal scope, judge the whole-plan result directly instead of pretending there was a final implementation diff.',
186
- 'Do not include markdown fences or prose outside the JSON object.',
187
203
  ...getUserGuidanceLines('reviewer'),
188
204
  '',
189
205
  'Last non-empty implementation scope reference:',
@@ -82,6 +82,10 @@ const COMPLETION_REVIEWER_CONTEXT = context('CompletionReviewerPromptContext', [
82
82
  field('inlineContext', 'repository_state', false, "Neal-inlined aggregate diff (or evidence-gap statement) and plan-document sections for reviewers without repository read access; only valid with the 'no-read' access mode."),
83
83
  field('accessMode', 'orchestrator_state', false, "Three-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute), 'read-only' (read tools only; no command execution, test runs, or scratch work), or 'no-read' (judge entirely from Neal-inlined context). When absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
84
84
  ]);
85
+ const BLOCKED_ADJUDICATOR_CONTEXT = context('BlockedAdjudicatorPromptContext', [
86
+ field('blockedReason', 'prompt_argument', true, 'Blocked reason reported by the stalled coder or reviewer turn.'),
87
+ field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context (plan content, open blocking findings, reviewer-round snapshots, or coder blocker plus changed files) the adjudicator judges entirely from.'),
88
+ ]);
85
89
  export const PROMPT_SPECS = [
86
90
  {
87
91
  id: 'plan_author',
@@ -542,6 +546,60 @@ export const PROMPT_SPECS = [
542
546
  },
543
547
  ],
544
548
  },
549
+ {
550
+ id: 'blocked_adjudicator',
551
+ role: 'reviewer',
552
+ purpose: 'Triage a blocked Neal run entirely from Neal-inlined context and decide whether the block is an in-scope recoverable misunderstanding or a genuine wall that must escalate to a human.',
553
+ requiredContext: BLOCKED_ADJUDICATOR_CONTEXT,
554
+ schemaTarget: {
555
+ kind: 'structured_json',
556
+ schemaBuilder: 'buildBlockedAdjudicatorSchema',
557
+ parser: 'validateBlockedAdjudicatorVerdictPayload',
558
+ providerSurface: 'neal_json_block_protocol',
559
+ },
560
+ baseInstructions: {
561
+ kind: 'builder',
562
+ modulePath: 'src/neal/agents/prompts.ts',
563
+ exportName: 'buildBlockedAdjudicatorPrompt',
564
+ inputShape: context('BuildBlockedAdjudicatorPromptArgs', [
565
+ field('blockedReason', 'prompt_argument', true, 'Blocked reason string.'),
566
+ field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context.'),
567
+ ]),
568
+ },
569
+ providerVariants: SHARED_PROVIDER_VARIANTS,
570
+ evaluationNotes: [
571
+ 'Render tests should assert the adjudicator judges entirely from inlined context and its static instructions carry no repository-access phrasing.',
572
+ 'A golden render test pins the exact prompt bytes, including the ALL-CAPS emphasis lines.',
573
+ ],
574
+ firstMigrationPriority: 3,
575
+ currentHome: 'mixed',
576
+ ownershipNotes: [
577
+ 'Prompt spec owns the adjudicator instructions and required context only; anti-thrash guarding, recovery routing, and verdict persistence stay in src/neal/adjudicator/ outside the prompt-spec library.',
578
+ ],
579
+ variants: [
580
+ {
581
+ kind: 'primary',
582
+ status: 'adjacent',
583
+ description: 'Read-only blocked-run adjudication round.',
584
+ currentRoundEntrypoints: ['runBlockedAdjudicatorRound'],
585
+ baseInstructions: {
586
+ kind: 'builder',
587
+ modulePath: 'src/neal/agents/prompts.ts',
588
+ exportName: 'buildBlockedAdjudicatorPrompt',
589
+ inputShape: context('BuildBlockedAdjudicatorPromptArgs', [
590
+ field('blockedReason', 'prompt_argument', true, 'Blocked reason string.'),
591
+ field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context.'),
592
+ ]),
593
+ },
594
+ schemaTarget: {
595
+ kind: 'structured_json',
596
+ schemaBuilder: 'buildBlockedAdjudicatorSchema',
597
+ parser: 'validateBlockedAdjudicatorVerdictPayload',
598
+ providerSurface: 'neal_json_block_protocol',
599
+ },
600
+ },
601
+ ],
602
+ },
545
603
  ];
546
604
  function getContractFieldKeys(contract) {
547
605
  return new Set(contract.fields.map((field) => field.key));