@mjasnikovs/pi-task 0.17.17 → 0.17.19

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.
@@ -7,6 +7,17 @@ export interface TaskEntry {
7
7
  export declare function allocateAutoId(cwd: string): Promise<string>;
8
8
  /** Parse a decompose-phase model output into a clean list of titles. */
9
9
  export declare function parseDecomposeList(raw: string): string[];
10
+ /** Parsed DECOMPOSE_COVERAGE_PROMPT verdict. */
11
+ export interface CoverageVerdict {
12
+ kind: 'complete' | 'incomplete';
13
+ missing: string[];
14
+ }
15
+ /**
16
+ * Parse the coverage-triage child's verdict. Returns null when no COVERAGE tag
17
+ * is present (the model wrote prose) — the caller treats that as "accept the
18
+ * list as-is", so a malformed judgment can never block planning.
19
+ */
20
+ export declare function parseCoverageVerdict(raw: string): CoverageVerdict | null;
10
21
  /** Parse the "## tasks" checkbox list. */
11
22
  export declare function parseTaskList(body: string): TaskEntry[];
12
23
  /** Build the initial AUTO-file body. */
@@ -39,6 +39,29 @@ export function parseDecomposeList(raw) {
39
39
  }
40
40
  return out;
41
41
  }
42
+ /**
43
+ * Parse the coverage-triage child's verdict. Returns null when no COVERAGE tag
44
+ * is present (the model wrote prose) — the caller treats that as "accept the
45
+ * list as-is", so a malformed judgment can never block planning.
46
+ */
47
+ export function parseCoverageVerdict(raw) {
48
+ const tag = /^\s*COVERAGE:\s*(COMPLETE|INCOMPLETE)\s*$/im.exec(raw);
49
+ if (!tag)
50
+ return null;
51
+ if (tag[1].toUpperCase() === 'COMPLETE')
52
+ return { kind: 'complete', missing: [] };
53
+ const missing = [];
54
+ for (const line of raw.split('\n')) {
55
+ const m = /^\s*MISSING:\s*(.+?)\s*$/i.exec(line);
56
+ if (m && m[1].length > 0)
57
+ missing.push(m[1]);
58
+ if (missing.length >= 8)
59
+ break;
60
+ }
61
+ // INCOMPLETE with no MISSING lines carries no actionable signal to reprompt
62
+ // with — treat it like an unparseable verdict rather than looping blind.
63
+ return missing.length === 0 ? null : { kind: 'incomplete', missing };
64
+ }
42
65
  const CHECKBOX_RE = /^- \[([ xX])\]\s+(.+?)\s*$/;
43
66
  const PRODUCED_ID_RE = /^(TASK_\d{4,})\s{2,}(.+)$/;
44
67
  /** Parse the "## tasks" checkbox list. */
@@ -10,10 +10,10 @@ import * as path from 'node:path';
10
10
  import { gateRunTask } from './orchestrator.js';
11
11
  import { parseClarifyList, parseAutoAnswer, autoAnswerHasTag, deriveTitle } from './parsers.js';
12
12
  import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
13
- import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT } from './auto-prompts.js';
13
+ import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
14
14
  import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
15
15
  import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
16
- import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
16
+ import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
17
17
  import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
18
18
  import { readTextFile } from '../shared/fs-text.js';
19
19
  import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
@@ -29,6 +29,18 @@ import { runGatesForTask } from './task-gates.js';
29
29
  // when the model emits NONE), but a model that never says NONE would otherwise
30
30
  // barrage the user — the real mx5 run asked 10, several of them redundant.
31
31
  const MAX_CLARIFY_QUESTIONS = 8;
32
+ // Bounded coverage-triage rounds after decompose: judge → reprompt-with-missing
33
+ // → judge again, at most. Two rounds so one flaky retry doesn't end the gate,
34
+ // while a judge that keeps flagging can't loop the plan phase forever.
35
+ const MAX_COVERAGE_ROUNDS = 2;
36
+ /** Reprompt prefix when the coverage triage found feature areas no task covers. */
37
+ function coverageRepromptHint(missing) {
38
+ return ('[SYSTEM NOTE: Your previous task list was INCOMPLETE — no task covered: '
39
+ + missing.join('; ')
40
+ + '. Regenerate the FULL ordered checkbox list for the ENTIRE feature: every '
41
+ + 'task your previous list already had (reworded freely) PLUS tasks covering '
42
+ + 'the areas above. Output every task, one "- [ ] " line each, nothing else.]');
43
+ }
32
44
  // Matches pi's @-file completion token (a path after @, until whitespace).
