@mjasnikovs/pi-task 0.18.15 → 0.18.16
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/README.md +2 -2
- package/dist/task/accept-debt.d.ts +28 -1
- package/dist/task/accept-debt.js +61 -3
- package/dist/task/auto-io.d.ts +4 -2
- package/dist/task/auto-io.js +6 -3
- package/dist/task/auto-orchestrator.d.ts +1 -0
- package/dist/task/auto-orchestrator.js +135 -19
- package/dist/task/auto-prompts.d.ts +5 -5
- package/dist/task/auto-prompts.js +9 -2
- package/dist/task/contracts.d.ts +8 -0
- package/dist/task/contracts.js +4 -2
- package/dist/task/decompose-fidelity.d.ts +47 -0
- package/dist/task/decompose-fidelity.js +132 -0
- package/dist/task/final-gate-fix.d.ts +22 -3
- package/dist/task/final-gate-fix.js +72 -7
- package/dist/task/final-gate.d.ts +48 -1
- package/dist/task/final-gate.js +182 -34
- package/dist/task/gate-deps.d.ts +7 -0
- package/dist/task/gate-deps.js +37 -1
- package/dist/task/launch-contract.d.ts +36 -1
- package/dist/task/launch-contract.js +80 -2
- package/dist/task/phases.d.ts +13 -1
- package/dist/task/phases.js +50 -11
- package/dist/task/prompts.js +2 -0
- package/dist/task/render-check.d.ts +32 -0
- package/dist/task/render-check.js +186 -0
- package/dist/task/requirements.d.ts +88 -0
- package/dist/task/requirements.js +331 -0
- package/dist/task/verify-reconcile.d.ts +36 -0
- package/dist/task/verify-reconcile.js +203 -0
- package/dist/task/write-guard.d.ts +52 -0
- package/dist/task/write-guard.js +112 -0
- package/package.json +1 -1
- package/dist/task/_ab.d.ts +0 -1
- package/dist/task/_ab.js +0 -68
- package/dist/task/task-file.d.ts +0 -14
- package/dist/task/task-file.js +0 -15
- package/dist/think-test/cli.d.ts +0 -1
- package/dist/think-test/cli.js +0 -98
- package/dist/think-test/client.d.ts +0 -26
- package/dist/think-test/client.js +0 -37
- package/dist/think-test/compressor.d.ts +0 -5
- package/dist/think-test/compressor.js +0 -25
- package/dist/think-test/judge.d.ts +0 -4
- package/dist/think-test/judge.js +0 -11
- package/dist/think-test/score.d.ts +0 -8
- package/dist/think-test/score.js +0 -22
- package/dist/think-test/serialize.d.ts +0 -19
- package/dist/think-test/serialize.js +0 -41
- package/dist/think-test/transcript.d.ts +0 -7
- package/dist/think-test/transcript.js +0 -41
- package/dist/think-test/transform.d.ts +0 -6
- package/dist/think-test/transform.js +0 -24
- package/dist/think-test/types.d.ts +0 -45
- package/dist/think-test/types.js +0 -1
package/dist/task/final-gate.js
CHANGED
|
@@ -42,8 +42,10 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
42
42
|
import { existsSync, readFileSync } from 'node:fs';
|
|
43
43
|
import * as path from 'node:path';
|
|
44
44
|
import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.js';
|
|
45
|
-
import { readAcceptDebts, recheckAcceptDebts, writeAcceptDebts, buildAcceptDebtNote } from './accept-debt.js';
|
|
46
|
-
import { readDeclaredScripts, missingDeclaredScripts } from './launch-contract.js';
|
|
45
|
+
import { readAcceptDebts, recheckAcceptDebts, writeAcceptDebts, buildAcceptDebtNote, annotateDebtConflicts } from './accept-debt.js';
|
|
46
|
+
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
47
|
+
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
48
|
+
import { runRenderCheck } from './render-check.js';
|
|
47
49
|
function packageScripts(cwd) {
|
|
48
50
|
try {
|
|
49
51
|
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -79,7 +81,9 @@ export function discoverIntegrationCommands(cwd) {
|
|
|
79
81
|
// order, then `build`. Env-gap SKIP still applies per command (a suite whose
|
|
80
82
|
// browser/runtime is absent skips, it does not fail — see runGateCommand).
|
|
81
83
|
const testNames = Object.keys(s).filter(n => n === 'test' || /^test[:_-]/.test(n));
|
|
82
|
-
testNames.sort((a, b) =>
|
|
84
|
+
testNames.sort((a, b) => a === 'test' ? -1
|
|
85
|
+
: b === 'test' ? 1
|
|
86
|
+
: 0);
|
|
83
87
|
for (const name of testNames)
|
|
84
88
|
cmds.push(['bun', ['run', name]]);
|
|
85
89
|
if (s.build)
|
|
@@ -218,32 +222,46 @@ export function detectsServedApp(cwd, planText) {
|
|
|
218
222
|
}
|
|
219
223
|
return planText !== undefined && SERVE_TEXT_RE.test(planText);
|
|
220
224
|
}
|
|
221
|
-
/**
|
|
225
|
+
/** Listening TCP sockets as {pid, port} pairs (best-effort; ss first, then lsof).
|
|
222
226
|
* Empty on any failure — the caller then cannot attribute a listener to our group
|
|
223
227
|
* and the served-app check degrades to survival (never a false FAIL). */
|
|
224
|
-
function
|
|
225
|
-
const
|
|
228
|
+
function listeningSockets() {
|
|
229
|
+
const out = [];
|
|
226
230
|
try {
|
|
227
231
|
const t = spawnSync('ss', ['-tlnpH'], { encoding: 'utf8', timeout: 4000 });
|
|
228
232
|
if (!t.error && t.stdout) {
|
|
229
|
-
for (const
|
|
230
|
-
|
|
233
|
+
for (const line of t.stdout.split('\n')) {
|
|
234
|
+
const pm = /pid=(\d+)/.exec(line);
|
|
235
|
+
if (!pm)
|
|
236
|
+
continue;
|
|
237
|
+
// Column 4 (0-based 3) is the local address; the port is its last
|
|
238
|
+
// `:`-suffixed number ("0.0.0.0:3000", "[::]:3000").
|
|
239
|
+
const local = line.trim().split(/\s+/)[3] ?? '';
|
|
240
|
+
const portm = /:(\d+)$/.exec(local);
|
|
241
|
+
if (!portm)
|
|
242
|
+
continue;
|
|
243
|
+
out.push({ pid: Number(pm[1]), port: Number(portm[1]) });
|
|
244
|
+
}
|
|
231
245
|
}
|
|
232
246
|
}
|
|
233
247
|
catch {
|
|
234
248
|
// ss missing — try lsof
|
|
235
249
|
}
|
|
236
|
-
if (
|
|
250
|
+
if (out.length === 0) {
|
|
237
251
|
try {
|
|
238
|
-
const t = spawnSync('lsof', ['-iTCP', '-sTCP:LISTEN', '-
|
|
252
|
+
const t = spawnSync('lsof', ['-iTCP', '-sTCP:LISTEN', '-n', '-P'], {
|
|
239
253
|
encoding: 'utf8',
|
|
240
254
|
timeout: 4000
|
|
241
255
|
});
|
|
242
256
|
if (!t.error && t.stdout) {
|
|
243
|
-
for (const line of t.stdout.split('\n')) {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
257
|
+
for (const line of t.stdout.split('\n').slice(1)) {
|
|
258
|
+
const cols = line.trim().split(/\s+/);
|
|
259
|
+
const pid = Number(cols[1]);
|
|
260
|
+
const name = cols.find(c => /:\d+$/.test(c)) ?? '';
|
|
261
|
+
const portm = /:(\d+)$/.exec(name);
|
|
262
|
+
if (Number.isInteger(pid) && pid > 0 && portm) {
|
|
263
|
+
out.push({ pid, port: Number(portm[1]) });
|
|
264
|
+
}
|
|
247
265
|
}
|
|
248
266
|
}
|
|
249
267
|
}
|
|
@@ -251,7 +269,7 @@ function listeningSocketPids() {
|
|
|
251
269
|
// neither tool available
|
|
252
270
|
}
|
|
253
271
|
}
|
|
254
|
-
return
|
|
272
|
+
return out;
|
|
255
273
|
}
|
|
256
274
|
/** Process-group id of `pid`, or null if it cannot be read. */
|
|
257
275
|
function pgidOf(pid) {
|
|
@@ -270,12 +288,21 @@ function pgidOf(pid) {
|
|
|
270
288
|
/** Default listener probe: any LISTENing socket owned by a pid in process group
|
|
271
289
|
* `pgid` (the detached boot child IS its own group leader, so pgid === child.pid). */
|
|
272
290
|
function defaultGroupHasListener(pgid) {
|
|
273
|
-
for (const pid of
|
|
291
|
+
for (const { pid } of listeningSockets()) {
|
|
274
292
|
if (pgidOf(pid) === pgid)
|
|
275
293
|
return true;
|
|
276
294
|
}
|
|
277
295
|
return false;
|
|
278
296
|
}
|
|
297
|
+
/** Default port lookup for the render check: the LOWEST port among the group's
|
|
298
|
+
* listeners (a dev toolchain may open an HMR socket too; the app's own server
|
|
299
|
+
* conventionally sits on the lower, configured port). Null when undeterminable. */
|
|
300
|
+
function defaultGroupListeningPort(pgid) {
|
|
301
|
+
const ports = listeningSockets()
|
|
302
|
+
.filter(({ pid }) => pgidOf(pid) === pgid)
|
|
303
|
+
.map(({ port }) => port);
|
|
304
|
+
return ports.length > 0 ? Math.min(...ports) : null;
|
|
305
|
+
}
|
|
279
306
|
/** Default port-holder lookup: `lsof` first, then `ss`/`fuser`. Returns null on any
|
|
280
307
|
* failure (the diagnosis then omits the pid — never blocks). */
|
|
281
308
|
function defaultFindPortHolder(port) {
|
|
@@ -389,21 +416,40 @@ export function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}) {
|
|
|
389
416
|
// group already gone
|
|
390
417
|
}
|
|
391
418
|
};
|
|
392
|
-
const passAndKill = () => {
|
|
393
|
-
settle({ outcome: 'pass' });
|
|
419
|
+
const passAndKill = (renderNote) => {
|
|
420
|
+
settle(renderNote ? { outcome: 'pass', renderNote } : { outcome: 'pass' });
|
|
421
|
+
killGroup('SIGTERM');
|
|
422
|
+
setTimeout(() => killGroup('SIGKILL'), 2_000).unref();
|
|
423
|
+
};
|
|
424
|
+
const failAndKill = (detail) => {
|
|
425
|
+
settle({ outcome: 'fail', detail });
|
|
394
426
|
killGroup('SIGTERM');
|
|
395
427
|
setTimeout(() => killGroup('SIGKILL'), 2_000).unref();
|
|
396
428
|
};
|
|
397
429
|
// Served apps only: poll for a listening socket owned by our process group.
|
|
398
|
-
// As soon as one appears the boot has demonstrably served →
|
|
430
|
+
// As soon as one appears the boot has demonstrably served → run the render
|
|
431
|
+
// check against the LIVE listener (mx5 runs 8/11: a listener that serves a
|
|
432
|
+
// permanently blank page passed every curl-shaped check), then PASS/FAIL.
|
|
433
|
+
// The probe is spawnSync, so the interval cannot re-enter mid-check.
|
|
399
434
|
const poll = expectServer ?
|
|
400
435
|
setInterval(() => {
|
|
401
436
|
if (settled || !child.pid)
|
|
402
437
|
return;
|
|
403
|
-
if (groupHasListener(child.pid))
|
|
404
|
-
|
|
405
|
-
|
|
438
|
+
if (!groupHasListener(child.pid))
|
|
439
|
+
return;
|
|
440
|
+
listenerSeen = true;
|
|
441
|
+
const probe = opts.deps?.renderProbe;
|
|
442
|
+
if (!probe)
|
|
443
|
+
return passAndKill();
|
|
444
|
+
const port = (opts.deps?.groupListeningPort ?? defaultGroupListeningPort)(child.pid);
|
|
445
|
+
if (port === null) {
|
|
446
|
+
return passAndKill('render check UNOBSERVED: a listener was seen but its port could not be determined');
|
|
447
|
+
}
|
|
448
|
+
const rr = probe(`http://127.0.0.1:${port}/`);
|
|
449
|
+
if (rr.outcome === 'fail') {
|
|
450
|
+
return failAndKill(`listens on :${port} but ${rr.detail}`);
|
|
406
451
|
}
|
|
452
|
+
passAndKill(rr.outcome === 'skip' ? `render check UNOBSERVED: ${rr.note}` : undefined);
|
|
407
453
|
}, 500)
|
|
408
454
|
: null;
|
|
409
455
|
const timer = setTimeout(() => {
|
|
@@ -483,14 +529,23 @@ function outputTail(stdout, stderr, limit = 400) {
|
|
|
483
529
|
* These exit non-zero (not 127), so they need output-shape recognition to skip.
|
|
484
530
|
*/
|
|
485
531
|
const ENV_GAP_OUTPUT_RE = /Executable doesn't exist|playwright install|browserType\.\w+: Executable|(?:wasn't|weren't) installed|Host system is missing dependencies|No usable sandbox|Cypress verification|Cypress executable (?:not found|was not found)|browser(?:s)? (?:is|are)? ?not installed/i;
|
|
532
|
+
/**
|
|
533
|
+
* A non-zero exit whose output shows the EXTERNAL INFRASTRUCTURE a launch script
|
|
534
|
+
* talks to is absent HERE — a database/daemon that is not running or not
|
|
535
|
+
* installed — rather than a fault in the script itself. Applied ONLY to
|
|
536
|
+
* launch-contract scripts (a migrate/seed against no DB is an environment gap on
|
|
537
|
+
* this box; the same wording in a `test` run is a real failure the suite must own).
|
|
538
|
+
*/
|
|
539
|
+
export const INFRA_GAP_OUTPUT_RE = /ECONNREFUSED|connection refused|ENOTFOUND|EAI_AGAIN|is the server running|could not connect|cannot connect to the docker daemon|connect: connection|no such host/i;
|
|
486
540
|
/**
|
|
487
541
|
* Run one gate command with the env-gap contract: tool missing, timeout, or
|
|
488
542
|
* command-not-found inside the script chain (127) → environment gap, not a code
|
|
489
543
|
* fault → skipped (same contract as repo-health). Also skips a non-zero exit whose
|
|
490
|
-
* output shows a missing browser/runtime (ENV_GAP_OUTPUT_RE)
|
|
491
|
-
*
|
|
544
|
+
* output shows a missing browser/runtime (ENV_GAP_OUTPUT_RE) — or, when the caller
|
|
545
|
+
* passes `extraGapRe` (launch scripts), missing external infrastructure. Only a
|
|
546
|
+
* command that actually ran and exited non-zero for a real reason fails.
|
|
492
547
|
*/
|
|
493
|
-
function runGateCommand(cwd, [bin, args], timeoutMs) {
|
|
548
|
+
function runGateCommand(cwd, [bin, args], timeoutMs, extraGapRe) {
|
|
494
549
|
// env passed explicitly: bun's spawnSync resolves the binary against a
|
|
495
550
|
// startup snapshot of the environment, not the live process.env.
|
|
496
551
|
const r = spawnSync(bin, args, {
|
|
@@ -502,7 +557,10 @@ function runGateCommand(cwd, [bin, args], timeoutMs) {
|
|
|
502
557
|
if (r.error || r.status === null || r.status === 127)
|
|
503
558
|
return { outcome: 'skip' };
|
|
504
559
|
if (r.status !== 0) {
|
|
505
|
-
|
|
560
|
+
const output = `${r.stdout ?? ''}\n${r.stderr ?? ''}`;
|
|
561
|
+
if (ENV_GAP_OUTPUT_RE.test(output))
|
|
562
|
+
return { outcome: 'skip' };
|
|
563
|
+
if (extraGapRe?.test(output))
|
|
506
564
|
return { outcome: 'skip' };
|
|
507
565
|
return { outcome: 'fail', status: r.status, tail: outputTail(r.stdout ?? '', r.stderr ?? '') };
|
|
508
566
|
}
|
|
@@ -528,6 +586,27 @@ async function recoverOrphanPort(cwd, boot, first, bootGraceMs, deps, expectServ
|
|
|
528
586
|
await new Promise(r => setTimeout(r, 1_500));
|
|
529
587
|
return runBootCheck(cwd, boot, bootGraceMs, { expectServer, deps });
|
|
530
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* The task whose commit INTRODUCED `rel` (oldest `--diff-filter=A` commit whose
|
|
591
|
+
* subject carries the pi-task `(TASK_nnnn)` suffix — both the task snapshot and the
|
|
592
|
+
* ENFORCE commit shapes match). Null when the file predates the run, was never
|
|
593
|
+
* committed, git is unavailable, or the adding commit is not a task commit — every
|
|
594
|
+
* unknown degrades to "no conflict claim".
|
|
595
|
+
*/
|
|
596
|
+
export function taskThatIntroduced(cwd, rel) {
|
|
597
|
+
const r = spawnSync('git', ['log', '--diff-filter=A', '--format=%s', '--', rel], {
|
|
598
|
+
cwd,
|
|
599
|
+
encoding: 'utf8'
|
|
600
|
+
});
|
|
601
|
+
if (r.error || r.status !== 0 || !r.stdout)
|
|
602
|
+
return null;
|
|
603
|
+
const subjects = r.stdout.trim().split('\n').filter(Boolean);
|
|
604
|
+
// Newest-first output; the LAST line is the original introduction (a
|
|
605
|
+
// delete-and-re-add later in history must not reattribute the file).
|
|
606
|
+
const first = subjects[subjects.length - 1] ?? '';
|
|
607
|
+
const m = /\((TASK_\d+)\)\s*$/.exec(first);
|
|
608
|
+
return m ? m[1] : null;
|
|
609
|
+
}
|
|
531
610
|
/**
|
|
532
611
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
533
612
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
@@ -544,15 +623,23 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
544
623
|
// run may not complete silently carrying an accepted defect. FP-safe by
|
|
545
624
|
// construction (see accept-debt.ts). Best-effort: a ledger read/write failure
|
|
546
625
|
// must never break the gate.
|
|
547
|
-
const { open:
|
|
626
|
+
const { open: openRaw, resolved } = recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
548
627
|
staticOk: stat.ok
|
|
549
628
|
});
|
|
550
629
|
if (resolved.length > 0)
|
|
551
|
-
await writeAcceptDebts(cwd,
|
|
630
|
+
await writeAcceptDebts(cwd, openRaw);
|
|
631
|
+
// Conflicting-claim annotation (mx5 run 11): an existence-as-failure debt whose
|
|
632
|
+
// named file is another task's committed deliverable is a plan defect — surface
|
|
633
|
+
// the contradiction with the debt so nobody (human or child) treats the claim as
|
|
634
|
+
// a deletion instruction. Pure git-history lookup; degrades to no annotation.
|
|
635
|
+
const openDebts = annotateDebtConflicts(openRaw, p => taskThatIntroduced(cwd, p));
|
|
552
636
|
const debtNote = buildAcceptDebtNote(openDebts);
|
|
637
|
+
// The debt note rides in its OWN field: `reason` stays the mechanical failure
|
|
638
|
+
// because it seeds the autofix child's prompt (see FinalGateOutcome.reason —
|
|
639
|
+
// run 11's fix child executed a recorded claim as an instruction).
|
|
553
640
|
const withDebts = (o) => ({
|
|
554
641
|
...o,
|
|
555
|
-
|
|
642
|
+
...(debtNote ? { debtNote } : {}),
|
|
556
643
|
openDebts
|
|
557
644
|
});
|
|
558
645
|
if (!stat.ok)
|
|
@@ -596,12 +683,67 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
596
683
|
ran.push(label);
|
|
597
684
|
}
|
|
598
685
|
}
|
|
686
|
+
// EXECUTE the launch contract (mx5 run 11): every declared script that is
|
|
687
|
+
// neither boot-class (the boot check below owns those) nor already covered by
|
|
688
|
+
// the integration commands above RUNS as a one-shot, in declared order —
|
|
689
|
+
// existence is not launchability (`migrate`/`seed` shipped as first-call
|
|
690
|
+
// TypeErrors while the gate checked only that they exist). The env-gap
|
|
691
|
+
// contract extends to missing external INFRASTRUCTURE (no DB/daemon on this
|
|
692
|
+
// box → skip, not fail); a skip whose script also carries a standing EXCUSE
|
|
693
|
+
// note (F7) is surfaced as an UNOBSERVED warning — the note may be covering a
|
|
694
|
+
// real defect the gate could not reach here (run 11's "pre-existing .rows
|
|
695
|
+
// bug" note excused the exact scripts that shipped broken).
|
|
696
|
+
const warnings = [];
|
|
697
|
+
if (declared.length > 0) {
|
|
698
|
+
const covered = cmds.flatMap(([bin, args]) => (bin === 'bun' || bin === 'npm') && args[0] === 'run' && args[1] ? [args[1]] : []);
|
|
699
|
+
const skippedLaunch = [];
|
|
700
|
+
for (const name of runnableDeclaredScripts(declared, covered)) {
|
|
701
|
+
const cmd = ['bun', ['run', name]];
|
|
702
|
+
const label = `${cmd[0]} ${cmd[1].join(' ')}`;
|
|
703
|
+
const r = runGateCommand(cwd, cmd, Math.min(timeoutMs, 180_000), INFRA_GAP_OUTPUT_RE);
|
|
704
|
+
if (r.outcome === 'skip') {
|
|
705
|
+
skippedLaunch.push(name);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (r.outcome === 'fail') {
|
|
709
|
+
return withDebts({
|
|
710
|
+
ok: false,
|
|
711
|
+
reason: `launch script: \`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
ran.push(label);
|
|
715
|
+
}
|
|
716
|
+
if (skippedLaunch.length > 0) {
|
|
717
|
+
const notes = parseEnvNotes(await readEnvNotes(cwd)).filter(n => isExcuseNote(n.fact));
|
|
718
|
+
for (const name of skippedLaunch) {
|
|
719
|
+
const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
720
|
+
const excuse = notes.find(n => re.test(n.fact));
|
|
721
|
+
if (excuse) {
|
|
722
|
+
warnings.push(`launch script \`${name}\` could not run here (environment gap) and a `
|
|
723
|
+
+ `standing excuse note covers it ("${excuse.fact.slice(0, 160)}") — `
|
|
724
|
+
+ `UNOBSERVED: verify it by hand before trusting the launch surface`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
599
729
|
if (boot) {
|
|
600
730
|
const label = `${boot[0]} ${boot[1].join(' ')}`;
|
|
601
731
|
const expectServer = detectsServedApp(cwd, planText);
|
|
602
|
-
|
|
732
|
+
// Render check (mx5 runs 8/11): for a served app, load the live page in a
|
|
733
|
+
// headless browser and judge the RENDERED DOM — curl can't run JS, so a
|
|
734
|
+
// blank-mount app passed every prior "renders" check. Default to the real
|
|
735
|
+
// probe; tests inject their own. runRenderCheck env-gap-SKIPs when no
|
|
736
|
+
// browser exists, so a box without one never gets a false FAIL.
|
|
737
|
+
const bootDepsWithRender = {
|
|
738
|
+
...bootDeps,
|
|
739
|
+
renderProbe: bootDeps.renderProbe ?? runRenderCheck
|
|
740
|
+
};
|
|
741
|
+
let b = await runBootCheck(cwd, boot, bootGraceMs, {
|
|
742
|
+
expectServer,
|
|
743
|
+
deps: bootDepsWithRender
|
|
744
|
+
});
|
|
603
745
|
if (b.outcome === 'orphan-port') {
|
|
604
|
-
b = await recoverOrphanPort(cwd, boot, b, bootGraceMs,
|
|
746
|
+
b = await recoverOrphanPort(cwd, boot, b, bootGraceMs, bootDepsWithRender, expectServer);
|
|
605
747
|
}
|
|
606
748
|
if (b.outcome === 'fail') {
|
|
607
749
|
return withDebts({ ok: false, reason: `boot check: \`${label}\` ${b.detail}` });
|
|
@@ -618,13 +760,19 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
618
760
|
reason: `boot check: \`${label}\` could not bind: orphaned process / port already in use${who} (harness condition, not an app fault)`
|
|
619
761
|
});
|
|
620
762
|
}
|
|
621
|
-
if (b.outcome === 'pass')
|
|
763
|
+
if (b.outcome === 'pass') {
|
|
622
764
|
ran.push(label);
|
|
765
|
+
// A listener that served, but whose page could not be OBSERVED to render
|
|
766
|
+
// (no browser, undeterminable port) → UNOBSERVED warning, not a silent pass.
|
|
767
|
+
if (b.renderNote)
|
|
768
|
+
warnings.push(b.renderNote);
|
|
769
|
+
}
|
|
623
770
|
}
|
|
771
|
+
const warningNote = warnings.length > 0 ? ` — WARNING: ${warnings.join('; WARNING: ')}` : '';
|
|
624
772
|
return withDebts({
|
|
625
773
|
ok: true,
|
|
626
|
-
reason: ran.length > 0 ?
|
|
774
|
+
reason: (ran.length > 0 ?
|
|
627
775
|
`statics + ${ran.map(c => `\`${c}\``).join(', ')} passed`
|
|
628
|
-
: 'statics passed (integration commands not runnable here)'
|
|
776
|
+
: 'statics passed (integration commands not runnable here)') + warningNote
|
|
629
777
|
});
|
|
630
778
|
}
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { GateDeps } from './task-gates.js';
|
|
|
3
3
|
import { type FinalFixResult } from './final-gate-fix.js';
|
|
4
4
|
import { type AddedLine } from './probe-gaming.js';
|
|
5
5
|
import { type ChangedFile } from './substitution-probe.js';
|
|
6
|
+
import { type TreeChangeSummary } from './write-guard.js';
|
|
6
7
|
/** A function that re-runs a task's implementation turn (AUTOFIX). Injected by the
|
|
7
8
|
* command so this module stays free of the orchestrators (avoids an import cycle). */
|
|
8
9
|
export type RunTaskFn = GateDeps['runTask'];
|
|
@@ -33,6 +34,12 @@ export declare function collectChangedFiles(cwd: string, signal?: AbortSignal):
|
|
|
33
34
|
* never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
|
|
34
35
|
*/
|
|
35
36
|
export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Promise<AddedLine[]>;
|
|
37
|
+
/**
|
|
38
|
+
* The working tree's current changes as a summary (write-guard shape): what a
|
|
39
|
+
* write-capable gate child changed, given the tree was clean when it started.
|
|
40
|
+
* Failures degrade to an empty summary — the guard then has nothing to reject.
|
|
41
|
+
*/
|
|
42
|
+
export declare function collectTreeChanges(cwd: string, signal?: AbortSignal): Promise<TreeChangeSummary>;
|
|
36
43
|
/**
|
|
37
44
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
38
45
|
* implementation re-runner, injected by the caller. The returned object also drives
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -30,6 +30,7 @@ import { extractProhibitions, findProhibitionViolations } from './prohibition-pr
|
|
|
30
30
|
import { frozenPathsFromSpec, revertFrozenPaths } from './frozen-path-guard.js';
|
|
31
31
|
import { findProbeGaming, parseAddedLines } from './probe-gaming.js';
|
|
32
32
|
import { findSubstitutionSuspects, isTestFile } from './substitution-probe.js';
|
|
33
|
+
import { parseTreeChanges, formatTreeChanges } from './write-guard.js';
|
|
33
34
|
import { findTestRebuiltAssemblies, testAssemblyVerifyFindings } from './test-assembly.js';
|
|
34
35
|
import { runBoundedLintFix } from './lint-fix.js';
|
|
35
36
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
@@ -121,6 +122,15 @@ export async function collectAddedLines(cwd, signal) {
|
|
|
121
122
|
}
|
|
122
123
|
return lines;
|
|
123
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The working tree's current changes as a summary (write-guard shape): what a
|
|
127
|
+
* write-capable gate child changed, given the tree was clean when it started.
|
|
128
|
+
* Failures degrade to an empty summary — the guard then has nothing to reject.
|
|
129
|
+
*/
|
|
130
|
+
export async function collectTreeChanges(cwd, signal) {
|
|
131
|
+
const r = await git(cwd, ['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
132
|
+
return r.exitCode === 0 ? parseTreeChanges(r.stdout) : { modified: [], deleted: [], added: [] };
|
|
133
|
+
}
|
|
124
134
|
/** Source extensions whose relative imports the test-assembly probe reasons over. */
|
|
125
135
|
const SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/;
|
|
126
136
|
/** Bounds so the probe stays cheap on large repos (it reads file text). */
|
|
@@ -282,6 +292,13 @@ export function buildGateDeps(params) {
|
|
|
282
292
|
log(failure ? `=== ${kind} end: FAIL — ${failure} ===` : `=== ${kind} end: ok ===`);
|
|
283
293
|
if (failure)
|
|
284
294
|
throw new Error(failure);
|
|
295
|
+
// CAPABILITY-LEVEL diff capture (mx5 run 11): any WRITE-capable
|
|
296
|
+
// child — decided by its tools, not by which phase spawned it —
|
|
297
|
+
// gets its tree changes logged, so a future write-capable kind
|
|
298
|
+
// cannot run invisibly the way the final-fix child's `rm` did.
|
|
299
|
+
if (/\b(?:edit|bash|write)\b/.test(tools)) {
|
|
300
|
+
log(`=== ${kind} tree changes: ${formatTreeChanges(await collectTreeChanges(cwd2, sig))} ===`);
|
|
301
|
+
}
|
|
285
302
|
return r.text;
|
|
286
303
|
}
|
|
287
304
|
finally {
|
|
@@ -502,7 +519,26 @@ export function buildGateDeps(params) {
|
|
|
502
519
|
// shrink guard's discovery is the gate's own (see final-gate.ts).
|
|
503
520
|
gate: c => runFinalIntegrationGate(c),
|
|
504
521
|
discoverLabels: discoverGateCommandLabels,
|
|
505
|
-
discard: discardTreeEdits
|
|
522
|
+
discard: discardTreeEdits,
|
|
523
|
+
// WRITE-GUARD STACK (mx5 run 11: this child ran with free bash and
|
|
524
|
+
// none of the run-8 guards — it rm'd a sibling task's verified
|
|
525
|
+
// deliverable and hand-copied a contract to green the lint). Diff
|
|
526
|
+
// capture happens at the makeGateChild seam; deletion guard +
|
|
527
|
+
// probe scan reject-and-discard here. The frozen-path deny
|
|
528
|
+
// (FinalFixDeps.frozenPaths/revertFrozen) is deliberately NOT
|
|
529
|
+
// wired: per-task fences are task-SCOPED ("this task must not
|
|
530
|
+
// touch a sibling's territory"), and the measured union over the
|
|
531
|
+
// run-11 specs would have reverted the one legitimate fix that
|
|
532
|
+
// run needed (migrate.ts, frozen by its own producing task). Wire
|
|
533
|
+
// it only when a run-GLOBAL freeze source exists (a design-level
|
|
534
|
+
// preserve registry), never a per-task union.
|
|
535
|
+
treeChanges: () => collectTreeChanges(cwd2, signal),
|
|
536
|
+
probeScan: () => collectAddedLines(cwd2, signal).then(findProbeGaming),
|
|
537
|
+
log: msg => {
|
|
538
|
+
void fsp
|
|
539
|
+
.appendFile(path.join(tasksDir(cwd2), 'final-gate-debug.log'), `${new Date().toISOString()} ${msg}\n`)
|
|
540
|
+
.catch(() => { });
|
|
541
|
+
}
|
|
506
542
|
}),
|
|
507
543
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
508
544
|
// Read the same composed spec the verify gate judged against, so the
|
|
@@ -11,12 +11,42 @@ export declare function parseScriptLines(text: string): string[];
|
|
|
11
11
|
* false-flag on it. Deduplicated, case-insensitive.
|
|
12
12
|
*/
|
|
13
13
|
export declare function keepGroundedScripts(names: string[], sourceDoc: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* DETERMINISTIC RECALL (mx5 run 11): enumerate every backticked, script-name-shaped
|
|
16
|
+
* token in a paragraph that mentions the word "script", as extraction CANDIDATES.
|
|
17
|
+
*
|
|
18
|
+
* The run-11 failure this closes: `test:ct` is backticked in the design's §2 tooling
|
|
19
|
+
* paragraph, so the grounding guard would have KEPT it — but the extraction child
|
|
20
|
+
* anchored on §9's one-line summary (`dev`,`build`,`migrate`,`seed`,`test`) and never
|
|
21
|
+
* emitted it. Grounding can only DROP a candidate, never add one, so recall was
|
|
22
|
+
* entirely the model's, over a 20KB doc. This makes recall mechanical: the host
|
|
23
|
+
* enumerates candidates and hands them to the child as an explicit checklist; the
|
|
24
|
+
* model's job flips from recall (weak) to per-candidate classification (strong).
|
|
25
|
+
*
|
|
26
|
+
* The paragraph gate (`\bscripts?\b`, word-bounded so "TypeScript"/"JavaScript"
|
|
27
|
+
* don't match) is a grounded-context filter, not a tuned knob: a design declares a
|
|
28
|
+
* script by calling it one. It keeps package-name paragraphs (`hono`, `react`) out
|
|
29
|
+
* of the checklist so a weak model isn't invited to keep junk the grounding guard
|
|
30
|
+
* would then bless (every package name is backticked somewhere). A design with no
|
|
31
|
+
* such paragraph yields no candidates and the prompt is unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export declare function enumerateScriptCandidates(sourceDoc: string): string[];
|
|
14
34
|
/** The stored declared-script list ('' when none recorded). */
|
|
15
35
|
export declare function readLaunchContractRaw(cwd: string): Promise<string>;
|
|
16
36
|
/** The declared script names recorded for this run (deduped, order preserved). */
|
|
17
37
|
export declare function readDeclaredScripts(cwd: string): Promise<string[]>;
|
|
18
38
|
/** Append grounded script names, deduped against what is stored, keeping newest MAX. */
|
|
19
39
|
export declare function appendDeclaredScripts(cwd: string, names: string[]): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* The declared scripts the final gate must EXECUTE as one-shot commands (mx5 run
|
|
42
|
+
* 11): everything the launch contract declares that is neither boot-class (the
|
|
43
|
+
* boot check exercises those) nor already covered by the gate's integration
|
|
44
|
+
* commands (`covered`, case-insensitive — the test/build-shaped scripts that ran).
|
|
45
|
+
* Run 11 shipped `migrate` and `seed` broken (`.rows` on a Bun sql array —
|
|
46
|
+
* TypeError on first call) while the gate checked only that the scripts EXIST;
|
|
47
|
+
* existence is not launchability.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runnableDeclaredScripts(declared: string[], covered: string[]): string[];
|
|
20
50
|
/**
|
|
21
51
|
* Declared scripts the manifest does NOT expose (case-insensitive). Empty when every
|
|
22
52
|
* declared script is present, or when nothing was declared (no check). This is the
|
|
@@ -27,5 +57,10 @@ export declare function missingDeclaredScripts(declared: string[], manifestScrip
|
|
|
27
57
|
* The plan-time extraction prompt: the design in hand, emit the scripts it declares.
|
|
28
58
|
* Runs with --no-tools (pure extraction). Every emitted name is re-grounded HOST-SIDE
|
|
29
59
|
* (keepGroundedScripts), so a hallucinated script cannot reach the diff.
|
|
60
|
+
*
|
|
61
|
+
* `candidates` is enumerateScriptCandidates' mechanical checklist. It exists so the
|
|
62
|
+
* model cannot MISS a declared script buried far from the design's summary list (the
|
|
63
|
+
* run-11 `test:ct` hole); the model still classifies each candidate against the
|
|
64
|
+
* design, and the host grounding still applies. Empty ⇒ the prompt is unchanged.
|
|
30
65
|
*/
|
|
31
|
-
export declare const LAUNCH_EXTRACT_PROMPT: (feature: string) => string;
|
|
66
|
+
export declare const LAUNCH_EXTRACT_PROMPT: (feature: string, candidates?: string[]) => string;
|
|
@@ -42,7 +42,10 @@ export function parseScriptLines(text) {
|
|
|
42
42
|
const out = [];
|
|
43
43
|
for (const m of text.matchAll(/^[ \t]*SCRIPT:[ \t]*(.+)$/gim)) {
|
|
44
44
|
// Take the first whitespace/comma-delimited token, stripping backticks/quotes.
|
|
45
|
-
const raw = m[1]
|
|
45
|
+
const raw = m[1]
|
|
46
|
+
.trim()
|
|
47
|
+
.split(/[\s,]+/)[0]
|
|
48
|
+
?.replace(/[`'"]/g, '') ?? '';
|
|
46
49
|
if (SCRIPT_NAME_RE.test(raw))
|
|
47
50
|
out.push(raw);
|
|
48
51
|
}
|
|
@@ -69,6 +72,44 @@ export function keepGroundedScripts(names, sourceDoc) {
|
|
|
69
72
|
}
|
|
70
73
|
return kept;
|
|
71
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* DETERMINISTIC RECALL (mx5 run 11): enumerate every backticked, script-name-shaped
|
|
77
|
+
* token in a paragraph that mentions the word "script", as extraction CANDIDATES.
|
|
78
|
+
*
|
|
79
|
+
* The run-11 failure this closes: `test:ct` is backticked in the design's §2 tooling
|
|
80
|
+
* paragraph, so the grounding guard would have KEPT it — but the extraction child
|
|
81
|
+
* anchored on §9's one-line summary (`dev`,`build`,`migrate`,`seed`,`test`) and never
|
|
82
|
+
* emitted it. Grounding can only DROP a candidate, never add one, so recall was
|
|
83
|
+
* entirely the model's, over a 20KB doc. This makes recall mechanical: the host
|
|
84
|
+
* enumerates candidates and hands them to the child as an explicit checklist; the
|
|
85
|
+
* model's job flips from recall (weak) to per-candidate classification (strong).
|
|
86
|
+
*
|
|
87
|
+
* The paragraph gate (`\bscripts?\b`, word-bounded so "TypeScript"/"JavaScript"
|
|
88
|
+
* don't match) is a grounded-context filter, not a tuned knob: a design declares a
|
|
89
|
+
* script by calling it one. It keeps package-name paragraphs (`hono`, `react`) out
|
|
90
|
+
* of the checklist so a weak model isn't invited to keep junk the grounding guard
|
|
91
|
+
* would then bless (every package name is backticked somewhere). A design with no
|
|
92
|
+
* such paragraph yields no candidates and the prompt is unchanged.
|
|
93
|
+
*/
|
|
94
|
+
export function enumerateScriptCandidates(sourceDoc) {
|
|
95
|
+
const out = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const para of sourceDoc.split(/\n[ \t]*\n/)) {
|
|
98
|
+
if (!/\bscripts?\b/i.test(para))
|
|
99
|
+
continue;
|
|
100
|
+
for (const m of para.matchAll(/`([^`\n]+)`/g)) {
|
|
101
|
+
const tok = m[1].trim();
|
|
102
|
+
if (!SCRIPT_NAME_RE.test(tok))
|
|
103
|
+
continue;
|
|
104
|
+
const key = tok.toLowerCase();
|
|
105
|
+
if (seen.has(key))
|
|
106
|
+
continue;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
out.push(tok);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out.slice(0, MAX_SCRIPTS);
|
|
112
|
+
}
|
|
72
113
|
/** The stored declared-script list ('' when none recorded). */
|
|
73
114
|
export async function readLaunchContractRaw(cwd) {
|
|
74
115
|
try {
|
|
@@ -117,6 +158,26 @@ export async function appendDeclaredScripts(cwd, names) {
|
|
|
117
158
|
// best-effort artifact
|
|
118
159
|
}
|
|
119
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Boot-class script names: long-running serve/watch shapes the gate's BOOT check
|
|
163
|
+
* owns (it needs a listener, a grace window, and a group kill). These are never run
|
|
164
|
+
* as one-shot gate commands — a `dev` server run synchronously would only burn the
|
|
165
|
+
* timeout. Suffixed variants (`dev:client`, `start-prod`) are boot-class too.
|
|
166
|
+
*/
|
|
167
|
+
const BOOT_CLASS_RE = /^(?:dev|start|serve|preview|watch)(?:[:_-].*)?$/i;
|
|
168
|
+
/**
|
|
169
|
+
* The declared scripts the final gate must EXECUTE as one-shot commands (mx5 run
|
|
170
|
+
* 11): everything the launch contract declares that is neither boot-class (the
|
|
171
|
+
* boot check exercises those) nor already covered by the gate's integration
|
|
172
|
+
* commands (`covered`, case-insensitive — the test/build-shaped scripts that ran).
|
|
173
|
+
* Run 11 shipped `migrate` and `seed` broken (`.rows` on a Bun sql array —
|
|
174
|
+
* TypeError on first call) while the gate checked only that the scripts EXIST;
|
|
175
|
+
* existence is not launchability.
|
|
176
|
+
*/
|
|
177
|
+
export function runnableDeclaredScripts(declared, covered) {
|
|
178
|
+
const have = new Set(covered.map(s => s.toLowerCase()));
|
|
179
|
+
return declared.filter(n => !BOOT_CLASS_RE.test(n) && !have.has(n.toLowerCase()));
|
|
180
|
+
}
|
|
120
181
|
/**
|
|
121
182
|
* Declared scripts the manifest does NOT expose (case-insensitive). Empty when every
|
|
122
183
|
* declared script is present, or when nothing was declared (no check). This is the
|
|
@@ -139,8 +200,13 @@ export function missingDeclaredScripts(declared, manifestScripts) {
|
|
|
139
200
|
* The plan-time extraction prompt: the design in hand, emit the scripts it declares.
|
|
140
201
|
* Runs with --no-tools (pure extraction). Every emitted name is re-grounded HOST-SIDE
|
|
141
202
|
* (keepGroundedScripts), so a hallucinated script cannot reach the diff.
|
|
203
|
+
*
|
|
204
|
+
* `candidates` is enumerateScriptCandidates' mechanical checklist. It exists so the
|
|
205
|
+
* model cannot MISS a declared script buried far from the design's summary list (the
|
|
206
|
+
* run-11 `test:ct` hole); the model still classifies each candidate against the
|
|
207
|
+
* design, and the host grounding still applies. Empty ⇒ the prompt is unchanged.
|
|
142
208
|
*/
|
|
143
|
-
export const LAUNCH_EXTRACT_PROMPT = (feature) => [
|
|
209
|
+
export const LAUNCH_EXTRACT_PROMPT = (feature, candidates = []) => [
|
|
144
210
|
'You are recording the PACKAGE/BUILD SCRIPTS the design below says the finished',
|
|
145
211
|
'project MUST expose (the `scripts` a package.json / Makefile / task runner must',
|
|
146
212
|
'declare — e.g. build, test, a migration runner, a seed step, a start/serve command).',
|
|
@@ -150,6 +216,18 @@ export const LAUNCH_EXTRACT_PROMPT = (feature) => [
|
|
|
150
216
|
'DESIGN (the ONLY source — name only scripts the design itself declares):',
|
|
151
217
|
feature.trim(),
|
|
152
218
|
'',
|
|
219
|
+
...(candidates.length > 0 ?
|
|
220
|
+
[
|
|
221
|
+
'CANDIDATE TOKENS — found mechanically in the design near the word "script".',
|
|
222
|
+
'This checklist exists ONLY so you do not MISS a declared script; many of these',
|
|
223
|
+
'tokens are NOT scripts (config option names, tool names). For EACH candidate,',
|
|
224
|
+
'decide from the design whether it is a script the finished project must expose,',
|
|
225
|
+
'and emit it only if so. A declared script MISSING from this checklist must still',
|
|
226
|
+
'be emitted — the checklist is a floor, not a ceiling.',
|
|
227
|
+
candidates.map(c => ` - ${c}`).join('\n'),
|
|
228
|
+
''
|
|
229
|
+
]
|
|
230
|
+
: []),
|
|
153
231
|
'For each script the design declares by name, emit exactly:',
|
|
154
232
|
' SCRIPT: <name>',
|
|
155
233
|
'one per line, the bare script name only (e.g. `SCRIPT: migrate`). RULES: (1) name',
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -59,6 +59,18 @@ export declare function refineExistingFilesBlock(deps: PhaseDeps): Promise<strin
|
|
|
59
59
|
* no-op. Best-effort: a read fault yields '' rather than blocking the phase.
|
|
60
60
|
*/
|
|
61
61
|
export declare function phaseContractsBlock(deps: PhaseDeps): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* The carried-context blocks a GENERATIVE phase (refine, compose) receives: the
|
|
64
|
+
* cross-slice contracts plus the carried cross-cutting requirements (mx5 run 11,
|
|
65
|
+
* goals A/C — `.pi-tasks/requirements.md`, written at plan time). The verbatim
|
|
66
|
+
* requirement quotes travel INTO every task's spec generation, so a mandated
|
|
67
|
+
* methodology ("a test lands in the same change as each new route") reaches the
|
|
68
|
+
* task's GOAL/CONSTRAINTS and its VERIFY — a pointer back to the spec doc
|
|
69
|
+
* recovered the dropped §10 in only 1 of ~6 applicable run-11 tasks; content
|
|
70
|
+
* travels, pointers don't. Both blocks are '' outside their runs, so a bare
|
|
71
|
+
* /task is byte-identical to before.
|
|
72
|
+
*/
|
|
73
|
+
export declare function phaseCarriedBlocks(deps: PhaseDeps): Promise<string>;
|
|
62
74
|
export declare const phaseRefine: (deps: PhaseDeps, raw: string, planContext?: string) => Promise<string>;
|
|
63
75
|
export declare function phaseVerifyTooling(deps: PhaseDeps, research: string): Promise<string>;
|
|
64
76
|
export interface PhaseResearchDeps extends ExternalContextDeps {
|
|
@@ -105,7 +117,7 @@ export interface PhaseAutoAnswerDeps {
|
|
|
105
117
|
export declare function phaseAutoAnswer(deps: PhaseDeps, refined: string, research: string, question: string, autoDeps?: PhaseAutoAnswerDeps): Promise<AutoAnswer>;
|
|
106
118
|
export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext, widgetState: WidgetState, refined: string, research: string): Promise<string>;
|
|
107
119
|
export declare function phaseCompose(deps: PhaseDeps, refined: string, research: string, qa: string): Promise<string>;
|
|
108
|
-
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string): Promise<string>;
|
|
120
|
+
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string, planContext?: string): Promise<string>;
|
|
109
121
|
export declare function critiqueWithFallback(d: PhaseDeps, p: PhaseContext): Promise<string>;
|
|
110
122
|
export declare const PHASES: PhaseConfig[];
|
|
111
123
|
export declare function postCommitPhase(phase: PhaseConfig, deps: PhaseDeps, pc: PhaseContext, out: string): Promise<void>;
|