@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,389 @@
1
+ import { anthropicClaudeProviderDefinition } from './anthropic-claude.js';
2
+ import { genericAgenticProviderDefinition } from './generic-agentic.js';
3
+ import { openAICodexProviderDefinition } from './openai-codex.js';
4
+ import { openAICompatibleProviderDefinition } from './openai-compatible.js';
5
+ // Computed lazily instead of as a top-level constant: openai-compatible.ts
6
+ // imports config.ts, which imports this module, so the definition bindings may
7
+ // still be in their temporal dead zone while this module body evaluates.
8
+ // Every consumer calls listRegisteredProviderDefinitions() at runtime, after
9
+ // the module graph has fully loaded.
10
+ function getBuiltInProviderDefinitions() {
11
+ return [
12
+ openAICodexProviderDefinition,
13
+ anthropicClaudeProviderDefinition,
14
+ openAICompatibleProviderDefinition,
15
+ genericAgenticProviderDefinition,
16
+ ];
17
+ }
18
+ const providerCapabilityOverrides = new Map();
19
+ const providerDefinitionRegistrationsForTesting = new Map();
20
+ function formatProviderValue(value) {
21
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
22
+ }
23
+ function getProviderDefinitionMap() {
24
+ return new Map(listRegisteredProviderDefinitions().map((definition) => [definition.id, definition]));
25
+ }
26
+ function getProviderAdapterFactories(provider) {
27
+ const definition = getProviderDefinition(provider);
28
+ const createCoderAdapter = definition.createCoderAdapter;
29
+ const createStructuredAdvisorAdapter = definition.createStructuredAdvisorAdapter;
30
+ const base = {
31
+ createCoderAdapter: createCoderAdapter
32
+ ? (config) => createCoderAdapter({ model: config.model, effort: config.effort ?? null })
33
+ : undefined,
34
+ createStructuredAdvisorAdapter: createStructuredAdvisorAdapter
35
+ ? (config) => createStructuredAdvisorAdapter({ model: config.model, effort: config.effort ?? null })
36
+ : undefined,
37
+ };
38
+ const override = providerCapabilityOverrides.get(provider);
39
+ return override ? { ...base, ...override } : base;
40
+ }
41
+ function listSupportedProvidersForRole(role) {
42
+ const providers = listRegisteredProviderDefinitions()
43
+ .filter((definition) => {
44
+ const adapters = getProviderAdapterFactories(definition.id);
45
+ return role === 'coder' || role === 'planner'
46
+ ? adapters.createCoderAdapter
47
+ : adapters.createStructuredAdvisorAdapter;
48
+ })
49
+ .map((definition) => definition.id);
50
+ return providers.length > 0 ? providers.join(', ') : 'none';
51
+ }
52
+ function getCapabilityReason(requirementContext, capability) {
53
+ return requirementContext.reasons?.[capability] ?? requirementContext.reason;
54
+ }
55
+ function formatCapabilityName(capability) {
56
+ return capability.replaceAll('_', ' ');
57
+ }
58
+ function formatRoleConfigKey(role, key) {
59
+ return `agent.${role}.${key}`;
60
+ }
61
+ function formatConfiguredModel(config) {
62
+ return config.model === null ? 'null (provider default)' : JSON.stringify(config.model);
63
+ }
64
+ function throwProviderCapabilityError(args) {
65
+ const context = args.requirementContext.context ? ` Context: ${args.requirementContext.context}.` : '';
66
+ throw new Error([
67
+ `Provider capability error for ${args.requirementContext.role} role: configured provider ${JSON.stringify(args.config.provider)} is missing ${formatCapabilityName(args.missingCapability)}.`,
68
+ `Config keys: ${formatRoleConfigKey(args.requirementContext.role, 'provider')}=${JSON.stringify(args.config.provider)}, ${formatRoleConfigKey(args.requirementContext.role, 'model')}=${formatConfiguredModel(args.config)}.`,
69
+ `Neal needs this because ${getCapabilityReason(args.requirementContext, args.missingCapability)}.`,
70
+ `Registered providers: ${formatRegisteredProviderIds()}.`,
71
+ context.trim(),
72
+ ].filter(Boolean).join(' '));
73
+ }
74
+ function throwProviderEffortError(args) {
75
+ const role = args.requirementContext.role;
76
+ const provider = args.config.provider;
77
+ const hasSupported = Boolean(args.supportedEfforts && args.supportedEfforts.length > 0);
78
+ const supportedClause = hasSupported
79
+ ? `Supported effort values for ${provider}: ${args.supportedEfforts.join(', ')}.`
80
+ : `Supported effort values for ${provider}: (none).`;
81
+ const headline = hasSupported
82
+ ? `Provider config error for ${role} role: provider ${JSON.stringify(provider)} does not support effort ${JSON.stringify(args.effort)}.`
83
+ : `Provider config error for ${role} role: provider ${JSON.stringify(provider)} does not support configuring effort.`;
84
+ throw new Error([headline, supportedClause].join(' '));
85
+ }
86
+ function getDefinitionForCapabilityCheck(config, requirementContext) {
87
+ if (!isRegisteredProviderId(config.provider)) {
88
+ throwProviderCapabilityError({
89
+ requirementContext,
90
+ config,
91
+ missingCapability: 'registered_provider',
92
+ });
93
+ }
94
+ return getProviderDefinition(config.provider);
95
+ }
96
+ function assertRoleCapabilities(args) {
97
+ if (!args.capabilities.supported) {
98
+ throwProviderCapabilityError({
99
+ requirementContext: args.requirementContext,
100
+ config: args.config,
101
+ missingCapability: 'role_support',
102
+ });
103
+ }
104
+ if (args.requirementContext.requireReadToolAccess && !args.capabilities.toolAccess.read) {
105
+ throwProviderCapabilityError({
106
+ requirementContext: args.requirementContext,
107
+ config: args.config,
108
+ missingCapability: 'read_tool_access',
109
+ });
110
+ }
111
+ if (args.requirementContext.requireReadToolAccessOrInlineReviewerContext &&
112
+ !args.capabilities.toolAccess.read &&
113
+ !(args.capabilities.supported && args.capabilities.supportsStructuredOutput)) {
114
+ throwProviderCapabilityError({
115
+ requirementContext: args.requirementContext,
116
+ config: args.config,
117
+ missingCapability: 'read_tool_access_or_inline_reviewer_context',
118
+ });
119
+ }
120
+ if (args.requirementContext.requireWriteToolAccess && !args.capabilities.toolAccess.write) {
121
+ throwProviderCapabilityError({
122
+ requirementContext: args.requirementContext,
123
+ config: args.config,
124
+ missingCapability: 'write_tool_access',
125
+ });
126
+ }
127
+ if (args.requirementContext.requireStructuredOutput && !args.capabilities.supportsStructuredOutput) {
128
+ throwProviderCapabilityError({
129
+ requirementContext: args.requirementContext,
130
+ config: args.config,
131
+ missingCapability: 'structured_output',
132
+ });
133
+ }
134
+ if (args.config.model !== null && !args.capabilities.supportsModelOverride) {
135
+ throwProviderCapabilityError({
136
+ requirementContext: args.requirementContext,
137
+ config: args.config,
138
+ missingCapability: 'model_override',
139
+ });
140
+ }
141
+ const effort = args.config.effort ?? null;
142
+ if (effort !== null) {
143
+ const supportedEfforts = args.capabilities.supportedEfforts;
144
+ if (!supportedEfforts || supportedEfforts.length === 0 || !supportedEfforts.includes(effort)) {
145
+ throwProviderEffortError({
146
+ requirementContext: args.requirementContext,
147
+ config: args.config,
148
+ effort,
149
+ supportedEfforts,
150
+ });
151
+ }
152
+ }
153
+ if (args.requirementContext.requireSessionResume && !args.capabilities.supportsSessionResume) {
154
+ throwProviderCapabilityError({
155
+ requirementContext: args.requirementContext,
156
+ config: args.config,
157
+ missingCapability: 'session_resume',
158
+ });
159
+ }
160
+ }
161
+ // Registry invariant: every *supported* structured-advisor (reviewer)
162
+ // capability must be read-only. Reviewers inspect work and return verdicts;
163
+ // they never mutate the checkout or run shell commands. Only `write`/`shell`
164
+ // are constrained here — `read` is unconstrained (a reviewer may read the
165
+ // checkout directly, or be a no-read inline-context reviewer). The `coder`
166
+ // capability is intentionally not checked: coders keep write/shell.
167
+ export function assertStructuredAdvisorReadOnly(definition) {
168
+ const capability = definition.capabilities['structured-advisor'];
169
+ if (!capability.supported) {
170
+ return;
171
+ }
172
+ const { write, shell } = capability.toolAccess;
173
+ if (write || shell) {
174
+ throw new Error([
175
+ `Provider ${JSON.stringify(definition.id)} declares a structured-advisor capability that is not read-only:`,
176
+ `reviewers must have write:false and shell:false (read is unconstrained), but got write:${write}, shell:${shell}.`,
177
+ 'Reviewer rounds inspect work and return verdicts; only the coder capability may write or run shell.',
178
+ ].join(' '));
179
+ }
180
+ }
181
+ export function listRegisteredProviderDefinitions() {
182
+ const definitions = [
183
+ ...getBuiltInProviderDefinitions(),
184
+ ...providerDefinitionRegistrationsForTesting.values(),
185
+ ];
186
+ for (const definition of definitions) {
187
+ assertStructuredAdvisorReadOnly(definition);
188
+ }
189
+ return definitions;
190
+ }
191
+ export function listRegisteredProviderIds() {
192
+ return listRegisteredProviderDefinitions().map((definition) => definition.id);
193
+ }
194
+ export function formatRegisteredProviderIds() {
195
+ return listRegisteredProviderIds().join(', ');
196
+ }
197
+ export function isRegisteredProviderId(value) {
198
+ return typeof value === 'string' && getProviderDefinitionMap().has(value);
199
+ }
200
+ export function parseProviderId(value, context = 'provider') {
201
+ if (typeof value !== 'string' || !value.trim()) {
202
+ throw new Error(`Invalid provider for ${context}: ${formatProviderValue(value)}. Registered providers: ${formatRegisteredProviderIds()}`);
203
+ }
204
+ const provider = value.trim();
205
+ if (!isRegisteredProviderId(provider)) {
206
+ throw new Error(`Invalid provider for ${context}: ${JSON.stringify(provider)}. Registered providers: ${formatRegisteredProviderIds()}`);
207
+ }
208
+ return provider;
209
+ }
210
+ export function getProviderDefinition(id) {
211
+ const definition = getProviderDefinitionMap().get(id);
212
+ if (!definition) {
213
+ throw new Error(`Unsupported provider ${formatProviderValue(id)}. Registered providers: ${formatRegisteredProviderIds()}`);
214
+ }
215
+ return definition;
216
+ }
217
+ export function assertProviderSupportsCoder(config, requirementContext) {
218
+ const definition = getDefinitionForCapabilityCheck(config, requirementContext);
219
+ const adapters = getProviderAdapterFactories(config.provider);
220
+ const capabilities = definition.capabilities.coder;
221
+ assertRoleCapabilities({
222
+ config,
223
+ capabilities,
224
+ requirementContext,
225
+ });
226
+ if (!adapters.createCoderAdapter) {
227
+ throwProviderCapabilityError({
228
+ requirementContext,
229
+ config,
230
+ missingCapability: 'coder_adapter',
231
+ });
232
+ }
233
+ }
234
+ export function assertProviderSupportsStructuredAdvisor(config, requirementContext) {
235
+ const definition = getDefinitionForCapabilityCheck(config, requirementContext);
236
+ const adapters = getProviderAdapterFactories(config.provider);
237
+ const capabilities = definition.capabilities['structured-advisor'];
238
+ assertRoleCapabilities({
239
+ config,
240
+ capabilities,
241
+ requirementContext,
242
+ });
243
+ if (!adapters.createStructuredAdvisorAdapter) {
244
+ throwProviderCapabilityError({
245
+ requirementContext,
246
+ config,
247
+ missingCapability: 'structured_advisor_adapter',
248
+ });
249
+ }
250
+ }
251
+ export function assertAgentConfigSupportsWriterRun(agentConfig, options = {}) {
252
+ const context = options.context ?? 'writer run';
253
+ assertProviderSupportsCoder(agentConfig.coder, {
254
+ role: 'coder',
255
+ context,
256
+ reason: 'Neal writer runs need a coder provider that can modify the target checkout and return structured continuation payloads',
257
+ requireWriteToolAccess: true,
258
+ requireStructuredOutput: true,
259
+ reasons: {
260
+ coder_adapter: 'Neal starts coder turns through the configured coder adapter before any scope work can run',
261
+ write_tool_access: 'plan and execute work may edit plan files, source files, and verification artifacts',
262
+ structured_output: 'coder response, support response, blocked recovery, and final-completion paths use schema-validated structured coder payloads',
263
+ model_override: 'a non-null coder model override is configured for this writer run',
264
+ },
265
+ });
266
+ assertProviderSupportsCoder(agentConfig.planner, {
267
+ role: 'planner',
268
+ context,
269
+ reason: 'Neal planning runs need a planner provider backed by the coder adapter so plan authoring can edit plan artifacts and return structured continuation payloads',
270
+ requireWriteToolAccess: true,
271
+ requireStructuredOutput: true,
272
+ reasons: {
273
+ coder_adapter: 'Neal starts planner turns through the coder adapter before plan authoring can run',
274
+ write_tool_access: 'planning work may create, refine, and recover plan files',
275
+ structured_output: 'plan authoring and plan-response paths use schema-validated structured coder payloads',
276
+ model_override: 'a non-null planner model override is configured for this writer run',
277
+ },
278
+ });
279
+ assertProviderSupportsStructuredAdvisor(agentConfig.reviewer, {
280
+ role: 'reviewer',
281
+ context,
282
+ reason: 'Neal writer runs need a reviewer provider that can inspect work and return structured verdicts',
283
+ requireReadToolAccessOrInlineReviewerContext: true,
284
+ requireStructuredOutput: true,
285
+ reasons: {
286
+ structured_advisor_adapter: 'reviewer, plan-review, support, and final-completion verdicts run through the structured-advisor adapter',
287
+ read_tool_access_or_inline_reviewer_context: 'reviewer rounds must either inspect repository state, diffs, and run artifacts directly with read tools, or support structured output so Neal can supply inline reviewer context (full diff, plan document, run artifacts) in the prompt',
288
+ structured_output: 'reviewer rounds return schema-validated structured verdicts',
289
+ model_override: 'a non-null reviewer model override is configured for this writer run',
290
+ },
291
+ });
292
+ assertProviderSupportsStructuredAdvisor(agentConfig.coder, {
293
+ role: 'coder',
294
+ context,
295
+ reason: 'the final-completion summary currently uses the configured coder provider through the structured-advisor path',
296
+ requireStructuredOutput: true,
297
+ reasons: {
298
+ structured_advisor_adapter: 'runCoderFinalCompletionSummaryRound obtains a structured-advisor adapter from the coder provider',
299
+ structured_output: 'the final-completion summary must return a schema-validated structured payload',
300
+ model_override: 'a non-null coder model override is configured for the final-completion structured-advisor call',
301
+ },
302
+ });
303
+ }
304
+ export function assertAgentConfigSupportsResume(agentConfig, state, options = {}) {
305
+ const context = options.context ?? 'writer run resume';
306
+ assertAgentConfigSupportsWriterRun(agentConfig, { context });
307
+ if (state.plannerSessionHandle) {
308
+ assertProviderSupportsCoder(agentConfig.planner, {
309
+ role: 'planner',
310
+ context,
311
+ reason: 'the selected run has a persisted planner session handle that Neal must resume before continuing plan-authoring work',
312
+ requireSessionResume: true,
313
+ reasons: {
314
+ session_resume: `persisted planner session ${JSON.stringify(state.plannerSessionHandle)} is present`,
315
+ },
316
+ });
317
+ }
318
+ if (state.coderSessionHandle) {
319
+ assertProviderSupportsCoder(agentConfig.coder, {
320
+ role: 'coder',
321
+ context,
322
+ reason: 'the selected run has a persisted coder session handle that Neal must resume before continuing writer work',
323
+ requireSessionResume: true,
324
+ reasons: {
325
+ session_resume: `persisted coder session ${JSON.stringify(state.coderSessionHandle)} is present`,
326
+ },
327
+ });
328
+ }
329
+ if (state.reviewerSessionHandle) {
330
+ assertProviderSupportsStructuredAdvisor(agentConfig.reviewer, {
331
+ role: 'reviewer',
332
+ context,
333
+ reason: 'the selected run has a persisted reviewer session handle that Neal must resume before continuing reviewer work',
334
+ requireSessionResume: true,
335
+ reasons: {
336
+ session_resume: `persisted reviewer session ${JSON.stringify(state.reviewerSessionHandle)} is present`,
337
+ },
338
+ });
339
+ }
340
+ }
341
+ export function assertSupportedAgentConfig(agentConfig) {
342
+ assertAgentConfigSupportsWriterRun(agentConfig);
343
+ }
344
+ export function getCoderAdapter(config) {
345
+ const createCoderAdapter = getProviderAdapterFactories(config.provider).createCoderAdapter;
346
+ if (createCoderAdapter) {
347
+ return createCoderAdapter(config);
348
+ }
349
+ throw new Error(`Unsupported coder provider: ${config.provider}. Supported today: ${listSupportedProvidersForRole('coder')}`);
350
+ }
351
+ export function getStructuredAdvisorAdapter(config) {
352
+ const createStructuredAdvisorAdapter = getProviderAdapterFactories(config.provider).createStructuredAdvisorAdapter;
353
+ if (createStructuredAdvisorAdapter) {
354
+ return createStructuredAdvisorAdapter(config);
355
+ }
356
+ throw new Error(`Unsupported reviewer provider: ${config.provider}. Supported today: ${listSupportedProvidersForRole('reviewer')}`);
357
+ }
358
+ // The two mutators below can substitute adapter factories/definitions after the
359
+ // registry's read-only assertion has run, so they must be structurally
360
+ // unreachable outside the test runner: node:test sets NODE_TEST_CONTEXT in
361
+ // every test process, and no production code path sets it. The clear* helpers
362
+ // stay unguarded — they can only restore production state.
363
+ function assertTestRunnerContext(hook) {
364
+ if (process.env.NODE_TEST_CONTEXT === undefined) {
365
+ throw new Error(`${hook} is a test-only registry mutation hook and can only run under the node:test runner ` +
366
+ '(NODE_TEST_CONTEXT is not set). Production code must never override provider capabilities.');
367
+ }
368
+ }
369
+ export function setProviderCapabilitiesOverrideForTesting(provider, override) {
370
+ assertTestRunnerContext('setProviderCapabilitiesOverrideForTesting');
371
+ providerCapabilityOverrides.set(provider, override);
372
+ }
373
+ export function clearProviderCapabilitiesOverridesForTesting() {
374
+ providerCapabilityOverrides.clear();
375
+ }
376
+ export function registerProviderDefinitionForTesting(definition) {
377
+ assertTestRunnerContext('registerProviderDefinitionForTesting');
378
+ if (isRegisteredProviderId(definition.id)) {
379
+ throw new Error(`Provider ${JSON.stringify(definition.id)} is already registered`);
380
+ }
381
+ assertStructuredAdvisorReadOnly(definition);
382
+ providerDefinitionRegistrationsForTesting.set(definition.id, definition);
383
+ return () => {
384
+ providerDefinitionRegistrationsForTesting.delete(definition.id);
385
+ };
386
+ }
387
+ export function clearProviderDefinitionRegistrationsForTesting() {
388
+ providerDefinitionRegistrationsForTesting.clear();
389
+ }
@@ -0,0 +1,208 @@
1
+ import { writeDetail, writeErrorDetail } from '../diagnostic.js';
2
+ import { getHeadCommit } from '../git.js';
3
+ export function providerShortName(provider) {
4
+ switch (provider) {
5
+ case 'openai-codex':
6
+ return 'codex';
7
+ case 'anthropic-claude':
8
+ return 'claude';
9
+ default:
10
+ return provider;
11
+ }
12
+ }
13
+ function detailRole(event) {
14
+ return event.label ?? event.role;
15
+ }
16
+ function detailPrefix(event) {
17
+ const provider = providerShortName(event.provider);
18
+ if (event.provider === 'openai-codex' && event.role === 'coder' && !event.label) {
19
+ return `[${provider}]`;
20
+ }
21
+ return `[${provider}:${detailRole(event)}]`;
22
+ }
23
+ function errorDetailPrefix(event) {
24
+ const prefix = detailPrefix(event);
25
+ return `${prefix.slice(0, -1)}:error]`;
26
+ }
27
+ function formatExitCode(exitCode) {
28
+ return typeof exitCode === 'number' ? `exit ${exitCode}` : 'exit unknown';
29
+ }
30
+ async function getGitHeadOrNull(cwd) {
31
+ try {
32
+ return await getHeadCommit(cwd);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function commonEventData(event) {
39
+ return {
40
+ provider: event.provider,
41
+ role: event.role,
42
+ ...(event.label ? { label: event.label } : {}),
43
+ ...(event.sessionHandle !== undefined ? { sessionHandle: event.sessionHandle } : {}),
44
+ ...(event.providerData ? { providerData: event.providerData } : {}),
45
+ };
46
+ }
47
+ async function writeRunEvent(event, options) {
48
+ const logger = options.logger;
49
+ if (!logger) {
50
+ return;
51
+ }
52
+ switch (event.type) {
53
+ case 'session_started':
54
+ await logger.event('provider.session_started', {
55
+ ...commonEventData(event),
56
+ sessionHandle: event.sessionHandle,
57
+ });
58
+ break;
59
+ case 'turn_started':
60
+ await logger.event('provider.turn_started', commonEventData(event));
61
+ break;
62
+ case 'turn_completed':
63
+ await logger.event('provider.turn_completed', {
64
+ ...commonEventData(event),
65
+ ...(event.usage !== undefined ? { usage: event.usage } : {}),
66
+ });
67
+ break;
68
+ case 'tool_started':
69
+ await logger.event('provider.tool_started', {
70
+ ...commonEventData(event),
71
+ ...(event.toolName ? { toolName: event.toolName } : {}),
72
+ ...(event.itemId !== undefined ? { itemId: event.itemId } : {}),
73
+ });
74
+ break;
75
+ case 'tool_progress':
76
+ await logger.event('provider.tool_progress', {
77
+ ...commonEventData(event),
78
+ ...(event.toolName ? { toolName: event.toolName } : {}),
79
+ ...(event.itemId !== undefined ? { itemId: event.itemId } : {}),
80
+ ...(event.message ? { message: event.message } : {}),
81
+ ...(event.isError ? { isError: event.isError } : {}),
82
+ });
83
+ break;
84
+ case 'command_completed': {
85
+ const cwd = event.cwd ?? options.cwd;
86
+ const gitHead = event.gitHead ?? await getGitHeadOrNull(cwd);
87
+ await logger.event('provider.command_completed', {
88
+ ...commonEventData(event),
89
+ ...(event.itemId !== undefined ? { itemId: event.itemId } : {}),
90
+ command: event.command,
91
+ ...(event.status !== undefined ? { status: event.status } : {}),
92
+ ...(event.exitCode !== undefined ? { exitCode: event.exitCode } : {}),
93
+ outputLength: event.outputLength ?? Buffer.byteLength(event.output ?? '', 'utf8'),
94
+ cwd,
95
+ gitHead,
96
+ });
97
+ break;
98
+ }
99
+ case 'file_changed':
100
+ await logger.event('provider.file_changed', {
101
+ ...commonEventData(event),
102
+ files: event.files,
103
+ });
104
+ break;
105
+ case 'assistant_text':
106
+ await logger.event('provider.assistant_text', {
107
+ ...commonEventData(event),
108
+ text: event.text,
109
+ });
110
+ break;
111
+ case 'assistant_thinking':
112
+ await logger.event('provider.assistant_thinking', {
113
+ ...commonEventData(event),
114
+ ...(event.estimatedTokens !== undefined ? { estimatedTokens: event.estimatedTokens } : {}),
115
+ });
116
+ break;
117
+ case 'structured_output_received':
118
+ await logger.event('provider.structured_output_received', commonEventData(event));
119
+ break;
120
+ case 'usage_reported':
121
+ await logger.event('provider.usage_reported', {
122
+ ...commonEventData(event),
123
+ usage: event.usage,
124
+ });
125
+ break;
126
+ case 'provider_error':
127
+ await logger.event('provider.provider_error', {
128
+ ...commonEventData(event),
129
+ message: event.message,
130
+ ...(event.errorKind ? { errorKind: event.errorKind } : {}),
131
+ });
132
+ break;
133
+ }
134
+ }
135
+ function renderDetail(event, logger) {
136
+ const context = {
137
+ provider: event.provider,
138
+ role: detailRole(event),
139
+ };
140
+ switch (event.type) {
141
+ case 'session_started':
142
+ writeDetail(`${detailPrefix(event)} thread ${event.sessionHandle}\n`, logger, context);
143
+ break;
144
+ case 'tool_progress':
145
+ if (!event.message) {
146
+ break;
147
+ }
148
+ if (event.isError) {
149
+ const message = event.message.endsWith('\n') ? event.message : `${event.message}\n`;
150
+ writeErrorDetail(`${detailPrefix(event)} ${message}`, logger, context);
151
+ }
152
+ else {
153
+ writeDetail(`${detailPrefix(event)} ${event.message}\n`, logger, context);
154
+ }
155
+ break;
156
+ case 'command_completed':
157
+ writeDetail(`\n$ ${event.command}\n${detailPrefix(event)} command ${event.status ?? 'completed'} (${formatExitCode(event.exitCode)}, ${event.outputLength ?? Buffer.byteLength(event.output ?? '', 'utf8')} output bytes)\n`, logger, {
158
+ ...context,
159
+ commandSummary: event.command,
160
+ });
161
+ if (event.output) {
162
+ writeDetail(`${event.output}\n`, logger, {
163
+ ...context,
164
+ commandSummary: event.command,
165
+ });
166
+ }
167
+ break;
168
+ case 'file_changed':
169
+ writeDetail(`${detailPrefix(event)} files ${event.files.join(', ')}\n`, logger, {
170
+ ...context,
171
+ fileCount: event.files.length,
172
+ });
173
+ break;
174
+ case 'assistant_text':
175
+ writeDetail(`${detailPrefix(event)} ${event.text}\n`, logger, context);
176
+ break;
177
+ case 'assistant_thinking':
178
+ writeDetail(`${detailPrefix(event)} thinking${typeof event.estimatedTokens === 'number' ? ` (~${event.estimatedTokens} tokens)` : ''}\n`, logger, context);
179
+ break;
180
+ case 'turn_completed': {
181
+ const subtype = event.providerData?.subtype;
182
+ if (typeof subtype === 'string' && subtype.trim()) {
183
+ writeDetail(`${detailPrefix(event)} result: ${subtype}\n`, logger, context);
184
+ }
185
+ break;
186
+ }
187
+ case 'provider_error':
188
+ writeErrorDetail(`${errorDetailPrefix(event)} ${event.message}\n`, logger, context);
189
+ break;
190
+ case 'turn_started':
191
+ case 'tool_started':
192
+ case 'structured_output_received':
193
+ case 'usage_reported':
194
+ break;
195
+ }
196
+ }
197
+ export function createProviderTelemetrySink(options) {
198
+ return async (event) => {
199
+ const resolvedEvent = {
200
+ ...event,
201
+ provider: event.provider ?? options.provider,
202
+ role: event.role ?? options.role,
203
+ label: event.label ?? options.label,
204
+ };
205
+ renderDetail(resolvedEvent, options.logger);
206
+ await writeRunEvent(resolvedEvent, options);
207
+ };
208
+ }
@@ -0,0 +1,21 @@
1
+ export class NealProviderError extends Error {
2
+ provider;
3
+ role;
4
+ sessionHandle;
5
+ kind;
6
+ retryable;
7
+ cause;
8
+ constructor(args) {
9
+ super(args.message);
10
+ this.name = 'NealProviderError';
11
+ this.provider = args.provider;
12
+ this.role = args.role;
13
+ this.sessionHandle = args.sessionHandle ?? null;
14
+ this.kind = args.kind ?? 'unknown';
15
+ this.retryable = args.retryable ?? false;
16
+ this.cause = args.cause;
17
+ }
18
+ }
19
+ export function isNealProviderError(error) {
20
+ return error instanceof NealProviderError;
21
+ }
@@ -0,0 +1,50 @@
1
+ import { formatPublicPhase } from './phase-display.js';
2
+ function summarizeTurn(record, turn) {
3
+ const lines = [
4
+ `- Turn ${turn.number} guidance: ${turn.operatorGuidance}`,
5
+ ];
6
+ if (turn.disposition) {
7
+ lines.push(`- Turn ${turn.number} coder action: ${turn.disposition.action}`, `- Turn ${turn.number} coder summary: ${turn.disposition.summary}`, `- Turn ${turn.number} resulting step: ${formatPublicPhase(turn.disposition.resultingPhase)}`);
8
+ }
9
+ else if (turn.number <= record.lastHandledTurn) {
10
+ lines.push(`- Turn ${turn.number} coder response: handled without a persisted disposition summary`);
11
+ }
12
+ else {
13
+ lines.push(`- Turn ${turn.number} coder response: pending`);
14
+ }
15
+ return lines;
16
+ }
17
+ export function summarizeInteractiveBlockedRecoveryHistory(history) {
18
+ if (history.length === 0) {
19
+ return null;
20
+ }
21
+ const latest = history.at(-1) ?? null;
22
+ return {
23
+ sessions: history.length,
24
+ lastAction: latest?.resolvedByAction ?? null,
25
+ lastResultPhase: latest?.resultPhase ?? null,
26
+ lastBlockedReason: latest?.blockedReason ?? null,
27
+ lastOperatorGuidance: latest?.turns.at(-1)?.operatorGuidance ?? null,
28
+ lastCoderSummary: latest?.turns.at(-1)?.disposition?.summary ?? null,
29
+ };
30
+ }
31
+ export function renderInteractiveBlockedRecoveryHistoryLines(history, heading = '## Interactive Blocked Recovery History') {
32
+ if (history.length === 0) {
33
+ return [];
34
+ }
35
+ const lines = ['', heading];
36
+ for (const [index, record] of history.entries()) {
37
+ lines.push(`### Recovery Session ${index + 1}`, `- Source step: ${formatPublicPhase(record.sourcePhase)}`, `- Blocked reason: ${record.blockedReason}`, `- Turns used: ${record.turns.length}/${record.maxTurns}`, `- Resolved at: ${record.resolvedAt}`, `- Resolution: ${record.resolvedByAction}`, `- Result step: ${formatPublicPhase(record.resultPhase)}`);
38
+ if (record.pendingDirective) {
39
+ lines.push(`- Pending directive at resolution: ${record.pendingDirective.operatorGuidance}`);
40
+ }
41
+ if (record.turns.length === 0) {
42
+ lines.push('- Operator guidance: none recorded');
43
+ continue;
44
+ }
45
+ for (const turn of record.turns) {
46
+ lines.push(...summarizeTurn(record, turn));
47
+ }
48
+ }
49
+ return lines;
50
+ }