33
45
  const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
34
46
  // Trailing punctuation a user naturally types AFTER an @-mention when it sits in
@@ -391,12 +403,43 @@ export async function planAuto(ctx, cwd, feature, deps) {
391
403
  }
392
404
  const clarifications = answers.join('\n');
393
405
  // decompose
394
- const listRaw = await deps.runChild('auto-decompose', 'read', AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications));
406
+ const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications);
407
+ const listRaw = await deps.runChild('auto-decompose', 'read', decomposePrompt);
408
+ let planTitles = parseDecomposeList(listRaw);
409
+ logPlanDebug(cwd, `decompose produced ${planTitles.length} title(s)`);
410
+ // Coverage gate: a stochastic degenerate completion (live mx5: ONE task +
411
+ // natural EOS for an 18KB design doc) is nonempty, so the length guard below
412
+ // never fires and the whole run "completes" after one task. Judge the list
413
+ // against the feature with a no-tools child; on INCOMPLETE, re-run decompose
414
+ // with the missing areas as a hint. Longest-list-wins so a flaky retry can
415
+ // never replace a better list; best-effort so a triage fault never blocks
416
+ // planning (mirrors triageClarifyQuestion).
417
+ for (let round = 0; round < MAX_COVERAGE_ROUNDS && planTitles.length > 0; round++) {
418
+ let verdict;
419
+ try {
420
+ verdict = parseCoverageVerdict(await deps.runChild('decompose-coverage', '', DECOMPOSE_COVERAGE_PROMPT(featureForModel, clarifications, planTitles)));
421
+ }
422
+ catch {
423
+ break;
424
+ }
425
+ if (verdict === null || verdict.kind === 'complete') {
426
+ logPlanDebug(cwd, `decompose-coverage round ${round + 1}: `
427
+ + (verdict === null ? 'no verdict — accepting list' : 'COMPLETE'));
428
+ break;
429
+ }
430
+ logPlanDebug(cwd, `decompose-coverage round ${round + 1}: INCOMPLETE — missing: `
431
+ + verdict.missing.join('; ').slice(0, 300));
432
+ const retryRaw = await deps.runChild('auto-decompose', 'read', prependHint(coverageRepromptHint(verdict.missing), decomposePrompt));
433
+ const retryTitles = parseDecomposeList(retryRaw);
434
+ logPlanDebug(cwd, `decompose retry produced ${retryTitles.length} title(s)`);
435
+ if (retryTitles.length > planTitles.length)
436
+ planTitles = retryTitles;
437
+ }
395
438
  // Thread the feature's spec doc(s) into every title so each per-task
396
439
  // pipeline — which only ever sees its title — reads the real spec instead of
397
440
  // a lossy one-line paraphrase of it.
398
441
  const refs = await readableMentions(cwd, feature);
