@hecer/yoke 1.19.0 → 1.21.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +4 -1
- package/bench/RESULTS.md +30 -0
- package/bench/analyze-codex-comparison.mjs +22 -0
- package/bench/compare-codex.mjs +71 -0
- package/bench/fixtures/independent-utils/.yoke/prd.yaml +30 -0
- package/bench/fixtures/independent-utils/bench-verify.mjs +4 -0
- package/bench/fixtures/independent-utils/package.json +1 -0
- package/bench/fixtures/independent-utils/src/chunk.mjs +1 -0
- package/bench/fixtures/independent-utils/src/range.mjs +1 -0
- package/bench/fixtures/independent-utils/src/unique.mjs +1 -0
- package/bench/fixtures/independent-utils/tests/STORY-1.test.mjs +8 -0
- package/bench/fixtures/independent-utils/tests/STORY-2.test.mjs +8 -0
- package/bench/fixtures/independent-utils/tests/STORY-3.test.mjs +8 -0
- package/bench/probe-goal-integration.mjs +93 -0
- package/bench/probe-parallel-gate-context.mjs +29 -0
- package/bench/results/codex-comparison-2026-09-29.json +305 -0
- package/bench/results/goal-integration-probes-2026-09-29.json +107 -0
- package/bench/results/native-goal-validation-2026-09-30.json +122 -0
- package/bench/results/parallel-validation-2026-09-30.json +86 -0
- package/dist/agents/process-record-identity.js +33 -0
- package/dist/check/command.js +218 -0
- package/dist/cli.js +85 -12
- package/dist/dashboard/server.js +26 -6
- package/dist/goals/codex-native.js +340 -0
- package/dist/goals/command.js +230 -29
- package/dist/loop/cleanup.js +110 -1
- package/dist/loop/dispatcher.js +82 -10
- package/dist/loop/git.js +1 -1
- package/dist/loop/parallel-adapters.js +53 -7
- package/dist/loop/parallel-command.js +8 -1
- package/dist/loop/recovery.js +81 -2
- package/dist/loop/reporter.js +3 -0
- package/dist/loop/resource-pool.js +21 -15
- package/dist/loop/run-command.js +98 -27
- package/dist/loop/run-state.js +116 -0
- package/dist/prd/explore.js +12 -4
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/config.js +1 -0
- package/dist/retrofit/gitignore.js +3 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- package/dist/setup/command.js +47 -5
- package/docs/CODEX-COMPARISON-2026-09-29.md +27 -0
- package/docs/CONTINUOUS-EXPLORATION.md +6 -2
- package/docs/GOALS-RESOURCE-AUDIT-2026-09-29.md +92 -0
- package/docs/GOALS.md +109 -0
- package/docs/RELEASE-VALIDATION-1.20.0.md +100 -0
- package/docs/VERIFIED-PROJECTS.md +4 -4
- package/docs/parallel-execution.md +15 -0
- package/docs/superpowers/plans/2026-09-30-goals-resources-release.md +78 -0
- package/gemini-extension.json +1 -1
- package/package.json +5 -1
package/dist/check/command.js
CHANGED
|
@@ -8,11 +8,159 @@ import { defaultConfig, loadConfig, resolveVerifyCommand } from '../retrofit/con
|
|
|
8
8
|
import { commandVerifier } from '../loop/verify.js';
|
|
9
9
|
import { workspaceFingerprint } from '../workspace/fingerprint.js';
|
|
10
10
|
import { statePath } from '../workspace/state.js';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { acquireSharedWorker } from '../loop/resource-pool.js';
|
|
13
|
+
import { compactCommandOutput } from '../output/compact.js';
|
|
14
|
+
import { writeOutputArtifact } from '../output/artifact.js';
|
|
15
|
+
import { DEFAULT_OUTPUT_POLICY } from '../output/types.js';
|
|
16
|
+
import { createProviderProcessRecord, filesystemProviderProcessRecordAdapter } from '../agents/process-record.js';
|
|
17
|
+
import { trackProcessRecordIdentity } from '../agents/process-record-identity.js';
|
|
11
18
|
const Criterion = z.object({ id: z.string().min(1).max(120), text: z.string().min(1).max(8000), commands: z.array(z.string().min(1).max(8000)).max(30) }).strict();
|
|
12
19
|
const Acceptance = z.object({ version: z.literal(1), criteria: z.array(Criterion).max(200), protected: z.array(z.string().min(1)).max(500).default([]) }).strict().superRefine((value, ctx) => {
|
|
13
20
|
if (new Set(value.criteria.map(c => c.id)).size !== value.criteria.length)
|
|
14
21
|
ctx.addIssue({ code: 'custom', message: 'Duplicate acceptance criterion id' });
|
|
15
22
|
});
|
|
23
|
+
export async function checkProjectAsync(directory, options = {}) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const abort = () => controller.abort(options.signal?.reason ?? 'Verification cancelled');
|
|
26
|
+
if (options.signal?.aborted)
|
|
27
|
+
abort();
|
|
28
|
+
else
|
|
29
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
30
|
+
const remaining = options.deadline === undefined ? undefined : options.deadline - Date.now();
|
|
31
|
+
if (remaining !== undefined && remaining <= 0)
|
|
32
|
+
controller.abort('Verification deadline reached');
|
|
33
|
+
const timer = remaining === undefined || remaining <= 0 ? undefined : setTimeout(() => controller.abort('Verification deadline reached'), remaining);
|
|
34
|
+
let lease;
|
|
35
|
+
let report;
|
|
36
|
+
let cleanupUnconfirmed = false;
|
|
37
|
+
try {
|
|
38
|
+
try {
|
|
39
|
+
lease = await acquireSharedWorker({ targetDir: directory, storyId: 'project-check', provider: 'codex', role: 'integration', resource: 'check', signal: controller.signal });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!controller.signal.aborted)
|
|
43
|
+
throw error;
|
|
44
|
+
// Persist failed evidence even when cancellation happens while queued.
|
|
45
|
+
}
|
|
46
|
+
if (options.deadline !== undefined && Date.now() >= options.deadline)
|
|
47
|
+
controller.abort('Verification deadline reached');
|
|
48
|
+
report = await checkProjectAsyncAdmitted(directory, { ...options, signal: controller.signal, onCleanupUnconfirmed: confirmed => { cleanupUnconfirmed = true; if (confirmed)
|
|
49
|
+
void confirmed.then(() => lease?.release()).catch(() => { }); } });
|
|
50
|
+
return report;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (timer)
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
options.signal?.removeEventListener('abort', abort);
|
|
56
|
+
if (!cleanupUnconfirmed)
|
|
57
|
+
await lease?.release();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function verifyCommandAsync(command, root, signal) {
|
|
61
|
+
if (signal?.aborted)
|
|
62
|
+
return Promise.resolve({ passed: false, summary: 'Verification cancelled or deadline reached' });
|
|
63
|
+
return new Promise(resolveResult => {
|
|
64
|
+
const child = spawn(command, { cwd: root, shell: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
65
|
+
const record = child.pid ? createProviderProcessRecord(realpathSync(root), child.pid, 'verification', `unverified:${new Date().toISOString()}`) : undefined;
|
|
66
|
+
let publishFailed = false;
|
|
67
|
+
if (record) {
|
|
68
|
+
try {
|
|
69
|
+
filesystemProviderProcessRecordAdapter.publish(record);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
publishFailed = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const cancelIdentity = trackProcessRecordIdentity(record, child);
|
|
76
|
+
let stdout = '', stderr = '', captured = 0, stopped = '', settled = false, exited = false, treeStopped = process.platform !== 'win32';
|
|
77
|
+
let resolveConfirmed = () => { };
|
|
78
|
+
const cleanupConfirmed = new Promise(resolveValue => { resolveConfirmed = resolveValue; });
|
|
79
|
+
const confirm = () => { if (exited && (!stopped || treeStopped)) {
|
|
80
|
+
cancelIdentity();
|
|
81
|
+
if (record)
|
|
82
|
+
filesystemProviderProcessRecordAdapter.remove(record.path);
|
|
83
|
+
resolveConfirmed();
|
|
84
|
+
} };
|
|
85
|
+
const finish = (passed, reason = '', cleanupUnconfirmed = false) => {
|
|
86
|
+
if (settled)
|
|
87
|
+
return;
|
|
88
|
+
settled = true;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
if (cleanupTimer)
|
|
91
|
+
clearTimeout(cleanupTimer);
|
|
92
|
+
signal?.removeEventListener('abort', abort);
|
|
93
|
+
cancelIdentity();
|
|
94
|
+
if (record && exited && !cleanupUnconfirmed)
|
|
95
|
+
filesystemProviderProcessRecordAdapter.remove(record.path);
|
|
96
|
+
if (passed) {
|
|
97
|
+
resolveResult({ passed: true, summary: `verify passed: ${command}` });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const raw = `=== stdout ===\n${stdout}\n=== stderr ===\n${stderr}`;
|
|
101
|
+
const compacted = compactCommandOutput(raw, { previewBytes: DEFAULT_OUTPUT_POLICY.previewBytes });
|
|
102
|
+
const parts = [`verify failed: ${command}${reason ? ` (${reason})` : ''}`, compacted.preview];
|
|
103
|
+
if (compacted.originalBytes > DEFAULT_OUTPUT_POLICY.artifactThresholdBytes) {
|
|
104
|
+
try {
|
|
105
|
+
parts.push(writeOutputArtifact(root, raw, { phase: 'verify', storyId: process.env.YOKE_STORY }).marker);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
parts.push('[artifact unavailable]');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
resolveResult({ passed: false, summary: parts.filter(Boolean).join('\n'), ...(cleanupUnconfirmed ? { cleanupUnconfirmed: true, cleanupConfirmed } : {}) });
|
|
112
|
+
};
|
|
113
|
+
let cleanupTimer;
|
|
114
|
+
const stop = (reason) => {
|
|
115
|
+
if (stopped || settled)
|
|
116
|
+
return;
|
|
117
|
+
stopped = reason;
|
|
118
|
+
if (child.pid) {
|
|
119
|
+
if (process.platform === 'win32') {
|
|
120
|
+
const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
121
|
+
killer.on('error', () => { });
|
|
122
|
+
killer.on('close', code => { treeStopped = code === 0; confirm(); if (exited && treeStopped)
|
|
123
|
+
finish(false, stopped); });
|
|
124
|
+
killer.unref();
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
child.kill('SIGKILL');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (process.platform !== 'win32')
|
|
136
|
+
child.kill('SIGKILL');
|
|
137
|
+
cleanupTimer = setTimeout(() => { if (process.platform !== 'win32')
|
|
138
|
+
child.kill('SIGKILL'); finish(false, `${stopped}; process cleanup could not be confirmed`, !exited || !treeStopped); }, 4000);
|
|
139
|
+
};
|
|
140
|
+
const abort = () => stop('cancelled or deadline reached');
|
|
141
|
+
const timer = setTimeout(() => stop('timed out'), 600_000);
|
|
142
|
+
const append = (stream, data) => {
|
|
143
|
+
const room = Math.max(0, 16 * 1024 * 1024 - captured);
|
|
144
|
+
captured += data.length;
|
|
145
|
+
if (stream === 'stdout')
|
|
146
|
+
stdout += data.subarray(0, room).toString('utf8');
|
|
147
|
+
else
|
|
148
|
+
stderr += data.subarray(0, room).toString('utf8');
|
|
149
|
+
if (captured > 16 * 1024 * 1024)
|
|
150
|
+
stop('capture limit exceeded');
|
|
151
|
+
};
|
|
152
|
+
child.stdout.on('data', data => append('stdout', data));
|
|
153
|
+
child.stderr.on('data', data => append('stderr', data));
|
|
154
|
+
child.on('error', () => finish(false, 'command could not start'));
|
|
155
|
+
child.on('close', code => { exited = true; confirm(); if (!stopped || treeStopped)
|
|
156
|
+
finish(!stopped && code === 0, stopped); });
|
|
157
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
158
|
+
if (signal?.aborted)
|
|
159
|
+
abort();
|
|
160
|
+
if (publishFailed)
|
|
161
|
+
stop('process ownership record could not be published');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
16
164
|
export function loadAcceptance(root) {
|
|
17
165
|
const file = statePath(root, 'acceptance.yaml');
|
|
18
166
|
return existsSync(file) ? Acceptance.parse(parse(readFileSync(file, 'utf8'))) : null;
|
|
@@ -112,3 +260,73 @@ export function checkProject(directory, options = {}) {
|
|
|
112
260
|
return report;
|
|
113
261
|
}
|
|
114
262
|
export function checkExitCode(report) { return report.status === 'passed' ? 0 : report.status === 'failed' ? 1 : 2; }
|
|
263
|
+
async function checkProjectAsyncAdmitted(directory, options) {
|
|
264
|
+
const root = realpathSync(directory);
|
|
265
|
+
const started = Date.now();
|
|
266
|
+
const before = workspaceFingerprint(root);
|
|
267
|
+
const problem = acceptanceProtectionProblem(root);
|
|
268
|
+
const criteria = [];
|
|
269
|
+
let cleanupUnconfirmed = false;
|
|
270
|
+
const rawExecute = options.execute ?? ((command, cwd) => verifyCommandAsync(command, cwd, options.signal));
|
|
271
|
+
const execute = async (command, cwd, signal) => {
|
|
272
|
+
if (cleanupUnconfirmed)
|
|
273
|
+
return { passed: false, summary: 'Previous verification cleanup unconfirmed; no further commands admitted', cleanupUnconfirmed: true };
|
|
274
|
+
const result = await rawExecute(command, cwd, signal);
|
|
275
|
+
if (result.cleanupUnconfirmed) {
|
|
276
|
+
cleanupUnconfirmed = true;
|
|
277
|
+
options.onCleanupUnconfirmed?.(result.cleanupConfirmed);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
};
|
|
281
|
+
if (problem)
|
|
282
|
+
criteria.push({ id: 'protected-acceptance', text: 'Acceptance infrastructure unchanged', commands: [], status: 'failed', summary: problem });
|
|
283
|
+
else {
|
|
284
|
+
const manifest = loadAcceptance(root);
|
|
285
|
+
for (const criterion of manifest?.criteria ?? []) {
|
|
286
|
+
const results = [];
|
|
287
|
+
for (const command of criterion.commands) {
|
|
288
|
+
results.push(await (async () => {
|
|
289
|
+
try {
|
|
290
|
+
if (options.signal?.aborted)
|
|
291
|
+
throw new Error('Verification cancelled or deadline reached');
|
|
292
|
+
return await execute(command, root, options.signal);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return { passed: false, summary: error.message };
|
|
296
|
+
}
|
|
297
|
+
})());
|
|
298
|
+
}
|
|
299
|
+
criteria.push({ ...criterion, status: results.length === 0 ? 'unverified' : results.every(r => r.passed) ? 'passed' : 'failed', summary: results.map(r => r.summary).join('\n') || 'No executable acceptance mapped' });
|
|
300
|
+
}
|
|
301
|
+
const command = resolveVerifyCommand(root, loadConfig(root) ?? defaultConfig('1.6.2'));
|
|
302
|
+
if (command) {
|
|
303
|
+
let result;
|
|
304
|
+
try {
|
|
305
|
+
if (options.signal?.aborted)
|
|
306
|
+
throw new Error('Verification cancelled or deadline reached');
|
|
307
|
+
result = await execute(command, root, options.signal);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
result = { passed: false, summary: error.message };
|
|
311
|
+
}
|
|
312
|
+
criteria.push({ id: 'project-suite', text: 'Configured project verification', commands: [command], status: result.passed ? 'passed' : 'failed', summary: result.summary });
|
|
313
|
+
}
|
|
314
|
+
if (options.requirement)
|
|
315
|
+
criteria.push({ id: 'requested-outcome', text: options.requirement, commands: [], status: 'unverified', summary: 'Map this outcome to executable criteria in .yoke/acceptance.yaml; a green suite alone is not proof of this requirement.' });
|
|
316
|
+
if (criteria.length === 0)
|
|
317
|
+
criteria.push({ id: 'acceptance', text: 'Project acceptance', commands: [], status: 'unverified', summary: 'No acceptance manifest or project verification command found' });
|
|
318
|
+
}
|
|
319
|
+
const changed = workspaceFingerprint(root) !== before;
|
|
320
|
+
const afterProblem = acceptanceProtectionProblem(root);
|
|
321
|
+
if (changed || afterProblem)
|
|
322
|
+
criteria.push({ id: 'source-integrity', text: 'Checked source remained stable', commands: [], status: 'failed', summary: afterProblem ?? 'Source changed during verification; run check again on a stable tree' });
|
|
323
|
+
if (options.signal?.aborted)
|
|
324
|
+
criteria.push({ id: 'verification-cancelled', text: 'Verification completed within its allowed run', commands: [], status: 'failed', summary: 'Verification cancelled or deadline reached' });
|
|
325
|
+
const status = criteria.some(c => c.status === 'failed') ? 'failed' : criteria.some(c => c.status === 'unverified') ? 'unverified' : 'passed';
|
|
326
|
+
const id = randomUUID();
|
|
327
|
+
const evidencePath = statePath(root, 'checks', `${id}.json`);
|
|
328
|
+
const report = { ...(cleanupUnconfirmed ? { cleanupUnconfirmed: true } : {}), version: 1, id, generatedAt: new Date().toISOString(), fingerprint: before, status, summary: changed ? 'Source changed during verification' : `${criteria.filter(c => c.status === 'passed').length}/${criteria.length} checks passed; ${status}`, criteria, durationMs: Date.now() - started, evidencePath };
|
|
329
|
+
mkdirSync(dirname(evidencePath), { recursive: true });
|
|
330
|
+
writeFileSync(evidencePath, JSON.stringify(report, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
331
|
+
return report;
|
|
332
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
|
|
|
5
5
|
import { checkProject, checkExitCode, protectAcceptance } from './check/command.js';
|
|
6
6
|
import { registerProject, listProjects, unregisterProject } from './dashboard/registry.js';
|
|
7
7
|
import { startDashboard } from './dashboard/server.js';
|
|
8
|
-
import { createProjectGoal, readProjectGoal, runProjectGoal, pauseProjectGoal, goalHandoff, budgetProjectGoal } from './goals/command.js';
|
|
8
|
+
import { createProjectGoal, readProjectGoal, runProjectGoal, pauseProjectGoal, goalHandoff, budgetProjectGoal, bindProjectGoal } from './goals/command.js';
|
|
9
9
|
import { validateCanon } from './canon/validate.js';
|
|
10
10
|
import { runRetrofit } from './retrofit/command.js';
|
|
11
11
|
import { setLoopEnabled, loopStatus, runLoopCommand } from './loop/run-command.js';
|
|
@@ -17,7 +17,7 @@ import { runNew } from './new/command.js';
|
|
|
17
17
|
import { runPrdDraft, runPrdCheck } from './prd/command.js';
|
|
18
18
|
import { runPrdAssess } from './prd/assess.js';
|
|
19
19
|
import { runPrdDecompose } from './prd/decompose.js';
|
|
20
|
-
import { runLoopCleanup } from './loop/cleanup.js';
|
|
20
|
+
import { runLoopCleanup, pruneWorktrees, pruneFleetWorktrees, listWorktrees, listFleetWorktrees } from './loop/cleanup.js';
|
|
21
21
|
import { runFlowSmoke } from './smoke/command.js';
|
|
22
22
|
import { maybeNotifyUpdate, currentYokeVersion } from './update/check.js';
|
|
23
23
|
import { runUpgrade } from './update/upgrade.js';
|
|
@@ -62,6 +62,22 @@ export function runDesignScan(targetDir, opts) {
|
|
|
62
62
|
console.log(`${label} — ✓`);
|
|
63
63
|
return 0;
|
|
64
64
|
}
|
|
65
|
+
export function parseKeyValueFlags(args, flagName) {
|
|
66
|
+
const result = {};
|
|
67
|
+
for (const arg of args) {
|
|
68
|
+
if (arg.startsWith(`--${flagName}=`)) {
|
|
69
|
+
const raw = arg.slice(flagName.length + 3);
|
|
70
|
+
const eqIdx = raw.indexOf(':');
|
|
71
|
+
if (eqIdx > 0) {
|
|
72
|
+
const key = raw.slice(0, eqIdx).trim();
|
|
73
|
+
const val = raw.slice(eqIdx + 1).trim();
|
|
74
|
+
if (key && val)
|
|
75
|
+
result[key] = val;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
65
81
|
export function parseQualityFlags(args) {
|
|
66
82
|
const quality = args.includes('--quality') ? true : args.includes('--no-quality') ? false : undefined;
|
|
67
83
|
const qualityUnbounded = args.includes('--quality-unbounded') || undefined;
|
|
@@ -185,9 +201,16 @@ export function main(argv) {
|
|
|
185
201
|
console.error('Invalid --model-provider (expected deepseek,kimi)');
|
|
186
202
|
return 1;
|
|
187
203
|
}
|
|
204
|
+
const runnerModel = rest.find(a => a.startsWith('--runner-model='))?.slice('--runner-model='.length);
|
|
205
|
+
const runnerReasoning = rest.find(a => a.startsWith('--runner-reasoning='))?.slice('--runner-reasoning='.length);
|
|
206
|
+
const agentModels = parseKeyValueFlags(rest, 'model');
|
|
207
|
+
const agentReasoning = parseKeyValueFlags(rest, 'reasoning');
|
|
208
|
+
const cleanWorktrees = rest.includes('--clean-worktrees');
|
|
188
209
|
return runSetup(targetDir, {
|
|
189
210
|
modelProviders: modelProviders,
|
|
190
211
|
host: hostArg, agents, runner: runnerArg,
|
|
212
|
+
runnerModel, runnerReasoning, agentModels, agentReasoning, cleanWorktrees,
|
|
213
|
+
configureModels: rest.includes('--configure-models'),
|
|
191
214
|
codeGraph: graphArg,
|
|
192
215
|
codeIntelligence: intelligenceArg,
|
|
193
216
|
loop, routing, decisionPolicy: policyArg,
|
|
@@ -218,6 +241,47 @@ export function main(argv) {
|
|
|
218
241
|
return 2;
|
|
219
242
|
}
|
|
220
243
|
}
|
|
244
|
+
case 'worktrees': {
|
|
245
|
+
const sub = rest[0] ?? 'list';
|
|
246
|
+
const targetDir = rest.slice(1).find(a => !a.startsWith('-')) ?? '.';
|
|
247
|
+
const all = rest.includes('--all');
|
|
248
|
+
const force = rest.includes('--force');
|
|
249
|
+
if (sub === 'list') {
|
|
250
|
+
if (all) {
|
|
251
|
+
const fleet = listFleetWorktrees();
|
|
252
|
+
for (const [name, list] of Object.entries(fleet)) {
|
|
253
|
+
console.log(`[${name}] ${list.length} worktree(s):`);
|
|
254
|
+
for (const wt of list)
|
|
255
|
+
console.log(` ${wt}`);
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
const list = listWorktrees(targetDir);
|
|
260
|
+
console.log(`Worktrees in ${targetDir} (${list.length}):`);
|
|
261
|
+
for (const wt of list)
|
|
262
|
+
console.log(` ${wt}`);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
if (sub === 'prune') {
|
|
266
|
+
if (all) {
|
|
267
|
+
const fleet = pruneFleetWorktrees({ force });
|
|
268
|
+
let totalRemoved = 0;
|
|
269
|
+
for (const [name, res] of Object.entries(fleet)) {
|
|
270
|
+
if (res.removed.length > 0 || res.failed.length > 0) {
|
|
271
|
+
console.log(`[${name}] Removed: ${res.removed.length}, Failed: ${res.failed.length}`);
|
|
272
|
+
totalRemoved += res.removed.length;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
console.log(`Fleet prune complete: ${totalRemoved} worktree(s) removed.`);
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const res = pruneWorktrees(targetDir, { force });
|
|
279
|
+
console.log(`Pruned ${res.removed.length} worktree(s) in ${targetDir}${res.failed.length > 0 ? ` (${res.failed.length} failed)` : ''}.`);
|
|
280
|
+
return res.failed.length === 0 ? 0 : 1;
|
|
281
|
+
}
|
|
282
|
+
console.log('usage: yoke worktrees <list|prune> [dir] [--all] [--force]');
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
221
285
|
case 'dashboard': {
|
|
222
286
|
const port = rest.find(a => a.startsWith('--port='))?.slice('--port='.length);
|
|
223
287
|
if (port !== undefined && (!Number.isInteger(Number(port)) || Number(port) < 0 || Number(port) > 65535)) {
|
|
@@ -250,7 +314,7 @@ export function main(argv) {
|
|
|
250
314
|
const objective = value('objective');
|
|
251
315
|
if (!objective)
|
|
252
316
|
throw new Error('Use goal set [dir] --objective="..." with executable .yoke/acceptance.yaml criteria');
|
|
253
|
-
const goal = createProjectGoal(targetDir, objective, { maxAttempts: value('attempts') ? Number(value('attempts')) : undefined, maxMinutes: value('minutes') ? Number(value('minutes')) : undefined, tokenBudget: value('tokens') ? Number(value('tokens')) : undefined });
|
|
317
|
+
const goal = createProjectGoal(targetDir, objective, { maxAttempts: value('attempts') ? Number(value('attempts')) : undefined, maxMinutes: value('minutes') ? Number(value('minutes')) : undefined, maxWallMinutes: value('wall-minutes') ? Number(value('wall-minutes')) : undefined, tokenBudget: value('tokens') ? Number(value('tokens')) : undefined, acceptanceIds: value('criteria')?.split(',') });
|
|
254
318
|
console.log(JSON.stringify(goal));
|
|
255
319
|
return 0;
|
|
256
320
|
}
|
|
@@ -258,8 +322,12 @@ export function main(argv) {
|
|
|
258
322
|
console.log(JSON.stringify(readProjectGoal(targetDir)));
|
|
259
323
|
return 0;
|
|
260
324
|
}
|
|
325
|
+
if (sub === 'bind') {
|
|
326
|
+
console.log(JSON.stringify(bindProjectGoal(targetDir, value('criteria')?.split(',') ?? [])));
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
261
329
|
if (sub === 'budget') {
|
|
262
|
-
console.log(JSON.stringify(budgetProjectGoal(targetDir, { maxAttempts: value('attempts') ? Number(value('attempts')) : undefined, maxMinutes: value('minutes') ? Number(value('minutes')) : undefined, tokenBudget: value('tokens') ? Number(value('tokens')) : undefined, clearTokenBudget: rest.includes('--clear-token-budget') })));
|
|
330
|
+
console.log(JSON.stringify(budgetProjectGoal(targetDir, { maxAttempts: value('attempts') ? Number(value('attempts')) : undefined, maxMinutes: value('minutes') ? Number(value('minutes')) : undefined, maxWallMinutes: value('wall-minutes') ? Number(value('wall-minutes')) : undefined, tokenBudget: value('tokens') ? Number(value('tokens')) : undefined, clearTokenBudget: rest.includes('--clear-token-budget') })));
|
|
263
331
|
return 0;
|
|
264
332
|
}
|
|
265
333
|
if (sub === 'handoff') {
|
|
@@ -272,15 +340,15 @@ export function main(argv) {
|
|
|
272
340
|
return 0;
|
|
273
341
|
}
|
|
274
342
|
if (sub === 'run' || sub === 'resume') {
|
|
275
|
-
const provider = value('runner')
|
|
276
|
-
if (!SUPPORTED_AGENTS.includes(provider))
|
|
343
|
+
const provider = value('runner');
|
|
344
|
+
if (provider && !SUPPORTED_AGENTS.includes(provider))
|
|
277
345
|
throw new Error('Unknown runner');
|
|
278
|
-
return runProjectGoal(targetDir, { provider: provider, selection: { model: value('model') } }).then(goal => {
|
|
346
|
+
return runProjectGoal(targetDir, { provider: provider, selection: { model: value('model'), reasoningEffort: value('effort'), bare: rest.includes('--bare') ? true : undefined }, native: rest.includes('--native-goal') ? true : rest.includes('--no-native-goal') ? false : undefined }).then(goal => {
|
|
279
347
|
console.log(JSON.stringify(goal));
|
|
280
348
|
return goal.status === 'complete' ? 0 : 1;
|
|
281
349
|
}).catch(error => { console.error(`Goal: ${error.message}`); return 2; });
|
|
282
350
|
}
|
|
283
|
-
throw new Error('Use goal set|status|run|resume|pause|handoff');
|
|
351
|
+
throw new Error('Use goal set|bind|status|run|resume|pause|handoff|budget');
|
|
284
352
|
}
|
|
285
353
|
catch (error) {
|
|
286
354
|
console.error(`Goal: ${error.message}`);
|
|
@@ -315,6 +383,11 @@ export function main(argv) {
|
|
|
315
383
|
case 'retrofit': {
|
|
316
384
|
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
|
|
317
385
|
const loop = rest.includes('--loop');
|
|
386
|
+
const cleanWorktrees = rest.includes('--clean-worktrees');
|
|
387
|
+
const runnerModel = rest.find(a => a.startsWith('--runner-model='))?.slice('--runner-model='.length);
|
|
388
|
+
const runnerReasoning = rest.find(a => a.startsWith('--runner-reasoning='))?.slice('--runner-reasoning='.length);
|
|
389
|
+
const agentModels = parseKeyValueFlags(rest, 'model');
|
|
390
|
+
const agentReasoning = parseKeyValueFlags(rest, 'reasoning');
|
|
318
391
|
const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
|
|
319
392
|
const all = [...SUPPORTED_AGENTS];
|
|
320
393
|
const agents = !agentArg || agentArg === 'all'
|
|
@@ -334,7 +407,7 @@ export function main(argv) {
|
|
|
334
407
|
console.error(`Invalid --code-intelligence value: ${ciArg} (expected off|shadow|active)`);
|
|
335
408
|
return 1;
|
|
336
409
|
}
|
|
337
|
-
return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg });
|
|
410
|
+
return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg, cleanWorktrees, runnerModel, runnerReasoning, agentModels, agentReasoning });
|
|
338
411
|
}
|
|
339
412
|
case 'change': {
|
|
340
413
|
const sub = rest[0];
|
|
@@ -383,7 +456,7 @@ export function main(argv) {
|
|
|
383
456
|
return 0;
|
|
384
457
|
}
|
|
385
458
|
if (sub === 'status') {
|
|
386
|
-
console.log(loopStatus(targetDir));
|
|
459
|
+
console.log(loopStatus(targetDir, undefined, { compact: rest.includes('--compact') }));
|
|
387
460
|
return 0;
|
|
388
461
|
}
|
|
389
462
|
if (sub === 'pause') {
|
|
@@ -614,7 +687,7 @@ export function main(argv) {
|
|
|
614
687
|
}
|
|
615
688
|
return runLoopCommand(targetDir, { maxIterations: rawMax, agent, isolate, resumeWorktree: rest.includes('--resume-worktree'), parallel, parallelAuto: parallelArg === '--parallel=auto', reviewer, review, allowSelfReview, timeoutMinutes, json, routing, onAmbiguity: oaArg, decisionPolicy: dpArg, permissions, ...(explore ? { explore: true } : {}), ...(exploreIntervalMinutes !== undefined ? { exploreIntervalMinutes } : {}), ...(exploreLimit.milliseconds !== undefined ? { exploreLimitMs: exploreLimit.milliseconds } : {}), ...qualityFlags.options });
|
|
616
689
|
}
|
|
617
|
-
console.log(`usage: yoke loop <on|off|status|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
|
|
690
|
+
console.log(`usage: yoke loop <on|off|status [--compact]|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
|
|
618
691
|
return 1;
|
|
619
692
|
}
|
|
620
693
|
case 'new': {
|
|
@@ -760,7 +833,7 @@ export function main(argv) {
|
|
|
760
833
|
return runUpgrade();
|
|
761
834
|
default:
|
|
762
835
|
console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
|
|
763
|
-
console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--loop] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
|
|
836
|
+
console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--clean-worktrees] [--runner-model=<model>] [--runner-reasoning=<effort>] [--model=<agent>:<model>] [--reasoning=<agent>:<effort>] [--loop] | worktrees <list|prune> [dir] [--all] [--force] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
|
|
764
837
|
return cmd ? 1 : 0;
|
|
765
838
|
}
|
|
766
839
|
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -10,7 +10,8 @@ import { dashboardPage } from './page.js';
|
|
|
10
10
|
import { readEvents } from '../observability/events.js';
|
|
11
11
|
import { estimateSchedule } from '../estimation/schedule.js';
|
|
12
12
|
import { pauseProjectGoal, runProjectGoal } from '../goals/command.js';
|
|
13
|
-
import { runLoopCommand } from '../loop/run-command.js';
|
|
13
|
+
import { runLoopCommand, resumeLoopCommand } from '../loop/run-command.js';
|
|
14
|
+
import { readRunState } from '../loop/run-state.js';
|
|
14
15
|
import { isPidAlive, readLock } from '../loop/lock.js';
|
|
15
16
|
import { appendEvent } from '../observability/events.js';
|
|
16
17
|
import { queueChange } from '../change/inbox.js';
|
|
@@ -331,9 +332,11 @@ export async function startDashboard(options = {}) {
|
|
|
331
332
|
send(409, { error: project.error });
|
|
332
333
|
return;
|
|
333
334
|
}
|
|
334
|
-
|
|
335
|
+
const saved = readRunState(project.root);
|
|
336
|
+
if (current.goal && (!saved || saved.mode === 'goal'))
|
|
335
337
|
pauseProjectGoal(project.root);
|
|
336
|
-
|
|
338
|
+
if (!saved || saved.mode !== 'goal')
|
|
339
|
+
writePauseSignal(project.root);
|
|
337
340
|
const response = { status: 'pause-requested' };
|
|
338
341
|
send(200, response);
|
|
339
342
|
return;
|
|
@@ -355,13 +358,30 @@ export async function startDashboard(options = {}) {
|
|
|
355
358
|
send(409, { status: 'already-running', error: 'Project is already running' });
|
|
356
359
|
return;
|
|
357
360
|
}
|
|
358
|
-
|
|
361
|
+
let saved;
|
|
362
|
+
try {
|
|
363
|
+
saved = readRunState(project.root);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
send(409, { error: 'Saved execution state is invalid; start a fresh run from the CLI' });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const goal = saved ? saved.mode === 'goal' : current.goal !== null && current.goal.status !== 'complete';
|
|
370
|
+
if (saved?.mode === 'goal') {
|
|
371
|
+
const rawGoal = safeFile(project.root, '.yoke/goal.json');
|
|
372
|
+
if (!rawGoal || JSON.parse(rawGoal).id !== saved.goalId) {
|
|
373
|
+
send(409, { error: 'Saved goal identity no longer matches this project' });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
359
377
|
activeResumes.add(project.root);
|
|
360
378
|
void Promise.resolve().then(async () => {
|
|
361
379
|
if (goal)
|
|
362
|
-
await runProjectGoal(project.root);
|
|
380
|
+
await runProjectGoal(project.root, saved?.mode === 'goal' ? { ...saved.options, provider: saved.options.agent, expectedGoalId: saved.goalId } : {});
|
|
381
|
+
else if (saved)
|
|
382
|
+
await resumeLoopCommand(project.root);
|
|
363
383
|
else
|
|
364
|
-
await runLoopCommand(project.root, {});
|
|
384
|
+
await runLoopCommand(project.root, { permissions: 'safe' });
|
|
365
385
|
}).catch(() => undefined).finally(() => { activeResumes.delete(project.root); });
|
|
366
386
|
const response = { status: 'resume-requested' };
|
|
367
387
|
send(202, response);
|