@mjasnikovs/pi-task 0.18.19 → 0.18.20
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.
|
@@ -42,7 +42,19 @@ import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, kee
|
|
|
42
42
|
const MAX_CLARIFY_QUESTIONS = 8;
|
|
43
43
|
// Bounded coverage-triage rounds after decompose: judge → reprompt-with-missing
|
|
44
44
|
// → judge again, at most. Two rounds so one flaky retry doesn't end the gate,
|
|
45
|
-
// while a judge that keeps flagging can't loop the plan phase forever.
|
|
45
|
+
// while a judge that keeps flagging can't loop the plan phase forever. Each round
|
|
46
|
+
// spawns three model children (decompose + coverage-map + coverage-verdict), so
|
|
47
|
+
// the ceiling is also a latency/spawn budget, not just a correctness bound.
|
|
48
|
+
//
|
|
49
|
+
// Why 2 is safe to sit this low: when this number was picked (2026-07-03) adoption
|
|
50
|
+
// was LAST-WINS, so more rounds meant more chances to overwrite a good plan with a
|
|
51
|
+
// worse regeneration — the cap was protective. Adoption is now MONOTONE
|
|
52
|
+
// (coverage-loop.ts, 2026-07-15): a retry that drops owned coverage is rejected,
|
|
53
|
+
// never adopted, so extra rounds can only hold or grow coverage. The one gap that
|
|
54
|
+
// remained is that an adoption landing ON the last round can expose a NEW area with
|
|
55
|
+
// no round left to chase it — handled surgically by a single bonus round granted
|
|
56
|
+
// only in that exact case (see the loop), rather than by raising this ceiling for
|
|
57
|
+
// every run.
|
|
46
58
|
const MAX_COVERAGE_ROUNDS = 2;
|
|
47
59
|
/** Reprompt prefix when the coverage triage found feature areas no task covers. */
|
|
48
60
|
function coverageRepromptHint(missing) {
|
|
@@ -106,6 +118,17 @@ function logPlanDebug(cwd, msg) {
|
|
|
106
118
|
.then(() => fsp.appendFile(path.join(dir, 'plan-debug.log'), line))
|
|
107
119
|
.catch(() => { });
|
|
108
120
|
}
|
|
121
|
+
/** Normalise a missing-area string for cross-round identity — lowercased alnum
|
|
122
|
+
* words, punctuation and quote-wrapping collapsed. Used only to tell whether an
|
|
123
|
+
* adopted plan introduced a NEW gap versus re-surfacing the same one (#2 bonus
|
|
124
|
+
* round); intentionally coarse, so trivial rewording of the same area does not
|
|
125
|
+
* read as new and buy an extra round. */
|
|
126
|
+
function normMissingArea(s) {
|
|
127
|
+
return s
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
130
|
+
.trim();
|
|
131
|
+
}
|
|
109
132
|
/**
|
|
110
133
|
* Clarify's answer-side TRIAGE — the second stage /task-auto's clarify gate was
|
|
111
134
|
* missing that /task's grill already had. /task-auto's clarify was single-stage:
|
|
@@ -569,7 +592,8 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
569
592
|
return {
|
|
570
593
|
plan: { titles, covered, missing },
|
|
571
594
|
accounting: acc,
|
|
572
|
-
suspect: isSuspectPlan(titles, featureForModel)
|
|
595
|
+
suspect: isSuspectPlan(titles, featureForModel),
|
|
596
|
+
judgeMissing: verdictMissing
|
|
573
597
|
};
|
|
574
598
|
};
|
|
575
599
|
const hasRequirements = reqEntries.length > 0;
|
|
@@ -579,6 +603,17 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
579
603
|
// The carried accounting (cross-cutting + unowned) for the plan that ships.
|
|
580
604
|
let accounting = best.accounting;
|
|
581
605
|
let round = 0;
|
|
606
|
+
// #2: the round cap can be lifted ONCE. An adoption is a fresh whole-plan roll,
|
|
607
|
+
// so the plan that gets adopted can expose an uncovered area the pre-adoption
|
|
608
|
+
// plan never had — and if that adoption lands on the last allowed round, the
|
|
609
|
+
// loop breaks before the new gap ever gets a reprompt (mx5 2026-07-16: the
|
|
610
|
+
// 55-title plan was adopted on the final round AND was the first to reveal §10's
|
|
611
|
+
// test-infra gap; the cap fired the same instant, so it was never chased). Grant
|
|
612
|
+
// exactly one bonus round when — and only when — an adoption introduces a NEW
|
|
613
|
+
// missing area at the cap. Bounded to one so a judge that flags forever still
|
|
614
|
+
// cannot loop the plan phase; a persistent (non-new) gap never re-triggers it.
|
|
615
|
+
let roundCap = MAX_COVERAGE_ROUNDS;
|
|
616
|
+
let bonusRoundUsed = false;
|
|
582
617
|
for (;;) {
|
|
583
618
|
if (best.plan.titles.length === 0)
|
|
584
619
|
break;
|
|
@@ -596,7 +631,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
596
631
|
}
|
|
597
632
|
break;
|
|
598
633
|
}
|
|
599
|
-
if (round >=
|
|
634
|
+
if (round >= roundCap)
|
|
600
635
|
break;
|
|
601
636
|
round++;
|
|
602
637
|
logPlanDebug(cwd, `decompose-coverage round ${round}: INCOMPLETE — missing: `
|
|
@@ -606,9 +641,29 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
606
641
|
const cand = await scorePlan(retryTitles);
|
|
607
642
|
const decision = decideAdoption(best.plan, cand.plan, hasRequirements);
|
|
608
643
|
if (decision.adopt) {
|
|
644
|
+
// Snapshot the pre-adoption plan to decide whether this adoption earns a
|
|
645
|
+
// bonus round. Two guards keep the bonus off generic judge churn: it must
|
|
646
|
+
// be a real coverage GAIN (grounded covered-set strictly grew — a flaky
|
|
647
|
+
// judge that just relabels the same-shaped plan's gap does not qualify),
|
|
648
|
+
// and it must expose a NEW area (a gap already present is one we have or
|
|
649
|
+
// will reprompt against anyway). Requirements-path only: without grounded
|
|
650
|
+
// requirements "missing" is pure holistic-judge free-text that can change
|
|
651
|
+
// every round, so there is no trustworthy "grew"/"new" signal to gate on.
|
|
652
|
+
const priorCovered = best.plan.covered.size;
|
|
653
|
+
const priorMissing = new Set(best.plan.missing.map(normMissingArea));
|
|
609
654
|
best = cand;
|
|
610
655
|
accounting = cand.accounting ?? accounting;
|
|
611
656
|
logPlanDebug(cwd, `decompose retry ADOPTED — ${decision.reason}`);
|
|
657
|
+
if (!bonusRoundUsed
|
|
658
|
+
&& round >= roundCap
|
|
659
|
+
&& hasRequirements
|
|
660
|
+
&& cand.plan.covered.size > priorCovered
|
|
661
|
+
&& cand.plan.missing.some(m => !priorMissing.has(normMissingArea(m)))) {
|
|
662
|
+
bonusRoundUsed = true;
|
|
663
|
+
roundCap++;
|
|
664
|
+
logPlanDebug(cwd, 'decompose-coverage: bonus round granted — adoption grew coverage and '
|
|
665
|
+
+ 'exposed a new uncovered area at the cap');
|
|
666
|
+
}
|
|
612
667
|
}
|
|
613
668
|
else {
|
|
614
669
|
// Rejected: keep the better current plan. The loop re-checks it at the
|
|
@@ -631,7 +686,10 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
631
686
|
if (unresolvedMissing !== null) {
|
|
632
687
|
logPlanDebug(cwd, `decompose-coverage exhausted ${round} round(s) still INCOMPLETE — missing: `
|
|
633
688
|
+ unresolvedMissing.join('; ').slice(0, 300));
|
|
634
|
-
ctx.ui.notify(`/task-auto:
|
|
689
|
+
ctx.ui.notify(`/task-auto: no task fully owns — ${unresolvedMissing.join('; ').slice(0, 200)}. `
|
|
690
|
+
+ 'Carried into every task via .pi-tasks/requirements.md, but not as a dedicated '
|
|
691
|
+
+ 'task. To give it one, stop now and add it to the plan in .pi-tasks/; otherwise '
|
|
692
|
+
+ 'it proceeds.', 'warning');
|
|
635
693
|
}
|
|
636
694
|
// Carry what no single task owns (goal A(b)/(c)): cross-cutting requirements
|
|
637
695
|
// become `.pi-tasks/requirements.md`, injected VERBATIM into every task's
|
|
@@ -639,15 +697,25 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
639
697
|
// is authoritative" pointer recovered it in 1 of ~6 tasks; content travels,
|
|
640
698
|
// pointers don't). Requirements still unmapped after the rounds are carried
|
|
641
699
|
// too — marked — and recorded user-visibly in the plan file, never dropped.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
700
|
+
//
|
|
701
|
+
// #1: the holistic-judge missing areas are carried as a THIRD channel. They are
|
|
702
|
+
// areas requirement-extraction never captured as a tracked entry (so the
|
|
703
|
+
// grounded accounting is structurally blind to them), seen only by the judge —
|
|
704
|
+
// exactly the class that, having no carrier, was warned-about then dropped (mx5
|
|
705
|
+
// 2026-07-16, §10 test-infra). Carried independent of `accounting` so a mapping
|
|
706
|
+
// fault (accounting === null) can't strand them either.
|
|
707
|
+
const carriedCrossCutting = accounting?.crossCutting ?? [];
|
|
708
|
+
const carriedUnmapped = accounting?.unmapped ?? [];
|
|
709
|
+
const carriedJudge = best.judgeMissing;
|
|
710
|
+
if (carriedCrossCutting.length > 0 || carriedUnmapped.length > 0 || carriedJudge.length > 0) {
|
|
711
|
+
await appendCarriedRequirements(cwd, carriedCrossCutting, carriedUnmapped, carriedJudge);
|
|
712
|
+
const parts = [
|
|
713
|
+
carriedCrossCutting.length > 0 ? `${carriedCrossCutting.length} cross-cutting` : '',
|
|
714
|
+
carriedUnmapped.length > 0 ? `${carriedUnmapped.length} unowned` : '',
|
|
715
|
+
carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : ''
|
|
716
|
+
].filter(p => p.length > 0);
|
|
717
|
+
ctx.ui.notify(`/task-auto: carrying ${parts.join(', ')} requirement(s) into every task`
|
|
718
|
+
+ ' — see .pi-tasks/requirements.md.', 'info');
|
|
651
719
|
}
|
|
652
720
|
// Cross-slice contract registry (mx5 run 8, F3): now that the plan is settled,
|
|
653
721
|
// extract the interface facts MORE THAN ONE slice must agree on — endpoint paths,
|
|
@@ -32,17 +32,106 @@
|
|
|
32
32
|
// domain-agnostic (no mx5/web vocabulary).
|
|
33
33
|
const COVERAGE_STOPWORDS = new Set([
|
|
34
34
|
// function words
|
|
35
|
-
'the',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
35
|
+
'the',
|
|
36
|
+
'a',
|
|
37
|
+
'an',
|
|
38
|
+
'and',
|
|
39
|
+
'or',
|
|
40
|
+
'of',
|
|
41
|
+
'to',
|
|
42
|
+
'in',
|
|
43
|
+
'on',
|
|
44
|
+
'for',
|
|
45
|
+
'with',
|
|
46
|
+
'by',
|
|
47
|
+
'at',
|
|
48
|
+
'as',
|
|
49
|
+
'is',
|
|
50
|
+
'are',
|
|
51
|
+
'be',
|
|
52
|
+
'it',
|
|
53
|
+
'its',
|
|
54
|
+
'that',
|
|
55
|
+
'this',
|
|
56
|
+
'from',
|
|
57
|
+
'into',
|
|
58
|
+
'out',
|
|
59
|
+
'up',
|
|
60
|
+
'per',
|
|
61
|
+
'via',
|
|
62
|
+
'not',
|
|
63
|
+
'no',
|
|
64
|
+
'but',
|
|
65
|
+
'if',
|
|
66
|
+
'then',
|
|
67
|
+
'than',
|
|
68
|
+
'so',
|
|
69
|
+
'such',
|
|
70
|
+
'each',
|
|
71
|
+
'any',
|
|
72
|
+
'all',
|
|
73
|
+
'every',
|
|
74
|
+
'when',
|
|
75
|
+
'where',
|
|
76
|
+
'must',
|
|
77
|
+
'should',
|
|
78
|
+
'shall',
|
|
79
|
+
'may',
|
|
80
|
+
'can',
|
|
81
|
+
'will',
|
|
82
|
+
'end',
|
|
83
|
+
'new',
|
|
39
84
|
// generic task verbs
|
|
40
|
-
'add',
|
|
41
|
-
'
|
|
42
|
-
'
|
|
85
|
+
'add',
|
|
86
|
+
'implement',
|
|
87
|
+
'create',
|
|
88
|
+
'build',
|
|
89
|
+
'scaffold',
|
|
90
|
+
'setup',
|
|
91
|
+
'set',
|
|
92
|
+
'support',
|
|
93
|
+
'handle',
|
|
94
|
+
'apply',
|
|
95
|
+
'use',
|
|
96
|
+
'used',
|
|
97
|
+
'using',
|
|
98
|
+
'make',
|
|
99
|
+
'makes',
|
|
100
|
+
'made',
|
|
101
|
+
'enable',
|
|
102
|
+
'provide',
|
|
103
|
+
'ensure',
|
|
104
|
+
'allow',
|
|
105
|
+
'run',
|
|
106
|
+
'runs',
|
|
107
|
+
'get',
|
|
108
|
+
'gets',
|
|
109
|
+
'define',
|
|
110
|
+
'configure',
|
|
111
|
+
'init',
|
|
112
|
+
'update',
|
|
113
|
+
'manage',
|
|
43
114
|
// generic project nouns
|
|
44
|
-
'cli',
|
|
45
|
-
'
|
|
115
|
+
'cli',
|
|
116
|
+
'tool',
|
|
117
|
+
'app',
|
|
118
|
+
'application',
|
|
119
|
+
'project',
|
|
120
|
+
'feature',
|
|
121
|
+
'task',
|
|
122
|
+
'tasks',
|
|
123
|
+
'user',
|
|
124
|
+
'users',
|
|
125
|
+
'mode',
|
|
126
|
+
'flag',
|
|
127
|
+
'flags',
|
|
128
|
+
'option',
|
|
129
|
+
'options',
|
|
130
|
+
'system',
|
|
131
|
+
'code',
|
|
132
|
+
'thing',
|
|
133
|
+
'things',
|
|
134
|
+
'work'
|
|
46
135
|
]);
|
|
47
136
|
/** Distinctive content tokens of a phrase: lowercased alphanumeric words ≥3 chars,
|
|
48
137
|
* minus the ubiquitous stopwords. `--json` → `json`, `dead-letter` → `dead`,`letter`.
|
|
@@ -76,11 +76,20 @@ export declare function accountCoverage(requirements: RequirementEntry[], mappin
|
|
|
76
76
|
/** The stored carried-requirements text ('' when none recorded). */
|
|
77
77
|
export declare function readRequirements(cwd: string): Promise<string>;
|
|
78
78
|
/**
|
|
79
|
-
* Append carried requirements
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* Append carried requirements, deduped against what is stored. Three channels,
|
|
80
|
+
* each better carried into every task than silently lost — host-side only,
|
|
81
|
+
* children never write it, best-effort:
|
|
82
|
+
* • `crossCutting` — obligations no single task owns (policy/global rules).
|
|
83
|
+
* • `unresolved` — grounded requirements still unmapped after the retry rounds.
|
|
84
|
+
* • `judgeFlagged` — free-text areas the holistic coverage judge flagged as
|
|
85
|
+
* uncovered that requirement-extraction never captured as a tracked entry, so
|
|
86
|
+
* the grounded channels above are structurally blind to them (mx5 2026-07-16:
|
|
87
|
+
* §10's test-infra setup was seen ONLY by the judge and, having no carrier,
|
|
88
|
+
* was warned-about then dropped). These are plain strings, not quotes of the
|
|
89
|
+
* source; marked distinctly so a task can tell an inferred area from a verbatim
|
|
90
|
+
* obligation.
|
|
82
91
|
*/
|
|
83
|
-
export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[]): Promise<void>;
|
|
92
|
+
export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[], judgeFlagged?: string[]): Promise<void>;
|
|
84
93
|
/**
|
|
85
94
|
* The read-only block refine/compose receive when carried requirements exist.
|
|
86
95
|
* Verbatim content travels with every task (the directive pattern that works),
|
|
@@ -308,12 +308,21 @@ function formatEntry(e, marker) {
|
|
|
308
308
|
return `"${e.quote}"${anchor}${marker ? ` [${marker}]` : ''}`;
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
|
-
* Append carried requirements
|
|
312
|
-
*
|
|
313
|
-
*
|
|
311
|
+
* Append carried requirements, deduped against what is stored. Three channels,
|
|
312
|
+
* each better carried into every task than silently lost — host-side only,
|
|
313
|
+
* children never write it, best-effort:
|
|
314
|
+
* • `crossCutting` — obligations no single task owns (policy/global rules).
|
|
315
|
+
* • `unresolved` — grounded requirements still unmapped after the retry rounds.
|
|
316
|
+
* • `judgeFlagged` — free-text areas the holistic coverage judge flagged as
|
|
317
|
+
* uncovered that requirement-extraction never captured as a tracked entry, so
|
|
318
|
+
* the grounded channels above are structurally blind to them (mx5 2026-07-16:
|
|
319
|
+
* §10's test-infra setup was seen ONLY by the judge and, having no carrier,
|
|
320
|
+
* was warned-about then dropped). These are plain strings, not quotes of the
|
|
321
|
+
* source; marked distinctly so a task can tell an inferred area from a verbatim
|
|
322
|
+
* obligation.
|
|
314
323
|
*/
|
|
315
|
-
export async function appendCarriedRequirements(cwd, crossCutting, unresolved = []) {
|
|
316
|
-
if (crossCutting.length === 0 && unresolved.length === 0)
|
|
324
|
+
export async function appendCarriedRequirements(cwd, crossCutting, unresolved = [], judgeFlagged = []) {
|
|
325
|
+
if (crossCutting.length === 0 && unresolved.length === 0 && judgeFlagged.length === 0)
|
|
317
326
|
return;
|
|
318
327
|
try {
|
|
319
328
|
const existing = (await readRequirements(cwd)).split('\n').filter(l => l.trim().length > 0);
|
|
@@ -324,7 +333,11 @@ export async function appendCarriedRequirements(cwd, crossCutting, unresolved =
|
|
|
324
333
|
const merged = [...existing];
|
|
325
334
|
for (const [entries, marker] of [
|
|
326
335
|
[crossCutting, undefined],
|
|
327
|
-
[unresolved, 'no task owns this — surfaced at plan time']
|
|
336
|
+
[unresolved, 'no task owns this — surfaced at plan time'],
|
|
337
|
+
[
|
|
338
|
+
judgeFlagged.map(q => ({ quote: q, anchor: '' })),
|
|
339
|
+
'judge-flagged uncovered area, no task owns this — surfaced at plan time'
|
|
340
|
+
]
|
|
328
341
|
]) {
|
|
329
342
|
for (const e of entries) {
|
|
330
343
|
const key = normalise(e.quote);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.20",
|
|
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",
|