@mjasnikovs/pi-task 0.23.0 → 0.24.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/task/auto-orchestrator.js +44 -3
- package/dist/task/coverage-loop.d.ts +4 -0
- package/dist/task/coverage-loop.js +41 -0
- package/dist/task/decompose-granularity.d.ts +12 -0
- package/dist/task/decompose-granularity.js +15 -1
- package/dist/task/requirements.d.ts +12 -1
- package/dist/task/requirements.js +113 -2
- package/package.json +1 -1
|
@@ -89,9 +89,26 @@ function coverageRepromptHint(missing) {
|
|
|
89
89
|
// that insists twice still ships its small plan, with a warning.
|
|
90
90
|
const SUSPECT_PLAN_MAX_TITLES = 2;
|
|
91
91
|
const SUSPECT_PLAN_MIN_SPEC_CHARS = 4000;
|
|
92
|
+
/**
|
|
93
|
+
* Extra retries granted when the plan is EMPTY rather than merely small. One
|
|
94
|
+
* hinted retry heals a small-but-nonempty plan reliably; an empty generation is a
|
|
95
|
+
* harder fault and was measured recurring back-to-back (2026-07-28 smoke: 13 empty
|
|
96
|
+
* draws across 24 reps of a 20KB spec, including two in a row in one rep).
|
|
97
|
+
*/
|
|
98
|
+
const EMPTY_PLAN_RETRIES = 2;
|
|
99
|
+
/**
|
|
100
|
+
* An empty list is NEVER a valid decomposition of any feature request, at any spec
|
|
101
|
+
* size. It used to escape this guard entirely — the old predicate opened with
|
|
102
|
+
* `titles.length > 0`, so zero titles was not "suspect", the suspect-retry never
|
|
103
|
+
* fired, the coverage loop broke immediately on `titles.length === 0`, and the run
|
|
104
|
+
* aborted with "no tasks produced from the feature". A single degenerate
|
|
105
|
+
* generation killed the whole run with no retry, which is the opposite of how the
|
|
106
|
+
* same fault is treated one title higher.
|
|
107
|
+
*/
|
|
92
108
|
function isSuspectPlan(titles, featureForModel) {
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
if (titles.length === 0)
|
|
110
|
+
return true;
|
|
111
|
+
return (titles.length <= SUSPECT_PLAN_MAX_TITLES
|
|
95
112
|
&& featureForModel.length >= SUSPECT_PLAN_MIN_SPEC_CHARS);
|
|
96
113
|
}
|
|
97
114
|
/** Reprompt prefix for a suspect (degenerate-count) list; unlike
|
|
@@ -695,8 +712,32 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
695
712
|
// catch it (3/10 live false-pass) and a hinted retry heals it reliably
|
|
696
713
|
// (5/5 live). Longer list wins; a still-suspect plan falls through to the
|
|
697
714
|
// judge loop as before, so this never blocks planning.
|
|
698
|
-
|
|
715
|
+
// An EMPTY plan gets extra attempts (see EMPTY_PLAN_RETRIES): falling through
|
|
716
|
+
// with zero titles aborts the whole run, so one roll of the dice is not enough.
|
|
717
|
+
// A merely-small plan keeps its single retry — it still ships if the retry does
|
|
718
|
+
// not help, so spending more children on it buys nothing.
|
|
719
|
+
//
|
|
720
|
+
// The two budgets are tracked SEPARATELY on purpose. A single counter bounded by
|
|
721
|
+
// `plan.length === 0 ? EMPTY_PLAN_RETRIES : 0` re-reads the bound against the
|
|
722
|
+
// CURRENT plan, so an empty draw that healed to a still-suspect 1-title plan saw
|
|
723
|
+
// the bound collapse to 0 and skipped the small-plan retry that an identical
|
|
724
|
+
// 1-title FIRST draw would have received. Same end state, different treatment,
|
|
725
|
+
// purely because of how it got there.
|
|
726
|
+
let emptyAttempts = 0;
|
|
727
|
+
let smallRetryUsed = false;
|
|
728
|
+
while (isSuspectPlan(planTitles, featureForModel)) {
|
|
729
|
+
if (planTitles.length === 0) {
|
|
730
|
+
if (emptyAttempts > EMPTY_PLAN_RETRIES)
|
|
731
|
+
break;
|
|
732
|
+
emptyAttempts++;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
if (smallRetryUsed)
|
|
736
|
+
break;
|
|
737
|
+
smallRetryUsed = true;
|
|
738
|
+
}
|
|
699
739
|
logPlanDebug(cwd, `decompose suspect (${planTitles.length} title(s) for a ${featureForModel.length}-char spec)`
|
|
740
|
+
+ `${emptyAttempts > 1 ? ` — empty retry ${emptyAttempts}` : ''}`
|
|
700
741
|
+ ` — raw output: ${listRaw.trim().slice(0, 300)}`);
|
|
701
742
|
const retryRaw = await deps.runChild('auto-decompose', 'read', prependHint(suspectPlanHint(planTitles.length), decomposePrompt));
|
|
702
743
|
const retryTitles = parsePlan(retryRaw);
|
|
@@ -85,6 +85,10 @@ export interface AdoptionDecision {
|
|
|
85
85
|
* current plan already owns (non-superset owned-set). This is the monotone
|
|
86
86
|
* guarantee; it holds regardless of how the un-ownable requirements were
|
|
87
87
|
* classified, so it backstops the cross-cutting classifier completely.
|
|
88
|
+
* 2b. WITH requirement signal: reject a retry that grows the plan while covering
|
|
89
|
+
* NOTHING new — the tiebreak that makes "ship the best" also mean "ship the
|
|
90
|
+
* smallest among equals". Without it the guard is inert against the superset
|
|
91
|
+
* the reprompt asks for; see the long note at the branch.
|
|
88
92
|
* 3. WITHOUT requirement signal: fall back to the count floor, and additionally
|
|
89
93
|
* refuse a retry that leaves MORE areas uncovered than the current plan — so
|
|
90
94
|
* the no-requirements path also ships the best, not the last.
|
|
@@ -215,6 +215,10 @@ export function droppedCoverage(current, retry) {
|
|
|
215
215
|
* current plan already owns (non-superset owned-set). This is the monotone
|
|
216
216
|
* guarantee; it holds regardless of how the un-ownable requirements were
|
|
217
217
|
* classified, so it backstops the cross-cutting classifier completely.
|
|
218
|
+
* 2b. WITH requirement signal: reject a retry that grows the plan while covering
|
|
219
|
+
* NOTHING new — the tiebreak that makes "ship the best" also mean "ship the
|
|
220
|
+
* smallest among equals". Without it the guard is inert against the superset
|
|
221
|
+
* the reprompt asks for; see the long note at the branch.
|
|
218
222
|
* 3. WITHOUT requirement signal: fall back to the count floor, and additionally
|
|
219
223
|
* refuse a retry that leaves MORE areas uncovered than the current plan — so
|
|
220
224
|
* the no-requirements path also ships the best, not the last.
|
|
@@ -238,6 +242,43 @@ export function decideAdoption(current, retry, hasRequirements) {
|
|
|
238
242
|
dropped
|
|
239
243
|
};
|
|
240
244
|
}
|
|
245
|
+
// Growth must PAY FOR ITSELF. Past this point the retry's owned-set is a
|
|
246
|
+
// superset, so an equal size means the sets are IDENTICAL — the retry
|
|
247
|
+
// covers nothing new. Adopting it anyway is how mx5 (2026-07-28) went
|
|
248
|
+
// 26 → 32 → 60 titles with the owned-set pinned at 27 in all three rounds,
|
|
249
|
+
// both retries logged as "preserves owned coverage".
|
|
250
|
+
//
|
|
251
|
+
// The old rule could not object, by construction: groundedCoverage is
|
|
252
|
+
// monotone in the title set (titleTokens is a union over titles; df/maxDF
|
|
253
|
+
// depend only on the quotes), and coverageRepromptHint asks the model for
|
|
254
|
+
// "every task your previous list already had ... PLUS" — a superset. So
|
|
255
|
+
// `dropped` is structurally empty whenever the model obeys the hint and the
|
|
256
|
+
// guard adopted unconditionally (measured: 2000/2000 superset retries
|
|
257
|
+
// adopted; 563/2000 independently-sampled ones rejected — the guard has
|
|
258
|
+
// power, just not against the shape the prompt requests).
|
|
259
|
+
//
|
|
260
|
+
// REJECT, never break. Rejection keeps the smaller plan and lets the loop
|
|
261
|
+
// reprompt again; breaking here forfeits a later round that would have
|
|
262
|
+
// gained (live: 19t/24c → 38t/24c → 40t/25c, where stopping at the tie
|
|
263
|
+
// loses the 25th requirement). "No gain this round" is not "no gain ever".
|
|
264
|
+
//
|
|
265
|
+
// Safety is structural, not statistical: this branch is reachable only when
|
|
266
|
+
// the retry covers NO MORE than the current plan, so it can never decline a
|
|
267
|
+
// strictly better one. Live A/B (Qwen3.6-27B, mx5 20KB spec, 24 reps,
|
|
268
|
+
// precondition 24/24): inflated plans 7/24 → 0/24, Fisher two-sided
|
|
269
|
+
// p=0.0094; coverage mean 24.58 → 24.79 (higher in 6 reps, lower in 1, and
|
|
270
|
+
// that one rep never fired this clause); plan size 41.8 → 38.8, which is
|
|
271
|
+
// NOT significant (sign 15/8, p=0.21) — the win is removing pathological
|
|
272
|
+
// inflation, not shrinking plans generally.
|
|
273
|
+
if (retry.covered.size <= current.covered.size
|
|
274
|
+
&& retry.titles.length > current.titles.length) {
|
|
275
|
+
return {
|
|
276
|
+
adopt: false,
|
|
277
|
+
reason: `no coverage gain for +${retry.titles.length - current.titles.length} titles `
|
|
278
|
+
+ `(${current.covered.size} owned, unchanged)`,
|
|
279
|
+
dropped: []
|
|
280
|
+
};
|
|
281
|
+
}
|
|
241
282
|
return { adopt: true, reason: 'preserves owned coverage', dropped: [] };
|
|
242
283
|
}
|
|
243
284
|
if (retry.missing.length > current.missing.length) {
|
|
@@ -43,6 +43,18 @@ export declare const MAX_REQUIREMENTS_PER_TASK = 2;
|
|
|
43
43
|
/**
|
|
44
44
|
* Fewest task titles a plan may have for `ownable` requirements. Zero when the
|
|
45
45
|
* requirement channel produced nothing, which disables every check below.
|
|
46
|
+
*
|
|
47
|
+
* Also zero below MIN_REQUIREMENTS_FOR_PLAN_SHAPE, for the reason that constant
|
|
48
|
+
* already documents: under a handful of requirements the plan is one or two tasks
|
|
49
|
+
* either way, and the requirement COUNT at that scale is an artifact of extraction
|
|
50
|
+
* granularity rather than real breadth. Measured (2026-07-28 size smoke): the
|
|
51
|
+
* 78-char feature "Add a `--version` flag to the CLI that prints the package
|
|
52
|
+
* version and exits 0" extracted THREE ownable requirements — the flag, the
|
|
53
|
+
* print, the exit code — yielding a floor of 2 for what is unambiguously one
|
|
54
|
+
* task. Both arms correctly shipped 1 title, so the floor bought nothing and cost
|
|
55
|
+
* a split-retry child; had anything ever made it binding it would have forced a
|
|
56
|
+
* bad split. The same cut governs both because it is the same judgement: the
|
|
57
|
+
* requirement channel is not load-bearing for shape until a feature has real breadth.
|
|
46
58
|
*/
|
|
47
59
|
export declare function granularityFloor(ownable: number): number;
|
|
48
60
|
/** Is this plan too coarse for the requirements it has to carry? */
|
|
@@ -43,9 +43,23 @@ export const MAX_REQUIREMENTS_PER_TASK = 2;
|
|
|
43
43
|
/**
|
|
44
44
|
* Fewest task titles a plan may have for `ownable` requirements. Zero when the
|
|
45
45
|
* requirement channel produced nothing, which disables every check below.
|
|
46
|
+
*
|
|
47
|
+
* Also zero below MIN_REQUIREMENTS_FOR_PLAN_SHAPE, for the reason that constant
|
|
48
|
+
* already documents: under a handful of requirements the plan is one or two tasks
|
|
49
|
+
* either way, and the requirement COUNT at that scale is an artifact of extraction
|
|
50
|
+
* granularity rather than real breadth. Measured (2026-07-28 size smoke): the
|
|
51
|
+
* 78-char feature "Add a `--version` flag to the CLI that prints the package
|
|
52
|
+
* version and exits 0" extracted THREE ownable requirements — the flag, the
|
|
53
|
+
* print, the exit code — yielding a floor of 2 for what is unambiguously one
|
|
54
|
+
* task. Both arms correctly shipped 1 title, so the floor bought nothing and cost
|
|
55
|
+
* a split-retry child; had anything ever made it binding it would have forced a
|
|
56
|
+
* bad split. The same cut governs both because it is the same judgement: the
|
|
57
|
+
* requirement channel is not load-bearing for shape until a feature has real breadth.
|
|
46
58
|
*/
|
|
47
59
|
export function granularityFloor(ownable) {
|
|
48
|
-
|
|
60
|
+
if (ownable < MIN_REQUIREMENTS_FOR_PLAN_SHAPE)
|
|
61
|
+
return 0;
|
|
62
|
+
return Math.ceil(ownable / MAX_REQUIREMENTS_PER_TASK);
|
|
49
63
|
}
|
|
50
64
|
/** Is this plan too coarse for the requirements it has to carry? */
|
|
51
65
|
export function isTooCoarse(titles, floor) {
|
|
@@ -31,7 +31,18 @@ export declare function keepGroundedRequirements(entries: RequirementEntry[], so
|
|
|
31
31
|
* never the model-authored anchor text. Without `sourceDoc` (or for quotes that
|
|
32
32
|
* cannot be located) the fill degrades to the old given-order behavior.
|
|
33
33
|
*/
|
|
34
|
-
export declare function capRequirements(entries: RequirementEntry[], passages: string[], sourceDoc?: string
|
|
34
|
+
export declare function capRequirements(entries: RequirementEntry[], passages: string[], sourceDoc?: string,
|
|
35
|
+
/** A/B seam: `false` reproduces the pre-budget rule, so an offline harness can
|
|
36
|
+
* score the shipped rule against the one it replaced without transcribing
|
|
37
|
+
* sectionFairFill and letting the copy drift. Production never passes it. */
|
|
38
|
+
deprioritiseLowValue?: boolean): RequirementEntry[];
|
|
39
|
+
/**
|
|
40
|
+
* Quotes that pass the grounding guard (verbatim substring of the doc) but state
|
|
41
|
+
* no obligation. There is no obligation test anywhere else in the pipeline —
|
|
42
|
+
* `keepGroundedRequirements` only checks the quote really appears in the source,
|
|
43
|
+
* so any sentence at all survives it and precision rests entirely on the model.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isLowValueQuote(quote: string): boolean;
|
|
35
46
|
/**
|
|
36
47
|
* DETERMINISTIC RECALL FLOOR (same medicine as the launch-contract checklist):
|
|
37
48
|
* paragraphs carrying an obligation marker (word-bounded "required"/"must").
|
|
@@ -98,7 +98,11 @@ export function keepGroundedRequirements(entries, sourceDoc) {
|
|
|
98
98
|
* never the model-authored anchor text. Without `sourceDoc` (or for quotes that
|
|
99
99
|
* cannot be located) the fill degrades to the old given-order behavior.
|
|
100
100
|
*/
|
|
101
|
-
export function capRequirements(entries, passages, sourceDoc
|
|
101
|
+
export function capRequirements(entries, passages, sourceDoc,
|
|
102
|
+
/** A/B seam: `false` reproduces the pre-budget rule, so an offline harness can
|
|
103
|
+
* score the shipped rule against the one it replaced without transcribing
|
|
104
|
+
* sectionFairFill and letting the copy drift. Production never passes it. */
|
|
105
|
+
deprioritiseLowValue = true) {
|
|
102
106
|
if (entries.length <= MAX_REQUIREMENTS)
|
|
103
107
|
return entries;
|
|
104
108
|
const norms = passages.map(normalise);
|
|
@@ -109,7 +113,114 @@ export function capRequirements(entries, passages, sourceDoc) {
|
|
|
109
113
|
const marked = entries.filter(covers);
|
|
110
114
|
const rest = entries.filter(e => !covers(e));
|
|
111
115
|
const budget = MAX_REQUIREMENTS - Math.min(marked.length, MAX_REQUIREMENTS);
|
|
112
|
-
|
|
116
|
+
// The low-value filter applies to the UNMARKED remainder only. Quoting an
|
|
117
|
+
// obligation-marked passage is the pipeline's existing, validated evidence
|
|
118
|
+
// that a quote states an obligation, and it outranks any lexical heuristic:
|
|
119
|
+
// "MUST log every request" is 22 characters and every length-based rule reads
|
|
120
|
+
// it as a fragment. Filtering ahead of the marked/rest split deleted it.
|
|
121
|
+
//
|
|
122
|
+
// INSURANCE, not a measured win on mx5: across both 30-run pools exactly one
|
|
123
|
+
// distinct quote per pool is low-value AND marked, and it is a genuinely
|
|
124
|
+
// truncated one. The layering matters for specs whose obligations are SHORT,
|
|
125
|
+
// which mx5's are not. Do not cite it as the reason tail coverage holds —
|
|
126
|
+
// tail coverage is identical with the filter applied before the split.
|
|
127
|
+
const pool = deprioritiseLowValue ? budgetedByObligation(rest, budget, sourceDoc) : rest;
|
|
128
|
+
return [...marked.slice(0, MAX_REQUIREMENTS), ...sectionFairFill(pool, budget, sourceDoc)];
|
|
129
|
+
}
|
|
130
|
+
/** Longest a dependency-pin row can be before it is presumed to carry an
|
|
131
|
+
* obligation after the pin. Real pins in the measured corpus run 32..48 chars;
|
|
132
|
+
* the pin-prefixed lines that DO obligate ("TypeScript `6.0.3` — one strict
|
|
133
|
+
* `tsconfig.json`: `strict`, `noUncheckedIndexedAccess`, …") run 130..260. */
|
|
134
|
+
const MAX_PIN_LENGTH = 80;
|
|
135
|
+
/** Cut mid-expression: an unbalanced fence or bracket, or a trailing separator.
|
|
136
|
+
* NOT `;` — a complete clause legitimately ends with one, and dropping on `;`
|
|
137
|
+
* discarded a runnable `lint` = `prettier … && eslint … && tsc --noEmit` line. */
|
|
138
|
+
function isTruncatedQuote(q) {
|
|
139
|
+
if ((q.match(/`/g) ?? []).length % 2 === 1)
|
|
140
|
+
return true;
|
|
141
|
+
for (const [open, close] of [
|
|
142
|
+
['(', ')'],
|
|
143
|
+
['[', ']'],
|
|
144
|
+
['{', '}']
|
|
145
|
+
]) {
|
|
146
|
+
const o = (q.match(new RegExp(`\\${open}`, 'g')) ?? []).length;
|
|
147
|
+
const c = (q.match(new RegExp(`\\${close}`, 'g')) ?? []).length;
|
|
148
|
+
if (o > c)
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return /[,:([{]\s*$/.test(q.trim());
|
|
152
|
+
}
|
|
153
|
+
/** A bare version row — data, not an obligation. Length-gated, see MAX_PIN_LENGTH. */
|
|
154
|
+
function isDependencyPin(q) {
|
|
155
|
+
const t = q.trim().replace(/^\*\*|\*\*$/g, '');
|
|
156
|
+
if (t.length > MAX_PIN_LENGTH)
|
|
157
|
+
return false;
|
|
158
|
+
return /^\**[`*]?[\w@/-]+[`*]?\**\s*[`']?\d+\.\d+/.test(t);
|
|
159
|
+
}
|
|
160
|
+
/** A DDL/schema row: `col type …`. */
|
|
161
|
+
function isSchemaRow(q) {
|
|
162
|
+
return /^\s*[\w_]+\s+(?:uuid|text|int|integer|bigint|boolean|timestamptz|bytea|jsonb|numeric|smallint)\b/i.test(q);
|
|
163
|
+
}
|
|
164
|
+
/** Too short to state an obligation. MIN_QUOTE_LENGTH (6) admits "Contact
|
|
165
|
+
* seller"; a clause needs a subject and a predicate. */
|
|
166
|
+
function isQuoteFragment(q) {
|
|
167
|
+
const t = q.trim();
|
|
168
|
+
return t.length < 25 || t.split(/\s+/).length < 4;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Quotes that pass the grounding guard (verbatim substring of the doc) but state
|
|
172
|
+
* no obligation. There is no obligation test anywhere else in the pipeline —
|
|
173
|
+
* `keepGroundedRequirements` only checks the quote really appears in the source,
|
|
174
|
+
* so any sentence at all survives it and precision rests entirely on the model.
|
|
175
|
+
*/
|
|
176
|
+
export function isLowValueQuote(quote) {
|
|
177
|
+
return (isTruncatedQuote(quote)
|
|
178
|
+
|| isDependencyPin(quote)
|
|
179
|
+
|| isSchemaRow(quote)
|
|
180
|
+
|| isQuoteFragment(quote));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Deprioritise obligation-free quotes, but only as far as the BUDGET requires.
|
|
184
|
+
*
|
|
185
|
+
* Measured (mx5, 20402-char spec, two independent 30-run extraction pools): the
|
|
186
|
+
* extractor's single-pass yield swings 20..160 for byte-identical input, and the
|
|
187
|
+
* padding crowds real obligations out of the 40 that ship — high-yield runs land
|
|
188
|
+
* FEWER critical obligations than low-yield ones. Critical obligations reaching
|
|
189
|
+
* the shipped list go 8.50 → 9.37 of 16 on the design pool (10 runs better, 0
|
|
190
|
+
* worse, p=0.0020) and 7.70 → 8.43 on the confirmation pool (12 / 0, p=0.0005).
|
|
191
|
+
*
|
|
192
|
+
* BUDGETED, not absolute. Below the cap no slot is contested, so dropping there
|
|
193
|
+
* destroys information and buys nothing; a run that filtered 55 quotes down to 20
|
|
194
|
+
* lost its only carrier of the Argon2id obligation — a DDL row that was correctly
|
|
195
|
+
* classified as one — while 20 slots sat empty. So the low-value entries come back
|
|
196
|
+
* in source-doc order until the list reaches the cap.
|
|
197
|
+
*
|
|
198
|
+
* Absolute filtering scored marginally higher on raw count (24 gains vs 22 across
|
|
199
|
+
* both pools) and was rejected anyway: its extra gains are one more obligation in
|
|
200
|
+
* an already-populated list, while its one loss is an obligation vanishing from a
|
|
201
|
+
* run outright. Those are not the same size of mistake.
|
|
202
|
+
*
|
|
203
|
+
* Doc order for the restore is the neutral choice: which entries return only
|
|
204
|
+
* matters when more were dropped than there are free slots, and ordering by
|
|
205
|
+
* anything fitted to an observed loss would be tuning the rule to one pool.
|
|
206
|
+
*/
|
|
207
|
+
function budgetedByObligation(entries, budget, sourceDoc) {
|
|
208
|
+
const keep = entries.filter(e => !isLowValueQuote(e.quote));
|
|
209
|
+
if (keep.length >= budget)
|
|
210
|
+
return keep;
|
|
211
|
+
const at = (e) => {
|
|
212
|
+
if (!sourceDoc)
|
|
213
|
+
return Number.MAX_SAFE_INTEGER;
|
|
214
|
+
const i = sourceDoc.indexOf(e.quote.trim());
|
|
215
|
+
return i < 0 ? Number.MAX_SAFE_INTEGER : i;
|
|
216
|
+
};
|
|
217
|
+
const restored = entries
|
|
218
|
+
.filter(e => isLowValueQuote(e.quote))
|
|
219
|
+
.map((e, given) => ({ e, at: at(e), given }))
|
|
220
|
+
.sort((x, y) => x.at - y.at || x.given - y.given)
|
|
221
|
+
.slice(0, budget - keep.length)
|
|
222
|
+
.map(x => x.e);
|
|
223
|
+
return [...keep, ...restored];
|
|
113
224
|
}
|
|
114
225
|
/** The doc split into heading-delimited sections, each pre-normalised for
|
|
115
226
|
* containment tests. Text before the first heading is its own section. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|