@mjasnikovs/pi-task 0.38.15 → 0.38.17
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/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
|
@@ -9,11 +9,11 @@ import { existsSync } from 'node:fs';
|
|
|
9
9
|
import * as fsp from 'node:fs/promises';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { gateRunTask, markResumable } from './orchestrator.js';
|
|
12
|
-
import {
|
|
12
|
+
import { RUN_END_POLICY, runSucceeded } from './run-end.js';
|
|
13
|
+
import { parseAutoAnswer, autoAnswerHasTag, deriveTitle } from './parsers.js';
|
|
13
14
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
14
15
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
15
16
|
import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
16
|
-
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
17
17
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, insertTaskAfter, findResumableAutoDetailed } from './auto-io.js';
|
|
18
18
|
import { decideResume, UNATTENDED_STATES } from './resume-gap.js';
|
|
19
19
|
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
@@ -32,16 +32,21 @@ import { runGatesForTask } from './task-gates.js';
|
|
|
32
32
|
import { runFinalGateStage } from './run-final-gate.js';
|
|
33
33
|
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
34
34
|
import { runFinalIntegrationGate, deriveOpenDebts } from './final-gate.js';
|
|
35
|
+
import { spawnCommand } from './command-run.js';
|
|
35
36
|
import { getConfig } from '../config/config.js';
|
|
36
37
|
import { debugLogLevel, shouldLogDebug } from './debug-log.js';
|
|
37
|
-
import { isYoloMode, yoloPickAnswer
|
|
38
|
+
import { isYoloMode, yoloPickAnswer } from './yolo.js';
|
|
39
|
+
import { QaTranscript, CLARIFY_QA_POLICY } from './qa-transcript.js';
|
|
40
|
+
import { makeQuestionSource } from './question-source.js';
|
|
41
|
+
import { CoverageLedger } from './plan-rounds.js';
|
|
42
|
+
import { CLARIFY_QUALITY_RULES, PLAN_FORMAT_HINT } from './plan-session.js';
|
|
38
43
|
import { configureResearchRun, resumeResearchRun } from '../workers/research-cache.js';
|
|
39
44
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
40
45
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
41
46
|
import { granularityFloor, granularitySplitHint, isPlanShapeQuestion, isTooCoarse, planShapeIsHostsToAnswer, PLAN_SHAPE_ANSWER } from './decompose-granularity.js';
|
|
42
47
|
import { mandatesTestsInSameChange, rewriteBatchTestPlan } from './batch-test-task.js';
|
|
43
48
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, writeOwnedRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
44
|
-
import {
|
|
49
|
+
import { groundedCoverage } from './coverage-loop.js';
|
|
45
50
|
import { buildOptionCards, resolveAnswer } from './question-dialog.js';
|
|
46
51
|
import { TERMINAL_OUTCOMES, formatAt, formatWhy } from './terminal-outcome.js';
|
|
47
52
|
import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, danglingCarryText } from './artifact-closure.js';
|
|
@@ -153,17 +158,6 @@ function logPlanDebug(cwd, msg) {
|
|
|
153
158
|
.then(() => fsp.appendFile(path.join(dir, 'plan-debug.log'), line))
|
|
154
159
|
.catch(() => { });
|
|
155
160
|
}
|
|
156
|
-
/** Normalise a missing-area string for cross-round identity — lowercased alnum
|
|
157
|
-
* words, punctuation and quote-wrapping collapsed. Used only to tell whether an
|
|
158
|
-
* adopted plan introduced a NEW gap versus re-surfacing the same one (#2 bonus
|
|
159
|
-
* round); intentionally coarse, so trivial rewording of the same area does not
|
|
160
|
-
* read as new and buy an extra round. */
|
|
161
|
-
function normMissingArea(s) {
|
|
162
|
-
return s
|
|
163
|
-
.toLowerCase()
|
|
164
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
165
|
-
.trim();
|
|
166
|
-
}
|
|
167
161
|
/**
|
|
168
162
|
* Clarify's answer-side TRIAGE — the second stage /task-auto's clarify gate was
|
|
169
163
|
* missing that /task's grill already had. /task-auto's clarify was single-stage:
|
|
@@ -507,40 +501,43 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
507
501
|
const { featureForModel, existingFilesBlock, ownableRequirements } = oriented;
|
|
508
502
|
const theme = ctx.ui.theme;
|
|
509
503
|
const ui = new SessionUI(ctx);
|
|
510
|
-
|
|
511
|
-
//
|
|
512
|
-
|
|
504
|
+
// ONE record, one numbering, one provenance table (task/qa-transcript.ts).
|
|
505
|
+
// Clarify's generator DOES see provenance — a question the triage already
|
|
506
|
+
// settled must read as settled so it is not re-asked — which is the one thing
|
|
507
|
+
// it and grill genuinely disagree about, and is now a named policy field.
|
|
508
|
+
const transcript = new QaTranscript(CLARIFY_QA_POLICY);
|
|
513
509
|
// Deterministic guard against a model that ignores "never re-ask": consecutive
|
|
514
510
|
// near-duplicate questions are reprompted with an explicit hint, and once it
|
|
515
511
|
// strikes out (can't produce anything novel) we stop instead of barraging the
|
|
516
512
|
// user with the same decision worded N ways. Also caps the absolute count.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
513
|
+
// The generate → parse → pick → dedupe → re-prompt state machine is
|
|
514
|
+
// task/question-source.ts, shared with the plan session. Clarify used to write
|
|
515
|
+
// its own, and it had drifted from the sibling in five ways — every one of
|
|
516
|
+
// which the shared source closes for free: `parsed[0]` became `pickQuestion`
|
|
517
|
+
// (an analysis note is no longer shown as the question, and the SUGGESTED
|
|
518
|
+
// attached further down is no longer lost), and an unparseable reply now buys
|
|
519
|
+
// one format re-prompt instead of ending the whole clarify — and decomposing
|
|
520
|
+
// the feature with ZERO clarifications — on a formatting slip.
|
|
521
|
+
//
|
|
522
|
+
// Of plan's three quality rules only the DEFERRAL guard crosses. The other two
|
|
523
|
+
// cost an extra child call every time they fire, and clarify is the most A/B'd
|
|
524
|
+
// path here; moving them is its own experiment. See CLARIFY_QUALITY_RULES.
|
|
525
|
+
const source = makeQuestionSource({
|
|
526
|
+
generate: hint => deps.runChild('auto-clarify', 'read', prependHint(hint, AUTO_CLARIFY_PROMPT(featureForModel, transcript.forGenerator()))),
|
|
527
|
+
formatHint: PLAN_FORMAT_HINT,
|
|
528
|
+
rules: CLARIFY_QUALITY_RULES,
|
|
529
|
+
cap: MAX_CLARIFY_QUESTIONS,
|
|
530
|
+
log: msg => logPlanDebug(cwd, `clarify: ${msg}`)
|
|
531
|
+
});
|
|
532
|
+
for (;;) {
|
|
533
|
+
const drawn = await source.next();
|
|
534
|
+
if (drawn.kind === 'exhausted')
|
|
535
|
+
break;
|
|
536
|
+
const { question, suggested, alt } = drawn.q;
|
|
539
537
|
// Render markdown (bold/code) for the displayed prompt; keep plain text
|
|
540
538
|
// for the editable default and the persisted file.
|
|
541
539
|
const shownQ = renderInlineMarkdown(question, theme);
|
|
542
|
-
const plainQ =
|
|
543
|
-
askedQuestions.push(plainQ);
|
|
540
|
+
const plainQ = drawn.plain;
|
|
544
541
|
// PLAN SHAPE is the host's call, not the triage's (mx5 41→11 tasks on the
|
|
545
542
|
// same spec, same base commit, same code — see decompose-granularity.ts).
|
|
546
543
|
// The triage answers this fork for itself in 8/8 live reps and stamps it
|
|
@@ -550,8 +547,7 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
550
547
|
// fixed value the user can read in the AUTO file and override next run.
|
|
551
548
|
if (planShapeIsHostsToAnswer(ownableRequirements) && isPlanShapeQuestion(plainQ)) {
|
|
552
549
|
logPlanDebug(cwd, `plan-shape question answered host-side (not the triage): ${plainQ.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
553
|
-
|
|
554
|
-
+ `A${answers.length + 1}: ${PLAN_SHAPE_ANSWER} (host-set — plan granularity is not left to chance)`);
|
|
550
|
+
transcript.add('host-set', plainQ, PLAN_SHAPE_ANSWER);
|
|
555
551
|
continue;
|
|
556
552
|
}
|
|
557
553
|
// Answer-side triage (grill parity): if the inlined spec already settles
|
|
@@ -560,8 +556,7 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
560
556
|
// won't re-ask it.
|
|
561
557
|
const autoResolved = await triageClarifyQuestion(deps, cwd, featureForModel, existingFilesBlock, plainQ);
|
|
562
558
|
if (autoResolved !== null) {
|
|
563
|
-
|
|
564
|
-
+ `A${answers.length + 1}: ${autoResolved} (auto-resolved — already settled by the spec)`);
|
|
559
|
+
transcript.add('auto-resolved', plainQ, autoResolved);
|
|
565
560
|
continue;
|
|
566
561
|
}
|
|
567
562
|
const plainSuggested = suggested === undefined ? undefined : stripInlineMarkdown(suggested);
|
|
@@ -575,9 +570,10 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
575
570
|
...(plainAlt !== undefined && { alt: plainAlt })
|
|
576
571
|
});
|
|
577
572
|
if (yolo !== null) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
573
|
+
if (yolo.kind === 'answer')
|
|
574
|
+
transcript.add('yolo', plainQ, yolo.answer);
|
|
575
|
+
else
|
|
576
|
+
transcript.add('yolo-skip', plainQ, `(skipped — ${yolo.note})`);
|
|
581
577
|
continue;
|
|
582
578
|
}
|
|
583
579
|
// The picker cards and the reply mapping are shared with /task's grill
|
|
@@ -609,21 +605,15 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
609
605
|
announceDone(ctx, '/task-auto cancelled.', 'warning');
|
|
610
606
|
return null;
|
|
611
607
|
}
|
|
612
|
-
const resolved = resolveAnswer(pending, a);
|
|
613
|
-
// Clarify's transcript records PROVENANCE; grill's deliberately does not,
|
|
614
|
-
// because grill's is fed back verbatim into the next grill-gen prompt. That
|
|
615
|
-
// is now the only difference between the two dialogs, and it is one line.
|
|
616
608
|
// An accept covers both routes to it: submitting empty, and pressing the
|
|
617
|
-
// single green card.
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
: resolved.answer;
|
|
621
|
-
answers.push(`Q${answers.length + 1}: ${plainQ}\nA${answers.length + 1}: ${answer}`);
|
|
609
|
+
// single green card. The suffix is the policy's, not this call site's.
|
|
610
|
+
const resolved = resolveAnswer(pending, a);
|
|
611
|
+
transcript.add(resolved.source === 'accepted' ? 'accepted' : 'typed', plainQ, resolved.answer);
|
|
622
612
|
}
|
|
623
|
-
if (
|
|
613
|
+
if (transcript.length === 0) {
|
|
624
614
|
ctx.ui.notify('No clarifying questions needed — planning tasks…', 'info');
|
|
625
615
|
}
|
|
626
|
-
return
|
|
616
|
+
return transcript.forRecord();
|
|
627
617
|
}
|
|
628
618
|
/**
|
|
629
619
|
* DECOMPOSE — turn the settled feature into a task list, then defend that list's
|
|
@@ -822,20 +812,16 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
822
812
|
// It is also the ONLY handle on the accounting. There used to be a second,
|
|
823
813
|
// `let accounting`, carried alongside — see the ScoredPlan doc comment for the
|
|
824
814
|
// requirement-to-wrong-task bug that cost us.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// exactly one bonus round when — and only when — an adoption introduces a NEW
|
|
834
|
-
// missing area at the cap. Bounded to one so a judge that flags forever still
|
|
835
|
-
// cannot loop the plan phase; a persistent (non-new) gap never re-triggers it.
|
|
836
|
-
let roundCap = MAX_COVERAGE_ROUNDS;
|
|
837
|
-
let bonusRoundUsed = false;
|
|
815
|
+
// The record, and the decisions it makes: task/plan-rounds.ts. This was five
|
|
816
|
+
// locals threaded by closure through a ~90-line loop, plus a
|
|
817
|
+
// snapshot-before-overwrite pair that existed only because the bonus-round
|
|
818
|
+
// decision was made downstream from the evidence it needed.
|
|
819
|
+
const rounds = new CoverageLedger(await scorePlan(planTitles), {
|
|
820
|
+
cap: MAX_COVERAGE_ROUNDS,
|
|
821
|
+
hasRequirements
|
|
822
|
+
});
|
|
838
823
|
for (;;) {
|
|
824
|
+
const best = rounds.best();
|
|
839
825
|
if (best.plan.titles.length === 0)
|
|
840
826
|
break;
|
|
841
827
|
if (best.plan.missing.length === 0) {
|
|
@@ -852,40 +838,19 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
852
838
|
}
|
|
853
839
|
break;
|
|
854
840
|
}
|
|
855
|
-
if (
|
|
841
|
+
if (!rounds.mayRetry())
|
|
856
842
|
break;
|
|
857
|
-
round
|
|
843
|
+
const round = rounds.startRound();
|
|
858
844
|
logPlanDebug(cwd, `decompose-coverage round ${round}: INCOMPLETE — missing: `
|
|
859
845
|
+ best.plan.missing.join('; ').slice(0, 300));
|
|
860
846
|
const retryTitles = parsePlan(await deps.runChild('auto-decompose', 'read', prependHint(coverageRepromptHint(best.plan.missing), decomposePrompt)));
|
|
861
847
|
logPlanDebug(cwd, `decompose retry produced ${retryTitles.length} title(s)`);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// judge that just relabels the same-shaped plan's gap does not qualify),
|
|
869
|
-
// and it must expose a NEW area (a gap already present is one we have or
|
|
870
|
-
// will reprompt against anyway). Requirements-path only: without grounded
|
|
871
|
-
// requirements "missing" is pure holistic-judge free-text that can change
|
|
872
|
-
// every round, so there is no trustworthy "grew"/"new" signal to gate on.
|
|
873
|
-
const priorCovered = best.plan.covered.size;
|
|
874
|
-
const priorMissing = new Set(best.plan.missing.map(normMissingArea));
|
|
875
|
-
// The WHOLE scored plan is adopted, titles and accounting together.
|
|
876
|
-
// This used to be two assignments, and the second one kept the OLD
|
|
877
|
-
// plan's accounting whenever the new plan's coverage-map child faulted
|
|
878
|
-
// (`cand.accounting ?? accounting`) — binding requirements to titles
|
|
879
|
-
// they were never mapped against. See the ScoredPlan doc comment.
|
|
880
|
-
best = cand;
|
|
881
|
-
logPlanDebug(cwd, `decompose retry ADOPTED — ${decision.reason}`);
|
|
882
|
-
if (!bonusRoundUsed
|
|
883
|
-
&& round >= roundCap
|
|
884
|
-
&& hasRequirements
|
|
885
|
-
&& cand.plan.covered.size > priorCovered
|
|
886
|
-
&& cand.plan.missing.some(m => !priorMissing.has(normMissingArea(m)))) {
|
|
887
|
-
bonusRoundUsed = true;
|
|
888
|
-
roundCap++;
|
|
848
|
+
// Compare, replace the plan WHOLE, and decide the bonus round — one call,
|
|
849
|
+
// so there is no window in which a snapshot and a replacement disagree.
|
|
850
|
+
const outcome = rounds.consider(await scorePlan(retryTitles));
|
|
851
|
+
if (outcome.adopted) {
|
|
852
|
+
logPlanDebug(cwd, `decompose retry ADOPTED — ${outcome.decision.reason}`);
|
|
853
|
+
if (outcome.grantedBonusRound) {
|
|
889
854
|
logPlanDebug(cwd, 'decompose-coverage: bonus round granted — adoption grew coverage and '
|
|
890
855
|
+ 'exposed a new uncovered area at the cap');
|
|
891
856
|
}
|
|
@@ -894,20 +859,22 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
894
859
|
// Rejected: keep the better current plan. The loop re-checks it at the
|
|
895
860
|
// top (still incomplete ⇒ another bounded reprompt) but its coverage is
|
|
896
861
|
// never sacrificed to a worse regeneration.
|
|
897
|
-
logPlanDebug(cwd, `decompose retry REJECTED — ${decision.reason}`
|
|
898
|
-
+ (decision.dropped.length > 0 ?
|
|
899
|
-
` [would drop: ${decision.dropped
|
|
862
|
+
logPlanDebug(cwd, `decompose retry REJECTED — ${outcome.decision.reason}`
|
|
863
|
+
+ (outcome.decision.dropped.length > 0 ?
|
|
864
|
+
` [would drop: ${outcome.decision.dropped
|
|
900
865
|
.map(i => `"${reqEntries[i].quote}"`)
|
|
901
866
|
.join('; ')
|
|
902
867
|
.slice(0, 200)}]`
|
|
903
868
|
: ''));
|
|
904
869
|
}
|
|
905
870
|
}
|
|
871
|
+
const best = rounds.best();
|
|
872
|
+
const round = rounds.round();
|
|
906
873
|
planTitles = best.plan.titles;
|
|
907
874
|
// Exhausted still INCOMPLETE: the best plan ships (the gate is best-effort), but
|
|
908
875
|
// silently shipping a KNOWN-gapped plan is how mx5 run 5 lost its whole test
|
|
909
876
|
// suite — tell the user what is still uncovered.
|
|
910
|
-
const unresolvedMissing =
|
|
877
|
+
const unresolvedMissing = rounds.unresolved();
|
|
911
878
|
if (unresolvedMissing !== null) {
|
|
912
879
|
logPlanDebug(cwd, `decompose-coverage exhausted ${round} round(s) still INCOMPLETE — missing: `
|
|
913
880
|
+ unresolvedMissing.join('; ').slice(0, 300));
|
|
@@ -1171,7 +1138,7 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
1171
1138
|
// The final integration gate follows the `verify work` switch: it is the
|
|
1172
1139
|
// run-level half of the same verification story.
|
|
1173
1140
|
finalGate: (cwd2, planText) => getConfig().verifyWork ?
|
|
1174
|
-
runFinalIntegrationGate(cwd2, { planText })
|
|
1141
|
+
runFinalIntegrationGate(cwd2, { planText, signal })
|
|
1175
1142
|
: Promise.resolve({ ok: true, reason: 'disabled' }),
|
|
1176
1143
|
// Uncommitted paths, for the stranded-sub-fix handling around the final-gate
|
|
1177
1144
|
// picker (mx5 run 13 PROMPT 4 item 3). Every task is committed by the time
|
|
@@ -1183,7 +1150,9 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
1183
1150
|
// Re-derive the debt ledger against the FINAL tree after a converged
|
|
1184
1151
|
// autofix (nexttask 6). Only ever reached from inside the gate's own
|
|
1185
1152
|
// resolution loop, so it needs no `verify work` switch of its own.
|
|
1186
|
-
|
|
1153
|
+
// Same section, same cancel: this re-runs every ACCEPT-debt VERIFY command
|
|
1154
|
+
// against the final tree, each under its own 300s cap.
|
|
1155
|
+
recheckOpenDebts: (cwd2, staticOk) => deriveOpenDebts(cwd2, staticOk, spawnCommand, signal)
|
|
1187
1156
|
};
|
|
1188
1157
|
}
|
|
1189
1158
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
@@ -1191,6 +1160,29 @@ let autoRunning = false;
|
|
|
1191
1160
|
export function requestAutoCancel() {
|
|
1192
1161
|
requestCancel();
|
|
1193
1162
|
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Report a stash pushed during one task and left behind.
|
|
1165
|
+
*
|
|
1166
|
+
* An orphan stash later pops as an unresolvable conflict (mx5 run 6), so the
|
|
1167
|
+
* capture before the task and this check after it are ONE fact. They were a local
|
|
1168
|
+
* and a check ~120 lines and three returns apart, which is how the check came to
|
|
1169
|
+
* run only on the success path. Best-effort: never throws, so it cannot mask the
|
|
1170
|
+
* outcome of the attempt it closes.
|
|
1171
|
+
*/
|
|
1172
|
+
async function reportStashDrift(active, deps, cwd, id, title, before) {
|
|
1173
|
+
if (!deps.stashRef || before === undefined)
|
|
1174
|
+
return;
|
|
1175
|
+
try {
|
|
1176
|
+
const after = await deps.stashRef(cwd);
|
|
1177
|
+
if (after === before)
|
|
1178
|
+
return;
|
|
1179
|
+
active.ui.notify(`${id}: the git stash stack changed during "${title}" and was left that way — `
|
|
1180
|
+
+ 'inspect `git stash list`; an orphan stash later pops as an unresolvable conflict.', 'warning');
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
// a git failure here is inconclusive, never a report
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1194
1186
|
/**
|
|
1195
1187
|
* Announce a terminal /task-auto-overall outcome both in the terminal and to
|
|
1196
1188
|
* subscribed devices. The push body reuses the exact terminal message, so a
|
|
@@ -1298,119 +1290,120 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1298
1290
|
// during the task (impl model or any child) and left behind is called
|
|
1299
1291
|
// out instead of silently waiting to detonate in a later task.
|
|
1300
1292
|
const stashBefore = deps.stashRef ? await deps.stashRef(cwd) : undefined;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
// GATE: actually RUN the task's verification against the just-finished
|
|
1359
|
-
// work, then hold the committed result to AGENTS.md / CLAUDE.md. Shared
|
|
1360
|
-
// verbatim with /task so both commands gate identically — see
|
|
1361
|
-
// runGatesForTask. `done` means the work verified (or was accepted),
|
|
1362
|
-
// was checked off + committed, and enforcement ran; every other kind is
|
|
1363
|
-
// a terminal stop this loop announces with its own /task-auto-resume
|
|
1364
|
-
// wording (the shared gate is command-agnostic).
|
|
1365
|
-
const gate = await runGatesForTask(active, deps, {
|
|
1366
|
-
cwd,
|
|
1367
|
-
taskId: res.taskId,
|
|
1368
|
-
title: next.title,
|
|
1369
|
-
tag: id,
|
|
1370
|
-
// Fence an AUTOFIX re-run against re-expanding the whole spec.
|
|
1371
|
-
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1372
|
-
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
1373
|
-
// allocated TASK_NNNN id (never empty here). The parent task-list
|
|
1374
|
-
// check-off runs after verify passes/accepts and before the commit,
|
|
1375
|
-
// so the commit captures the checked box too.
|
|
1376
|
-
onVerified: () => checkOffTask(cwd, id, next.index, res.taskId, next.title)
|
|
1377
|
-
});
|
|
1378
|
-
active = gate.ctx;
|
|
1379
|
-
// Every terminal gate outcome — what to demote, what to fail, what to
|
|
1380
|
-
// say — comes from TERMINAL_OUTCOMES, shared verbatim with /task's
|
|
1381
|
-
// loop. `done` alone is not terminal here: it falls through to the
|
|
1382
|
-
// next task.
|
|
1383
|
-
if (gate.kind !== 'done') {
|
|
1384
|
-
const outcome = TERMINAL_OUTCOMES[gate.kind];
|
|
1385
|
-
if (outcome.markResumable)
|
|
1386
|
-
await markResumable(cwd, res.taskId);
|
|
1387
|
-
if (outcome.failParent) {
|
|
1388
|
-
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
1293
|
+
try {
|
|
1294
|
+
// SAFE CHECKPOINT (pre-task): the tree is committed and no inner task
|
|
1295
|
+
// is stamped yet, so stopping here just leaves this entry unchecked —
|
|
1296
|
+
// a resume restarts it from scratch. Cheapest possible stop, and the
|
|
1297
|
+
// last one before we commit to a ~30-minute task.
|
|
1298
|
+
if (cancelCheckpoint('pre-task')) {
|
|
1299
|
+
announceDone(active, `${id} cancelled before "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const res = await deps.runTask(active, cwd, next.title, {
|
|
1303
|
+
resumeId,
|
|
1304
|
+
// Fence this step against re-expanding the whole referenced spec:
|
|
1305
|
+
// name the sibling steps so refine bounds this step's slice. Only
|
|
1306
|
+
// matters when refine runs fresh (a resumed task past refine ignores
|
|
1307
|
+
// it), but always supplied so a resume that restarts at refine is
|
|
1308
|
+
// fenced too.
|
|
1309
|
+
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1310
|
+
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
1311
|
+
});
|
|
1312
|
+
active = res.ctx ?? active;
|
|
1313
|
+
// One dispatch over the named ending. The five-branch ladder this
|
|
1314
|
+
// replaces had to ask `isCancelRequested()` — a module global
|
|
1315
|
+
// `/task-cancel` never sets — to tell a user stop from a fault, so a
|
|
1316
|
+
// cancel during a task was announced in red as "stopped … fix and
|
|
1317
|
+
// resume" and the inner file's `cancelled` was overwritten with
|
|
1318
|
+
// `failed`. The runner names the ending now; resumability is
|
|
1319
|
+
// RUN_END_POLICY's; only the wording is this command's.
|
|
1320
|
+
if (!runSucceeded(res.end)) {
|
|
1321
|
+
const policy = RUN_END_POLICY[res.end.kind];
|
|
1322
|
+
// Demote the INNER task file: it reads `completed` from
|
|
1323
|
+
// spec-handoff, and leaving it that way is how a failed run's task
|
|
1324
|
+
// file claimed success in the run 6 audit.
|
|
1325
|
+
if (policy.resumable)
|
|
1326
|
+
await markResumable(cwd, res.taskId);
|
|
1327
|
+
// The PLAN fails only on a fault. A declined-steer interrupt leaves
|
|
1328
|
+
// it in progress so /task-auto-resume re-delivers this task's spec.
|
|
1329
|
+
if (policy.failsRun)
|
|
1330
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
1331
|
+
const why = res.end.kind === 'failed' && res.end.reason ?
|
|
1332
|
+
` — ${res.end.reason.slice(0, 160)}`
|
|
1333
|
+
: '';
|
|
1334
|
+
const msg = res.end.kind === 'no-session' ?
|
|
1335
|
+
`${id} paused — could not start a session. Run /task-auto-resume to retry.`
|
|
1336
|
+
: res.end.kind === 'cancelled' ?
|
|
1337
|
+
`${id} cancelled during "${next.title}" — resume with /task-auto-resume.`
|
|
1338
|
+
: res.end.kind === 'interrupted' ?
|
|
1339
|
+
// The user interrupted implementation (ESC) and then declined
|
|
1340
|
+
// to steer — they want to stop here. Paused without checking
|
|
1341
|
+
// the task off, so /task-auto-resume re-delivers this task's
|
|
1342
|
+
// spec. (A plain ESC followed by steering text never reaches
|
|
1343
|
+
// here — that loops inside runSingleTask until a turn ends.)
|
|
1344
|
+
`${id} paused at "${next.title}" — resume with /task-auto-resume.`
|
|
1345
|
+
: `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`;
|
|
1346
|
+
announceDone(active, msg, policy.level);
|
|
1347
|
+
return;
|
|
1389
1348
|
}
|
|
1390
|
-
|
|
1349
|
+
// GATE: actually RUN the task's verification against the just-finished
|
|
1350
|
+
// work, then hold the committed result to AGENTS.md / CLAUDE.md. Shared
|
|
1351
|
+
// verbatim with /task so both commands gate identically — see
|
|
1352
|
+
// runGatesForTask. `done` means the work verified (or was accepted),
|
|
1353
|
+
// was checked off + committed, and enforcement ran; every other kind is
|
|
1354
|
+
// a terminal stop this loop announces with its own /task-auto-resume
|
|
1355
|
+
// wording (the shared gate is command-agnostic).
|
|
1356
|
+
const gate = await runGatesForTask(active, deps, {
|
|
1357
|
+
cwd,
|
|
1358
|
+
taskId: res.taskId,
|
|
1359
|
+
title: next.title,
|
|
1391
1360
|
tag: id,
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1361
|
+
// Fence an AUTOFIX re-run against re-expanding the whole spec.
|
|
1362
|
+
planContext: buildStepFence(entries.map(e => e.title), next.index),
|
|
1363
|
+
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
1364
|
+
// allocated TASK_NNNN id (never empty here). The parent task-list
|
|
1365
|
+
// check-off runs after verify passes/accepts and before the commit,
|
|
1366
|
+
// so the commit captures the checked box too.
|
|
1367
|
+
onVerified: () => checkOffTask(cwd, id, next.index, res.taskId, next.title)
|
|
1368
|
+
});
|
|
1369
|
+
active = gate.ctx;
|
|
1370
|
+
// Every terminal gate outcome — what to demote, what to fail, what to
|
|
1371
|
+
// say — comes from TERMINAL_OUTCOMES, shared verbatim with /task's
|
|
1372
|
+
// loop. `done` alone is not terminal here: it falls through to the
|
|
1373
|
+
// next task.
|
|
1374
|
+
if (gate.kind !== 'done') {
|
|
1375
|
+
const outcome = TERMINAL_OUTCOMES[gate.kind];
|
|
1376
|
+
if (outcome.markResumable)
|
|
1377
|
+
await markResumable(cwd, res.taskId);
|
|
1378
|
+
if (outcome.failParent) {
|
|
1379
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
1380
|
+
}
|
|
1381
|
+
announceDone(active, outcome.message({
|
|
1382
|
+
tag: id,
|
|
1383
|
+
at: formatAt(next.title),
|
|
1384
|
+
why: formatWhy(gate.kind === 'failed' ? gate.reason : undefined),
|
|
1385
|
+
resumeCmd: '/task-auto-resume'
|
|
1386
|
+
}), outcome.level);
|
|
1387
|
+
return;
|
|
1413
1388
|
}
|
|
1389
|
+
// ROOT-CAUSE REPAIR (mx5 run 14 item 5): the gate may have attributed a
|
|
1390
|
+
// FAIL to a pre-existing defect in a file some OTHER task created. It can
|
|
1391
|
+
// only QUEUE that finding — mutating the plan is this loop's job. Drain
|
|
1392
|
+
// the queue and splice a scoped repair step in right after the step that
|
|
1393
|
+
// just finished, so the defect is fixed BEFORE the next dependent task
|
|
1394
|
+
// trips over it too (run 14 recorded the same `test/teardown.ts` cause
|
|
1395
|
+
// twice, scheduled nothing, and the bug outlived ~24h of the run).
|
|
1396
|
+
await schedulePendingRepairs(cwd, id, next.index, active, deps);
|
|
1397
|
+
}
|
|
1398
|
+
finally {
|
|
1399
|
+
// EVERY exit from this attempt passes here — the two mid-attempt
|
|
1400
|
+
// returns and a throw included. The check used to sit at the very
|
|
1401
|
+
// end of the fall-through, ~120 lines and three returns below the
|
|
1402
|
+
// capture, so it ran only when the task SUCCEEDED and the gate said
|
|
1403
|
+
// `done`. On a failed or interrupted task the user is told to run
|
|
1404
|
+
// /task-auto-resume, straight onto the landmine the guard exists to
|
|
1405
|
+
// name. The pairing is structural now, not positional.
|
|
1406
|
+
await reportStashDrift(active, deps, cwd, id, next.title, stashBefore);
|
|
1414
1407
|
}
|
|
1415
1408
|
}
|
|
1416
1409
|
}
|