spec-and-loop 3.3.2 → 3.3.3
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/lib/mini-ralph/runner.js +257 -1
- package/package.json +1 -1
- package/scripts/mini-ralph-cli.js +27 -1
- package/scripts/ralph-run.sh +18 -0
package/lib/mini-ralph/runner.js
CHANGED
|
@@ -50,6 +50,10 @@ const DEFAULTS = {
|
|
|
50
50
|
// toward the streak because their signal is already surfaced via the
|
|
51
51
|
// `Recent Loop Signals` feedback block.
|
|
52
52
|
stallThreshold: 3,
|
|
53
|
+
// Opt-in continuation after a BLOCKED_HANDOFF only when the handoff note has
|
|
54
|
+
// explicit evidence for a safe, bounded resolution class.
|
|
55
|
+
autoResolveHandoffs: true,
|
|
56
|
+
autoResolveHandoffMaxPerRun: 6,
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -80,6 +84,170 @@ function _iterationIsStalled(iterationSignals) {
|
|
|
80
84
|
return true;
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
function _resolveAutoResolveHandoffConfig(options, existingState) {
|
|
88
|
+
const enabled = options.autoResolveHandoffs === true;
|
|
89
|
+
const maxPerRun =
|
|
90
|
+
Number.isInteger(options.autoResolveHandoffMaxPerRun) &&
|
|
91
|
+
options.autoResolveHandoffMaxPerRun > 0
|
|
92
|
+
? options.autoResolveHandoffMaxPerRun
|
|
93
|
+
: DEFAULTS.autoResolveHandoffMaxPerRun;
|
|
94
|
+
const previous =
|
|
95
|
+
existingState &&
|
|
96
|
+
existingState.autoResolveHandoffs &&
|
|
97
|
+
typeof existingState.autoResolveHandoffs === 'object'
|
|
98
|
+
? existingState.autoResolveHandoffs
|
|
99
|
+
: {};
|
|
100
|
+
const previousAttempts =
|
|
101
|
+
previous.attempts && typeof previous.attempts === 'object'
|
|
102
|
+
? previous.attempts
|
|
103
|
+
: {};
|
|
104
|
+
const previousTotal = Number.isInteger(previous.totalAttempts)
|
|
105
|
+
? previous.totalAttempts
|
|
106
|
+
: 0;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
enabled,
|
|
110
|
+
maxPerRun,
|
|
111
|
+
state: {
|
|
112
|
+
enabled,
|
|
113
|
+
maxPerRun,
|
|
114
|
+
totalAttempts: previousTotal,
|
|
115
|
+
attempts: Object.assign({}, previousAttempts),
|
|
116
|
+
lastDecision: previous.lastDecision || null,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _handoffHasFocusedVerifierEvidence(note) {
|
|
122
|
+
if (!note) return false;
|
|
123
|
+
const text = String(note);
|
|
124
|
+
const mentionsFocusedVerifier =
|
|
125
|
+
/\bfocused\b[\s\S]{0,500}\b(verifier|command|test|spec|vitest)\b/i.test(text) ||
|
|
126
|
+
/\b(verifier|command|test|spec|vitest)\b[\s\S]{0,500}\bfocused\b/i.test(text);
|
|
127
|
+
const saysFocusedPasses =
|
|
128
|
+
/\b(passes?|passed|exits?\s+0|exit(?:ed)?\s+0|green)\b/i.test(text);
|
|
129
|
+
const saysBroadFails =
|
|
130
|
+
/\b(broad|full|required|suite|repo-wide)\b[\s\S]{0,500}\b(fails?|failed|red|non[-\s]?zero)\b/i.test(text) ||
|
|
131
|
+
/\b(fails?|failed|red|non[-\s]?zero)\b[\s\S]{0,500}\b(broad|full|required|suite|repo-wide)\b/i.test(text);
|
|
132
|
+
const saysFailuresAreUnrelated =
|
|
133
|
+
/\b(unrelated|pre[-\s]?existing|out[-\s]?of[-\s]?scope|known failures?|not introduced|baseline)\b/i.test(text);
|
|
134
|
+
|
|
135
|
+
return mentionsFocusedVerifier && saysFocusedPasses && saysBroadFails && saysFailuresAreUnrelated;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _classifyAutoResolvableHandoff(blockerNote, baselineGateConflict) {
|
|
139
|
+
if (_handoffHasFocusedVerifierEvidence(blockerNote)) {
|
|
140
|
+
return {
|
|
141
|
+
className: 'verifier_narrowing',
|
|
142
|
+
summary: 'focused verifier passes while the broad verifier fails on unrelated/pre-existing failures',
|
|
143
|
+
allowedFiles: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
baselineGateConflict &&
|
|
149
|
+
baselineGateConflict.mode === 'authorized_cleanup' &&
|
|
150
|
+
baselineGateConflict.budgetUsed !== true &&
|
|
151
|
+
Array.isArray(baselineGateConflict.allowedFiles) &&
|
|
152
|
+
baselineGateConflict.allowedFiles.length > 0
|
|
153
|
+
) {
|
|
154
|
+
return {
|
|
155
|
+
className: 'authorized_cleanup',
|
|
156
|
+
summary: 'task text explicitly authorizes one cleanup attempt for named files',
|
|
157
|
+
allowedFiles: baselineGateConflict.allowedFiles.slice(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _autoResolveHandoffBudgetKey(currentTaskMeta, className) {
|
|
165
|
+
const taskId =
|
|
166
|
+
currentTaskMeta && currentTaskMeta.number
|
|
167
|
+
? currentTaskMeta.number
|
|
168
|
+
: currentTaskMeta && currentTaskMeta.description
|
|
169
|
+
? currentTaskMeta.description
|
|
170
|
+
: 'unknown-task';
|
|
171
|
+
return `${taskId}:${className || 'unknown'}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _decideAutoResolveHandoff(config, blockerNote, currentTaskMeta, baselineGateConflict) {
|
|
175
|
+
const disabledDecision = { allowed: false, reason: 'disabled', className: '', budgetKey: '' };
|
|
176
|
+
if (!config || config.enabled !== true) return disabledDecision;
|
|
177
|
+
|
|
178
|
+
const classification = _classifyAutoResolvableHandoff(blockerNote, baselineGateConflict);
|
|
179
|
+
if (!classification) {
|
|
180
|
+
return {
|
|
181
|
+
allowed: false,
|
|
182
|
+
reason: 'ambiguous_or_unsupported_handoff',
|
|
183
|
+
className: '',
|
|
184
|
+
budgetKey: '',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const budgetKey = _autoResolveHandoffBudgetKey(currentTaskMeta, classification.className);
|
|
189
|
+
const totalAttempts = Number.isInteger(config.state && config.state.totalAttempts)
|
|
190
|
+
? config.state.totalAttempts
|
|
191
|
+
: 0;
|
|
192
|
+
const maxPerRun = Number.isInteger(config.maxPerRun)
|
|
193
|
+
? config.maxPerRun
|
|
194
|
+
: DEFAULTS.autoResolveHandoffMaxPerRun;
|
|
195
|
+
const attempts = config.state && config.state.attempts ? config.state.attempts : {};
|
|
196
|
+
|
|
197
|
+
if (totalAttempts >= maxPerRun) {
|
|
198
|
+
return Object.assign({}, classification, {
|
|
199
|
+
allowed: false,
|
|
200
|
+
reason: 'global_budget_exhausted',
|
|
201
|
+
budgetKey,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (attempts[budgetKey]) {
|
|
206
|
+
return Object.assign({}, classification, {
|
|
207
|
+
allowed: false,
|
|
208
|
+
reason: 'task_class_budget_exhausted',
|
|
209
|
+
budgetKey,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return Object.assign({}, classification, {
|
|
214
|
+
allowed: true,
|
|
215
|
+
reason: 'authorized',
|
|
216
|
+
budgetKey,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _consumeAutoResolveHandoffBudget(config, decision, iteration) {
|
|
221
|
+
if (!config || !config.state || !decision || decision.allowed !== true || !decision.budgetKey) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const attempts = Object.assign({}, config.state.attempts || {});
|
|
226
|
+
attempts[decision.budgetKey] = {
|
|
227
|
+
className: decision.className,
|
|
228
|
+
iteration,
|
|
229
|
+
attemptedAt: new Date().toISOString(),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const totalAttempts = (Number.isInteger(config.state.totalAttempts)
|
|
233
|
+
? config.state.totalAttempts
|
|
234
|
+
: 0) + 1;
|
|
235
|
+
|
|
236
|
+
config.state = Object.assign({}, config.state, {
|
|
237
|
+
totalAttempts,
|
|
238
|
+
attempts,
|
|
239
|
+
lastDecision: {
|
|
240
|
+
className: decision.className,
|
|
241
|
+
reason: decision.reason,
|
|
242
|
+
budgetKey: decision.budgetKey,
|
|
243
|
+
iteration,
|
|
244
|
+
allowedFiles: decision.allowedFiles || [],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return config.state;
|
|
249
|
+
}
|
|
250
|
+
|
|
83
251
|
function _isFailedIteration(result) {
|
|
84
252
|
if (!result || typeof result !== 'object') return false;
|
|
85
253
|
if (result.signal !== null && result.signal !== undefined && result.signal !== '') {
|
|
@@ -462,6 +630,7 @@ async function run(opts) {
|
|
|
462
630
|
const resumeIteration = _resolveStartIteration(existingState, options);
|
|
463
631
|
const priorRunWasBlockedHandoff =
|
|
464
632
|
existingState && existingState.exitReason === 'blocked_handoff';
|
|
633
|
+
const autoResolveHandoffs = _resolveAutoResolveHandoffConfig(options, existingState);
|
|
465
634
|
|
|
466
635
|
if (options.verbose && resumeIteration > 1) {
|
|
467
636
|
process.stderr.write(
|
|
@@ -512,6 +681,7 @@ async function run(opts) {
|
|
|
512
681
|
stoppedAt: null,
|
|
513
682
|
exitReason: null,
|
|
514
683
|
pendingDirtyPaths,
|
|
684
|
+
autoResolveHandoffs: autoResolveHandoffs.state,
|
|
515
685
|
});
|
|
516
686
|
stateInitialized = true;
|
|
517
687
|
|
|
@@ -597,6 +767,7 @@ async function run(opts) {
|
|
|
597
767
|
fullHistory,
|
|
598
768
|
);
|
|
599
769
|
const baselineGateFeedback = _formatBaselineGateFeedback(baselineGateConflict);
|
|
770
|
+
const autoResolveHandoffFeedback = _buildAutoResolveHandoffFeedback(recentHistory);
|
|
600
771
|
|
|
601
772
|
// Inject any pending context
|
|
602
773
|
const pendingContext = context.consume(ralphDir);
|
|
@@ -612,6 +783,10 @@ async function run(opts) {
|
|
|
612
783
|
promptSections.push(`## Recent Loop Signals\n\n${iterationFeedback}`);
|
|
613
784
|
}
|
|
614
785
|
|
|
786
|
+
if (autoResolveHandoffFeedback) {
|
|
787
|
+
promptSections.push(`## Auto-Resolve Handoff\n\n${autoResolveHandoffFeedback}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
615
790
|
if (lessonsSection) {
|
|
616
791
|
promptSections.push(lessonsSection);
|
|
617
792
|
}
|
|
@@ -705,6 +880,24 @@ async function run(opts) {
|
|
|
705
880
|
const blockerNote = hasBlockedHandoff
|
|
706
881
|
? _extractBlockerNote(outputText, blockedHandoffPromise)
|
|
707
882
|
: '';
|
|
883
|
+
const autoResolveHandoffDecision = hasBlockedHandoff
|
|
884
|
+
? _decideAutoResolveHandoff(
|
|
885
|
+
autoResolveHandoffs,
|
|
886
|
+
blockerNote,
|
|
887
|
+
currentTaskMeta,
|
|
888
|
+
baselineGateConflict,
|
|
889
|
+
)
|
|
890
|
+
: null;
|
|
891
|
+
if (autoResolveHandoffDecision && autoResolveHandoffDecision.allowed) {
|
|
892
|
+
const nextAutoResolveState = _consumeAutoResolveHandoffBudget(
|
|
893
|
+
autoResolveHandoffs,
|
|
894
|
+
autoResolveHandoffDecision,
|
|
895
|
+
iterationCount,
|
|
896
|
+
);
|
|
897
|
+
if (nextAutoResolveState) {
|
|
898
|
+
state.update(ralphDir, { autoResolveHandoffs: nextAutoResolveState });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
708
901
|
const tasksAfter = options.tasksMode && options.tasksFile
|
|
709
902
|
? tasks.parseTasks(options.tasksFile)
|
|
710
903
|
: [];
|
|
@@ -787,6 +980,15 @@ async function run(opts) {
|
|
|
787
980
|
signal: result.signal || '',
|
|
788
981
|
failureStage: result.failureStage || '',
|
|
789
982
|
completedTasks: completedTasks.map((task) => task.fullDescription || task.description),
|
|
983
|
+
...(autoResolveHandoffDecision
|
|
984
|
+
? {
|
|
985
|
+
autoResolveHandoffAttempted: autoResolveHandoffDecision.allowed === true,
|
|
986
|
+
autoResolveHandoffClass: autoResolveHandoffDecision.className || '',
|
|
987
|
+
autoResolveHandoffReason: autoResolveHandoffDecision.reason || '',
|
|
988
|
+
autoResolveHandoffBudgetKey: autoResolveHandoffDecision.budgetKey || '',
|
|
989
|
+
autoResolveHandoffAllowedFiles: autoResolveHandoffDecision.allowedFiles || [],
|
|
990
|
+
}
|
|
991
|
+
: {}),
|
|
790
992
|
commitAttempted: commitResult.attempted,
|
|
791
993
|
commitCreated: commitResult.committed,
|
|
792
994
|
commitAnomaly: commitResult.anomaly ? commitResult.anomaly.message : '',
|
|
@@ -894,9 +1096,21 @@ async function run(opts) {
|
|
|
894
1096
|
reporter.note(
|
|
895
1097
|
handoffPath
|
|
896
1098
|
? `agent emitted ${blockedHandoffPromise}; blocker note saved to ${handoffPath}.`
|
|
897
|
-
: `agent emitted ${blockedHandoffPromise};
|
|
1099
|
+
: `agent emitted ${blockedHandoffPromise}; HANDOFF.md write failed (see stderr).`,
|
|
898
1100
|
'warn'
|
|
899
1101
|
);
|
|
1102
|
+
if (autoResolveHandoffDecision && autoResolveHandoffDecision.allowed) {
|
|
1103
|
+
reporter.note(
|
|
1104
|
+
`auto-resolve handoffs: continuing once for ${autoResolveHandoffDecision.className} (${autoResolveHandoffDecision.budgetKey}).`,
|
|
1105
|
+
'warn'
|
|
1106
|
+
);
|
|
1107
|
+
if (options.verbose) {
|
|
1108
|
+
process.stderr.write(
|
|
1109
|
+
`[mini-ralph] auto-resolve handoff consumed budget key ${autoResolveHandoffDecision.budgetKey}; continuing.\n`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
900
1114
|
if (options.verbose) {
|
|
901
1115
|
process.stderr.write(
|
|
902
1116
|
`[mini-ralph] ${blockedHandoffPromise} detected at iteration ${iterationCount}; halting.\n`
|
|
@@ -1780,6 +1994,42 @@ function _buildIterationFeedback(recentHistory, errorEntries, blockerArtifacts)
|
|
|
1780
1994
|
return sections.join('\n');
|
|
1781
1995
|
}
|
|
1782
1996
|
|
|
1997
|
+
function _buildAutoResolveHandoffFeedback(recentHistory) {
|
|
1998
|
+
if (!Array.isArray(recentHistory) || recentHistory.length === 0) return '';
|
|
1999
|
+
|
|
2000
|
+
const entry = recentHistory
|
|
2001
|
+
.slice()
|
|
2002
|
+
.reverse()
|
|
2003
|
+
.find((item) => item && item.autoResolveHandoffAttempted === true);
|
|
2004
|
+
|
|
2005
|
+
if (!entry) return '';
|
|
2006
|
+
|
|
2007
|
+
const className = entry.autoResolveHandoffClass || 'unknown';
|
|
2008
|
+
const lines = [
|
|
2009
|
+
`The previous iteration emitted BLOCKED_HANDOFF, but auto-resolution is enabled and spent its bounded attempt for ${className}.`,
|
|
2010
|
+
'You have exactly one continuation attempt for this task/blocker class. Do not broaden task scope, do not repair unrelated snapshots or UI behavior, and do not keep retrying if the evidence does not hold.',
|
|
2011
|
+
];
|
|
2012
|
+
|
|
2013
|
+
if (className === 'verifier_narrowing') {
|
|
2014
|
+
lines.push(
|
|
2015
|
+
'Allowed action: if the handoff explicitly names a focused verifier that passes and a broad verifier that fails only on unrelated/pre-existing failures, update only the current task verifier from the broad command to that focused command, run the focused command once, and complete the task only if it passes. If the focused command is absent, ambiguous, or fails, emit BLOCKED_HANDOFF instead of retrying.'
|
|
2016
|
+
);
|
|
2017
|
+
} else if (className === 'authorized_cleanup') {
|
|
2018
|
+
const files = Array.isArray(entry.autoResolveHandoffAllowedFiles)
|
|
2019
|
+
? entry.autoResolveHandoffAllowedFiles.filter(Boolean)
|
|
2020
|
+
: [];
|
|
2021
|
+
lines.push(
|
|
2022
|
+
`Allowed action: make one cleanup attempt only in the task-authorized file list${files.length > 0 ? ` (${files.join(', ')})` : ''}. If the gate still fails, emit BLOCKED_HANDOFF instead of continuing.`
|
|
2023
|
+
);
|
|
2024
|
+
} else {
|
|
2025
|
+
lines.push(
|
|
2026
|
+
'Allowed action: continue only if the blocker evidence remains explicit and within the runner-approved safe class; otherwise emit BLOCKED_HANDOFF.'
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
return lines.join('\n');
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1783
2033
|
function _buildBaselineGateFeedback(ralphDir, tasksFile, currentTaskMeta, recentHistory) {
|
|
1784
2034
|
return _formatBaselineGateFeedback(
|
|
1785
2035
|
_analyzeBaselineGateConflict(ralphDir, tasksFile, currentTaskMeta, recentHistory)
|
|
@@ -2392,6 +2642,12 @@ module.exports = {
|
|
|
2392
2642
|
_formatAutoCommitMessage,
|
|
2393
2643
|
_truncateSubjectSummary,
|
|
2394
2644
|
_buildIterationFeedback,
|
|
2645
|
+
_buildAutoResolveHandoffFeedback,
|
|
2646
|
+
_resolveAutoResolveHandoffConfig,
|
|
2647
|
+
_handoffHasFocusedVerifierEvidence,
|
|
2648
|
+
_classifyAutoResolvableHandoff,
|
|
2649
|
+
_decideAutoResolveHandoff,
|
|
2650
|
+
_consumeAutoResolveHandoffBudget,
|
|
2395
2651
|
_buildBaselineGateFeedback,
|
|
2396
2652
|
_analyzeBaselineGateConflict,
|
|
2397
2653
|
_formatBaselineGateFeedback,
|
package/package.json
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* Loop exits cleanly with `blocked_handoff` when the
|
|
28
28
|
* agent emits this tag and writes the agent's note
|
|
29
29
|
* to <ralph-dir>/HANDOFF.md.
|
|
30
|
+
* --auto-resolve-handoffs Enable bounded continuation attempts for
|
|
31
|
+
* explicit, safe BLOCKED_HANDOFF classes
|
|
32
|
+
* --no-auto-resolve-handoffs
|
|
33
|
+
* Disable auto-resolution even when enabled by env
|
|
30
34
|
* --no-commit Suppress auto-commit
|
|
31
35
|
* --model <name> Optional model override
|
|
32
36
|
* --verbose Verbose output
|
|
@@ -44,6 +48,11 @@ const miniRalph = require('../lib/mini-ralph/index');
|
|
|
44
48
|
// Argument parsing
|
|
45
49
|
// ---------------------------------------------------------------------------
|
|
46
50
|
|
|
51
|
+
function _envFlagDefaultEnabled(value) {
|
|
52
|
+
if (value === undefined) return true;
|
|
53
|
+
return !/^(0|false|no|off)$/i.test(String(value || '').trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
function parseArgs(argv) {
|
|
48
57
|
const args = argv.slice(2);
|
|
49
58
|
const opts = {
|
|
@@ -59,6 +68,7 @@ function parseArgs(argv) {
|
|
|
59
68
|
completionPromise: 'COMPLETE',
|
|
60
69
|
taskPromise: 'READY_FOR_NEXT_TASK',
|
|
61
70
|
blockedHandoffPromise: 'BLOCKED_HANDOFF',
|
|
71
|
+
autoResolveHandoffs: _envFlagDefaultEnabled(process.env.RALPH_AUTO_RESOLVE_HANDOFFS),
|
|
62
72
|
noCommit: false,
|
|
63
73
|
model: '',
|
|
64
74
|
verbose: false,
|
|
@@ -110,6 +120,12 @@ function parseArgs(argv) {
|
|
|
110
120
|
case '--blocked-handoff-promise':
|
|
111
121
|
opts.blockedHandoffPromise = args[++i];
|
|
112
122
|
break;
|
|
123
|
+
case '--auto-resolve-handoffs':
|
|
124
|
+
opts.autoResolveHandoffs = true;
|
|
125
|
+
break;
|
|
126
|
+
case '--no-auto-resolve-handoffs':
|
|
127
|
+
opts.autoResolveHandoffs = false;
|
|
128
|
+
break;
|
|
113
129
|
case '--no-commit':
|
|
114
130
|
opts.noCommit = true;
|
|
115
131
|
break;
|
|
@@ -165,6 +181,8 @@ Options:
|
|
|
165
181
|
--task-promise <s> Task promise string
|
|
166
182
|
--blocked-handoff-promise <s>
|
|
167
183
|
Blocked-handoff promise string (default: BLOCKED_HANDOFF)
|
|
184
|
+
--auto-resolve-handoffs Enable bounded continuation for explicit safe handoffs
|
|
185
|
+
--no-auto-resolve-handoffs Disable bounded continuation for explicit safe handoffs
|
|
168
186
|
--no-commit Suppress auto-commit
|
|
169
187
|
--model <name> Model override
|
|
170
188
|
--verbose Verbose output
|
|
@@ -224,6 +242,7 @@ async function main() {
|
|
|
224
242
|
completionPromise: opts.completionPromise,
|
|
225
243
|
taskPromise: opts.taskPromise,
|
|
226
244
|
blockedHandoffPromise: opts.blockedHandoffPromise,
|
|
245
|
+
autoResolveHandoffs: opts.autoResolveHandoffs,
|
|
227
246
|
noCommit: opts.noCommit,
|
|
228
247
|
model: opts.model,
|
|
229
248
|
verbose: opts.verbose,
|
|
@@ -251,4 +270,11 @@ async function main() {
|
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
main
|
|
273
|
+
if (require.main === module) {
|
|
274
|
+
main();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
_envFlagDefaultEnabled,
|
|
279
|
+
_parseArgs: parseArgs,
|
|
280
|
+
};
|
package/scripts/ralph-run.sh
CHANGED
|
@@ -129,6 +129,7 @@ resolve_ralph_command() {
|
|
|
129
129
|
CHANGE_NAME=""
|
|
130
130
|
MAX_ITERATIONS=""
|
|
131
131
|
NO_COMMIT=false
|
|
132
|
+
AUTO_RESOLVE_HANDOFFS=""
|
|
132
133
|
SHOW_STATUS=false
|
|
133
134
|
SHOW_VERSION=false
|
|
134
135
|
ADD_CONTEXT=""
|
|
@@ -186,6 +187,9 @@ OPTIONS:
|
|
|
186
187
|
--change <name> Specify the OpenSpec change to execute (default: auto-detect)
|
|
187
188
|
--max-iterations <n> Maximum iterations for Ralph loop (default: 50)
|
|
188
189
|
--no-commit Suppress automatic git commits during the loop
|
|
190
|
+
--auto-resolve-handoffs Enable bounded continuation for explicit safe handoffs
|
|
191
|
+
--no-auto-resolve-handoffs
|
|
192
|
+
Disable bounded continuation for explicit safe handoffs
|
|
189
193
|
--verbose, -v Enable verbose mode for debugging
|
|
190
194
|
--quiet Suppress the per-iteration progress stream
|
|
191
195
|
--version Print the version and exit
|
|
@@ -232,6 +236,14 @@ parse_arguments() {
|
|
|
232
236
|
NO_COMMIT=true
|
|
233
237
|
shift
|
|
234
238
|
;;
|
|
239
|
+
--auto-resolve-handoffs)
|
|
240
|
+
AUTO_RESOLVE_HANDOFFS=true
|
|
241
|
+
shift
|
|
242
|
+
;;
|
|
243
|
+
--no-auto-resolve-handoffs)
|
|
244
|
+
AUTO_RESOLVE_HANDOFFS=false
|
|
245
|
+
shift
|
|
246
|
+
;;
|
|
235
247
|
--verbose|-v)
|
|
236
248
|
VERBOSE=true
|
|
237
249
|
shift
|
|
@@ -1006,6 +1018,12 @@ Do not create git commits yourself. The Ralph runner manages automatic task comm
|
|
|
1006
1018
|
mini_ralph_args+=("--no-commit")
|
|
1007
1019
|
fi
|
|
1008
1020
|
|
|
1021
|
+
if [[ "$AUTO_RESOLVE_HANDOFFS" == true ]]; then
|
|
1022
|
+
mini_ralph_args+=("--auto-resolve-handoffs")
|
|
1023
|
+
elif [[ "$AUTO_RESOLVE_HANDOFFS" == false ]]; then
|
|
1024
|
+
mini_ralph_args+=("--no-auto-resolve-handoffs")
|
|
1025
|
+
fi
|
|
1026
|
+
|
|
1009
1027
|
if [[ "$VERBOSE" == true ]]; then
|
|
1010
1028
|
mini_ralph_args+=("--verbose")
|
|
1011
1029
|
fi
|