@navels/neal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
package/config.yml
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# neal runtime configuration template.
|
|
2
|
+
# This file is intentionally fully commented out:
|
|
3
|
+
# - uncomment any setting you want to override for this repository; repo config
|
|
4
|
+
# takes precedence over ~/.neal/config.yml
|
|
5
|
+
# - put machine-local overrides such as `neal.notify_bin` or role-specific model
|
|
6
|
+
# choices in ~/.neal/config.yml
|
|
7
|
+
# - run `neal setup` for first-time writer-run provider configuration
|
|
8
|
+
|
|
9
|
+
# neal:
|
|
10
|
+
# # Milliseconds between wrapper heartbeat messages on stderr.
|
|
11
|
+
# # Set to 0 to disable heartbeats entirely.
|
|
12
|
+
# phase_heartbeat_ms: 60000
|
|
13
|
+
#
|
|
14
|
+
# # Maximum reviewer rounds before neal blocks on non-converging review.
|
|
15
|
+
# max_review_rounds: 20
|
|
16
|
+
#
|
|
17
|
+
# # Consecutive rounds with no reduction in open blocking findings before
|
|
18
|
+
# # neal treats review as stuck and blocks early.
|
|
19
|
+
# review_stuck_window: 5
|
|
20
|
+
#
|
|
21
|
+
# # Milliseconds of inactivity before a coder or reviewer turn times out.
|
|
22
|
+
# inactivity_timeout_ms: 600000
|
|
23
|
+
#
|
|
24
|
+
# # Maximum retry attempts for transient API/internal provider failures.
|
|
25
|
+
# api_retry_limit: 10
|
|
26
|
+
#
|
|
27
|
+
# # Milliseconds an agent turn may stay silent after starting, with no
|
|
28
|
+
# # observable progress (no tool use, commands, file changes, or assistant
|
|
29
|
+
# # text), before neal aborts and retries the turn; once any progress is
|
|
30
|
+
# # observed, inactivity_timeout_ms governs instead.
|
|
31
|
+
# agent_turn_startup_timeout_ms: 300000
|
|
32
|
+
#
|
|
33
|
+
# # Maximum number of times neal retries an agent turn that was aborted for
|
|
34
|
+
# # startup silence before failing the turn.
|
|
35
|
+
# agent_turn_retry_limit: 1
|
|
36
|
+
#
|
|
37
|
+
# # Maximum operator guidance turns allowed during interactive blocked recovery
|
|
38
|
+
# # before neal requires a terminal disposition.
|
|
39
|
+
# interactive_blocked_recovery_max_turns: 3
|
|
40
|
+
#
|
|
41
|
+
# # Maximum number of times final completion review may send execution back
|
|
42
|
+
# # for more work before neal stops reopening the plan.
|
|
43
|
+
# final_completion_continue_execution_max: 2
|
|
44
|
+
#
|
|
45
|
+
# # Optional local notification command. Leave commented to keep
|
|
46
|
+
# # notifications disabled.
|
|
47
|
+
# # notify_bin: /absolute/path/to/notify
|
|
48
|
+
#
|
|
49
|
+
# agent:
|
|
50
|
+
# # Fresh writer-run commands require explicit coder and reviewer providers.
|
|
51
|
+
# # These commented examples are templates only; run `neal setup` to write
|
|
52
|
+
# # active provider defaults to ~/.neal/config.yml, or uncomment and edit the
|
|
53
|
+
# # role settings here for repository-level defaults.
|
|
54
|
+
# coder:
|
|
55
|
+
# # Provider for new plan/execute/run workflows.
|
|
56
|
+
# provider: openai-codex
|
|
57
|
+
#
|
|
58
|
+
# # Or run the coder against any OpenAI-compatible tool-calling endpoint
|
|
59
|
+
# # via the providers.openai_compatible block (see docs/providers.md):
|
|
60
|
+
# # provider: generic-agentic
|
|
61
|
+
#
|
|
62
|
+
# # Optional default model override for the coder role.
|
|
63
|
+
# # Leave null to let the provider choose its default model.
|
|
64
|
+
# model: null
|
|
65
|
+
#
|
|
66
|
+
# # Optional reasoning-effort override for the coder role.
|
|
67
|
+
# # Leave null or omit to use the provider default.
|
|
68
|
+
# # Supported values per provider:
|
|
69
|
+
# # openai-codex: minimal, low, medium, high, xhigh
|
|
70
|
+
# # anthropic-claude: low, medium, high, xhigh, max
|
|
71
|
+
# # An effort value not supported by the configured provider is rejected
|
|
72
|
+
# # before a run starts.
|
|
73
|
+
# effort: null
|
|
74
|
+
#
|
|
75
|
+
# reviewer:
|
|
76
|
+
# # Provider for the reviewer role.
|
|
77
|
+
# provider: anthropic-claude
|
|
78
|
+
#
|
|
79
|
+
# # Optional default model override for the reviewer role.
|
|
80
|
+
# # Leave null to let the provider choose its default model.
|
|
81
|
+
# model: null
|
|
82
|
+
#
|
|
83
|
+
# # Optional reasoning-effort override for the reviewer role.
|
|
84
|
+
# # Leave null or omit to use the provider default.
|
|
85
|
+
# # Supported values per provider:
|
|
86
|
+
# # openai-codex: minimal, low, medium, high, xhigh
|
|
87
|
+
# # anthropic-claude: low, medium, high, xhigh, max
|
|
88
|
+
# effort: null
|
|
89
|
+
#
|
|
90
|
+
# # The planner inherits the coder provider/model/effort unless configured
|
|
91
|
+
# # explicitly. Uncomment to override the planner independently.
|
|
92
|
+
# # planner:
|
|
93
|
+
# # provider: openai-codex
|
|
94
|
+
# # model: null
|
|
95
|
+
# # effort: null
|
|
96
|
+
#
|
|
97
|
+
# # Run headlessly when no operator is available to answer an operator block
|
|
98
|
+
# # (CI, cron, or a benchmark harness). When true, the execute-mode
|
|
99
|
+
# # interactive-recovery loop auto-resumes with conservative guidance for a
|
|
100
|
+
# # bounded number of turns and then fails cleanly, while the final-completion
|
|
101
|
+
# # and top-level plan-review gates fail cleanly and terminally instead of
|
|
102
|
+
# # waiting. Verification, authorization, and squash/grading are unchanged.
|
|
103
|
+
# # The `--unattended` CLI flag overrides this key. Defaults to false.
|
|
104
|
+
# unattended: false
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 1_000;
|
|
2
|
+
const DEFAULT_MIN_REDRAW_INTERVAL_MS = 100;
|
|
3
|
+
const DEFAULT_MIN_COLUMNS = 60;
|
|
4
|
+
function formatElapsed(elapsedMs) {
|
|
5
|
+
const totalSeconds = Math.max(0, Math.round(elapsedMs / 1_000));
|
|
6
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
7
|
+
const seconds = totalSeconds % 60;
|
|
8
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
9
|
+
}
|
|
10
|
+
function truncateForColumns(text, columns) {
|
|
11
|
+
if (columns <= 0 || text.length <= columns) {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
if (columns <= 3) {
|
|
15
|
+
return text.slice(0, columns);
|
|
16
|
+
}
|
|
17
|
+
return `${text.slice(0, columns - 3)}...`;
|
|
18
|
+
}
|
|
19
|
+
export function renderActivityFooterLine(state) {
|
|
20
|
+
const subject = state.subject ? ` ${state.subject}` : '';
|
|
21
|
+
const now = state.now ?? Date.now();
|
|
22
|
+
return [
|
|
23
|
+
`[neal] ${state.mode}${subject}`,
|
|
24
|
+
`activity: ${state.activity}`,
|
|
25
|
+
`elapsed: ${formatElapsed(now - state.startedAt)}`,
|
|
26
|
+
`status: ${state.status}`,
|
|
27
|
+
].join(' | ');
|
|
28
|
+
}
|
|
29
|
+
export class ActivityFooter {
|
|
30
|
+
stream;
|
|
31
|
+
now;
|
|
32
|
+
refreshIntervalMs;
|
|
33
|
+
minRedrawIntervalMs;
|
|
34
|
+
minColumns;
|
|
35
|
+
refreshTimer = null;
|
|
36
|
+
redrawTimer = null;
|
|
37
|
+
resizeListener = null;
|
|
38
|
+
currentLine = '';
|
|
39
|
+
footerVisible = false;
|
|
40
|
+
lastRedrawAt = 0;
|
|
41
|
+
state = null;
|
|
42
|
+
disposed = false;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.stream = options.stream ?? process.stderr;
|
|
45
|
+
this.now = options.now ?? (() => Date.now());
|
|
46
|
+
this.refreshIntervalMs = options.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;
|
|
47
|
+
this.minRedrawIntervalMs = options.minRedrawIntervalMs ?? DEFAULT_MIN_REDRAW_INTERVAL_MS;
|
|
48
|
+
this.minColumns = options.minColumns ?? DEFAULT_MIN_COLUMNS;
|
|
49
|
+
if (this.isEnabled() && this.stream === process.stderr) {
|
|
50
|
+
this.resizeListener = () => {
|
|
51
|
+
this.handleResize();
|
|
52
|
+
};
|
|
53
|
+
process.on('SIGWINCH', this.resizeListener);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
isEnabled() {
|
|
57
|
+
return this.stream.isTTY === true;
|
|
58
|
+
}
|
|
59
|
+
setState(state) {
|
|
60
|
+
if (this.disposed) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.state = state;
|
|
64
|
+
this.currentLine = renderActivityFooterLine({
|
|
65
|
+
...state,
|
|
66
|
+
now: this.now(),
|
|
67
|
+
});
|
|
68
|
+
this.ensureRefreshTimer();
|
|
69
|
+
this.renderFooter(true);
|
|
70
|
+
}
|
|
71
|
+
write(message) {
|
|
72
|
+
if (!this.isEnabled() || this.disposed) {
|
|
73
|
+
this.stream.write(message);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.clearFooter();
|
|
77
|
+
this.stream.write(message);
|
|
78
|
+
if (message.endsWith('\n') || message.endsWith('\r\n')) {
|
|
79
|
+
this.renderFooter(true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
replaceView(message) {
|
|
83
|
+
if (!this.isEnabled() || this.disposed) {
|
|
84
|
+
this.stream.write(message);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.clearFooter();
|
|
88
|
+
this.stream.write('\x1b[H\x1b[2J');
|
|
89
|
+
if (message !== '') {
|
|
90
|
+
this.stream.write(message);
|
|
91
|
+
}
|
|
92
|
+
this.renderFooter(true);
|
|
93
|
+
}
|
|
94
|
+
dispose() {
|
|
95
|
+
if (this.disposed) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.disposed = true;
|
|
99
|
+
if (this.refreshTimer) {
|
|
100
|
+
clearInterval(this.refreshTimer);
|
|
101
|
+
this.refreshTimer = null;
|
|
102
|
+
}
|
|
103
|
+
if (this.redrawTimer) {
|
|
104
|
+
clearTimeout(this.redrawTimer);
|
|
105
|
+
this.redrawTimer = null;
|
|
106
|
+
}
|
|
107
|
+
if (this.resizeListener) {
|
|
108
|
+
process.off('SIGWINCH', this.resizeListener);
|
|
109
|
+
this.resizeListener = null;
|
|
110
|
+
}
|
|
111
|
+
this.clearFooter();
|
|
112
|
+
}
|
|
113
|
+
handleResize() {
|
|
114
|
+
if (this.disposed) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!this.canRender()) {
|
|
118
|
+
this.clearFooter();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.refresh(true);
|
|
122
|
+
}
|
|
123
|
+
refresh(force = false) {
|
|
124
|
+
if (!this.state || this.disposed) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.currentLine = renderActivityFooterLine({
|
|
128
|
+
...this.state,
|
|
129
|
+
now: this.now(),
|
|
130
|
+
});
|
|
131
|
+
this.renderFooter(force);
|
|
132
|
+
}
|
|
133
|
+
ensureRefreshTimer() {
|
|
134
|
+
if (this.refreshTimer || this.disposed || this.refreshIntervalMs <= 0 || !this.isEnabled()) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.refreshTimer = setInterval(() => {
|
|
138
|
+
this.refresh();
|
|
139
|
+
}, this.refreshIntervalMs);
|
|
140
|
+
this.refreshTimer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
canRender() {
|
|
143
|
+
return (this.isEnabled() &&
|
|
144
|
+
!this.disposed &&
|
|
145
|
+
this.currentLine !== '' &&
|
|
146
|
+
(this.stream.columns ?? 0) >= this.minColumns);
|
|
147
|
+
}
|
|
148
|
+
renderFooter(force = false) {
|
|
149
|
+
if (!this.canRender()) {
|
|
150
|
+
this.clearFooter();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const elapsed = this.now() - this.lastRedrawAt;
|
|
154
|
+
if (!force && elapsed < this.minRedrawIntervalMs) {
|
|
155
|
+
if (!this.redrawTimer) {
|
|
156
|
+
this.redrawTimer = setTimeout(() => {
|
|
157
|
+
this.redrawTimer = null;
|
|
158
|
+
this.renderFooter(true);
|
|
159
|
+
}, this.minRedrawIntervalMs - elapsed);
|
|
160
|
+
this.redrawTimer.unref?.();
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.lastRedrawAt = this.now();
|
|
165
|
+
const columns = this.stream.columns ?? this.currentLine.length;
|
|
166
|
+
const line = truncateForColumns(this.currentLine, Math.max(0, columns - 1));
|
|
167
|
+
this.stream.write(`\r\x1b[2K${line}`);
|
|
168
|
+
this.footerVisible = true;
|
|
169
|
+
}
|
|
170
|
+
clearFooter() {
|
|
171
|
+
if (!this.footerVisible || !this.isEnabled()) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.stream.write('\r\x1b[2K');
|
|
175
|
+
this.footerVisible = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isExecuteFinalizationPhase } from '../execute-finalization.js';
|
|
2
|
+
import { renderAdjudicatedLoopContractLines } from './contracts.js';
|
|
3
|
+
import { resolveExecuteAdjudicationContext } from './execute.js';
|
|
4
|
+
import { resolvePlanningAdjudicationContext } from './planning.js';
|
|
5
|
+
import { getAdjudicationSpec } from './specs.js';
|
|
6
|
+
import { hasFinalCompletionReviewState } from '../state-views.js';
|
|
7
|
+
function resolveArtifactPhase(state) {
|
|
8
|
+
if (hasFinalCompletionReviewState(state)) {
|
|
9
|
+
return 'final_completion_review';
|
|
10
|
+
}
|
|
11
|
+
if (state.phase === 'blocked' && state.blockedFromPhase) {
|
|
12
|
+
return state.blockedFromPhase;
|
|
13
|
+
}
|
|
14
|
+
return state.phase;
|
|
15
|
+
}
|
|
16
|
+
export function resolveArtifactAdjudicationContract(state) {
|
|
17
|
+
const sourcePhase = resolveArtifactPhase(state);
|
|
18
|
+
if (sourcePhase === 'coder_plan' ||
|
|
19
|
+
sourcePhase === 'reviewer_plan' ||
|
|
20
|
+
sourcePhase === 'coder_plan_response' ||
|
|
21
|
+
sourcePhase === 'coder_plan_optional_response') {
|
|
22
|
+
return {
|
|
23
|
+
spec: resolvePlanningAdjudicationContext(state).spec,
|
|
24
|
+
sourcePhase,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (sourcePhase === 'coder_scope' ||
|
|
28
|
+
sourcePhase === 'reviewer_scope' ||
|
|
29
|
+
sourcePhase === 'coder_response' ||
|
|
30
|
+
sourcePhase === 'coder_optional_response' ||
|
|
31
|
+
isExecuteFinalizationPhase(sourcePhase)) {
|
|
32
|
+
return {
|
|
33
|
+
spec: resolveExecuteAdjudicationContext(state).spec,
|
|
34
|
+
sourcePhase,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (sourcePhase === 'final_completion_review') {
|
|
38
|
+
return {
|
|
39
|
+
spec: getAdjudicationSpec('final_completion_review'),
|
|
40
|
+
sourcePhase,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
export function renderAdjudicationContractLines(state) {
|
|
46
|
+
const contract = resolveArtifactAdjudicationContract(state);
|
|
47
|
+
if (!contract) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
'## Adjudication Contract',
|
|
52
|
+
`- Adjudication spec id: ${contract.spec.id}`,
|
|
53
|
+
`- Adjudication family: ${contract.spec.family}`,
|
|
54
|
+
...renderAdjudicatedLoopContractLines(contract.spec.loopContract),
|
|
55
|
+
`- Allowed transition outcomes: ${contract.spec.transitionSignals.join(', ')}`,
|
|
56
|
+
'- Contract role: validated allowed outcomes for debugging; runtime routing remains explicit elsewhere.',
|
|
57
|
+
];
|
|
58
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { runBlockedAdjudicatorRound } from '../agents/rounds.js';
|
|
3
|
+
import { createInlineSection, readTextForInlineSection, } from '../context/inline-review-context.js';
|
|
4
|
+
import { getChangedFilesForRange, getHeadCommit } from '../git.js';
|
|
5
|
+
import { getExecutionPlanPath } from '../scopes.js';
|
|
6
|
+
// Maximum number of most-recent reviewer rounds inlined as snapshots for the
|
|
7
|
+
// adjudicator. The deadlock signal is dominated by the latest rounds, so a small
|
|
8
|
+
// window keeps the prompt bounded while still showing how the disagreement
|
|
9
|
+
// evolved.
|
|
10
|
+
const RECENT_ROUND_SNAPSHOT_LIMIT = 5;
|
|
11
|
+
// The source phases the generalized adjudicator triages: a reviewer `review_stuck`
|
|
12
|
+
// deadlock (`reviewer_scope`/`reviewer_plan`) and a coder-blocked signal
|
|
13
|
+
// (`coder_scope`/`coder_response`/`coder_optional_response`, which after the
|
|
14
|
+
// split-plan reroute also carries the invalid-payload block). Every other accepted
|
|
15
|
+
// recovery source phase is ineligible and keeps today's generic recovery behavior;
|
|
16
|
+
// the recovery chokepoint enforces this gate.
|
|
17
|
+
export const ADJUDICATOR_ELIGIBLE_SOURCE_PHASES = new Set([
|
|
18
|
+
'reviewer_scope',
|
|
19
|
+
'reviewer_plan',
|
|
20
|
+
'coder_scope',
|
|
21
|
+
'coder_response',
|
|
22
|
+
'coder_optional_response',
|
|
23
|
+
]);
|
|
24
|
+
export function isReviewerAdjudicatorPhase(sourcePhase) {
|
|
25
|
+
return sourcePhase === 'reviewer_scope' || sourcePhase === 'reviewer_plan';
|
|
26
|
+
}
|
|
27
|
+
function isCoderAdjudicatorPhase(sourcePhase) {
|
|
28
|
+
return (sourcePhase === 'coder_scope' ||
|
|
29
|
+
sourcePhase === 'coder_response' ||
|
|
30
|
+
sourcePhase === 'coder_optional_response');
|
|
31
|
+
}
|
|
32
|
+
// --- Anti-thrash guard ------------------------------------------------------------
|
|
33
|
+
// These helpers are PURE and read-only. They never write or persist state; the
|
|
34
|
+
// recovery chokepoint is the sole writer of `state.recentBlocks`.
|
|
35
|
+
function normalizeBlockerKey(input, cwd) {
|
|
36
|
+
const escapedCwd = cwd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
return input
|
|
38
|
+
.replace(new RegExp(escapedCwd, 'gi'), '')
|
|
39
|
+
.replace(/(?:[A-Za-z]:)?(?:\/[^/\s:]+){2,}\/((?:src|test|tests|benchmark|docs|tmp|packages|lib|app)\/[^\s:]+)/g, '$1')
|
|
40
|
+
.replace(/[^\S\r\n]+/g, ' ')
|
|
41
|
+
.replace(/[!?.,;:]{2,}/g, (match) => match[0] ?? '')
|
|
42
|
+
.replace(/[!?.,;:]+(?=\s|$)/g, '')
|
|
43
|
+
.trim()
|
|
44
|
+
.toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
// The "new evidence" escape needs a signal that deterministically CHANGES when
|
|
47
|
+
// the underlying situation changes, even when the blocker wording does not. The
|
|
48
|
+
// blocker text itself cannot be that signal (subtracting the blocker from itself
|
|
49
|
+
// is always empty), so the fingerprint is derived from the one persisted artifact
|
|
50
|
+
// that moves exactly when the coder actually did new work: the scope's commit
|
|
51
|
+
// trail. `state.createdCommits` is append-only for the lifetime of the anti-thrash
|
|
52
|
+
// window (every transition that resets it also resets `recentBlocks`, and a scope
|
|
53
|
+
// boundary changes the scope identity anyway), and the coder phases append the
|
|
54
|
+
// blocked round's commits BEFORE the recovery chokepoint runs, so the tail hash
|
|
55
|
+
// alone captures "the coder committed new work since the recorded block".
|
|
56
|
+
// Deliberately NOT evidence: recovery turns/history (they accrue mechanically on
|
|
57
|
+
// every adjudicated block, so every repeat would escape and the guard would never
|
|
58
|
+
// fire) and LLM free-text summaries/rationales (never byte-stable across rounds,
|
|
59
|
+
// which would defeat the guard the same way).
|
|
60
|
+
function commitTrailEvidenceFingerprint(state) {
|
|
61
|
+
return state.createdCommits.at(-1) ?? '';
|
|
62
|
+
}
|
|
63
|
+
// Build the anti-thrash candidate identity for a block from its `reason` + the
|
|
64
|
+
// current scope identity. Identically worded blockers can hide genuinely
|
|
65
|
+
// different underlying causes, so the candidate also carries the commit-trail
|
|
66
|
+
// evidence fingerprint: when the coder has committed new work since a prior
|
|
67
|
+
// identical block, `findRepeatedRecentBlock` treats the block as carrying new
|
|
68
|
+
// evidence and the adjudicator runs instead of short-circuiting.
|
|
69
|
+
export function buildRecentBlockCandidate(state, reason, sourcePhase) {
|
|
70
|
+
const blocker = reason.trim();
|
|
71
|
+
return {
|
|
72
|
+
scopeNumber: state.currentScopeNumber,
|
|
73
|
+
derivedScopeIndex: state.derivedScopeIndex,
|
|
74
|
+
sourcePhase,
|
|
75
|
+
normalizedKey: normalizeBlockerKey(blocker, state.cwd),
|
|
76
|
+
evidenceFingerprint: commitTrailEvidenceFingerprint(state),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Return the most-recent window record that the candidate repeats, or `null` when
|
|
80
|
+
// there is no repeat. A match requires the SAME scope identity (`scopeNumber` +
|
|
81
|
+
// `derivedScopeIndex`), the same `sourcePhase`, and the same normalized blocker
|
|
82
|
+
// key. When that record exists but the candidate carries materially new evidence
|
|
83
|
+
// (a non-empty `evidenceFingerprint` that differs from the prior one — i.e. the
|
|
84
|
+
// coder has committed new work since the recorded block), it is NOT a repeat —
|
|
85
|
+
// the underlying situation has changed even though the blocker text has not — so
|
|
86
|
+
// `null` is returned and the adjudicator gets to look again.
|
|
87
|
+
export function findRepeatedRecentBlock(recentBlocks, candidate) {
|
|
88
|
+
for (const prior of [...recentBlocks].reverse()) {
|
|
89
|
+
if (prior.scopeNumber !== candidate.scopeNumber) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (prior.derivedScopeIndex !== candidate.derivedScopeIndex) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (prior.sourcePhase !== candidate.sourcePhase) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (prior.normalizedKey !== candidate.normalizedKey) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (candidate.evidenceFingerprint && candidate.evidenceFingerprint !== prior.evidenceFingerprint) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return prior;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// Return a NEW window array that either appends the candidate as a fresh record
|
|
109
|
+
// (`count:1`) or, on an exact match (per `findRepeatedRecentBlock`), increments the
|
|
110
|
+
// matched record's `count` and refreshes its `recordedAt`. Pure: the input array
|
|
111
|
+
// is never mutated.
|
|
112
|
+
export function upsertRecentBlock(recentBlocks, candidate, now = new Date().toISOString()) {
|
|
113
|
+
const match = findRepeatedRecentBlock(recentBlocks, candidate);
|
|
114
|
+
if (match) {
|
|
115
|
+
return recentBlocks.map((record) => record === match ? { ...record, count: record.count + 1, recordedAt: now } : record);
|
|
116
|
+
}
|
|
117
|
+
return [...recentBlocks, { ...candidate, count: 1, recordedAt: now }];
|
|
118
|
+
}
|
|
119
|
+
// --- Inline context ---------------------------------------------------------------
|
|
120
|
+
function renderOpenBlockingFindings(findings) {
|
|
121
|
+
if (findings.length === 0) {
|
|
122
|
+
return '(no open blocking findings recorded)';
|
|
123
|
+
}
|
|
124
|
+
return findings
|
|
125
|
+
.map((finding) => [
|
|
126
|
+
`- canonicalId: ${finding.canonicalId}`,
|
|
127
|
+
` claim: ${finding.claim}`,
|
|
128
|
+
` requiredAction: ${finding.requiredAction}`,
|
|
129
|
+
` coderDisposition: ${finding.coderDisposition ?? '(none recorded)'}`,
|
|
130
|
+
].join('\n'))
|
|
131
|
+
.join('\n');
|
|
132
|
+
}
|
|
133
|
+
function renderRecentRoundSnapshots(rounds) {
|
|
134
|
+
if (rounds.length === 0) {
|
|
135
|
+
return '(no reviewer rounds recorded)';
|
|
136
|
+
}
|
|
137
|
+
return rounds
|
|
138
|
+
.slice(-RECENT_ROUND_SNAPSHOT_LIMIT)
|
|
139
|
+
.map((round) => {
|
|
140
|
+
const openBlocking = round.openBlockingCanonicalIds ?? [];
|
|
141
|
+
const openBlockingText = openBlocking.length > 0 ? openBlocking.join(', ') : '(none)';
|
|
142
|
+
return `- round ${round.round}: openBlockingCanonicalIds: ${openBlockingText}`;
|
|
143
|
+
})
|
|
144
|
+
.join('\n');
|
|
145
|
+
}
|
|
146
|
+
async function buildReviewerInlineContext(state) {
|
|
147
|
+
const planContent = await readTextForInlineSection(resolve(state.cwd, getExecutionPlanPath(state)));
|
|
148
|
+
const openBlockingFindings = state.findings.filter((finding) => finding.severity === 'blocking' && finding.status === 'open');
|
|
149
|
+
return {
|
|
150
|
+
sections: [
|
|
151
|
+
createInlineSection('Execution plan content', planContent || '(plan document content unavailable)'),
|
|
152
|
+
createInlineSection('Open blocking findings', renderOpenBlockingFindings(openBlockingFindings)),
|
|
153
|
+
createInlineSection('Recent reviewer-round snapshots', renderRecentRoundSnapshots(state.rounds)),
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function readChangedFilesBestEffort(state) {
|
|
158
|
+
const base = state.baseCommit;
|
|
159
|
+
if (!base) {
|
|
160
|
+
return '(no scope base commit recorded)';
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const head = await getHeadCommit(state.cwd);
|
|
164
|
+
const files = await getChangedFilesForRange(state.cwd, base, head);
|
|
165
|
+
return files.length > 0 ? files.map((file) => `- ${file}`).join('\n') : '(no changed files in the current scope)';
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return '(changed-file context unavailable)';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function buildCoderInlineContext(state, reason) {
|
|
172
|
+
const planContent = await readTextForInlineSection(resolve(state.cwd, getExecutionPlanPath(state)));
|
|
173
|
+
const changedFiles = await readChangedFilesBestEffort(state);
|
|
174
|
+
return {
|
|
175
|
+
sections: [
|
|
176
|
+
createInlineSection('Execution plan content', planContent || '(plan document content unavailable)'),
|
|
177
|
+
createInlineSection('Coder blocker summary', reason.trim() || '(no blocker summary provided)'),
|
|
178
|
+
createInlineSection('Changed files since scope base', changedFiles),
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Always supplies a non-null InlineReviewerContext built entirely from in-memory
|
|
183
|
+
// OrchestrationState artifacts, so the adjudicator works for every reviewer
|
|
184
|
+
// provider including no-read providers. Throws for any source phase outside
|
|
185
|
+
// `ADJUDICATOR_ELIGIBLE_SOURCE_PHASES`, so an ineligible phase can never silently
|
|
186
|
+
// reach an LLM round.
|
|
187
|
+
async function buildAdjudicatorInlineContext(state, reason, sourcePhase) {
|
|
188
|
+
if (isReviewerAdjudicatorPhase(sourcePhase)) {
|
|
189
|
+
return buildReviewerInlineContext(state);
|
|
190
|
+
}
|
|
191
|
+
if (isCoderAdjudicatorPhase(sourcePhase)) {
|
|
192
|
+
return buildCoderInlineContext(state, reason);
|
|
193
|
+
}
|
|
194
|
+
throw new Error(`Blocked adjudicator cannot build context for ineligible source phase: ${String(sourcePhase)}`);
|
|
195
|
+
}
|
|
196
|
+
// Thin, read-only adjudicator for every triaged block class. It first applies the
|
|
197
|
+
// pure anti-thrash guard against `state.recentBlocks`: if this block repeats a
|
|
198
|
+
// recent block for the same scope identity + sourcePhase + normalized key with no
|
|
199
|
+
// new evidence, it returns a non-recoverable verdict WITHOUT running an LLM round.
|
|
200
|
+
// Otherwise it assembles the InlineReviewerContext for the source phase and runs
|
|
201
|
+
// the reviewer round. The module performs NO writes and NO commits — it only
|
|
202
|
+
// returns a `BlockedAdjudicatorVerdict`; `state.recentBlocks` is written elsewhere
|
|
203
|
+
// (the recovery chokepoint), never here.
|
|
204
|
+
export async function runBlockedAdjudicator(state, reason, sourcePhase, logger) {
|
|
205
|
+
const candidate = buildRecentBlockCandidate(state, reason, sourcePhase);
|
|
206
|
+
if (findRepeatedRecentBlock(state.recentBlocks, candidate)) {
|
|
207
|
+
return {
|
|
208
|
+
recoverable: false,
|
|
209
|
+
triageCategory: 'impossible_task',
|
|
210
|
+
resolutionDirective: '',
|
|
211
|
+
rationale: 'This blocker repeats a recent block for the same scope with no new evidence; the adjudicator short-circuited to avoid thrashing without re-running a reviewer round.',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const inlineContext = await buildAdjudicatorInlineContext(state, reason, sourcePhase);
|
|
215
|
+
const { verdict } = await runBlockedAdjudicatorRound({
|
|
216
|
+
reviewer: state.agentConfig.reviewer,
|
|
217
|
+
cwd: state.cwd,
|
|
218
|
+
blockedReason: reason,
|
|
219
|
+
inlineContext,
|
|
220
|
+
logger,
|
|
221
|
+
});
|
|
222
|
+
return verdict;
|
|
223
|
+
}
|