@mjasnikovs/pi-task 0.40.50 → 0.42.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/README.md +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +31 -1
- package/dist/task/accept-debt.js +49 -3
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +66 -7
- package/dist/task/auto-io.js +152 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +184 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +201 -39
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/health-repair.d.ts +68 -0
- package/dist/task/health-repair.js +124 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +52 -58
- package/dist/task/task-gates.js +161 -199
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +32 -6
- package/dist/task/verify-resolution.js +78 -15
- package/dist/task/verify-work.d.ts +105 -35
- package/dist/task/verify-work.js +232 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
52
52
|
import * as path from 'node:path';
|
|
53
53
|
import { shippedSources, stripCommentLines, SOURCE_HTML_RE, SOURCE_JS_RE } from './shipped-source.js';
|
|
54
|
+
import { blocksOf, parseSpecDoc } from './spec-doc.js';
|
|
54
55
|
export function emptyProducers() {
|
|
55
56
|
return {
|
|
56
57
|
files: new Set(),
|
|
@@ -940,10 +941,15 @@ const PROSE_CONSUME_RE = /\b(?:serves?|serving|served|fallback|reads?|loads?|ren
|
|
|
940
941
|
* `index.html`, `data/seed.json` and `assets/logo.svg` all extract.
|
|
941
942
|
* Code-construct refs are not subject to this list. */
|
|
942
943
|
const PROSE_ASSET_EXT_RE = /\.(?:html?|css|m?js|cjs|json|svg|png|jpe?g|gif|webp|ico|woff2?|ttf|otf|wasm|webmanifest|xml|csv|sql|ya?ml|toml|pdf|mp[34]|db|sqlite)$/i;
|
|
944
|
+
/** The block kinds that can carry consuming PROSE. A fence is code — its
|
|
945
|
+
* `serve`/`read` calls are the code channel's to resolve, not prose's — and a
|
|
946
|
+
* blockquote is quoting something the spec is not itself saying. */
|
|
947
|
+
const PROSE_KINDS = new Set(['para', 'list-item', 'table-row']);
|
|
943
948
|
/** Backticked, asset-extension, path-shaped tokens on consuming-verb lines. */
|
|
944
949
|
export function extractSpecProseRefs(spec) {
|
|
945
950
|
const out = [];
|
|
946
|
-
|
|
951
|
+
const prose = blocksOf(parseSpecDoc(spec)).filter(b => PROSE_KINDS.has(b.kind));
|
|
952
|
+
for (const line of prose.flatMap(b => b.text.split('\n'))) {
|
|
947
953
|
if (!PROSE_CONSUME_RE.test(line))
|
|
948
954
|
continue;
|
|
949
955
|
const tick = /`([^`\n]+)`/g;
|
package/dist/task/auto-io.d.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import type { TaskState } from './task-types.js';
|
|
2
2
|
import type { AutoResumeCandidate } from './resume-gap.js';
|
|
3
|
+
import { type RunEndKind } from './run-end.js';
|
|
3
4
|
export interface TaskEntry {
|
|
5
|
+
/** Position in the list — shifts when a repair step is spliced in. */
|
|
4
6
|
index: number;
|
|
7
|
+
/**
|
|
8
|
+
* The entry's stable identity, allocated at plan time and never reused.
|
|
9
|
+
* `index` moves and `title` is prose a later pass may rewrite; this is what
|
|
10
|
+
* the owned-requirements ledger joins on. Absent on an AUTO file written
|
|
11
|
+
* before the key existed, where the title join is still the only one there is.
|
|
12
|
+
*/
|
|
13
|
+
key?: string;
|
|
5
14
|
title: string;
|
|
6
15
|
done: boolean;
|
|
7
16
|
producedId?: string;
|
|
17
|
+
/** How many implementation attempts this entry has had. */
|
|
18
|
+
attempts?: number;
|
|
19
|
+
/**
|
|
20
|
+
* How the last attempt ENDED, recorded only when it ended abnormally. A
|
|
21
|
+
* resume otherwise cannot tell an entry the user cancelled from one that
|
|
22
|
+
* faulted, because both leave the same unchecked, stamped line.
|
|
23
|
+
*/
|
|
24
|
+
lastEnd?: RunEndKind;
|
|
8
25
|
}
|
|
9
26
|
export declare function allocateAutoId(cwd: string): Promise<string>;
|
|
10
27
|
/**
|
|
@@ -23,20 +40,40 @@ export declare function allocateAutoId(cwd: string): Promise<string>;
|
|
|
23
40
|
*/
|
|
24
41
|
export declare function parseDecomposeList(raw: string): string[];
|
|
25
42
|
/** Parsed DECOMPOSE_COVERAGE_PROMPT verdict. */
|
|
26
|
-
export
|
|
27
|
-
kind: 'complete'
|
|
43
|
+
export type CoverageVerdict = {
|
|
44
|
+
kind: 'complete';
|
|
45
|
+
missing: string[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'incomplete';
|
|
28
48
|
missing: string[];
|
|
29
49
|
}
|
|
50
|
+
/** The judge DID rule INCOMPLETE but named nothing to reprompt with. */
|
|
51
|
+
| {
|
|
52
|
+
kind: 'unparseable';
|
|
53
|
+
};
|
|
54
|
+
/** What an `unparseable` verdict contributes to the plan's missing-area list, so
|
|
55
|
+
* the judge's own INCOMPLETE cannot be shipped as COMPLETE. */
|
|
56
|
+
export declare const UNNAMED_COVERAGE_GAP = "the coverage judge ruled INCOMPLETE without naming the uncovered area";
|
|
30
57
|
/**
|
|
31
58
|
* Parse the coverage-triage child's verdict. Returns null when no COVERAGE tag is
|
|
32
|
-
* present (the model wrote prose), and the caller reads a null verdict as
|
|
33
|
-
* empty missing-list, so a malformed judgment can never block planning.
|
|
59
|
+
* present at all (the model wrote prose), and the caller reads a null verdict as
|
|
60
|
+
* an empty missing-list, so a malformed judgment can never block planning.
|
|
34
61
|
*
|
|
35
|
-
* `COVERAGE: INCOMPLETE` with no MISSING lines
|
|
36
|
-
*
|
|
37
|
-
*
|
|
62
|
+
* `COVERAGE: INCOMPLETE` with no MISSING lines is NOT null, and that distinction
|
|
63
|
+
* is the whole point: prose is no verdict, while this is a verdict of INCOMPLETE
|
|
64
|
+
* that happens to name nothing. Collapsing the two shipped a plan the judge had
|
|
65
|
+
* just ruled incomplete, logged as COMPLETE.
|
|
38
66
|
*/
|
|
39
67
|
export declare function parseCoverageVerdict(raw: string): CoverageVerdict | null;
|
|
68
|
+
/** The plan key for the `index`th entry of a freshly planned list. */
|
|
69
|
+
export declare function planKeyAt(index: number): string;
|
|
70
|
+
/**
|
|
71
|
+
* The lowest key number no entry in `entries` holds. Keys are allocated 1:1 with
|
|
72
|
+
* plan entries, so an unkeyed legacy list has implicitly spent its first N
|
|
73
|
+
* numbers — counting it in keeps a spliced repair step from minting a key an
|
|
74
|
+
* eventual migration would hand to an existing entry.
|
|
75
|
+
*/
|
|
76
|
+
export declare function nextPlanKey(entries: readonly TaskEntry[]): string;
|
|
40
77
|
/** Parse the "## tasks" checkbox list. */
|
|
41
78
|
export declare function parseTaskList(body: string): TaskEntry[];
|
|
42
79
|
/** Build the initial AUTO-file body. `coverage` is the requirement-level
|
|
@@ -51,8 +88,23 @@ export declare function checkOffTask(cwd: string, id: string, index: number, pro
|
|
|
51
88
|
* the inner task is allocated. This links the AUTO entry to its in-progress
|
|
52
89
|
* inner task so /task-auto-resume can continue it from its saved phase instead
|
|
53
90
|
* of starting a brand-new task — matching how /task-resume behaves.
|
|
91
|
+
*
|
|
92
|
+
* The first stamp is also where the attempt counter is minted: an entry under way
|
|
93
|
+
* has had one attempt.
|
|
54
94
|
*/
|
|
55
95
|
export declare function stampTaskInProgress(cwd: string, id: string, index: number, producedId: string, title: string): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Count one more attempt on the `index`th entry and return the new total.
|
|
98
|
+
*
|
|
99
|
+
* Called for a fresh start AND for a resume, because both spend a run on the
|
|
100
|
+
* entry: a task that crashes in refine is re-entered from scratch every time, and
|
|
101
|
+
* a counter that only saw fresh starts would read 1 forever while the loop re-ran
|
|
102
|
+
* it without end. The previous ending is cleared here — it describes the attempt
|
|
103
|
+
* that is now over.
|
|
104
|
+
*/
|
|
105
|
+
export declare function beginTaskAttempt(cwd: string, id: string, index: number): Promise<number>;
|
|
106
|
+
/** Record how the `index`th entry's attempt ended. */
|
|
107
|
+
export declare function recordTaskEnd(cwd: string, id: string, index: number, lastEnd: RunEndKind): Promise<void>;
|
|
56
108
|
/**
|
|
57
109
|
* Insert a NEW unchecked entry directly after the `afterIndex`th checkbox — the
|
|
58
110
|
* mid-run plan mutation the root-cause repair channel needs: a repair task must
|
|
@@ -75,6 +127,13 @@ export declare function stampTaskInProgress(cwd: string, id: string, index: numb
|
|
|
75
127
|
* cached index.
|
|
76
128
|
*/
|
|
77
129
|
export declare function insertTaskAfter(cwd: string, id: string, afterIndex: number, title: string): Promise<boolean>;
|
|
130
|
+
/**
|
|
131
|
+
* Insert a NEW unchecked entry directly BEFORE the `index`th checkbox — the
|
|
132
|
+
* position a health repair needs: the task about to run must wait until the red
|
|
133
|
+
* it would build on is fixed. Same monotonic, duplicate-refusing splice as
|
|
134
|
+
* {@link insertTaskAfter}.
|
|
135
|
+
*/
|
|
136
|
+
export declare function insertTaskBefore(cwd: string, id: string, index: number, title: string): Promise<boolean>;
|
|
78
137
|
/**
|
|
79
138
|
* Find the most-recently-updated resumable TASK_AUTO_* file, with the state and
|
|
80
139
|
* last-write time the resume banner reports (see resume-gap.ts). Null when there
|
package/dist/task/auto-io.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* `## coverage`. The checkbox list under `## tasks` is the resume cursor, and the
|
|
8
8
|
* loop finds its next step with `entries.find(e => !e.done)` rather than a
|
|
9
9
|
* remembered position.
|
|
10
|
+
*
|
|
11
|
+
* A checkbox line reads `- [ ] P01 TASK_0006 a2 title` — see {@link ENTRY_RE}.
|
|
10
12
|
*/
|
|
11
13
|
import * as fsp from 'node:fs/promises';
|
|
12
14
|
import * as path from 'node:path';
|
|
@@ -14,6 +16,7 @@ import { tasksDir, ensureTasksDir, readTaskFile, setTaskSection } from './task-i
|
|
|
14
16
|
import { extractSection, parseFrontMatter } from './task-parsers.js';
|
|
15
17
|
import { readTextFile } from '../shared/fs-text.js';
|
|
16
18
|
import { RESUMABLE_STATES } from './task-types.js';
|
|
19
|
+
import { RUN_END_POLICY } from './run-end.js';
|
|
17
20
|
const AUTO_FILE_RE = /^(TASK_AUTO_\d{4,})\.md$/;
|
|
18
21
|
export async function allocateAutoId(cwd) {
|
|
19
22
|
await ensureTasksDir(cwd);
|
|
@@ -52,14 +55,21 @@ export function parseDecomposeList(raw) {
|
|
|
52
55
|
}
|
|
53
56
|
return out;
|
|
54
57
|
}
|
|
58
|
+
/** The MISSING bound the coverage prompt states; a longer list is truncated
|
|
59
|
+
* rather than rejected. */
|
|
60
|
+
const MAX_MISSING_AREAS = 8;
|
|
61
|
+
/** What an `unparseable` verdict contributes to the plan's missing-area list, so
|
|
62
|
+
* the judge's own INCOMPLETE cannot be shipped as COMPLETE. */
|
|
63
|
+
export const UNNAMED_COVERAGE_GAP = 'the coverage judge ruled INCOMPLETE without naming the uncovered area';
|
|
55
64
|
/**
|
|
56
65
|
* Parse the coverage-triage child's verdict. Returns null when no COVERAGE tag is
|
|
57
|
-
* present (the model wrote prose), and the caller reads a null verdict as
|
|
58
|
-
* empty missing-list, so a malformed judgment can never block planning.
|
|
66
|
+
* present at all (the model wrote prose), and the caller reads a null verdict as
|
|
67
|
+
* an empty missing-list, so a malformed judgment can never block planning.
|
|
59
68
|
*
|
|
60
|
-
* `COVERAGE: INCOMPLETE` with no MISSING lines
|
|
61
|
-
*
|
|
62
|
-
*
|
|
69
|
+
* `COVERAGE: INCOMPLETE` with no MISSING lines is NOT null, and that distinction
|
|
70
|
+
* is the whole point: prose is no verdict, while this is a verdict of INCOMPLETE
|
|
71
|
+
* that happens to name nothing. Collapsing the two shipped a plan the judge had
|
|
72
|
+
* just ruled incomplete, logged as COMPLETE.
|
|
63
73
|
*/
|
|
64
74
|
export function parseCoverageVerdict(raw) {
|
|
65
75
|
const tag = /^\s*COVERAGE:\s*(COMPLETE|INCOMPLETE)\s*$/im.exec(raw);
|
|
@@ -72,39 +82,95 @@ export function parseCoverageVerdict(raw) {
|
|
|
72
82
|
const m = /^\s*MISSING:\s*(.+?)\s*$/i.exec(line);
|
|
73
83
|
if (m && m[1].length > 0)
|
|
74
84
|
missing.push(m[1]);
|
|
75
|
-
if (missing.length >=
|
|
85
|
+
if (missing.length >= MAX_MISSING_AREAS)
|
|
76
86
|
break;
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
// with — treat it like an unparseable verdict rather than looping blind.
|
|
80
|
-
return missing.length === 0 ? null : { kind: 'incomplete', missing };
|
|
88
|
+
return missing.length === 0 ? { kind: 'unparseable' } : { kind: 'incomplete', missing };
|
|
81
89
|
}
|
|
82
90
|
const CHECKBOX_RE = /^- \[([ xX])\]\s+(.+?)\s*$/;
|
|
83
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The checkbox line grammar: `- [ ] P01 TASK_0006 a2 title`, where the attempts
|
|
93
|
+
* field may carry how the last attempt ended — `a2:failed`.
|
|
94
|
+
*
|
|
95
|
+
* Every field is optional and each is followed by ONE space; the title is set off
|
|
96
|
+
* by an EXTRA space. That two-space delimiter is what keeps the grammar
|
|
97
|
+
* unambiguous against prose: a title of its own may begin `TASK_0006 is broken`
|
|
98
|
+
* or `(auto) …` and is read whole, because a single space never separates a field
|
|
99
|
+
* from a title. Both legacy forms — `TASK_0006 title` and a bare `title` — are
|
|
100
|
+
* the same grammar with fields missing, so no migration pass is needed on read.
|
|
101
|
+
*
|
|
102
|
+
* The end suffix spells out the endings RUN_END_POLICY registers rather than
|
|
103
|
+
* accepting any word, so `a2:whatever ` stays prose and a renamed ending is a
|
|
104
|
+
* parse miss, never a silently mis-read field.
|
|
105
|
+
*/
|
|
106
|
+
const ENTRY_RE = new RegExp('^(?:(?:(P\\d{2,}) )?(?:(TASK_\\d{4,}) )?'
|
|
107
|
+
+ `(?:a(\\d+)(?::(${Object.keys(RUN_END_POLICY).join('|')}))? )? )?(.+)$`);
|
|
108
|
+
function parseEntryLine(line, index) {
|
|
109
|
+
const m = CHECKBOX_RE.exec(line.trim());
|
|
110
|
+
if (!m)
|
|
111
|
+
return null;
|
|
112
|
+
const f = ENTRY_RE.exec(m[2].trim());
|
|
113
|
+
if (!f)
|
|
114
|
+
return null;
|
|
115
|
+
const [, key, producedId, attempts, lastEnd, title] = f;
|
|
116
|
+
// Attempts are only ever written alongside an id (they are minted when the
|
|
117
|
+
// inner task starts), so a bare `a7 …` is a title, not a field.
|
|
118
|
+
const keyed = key !== undefined || producedId !== undefined;
|
|
119
|
+
return {
|
|
120
|
+
index,
|
|
121
|
+
done: m[1].toLowerCase() === 'x',
|
|
122
|
+
// A line carries a stamped TASK_NNNN id both when done (the completed
|
|
123
|
+
// inner task) and when merely started — an unchecked, stamped line is an
|
|
124
|
+
// in-progress entry whose inner task can be resumed.
|
|
125
|
+
...(key !== undefined && { key }),
|
|
126
|
+
...(producedId !== undefined && { producedId }),
|
|
127
|
+
...(keyed && attempts !== undefined && { attempts: parseInt(attempts, 10) }),
|
|
128
|
+
...(keyed && lastEnd !== undefined && { lastEnd: lastEnd }),
|
|
129
|
+
title: keyed || attempts === undefined ? title.trim() : m[2].trim()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Render one entry back into the line grammar.
|
|
134
|
+
*
|
|
135
|
+
* An entry with NEITHER a key nor an id has no field to anchor an attempts count
|
|
136
|
+
* against, and `a2 title` alone re-parses as a title (see {@link ENTRY_RE}) — so
|
|
137
|
+
* the count is dropped rather than written into a line that would read it back as
|
|
138
|
+
* prose. Only pre-key legacy plans are in that state, and they predate the counter.
|
|
139
|
+
*/
|
|
140
|
+
function renderEntryLine(e) {
|
|
141
|
+
const anchored = e.key !== undefined || e.producedId !== undefined;
|
|
142
|
+
const attempts = e.attempts === undefined || !anchored ?
|
|
143
|
+
undefined
|
|
144
|
+
: `a${e.attempts}${e.lastEnd === undefined ? '' : `:${e.lastEnd}`}`;
|
|
145
|
+
const fields = [e.key, e.producedId, attempts]
|
|
146
|
+
.filter((f) => f !== undefined && f.length > 0)
|
|
147
|
+
.join(' ');
|
|
148
|
+
return `- [${e.done ? 'x' : ' '}] ${fields.length > 0 ? `${fields} ` : ''}${e.title}`;
|
|
149
|
+
}
|
|
150
|
+
/** The plan key for the `index`th entry of a freshly planned list. */
|
|
151
|
+
export function planKeyAt(index) {
|
|
152
|
+
return `P${String(index + 1).padStart(2, '0')}`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The lowest key number no entry in `entries` holds. Keys are allocated 1:1 with
|
|
156
|
+
* plan entries, so an unkeyed legacy list has implicitly spent its first N
|
|
157
|
+
* numbers — counting it in keeps a spliced repair step from minting a key an
|
|
158
|
+
* eventual migration would hand to an existing entry.
|
|
159
|
+
*/
|
|
160
|
+
export function nextPlanKey(entries) {
|
|
161
|
+
const highest = entries.reduce((max, e) => Math.max(max, e.key ? parseInt(e.key.slice(1), 10) : 0), entries.length);
|
|
162
|
+
return planKeyAt(highest);
|
|
163
|
+
}
|
|
84
164
|
/** Parse the "## tasks" checkbox list. */
|
|
85
165
|
export function parseTaskList(body) {
|
|
86
166
|
const section = extractSection(body, 'tasks');
|
|
87
167
|
if (section === null)
|
|
88
168
|
return [];
|
|
89
169
|
const entries = [];
|
|
90
|
-
let index = 0;
|
|
91
170
|
for (const line of section.split('\n')) {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
const done = m[1].toLowerCase() === 'x';
|
|
96
|
-
const rest = m[2].trim();
|
|
97
|
-
// A line carries a stamped TASK_NNNN id both when done (the completed
|
|
98
|
-
// inner task) and when merely started — an unchecked, stamped line is an
|
|
99
|
-
// in-progress entry whose inner task can be resumed.
|
|
100
|
-
const idm = PRODUCED_ID_RE.exec(rest);
|
|
101
|
-
if (idm) {
|
|
102
|
-
entries.push({ index, title: idm[2].trim(), done, producedId: idm[1] });
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
entries.push({ index, title: rest, done });
|
|
106
|
-
}
|
|
107
|
-
index++;
|
|
171
|
+
const entry = parseEntryLine(line, entries.length);
|
|
172
|
+
if (entry)
|
|
173
|
+
entries.push(entry);
|
|
108
174
|
}
|
|
109
175
|
return entries;
|
|
110
176
|
}
|
|
@@ -113,24 +179,31 @@ export function parseTaskList(body) {
|
|
|
113
179
|
* cross-cutting and what stayed unowned. An empty string omits the section
|
|
114
180
|
* entirely rather than emitting a blank heading. */
|
|
115
181
|
export function buildAutoBody(feature, clarifications, titles, coverage = '') {
|
|
116
|
-
const tasks = titles
|
|
182
|
+
const tasks = titles
|
|
183
|
+
.map((t, i) => renderEntryLine({ key: planKeyAt(i), title: t, done: false }))
|
|
184
|
+
.join('\n');
|
|
117
185
|
return (`\n## feature prompt\n\n${feature.trim() || '(none)'}\n\n`
|
|
118
186
|
+ `## clarifications\n\n${clarifications.trim() || '(none)'}\n\n`
|
|
119
187
|
+ `## tasks\n\n${tasks}\n`
|
|
120
188
|
+ (coverage.trim().length > 0 ? `\n## coverage\n\n${coverage.trim()}\n` : ''));
|
|
121
189
|
}
|
|
122
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Rewrite the Nth checkbox line of the "## tasks" section in place. `render`
|
|
192
|
+
* receives the line as parsed, so a rewrite that only changes one field carries
|
|
193
|
+
* the others — a check-off must not drop the key the ownership join reads.
|
|
194
|
+
*/
|
|
123
195
|
async function rewriteTaskLine(cwd, id, index, render, label) {
|
|
124
196
|
const { body } = await readTaskFile(cwd, id);
|
|
125
197
|
const section = extractSection(body, 'tasks') ?? '';
|
|
126
198
|
const lines = section.split('\n');
|
|
127
199
|
let seen = -1;
|
|
128
200
|
for (let i = 0; i < lines.length; i++) {
|
|
129
|
-
|
|
201
|
+
const entry = parseEntryLine(lines[i], seen + 1);
|
|
202
|
+
if (!entry)
|
|
130
203
|
continue;
|
|
131
204
|
seen++;
|
|
132
205
|
if (seen === index) {
|
|
133
|
-
lines[i] = render();
|
|
206
|
+
lines[i] = render(entry);
|
|
134
207
|
break;
|
|
135
208
|
}
|
|
136
209
|
}
|
|
@@ -141,25 +214,45 @@ async function rewriteTaskLine(cwd, id, index, render, label) {
|
|
|
141
214
|
}
|
|
142
215
|
/** Check off the Nth checkbox line, stamping the produced TASK_NNNN id. */
|
|
143
216
|
export async function checkOffTask(cwd, id, index, producedId, title) {
|
|
144
|
-
await rewriteTaskLine(cwd, id, index,
|
|
217
|
+
await rewriteTaskLine(cwd, id, index, e => renderEntryLine({ ...e, done: true, title, producedId: producedId || undefined }), 'checkOffTask');
|
|
145
218
|
}
|
|
146
219
|
/**
|
|
147
220
|
* Stamp the inner TASK_NNNN id onto the Nth (still-unchecked) entry the moment
|
|
148
221
|
* the inner task is allocated. This links the AUTO entry to its in-progress
|
|
149
222
|
* inner task so /task-auto-resume can continue it from its saved phase instead
|
|
150
223
|
* of starting a brand-new task — matching how /task-resume behaves.
|
|
224
|
+
*
|
|
225
|
+
* The first stamp is also where the attempt counter is minted: an entry under way
|
|
226
|
+
* has had one attempt.
|
|
151
227
|
*/
|
|
152
228
|
export async function stampTaskInProgress(cwd, id, index, producedId, title) {
|
|
153
|
-
await rewriteTaskLine(cwd, id, index,
|
|
229
|
+
await rewriteTaskLine(cwd, id, index, e => renderEntryLine({ ...e, done: false, title, producedId }), 'stampTaskInProgress');
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Count one more attempt on the `index`th entry and return the new total.
|
|
233
|
+
*
|
|
234
|
+
* Called for a fresh start AND for a resume, because both spend a run on the
|
|
235
|
+
* entry: a task that crashes in refine is re-entered from scratch every time, and
|
|
236
|
+
* a counter that only saw fresh starts would read 1 forever while the loop re-ran
|
|
237
|
+
* it without end. The previous ending is cleared here — it describes the attempt
|
|
238
|
+
* that is now over.
|
|
239
|
+
*/
|
|
240
|
+
export async function beginTaskAttempt(cwd, id, index) {
|
|
241
|
+
let attempts = 1;
|
|
242
|
+
await rewriteTaskLine(cwd, id, index, e => {
|
|
243
|
+
attempts = (e.attempts ?? 0) + 1;
|
|
244
|
+
const { lastEnd: _ended, ...rest } = e;
|
|
245
|
+
return renderEntryLine({ ...rest, attempts });
|
|
246
|
+
}, 'beginTaskAttempt');
|
|
247
|
+
return attempts;
|
|
154
248
|
}
|
|
155
|
-
/**
|
|
249
|
+
/** Record how the `index`th entry's attempt ended. */
|
|
250
|
+
export async function recordTaskEnd(cwd, id, index, lastEnd) {
|
|
251
|
+
await rewriteTaskLine(cwd, id, index, e => renderEntryLine({ ...e, attempts: e.attempts ?? 1, lastEnd }), 'recordTaskEnd');
|
|
252
|
+
}
|
|
253
|
+
/** The bare title of a checkbox line (fields stripped), or null if not one. */
|
|
156
254
|
function entryTitle(line) {
|
|
157
|
-
|
|
158
|
-
if (!m)
|
|
159
|
-
return null;
|
|
160
|
-
const rest = m[2].trim();
|
|
161
|
-
const idm = PRODUCED_ID_RE.exec(rest);
|
|
162
|
-
return idm ? idm[2].trim() : rest;
|
|
255
|
+
return parseEntryLine(line, 0)?.title ?? null;
|
|
163
256
|
}
|
|
164
257
|
/**
|
|
165
258
|
* Insert a NEW unchecked entry directly after the `afterIndex`th checkbox — the
|
|
@@ -194,15 +287,18 @@ export async function insertTaskAfter(cwd, id, afterIndex, title) {
|
|
|
194
287
|
// sits relative to afterIndex — never add a second one.
|
|
195
288
|
if (lines.some(l => entryTitle(l) === clean))
|
|
196
289
|
return false;
|
|
197
|
-
|
|
290
|
+
const entries = [];
|
|
198
291
|
let insertAt = -1;
|
|
199
292
|
for (let i = 0; i < lines.length; i++) {
|
|
200
|
-
|
|
293
|
+
const entry = parseEntryLine(lines[i], entries.length);
|
|
294
|
+
if (!entry)
|
|
201
295
|
continue;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
296
|
+
entries.push(entry);
|
|
297
|
+
// A negative afterIndex means "before the first entry".
|
|
298
|
+
if (entries.length === 1 && afterIndex < 0)
|
|
299
|
+
insertAt = i;
|
|
300
|
+
if (entries.length - 1 <= afterIndex)
|
|
301
|
+
insertAt = i + 1;
|
|
206
302
|
}
|
|
207
303
|
// An out-of-range afterIndex is not an error: the loop above leaves `insertAt`
|
|
208
304
|
// just past the LAST checkbox, so the entry is appended rather than lost — a
|
|
@@ -213,10 +309,19 @@ export async function insertTaskAfter(cwd, id, afterIndex, title) {
|
|
|
213
309
|
// lines at all, where there is no position to splice into.
|
|
214
310
|
if (insertAt === -1)
|
|
215
311
|
return false;
|
|
216
|
-
lines.splice(insertAt, 0,
|
|
312
|
+
lines.splice(insertAt, 0, renderEntryLine({ key: nextPlanKey(entries), title: clean, done: false }));
|
|
217
313
|
await setTaskSection(cwd, id, 'tasks', lines.join('\n'));
|
|
218
314
|
return true;
|
|
219
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Insert a NEW unchecked entry directly BEFORE the `index`th checkbox — the
|
|
318
|
+
* position a health repair needs: the task about to run must wait until the red
|
|
319
|
+
* it would build on is fixed. Same monotonic, duplicate-refusing splice as
|
|
320
|
+
* {@link insertTaskAfter}.
|
|
321
|
+
*/
|
|
322
|
+
export function insertTaskBefore(cwd, id, index, title) {
|
|
323
|
+
return insertTaskAfter(cwd, id, index - 1, title);
|
|
324
|
+
}
|
|
220
325
|
/**
|
|
221
326
|
* Find the most-recently-updated resumable TASK_AUTO_* file, with the state and
|
|
222
327
|
* last-write time the resume banner reports (see resume-gap.ts). Null when there
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { type TaskEntry } from './auto-io.js';
|
|
3
|
+
import { expandFeatureMentions, readableMentions } from './mentions.js';
|
|
4
|
+
export { expandFeatureMentions, readableMentions };
|
|
2
5
|
import { type GateDeps } from './task-gates.js';
|
|
3
6
|
import { type FinalGateStageDeps } from './run-final-gate.js';
|
|
4
7
|
import { type RequirementEntry } from './requirements.js';
|
|
@@ -31,23 +34,6 @@ export interface AutoDeps extends GateDeps, FinalGateStageDeps {
|
|
|
31
34
|
}
|
|
32
35
|
/** Wait for every plan-debug line written so far to reach disk. Tests only. */
|
|
33
36
|
export declare function flushPlanDebug(): Promise<unknown>;
|
|
34
|
-
/**
|
|
35
|
-
* Expand any @file references in the feature text by appending each referenced
|
|
36
|
-
* file's contents, so the planning children (clarify, decompose) always see the
|
|
37
|
-
* real spec inline instead of relying on the model to open the file itself.
|
|
38
|
-
* Without this, clarify on a one-line "Implement @spec.md" tends to bail with
|
|
39
|
-
* NONE because, to the model, the request looks small and unambiguous.
|
|
40
|
-
* Unreadable mentions (typos, non-file @tokens) are left untouched; the feature
|
|
41
|
-
* is returned verbatim when nothing readable is referenced.
|
|
42
|
-
*/
|
|
43
|
-
export declare function expandFeatureMentions(cwd: string, feature: string): Promise<string>;
|
|
44
|
-
/**
|
|
45
|
-
* The @file references in the feature that point at a readable file on disk —
|
|
46
|
-
* the bare path tokens, deduped, in first-seen order. Unreadable mentions
|
|
47
|
-
* (typos, non-file @tokens) are dropped so we never advertise a missing file as
|
|
48
|
-
* an authoritative spec.
|
|
49
|
-
*/
|
|
50
|
-
export declare function readableMentions(cwd: string, feature: string): Promise<string[]>;
|
|
51
37
|
/**
|
|
52
38
|
* Thread the feature's spec references AND any per-task decisions into every
|
|
53
39
|
* decomposed task title. A title is ALL a per-task pipeline ever sees, so both the
|
|
@@ -76,11 +62,28 @@ export declare function attachSpecRefs(titles: string[], refs: string[]): string
|
|
|
76
62
|
*
|
|
77
63
|
* The plan listing strips the threaded "| decisions … | spec …" tail from each
|
|
78
64
|
* title and keeps the human-readable head, so the model reads clean step names,
|
|
79
|
-
* and it marks the current one
|
|
65
|
+
* and it marks the current one `[this]` — both confirmed by building a fence
|
|
80
66
|
* over threaded titles. The authoritative spec ref still rides on THIS step's own
|
|
81
67
|
* title via attachSpecRefs.
|
|
68
|
+
*
|
|
69
|
+
* Every sibling used to read as unbuilt, which is a lie about the first half of
|
|
70
|
+
* any plan past step one: a step told its predecessors are "done in later runs"
|
|
71
|
+
* re-scaffolds what is already in the tree. The entries carry their own done
|
|
72
|
+
* state, so the listing states it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildScopeFence(entries: readonly TaskEntry[], currentIndex: number): string;
|
|
75
|
+
/**
|
|
76
|
+
* The scope fence for step `currentIndex`, plus the REPAIR fence when that step is
|
|
77
|
+
* a queued root-cause repair. A repair title ("repair test/teardown.ts: …") reads
|
|
78
|
+
* to refine like any other feature step, and refine's job is to expand a title into
|
|
79
|
+
* a full spec — which is exactly how "repair the teardown" becomes "overhaul the
|
|
80
|
+
* test infrastructure". The extra fence pins the one editable file, and pins VERIFY
|
|
81
|
+
* to the failing command WHEN the title carries one: `extractFailingCommand` reads
|
|
82
|
+
* a backticked runner command out of the defect text, so a defect quoting
|
|
83
|
+
* `bun run test` yields it while a plain-prose defect yields undefined and only the
|
|
84
|
+
* file pin applies.
|
|
82
85
|
*/
|
|
83
|
-
export declare function
|
|
86
|
+
export declare function buildStepFence(entries: readonly TaskEntry[], currentIndex: number): string;
|
|
84
87
|
/**
|
|
85
88
|
* What ORIENT establishes about the feature before anyone is asked anything: the
|
|
86
89
|
* spec text the planning children will actually read, and the requirement ledger
|