@hecer/yoke 1.21.1 → 1.23.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 +48 -0
- package/README.md +8 -1
- package/TODOS.md +6 -0
- package/bench/analyze-codex-comparison.mjs +90 -17
- package/bench/compare-codex.mjs +159 -36
- package/bench/result-schema.mjs +132 -0
- package/canon/manifest.yaml +1 -1
- package/canon/skills/visual-verification/SKILL.md +25 -2
- package/canon/tools/codex-rtk-hook.mjs +6 -16
- package/dist/agents/pi-telemetry.js +2 -1
- package/dist/agents/process-streams.js +12 -64
- package/dist/agents/provider-selection.js +12 -0
- package/dist/agents/telemetry.js +52 -52
- package/dist/change/inbox.js +8 -3
- package/dist/check/command.js +69 -17
- package/dist/check/delivery.js +121 -0
- package/dist/cli.js +91 -3
- package/dist/code-intelligence/adapters/mcp.js +1 -0
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +159 -85
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/index.js +1 -0
- package/dist/code-intelligence/mcp-client.js +25 -6
- package/dist/code-intelligence/mcp-server.js +14 -11
- package/dist/code-intelligence/preflight.js +71 -0
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/cache-isolation.js +36 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/dispatcher.js +89 -26
- package/dist/loop/failure.js +104 -0
- package/dist/loop/gate-snapshot.js +19 -0
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +124 -70
- package/dist/loop/parallel-adapters.js +57 -6
- package/dist/loop/parallel-command.js +49 -7
- package/dist/loop/proof-retention.js +70 -0
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +22 -5
- package/dist/loop/run-command.js +101 -47
- package/dist/loop/runner.js +6 -5
- package/dist/loop/worker.js +152 -91
- package/dist/observability/history.js +2 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/local-report.js +120 -0
- package/dist/observability/usage.js +19 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/config.js +29 -2
- package/dist/retrofit/gitignore.js +12 -0
- package/dist/retrofit/planners/codex.js +20 -20
- package/dist/routing/attempts.js +241 -0
- package/dist/routing/capability.js +13 -9
- package/dist/routing/optimization.js +73 -0
- package/dist/routing/registry.js +7 -1
- package/dist/routing/router.js +282 -127
- package/dist/setup/command.js +8 -2
- package/dist/smoke/command.js +387 -85
- package/dist/update/check.js +1 -1
- package/docs/BENCHMARK-MANIFEST.md +131 -0
- package/docs/CODE-INTELLIGENCE.md +43 -1
- package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
- package/docs/DELIVERY-JOURNEYS.md +206 -0
- package/docs/ECONOMIC-ROUTING.md +180 -0
- package/docs/GOALS.md +61 -4
- package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
- package/docs/RELEASE-VALIDATION-1.23.0.md +39 -0
- package/docs/benchmarks/2026-10-04-efficiency/ANALYSE.md +182 -0
- package/docs/benchmarks/2026-10-04-efficiency/compare-help.py +55 -0
- package/docs/benchmarks/2026-10-04-efficiency/manifest.json +125 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-analysis.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-design.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-original-report.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/DEVELOPMENT_ANALYSIS.md +142 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/RESULT.md +21 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/commands.jsonl +26 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/environment.json +31 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/final-yoke-smoke.json +40 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/model-purpose-hints.csv +19 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/observations.jsonl +21 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/observer-command-phases.csv +12 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/roles.csv +5 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/shell-categories.csv +8 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/stories.csv +8 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/summary.json +469 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-history.jsonl +104 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-1.log +58 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-2.log +29 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-3.log +5 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-4.log +12 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-phases.csv +10 -0
- package/docs/benchmarks/2026-10-04-efficiency/regression-comparison.json +104 -0
- package/docs/parallel-execution.md +37 -9
- package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency-prd.json +11 -0
- package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency.md +83 -0
- package/docs/superpowers/specs/2026-10-04-yoke-1.23-efficiency-design.md +120 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/loop/loop.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { cacheIsolationProblem } from './cache-isolation.js';
|
|
2
|
+
import { retainRuntimeProof } from './proof-retention.js';
|
|
1
3
|
import { knownInfrastructureFailure } from "../routing/capability.js";
|
|
2
4
|
import { existsSync, unlinkSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
5
|
import { acceptanceProtectionProblem } from '../check/command.js';
|
|
4
6
|
import { join, relative } from 'node:path';
|
|
5
7
|
import { isAcceptanceCriterion, loadPrd, savePrd, selectNextStory, allPass, progress, storyPathSegment } from './prd.js';
|
|
6
8
|
import { stopTheLineGate, preDispatchGate } from './gates.js';
|
|
9
|
+
import { observeFailure, clearFailureProgress } from './failure.js';
|
|
10
|
+
import { gateIdentity, snapshotGates, reuseGates } from './gate-snapshot.js';
|
|
7
11
|
import { appendDecision, contextDir } from '../context/context.js';
|
|
8
12
|
import { noopReporter } from './reporter.js';
|
|
9
13
|
import { consumeDecisionRequest } from './decision.js';
|
|
@@ -68,7 +72,8 @@ function runQualityReview(opts, executionDir, story, reporter) {
|
|
|
68
72
|
}
|
|
69
73
|
return { kind: 'passed' };
|
|
70
74
|
};
|
|
71
|
-
|
|
75
|
+
let failure;
|
|
76
|
+
const outcome = runQualityRepairLoop({
|
|
72
77
|
quality: qualityAssessment
|
|
73
78
|
? round => {
|
|
74
79
|
reporter.phase('comparing');
|
|
@@ -85,7 +90,13 @@ function runQualityReview(opts, executionDir, story, reporter) {
|
|
|
85
90
|
reporter.phase('repairing');
|
|
86
91
|
if (!opts.repair)
|
|
87
92
|
return { kind: 'blocked', summary: 'repair callback is not configured' };
|
|
88
|
-
const
|
|
93
|
+
const observed = observeFailure({ root: opts.targetDir, directory: executionDir, story, stage: 'quality', summary: JSON.stringify(request.finding) });
|
|
94
|
+
if (observed.action === 'blocked') {
|
|
95
|
+
failure = observed.failure;
|
|
96
|
+
return { kind: 'blocked', summary: observed.feedback };
|
|
97
|
+
}
|
|
98
|
+
const targetedRequest = observed.action === 'diagnose' ? { ...request, finding: { ...request.finding, message: observed.feedback } } : request;
|
|
99
|
+
const result = opts.repair({ targetDir: executionDir, story }, targetedRequest);
|
|
89
100
|
return result.success ? { kind: 'repaired' } : { kind: 'blocked', summary: result.summary };
|
|
90
101
|
},
|
|
91
102
|
rerunGates,
|
|
@@ -96,6 +107,7 @@ function runQualityReview(opts, executionDir, story, reporter) {
|
|
|
96
107
|
reporter.quality({ ...status, ...qualityMetadata });
|
|
97
108
|
},
|
|
98
109
|
});
|
|
110
|
+
return { ...outcome, ...(failure ? { failure } : {}) };
|
|
99
111
|
}
|
|
100
112
|
// Control file a supervisor drops to pause the loop at the next story boundary.
|
|
101
113
|
// The loop consumes (deletes) it and stops with state 'paused' — never mid-story.
|
|
@@ -146,7 +158,7 @@ function runGate(gate, dir, storyId) {
|
|
|
146
158
|
process.env.YOKE_STORY = prev;
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
|
-
function consumeAmbiguity(dir) {
|
|
161
|
+
export function consumeAmbiguity(dir) {
|
|
150
162
|
const file = ambiguityFilePath(dir);
|
|
151
163
|
if (!existsSync(file))
|
|
152
164
|
return null;
|
|
@@ -167,14 +179,16 @@ function runCompletionGate(opts, stories) {
|
|
|
167
179
|
return null;
|
|
168
180
|
const previous = process.env.YOKE_PHASE;
|
|
169
181
|
process.env.YOKE_PHASE = 'completion';
|
|
170
|
-
let
|
|
182
|
+
let reason;
|
|
171
183
|
try {
|
|
172
|
-
verdict = opts.completion(opts.targetDir);
|
|
184
|
+
const verdict = opts.completion(opts.targetDir);
|
|
185
|
+
if (!verdict.passed)
|
|
186
|
+
reason = `integrated system did not verify: ${verdict.summary}`;
|
|
187
|
+
else if (!opts.git.isClean(opts.targetDir))
|
|
188
|
+
reason = 'completion command left source or final assets dirty; preserve changes and move rerun proofs to ignored runtime paths before resuming';
|
|
173
189
|
}
|
|
174
190
|
catch (error) {
|
|
175
|
-
|
|
176
|
-
(opts.reporter ?? noopReporter).blocked(reason);
|
|
177
|
-
return { status: 'blocked', iterations: 0, reason, finalProgress: progress(stories) };
|
|
191
|
+
reason = `integrated completion gate failed: ${error.message}`;
|
|
178
192
|
}
|
|
179
193
|
finally {
|
|
180
194
|
if (previous === undefined)
|
|
@@ -182,13 +196,20 @@ function runCompletionGate(opts, stories) {
|
|
|
182
196
|
else
|
|
183
197
|
process.env.YOKE_PHASE = previous;
|
|
184
198
|
}
|
|
185
|
-
if (
|
|
199
|
+
if (!reason) {
|
|
200
|
+
clearFailureProgress(opts.targetDir);
|
|
186
201
|
return null;
|
|
187
|
-
|
|
188
|
-
(opts.
|
|
189
|
-
|
|
202
|
+
}
|
|
203
|
+
const observed = observeFailure({ root: opts.targetDir, directory: opts.targetDir, stage: 'completion', summary: reason });
|
|
204
|
+
if (observed.action !== 'retry')
|
|
205
|
+
reason = observed.feedback;
|
|
206
|
+
(opts.reporter ?? noopReporter).blocked(reason, observed.failure);
|
|
207
|
+
return { status: 'blocked', iterations: 0, reason, failure: observed.failure, finalProgress: progress(stories) };
|
|
190
208
|
}
|
|
191
209
|
function runCriterionGates(opts, executionDir, story) {
|
|
210
|
+
const cacheProblem = opts.isolate && executionDir !== opts.targetDir ? cacheIsolationProblem(executionDir) : undefined;
|
|
211
|
+
if (cacheProblem)
|
|
212
|
+
return { passed: false, summary: cacheProblem };
|
|
192
213
|
const protectionProblem = acceptanceProtectionProblem(executionDir, opts.targetDir);
|
|
193
214
|
if (protectionProblem)
|
|
194
215
|
return { passed: false, summary: protectionProblem };
|
|
@@ -200,10 +221,10 @@ function runCriterionGates(opts, executionDir, story) {
|
|
|
200
221
|
}
|
|
201
222
|
if (!opts.verifyCriterion)
|
|
202
223
|
return { passed: false, summary: `story ${story.id} has criteria but no criterion verifier` };
|
|
203
|
-
const evidence = criteria.map(criterion =>
|
|
204
|
-
|
|
205
|
-
result: opts.verifyCriterion(executionDir, story, criterion)
|
|
206
|
-
})
|
|
224
|
+
const evidence = criteria.map(criterion => {
|
|
225
|
+
const problem = opts.isolate && executionDir !== opts.targetDir ? cacheIsolationProblem(executionDir) : undefined;
|
|
226
|
+
return { criterion, result: problem ? { passed: false, summary: problem } : opts.verifyCriterion(executionDir, story, criterion) };
|
|
227
|
+
});
|
|
207
228
|
try {
|
|
208
229
|
writeCriterionEvidence(opts.targetDir, story, evidence);
|
|
209
230
|
}
|
|
@@ -216,6 +237,15 @@ function runCriterionGates(opts, executionDir, story) {
|
|
|
216
237
|
: { passed: true, summary: `${evidence.length} acceptance criteria verified` };
|
|
217
238
|
}
|
|
218
239
|
export function runLoop(opts) {
|
|
240
|
+
if (opts.isolate)
|
|
241
|
+
for (const name of ['verify', 'design', 'perf', 'audit']) {
|
|
242
|
+
const gate = opts[name];
|
|
243
|
+
if (gate)
|
|
244
|
+
opts = { ...opts, [name]: (dir) => {
|
|
245
|
+
const problem = dir !== opts.targetDir ? cacheIsolationProblem(dir) : undefined;
|
|
246
|
+
return problem ? { passed: false, summary: problem } : gate(dir);
|
|
247
|
+
} };
|
|
248
|
+
}
|
|
219
249
|
let iterations = 0;
|
|
220
250
|
const reporter = opts.reporter ?? noopReporter;
|
|
221
251
|
for (;;) {
|
|
@@ -313,86 +343,88 @@ export function runLoop(opts) {
|
|
|
313
343
|
const wt = join(opts.targetDir, '.yoke', 'worktrees', storyPathSegment(story.id));
|
|
314
344
|
const wtPrd = join(wt, relative(opts.targetDir, opts.prdPath));
|
|
315
345
|
let landed = null;
|
|
346
|
+
let activeRouting;
|
|
316
347
|
try {
|
|
317
348
|
opts.git.addWorktree(opts.targetDir, wt);
|
|
318
|
-
const
|
|
349
|
+
const implementation = runImplementation(opts, wt, story, reporter);
|
|
350
|
+
const result = implementation.result;
|
|
351
|
+
activeRouting = result.routing;
|
|
319
352
|
iterations++;
|
|
320
353
|
if (result.tokens)
|
|
321
354
|
reporter.addTokens(result.tokens);
|
|
322
355
|
if (result.infrastructureFailure || result.routing?.blocked) {
|
|
323
356
|
if (result.infrastructureFailure)
|
|
324
357
|
result.routing?.recordOutcome(false, "infrastructure");
|
|
325
|
-
reporter.blocked(result.summary);
|
|
326
|
-
return { status: "blocked", iterations, reason: result.summary, finalProgress: progress(stories) };
|
|
358
|
+
reporter.blocked(result.summary, implementation.failure);
|
|
359
|
+
return { status: "blocked", iterations, reason: result.summary, ...(implementation.failure ? { failure: implementation.failure } : {}), finalProgress: progress(stories) };
|
|
327
360
|
}
|
|
328
361
|
let decision;
|
|
329
362
|
try {
|
|
330
363
|
decision = consumeDecisionRequest(wt, opts.targetDir, story.id);
|
|
331
364
|
}
|
|
332
365
|
catch (error) {
|
|
366
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
333
367
|
const reason = `invalid critical decision request for story ${story.id}: ${error.message}`;
|
|
334
368
|
reporter.blocked(reason);
|
|
335
369
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
336
370
|
}
|
|
337
371
|
if (decision) {
|
|
372
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
338
373
|
const reason = `critical decision required for story ${story.id}: ${decision.question}`;
|
|
339
374
|
reporter.blocked(reason);
|
|
340
375
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
341
376
|
}
|
|
342
377
|
const ambiguity = consumeAmbiguity(wt);
|
|
343
378
|
if (ambiguity) {
|
|
379
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
344
380
|
const reason = `story ${story.id} stopped: ambiguous acceptance criteria — ${ambiguity}`;
|
|
345
381
|
reporter.blocked(reason);
|
|
346
382
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
347
383
|
}
|
|
348
|
-
const
|
|
384
|
+
const verified = reuseGates(wt, story, implementation.gates);
|
|
385
|
+
const criteriaVerdict = verified?.criteria ?? runCriterionGates(opts, wt, story);
|
|
349
386
|
if (!criteriaVerdict.passed) {
|
|
350
387
|
result.routing?.recordOutcome(false);
|
|
351
388
|
const reason = blockReason(`story ${story.id} lacks acceptance evidence: ${criteriaVerdict.summary}`, opts.targetDir, opts.git);
|
|
352
|
-
|
|
353
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
389
|
+
return blockedMechanicalGate(opts, wt, story, 'criterion', criteriaVerdict.summary, reason, iterations, stories);
|
|
354
390
|
}
|
|
355
391
|
// Verify is the source of truth — NOT the runner's exit code. A spurious non-zero
|
|
356
392
|
// exit (e.g. a Windows .cmd wrapper ghost) must not block a story whose tests are green.
|
|
357
393
|
reporter.phase('verifying');
|
|
358
|
-
const verdict = runGate(opts.verify, wt, story.id);
|
|
394
|
+
const verdict = verified?.verify ?? runGate(opts.verify, wt, story.id);
|
|
359
395
|
if (!verdict.passed) {
|
|
360
396
|
result.routing?.recordOutcome(false);
|
|
361
397
|
const base = result.success
|
|
362
398
|
? `story ${story.id} did not verify: ${verdict.summary}`
|
|
363
399
|
: `story ${story.id} runner failed (${result.summary}) and verify is red: ${verdict.summary}`;
|
|
364
400
|
const reason = blockReason(base, opts.targetDir, opts.git);
|
|
365
|
-
|
|
366
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
401
|
+
return blockedMechanicalGate(opts, wt, story, 'verify', verdict.summary, reason, iterations, stories);
|
|
367
402
|
}
|
|
368
403
|
if (opts.design) {
|
|
369
404
|
reporter.phase('design');
|
|
370
|
-
const designVerdict = runGate(opts.design, wt, story.id);
|
|
405
|
+
const designVerdict = verified?.design ?? runGate(opts.design, wt, story.id);
|
|
371
406
|
if (!designVerdict.passed) {
|
|
372
407
|
result.routing?.recordOutcome(false);
|
|
373
408
|
const reason = blockReason(`story ${story.id} failed its design gate: ${designVerdict.summary}`, opts.targetDir, opts.git);
|
|
374
|
-
|
|
375
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
409
|
+
return blockedMechanicalGate(opts, wt, story, 'design', designVerdict.summary, reason, iterations, stories);
|
|
376
410
|
}
|
|
377
411
|
}
|
|
378
412
|
if (opts.perf) {
|
|
379
413
|
reporter.phase('perf');
|
|
380
|
-
const perfVerdict = runGate(opts.perf, wt, story.id);
|
|
414
|
+
const perfVerdict = verified?.perf ?? runGate(opts.perf, wt, story.id);
|
|
381
415
|
if (!perfVerdict.passed) {
|
|
382
416
|
result.routing?.recordOutcome(false);
|
|
383
417
|
const reason = blockReason(`story ${story.id} exceeded its performance budget: ${perfVerdict.summary}`, opts.targetDir, opts.git);
|
|
384
|
-
|
|
385
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
418
|
+
return blockedMechanicalGate(opts, wt, story, 'perf', perfVerdict.summary, reason, iterations, stories);
|
|
386
419
|
}
|
|
387
420
|
}
|
|
388
421
|
if (opts.audit) {
|
|
389
422
|
reporter.phase('audit');
|
|
390
|
-
const auditVerdict = runGate(opts.audit, wt, story.id);
|
|
423
|
+
const auditVerdict = verified?.audit ?? runGate(opts.audit, wt, story.id);
|
|
391
424
|
if (!auditVerdict.passed) {
|
|
392
425
|
result.routing?.recordOutcome(false);
|
|
393
426
|
const reason = blockReason(`story ${story.id} failed security audit: ${auditVerdict.summary}`, opts.targetDir, opts.git);
|
|
394
|
-
|
|
395
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
427
|
+
return blockedMechanicalGate(opts, wt, story, 'audit', auditVerdict.summary, reason, iterations, stories);
|
|
396
428
|
}
|
|
397
429
|
}
|
|
398
430
|
const summary = result.success
|
|
@@ -401,14 +433,15 @@ export function runLoop(opts) {
|
|
|
401
433
|
const repairOutcome = runQualityReview(opts, wt, story, reporter);
|
|
402
434
|
if (repairOutcome) {
|
|
403
435
|
if (repairOutcome.kind === 'paused') {
|
|
436
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
404
437
|
reporter.paused(progress(stories));
|
|
405
438
|
return { status: 'paused', iterations, finalProgress: progress(stories) };
|
|
406
439
|
}
|
|
407
440
|
const reason = repairBlockReason(repairOutcome, story, opts.targetDir, opts.git);
|
|
408
441
|
if (reason) {
|
|
409
442
|
result.routing?.recordOutcome(false);
|
|
410
|
-
reporter.blocked(reason);
|
|
411
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
443
|
+
reporter.blocked(reason, repairOutcome.failure);
|
|
444
|
+
return { status: 'blocked', iterations, reason, ...(repairOutcome.failure ? { failure: repairOutcome.failure } : {}), finalProgress: progress(stories) };
|
|
412
445
|
}
|
|
413
446
|
}
|
|
414
447
|
const protection = acceptanceProtectionProblem(wt, opts.targetDir);
|
|
@@ -428,12 +461,15 @@ export function runLoop(opts) {
|
|
|
428
461
|
});
|
|
429
462
|
const updated = stories.map(s => (s.id === story.id ? { ...s, passes: true } : s));
|
|
430
463
|
savePrd(wtPrd, updated);
|
|
464
|
+
retainRuntimeProof(wt, story.id, opts.targetDir);
|
|
431
465
|
opts.git.commitAll(wt, `yoke: complete ${story.id} ${story.title}`, opts.commitIdentity);
|
|
432
466
|
opts.git.integrate(opts.targetDir, wt);
|
|
433
467
|
result.routing?.recordOutcome(true);
|
|
468
|
+
clearFailureProgress(opts.targetDir, story.id);
|
|
434
469
|
landed = progress(updated);
|
|
435
470
|
}
|
|
436
471
|
catch (e) {
|
|
472
|
+
activeRouting?.recordOutcome(false, 'infrastructure');
|
|
437
473
|
const reason = blockReason(`isolated iteration failed for ${story.id}: ${e.message}`, opts.targetDir, opts.git);
|
|
438
474
|
reporter.blocked(reason);
|
|
439
475
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
@@ -452,89 +488,84 @@ export function runLoop(opts) {
|
|
|
452
488
|
reporter.storyDone({ id: story.id, title: story.title }, landed);
|
|
453
489
|
continue;
|
|
454
490
|
}
|
|
455
|
-
const
|
|
491
|
+
const implementation = runImplementation(opts, opts.targetDir, story, reporter);
|
|
492
|
+
const result = implementation.result;
|
|
456
493
|
iterations++;
|
|
457
494
|
if (result.tokens)
|
|
458
495
|
reporter.addTokens(result.tokens);
|
|
459
496
|
if (result.infrastructureFailure || result.routing?.blocked) {
|
|
460
497
|
if (result.infrastructureFailure)
|
|
461
498
|
result.routing?.recordOutcome(false, "infrastructure");
|
|
462
|
-
reporter.blocked(result.summary);
|
|
463
|
-
return { status: "blocked", iterations, reason: result.summary, finalProgress: progress(stories) };
|
|
499
|
+
reporter.blocked(result.summary, implementation.failure);
|
|
500
|
+
return { status: "blocked", iterations, reason: result.summary, ...(implementation.failure ? { failure: implementation.failure } : {}), finalProgress: progress(stories) };
|
|
464
501
|
}
|
|
465
502
|
let decision;
|
|
466
503
|
try {
|
|
467
504
|
decision = consumeDecisionRequest(opts.targetDir, opts.targetDir, story.id);
|
|
468
505
|
}
|
|
469
506
|
catch (error) {
|
|
507
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
470
508
|
const reason = `invalid critical decision request for story ${story.id}: ${error.message}`;
|
|
471
509
|
reporter.blocked(reason);
|
|
472
510
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
473
511
|
}
|
|
474
512
|
if (decision) {
|
|
513
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
475
514
|
const reason = `critical decision required for story ${story.id}: ${decision.question}`;
|
|
476
515
|
reporter.blocked(reason);
|
|
477
516
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
478
517
|
}
|
|
479
518
|
const ambiguity = consumeAmbiguity(opts.targetDir);
|
|
480
519
|
if (ambiguity) {
|
|
520
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
481
521
|
const reason = `story ${story.id} stopped: ambiguous acceptance criteria — ${ambiguity}`;
|
|
482
522
|
reporter.blocked(reason);
|
|
483
523
|
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
484
524
|
}
|
|
485
|
-
const
|
|
525
|
+
const verified = reuseGates(opts.targetDir, story, implementation.gates);
|
|
526
|
+
const criteriaVerdict = verified?.criteria ?? runCriterionGates(opts, opts.targetDir, story);
|
|
486
527
|
if (!criteriaVerdict.passed) {
|
|
487
528
|
result.routing?.recordOutcome(false);
|
|
488
529
|
const reason = blockReason(`story ${story.id} lacks acceptance evidence: ${criteriaVerdict.summary}`, opts.targetDir, opts.git);
|
|
489
|
-
|
|
490
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
530
|
+
return blockedMechanicalGate(opts, opts.targetDir, story, 'criterion', criteriaVerdict.summary, reason, iterations, stories);
|
|
491
531
|
}
|
|
492
532
|
// Verify is the source of truth — NOT the runner's exit code. A spurious non-zero
|
|
493
533
|
// exit (e.g. a Windows .cmd wrapper ghost) must not block a story whose tests are green.
|
|
494
534
|
reporter.phase('verifying');
|
|
495
|
-
const verdict = runGate(opts.verify, opts.targetDir, story.id);
|
|
535
|
+
const verdict = verified?.verify ?? runGate(opts.verify, opts.targetDir, story.id);
|
|
496
536
|
if (!verdict.passed) {
|
|
497
537
|
result.routing?.recordOutcome(false);
|
|
498
538
|
const base = result.success
|
|
499
539
|
? `story ${story.id} did not verify: ${verdict.summary}`
|
|
500
540
|
: `story ${story.id} runner failed (${result.summary}) and verify is red: ${verdict.summary}`;
|
|
501
541
|
const reason = blockReason(base, opts.targetDir, opts.git);
|
|
502
|
-
|
|
503
|
-
return {
|
|
504
|
-
status: 'blocked',
|
|
505
|
-
iterations,
|
|
506
|
-
reason,
|
|
507
|
-
finalProgress: progress(stories),
|
|
508
|
-
};
|
|
542
|
+
return blockedMechanicalGate(opts, opts.targetDir, story, 'verify', verdict.summary, reason, iterations, stories);
|
|
509
543
|
}
|
|
510
544
|
if (opts.design) {
|
|
511
545
|
reporter.phase('design');
|
|
512
|
-
const designVerdict = runGate(opts.design, opts.targetDir, story.id);
|
|
546
|
+
const designVerdict = verified?.design ?? runGate(opts.design, opts.targetDir, story.id);
|
|
513
547
|
if (!designVerdict.passed) {
|
|
514
548
|
result.routing?.recordOutcome(false);
|
|
515
549
|
const reason = blockReason(`story ${story.id} failed its design gate: ${designVerdict.summary}`, opts.targetDir, opts.git);
|
|
516
|
-
|
|
517
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
550
|
+
return blockedMechanicalGate(opts, opts.targetDir, story, 'design', designVerdict.summary, reason, iterations, stories);
|
|
518
551
|
}
|
|
519
552
|
}
|
|
520
553
|
if (opts.perf) {
|
|
521
554
|
reporter.phase('perf');
|
|
522
|
-
const perfVerdict = runGate(opts.perf, opts.targetDir, story.id);
|
|
555
|
+
const perfVerdict = verified?.perf ?? runGate(opts.perf, opts.targetDir, story.id);
|
|
523
556
|
if (!perfVerdict.passed) {
|
|
524
557
|
result.routing?.recordOutcome(false);
|
|
525
558
|
const reason = blockReason(`story ${story.id} exceeded its performance budget: ${perfVerdict.summary}`, opts.targetDir, opts.git);
|
|
526
|
-
|
|
527
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
559
|
+
return blockedMechanicalGate(opts, opts.targetDir, story, 'perf', perfVerdict.summary, reason, iterations, stories);
|
|
528
560
|
}
|
|
529
561
|
}
|
|
530
562
|
if (opts.audit) {
|
|
531
563
|
reporter.phase('audit');
|
|
532
|
-
const auditVerdict = runGate(opts.audit, opts.targetDir, story.id);
|
|
564
|
+
const auditVerdict = verified?.audit ?? runGate(opts.audit, opts.targetDir, story.id);
|
|
533
565
|
if (!auditVerdict.passed) {
|
|
534
566
|
result.routing?.recordOutcome(false);
|
|
535
567
|
const reason = blockReason(`story ${story.id} failed security audit: ${auditVerdict.summary}`, opts.targetDir, opts.git);
|
|
536
|
-
|
|
537
|
-
return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
|
|
568
|
+
return blockedMechanicalGate(opts, opts.targetDir, story, 'audit', auditVerdict.summary, reason, iterations, stories);
|
|
538
569
|
}
|
|
539
570
|
}
|
|
540
571
|
const summary = result.success
|
|
@@ -543,17 +574,19 @@ export function runLoop(opts) {
|
|
|
543
574
|
const repairOutcome = runQualityReview(opts, opts.targetDir, story, reporter);
|
|
544
575
|
if (repairOutcome) {
|
|
545
576
|
if (repairOutcome.kind === 'paused') {
|
|
577
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
546
578
|
reporter.paused(progress(stories));
|
|
547
579
|
return { status: 'paused', iterations, finalProgress: progress(stories) };
|
|
548
580
|
}
|
|
549
581
|
const reason = repairBlockReason(repairOutcome, story, opts.targetDir, opts.git);
|
|
550
582
|
if (reason) {
|
|
551
583
|
result.routing?.recordOutcome(false);
|
|
552
|
-
reporter.blocked(reason);
|
|
584
|
+
reporter.blocked(reason, repairOutcome.failure);
|
|
553
585
|
return {
|
|
554
586
|
status: 'blocked',
|
|
555
587
|
iterations,
|
|
556
588
|
reason,
|
|
589
|
+
...(repairOutcome.failure ? { failure: repairOutcome.failure } : {}),
|
|
557
590
|
finalProgress: progress(stories),
|
|
558
591
|
};
|
|
559
592
|
}
|
|
@@ -564,7 +597,6 @@ export function runLoop(opts) {
|
|
|
564
597
|
reporter.blocked(protection);
|
|
565
598
|
return { status: 'blocked', iterations, reason: protection, finalProgress: progress(stories) };
|
|
566
599
|
}
|
|
567
|
-
result.routing?.recordOutcome(true);
|
|
568
600
|
reporter.phase('committing');
|
|
569
601
|
const dec = appendDecision(contextDir(opts.targetDir), {
|
|
570
602
|
storyId: story.id,
|
|
@@ -581,6 +613,7 @@ export function runLoop(opts) {
|
|
|
581
613
|
opts.git.commitAll(opts.targetDir, `yoke: complete ${story.id} ${story.title}`, opts.commitIdentity);
|
|
582
614
|
}
|
|
583
615
|
catch (e) {
|
|
616
|
+
result.routing?.recordOutcome(false, 'infrastructure');
|
|
584
617
|
savePrd(opts.prdPath, onDisk); // revert — never persist passes:true without a commit
|
|
585
618
|
dec.rollback(); // and never leave an orphan decision
|
|
586
619
|
const reason = blockReason(`commit failed for ${story.id}: ${e.message}`, opts.targetDir, opts.git);
|
|
@@ -592,38 +625,59 @@ export function runLoop(opts) {
|
|
|
592
625
|
finalProgress: progress(stories),
|
|
593
626
|
};
|
|
594
627
|
}
|
|
628
|
+
result.routing?.recordOutcome(true);
|
|
629
|
+
clearFailureProgress(opts.targetDir, story.id);
|
|
595
630
|
reporter.storyDone({ id: story.id, title: story.title }, progress(updated));
|
|
596
631
|
}
|
|
597
632
|
}
|
|
633
|
+
function blockedMechanicalGate(opts, directory, story, stage, summary, reason, iterations, stories) {
|
|
634
|
+
const observed = observeFailure({ root: opts.targetDir, directory, story, stage, summary });
|
|
635
|
+
const detail = observed.action === 'retry' ? reason : `${reason}\n${observed.feedback}`;
|
|
636
|
+
(opts.reporter ?? noopReporter).blocked(detail, observed.failure);
|
|
637
|
+
return { status: 'blocked', iterations, reason: detail, failure: observed.failure, finalProgress: progress(stories) };
|
|
638
|
+
}
|
|
598
639
|
function runImplementation(opts, dir, story, reporter) {
|
|
599
|
-
let feedback;
|
|
640
|
+
let feedback = opts.feedback;
|
|
600
641
|
for (let attempt = 0;; attempt++) {
|
|
601
642
|
const result = opts.runner({ targetDir: dir, story, feedback });
|
|
643
|
+
const cacheProblem = opts.isolate && dir !== opts.targetDir ? cacheIsolationProblem(dir) : undefined;
|
|
644
|
+
if (cacheProblem)
|
|
645
|
+
return { result: { ...result, success: false, infrastructureFailure: true, summary: cacheProblem } };
|
|
602
646
|
if (!result.routing?.canRetry || result.routing.blocked || attempt >= 7)
|
|
603
|
-
return result;
|
|
647
|
+
return { result };
|
|
604
648
|
if (["decision-request.yaml", "ambiguity.md", "loop.pause"].some(name => existsSync(join(dir, ".yoke", name))) || existsSync(pauseFilePath(opts.targetDir)))
|
|
605
|
-
return result;
|
|
649
|
+
return { result };
|
|
606
650
|
const protection = acceptanceProtectionProblem(dir, opts.targetDir);
|
|
607
651
|
if (protection)
|
|
608
|
-
return result;
|
|
652
|
+
return { result };
|
|
653
|
+
const before = gateIdentity(dir, story);
|
|
609
654
|
const criteria = runCriterionGates(opts, dir, story);
|
|
610
|
-
const
|
|
655
|
+
const evidence = { criteria };
|
|
611
656
|
let verdict = criteria;
|
|
657
|
+
let failedStage = 'criterion';
|
|
612
658
|
if (verdict.passed)
|
|
613
|
-
for (const
|
|
659
|
+
for (const name of ['verify', 'design', 'perf', 'audit']) {
|
|
660
|
+
const gate = opts[name];
|
|
661
|
+
if (!gate)
|
|
662
|
+
continue;
|
|
614
663
|
verdict = runGate(gate, dir, story.id);
|
|
664
|
+
failedStage = name;
|
|
665
|
+
evidence[name] = verdict;
|
|
615
666
|
if (!verdict.passed)
|
|
616
667
|
break;
|
|
617
668
|
}
|
|
618
669
|
if (verdict.passed)
|
|
619
|
-
return result;
|
|
670
|
+
return { result, gates: snapshotGates(dir, story, before, evidence) };
|
|
620
671
|
if (knownInfrastructureFailure(verdict.summary)) {
|
|
621
672
|
result.routing.recordOutcome(false, 'infrastructure');
|
|
622
|
-
return { ...result, success: false, infrastructureFailure: true, summary: verdict.summary, routing: { ...result.routing, blocked: true, canRetry: false } };
|
|
673
|
+
return { result: { ...result, success: false, infrastructureFailure: true, summary: verdict.summary, routing: { ...result.routing, blocked: true, canRetry: false } } };
|
|
623
674
|
}
|
|
624
675
|
result.routing.recordOutcome(false);
|
|
676
|
+
const observed = observeFailure({ root: opts.targetDir, directory: dir, story, stage: failedStage, summary: verdict.summary });
|
|
677
|
+
if (observed.action === 'blocked')
|
|
678
|
+
return { result: { ...result, success: false, summary: observed.feedback, routing: { ...result.routing, blocked: true, canRetry: false } }, failure: observed.failure };
|
|
625
679
|
if (result.tokens)
|
|
626
680
|
reporter.addTokens(result.tokens);
|
|
627
|
-
feedback =
|
|
681
|
+
feedback = observed.feedback;
|
|
628
682
|
}
|
|
629
683
|
}
|
|
@@ -3,11 +3,14 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { isProviderTreeAlive, reapProviderProcesses } from './cleanup.js';
|
|
6
|
-
import { realGitOps, stageImplementation } from './git.js';
|
|
6
|
+
import { realGitOps, stageImplementation, RUNTIME_EXCLUDES } from './git.js';
|
|
7
7
|
import { isPidAlive } from './lock.js';
|
|
8
8
|
import { storyPathSegment } from './prd.js';
|
|
9
9
|
import { killProcessTreeForCleanup } from './watchdog.js';
|
|
10
10
|
import { parallelAcceptanceDigest, recoverParallelWorktree, retainParallelWorktree } from './recovery.js';
|
|
11
|
+
import { cacheIsolationProblem } from './cache-isolation.js';
|
|
12
|
+
import { retainRuntimeProof } from './proof-retention.js';
|
|
13
|
+
import { writeScopesOverlap } from './scheduler.js';
|
|
11
14
|
import { statePath } from '../workspace/state.js';
|
|
12
15
|
export function makeParallelAdapters(targetDir, identity, injectedGit) {
|
|
13
16
|
return injectedGit
|
|
@@ -62,11 +65,11 @@ function productionAdapters(targetDir, identity) {
|
|
|
62
65
|
return { path, baseCommit };
|
|
63
66
|
},
|
|
64
67
|
cleanupProcess: cleanupProviderProcesses,
|
|
65
|
-
retain: (input, reason) => {
|
|
68
|
+
retain: (input, reason, phase = 'integration') => {
|
|
66
69
|
const file = recoveryPath(targetDir, input.story.id);
|
|
67
70
|
// Preserve the path's original ownership identity when a retry is rejected again.
|
|
68
71
|
const ownerToken = ownershipTokens.get(input.worktree.path) ?? input.ownerToken;
|
|
69
|
-
retainParallelWorktree(targetDir, file, { storyId: input.story.id, worktree: input.worktree.path, baseCommit: input.worktree.baseCommit, prdHash: acceptanceDigests.get(input.story.id), ownerToken, reason });
|
|
72
|
+
retainParallelWorktree(targetDir, file, { storyId: input.story.id, worktree: input.worktree.path, baseCommit: input.worktree.baseCommit, prdHash: acceptanceDigests.get(input.story.id), ownerToken, reason, phase });
|
|
70
73
|
},
|
|
71
74
|
remove: input => {
|
|
72
75
|
removeOwnedWorktree(targetDir, input, owned, removed, realGitOps);
|
|
@@ -78,16 +81,23 @@ function productionAdapters(targetDir, identity) {
|
|
|
78
81
|
git: {
|
|
79
82
|
isClean: dir => realGitOps.isClean(dir),
|
|
80
83
|
rebase: input => rebaseCandidate(targetDir, input),
|
|
81
|
-
commit: input =>
|
|
84
|
+
commit: input => {
|
|
85
|
+
const unexpected = unexpectedWrites(input, changedPaths(input.worktree.path, gitText(input.worktree.path, ['rev-parse', 'HEAD'])));
|
|
86
|
+
if (unexpected.length)
|
|
87
|
+
throw new Error(`Candidate writes outside declared scopes after integrated gates: ${unexpected.join(', ')}`);
|
|
88
|
+
retainRuntimeProof(input.worktree.path, input.story.id, targetDir);
|
|
89
|
+
realGitOps.commitAll(input.worktree.path, `yoke: complete ${input.story.id} ${input.story.title}`, identity);
|
|
90
|
+
},
|
|
82
91
|
integrate: (input, expectedHead) => integrateCandidate(targetDir, input, expectedHead),
|
|
83
92
|
},
|
|
84
|
-
candidates: primary => makeCandidateLifecycle(targetDir, primary, owned, removed, realGitOps, ownershipTokens),
|
|
93
|
+
candidates: primary => makeCandidateLifecycle(targetDir, primary, owned, removed, realGitOps, ownershipTokens, acceptanceDigests.get(primary.story.id)),
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
function cleanupProviderProcesses(input) {
|
|
88
97
|
reapProviderProcesses(input.worktree.path, isPidAlive, isProviderTreeAlive, killProcessTreeForCleanup);
|
|
89
98
|
}
|
|
90
|
-
function makeCandidateLifecycle(targetDir, primary, owned, removed, git, ownershipTokens) {
|
|
99
|
+
function makeCandidateLifecycle(targetDir, primary, owned, removed, git, ownershipTokens, acceptanceDigest) {
|
|
100
|
+
const retainedRecords = new Map();
|
|
91
101
|
return {
|
|
92
102
|
reserve: input => {
|
|
93
103
|
const worktree = input.candidateId === 'candidate-1'
|
|
@@ -114,6 +124,23 @@ function makeCandidateLifecycle(targetDir, primary, owned, removed, git, ownersh
|
|
|
114
124
|
reap: ownership => {
|
|
115
125
|
reapProviderProcesses(ownership.worktree.path, isPidAlive, isProviderTreeAlive, killProcessTreeForCleanup);
|
|
116
126
|
},
|
|
127
|
+
...(ownershipTokens && acceptanceDigest ? {
|
|
128
|
+
retain: (ownership, reason, phase) => {
|
|
129
|
+
// The first retained candidate resumes through the existing dispatcher recovery path.
|
|
130
|
+
// Additional alternatives remain inspectable without creating a second scheduler.
|
|
131
|
+
const ownerToken = ownershipTokens.get(ownership.worktree.path) ?? ownership.ownerToken;
|
|
132
|
+
const suffix = createHash('sha256').update(ownerToken).digest('hex').slice(0, 24);
|
|
133
|
+
const file = retainedRecords.get(ownership.worktree.path) ?? (retainedRecords.size === 0
|
|
134
|
+
? recoveryPath(targetDir, ownership.storyId)
|
|
135
|
+
: statePath(targetDir, 'integration-recovery', `${storyPathSegment(ownership.storyId)}-candidate-${suffix}.json`));
|
|
136
|
+
retainedRecords.set(ownership.worktree.path, file);
|
|
137
|
+
retainParallelWorktree(targetDir, file, {
|
|
138
|
+
storyId: ownership.storyId, worktree: ownership.worktree.path, baseCommit: ownership.worktree.baseCommit,
|
|
139
|
+
prdHash: acceptanceDigest, ownerToken, reason, phase,
|
|
140
|
+
});
|
|
141
|
+
writeCandidateStatus(targetDir, ownership, ownership.worktree.path, 'retained', reason);
|
|
142
|
+
},
|
|
143
|
+
} : {}),
|
|
117
144
|
remove: ownership => {
|
|
118
145
|
removeOwnedWorktree(targetDir, ownership, owned, removed, git);
|
|
119
146
|
writeCandidateStatus(targetDir, ownership, ownership.worktree.path, 'removed');
|
|
@@ -159,7 +186,18 @@ function writeCandidateStatus(targetDir, input, worktree, state, reason) {
|
|
|
159
186
|
writeFileSync(file, JSON.stringify({ storyId: input.storyId, candidateId: input.candidateId, worktree, state, ...(reason ? { reason } : {}) }));
|
|
160
187
|
}
|
|
161
188
|
function rebaseCandidate(targetDir, input) {
|
|
189
|
+
const cacheProblem = cacheIsolationProblem(input.worktree.path);
|
|
190
|
+
if (cacheProblem)
|
|
191
|
+
return { kind: 'reopen', reason: cacheProblem };
|
|
162
192
|
const currentHead = gitText(targetDir, ['rev-parse', 'HEAD']);
|
|
193
|
+
const actual = changedPaths(input.worktree.path, input.worktree.baseCommit);
|
|
194
|
+
const unexpected = unexpectedWrites(input, actual);
|
|
195
|
+
const siblings = currentHead === input.worktree.baseCommit ? [] : changedPaths(targetDir, input.worktree.baseCommit);
|
|
196
|
+
const collisions = actual.filter(path => writeScopesOverlap([path], siblings));
|
|
197
|
+
if (unexpected.length)
|
|
198
|
+
return { kind: 'reopen', reason: `Candidate writes outside declared scopes: ${unexpected.join(', ')}${collisions.length ? `; integrated sibling collisions: ${collisions.join(', ')}` : ''}` };
|
|
199
|
+
if (collisions.length)
|
|
200
|
+
return { kind: 'reopen', reason: `Candidate writes collide with integrated sibling changes: ${collisions.join(', ')}` };
|
|
163
201
|
const candidateHead = gitText(input.worktree.path, ['rev-parse', 'HEAD']);
|
|
164
202
|
try {
|
|
165
203
|
execFileSync('git', ['merge-base', '--is-ancestor', input.worktree.baseCommit, 'HEAD'], { cwd: input.worktree.path, stdio: 'pipe' });
|
|
@@ -229,3 +267,16 @@ function gitText(dir, args) {
|
|
|
229
267
|
function errorMessage(error) {
|
|
230
268
|
return error instanceof Error ? error.message : String(error);
|
|
231
269
|
}
|
|
270
|
+
function changedPaths(directory, base) {
|
|
271
|
+
const read = (args) => execFileSync('git', args, { cwd: directory, stdio: 'pipe' }).toString().split('\0').filter(Boolean);
|
|
272
|
+
return [...new Set([...read(['diff', '--no-renames', '--name-only', '-z', base, '--', '.', ...RUNTIME_EXCLUDES]), ...read(['ls-files', '--others', '--exclude-standard', '-z', '--', '.', ...RUNTIME_EXCLUDES])])].filter(path => !/^(?:\.yoke\/(?:proof|context)\/|\.yoke\/prd\.(?:yaml|json)$)/u.test(path));
|
|
273
|
+
}
|
|
274
|
+
function unexpectedWrites(input, paths) {
|
|
275
|
+
if (input.story.writes === undefined)
|
|
276
|
+
return [];
|
|
277
|
+
const normalize = (value) => {
|
|
278
|
+
const path = value.replace(/\\/gu, '/').replace(/\/$/u, '');
|
|
279
|
+
return process.platform === 'win32' ? path.toLowerCase() : path;
|
|
280
|
+
};
|
|
281
|
+
return paths.filter(path => !input.story.writes.some(scope => normalize(path) === normalize(scope) || normalize(path).startsWith(`${normalize(scope)}/`)));
|
|
282
|
+
}
|