@hecer/yoke 1.9.0 → 1.10.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -1
- package/README.md +8 -6
- package/canon/loop/prd.schema.md +6 -0
- package/dist/agents/process-incarnation.js +1 -1
- package/dist/agents/process.js +74 -6
- package/dist/agents/supervision.js +153 -0
- package/dist/agents/windows-launch.js +80 -0
- package/dist/change/inbox.js +21 -5
- package/dist/cli.js +11 -2
- package/dist/dashboard/page.js +102 -8
- package/dist/dashboard/panels.js +84 -8
- package/dist/goals/command.js +2 -0
- package/dist/loop/git.js +12 -4
- package/dist/loop/loop.js +8 -4
- package/dist/loop/parallel-adapters.js +2 -3
- package/dist/loop/parallel-command.js +5 -0
- package/dist/loop/prd.js +2 -0
- package/dist/loop/reporter.js +4 -1
- package/dist/loop/run-command.js +11 -2
- package/dist/loop/runner.js +22 -26
- package/dist/loop/watchdog.js +87 -11
- package/dist/loop/worker.js +5 -3
- package/dist/prd/assess.js +145 -0
- package/dist/prd/command.js +59 -21
- package/dist/retrofit/config.js +11 -0
- package/dist/routing/assessment.js +1 -1
- package/dist/routing/capability.js +25 -13
- package/dist/routing/contracts.js +60 -0
- package/dist/routing/planning.js +12 -0
- package/dist/routing/router.js +50 -15
- package/dist/setup/command.js +2 -0
- package/docs/BATCH-PLANNING-VALIDATION.md +67 -0
- package/docs/CAPABILITY-ROUTING.md +31 -3
- package/docs/DASHBOARD-EVOLUTION.md +33 -0
- package/docs/PRODUCT-DIRECTION-2026-09-05.md +18 -0
- package/docs/WINDOWS-RUNNER-VALIDATION.md +104 -0
- package/docs/assets/yoke-logo.png +0 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "yoke",
|
|
4
4
|
"displayName": "Yoke",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.10.0",
|
|
6
6
|
"description": "Cross-agent coding harness: one curated skill canon (TDD, brainstorming, plans, reviews, shipping, design verification) plus mechanical safety gates and an autonomous loop via the yoke CLI.",
|
|
7
7
|
"author": { "name": "HECer", "url": "https://github.com/HECer" },
|
|
8
8
|
"homepage": "https://github.com/HECer/yoke#readme",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.10.0 — 2026-09-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Adopt the Yoke wordmark in the GitHub and npm README.
|
|
7
|
+
- Prepare task assessments in one bounded planning call with `yoke prd assess`; draft and inbox planning bind assessments to requirements, upstream dependencies and the approved brief.
|
|
8
|
+
- Separate `planning.agent`/`model`/`reasoningEffort` from execution settings. New setups require prepared assessments and block missing worker profiles; existing configurations retain on-demand planning and parent fallback.
|
|
9
|
+
- Add selective reassessment, planning input limits and `routing.maxTier` to bound automatic model selection and escalation.
|
|
10
|
+
- Add attention-first project search and status filters, with separate goal/loop states and explicit stale activity warnings.
|
|
11
|
+
- Restore dashboard views and UTC period controls through URL links, browser history and refresh; cancel obsolete requests and bound project comparison concurrency.
|
|
12
|
+
- Compare recorded tokens, accepted tasks and reported costs against the preceding equal-duration period without inventing missing measurements or percentages from zero baselines.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Fix Windows safe-mode Codex execution by screening Store PowerShell/aliases and probing a native shell under the same sandbox before model work; keep permissions intact.
|
|
16
|
+
- Launch Windows provider executables/npm entry points with literal argv; bound preflight and provider lifetime, recognize streamed infrastructure failures, retain failed-worktree/process evidence, and guard against unconfirmed termination before another worker starts.
|
|
17
|
+
- Separate provider liveness, supervisor heartbeat, output and successful-tool progress in status/dashboard; label backlog percentages and preserve explicit bare startup through capability routing. See [Windows runner validation](docs/WINDOWS-RUNNER-VALIDATION.md).
|
|
18
|
+
- Keep local loop locks and dashboard status out of implementation commits and clean-worktree checks.
|
|
19
|
+
- Stage implementation files safely when runtime history directories are already ignored, including literal filenames and tracked deletions, for serial commits and parallel candidate snapshots.
|
|
20
|
+
|
|
21
|
+
### Migration and validation limits
|
|
22
|
+
- Existing routing configurations retain on-demand assessment and parent fallback. Use `yoke prd assess` to prepare task packages; configure `planning.agent`, `planning.model`, `planning.reasoningEffort` and `routing.maxTier` to separate planning from bounded execution. New setups require prepared assessments and block missing profiles.
|
|
23
|
+
- Update installed packages and restart the dashboard/runner to use the new code. Existing running processes are not upgraded or instrumented retroactively. Windows preflight preserves sandbox permissions and changes only the provider environment.
|
|
24
|
+
- Dashboard comparisons report recorded measurements, not reconstructed history or calibrated savings. Tokens per minute are consumption rates, not generation speed. Live routed Windows validation covered Codex on the documented machine; cross-provider performance and universal Windows compatibility are not established. See the linked dashboard, batch-planning and runner validation records.
|
|
4
25
|
|
|
5
26
|
## 1.9.0 — 2026-09-06
|
|
6
27
|
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1><img src="https://raw.githubusercontent.com/HECer/yoke/v1.10.0/docs/assets/yoke-logo.png" alt="Yoke" width="100" height="63"></h1>
|
|
4
4
|
|
|
5
|
-
<!-- yoke:version:start -->1.
|
|
6
|
-
<!-- yoke:tests:start -->
|
|
5
|
+
<!-- yoke:version:start -->1.10.0<!-- yoke:version:end -->
|
|
6
|
+
<!-- yoke:tests:start -->1173<!-- yoke:tests:end -->
|
|
7
7
|
<!-- yoke:skills:start -->34<!-- yoke:skills:end -->
|
|
8
8
|
<!-- yoke:agents:start -->Claude | Codex | Gemini<!-- yoke:agents:end -->
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
[](#-license)
|
|
18
18
|

|
|
19
19
|

|
|
20
|
-

|
|
21
21
|

|
|
22
22
|

|
|
23
23
|
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
> **TL;DR** — `yoke setup .` asks six questions and installs the native harness for your agent. `yoke new my-app --idea="..."` bootstraps a project and drafts its story backlog. `yoke loop run my-app --isolate --review` then implements it behind hard gates: **clean tree → acceptance criteria → your real tests green → an independent model approves → commit**. Add `--parallel=N` for dependency-aware workers, or declare a reference and add `--quality` for a bounded critic/repair gauntlet. If any blocking gate is red, nothing is committed. Proof lives in `.yoke/proof/<story>/`.
|
|
29
29
|
|
|
30
|
-
**New in 1.
|
|
30
|
+
**New in 1.10.0:** [dashboard search, filters and period comparisons](docs/DASHBOARD-EVOLUTION.md), [batch task assessments with separate planning models](docs/CAPABILITY-ROUTING.md), and [Windows sandbox preflight and process supervision](docs/WINDOWS-RUNNER-VALIDATION.md). Existing routing settings remain authoritative. See the [changelog](CHANGELOG.md) for migration and validation limits.
|
|
31
31
|
|
|
32
32
|
### One dashboard, multiple projects
|
|
33
33
|
|
|
@@ -40,6 +40,8 @@ yoke dashboard --no-register
|
|
|
40
40
|
|
|
41
41
|
Open the printed `http://127.0.0.1:...` URL. Each registered project has its own goals, tasks and evidence. The dashboard shows available worker state, per-task duration estimates, planned start offsets, input/output tokens, costs and unknown measurements. You can request a goal pause at a safe boundary.
|
|
42
42
|
|
|
43
|
+
The dashboard includes attention-first project search and filters, restorable view links, and usage comparisons against the preceding period. See [dashboard behavior and measurement limits](docs/DASHBOARD-EVOLUTION.md).
|
|
44
|
+
|
|
43
45
|
Projects are registered explicitly; this version does not automatically discover every process or aggregate other computers. Start/resume and budget changes use the CLI. Missing history appears as unknown; time ranges are empirical estimates, not exact deadlines.
|
|
44
46
|
|
|
45
47
|
### Verified goals and efficient execution
|
|
@@ -895,7 +897,7 @@ release provenance.
|
|
|
895
897
|
## 🧪 Development
|
|
896
898
|
|
|
897
899
|
```bash
|
|
898
|
-
npm test # vitest (
|
|
900
|
+
npm test # vitest (1173 tests)
|
|
899
901
|
npm run build # tsc, no emit errors
|
|
900
902
|
npm run yoke -- validate canon
|
|
901
903
|
```
|
package/canon/loop/prd.schema.md
CHANGED
|
@@ -34,6 +34,12 @@ belongs in a trusted `completion.command` journey suite.
|
|
|
34
34
|
`sourceChange` is an optional Yoke-owned request ID. The change inbox uses it to append new
|
|
35
35
|
stories idempotently; authors normally omit it.
|
|
36
36
|
|
|
37
|
+
`assessment` holds the planner's task class, difficulty, uncertainty, risk, scope, testability,
|
|
38
|
+
reason and approach. `assessmentFor` is a Yoke-generated SHA-256 binding to the task requirements,
|
|
39
|
+
upstream contracts and approved brief. Do not copy a binding onto changed requirements.
|
|
40
|
+
With `routing.assessmentPolicy: prepared`, use `yoke prd assess` after editing contracts;
|
|
41
|
+
dispatch blocks missing or stale assessments. See [capability routing](../../docs/CAPABILITY-ROUTING.md).
|
|
42
|
+
|
|
37
43
|
Stories without `needs`, `area`, or `agent` retain serial behavior. A story is ready only when
|
|
38
44
|
every ID in `needs` passes. The scheduler orders ready work by priority, avoids simultaneously
|
|
39
45
|
active areas, and uses `agent` as an affinity hint.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
const queryProcessIdentity = (command, args, options) => execFileSync(command, args, { stdio: 'pipe', ...options }).toString();
|
|
2
|
+
const queryProcessIdentity = (command, args, options) => execFileSync(command, args, { stdio: 'pipe', timeout: 5000, windowsHide: true, ...options }).toString();
|
|
3
3
|
export function processIncarnation(pid, platform = process.platform, query = queryProcessIdentity) {
|
|
4
4
|
try {
|
|
5
5
|
if (platform === 'win32') {
|
package/dist/agents/process.js
CHANGED
|
@@ -4,35 +4,64 @@ import { killProcessTreeForCleanup } from '../loop/watchdog.js';
|
|
|
4
4
|
import { createProviderProcessRecord, filesystemProviderProcessRecordAdapter, } from './process-record.js';
|
|
5
5
|
import { createBoundedOutput, createTelemetryAccumulator } from './process-streams.js';
|
|
6
6
|
import { processIncarnation } from './process-incarnation.js';
|
|
7
|
+
import { prepareWindowsInvocation, resolveWindowsCommand } from './windows-launch.js';
|
|
8
|
+
import { createSupervision, supervisionLimits, assertPreviousProvidersStopped } from './supervision.js';
|
|
7
9
|
function cancellationReason(signal) {
|
|
8
10
|
return typeof signal.reason === 'string' && signal.reason.length > 0
|
|
9
11
|
? signal.reason
|
|
10
12
|
: 'provider process cancellation requested';
|
|
11
13
|
}
|
|
12
14
|
export function providerSpawnOptions(invocation, platform = process.platform) {
|
|
13
|
-
const
|
|
15
|
+
const resolved = platform === 'win32' ? resolveWindowsCommand(invocation.command, invocation.args) : invocation;
|
|
14
16
|
return {
|
|
15
|
-
command:
|
|
16
|
-
args:
|
|
17
|
+
command: resolved.command,
|
|
18
|
+
args: resolved.args,
|
|
17
19
|
cwd: invocation.cwd,
|
|
18
|
-
shell:
|
|
20
|
+
shell: false,
|
|
19
21
|
detached: platform !== 'win32',
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
export function startProviderProcess(agent, invocation, options = {}) {
|
|
23
|
-
|
|
25
|
+
let failure = () => { };
|
|
26
|
+
let progress = () => { };
|
|
27
|
+
const limits = supervisionLimits(invocation.cwd);
|
|
28
|
+
const supervision = createSupervision(invocation.cwd, reason => failure(reason), () => progress(), options.attempt);
|
|
29
|
+
let prepared;
|
|
30
|
+
try {
|
|
31
|
+
assertPreviousProvidersStopped(invocation.cwd);
|
|
32
|
+
prepared = process.platform === 'win32' ? prepareWindowsInvocation(invocation) : { command: invocation.command, args: invocation.args, env: process.env };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error.message;
|
|
36
|
+
supervision.stop(message);
|
|
37
|
+
return { pid: undefined, invocation, recordPath: '', cancel: () => false, completion: Promise.resolve({ kind: 'spawn-failed', error: message, invocation, pid: undefined, stdout: '', stderr: '', stdoutTruncated: false, stderrTruncated: false, telemetry: { usageAvailable: false } }) };
|
|
38
|
+
}
|
|
39
|
+
const spawnOptions = { ...prepared, cwd: invocation.cwd, shell: false, detached: process.platform !== 'win32' };
|
|
24
40
|
const child = spawn(spawnOptions.command, [...spawnOptions.args], {
|
|
25
41
|
cwd: spawnOptions.cwd,
|
|
26
42
|
shell: spawnOptions.shell,
|
|
27
43
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
44
|
detached: spawnOptions.detached,
|
|
45
|
+
env: prepared.env,
|
|
46
|
+
windowsHide: true,
|
|
29
47
|
});
|
|
30
48
|
const targetDir = resolve(invocation.cwd);
|
|
31
49
|
const pid = child.pid;
|
|
32
50
|
const startedAt = pid === undefined ? `unverified:${new Date().toISOString()}` : processIncarnation(pid) ?? `unverified:${new Date().toISOString()}`;
|
|
51
|
+
supervision.start(pid, 'shell' in prepared ? prepared.shell : undefined, startedAt.startsWith('unverified:') ? undefined : startedAt);
|
|
33
52
|
const record = createProviderProcessRecord(targetDir, pid ?? 0, options.workerId, startedAt);
|
|
34
53
|
const recordAdapter = options.recordAdapter ?? filesystemProviderProcessRecordAdapter;
|
|
35
|
-
const terminateProcessTree = options.terminateProcessTree ?? ((processPid) =>
|
|
54
|
+
const terminateProcessTree = options.terminateProcessTree ?? ((processPid) => {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(processPid, 0);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (startedAt.startsWith('unverified:') || processIncarnation(processPid) !== startedAt)
|
|
62
|
+
return false;
|
|
63
|
+
return killProcessTreeForCleanup(processPid);
|
|
64
|
+
});
|
|
36
65
|
let recordPublished = false;
|
|
37
66
|
const stdout = createBoundedOutput(options.outputLimitBytes ?? 1_048_576);
|
|
38
67
|
const stderr = createBoundedOutput(options.outputLimitBytes ?? 1_048_576);
|
|
@@ -42,6 +71,9 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
42
71
|
let termination;
|
|
43
72
|
let idleTimer;
|
|
44
73
|
let forceTimer;
|
|
74
|
+
let totalTimer;
|
|
75
|
+
let progressTimer;
|
|
76
|
+
let completionTimer;
|
|
45
77
|
let recordFailure;
|
|
46
78
|
let terminationConfirmed = false;
|
|
47
79
|
let settled = false;
|
|
@@ -58,6 +90,12 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
58
90
|
clearTimeout(idleTimer);
|
|
59
91
|
if (forceTimer)
|
|
60
92
|
clearTimeout(forceTimer);
|
|
93
|
+
if (totalTimer)
|
|
94
|
+
clearTimeout(totalTimer);
|
|
95
|
+
if (progressTimer)
|
|
96
|
+
clearTimeout(progressTimer);
|
|
97
|
+
if (completionTimer)
|
|
98
|
+
clearTimeout(completionTimer);
|
|
61
99
|
idleTimer = undefined;
|
|
62
100
|
forceTimer = undefined;
|
|
63
101
|
};
|
|
@@ -66,6 +104,7 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
66
104
|
return;
|
|
67
105
|
settled = true;
|
|
68
106
|
clearTimers();
|
|
107
|
+
supervision.stop(termination?.reason ?? (result.kind === 'succeeded' ? 'provider-exited' : 'provider-failed'), !termination || terminationConfirmed);
|
|
69
108
|
options.signal?.removeEventListener('abort', onAbort);
|
|
70
109
|
if (!termination || terminationConfirmed)
|
|
71
110
|
removeRecord();
|
|
@@ -81,6 +120,9 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
81
120
|
telemetry: telemetry.finish(),
|
|
82
121
|
});
|
|
83
122
|
const finalize = (exitCode) => {
|
|
123
|
+
if (settled)
|
|
124
|
+
return;
|
|
125
|
+
supervision.flush();
|
|
84
126
|
// Windows can emit close before taskkill's process-tree state is observable.
|
|
85
127
|
// Reconfirm here so successful termination does not leave a stale ownership record.
|
|
86
128
|
if (termination && pid !== undefined && !terminationConfirmed) {
|
|
@@ -114,6 +156,21 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
114
156
|
forceTimer = setTimeout(() => {
|
|
115
157
|
if (pid !== undefined && !settled)
|
|
116
158
|
terminationConfirmed = terminateProcessTree(pid, true);
|
|
159
|
+
// Allow close/pipe draining to confirm termination before the bounded fallback.
|
|
160
|
+
if (!settled)
|
|
161
|
+
completionTimer = setTimeout(() => {
|
|
162
|
+
if (settled)
|
|
163
|
+
return;
|
|
164
|
+
// A killer's return value is not an observed process exit.
|
|
165
|
+
if (pid !== undefined) {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
terminationConfirmed = false;
|
|
169
|
+
}
|
|
170
|
+
catch { /* exited */ }
|
|
171
|
+
}
|
|
172
|
+
finalize(null);
|
|
173
|
+
}, 5000);
|
|
117
174
|
}, terminationGraceMs);
|
|
118
175
|
return true;
|
|
119
176
|
};
|
|
@@ -140,8 +197,16 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
140
197
|
stderr.append(text);
|
|
141
198
|
}
|
|
142
199
|
options.onOutput?.({ stream, text });
|
|
200
|
+
supervision.output(stream, text);
|
|
143
201
|
armIdleTimer();
|
|
144
202
|
};
|
|
203
|
+
failure = reason => { terminate({ kind: 'cancelled', reason }); };
|
|
204
|
+
progress = () => {
|
|
205
|
+
if (progressTimer)
|
|
206
|
+
clearTimeout(progressTimer);
|
|
207
|
+
if ((options.progressTimeoutMs ?? limits.progressMs) > 0)
|
|
208
|
+
progressTimer = setTimeout(() => terminate({ kind: 'timed-out', reason: 'provider-progress-timeout' }), options.progressTimeoutMs ?? limits.progressMs);
|
|
209
|
+
};
|
|
145
210
|
child.stdout?.on('data', chunk => { onOutput('stdout', chunk); });
|
|
146
211
|
child.stderr?.on('data', chunk => { onOutput('stderr', chunk); });
|
|
147
212
|
child.stdin?.on('error', () => { });
|
|
@@ -178,5 +243,8 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
178
243
|
else
|
|
179
244
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
180
245
|
armIdleTimer();
|
|
246
|
+
if ((options.totalTimeoutMs ?? limits.totalMs) > 0)
|
|
247
|
+
totalTimer = setTimeout(() => terminate({ kind: 'timed-out', reason: 'provider-total-timeout' }), options.totalTimeoutMs ?? limits.totalMs);
|
|
248
|
+
progress();
|
|
181
249
|
return handle;
|
|
182
250
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, writeFileSync, renameSync, readFileSync, readdirSync, lstatSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { processIncarnation } from './process-incarnation.js';
|
|
5
|
+
import { loadConfig } from '../retrofit/config.js';
|
|
6
|
+
export function supervisionLimits(cwd) {
|
|
7
|
+
const loop = loadConfig(cwd)?.loop;
|
|
8
|
+
return { totalMs: (loop?.maxCallMinutes ?? 30) * 60_000, progressMs: (loop?.progressTimeoutMinutes ?? 20) * 60_000 };
|
|
9
|
+
}
|
|
10
|
+
export function assertPreviousProvidersStopped(cwd) {
|
|
11
|
+
if (readSupervision(cwd, '').some(record => record.state === 'termination-unconfirmed' && record.liveness !== 'exited')) {
|
|
12
|
+
throw Error('A previous provider termination is unconfirmed; inspect owned process records before starting another worker');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Only provider error/tool-result envelopes are failure evidence; stderr MCP chatter is not. */
|
|
16
|
+
export function inspectProviderEvent(line) {
|
|
17
|
+
let event;
|
|
18
|
+
try {
|
|
19
|
+
event = JSON.parse(line);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
if (!event || typeof event !== 'object')
|
|
25
|
+
return {};
|
|
26
|
+
const item = event.item;
|
|
27
|
+
const failedCommand = event.type === 'item.completed' && item?.type === 'command_execution' && (item.status === 'failed' || (typeof item.exit_code === 'number' && item.exit_code !== 0));
|
|
28
|
+
const fatal = event.type === 'error' || event.type === 'turn.failed' || (event.type === 'result' && (event.is_error === true || event.status === 'error'));
|
|
29
|
+
if (failedCommand || fatal) {
|
|
30
|
+
const details = JSON.stringify(failedCommand ? item.aggregated_output : event.error ?? event.errors ?? event.result ?? event.message);
|
|
31
|
+
if (/CreateProcess(?:AsUserW|W)? failed|Failed to create unified exec process/iu.test(details ?? ''))
|
|
32
|
+
return { failure: 'worker-shell-spawn-failed' };
|
|
33
|
+
if (/AuthRequired|authentication failed|invalid api key|No access token|credentials (?:missing|not found)/iu.test(details ?? ''))
|
|
34
|
+
return { failure: 'provider-authentication-failed' };
|
|
35
|
+
if (fatal)
|
|
36
|
+
return { failure: 'provider-terminal-error' };
|
|
37
|
+
}
|
|
38
|
+
return { progress: (event.type === 'item.completed' && ((item?.type === 'command_execution' && item.exit_code === 0) || (item?.type === 'file_change' && item.status === 'completed')))
|
|
39
|
+
|| (event.type === 'tool_result' && event.status === 'success')
|
|
40
|
+
|| (event.type === 'user' && Array.isArray(event.message?.content) && event.message.content.some((part) => part.type === 'tool_result' && part.is_error === false)) };
|
|
41
|
+
}
|
|
42
|
+
export function inspectProviderDiagnostic(line) {
|
|
43
|
+
if (/\bERROR codex_core::tools::router: error=exec_command failed: CreateProcess\b/u.test(line) && /CreateProcess(?:AsUserW|W)? failed/u.test(line))
|
|
44
|
+
return 'worker-shell-spawn-failed';
|
|
45
|
+
// Unrelated rmcp/MCP authentication diagnostics are intentionally excluded.
|
|
46
|
+
if (/^(?:Error: |error: )(?:authentication failed|invalid api key|No access token)/iu.test(line))
|
|
47
|
+
return 'provider-authentication-failed';
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
export function createSupervision(cwd, onFailure, onProgress = () => { }, attempt = 1) {
|
|
51
|
+
const id = randomUUID(), dir = join(cwd, '.yoke', 'supervision'), path = join(dir, id + '.json');
|
|
52
|
+
const now = () => new Date().toISOString();
|
|
53
|
+
const state = { id, supervisorPid: process.pid, startedAt: now(), heartbeatAt: now(), state: 'preflight', retry: Math.max(0, attempt - 1) };
|
|
54
|
+
let pending = '', stderrPending = '', terminal = false, failure;
|
|
55
|
+
const save = () => {
|
|
56
|
+
state.heartbeatAt = now();
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
const temp = path + '.tmp';
|
|
59
|
+
writeFileSync(temp, JSON.stringify(state));
|
|
60
|
+
renameSync(temp, path);
|
|
61
|
+
};
|
|
62
|
+
save();
|
|
63
|
+
const timer = setInterval(() => { if (!terminal)
|
|
64
|
+
save(); }, 5000);
|
|
65
|
+
timer.unref();
|
|
66
|
+
return {
|
|
67
|
+
state,
|
|
68
|
+
flush() { if (pending)
|
|
69
|
+
this.output('stdout', '\n'); if (stderrPending)
|
|
70
|
+
this.output('stderr', '\n'); },
|
|
71
|
+
start(pid, shell, incarnation) { state.childPid = pid; state.childIncarnation = incarnation; state.shell = shell; state.state = 'running'; save(); },
|
|
72
|
+
output(stream, text) {
|
|
73
|
+
state.lastOutputAt = now();
|
|
74
|
+
const lines = ((stream === 'stdout' ? pending : stderrPending) + text).split(/\r?\n/u);
|
|
75
|
+
const tail = lines.pop() ?? '';
|
|
76
|
+
if (stream === 'stdout')
|
|
77
|
+
pending = tail.length > 262144 ? '' : tail;
|
|
78
|
+
else
|
|
79
|
+
stderrPending = tail.length > 262144 ? '' : tail;
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (line.length > 262144)
|
|
82
|
+
continue;
|
|
83
|
+
const result = stream === 'stdout' ? inspectProviderEvent(line) : { failure: inspectProviderDiagnostic(line) };
|
|
84
|
+
if (result.progress) {
|
|
85
|
+
state.lastProgressAt = now();
|
|
86
|
+
onProgress();
|
|
87
|
+
}
|
|
88
|
+
if (result.failure && !failure) {
|
|
89
|
+
failure = result.failure;
|
|
90
|
+
state.reason = failure;
|
|
91
|
+
state.state = 'stopping';
|
|
92
|
+
save();
|
|
93
|
+
onFailure(failure);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
stop(reason, confirmed = true) { terminal = true; clearInterval(timer); state.reason = failure ?? reason; state.state = confirmed ? 'stopped' : 'termination-unconfirmed'; save(); },
|
|
98
|
+
get failure() { return failure; },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function readSupervision(root, since) {
|
|
102
|
+
const roots = [root];
|
|
103
|
+
try {
|
|
104
|
+
for (const name of readdirSync(join(root, '.yoke', 'worktrees')).slice(0, 100)) {
|
|
105
|
+
const path = join(root, '.yoke', 'worktrees', name);
|
|
106
|
+
if (lstatSync(path).isDirectory() && !lstatSync(path).isSymbolicLink())
|
|
107
|
+
roots.push(path);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch { /* no isolated workers */ }
|
|
111
|
+
const records = [];
|
|
112
|
+
for (const cwd of roots) {
|
|
113
|
+
const dir = join(cwd, '.yoke', 'supervision');
|
|
114
|
+
try {
|
|
115
|
+
if (lstatSync(dir).isSymbolicLink())
|
|
116
|
+
continue;
|
|
117
|
+
for (const name of readdirSync(dir).filter(n => n.endsWith('.json')).slice(-200)) {
|
|
118
|
+
const path = join(dir, name), stat = lstatSync(path);
|
|
119
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8192)
|
|
120
|
+
continue;
|
|
121
|
+
try {
|
|
122
|
+
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
123
|
+
if (typeof record.startedAt === 'string' && record.startedAt >= since && typeof record.heartbeatAt === 'string')
|
|
124
|
+
records.push(record);
|
|
125
|
+
}
|
|
126
|
+
catch { /* incomplete/untrusted state */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { /* no process evidence */ }
|
|
130
|
+
}
|
|
131
|
+
return records.sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20).map(record => ({ ...record, liveness: processLiveness(record) }));
|
|
132
|
+
}
|
|
133
|
+
const identities = new Map();
|
|
134
|
+
function processLiveness(record) {
|
|
135
|
+
if (!Number.isInteger(record.childPid) || record.childPid <= 0)
|
|
136
|
+
return 'identity-unverified';
|
|
137
|
+
try {
|
|
138
|
+
process.kill(record.childPid, 0);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return error.code === 'ESRCH' ? 'exited' : 'identity-unverified';
|
|
142
|
+
}
|
|
143
|
+
if (!record.childIncarnation)
|
|
144
|
+
return 'identity-unverified';
|
|
145
|
+
let identity = identities.get(record.childPid);
|
|
146
|
+
if (!identity || Date.now() - identity.at > 5000) {
|
|
147
|
+
identity = { at: Date.now(), value: processIncarnation(record.childPid) };
|
|
148
|
+
identities.set(record.childPid, identity);
|
|
149
|
+
if (identities.size > 100)
|
|
150
|
+
identities.delete(identities.keys().next().value);
|
|
151
|
+
}
|
|
152
|
+
return identity.value === record.childIncarnation ? 'alive' : 'identity-unverified';
|
|
153
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { delimiter, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
const file = (path) => { try {
|
|
7
|
+
return statSync(path).isFile();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
} };
|
|
12
|
+
const pathValue = (env) => env[Object.keys(env).find(k => k.toLowerCase() === 'path') ?? 'PATH'] ?? '';
|
|
13
|
+
/** Resolve npm launchers without cmd.exe argument concatenation. */
|
|
14
|
+
export function resolveWindowsCommand(command, args, env = process.env) {
|
|
15
|
+
if (command === 'node' || command === process.execPath)
|
|
16
|
+
return { command: process.execPath, args: [...args] };
|
|
17
|
+
if (isAbsolute(command) && /\.(exe|com)$/iu.test(command))
|
|
18
|
+
return { command, args: [...args] };
|
|
19
|
+
const candidates = isAbsolute(command) || /[\\/]/u.test(command) ? [command]
|
|
20
|
+
: pathValue(env).split(';').filter(Boolean).flatMap(dir => extname(command) ? [join(dir, command)] : ['.exe', '.com', '.cmd', '.bat'].map(ext => join(dir, command + ext)));
|
|
21
|
+
const found = candidates.find(file);
|
|
22
|
+
if (!found)
|
|
23
|
+
throw Error(`Provider executable unavailable: ${command}`);
|
|
24
|
+
if (/\.(exe|com)$/iu.test(found))
|
|
25
|
+
return { command: found, args: [...args] };
|
|
26
|
+
const script = readFileSync(found, 'utf8');
|
|
27
|
+
const entry = script.match(/"%dp0%[\\/]([^"\r\n]+\.(?:m?js|cjs))"/iu)?.[1];
|
|
28
|
+
if (!entry)
|
|
29
|
+
throw Error('Unsupported Windows batch launcher; use a native executable or an npm-generated CLI launcher');
|
|
30
|
+
const target = resolve(dirname(found), ...entry.split(/[\\/]/u));
|
|
31
|
+
if (!file(target))
|
|
32
|
+
throw Error('Provider npm entry point is missing');
|
|
33
|
+
const localNode = join(dirname(found), 'node.exe');
|
|
34
|
+
return { command: file(localNode) ? localNode : process.execPath, args: [target, ...args] };
|
|
35
|
+
}
|
|
36
|
+
export function inaccessibleStoreShell(path) {
|
|
37
|
+
const bad = (p) => /[\\/]WindowsApps[\\/](?:Microsoft\.PowerShell[^\\/]*[\\/]|(?:pwsh|powershell)\.exe$)/iu.test(p);
|
|
38
|
+
if (bad(path))
|
|
39
|
+
return true;
|
|
40
|
+
try {
|
|
41
|
+
return bad(realpathSync(path));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function windowsShellEnvironment(env = process.env) {
|
|
48
|
+
// Change only this provider's PATH. Never change machine/user configuration or permissions.
|
|
49
|
+
const paths = pathValue(env).split(';').filter(Boolean).filter(dir => !['pwsh.exe', 'powershell.exe'].some(name => inaccessibleStoreShell(join(dir, name))));
|
|
50
|
+
const nativePwsh = join(env.ProgramFiles ?? 'C:\\Program Files', 'PowerShell', '7', 'pwsh.exe');
|
|
51
|
+
const systemShell = join(env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
52
|
+
const shell = [...paths.map(p => join(p, 'pwsh.exe')), nativePwsh, ...paths.map(p => join(p, 'powershell.exe')), systemShell].find(p => file(p) && !inaccessibleStoreShell(p));
|
|
53
|
+
if (!shell)
|
|
54
|
+
throw Error('No native PowerShell available for sandbox preflight');
|
|
55
|
+
const next = { ...env };
|
|
56
|
+
for (const key of Object.keys(next))
|
|
57
|
+
if (key.toLowerCase() === 'path')
|
|
58
|
+
delete next[key];
|
|
59
|
+
next.PATH = [dirname(shell), ...paths].join(delimiter);
|
|
60
|
+
return { env: next, shell };
|
|
61
|
+
}
|
|
62
|
+
/** Model-free probe under the same Codex permission profile and isolated cwd. */
|
|
63
|
+
export function prepareWindowsInvocation(inv, env = process.env) {
|
|
64
|
+
if (inv.command !== 'codex' || !inv.args.includes('exec') || inv.args.includes('--dangerously-bypass-approvals-and-sandbox'))
|
|
65
|
+
return { ...resolveWindowsCommand(inv.command, inv.args, env), env };
|
|
66
|
+
const prepared = windowsShellEnvironment(env);
|
|
67
|
+
const readOnly = inv.args.includes('read-only');
|
|
68
|
+
const config = ['--config', `shell_environment_policy.set.PATH=${JSON.stringify(prepared.env.PATH)}`];
|
|
69
|
+
const probe = resolveWindowsCommand('codex', ['sandbox', '-P', readOnly ? ':read-only' : ':workspace', '-C', inv.cwd, ...config, '--', prepared.shell, '-NoProfile', '-NonInteractive', '-Command', 'Write-Output YOKE_SHELL_OK'], prepared.env);
|
|
70
|
+
const compiled = fileURLToPath(new URL('../loop/watchdog.js', import.meta.url));
|
|
71
|
+
const watchdog = existsSync(compiled) ? [compiled] : ['--import', pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href, fileURLToPath(new URL('../loop/watchdog.ts', import.meta.url))];
|
|
72
|
+
// The inner watchdog owns the native process tree. Killing only an npm wrapper
|
|
73
|
+
// on spawnSync timeout could otherwise leave the sandbox helper behind.
|
|
74
|
+
const result = spawnSync(process.execPath, [...watchdog, '--idle-ms=30000', '--total-ms=30000', '--', probe.command, ...probe.args], { cwd: inv.cwd, env: prepared.env, shell: false, windowsHide: true, encoding: 'utf8', timeout: 90_000, maxBuffer: 64 * 1024 });
|
|
75
|
+
if (result.status !== 0 || !result.stdout?.includes('YOKE_SHELL_OK')) {
|
|
76
|
+
const signature = /CreateProcessAsUserW failed:\s*-?\d+/u.exec(result.stderr ?? '')?.[0];
|
|
77
|
+
throw Error(`Codex sandbox preflight failed (${signature ?? result.error?.code ?? result.status ?? 'no completion'}); no model started. Check native PowerShell and Codex sandbox setup.`);
|
|
78
|
+
}
|
|
79
|
+
return { ...resolveWindowsCommand(inv.command, [...inv.args, ...config], prepared.env), ...prepared };
|
|
80
|
+
}
|
package/dist/change/inbox.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { assessmentInstructions } from "../routing/assessment.js";
|
|
2
|
+
import { bindAssessments, preparedProblems } from '../prd/assess.js';
|
|
3
|
+
import { loadConfig } from '../retrofit/config.js';
|
|
4
|
+
import { resolvePlanner } from '../routing/planning.js';
|
|
5
|
+
import { readPlanningFile } from '../routing/contracts.js';
|
|
2
6
|
import { randomUUID } from 'node:crypto';
|
|
3
7
|
import { execFileSync } from 'node:child_process';
|
|
4
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -88,13 +92,14 @@ export function pendingChanges(targetDir) {
|
|
|
88
92
|
.map(name => ChangeRequestSchema.parse(JSON.parse(readFileSync(join(dir, name), 'utf8'))))
|
|
89
93
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
90
94
|
}
|
|
91
|
-
export function buildChangePrompt(request, proposalPath, stories) {
|
|
95
|
+
export function buildChangePrompt(request, proposalPath, stories, brief = '') {
|
|
92
96
|
return [
|
|
93
97
|
'You are planning one newly requested product change for the Yoke autonomous loop.',
|
|
94
98
|
`Change request ${request.id}: ${request.request}`,
|
|
95
99
|
'',
|
|
96
100
|
'Create an append-only proposal: add small new stories; never rewrite or delete existing stories.',
|
|
97
101
|
assessmentInstructions,
|
|
102
|
+
...(brief ? ['Approved planning brief:', brief] : []),
|
|
98
103
|
`Existing story IDs: ${stories.map(story => story.id).join(', ') || '(none)'}`,
|
|
99
104
|
'Every proposed story must have passes: false and 2-5 structured acceptance criteria.',
|
|
100
105
|
'Every criterion must have a stable id, behavioral text, and one or more executable verify commands.',
|
|
@@ -128,6 +133,9 @@ function archiveRequest(targetDir, request) {
|
|
|
128
133
|
rmSync(reviewFile(targetDir, request.id), { force: true });
|
|
129
134
|
}
|
|
130
135
|
export function runChangeApply(targetDir, opts) {
|
|
136
|
+
const config = loadConfig(targetDir);
|
|
137
|
+
const planner = resolvePlanner(config, opts.runner, opts.selection);
|
|
138
|
+
opts = { ...opts, runner: planner.agent, selection: planner.selection };
|
|
131
139
|
let requests;
|
|
132
140
|
try {
|
|
133
141
|
requests = pendingChanges(targetDir);
|
|
@@ -170,15 +178,16 @@ export function runChangeApply(targetDir, opts) {
|
|
|
170
178
|
if (!available(reviewer))
|
|
171
179
|
return { ok: false, added: 0, summary: `reviewer CLI "${reviewer}" is unavailable`, changeId: request.id };
|
|
172
180
|
const proposal = proposalFile(targetDir, request.id);
|
|
181
|
+
const brief = readPlanningFile(targetDir, '.yoke/plan.md', 80_000) ?? '';
|
|
173
182
|
const plannerDir = changesDir(targetDir);
|
|
174
183
|
mkdirSync(plannerDir, { recursive: true });
|
|
175
184
|
rmSync(proposal, { force: true });
|
|
176
|
-
const base = agentInvocation(opts.runner, buildChangePrompt(request, proposal, existing), plannerDir, opts.permissions ?? 'safe', opts.selection);
|
|
185
|
+
const base = agentInvocation(opts.runner, buildChangePrompt(request, proposal, existing, brief), plannerDir, opts.permissions ?? 'safe', opts.selection);
|
|
177
186
|
const invocation = buildWatchdogInvocation(base, opts.timeoutMs ?? 0);
|
|
178
187
|
const result = (opts.run ?? runAgent)(invocation);
|
|
179
188
|
if (!result.success)
|
|
180
189
|
return { ok: false, added: 0, summary: `planner failed: ${result.summary}`, changeId: request.id };
|
|
181
|
-
if (readFileSync(prdPath, 'utf8') !== existingText) {
|
|
190
|
+
if (readFileSync(prdPath, 'utf8') !== existingText || (readPlanningFile(targetDir, '.yoke/plan.md', 80_000) ?? '') !== brief) {
|
|
182
191
|
return {
|
|
183
192
|
ok: false,
|
|
184
193
|
added: 0,
|
|
@@ -221,12 +230,19 @@ export function runChangeApply(targetDir, opts) {
|
|
|
221
230
|
}
|
|
222
231
|
proposedIds.add(story.id);
|
|
223
232
|
}
|
|
224
|
-
|
|
233
|
+
let appended = proposed.map(story => ({ ...story, sourceChange: request.id }));
|
|
225
234
|
const dependencyIssues = validateDependencies([...existing, ...appended]);
|
|
226
235
|
if (dependencyIssues.length > 0) {
|
|
227
236
|
return { ok: false, added: 0, summary: `invalid combined dependency graph: ${dependencyIssues.join('; ')}`, changeId: request.id };
|
|
228
237
|
}
|
|
229
238
|
const reviewPath = reviewFile(targetDir, request.id);
|
|
239
|
+
const bound = bindAssessments([...existing, ...appended], brief);
|
|
240
|
+
appended = bound.slice(existing.length).map(s => ({ ...s, sourceChange: request.id }));
|
|
241
|
+
if (config?.routing?.assessmentPolicy === 'prepared') {
|
|
242
|
+
const issues = preparedProblems(bound, brief).filter(issue => appended.some(s => issue.startsWith(s.id + ':')));
|
|
243
|
+
if (issues.length)
|
|
244
|
+
return { ok: false, added: 0, summary: issues.join('; '), changeId: request.id };
|
|
245
|
+
}
|
|
230
246
|
rmSync(reviewPath, { force: true });
|
|
231
247
|
const reviewBase = agentInvocation(reviewer, buildChangeReviewPrompt(request, reviewPath, appended), plannerDir, opts.permissions ?? 'safe', reviewer === opts.runner ? opts.selection : undefined);
|
|
232
248
|
const reviewInvocation = buildWatchdogInvocation(reviewBase, opts.timeoutMs ?? 0);
|
|
@@ -234,7 +250,7 @@ export function runChangeApply(targetDir, opts) {
|
|
|
234
250
|
if (!reviewResult.success) {
|
|
235
251
|
return { ok: false, added: 0, summary: `coverage review failed: ${reviewResult.summary}`, changeId: request.id };
|
|
236
252
|
}
|
|
237
|
-
if (readFileSync(prdPath, 'utf8') !== existingText) {
|
|
253
|
+
if (readFileSync(prdPath, 'utf8') !== existingText || (readPlanningFile(targetDir, '.yoke/plan.md', 80_000) ?? '') !== brief) {
|
|
238
254
|
return {
|
|
239
255
|
ok: false, added: 0,
|
|
240
256
|
summary: 'PRD changed while the coverage reviewer was running; refusing to overwrite concurrent or out-of-contract edits',
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runReview } from './review/command.js';
|
|
|
13
13
|
import { scanDir } from './scan/design.js';
|
|
14
14
|
import { runNew } from './new/command.js';
|
|
15
15
|
import { runPrdDraft, runPrdCheck } from './prd/command.js';
|
|
16
|
+
import { runPrdAssess } from './prd/assess.js';
|
|
16
17
|
import { runLoopCleanup } from './loop/cleanup.js';
|
|
17
18
|
import { runFlowSmoke } from './smoke/command.js';
|
|
18
19
|
import { maybeNotifyUpdate, currentYokeVersion } from './update/check.js';
|
|
@@ -550,6 +551,14 @@ export function main(argv) {
|
|
|
550
551
|
case 'prd': {
|
|
551
552
|
const sub = rest[0];
|
|
552
553
|
const targetDir = rest.slice(1).find(a => !a.startsWith('-')) ?? '.';
|
|
554
|
+
if (sub === 'assess') {
|
|
555
|
+
const runner = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
|
|
556
|
+
if (runner && !['codex', 'claude', 'gemini'].includes(runner)) {
|
|
557
|
+
console.error('Invalid planning runner');
|
|
558
|
+
return 1;
|
|
559
|
+
}
|
|
560
|
+
return runPrdAssess(targetDir, { runner: runner, story: rest.find(a => a.startsWith('--story='))?.slice('--story='.length), reassess: rest.includes('--reassess') });
|
|
561
|
+
}
|
|
553
562
|
if (sub === 'draft') {
|
|
554
563
|
const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
|
|
555
564
|
if (!idea) {
|
|
@@ -577,7 +586,7 @@ export function main(argv) {
|
|
|
577
586
|
}
|
|
578
587
|
if (sub === 'check')
|
|
579
588
|
return runPrdCheck(targetDir);
|
|
580
|
-
console.log('usage: yoke prd <draft|check> [dir] [--idea="..."] [--runner=<claude|codex|gemini>] [--force] [--timeout=<minutes>]');
|
|
589
|
+
console.log('usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<claude|codex|gemini>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]');
|
|
581
590
|
return 1;
|
|
582
591
|
}
|
|
583
592
|
case 'context': {
|
|
@@ -642,7 +651,7 @@ export function main(argv) {
|
|
|
642
651
|
return runUpgrade();
|
|
643
652
|
default:
|
|
644
653
|
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]');
|
|
645
|
-
console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
|
|
654
|
+
console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
|
|
646
655
|
return cmd ? 1 : 0;
|
|
647
656
|
}
|
|
648
657
|
}
|