@mjasnikovs/pi-task 0.35.0 → 0.37.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/dist/remote/bridge.d.ts +6 -0
- package/dist/remote/bridge.js +1 -0
- package/dist/task/gate-deps.d.ts +35 -0
- package/dist/task/gate-deps.js +189 -104
- package/dist/task/plan-io.d.ts +22 -4
- package/dist/task/plan-io.js +30 -7
- package/dist/task/plan-prompts.js +8 -0
- package/dist/task/plan-session.d.ts +36 -3
- package/dist/task/plan-session.js +89 -14
- package/dist/task/question-box.d.ts +8 -0
- package/dist/task/question-box.js +6 -4
- package/dist/task/repo-health-check.d.ts +22 -0
- package/dist/task/repo-health-check.js +109 -25
- package/dist/task/task-gates.d.ts +5 -1
- package/dist/task/task-gates.js +4 -2
- package/dist/task/verify-work.d.ts +8 -0
- package/dist/task/verify-work.js +17 -0
- package/package.json +1 -1
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -57,6 +57,12 @@ export interface AskSpec {
|
|
|
57
57
|
* {@link AskQuestionBoxSpec.manualLabel}). Ignored without `options`.
|
|
58
58
|
*/
|
|
59
59
|
manualLabel?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Where the free-text card sits among `options` (see
|
|
62
|
+
* {@link AskQuestionBoxSpec.manualPosition}). Local picker only — remote
|
|
63
|
+
* browsers always render the text box above the action buttons.
|
|
64
|
+
*/
|
|
65
|
+
manualPosition?: number;
|
|
60
66
|
/**
|
|
61
67
|
* Extra buttons the BROWSER card shows alongside the recommendation, each
|
|
62
68
|
* answering with its own `value`. Unlike `options` — which are answers, and
|
package/dist/remote/bridge.js
CHANGED
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -38,6 +38,32 @@ export declare function collectChangedFiles(cwd: string, signal?: AbortSignal):
|
|
|
38
38
|
* never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
|
|
39
39
|
*/
|
|
40
40
|
export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Promise<AddedLine[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic neutered-check-script pass (see script-escape.ts, mx5 run 13 PROMPT
|
|
43
|
+
* 4 item 4): check-class scripts that cannot report failure, in a manifest THIS
|
|
44
|
+
* task changed.
|
|
45
|
+
*
|
|
46
|
+
* Scoped to manifests the task touched, so the finding lands on the task that
|
|
47
|
+
* authored the script rather than being re-served to every later task. A script
|
|
48
|
+
* neutered by an earlier task is the whole-repo final gate's business, which
|
|
49
|
+
* re-checks the shipped manifest at run end regardless of who wrote it.
|
|
50
|
+
*
|
|
51
|
+
* Failures degrade to no findings — a sharpener, never a blocker.
|
|
52
|
+
*/
|
|
53
|
+
export declare function collectScriptEscapeFindings(cwd: string, signal?: AbortSignal): Promise<string[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Deterministic test-runner glob-collision pass (see runner-globs.ts, mx5 runs 7 AND
|
|
56
|
+
* 13, PROMPT 4 item 2): the manifest declares both `bun test` and `playwright test`
|
|
57
|
+
* without a provably disjoint file set, so `bun test` imports the playwright specs
|
|
58
|
+
* and dies during collection.
|
|
59
|
+
*
|
|
60
|
+
* Whole-repo rather than diff-scoped, unlike the neutered-script probe: a collision
|
|
61
|
+
* is a property of the PAIR of declarations, and the task that completes the pair is
|
|
62
|
+
* rarely the one that will be blamed by a diff. It is cheap (two small file reads)
|
|
63
|
+
* and silent unless both runners are actually declared. Failures degrade to no
|
|
64
|
+
* findings — a sharpener, never a blocker.
|
|
65
|
+
*/
|
|
66
|
+
export declare function collectRunnerGlobFindings(cwd: string): Promise<string[]>;
|
|
41
67
|
/**
|
|
42
68
|
* The working tree's current changes as a summary (write-guard shape): what a
|
|
43
69
|
* write-capable gate child changed, given the tree was clean when it started.
|
|
@@ -87,6 +113,15 @@ export declare function gatePassesWithoutIgnored(cwd: string, paths: string[], r
|
|
|
87
113
|
* a sharpener, never a blocker. Same fallback discipline as collectChangedFiles.
|
|
88
114
|
*/
|
|
89
115
|
export declare function collectTaskTreeChanges(cwd: string, signal?: AbortSignal): Promise<TreeChangeSummary>;
|
|
116
|
+
/**
|
|
117
|
+
* Deterministic test-assembly probe input (see test-assembly.ts, run-8 F4): read the
|
|
118
|
+
* task's own changed TEST files plus the repo's tracked source files, and return the
|
|
119
|
+
* finding lines naming any test that rebuilds a production assembly it never imports.
|
|
120
|
+
* Pure import-graph shape; failures degrade to no findings (the probe is a sharpener,
|
|
121
|
+
* never a blocker). `changed` is the already-collected task diff, reused so the probe
|
|
122
|
+
* costs one extra tracked-file listing, not a second diff.
|
|
123
|
+
*/
|
|
124
|
+
export declare function collectTestAssemblyFindings(cwd: string, changed: ChangedFile[], signal?: AbortSignal): Promise<string[]>;
|
|
90
125
|
/**
|
|
91
126
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
92
127
|
* implementation re-runner, injected by the caller. The returned object also drives
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -24,7 +24,7 @@ import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
|
24
24
|
import { readContracts } from './contracts.js';
|
|
25
25
|
import { recordAcceptDebt, recordEnforceKeptDebt, recordEnforceRevertDebt, recordFrozenBlockedDebt, recordCrossTaskDeletionDebt, recordYoloAcceptDebt, recordRootCauseDebt } from './accept-debt.js';
|
|
26
26
|
import { recordRepairCandidate } from './root-cause-repair.js';
|
|
27
|
-
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
27
|
+
import { runRepoHealthCheck, runRepoHealthCheckAsync } from './repo-health-check.js';
|
|
28
28
|
import { runFinalIntegrationGate, discoverGateCommandLabels, discoverGateCommandBodies } from './final-gate.js';
|
|
29
29
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
30
30
|
import { researchResolution } from './verify-resolution.js';
|
|
@@ -174,7 +174,7 @@ const MANIFEST_RE = /(^|\/)package\.json$/;
|
|
|
174
174
|
*
|
|
175
175
|
* Failures degrade to no findings — a sharpener, never a blocker.
|
|
176
176
|
*/
|
|
177
|
-
async function collectScriptEscapeFindings(cwd, signal) {
|
|
177
|
+
export async function collectScriptEscapeFindings(cwd, signal) {
|
|
178
178
|
const changed = await collectChangedFiles(cwd, signal);
|
|
179
179
|
const manifests = changed.map(f => f.path).filter(p => MANIFEST_RE.test(p));
|
|
180
180
|
const findings = [];
|
|
@@ -217,7 +217,7 @@ async function readOrNull(cwd, rel) {
|
|
|
217
217
|
* and silent unless both runners are actually declared. Failures degrade to no
|
|
218
218
|
* findings — a sharpener, never a blocker.
|
|
219
219
|
*/
|
|
220
|
-
async function collectRunnerGlobFindings(cwd) {
|
|
220
|
+
export async function collectRunnerGlobFindings(cwd) {
|
|
221
221
|
const manifestText = await readOrNull(cwd, 'package.json');
|
|
222
222
|
if (manifestText === null)
|
|
223
223
|
return [];
|
|
@@ -419,7 +419,7 @@ async function readRepoFile(cwd, rel) {
|
|
|
419
419
|
* never a blocker). `changed` is the already-collected task diff, reused so the probe
|
|
420
420
|
* costs one extra tracked-file listing, not a second diff.
|
|
421
421
|
*/
|
|
422
|
-
async function collectTestAssemblyFindings(cwd, changed, signal) {
|
|
422
|
+
export async function collectTestAssemblyFindings(cwd, changed, signal) {
|
|
423
423
|
const changedTests = changed.filter(f => isTestFile(f.path));
|
|
424
424
|
if (changedTests.length === 0)
|
|
425
425
|
return [];
|
|
@@ -454,6 +454,11 @@ async function collectTestAssemblyFindings(cwd, changed, signal) {
|
|
|
454
454
|
*/
|
|
455
455
|
export function buildGateDeps(params) {
|
|
456
456
|
const { signal, parentContextWindow, runTask } = params;
|
|
457
|
+
// A/B seam (scripts/verify-deadair-ab.ts), same shape as CANCEL_AB_ARM in
|
|
458
|
+
// cancel-points.ts: reproduce the pre-fix gate — blocking sync repo health, no
|
|
459
|
+
// loader across the deterministic stage — so the dead air can be measured in the
|
|
460
|
+
// SAME binary rather than against a remembered baseline. Unset in every real run.
|
|
461
|
+
const deadAirBaseline = process.env.DEADAIR_AB_ARM === 'baseline';
|
|
457
462
|
// Captured by each gate child's loader so the widget mirrors the child's latest
|
|
458
463
|
// output line and context usage, exactly like the single-task phase widget.
|
|
459
464
|
let lastLine;
|
|
@@ -475,7 +480,11 @@ export function buildGateDeps(params) {
|
|
|
475
480
|
// disabled because re-running the same check is the job), with a status widget
|
|
476
481
|
// and a per-gate debug log. Returns the closure runWorkVerification /
|
|
477
482
|
// researchResolution expect as `runChild`.
|
|
478
|
-
const makeGateChild = (gateCtx, cwd2, taskTitle, kind, logFile
|
|
483
|
+
const makeGateChild = (gateCtx, cwd2, taskTitle, kind, logFile,
|
|
484
|
+
/** `loader: false` when the CALLER already renders a loader that spans
|
|
485
|
+
* this child (the verify gate does — see its dead-air note). Two
|
|
486
|
+
* loaders on one widget key only fight each other. */
|
|
487
|
+
opts = {}) => async (tools, prompt, sig) => {
|
|
479
488
|
lastLine = undefined;
|
|
480
489
|
contextUsage = undefined;
|
|
481
490
|
lastGuardReconcile = null;
|
|
@@ -493,16 +502,18 @@ export function buildGateDeps(params) {
|
|
|
493
502
|
// Snapshot before, deterministically restore after; lint-fix is excluded
|
|
494
503
|
// because editing is its job (it carries its own revert guard).
|
|
495
504
|
const guardSnapshot = kind === 'verify' || kind === 'recommend' ? await captureGitState(cwd2, sig) : null;
|
|
496
|
-
const stopLoader =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
505
|
+
const stopLoader = opts.loader === false ?
|
|
506
|
+
() => { }
|
|
507
|
+
: startAutoLoader(gateCtx, () => ({
|
|
508
|
+
title: taskTitle,
|
|
509
|
+
kind,
|
|
510
|
+
step: kind,
|
|
511
|
+
stepNum: 1,
|
|
512
|
+
stepTotal: 1,
|
|
513
|
+
startedAt,
|
|
514
|
+
lastLine,
|
|
515
|
+
contextUsage
|
|
516
|
+
}));
|
|
506
517
|
try {
|
|
507
518
|
let r;
|
|
508
519
|
try {
|
|
@@ -804,93 +815,145 @@ export function buildGateDeps(params) {
|
|
|
804
815
|
catch {
|
|
805
816
|
spec = null;
|
|
806
817
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
818
|
+
// DEAD AIR (the reason this loader exists). The gate's DETERMINISTIC
|
|
819
|
+
// stage — repo health plus ten probes — runs before the verify child,
|
|
820
|
+
// and the child's own loader only starts once the child does. The impl
|
|
821
|
+
// widget was cleared at `agent_end`, so until now the screen simply
|
|
822
|
+
// stopped: no spinner, no clock, no line (the `verifying…` notify cannot
|
|
823
|
+
// even paint, since pi-tui schedules renders on process.nextTick and the
|
|
824
|
+
// health check used to block the loop outright). MEASURED on real repos:
|
|
825
|
+
// 15s (mx5) to 69s (aiz-client) per health run, 0 of 686 expected 100ms
|
|
826
|
+
// timer ticks delivered. One loader now spans the WHOLE gate — the
|
|
827
|
+
// deterministic stage and the child — so the run is never silent.
|
|
828
|
+
const gateStartedAt = Date.now();
|
|
829
|
+
let stageLine;
|
|
830
|
+
// Clear the PREVIOUS child's trailer before the loader goes up: the
|
|
831
|
+
// deterministic stage has no child of its own, so a stale `↳` line from
|
|
832
|
+
// the last task's enforce pass would otherwise sit under the new
|
|
833
|
+
// status block as if it were live.
|
|
834
|
+
lastLine = undefined;
|
|
835
|
+
contextUsage = undefined;
|
|
836
|
+
const stopGateLoader = deadAirBaseline ?
|
|
837
|
+
() => { }
|
|
838
|
+
: startAutoLoader(verifyCtx, () => ({
|
|
839
|
+
title: taskTitle,
|
|
840
|
+
kind: 'verify',
|
|
841
|
+
step: 'verify',
|
|
842
|
+
stepNum: 1,
|
|
843
|
+
stepTotal: 1,
|
|
844
|
+
startedAt: gateStartedAt,
|
|
845
|
+
lastLine: lastLine ?? stageLine,
|
|
846
|
+
contextUsage
|
|
847
|
+
}));
|
|
848
|
+
try {
|
|
849
|
+
return await runWorkVerification({
|
|
850
|
+
cwd: cwd2,
|
|
851
|
+
signal,
|
|
852
|
+
spec,
|
|
853
|
+
// The child renders no loader of its own: the gate-wide one above is
|
|
854
|
+
// already live and reads the same `lastLine`/`contextUsage` the child
|
|
855
|
+
// feeds, so a second widget on the same key would only fight it.
|
|
856
|
+
runChild: makeGateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log', {
|
|
857
|
+
loader: deadAirBaseline
|
|
858
|
+
}),
|
|
859
|
+
// Names the deterministic step in the live status line.
|
|
860
|
+
onStage: label => {
|
|
861
|
+
stageLine = label;
|
|
862
|
+
},
|
|
863
|
+
// Deterministic whole-repo static-analysis gate — runs the project's
|
|
864
|
+
// own lint/typecheck and fails on a real non-zero exit, independent of
|
|
865
|
+
// the model-authored VERIFY block (which may not lint at all). ASYNC:
|
|
866
|
+
// the sync runner froze the event loop for the whole lint (see above).
|
|
867
|
+
repoHealth: () => deadAirBaseline ?
|
|
868
|
+
Promise.resolve(runRepoHealthCheck(cwd2))
|
|
869
|
+
: runRepoHealthCheckAsync(cwd2, {
|
|
870
|
+
signal,
|
|
871
|
+
onCommand: c => {
|
|
872
|
+
stageLine = `repo health · ${c}`;
|
|
873
|
+
}
|
|
874
|
+
}),
|
|
875
|
+
// Deterministic self-verification probe: test files the task itself
|
|
876
|
+
// authored/changed become prompt-level findings mandating the child
|
|
877
|
+
// to drive the real artifact before trusting their green result.
|
|
878
|
+
probe: () => collectChangedFiles(cwd2, signal).then(findSubstitutionSuspects),
|
|
879
|
+
// Deterministic test-assembly probe (F4): authored test files that
|
|
880
|
+
// rebuild production wiring — importing the leaf modules the shipped
|
|
881
|
+
// entry composes and assembling their own copy — become rule-3f
|
|
882
|
+
// findings so the child drives the REAL assembly, not the copy.
|
|
883
|
+
testAssemblyProbe: () => collectChangedFiles(cwd2, signal).then(changed => collectTestAssemblyFindings(cwd2, changed, signal)),
|
|
884
|
+
// Deterministic probe-gaming probe (F6): added lines whose stated
|
|
885
|
+
// purpose is to make a check pass rather than meet the requirement
|
|
886
|
+
// ("return 401 so the verification test passes") become rule-4c
|
|
887
|
+
// findings so the child verifies the real requirement, not the check.
|
|
888
|
+
probeGamingProbe: () => collectAddedLines(cwd2, signal).then(findProbeGaming),
|
|
889
|
+
// Deterministic cross-task deletion probe (mx5 run 12 PROMPT 2):
|
|
890
|
+
// tracked files this task's diff DELETES whose introducing commit
|
|
891
|
+
// belongs to a DIFFERENT task — a sibling's committed deliverable
|
|
892
|
+
// destroyed (typically to green a check). Injected under rule 4d and
|
|
893
|
+
// carried on a FAIL so an ACCEPT records durable debts.
|
|
894
|
+
crossTaskDeletionProbe: () => collectTaskTreeChanges(cwd2, signal).then(changes => findCrossTaskDeletions(changes, taskId, rel => taskThatIntroduced(cwd2, rel))),
|
|
895
|
+
// Deterministic sandbox-path-leak probe (mx5 run 13 PROMPT 4 item
|
|
896
|
+
// 1): absolute paths committed from the authoring child's own
|
|
897
|
+
// environment (`/workspace/src/shared`) that resolve nowhere here.
|
|
898
|
+
// Repaired deterministically where the relative form provably
|
|
899
|
+
// resolves; the remainder is injected under rule 4e, whose point is
|
|
900
|
+
// that such a path breaks the BUILD — so the checks that would have
|
|
901
|
+
// caught it report nothing rather than failing.
|
|
902
|
+
foreignPathProbe: () => collectForeignPathFindings(cwd2, signal, makeDebugAppender(path.join(tasksDir(cwd2), 'verify-debug.log'))),
|
|
903
|
+
// Deterministic neutered-check-script probe (mx5 run 13 PROMPT 4
|
|
904
|
+
// item 4): a check script this task authored that cannot fail
|
|
905
|
+
// (`… || true`, an inverted-grep launder). Injected under rule 4f,
|
|
906
|
+
// because the child provably cannot find this by running the
|
|
907
|
+
// script — it passes, which IS the defect.
|
|
908
|
+
scriptEscapeProbe: () => collectScriptEscapeFindings(cwd2, signal),
|
|
909
|
+
// Deterministic runner glob-collision probe (mx5 runs 7 AND 13,
|
|
910
|
+
// PROMPT 4 item 2): both `bun test` and `playwright test` declared
|
|
911
|
+
// with no proof their file sets are disjoint. Injected under rule
|
|
912
|
+
// 4g — the collision kills the suite during COLLECTION, which does
|
|
913
|
+
// not look like a test failure.
|
|
914
|
+
runnerGlobProbe: () => collectRunnerGlobFindings(cwd2),
|
|
915
|
+
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
916
|
+
// that the task's diff modified anyway become prompt-level findings
|
|
917
|
+
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
918
|
+
// diff` and cannot even see the violation.
|
|
919
|
+
prohibitionProbe: () => {
|
|
920
|
+
const banned = spec ? extractProhibitions(spec) : [];
|
|
921
|
+
if (banned.length === 0)
|
|
922
|
+
return Promise.resolve([]);
|
|
923
|
+
return collectChangedFiles(cwd2, signal).then(files => findProhibitionViolations(banned, files));
|
|
924
|
+
},
|
|
925
|
+
// Git-state guard result of the most recent child run: a verdict
|
|
926
|
+
// computed on a tree the child itself mutated is discarded — but ONLY
|
|
927
|
+
// when the mutation touched graded state (verdictTainted). A child
|
|
928
|
+
// that merely left test-runner output behind (test-results/,
|
|
929
|
+
// playwright-report/ …) judged an equivalent tree; its verdict stands
|
|
930
|
+
// and the artifacts were still cleaned (mx5 run 9 lost 7 verdicts this
|
|
931
|
+
// way — see git-state-guard.ts).
|
|
932
|
+
mutationCheck: () => lastGuardReconcile?.verdictTainted ?
|
|
933
|
+
{ mutated: true, detail: lastGuardReconcile.actions.join('; ') }
|
|
934
|
+
: { mutated: false, detail: '' },
|
|
935
|
+
// Per-run environment-facts cache under .pi-tasks/ (survives
|
|
936
|
+
// discardEdits): earlier children's discoveries save this child
|
|
937
|
+
// the re-archaeology; its own ENV-NOTE lines are stored for the
|
|
938
|
+
// next one, stamped with this task's id as their origin so a
|
|
939
|
+
// later child sees a cited fact is second-hand and must
|
|
940
|
+
// re-validate before excusing a failure (F7). Facts only —
|
|
941
|
+
// verdict rules unaffected.
|
|
942
|
+
envNotes: {
|
|
943
|
+
read: () => readEnvNotes(cwd2),
|
|
944
|
+
append: notes => appendEnvNotes(cwd2, notes, taskId)
|
|
945
|
+
},
|
|
946
|
+
// Per-run cross-slice contract registry under .pi-tasks/ (F3): the
|
|
947
|
+
// verbatim interface facts the design pins that multiple slices
|
|
948
|
+
// share, so the verify child checks this slice's boundary against
|
|
949
|
+
// them. Empty on single-`/task` runs or a design with no shared
|
|
950
|
+
// boundary → no block.
|
|
951
|
+
contracts: () => readContracts(cwd2)
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
stopGateLoader();
|
|
956
|
+
}
|
|
894
957
|
},
|
|
895
958
|
lintFix: async (fixCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
896
959
|
// Same frozen extraction the enforce guard and the verify rule-4b
|
|
@@ -911,7 +974,7 @@ export function buildGateDeps(params) {
|
|
|
911
974
|
signal,
|
|
912
975
|
failReason,
|
|
913
976
|
runChild: makeGateChild(fixCtx, cwd2, taskTitle, 'lint-fix', 'verify-debug.log'),
|
|
914
|
-
repoHealth: () =>
|
|
977
|
+
repoHealth: () => runRepoHealthCheckAsync(cwd2, { signal }),
|
|
915
978
|
git: async (args) => {
|
|
916
979
|
const r = await git(cwd2, args, signal);
|
|
917
980
|
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
@@ -927,7 +990,29 @@ export function buildGateDeps(params) {
|
|
|
927
990
|
});
|
|
928
991
|
},
|
|
929
992
|
// Deterministic static check + tree helpers for the enforce pre-commit gate.
|
|
930
|
-
|
|
993
|
+
// Runs TWICE per task there (a baseline before the edit pass, a differential
|
|
994
|
+
// check after it), each one as long as the project's own lint — so it gets
|
|
995
|
+
// the same treatment as the verify-side run: async, and under a live loader
|
|
996
|
+
// naming the command, instead of a frozen screen.
|
|
997
|
+
repoHealth: (healthCtx, cwd2, label) => {
|
|
998
|
+
const startedAt = Date.now();
|
|
999
|
+
let running;
|
|
1000
|
+
const stop = startAutoLoader(healthCtx, () => ({
|
|
1001
|
+
title: label,
|
|
1002
|
+
kind: 'enforce',
|
|
1003
|
+
step: 'repo health',
|
|
1004
|
+
stepNum: 1,
|
|
1005
|
+
stepTotal: 1,
|
|
1006
|
+
startedAt,
|
|
1007
|
+
lastLine: running ? `repo health · ${running}` : 'repo health'
|
|
1008
|
+
}));
|
|
1009
|
+
return runRepoHealthCheckAsync(cwd2, {
|
|
1010
|
+
signal,
|
|
1011
|
+
onCommand: c => {
|
|
1012
|
+
running = c;
|
|
1013
|
+
}
|
|
1014
|
+
}).finally(stop);
|
|
1015
|
+
},
|
|
931
1016
|
dirty: async (cwd2) => {
|
|
932
1017
|
const r = await git(cwd2, ['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
933
1018
|
return r.exitCode === 0 && r.stdout.trim().length > 0;
|
package/dist/task/plan-io.d.ts
CHANGED
|
@@ -43,13 +43,31 @@ export declare function buildPlanBody(task: string): string;
|
|
|
43
43
|
* there is no hidden channel.
|
|
44
44
|
*/
|
|
45
45
|
export declare function formatPlanDecisions(entries: readonly PlanEntry[]): string;
|
|
46
|
+
/**
|
|
47
|
+
* The line that pins the DELIVERABLE, and it is not optional.
|
|
48
|
+
*
|
|
49
|
+
* The task prompt leads the handoff verbatim, and users reach /task-plan by
|
|
50
|
+
* phrasing the request as planning — live (aiz-client TASK_PLAN_0001,
|
|
51
|
+
* 2026-08-05): "Lets plan new tab and report @src/app/reports/". /task's refine
|
|
52
|
+
* read the verb as the deliverable and produced a task titled "Plan the addition
|
|
53
|
+
* of a new sub-tab…", whose ACCEPTANCE was "a planning document exists with
|
|
54
|
+
* placeholder sections" and whose VERIFY asserted that no `.ts`/`.tsx` file had
|
|
55
|
+
* changed. It passed. Nothing was built.
|
|
56
|
+
*
|
|
57
|
+
* Planning already happened — this prompt IS its output — so the handoff says so
|
|
58
|
+
* rather than letting the request's own wording re-open it. It rides on every
|
|
59
|
+
* handoff, decisions or none: the verb leaks regardless of how much got settled.
|
|
60
|
+
*/
|
|
61
|
+
export declare const HANDOFF_DELIVERABLE_RULE: string;
|
|
46
62
|
/**
|
|
47
63
|
* The prompt handed to /task when the user proceeds to execution.
|
|
48
64
|
*
|
|
49
65
|
* The task prompt leads, exactly as a bare `/task <prompt>` would, so refine sees
|
|
50
|
-
* a normal task description first; the
|
|
51
|
-
* Anything the user did NOT settle is simply
|
|
52
|
-
* about what is left, which is why this
|
|
53
|
-
* gap.
|
|
66
|
+
* a normal task description first; the deliverable rule and then the decisions
|
|
67
|
+
* follow as an authoritative block. Anything the user did NOT settle is simply
|
|
68
|
+
* absent — /task's own grill phase asks about what is left, which is why this
|
|
69
|
+
* block never invents a decision to fill a gap. A question the user left
|
|
70
|
+
* unanswered is likewise absent: "(skipped)" is not a decision, and carrying it
|
|
71
|
+
* as one is how a non-answer becomes an instruction.
|
|
54
72
|
*/
|
|
55
73
|
export declare function buildHandoffPrompt(task: string, entries: readonly PlanEntry[]): string;
|
package/dist/task/plan-io.js
CHANGED
|
@@ -73,20 +73,43 @@ export function buildPlanBody(task) {
|
|
|
73
73
|
export function formatPlanDecisions(entries) {
|
|
74
74
|
return entries.length === 0 ? '(none yet)' : formatPlanTranscript(entries);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The line that pins the DELIVERABLE, and it is not optional.
|
|
78
|
+
*
|
|
79
|
+
* The task prompt leads the handoff verbatim, and users reach /task-plan by
|
|
80
|
+
* phrasing the request as planning — live (aiz-client TASK_PLAN_0001,
|
|
81
|
+
* 2026-08-05): "Lets plan new tab and report @src/app/reports/". /task's refine
|
|
82
|
+
* read the verb as the deliverable and produced a task titled "Plan the addition
|
|
83
|
+
* of a new sub-tab…", whose ACCEPTANCE was "a planning document exists with
|
|
84
|
+
* placeholder sections" and whose VERIFY asserted that no `.ts`/`.tsx` file had
|
|
85
|
+
* changed. It passed. Nothing was built.
|
|
86
|
+
*
|
|
87
|
+
* Planning already happened — this prompt IS its output — so the handoff says so
|
|
88
|
+
* rather than letting the request's own wording re-open it. It rides on every
|
|
89
|
+
* handoff, decisions or none: the verb leaks regardless of how much got settled.
|
|
90
|
+
*/
|
|
91
|
+
export const HANDOFF_DELIVERABLE_RULE = 'PLANNING IS ALREADY DONE. This prompt is the OUTPUT of an interactive planning session '
|
|
92
|
+
+ 'that has now ended; you are the implementation step. Build the thing. Do not produce a '
|
|
93
|
+
+ 'plan, a design, a proposal, or a "planning-only" deliverable, do not write a document '
|
|
94
|
+
+ 'whose acceptance is that no code changed, and do not defer the work pending user '
|
|
95
|
+
+ 'confirmation — no user is available from here on.';
|
|
76
96
|
/**
|
|
77
97
|
* The prompt handed to /task when the user proceeds to execution.
|
|
78
98
|
*
|
|
79
99
|
* The task prompt leads, exactly as a bare `/task <prompt>` would, so refine sees
|
|
80
|
-
* a normal task description first; the
|
|
81
|
-
* Anything the user did NOT settle is simply
|
|
82
|
-
* about what is left, which is why this
|
|
83
|
-
* gap.
|
|
100
|
+
* a normal task description first; the deliverable rule and then the decisions
|
|
101
|
+
* follow as an authoritative block. Anything the user did NOT settle is simply
|
|
102
|
+
* absent — /task's own grill phase asks about what is left, which is why this
|
|
103
|
+
* block never invents a decision to fill a gap. A question the user left
|
|
104
|
+
* unanswered is likewise absent: "(skipped)" is not a decision, and carrying it
|
|
105
|
+
* as one is how a non-answer becomes an instruction.
|
|
84
106
|
*/
|
|
85
107
|
export function buildHandoffPrompt(task, entries) {
|
|
86
|
-
const decisions = entries.filter(e => e.kind !== 'note');
|
|
108
|
+
const decisions = entries.filter(e => e.kind !== 'note' && !(e.kind === 'decision' && /^\(skipped/i.test(e.answer.trim())));
|
|
109
|
+
const head = `${task.trim()}\n\n${HANDOFF_DELIVERABLE_RULE}`;
|
|
87
110
|
if (decisions.length === 0)
|
|
88
|
-
return
|
|
89
|
-
return (`${
|
|
111
|
+
return head;
|
|
112
|
+
return (`${head}\n\n`
|
|
90
113
|
+ `PLANNING DECISIONS — these were settled with the user before this task was started. `
|
|
91
114
|
+ `They are authoritative: implement them as written, and do not re-open or contradict `
|
|
92
115
|
+ `them. They may not cover everything; decide anything they leave open as usual.\n\n`
|
|
@@ -67,6 +67,14 @@ repo, and any stated constraints; it is shown to the user as a recommendation th
|
|
|
67
67
|
accept or override. When the question is a genuine binary "A or B?" fork, also give
|
|
68
68
|
the single best alternative as an ALT line; otherwise emit only the one SUGGESTED.
|
|
69
69
|
|
|
70
|
+
The SUGGESTED must DECIDE. Never recommend asking, clarifying, confirming, or
|
|
71
|
+
checking with the user, and never recommend waiting, deferring, or leaving the
|
|
72
|
+
question open — the user is answering this very question right now, so "find out
|
|
73
|
+
from the user" is not an answer, it is the question you just asked. If you truly
|
|
74
|
+
cannot tell which way to go, still commit to the option you would take if it were
|
|
75
|
+
your call, and let the user override it. A default that could not be implemented
|
|
76
|
+
as written is not a default.
|
|
77
|
+
|
|
70
78
|
OUTPUT FORMAT (exact) — read as much as you like, but your written REPLY is 2 or
|
|
71
79
|
3 lines and nothing else:
|
|
72
80
|
- Do NOT report what you read. No preamble, no analysis, no findings, no numbered
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* doubles as "state a decision" when the model
|
|
21
21
|
* has nothing to ask
|
|
22
22
|
* ▶ proceed to execution — stop planning, hand the decisions to /task
|
|
23
|
-
* (PLAN_PROCEED)
|
|
23
|
+
* (PLAN_PROCEED). Always the LAST card in the
|
|
24
|
+
* box — it ends the session, so it sits under
|
|
25
|
+
* every move that continues it, including the
|
|
26
|
+
* free-text card (see `manualPosition`).
|
|
24
27
|
*
|
|
25
28
|
* The loop is pure with respect to I/O: every side effect (child calls, dialogs,
|
|
26
29
|
* persistence) arrives through {@link PlanSessionDeps}, so the whole interaction
|
|
@@ -85,6 +88,32 @@ export declare function pickQuestion<T extends {
|
|
|
85
88
|
* Deliberately shallow — an "X or Y?" in the question's own clause.
|
|
86
89
|
*/
|
|
87
90
|
export declare function looksLikeFork(question: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Does the recommended default DEFER the decision instead of making one?
|
|
93
|
+
*
|
|
94
|
+
* The prompt asks for a "concrete, decisive default", and nothing enforced it.
|
|
95
|
+
* Live (aiz-client TASK_PLAN_0001, 2026-08-05): the model asked "what specific
|
|
96
|
+
* report should this new tab display?" and recommended
|
|
97
|
+
* "clarify with the user what the report is meant to show before proceeding".
|
|
98
|
+
* The user pressed enter, so it was recorded `(accepted recommendation)` and rode
|
|
99
|
+
* into /task's handoff as an AUTHORITATIVE decision — an order not to proceed,
|
|
100
|
+
* addressed to a run where no user exists. /task duly built a task whose
|
|
101
|
+
* ACCEPTANCE was "a planning document with placeholder sections" and whose VERIFY
|
|
102
|
+
* asserted that no source file had changed.
|
|
103
|
+
*
|
|
104
|
+
* A deferral is not an answer, and the one place it can never be one is here: the
|
|
105
|
+
* user IS present during planning, so "ask the user" is a null move — that IS the
|
|
106
|
+
* question. Detection is anchored to the START of the default, which keeps it off
|
|
107
|
+
* legitimate product behaviour ("prompt the user to confirm deletion" decides
|
|
108
|
+
* something; "ask the user which report" decides nothing).
|
|
109
|
+
*/
|
|
110
|
+
export declare function isDeferralSuggestion(suggested: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Corrective re-prompt for a default that deferred the decision. Same one-shot
|
|
113
|
+
* budget and same quote-it-back shape as {@link planForkHint}, because the child
|
|
114
|
+
* is stateless and cannot otherwise know what it just recommended.
|
|
115
|
+
*/
|
|
116
|
+
export declare function planDecisiveHint(question: string, suggested: string): string;
|
|
88
117
|
/**
|
|
89
118
|
* Corrective re-prompt for a fork-shaped question that shipped only ONE option.
|
|
90
119
|
*
|
|
@@ -109,6 +138,7 @@ export type PlanAskSpec = AskSpec & {
|
|
|
109
138
|
value: string;
|
|
110
139
|
}[];
|
|
111
140
|
manualLabel: string;
|
|
141
|
+
manualPosition: number;
|
|
112
142
|
actions: {
|
|
113
143
|
label: string;
|
|
114
144
|
value: string;
|
|
@@ -155,9 +185,10 @@ interface PendingQuestion {
|
|
|
155
185
|
/**
|
|
156
186
|
* Build the picker for a pending model question: the recommendation first (index
|
|
157
187
|
* 0 is the green RECOMMENDED card), the alternative second when the question is a
|
|
158
|
-
* binary fork, then the two control actions. The free-text card is
|
|
188
|
+
* binary fork, then the two control actions. The free-text card is supplied by
|
|
159
189
|
* askQuestionBox itself — that is the "answer in your own words" affordance, and
|
|
160
|
-
* it is the same card grill and clarify already show
|
|
190
|
+
* it is the same card grill and clarify already show — placed just above the
|
|
191
|
+
* trailing "proceed to execution" card so proceed stays last.
|
|
161
192
|
*/
|
|
162
193
|
export declare function buildQuestionSpec(p: PendingQuestion): PlanAskSpec;
|
|
163
194
|
/**
|
|
@@ -165,6 +196,8 @@ export declare function buildQuestionSpec(p: PendingQuestion): PlanAskSpec;
|
|
|
165
196
|
* questions, or the cap/duplicate backstop stopped it. The same three moves are
|
|
166
197
|
* still on offer; only "answer this question" is gone, because there is no
|
|
167
198
|
* question, so the free-text card becomes "add a decision of your own".
|
|
199
|
+
* Proceed stays last here, as it is under a question: handing off to /task ends
|
|
200
|
+
* planning, so it never sits where a reflexive first-item press can hit it.
|
|
168
201
|
*/
|
|
169
202
|
export declare function buildIdleSpec(): PlanAskSpec;
|
|
170
203
|
/**
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* doubles as "state a decision" when the model
|
|
21
21
|
* has nothing to ask
|
|
22
22
|
* ▶ proceed to execution — stop planning, hand the decisions to /task
|
|
23
|
-
* (PLAN_PROCEED)
|
|
23
|
+
* (PLAN_PROCEED). Always the LAST card in the
|
|
24
|
+
* box — it ends the session, so it sits under
|
|
25
|
+
* every move that continues it, including the
|
|
26
|
+
* free-text card (see `manualPosition`).
|
|
24
27
|
*
|
|
25
28
|
* The loop is pure with respect to I/O: every side effect (child calls, dialogs,
|
|
26
29
|
* persistence) arrives through {@link PlanSessionDeps}, so the whole interaction
|
|
@@ -98,6 +101,51 @@ export function pickQuestion(parsed) {
|
|
|
98
101
|
export function looksLikeFork(question) {
|
|
99
102
|
return /\bor\b/i.test(question.split('?')[0] ?? '');
|
|
100
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Does the recommended default DEFER the decision instead of making one?
|
|
106
|
+
*
|
|
107
|
+
* The prompt asks for a "concrete, decisive default", and nothing enforced it.
|
|
108
|
+
* Live (aiz-client TASK_PLAN_0001, 2026-08-05): the model asked "what specific
|
|
109
|
+
* report should this new tab display?" and recommended
|
|
110
|
+
* "clarify with the user what the report is meant to show before proceeding".
|
|
111
|
+
* The user pressed enter, so it was recorded `(accepted recommendation)` and rode
|
|
112
|
+
* into /task's handoff as an AUTHORITATIVE decision — an order not to proceed,
|
|
113
|
+
* addressed to a run where no user exists. /task duly built a task whose
|
|
114
|
+
* ACCEPTANCE was "a planning document with placeholder sections" and whose VERIFY
|
|
115
|
+
* asserted that no source file had changed.
|
|
116
|
+
*
|
|
117
|
+
* A deferral is not an answer, and the one place it can never be one is here: the
|
|
118
|
+
* user IS present during planning, so "ask the user" is a null move — that IS the
|
|
119
|
+
* question. Detection is anchored to the START of the default, which keeps it off
|
|
120
|
+
* legitimate product behaviour ("prompt the user to confirm deletion" decides
|
|
121
|
+
* something; "ask the user which report" decides nothing).
|
|
122
|
+
*/
|
|
123
|
+
export function isDeferralSuggestion(suggested) {
|
|
124
|
+
const s = suggested.trim().replace(/^["'`*_\s]+/, '');
|
|
125
|
+
return (/^(ask|clarify|confirm|check|discuss|decide)\b[^.]{0,60}\b(with |from |the )?user\b/i.test(s)
|
|
126
|
+
|| /^(wait|hold off|hold|defer|postpone|pause|park)\b/i.test(s)
|
|
127
|
+
|| /^(tbd|to be (determined|decided|defined|specified))\b/i.test(s)
|
|
128
|
+
|| /^(pending|awaiting|await)\b/i.test(s)
|
|
129
|
+
|| /^leave (it|this|that)?\s*(to|for|open|undecided|unspecified)\b/i.test(s)
|
|
130
|
+
|| /^the user (must|should|needs? to|has to|will)\b/i.test(s)
|
|
131
|
+
|| /^(do not|don'?t|no)\b[^.]{0,40}\b(proceed|implement|build|start|write|decide)\b/i.test(s));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Corrective re-prompt for a default that deferred the decision. Same one-shot
|
|
135
|
+
* budget and same quote-it-back shape as {@link planForkHint}, because the child
|
|
136
|
+
* is stateless and cannot otherwise know what it just recommended.
|
|
137
|
+
*/
|
|
138
|
+
export function planDecisiveHint(question, suggested) {
|
|
139
|
+
return ('[SYSTEM NOTE: Your previous reply asked this question:\n'
|
|
140
|
+
+ `"${question}"\n`
|
|
141
|
+
+ `and recommended: "${suggested}"\n`
|
|
142
|
+
+ 'That recommendation DEFERS the decision instead of making one — it tells the user to '
|
|
143
|
+
+ 'ask, clarify, wait, or leave it open. The user is answering this question RIGHT NOW, so '
|
|
144
|
+
+ '"find out from the user" is not an answer, it is the question you just asked. Ask the '
|
|
145
|
+
+ 'SAME question again — do not change the subject — and this time make SUGGESTED a '
|
|
146
|
+
+ 'concrete choice that could be implemented as written, naming real things from the repo. '
|
|
147
|
+
+ 'If the question is a genuine A-or-B fork, add the ALT line too. Nothing else.]');
|
|
148
|
+
}
|
|
101
149
|
/**
|
|
102
150
|
* Corrective re-prompt for a fork-shaped question that shipped only ONE option.
|
|
103
151
|
*
|
|
@@ -124,9 +172,10 @@ export function planForkHint(question) {
|
|
|
124
172
|
/**
|
|
125
173
|
* Build the picker for a pending model question: the recommendation first (index
|
|
126
174
|
* 0 is the green RECOMMENDED card), the alternative second when the question is a
|
|
127
|
-
* binary fork, then the two control actions. The free-text card is
|
|
175
|
+
* binary fork, then the two control actions. The free-text card is supplied by
|
|
128
176
|
* askQuestionBox itself — that is the "answer in your own words" affordance, and
|
|
129
|
-
* it is the same card grill and clarify already show
|
|
177
|
+
* it is the same card grill and clarify already show — placed just above the
|
|
178
|
+
* trailing "proceed to execution" card so proceed stays last.
|
|
130
179
|
*/
|
|
131
180
|
export function buildQuestionSpec(p) {
|
|
132
181
|
const options = [];
|
|
@@ -153,7 +202,10 @@ export function buildQuestionSpec(p) {
|
|
|
153
202
|
allowSkip: false,
|
|
154
203
|
options: [...options, ...actions],
|
|
155
204
|
actions,
|
|
156
|
-
manualLabel: PLAN_ANSWER_LABEL
|
|
205
|
+
manualLabel: PLAN_ANSWER_LABEL,
|
|
206
|
+
// The free-text card goes ABOVE "proceed to execution" — proceed ends the
|
|
207
|
+
// session, so it is the last card in the box, under every other move.
|
|
208
|
+
manualPosition: options.length + actions.length - 1
|
|
157
209
|
};
|
|
158
210
|
}
|
|
159
211
|
/**
|
|
@@ -161,11 +213,13 @@ export function buildQuestionSpec(p) {
|
|
|
161
213
|
* questions, or the cap/duplicate backstop stopped it. The same three moves are
|
|
162
214
|
* still on offer; only "answer this question" is gone, because there is no
|
|
163
215
|
* question, so the free-text card becomes "add a decision of your own".
|
|
216
|
+
* Proceed stays last here, as it is under a question: handing off to /task ends
|
|
217
|
+
* planning, so it never sits where a reflexive first-item press can hit it.
|
|
164
218
|
*/
|
|
165
219
|
export function buildIdleSpec() {
|
|
166
220
|
const actions = [
|
|
167
|
-
{ label:
|
|
168
|
-
{ label:
|
|
221
|
+
{ label: PLAN_ASK_LABEL, value: PLAN_ASK },
|
|
222
|
+
{ label: PLAN_PROCEED_LABEL, value: PLAN_PROCEED }
|
|
169
223
|
];
|
|
170
224
|
return {
|
|
171
225
|
localTitle: PLAN_NO_QUESTIONS,
|
|
@@ -177,7 +231,8 @@ export function buildIdleSpec() {
|
|
|
177
231
|
allowSkip: false,
|
|
178
232
|
options: actions,
|
|
179
233
|
actions,
|
|
180
|
-
manualLabel: PLAN_STATE_LABEL
|
|
234
|
+
manualLabel: PLAN_STATE_LABEL,
|
|
235
|
+
manualPosition: actions.length - 1
|
|
181
236
|
};
|
|
182
237
|
}
|
|
183
238
|
/**
|
|
@@ -278,11 +333,31 @@ export async function runPlanSession(deps) {
|
|
|
278
333
|
formatHint = PLAN_FORMAT_HINT;
|
|
279
334
|
continue;
|
|
280
335
|
}
|
|
336
|
+
// A default that defers decides nothing, and an accepted deferral
|
|
337
|
+
// reaches /task dressed as an authoritative decision. One re-prompt to
|
|
338
|
+
// make it decisive; if it comes back deferring anyway the option is
|
|
339
|
+
// DROPPED rather than shown, so an empty submit records "(skipped)" —
|
|
340
|
+
// an unanswered question — instead of a decision the user never made.
|
|
341
|
+
const defers = suggested !== undefined && isDeferralSuggestion(suggested);
|
|
342
|
+
if (defers && formatHint === null) {
|
|
343
|
+
deps.logDebug?.('plan: SUGGESTED deferred the decision — one re-prompt');
|
|
344
|
+
formatHint = planDecisiveHint(plain, suggested);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// When only the recommendation defers, the ALT is still a real
|
|
348
|
+
// commitment: promote it so the question keeps a usable default.
|
|
349
|
+
const usableSuggested = defers ? alt : suggested;
|
|
350
|
+
const usableAlt = defers ? undefined : alt;
|
|
351
|
+
if (defers) {
|
|
352
|
+
deps.logDebug?.(usableSuggested === undefined ?
|
|
353
|
+
'plan: SUGGESTED still deferred — question shown with no recommendation'
|
|
354
|
+
: 'plan: SUGGESTED still deferred — promoted the ALT to the recommendation');
|
|
355
|
+
}
|
|
281
356
|
// A question that offers a choice but ships one option leaves the
|
|
282
357
|
// user typing out the alternative the model just named. Same one-shot
|
|
283
358
|
// budget, quoting the question back so the (stateless) child re-asks
|
|
284
359
|
// this one instead of a new one.
|
|
285
|
-
if (
|
|
360
|
+
if (usableAlt === undefined && formatHint === null && !defers && looksLikeFork(plain)) {
|
|
286
361
|
deps.logDebug?.('plan: fork-shaped question with no ALT — one re-prompt');
|
|
287
362
|
formatHint = planForkHint(plain);
|
|
288
363
|
continue;
|
|
@@ -294,13 +369,13 @@ export async function runPlanSession(deps) {
|
|
|
294
369
|
pending = {
|
|
295
370
|
plain,
|
|
296
371
|
shown: render(question),
|
|
297
|
-
...(
|
|
298
|
-
suggested: stripInlineMarkdown(
|
|
299
|
-
shownSuggested: render(
|
|
372
|
+
...(usableSuggested !== undefined && {
|
|
373
|
+
suggested: stripInlineMarkdown(usableSuggested),
|
|
374
|
+
shownSuggested: render(usableSuggested)
|
|
300
375
|
}),
|
|
301
|
-
...(
|
|
302
|
-
alt: stripInlineMarkdown(
|
|
303
|
-
shownAlt: render(
|
|
376
|
+
...(usableAlt !== undefined && {
|
|
377
|
+
alt: stripInlineMarkdown(usableAlt),
|
|
378
|
+
shownAlt: render(usableAlt)
|
|
304
379
|
})
|
|
305
380
|
};
|
|
306
381
|
}
|
|
@@ -84,6 +84,14 @@ export interface AskQuestionBoxSpec {
|
|
|
84
84
|
* what the card actually does.
|
|
85
85
|
*/
|
|
86
86
|
manualLabel?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Where the free-text card sits among the options. Defaults to the end, which
|
|
89
|
+
* is right when the picker is answering a question. /task-plan passes an
|
|
90
|
+
* earlier index so its "proceed to execution" card — the one that ENDS the
|
|
91
|
+
* session — is literally the last entry in the list, and never sits above a
|
|
92
|
+
* card the user is more likely to want.
|
|
93
|
+
*/
|
|
94
|
+
manualPosition?: number;
|
|
87
95
|
}
|
|
88
96
|
/**
|
|
89
97
|
* Show the boxed picker and resolve to the chosen option's `value`, the text the
|
|
@@ -143,12 +143,14 @@ export class QuestionBoxComponent {
|
|
|
143
143
|
*/
|
|
144
144
|
export async function askQuestionBox(ctx, spec) {
|
|
145
145
|
const { question, options, inputTitle, signal } = spec;
|
|
146
|
+
const manualIndex = Math.max(0, Math.min(options.length, spec.manualPosition ?? options.length));
|
|
147
|
+
const asCard = (o) => ({ label: o.label, recommended: o.recommended });
|
|
146
148
|
const cards = [
|
|
147
|
-
...options.
|
|
148
|
-
{ label: spec.manualLabel ?? MANUAL_CARD_LABEL }
|
|
149
|
+
...options.slice(0, manualIndex).map(asCard),
|
|
150
|
+
{ label: spec.manualLabel ?? MANUAL_CARD_LABEL },
|
|
151
|
+
...options.slice(manualIndex).map(asCard)
|
|
149
152
|
];
|
|
150
153
|
const colors = boxColors(ctx.ui.theme);
|
|
151
|
-
const manualIndex = options.length;
|
|
152
154
|
const choice = await ctx.ui.custom((_tui, _theme, _kb, done) => {
|
|
153
155
|
if (signal.aborted) {
|
|
154
156
|
done(undefined);
|
|
@@ -167,5 +169,5 @@ export async function askQuestionBox(ctx, spec) {
|
|
|
167
169
|
if (choice === manualIndex) {
|
|
168
170
|
return ctx.ui.input(inputTitle, undefined, { signal });
|
|
169
171
|
}
|
|
170
|
-
return options[choice]?.value;
|
|
172
|
+
return options[choice < manualIndex ? choice : choice - 1]?.value;
|
|
171
173
|
}
|
|
@@ -40,5 +40,27 @@ export declare function discoverHealthCommands(cwd: string): {
|
|
|
40
40
|
*
|
|
41
41
|
* A generous per-command timeout guards against a wedged tool; a timeout is treated
|
|
42
42
|
* as an inconclusive skip, not a fault (it is an environment problem, not the code's).
|
|
43
|
+
*
|
|
44
|
+
* SYNCHRONOUS — it blocks the event loop for as long as the project's own lint takes
|
|
45
|
+
* (MEASURED: 15s on mx5, 69s on aiz-client), so nothing can render or animate while
|
|
46
|
+
* it runs. Gate callers must use {@link runRepoHealthCheckAsync} instead; this stays
|
|
47
|
+
* for callers that genuinely have no async seam.
|
|
43
48
|
*/
|
|
44
49
|
export declare function runRepoHealthCheck(cwd: string, timeoutMs?: number): HealthOutcome;
|
|
50
|
+
/** Progress hook: called with each command's label as it STARTS, so a caller can
|
|
51
|
+
* keep a live status line naming what is currently running. */
|
|
52
|
+
export type HealthProgress = (command: string) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Same check, same verdicts, without blocking the event loop.
|
|
55
|
+
*
|
|
56
|
+
* The gate runs this immediately after the implementation turn ends, when the impl
|
|
57
|
+
* widget has just been cleared — the sync version froze the whole TUI there for the
|
|
58
|
+
* duration of the project's lint (MEASURED: 0 of 686 expected 100ms timer ticks
|
|
59
|
+
* fired during a 69s aiz-client run), so no spinner, clock or queued notify could
|
|
60
|
+
* paint. `onCommand` lets the caller name the running command in a live status line.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runRepoHealthCheckAsync(cwd: string, opts?: {
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
signal?: AbortSignal;
|
|
65
|
+
onCommand?: HealthProgress;
|
|
66
|
+
}): Promise<HealthOutcome>;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* environment gap, not a code fault, so that command is SKIPPED — only a command that
|
|
30
30
|
* actually ran and returned non-zero fails the check.
|
|
31
31
|
*/
|
|
32
|
-
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
33
33
|
import { existsSync, readFileSync } from 'node:fs';
|
|
34
34
|
import * as path from 'node:path';
|
|
35
35
|
import { resolveRunner, runnerEnv, isCommandNotFound } from './runner-resolve.js';
|
|
@@ -106,6 +106,38 @@ export function discoverHealthCommands(cwd) {
|
|
|
106
106
|
}
|
|
107
107
|
return { ecosystem: null, cmds: [] };
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Verdict for ONE finished command: 'skip' (environment gap — cannot conclude),
|
|
111
|
+
* 'pass', or the FAIL outcome. Shared by the sync and async runners so their
|
|
112
|
+
* semantics cannot drift apart — the async runner exists only to stop blocking the
|
|
113
|
+
* event loop, and a behaviour difference between the two would be a silent gate
|
|
114
|
+
* change rather than a UI fix.
|
|
115
|
+
*/
|
|
116
|
+
function classifyHealthRun(bin, args, ecosystem, r) {
|
|
117
|
+
// Tool missing (ENOENT) or killed by timeout → cannot conclude; skip it.
|
|
118
|
+
if (r.failedToStart || r.status === null)
|
|
119
|
+
return 'skip';
|
|
120
|
+
// "Command not found" INSIDE the script chain (e.g. `bun run lint` before
|
|
121
|
+
// node_modules exists — seen live failing TASK_0001's first verify). Same
|
|
122
|
+
// environment gap as ENOENT, just surfaced through the runner's shell —
|
|
123
|
+
// as exit 127 where a posix shell ran it, else by the runner's own wording
|
|
124
|
+
// (Windows bun reports the miss itself and exits 1).
|
|
125
|
+
if (isCommandNotFound(r.status, `${r.stdout ?? ''}\n${r.stderr ?? ''}`))
|
|
126
|
+
return 'skip';
|
|
127
|
+
if (r.status !== 0) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
reason: `\`${bin} ${args.join(' ')}\` exited ${r.status}`,
|
|
131
|
+
ecosystem,
|
|
132
|
+
output: captureHealthOutput(r.stdout, r.stderr)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return 'pass';
|
|
136
|
+
}
|
|
137
|
+
/** The nothing-to-run outcome, shared by both runners. */
|
|
138
|
+
function noCommandOutcome(ecosystem) {
|
|
139
|
+
return { ok: true, reason: 'no repo-wide static-analysis command found', ecosystem, output: '' };
|
|
140
|
+
}
|
|
109
141
|
/**
|
|
110
142
|
* Run the discovered static checks whole-repo and let the real exit codes decide.
|
|
111
143
|
* Deterministic and synchronous under the hood (a wrapper keeps the caller async).
|
|
@@ -117,17 +149,16 @@ export function discoverHealthCommands(cwd) {
|
|
|
117
149
|
*
|
|
118
150
|
* A generous per-command timeout guards against a wedged tool; a timeout is treated
|
|
119
151
|
* as an inconclusive skip, not a fault (it is an environment problem, not the code's).
|
|
152
|
+
*
|
|
153
|
+
* SYNCHRONOUS — it blocks the event loop for as long as the project's own lint takes
|
|
154
|
+
* (MEASURED: 15s on mx5, 69s on aiz-client), so nothing can render or animate while
|
|
155
|
+
* it runs. Gate callers must use {@link runRepoHealthCheckAsync} instead; this stays
|
|
156
|
+
* for callers that genuinely have no async seam.
|
|
120
157
|
*/
|
|
121
158
|
export function runRepoHealthCheck(cwd, timeoutMs = 600_000) {
|
|
122
159
|
const { ecosystem, cmds } = discoverHealthCommands(cwd);
|
|
123
|
-
if (!ecosystem || cmds.length === 0)
|
|
124
|
-
return
|
|
125
|
-
ok: true,
|
|
126
|
-
reason: 'no repo-wide static-analysis command found',
|
|
127
|
-
ecosystem,
|
|
128
|
-
output: ''
|
|
129
|
-
};
|
|
130
|
-
}
|
|
160
|
+
if (!ecosystem || cmds.length === 0)
|
|
161
|
+
return noCommandOutcome(ecosystem);
|
|
131
162
|
for (const [bin, args] of cmds) {
|
|
132
163
|
// Runner resolution (mx5 run 16): a PATH-stripped environment must not
|
|
133
164
|
// silently skip the statics when the runner sits at a known install
|
|
@@ -139,24 +170,77 @@ export function runRepoHealthCheck(cwd, timeoutMs = 600_000) {
|
|
|
139
170
|
timeout: timeoutMs,
|
|
140
171
|
env: runnerEnv(runner)
|
|
141
172
|
});
|
|
142
|
-
|
|
143
|
-
|
|
173
|
+
const verdict = classifyHealthRun(bin, args, ecosystem, {
|
|
174
|
+
failedToStart: r.error !== undefined,
|
|
175
|
+
status: r.status,
|
|
176
|
+
stdout: r.stdout ?? '',
|
|
177
|
+
stderr: r.stderr ?? ''
|
|
178
|
+
});
|
|
179
|
+
if (verdict === 'skip' || verdict === 'pass')
|
|
144
180
|
continue;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
return verdict;
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, reason: `${ecosystem}: static checks passed`, ecosystem, output: '' };
|
|
184
|
+
}
|
|
185
|
+
/** Spawn one health command without blocking the event loop. Mirrors spawnSync's
|
|
186
|
+
* result shape (status null when killed, failedToStart on ENOENT). */
|
|
187
|
+
function spawnHealthCommand(bin, args, cwd, timeoutMs, signal) {
|
|
188
|
+
return new Promise(resolve => {
|
|
189
|
+
const runner = resolveRunner(bin);
|
|
190
|
+
let stdout = '';
|
|
191
|
+
let stderr = '';
|
|
192
|
+
let settled = false;
|
|
193
|
+
const child = spawn(runner.bin, args, { cwd, env: runnerEnv(runner) });
|
|
194
|
+
const done = (r) => {
|
|
195
|
+
if (settled)
|
|
196
|
+
return;
|
|
197
|
+
settled = true;
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
signal?.removeEventListener('abort', onAbort);
|
|
200
|
+
resolve(r);
|
|
201
|
+
};
|
|
202
|
+
const kill = () => {
|
|
203
|
+
try {
|
|
204
|
+
child.kill('SIGKILL');
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
/* already gone */
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const timer = setTimeout(kill, timeoutMs);
|
|
211
|
+
timer.unref?.();
|
|
212
|
+
const onAbort = () => kill();
|
|
213
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
214
|
+
child.stdout?.on('data', (d) => {
|
|
215
|
+
stdout += d.toString();
|
|
216
|
+
});
|
|
217
|
+
child.stderr?.on('data', (d) => {
|
|
218
|
+
stderr += d.toString();
|
|
219
|
+
});
|
|
220
|
+
child.on('error', () => done({ failedToStart: true, status: null, stdout, stderr }));
|
|
221
|
+
child.on('close', (code) => done({ failedToStart: false, status: code, stdout, stderr }));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Same check, same verdicts, without blocking the event loop.
|
|
226
|
+
*
|
|
227
|
+
* The gate runs this immediately after the implementation turn ends, when the impl
|
|
228
|
+
* widget has just been cleared — the sync version froze the whole TUI there for the
|
|
229
|
+
* duration of the project's lint (MEASURED: 0 of 686 expected 100ms timer ticks
|
|
230
|
+
* fired during a 69s aiz-client run), so no spinner, clock or queued notify could
|
|
231
|
+
* paint. `onCommand` lets the caller name the running command in a live status line.
|
|
232
|
+
*/
|
|
233
|
+
export async function runRepoHealthCheckAsync(cwd, opts = {}) {
|
|
234
|
+
const { ecosystem, cmds } = discoverHealthCommands(cwd);
|
|
235
|
+
if (!ecosystem || cmds.length === 0)
|
|
236
|
+
return noCommandOutcome(ecosystem);
|
|
237
|
+
for (const [bin, args] of cmds) {
|
|
238
|
+
opts.onCommand?.(`${bin} ${args.join(' ')}`);
|
|
239
|
+
const r = await spawnHealthCommand(bin, args, cwd, opts.timeoutMs ?? 600_000, opts.signal);
|
|
240
|
+
const verdict = classifyHealthRun(bin, args, ecosystem, r);
|
|
241
|
+
if (verdict === 'skip' || verdict === 'pass')
|
|
151
242
|
continue;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
ok: false,
|
|
155
|
-
reason: `\`${bin} ${args.join(' ')}\` exited ${r.status}`,
|
|
156
|
-
ecosystem,
|
|
157
|
-
output: captureHealthOutput(r.stdout, r.stderr)
|
|
158
|
-
};
|
|
159
|
-
}
|
|
243
|
+
return verdict;
|
|
160
244
|
}
|
|
161
245
|
return { ok: true, reason: `${ecosystem}: static checks passed`, ecosystem, output: '' };
|
|
162
246
|
}
|
|
@@ -99,7 +99,11 @@ export interface GateDeps {
|
|
|
99
99
|
* revert cycle. Checking before committing skips that cycle. Absent → the old
|
|
100
100
|
* commit-then-differential path runs unchanged.
|
|
101
101
|
*/
|
|
102
|
-
|
|
102
|
+
/** Deterministic whole-repo static check for the enforce pre-commit gate. Takes
|
|
103
|
+
* the live ctx and a label so the implementation can render a status line while
|
|
104
|
+
* it runs — it is as slow as the project's own lint (15–69s measured), and a
|
|
105
|
+
* gate step that long with no widget is indistinguishable from a hang. */
|
|
106
|
+
repoHealth?: (ctx: ExtensionCommandContext, cwd: string, label: string) => Promise<{
|
|
103
107
|
ok: boolean;
|
|
104
108
|
reason: string;
|
|
105
109
|
output?: string;
|
package/dist/task/task-gates.js
CHANGED
|
@@ -419,7 +419,9 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
419
419
|
// good work for a fault it did not cause). Only meaningful in edit mode (flag
|
|
420
420
|
// makes no edits); the task's work is already committed so this reflects the
|
|
421
421
|
// committed state the pass is about to build on.
|
|
422
|
-
const healthBefore = mode === 'edit' && deps.repoHealth ?
|
|
422
|
+
const healthBefore = mode === 'edit' && deps.repoHealth ?
|
|
423
|
+
await deps.repoHealth(active, p.cwd, p.title)
|
|
424
|
+
: undefined;
|
|
423
425
|
const verdict = await deps.enforce(active, p.cwd, p.title, mode);
|
|
424
426
|
// FROZEN-PATH WRITE-DENY (mechanical, not prompt — the "MUST NOT edit"
|
|
425
427
|
// instruction is A/B-proven ~0–1/5 reliable on the weak model): the enforce
|
|
@@ -466,7 +468,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
466
468
|
// unreproducible precisely because only the exit code was recorded.
|
|
467
469
|
let enforceEditsBlocked = false;
|
|
468
470
|
if (mode === 'edit' && deps.repoHealth && editsMade !== false) {
|
|
469
|
-
const after = await deps.repoHealth(p.cwd);
|
|
471
|
+
const after = await deps.repoHealth(active, p.cwd, p.title);
|
|
470
472
|
// A regression needs a clean (or unknown) baseline turning to a fail. If
|
|
471
473
|
// healthBefore is undefined (repoHealth was absent at baseline time) treat
|
|
472
474
|
// the baseline as clean — the conservative absolute behavior.
|
|
@@ -122,6 +122,14 @@ export interface VerificationDeps {
|
|
|
122
122
|
ok: boolean;
|
|
123
123
|
reason: string;
|
|
124
124
|
}>;
|
|
125
|
+
/**
|
|
126
|
+
* Progress hook for the DETERMINISTIC stage — the repo-health run plus the ten
|
|
127
|
+
* probes below, all of which run BEFORE the child (and therefore before the
|
|
128
|
+
* child's own status widget exists). Called with a short label as each step
|
|
129
|
+
* starts, so the caller can keep a live line on screen through what was
|
|
130
|
+
* otherwise the run's longest stretch of dead air (MEASURED at 15–69s per
|
|
131
|
+
* repo-health run). ABSENT → no progress reporting, same behaviour as before. */
|
|
132
|
+
onStage?: (stage: string) => void;
|
|
125
133
|
/**
|
|
126
134
|
* DETERMINISTIC substitution probe (see substitution-probe.ts): scans the task's
|
|
127
135
|
* changed test files for test-the-copy shapes and returns finding lines to inject
|
package/dist/task/verify-work.js
CHANGED
|
@@ -597,7 +597,16 @@ export async function runWorkVerification(deps) {
|
|
|
597
597
|
// 5/5 false-PASS live) — because it does not depend on that block. A fail is the
|
|
598
598
|
// ordinary verify-FAIL outcome, so it flows into the existing resolution picker.
|
|
599
599
|
// Absent dep, or a no-op result (no tooling to run), falls through to the model.
|
|
600
|
+
const stage = (label) => {
|
|
601
|
+
try {
|
|
602
|
+
deps.onStage?.(label);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// progress reporting must never break the gate
|
|
606
|
+
}
|
|
607
|
+
};
|
|
600
608
|
if (deps.repoHealth) {
|
|
609
|
+
stage('repo health');
|
|
601
610
|
const h = await deps.repoHealth();
|
|
602
611
|
if (!h.ok)
|
|
603
612
|
return { ok: false, reason: `repo health: ${h.reason}` };
|
|
@@ -609,6 +618,7 @@ export async function runWorkVerification(deps) {
|
|
|
609
618
|
// verification (it is an optional sharpener, the gate still runs without it).
|
|
610
619
|
let findings = [];
|
|
611
620
|
if (deps.probe) {
|
|
621
|
+
stage('substitution probe');
|
|
612
622
|
try {
|
|
613
623
|
findings = await deps.probe();
|
|
614
624
|
}
|
|
@@ -618,6 +628,7 @@ export async function runWorkVerification(deps) {
|
|
|
618
628
|
}
|
|
619
629
|
let prohibitions = [];
|
|
620
630
|
if (deps.prohibitionProbe) {
|
|
631
|
+
stage('prohibition probe');
|
|
621
632
|
try {
|
|
622
633
|
prohibitions = await deps.prohibitionProbe();
|
|
623
634
|
}
|
|
@@ -629,6 +640,7 @@ export async function runWorkVerification(deps) {
|
|
|
629
640
|
// block verification — it is an optional sharpener like the substitution probe.
|
|
630
641
|
let testAssembly = [];
|
|
631
642
|
if (deps.testAssemblyProbe) {
|
|
643
|
+
stage('test-assembly probe');
|
|
632
644
|
try {
|
|
633
645
|
testAssembly = await deps.testAssemblyProbe();
|
|
634
646
|
}
|
|
@@ -640,6 +652,7 @@ export async function runWorkVerification(deps) {
|
|
|
640
652
|
// block verification — an optional sharpener like the other diff-shape probes.
|
|
641
653
|
let probeGaming = [];
|
|
642
654
|
if (deps.probeGamingProbe) {
|
|
655
|
+
stage('probe-gaming probe');
|
|
643
656
|
try {
|
|
644
657
|
probeGaming = await deps.probeGamingProbe();
|
|
645
658
|
}
|
|
@@ -652,6 +665,7 @@ export async function runWorkVerification(deps) {
|
|
|
652
665
|
// failure must never block verification.
|
|
653
666
|
let crossDeletions = [];
|
|
654
667
|
if (deps.crossTaskDeletionProbe) {
|
|
668
|
+
stage('cross-task deletion probe');
|
|
655
669
|
try {
|
|
656
670
|
crossDeletions = await deps.crossTaskDeletionProbe();
|
|
657
671
|
}
|
|
@@ -663,6 +677,7 @@ export async function runWorkVerification(deps) {
|
|
|
663
677
|
// under rule 4e. A probe failure must never block verification.
|
|
664
678
|
let foreignPaths = [];
|
|
665
679
|
if (deps.foreignPathProbe) {
|
|
680
|
+
stage('foreign-path probe');
|
|
666
681
|
try {
|
|
667
682
|
foreignPaths = await deps.foreignPathProbe();
|
|
668
683
|
}
|
|
@@ -674,6 +689,7 @@ export async function runWorkVerification(deps) {
|
|
|
674
689
|
// 4f. A probe failure must never block verification.
|
|
675
690
|
let scriptEscapes = [];
|
|
676
691
|
if (deps.scriptEscapeProbe) {
|
|
692
|
+
stage('script-escape probe');
|
|
677
693
|
try {
|
|
678
694
|
scriptEscapes = await deps.scriptEscapeProbe();
|
|
679
695
|
}
|
|
@@ -685,6 +701,7 @@ export async function runWorkVerification(deps) {
|
|
|
685
701
|
// never block verification.
|
|
686
702
|
let runnerGlobs = [];
|
|
687
703
|
if (deps.runnerGlobProbe) {
|
|
704
|
+
stage('runner-glob probe');
|
|
688
705
|
try {
|
|
689
706
|
runnerGlobs = await deps.runnerGlobProbe();
|
|
690
707
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|