399
- const titles = attachSpecRefs(parseDecomposeList(listRaw), refs);
442
+ const titles = attachSpecRefs(planTitles, refs);
400
443
  if (titles.length === 0) {
401
444
  announceDone(ctx, '/task-auto: no tasks produced from the feature.', 'warning');
402
445
  return null;
@@ -418,7 +461,8 @@ export async function planAuto(ctx, cwd, feature, deps) {
418
461
  /** The two feature-level planning children, shown as steps in the loader. */
419
462
  const AUTO_PLAN_STEPS = {
420
463
  'auto-clarify': { step: 'clarify', stepNum: 1 },
421
- 'auto-decompose': { step: 'decompose', stepNum: 2 }
464
+ 'auto-decompose': { step: 'decompose', stepNum: 2 },
465
+ 'decompose-coverage': { step: 'coverage', stepNum: 2 }
422
466
  };
423
467
  const AUTO_PLAN_STEP_TOTAL = 2;
424
468
  function defaultDeps(ctx, cwd, signal, title) {
@@ -14,3 +14,13 @@ export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) =>
14
14
  * Decompose: output a markdown checkbox list of task titles (one line each).
15
15
  */
16
16
  export declare const AUTO_DECOMPOSE_PROMPT: (feature: string, clarifications: string) => string;
17
+ /**
18
+ * Coverage triage: judge whether a decomposed task list covers the whole
19
+ * feature. Guards the plan — the highest-leverage artifact in /task-auto —
20
+ * against a degenerate-but-nonempty decompose completion (live mx5: the model
21
+ * emitted ONE task for an 18KB design doc with a natural EOS, and the only
22
+ * existing gate was `titles.length === 0`, so the run "completed" after one
23
+ * task). Pure judgment over text already in hand, so it runs with --no-tools.
24
+ * Output MUST match parseCoverageVerdict.
25
+ */
26
+ export declare const DECOMPOSE_COVERAGE_PROMPT: (feature: string, clarifications: string, list: string[]) => string;
@@ -89,3 +89,37 @@ RULES:
89
89
  doc; phrase them as imperative directives (e.g. "use Bun's built-in bundler, do
90
90
  not add vite"). Do NOT invent decisions — only restate ones from CLARIFICATIONS.
91
91
  - Output the checkbox list and NOTHING else (no preamble, no numbering).`;
92
+ /**
93
+ * Coverage triage: judge whether a decomposed task list covers the whole
94
+ * feature. Guards the plan — the highest-leverage artifact in /task-auto —
95
+ * against a degenerate-but-nonempty decompose completion (live mx5: the model
96
+ * emitted ONE task for an 18KB design doc with a natural EOS, and the only
97
+ * existing gate was `titles.length === 0`, so the run "completed" after one
98
+ * task). Pure judgment over text already in hand, so it runs with --no-tools.
99
+ * Output MUST match parseCoverageVerdict.
100
+ */
101
+ export const DECOMPOSE_COVERAGE_PROMPT = (feature, clarifications, list) => `You are auditing a task decomposition for completeness. Below is a FEATURE
102
+ REQUEST (with any referenced design spec inlined), the CLARIFICATIONS the user
103
+ settled, and the proposed ordered TASK LIST that is supposed to implement ALL
104
+ of it. Each listed task will later get its own research and spec — so judge
105
+ coverage only, not wording or level of detail.
106
+
107
+ FEATURE REQUEST:
108
+ ${feature.trim()}
109
+
110
+ CLARIFICATIONS:
111
+ ${clarifications.trim() || '(none)'}
112
+
113
+ TASK LIST:
114
+ ${list.map((t, i) => `${i + 1}. ${t}`).join('\n')}
115
+
116
+ Judge ONE thing: taken together, do these tasks cover the whole feature end to
117
+ end? An area is MISSING only when NO task plausibly covers it — do not flag
118
+ wording, ordering, task size, or detail a task's own research will fill in.
119
+
120
+ Output — no preamble, no markdown:
121
+ COVERAGE: COMPLETE
122
+ or
123
+ COVERAGE: INCOMPLETE
124
+ MISSING: <feature area no task covers>
125
+ (one MISSING line per uncovered area, most important first, at most 8)`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.17.17",
4
- "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
3
+ "version": "0.17.19",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -52,12 +52,30 @@
52
52
  "pi-package",
53
53
  "pi",
54
54
  "pi-extension",
55
+ "pi-coding-agent",
56
+ "pi.dev",
55
57
  "coding-agent",
58
+ "ai-agent",
59
+ "agent",
60
+ "subagent",
56
61
  "task-orchestration",
62
+ "spec-orchestration",
63
+ "spec-driven",
64
+ "task-planning",
65
+ "planning",
66
+ "autonomous",
57
67
  "local-llm",
68
+ "llm",
69
+ "llama-server",
58
70
  "remote",
59
- "web"
71
+ "web-view",
72
+ "web-push"
60
73
  ],
74
+ "author": "mjasnikovs <mjasnikovs@googlemail.com>",
75
+ "homepage": "https://github.com/mjasnikovs/pi-task#readme",
76
+ "bugs": {
77
+ "url": "https://github.com/mjasnikovs/pi-task/issues"
78
+ },
61
79
  "repository": {
62
80
  "type": "git",
63
81
  "url": "git+https://github.com/mjasnikovs/pi-task.git"