@mjasnikovs/pi-task 0.13.20 → 0.13.22
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/loop-detector.d.ts +30 -7
- package/dist/task/loop-detector.js +80 -13
- package/dist/task/phases.d.ts +16 -0
- package/dist/task/phases.js +93 -34
- package/dist/task/task-io.d.ts +2 -0
- package/dist/task/task-io.js +9 -0
- package/package.json +1 -1
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure-logic loop detector for pi-task sub-agent phases.
|
|
3
3
|
*
|
|
4
|
-
* Watches a ring buffer of recent tool
|
|
5
|
-
* `(toolName, stable-stringified args)` key appears `threshold` times
|
|
6
|
-
* within the last `window` events, returns a LoopHit so the caller can
|
|
7
|
-
* kill the child and re-spawn with a hint.
|
|
4
|
+
* Watches a ring buffer of recent tool calls and trips on two thrash patterns:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
6
|
+
* 1. EXACT repeats — the same `(toolName, stable-stringified args)` key appears
|
|
7
|
+
* `threshold` times within the last `window` events.
|
|
8
|
+
* 2. PATH revisits — the same primary file path is re-targeted `pathThreshold`
|
|
9
|
+
* times within the window WITHOUT the read offset advancing, which the exact
|
|
10
|
+
* key misses when `offset/limit` vary call-to-call. Strictly-forward paging
|
|
11
|
+
* never trips (see countRevisits).
|
|
12
|
+
*
|
|
13
|
+
* Either pattern returns a LoopHit so the caller can kill the child and re-spawn
|
|
14
|
+
* with a hint. No I/O. No imports from index.ts. Trivially unit-testable.
|
|
10
15
|
*/
|
|
11
16
|
export interface ToolCall {
|
|
12
17
|
name: string;
|
|
@@ -22,11 +27,29 @@ export interface LoopHit {
|
|
|
22
27
|
* Arrays preserve their order (positional). undefined / primitives passthrough.
|
|
23
28
|
*/
|
|
24
29
|
export declare function stableStringify(value: unknown): string;
|
|
30
|
+
/**
|
|
31
|
+
* The primary file path a tool call targets, mirroring summarizeToolArgs' field
|
|
32
|
+
* precedence. Returns null when the call names no path (e.g. bash, or a grep
|
|
33
|
+
* with only a pattern) so such calls never participate in path detection.
|
|
34
|
+
*/
|
|
35
|
+
export declare function primaryPath(args: unknown): string | null;
|
|
25
36
|
export declare class LoopDetector {
|
|
26
37
|
private readonly window;
|
|
27
38
|
private readonly threshold;
|
|
39
|
+
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
40
|
+
private readonly pathThreshold;
|
|
28
41
|
private readonly buf;
|
|
29
|
-
constructor(window?: number, threshold?: number
|
|
30
|
-
/**
|
|
42
|
+
constructor(window?: number, threshold?: number,
|
|
43
|
+
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
44
|
+
pathThreshold?: number);
|
|
45
|
+
/** Record a tool call. Returns LoopHit if either threshold is breached, else null. */
|
|
31
46
|
record(call: ToolCall): LoopHit | null;
|
|
47
|
+
/**
|
|
48
|
+
* Count same-path calls in the window that are "revisits" — accesses whose
|
|
49
|
+
* offset does not advance past the furthest offset already seen for that
|
|
50
|
+
* path. Linear paging (strictly increasing offsets) yields zero revisits and
|
|
51
|
+
* never trips; repeated whole-file re-reads (offset 0 each time), backward
|
|
52
|
+
* jumps, and path-targeting greps (offset 0) accumulate.
|
|
53
|
+
*/
|
|
54
|
+
private countRevisits;
|
|
32
55
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure-logic loop detector for pi-task sub-agent phases.
|
|
3
3
|
*
|
|
4
|
-
* Watches a ring buffer of recent tool
|
|
5
|
-
* `(toolName, stable-stringified args)` key appears `threshold` times
|
|
6
|
-
* within the last `window` events, returns a LoopHit so the caller can
|
|
7
|
-
* kill the child and re-spawn with a hint.
|
|
4
|
+
* Watches a ring buffer of recent tool calls and trips on two thrash patterns:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
6
|
+
* 1. EXACT repeats — the same `(toolName, stable-stringified args)` key appears
|
|
7
|
+
* `threshold` times within the last `window` events.
|
|
8
|
+
* 2. PATH revisits — the same primary file path is re-targeted `pathThreshold`
|
|
9
|
+
* times within the window WITHOUT the read offset advancing, which the exact
|
|
10
|
+
* key misses when `offset/limit` vary call-to-call. Strictly-forward paging
|
|
11
|
+
* never trips (see countRevisits).
|
|
12
|
+
*
|
|
13
|
+
* Either pattern returns a LoopHit so the caller can kill the child and re-spawn
|
|
14
|
+
* with a hint. No I/O. No imports from index.ts. Trivially unit-testable.
|
|
10
15
|
*/
|
|
11
16
|
/**
|
|
12
17
|
* JSON.stringify with sorted object keys so {a:1,b:2} and {b:2,a:1} hash equal.
|
|
@@ -23,24 +28,86 @@ export function stableStringify(value) {
|
|
|
23
28
|
return sorted;
|
|
24
29
|
});
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The primary file path a tool call targets, mirroring summarizeToolArgs' field
|
|
33
|
+
* precedence. Returns null when the call names no path (e.g. bash, or a grep
|
|
34
|
+
* with only a pattern) so such calls never participate in path detection.
|
|
35
|
+
*/
|
|
36
|
+
export function primaryPath(args) {
|
|
37
|
+
if (!args || typeof args !== 'object')
|
|
38
|
+
return null;
|
|
39
|
+
const a = args;
|
|
40
|
+
if (typeof a.file_path === 'string')
|
|
41
|
+
return a.file_path;
|
|
42
|
+
if (typeof a.path === 'string')
|
|
43
|
+
return a.path;
|
|
44
|
+
if (typeof a.filePath === 'string')
|
|
45
|
+
return a.filePath;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** Numeric read offset (0 when absent/non-numeric) — drives progress vs revisit. */
|
|
49
|
+
function readOffset(args) {
|
|
50
|
+
if (!args || typeof args !== 'object')
|
|
51
|
+
return 0;
|
|
52
|
+
const o = args.offset;
|
|
53
|
+
return typeof o === 'number' && Number.isFinite(o) ? o : 0;
|
|
54
|
+
}
|
|
26
55
|
export class LoopDetector {
|
|
27
56
|
window;
|
|
28
57
|
threshold;
|
|
58
|
+
pathThreshold;
|
|
29
59
|
buf = [];
|
|
30
|
-
constructor(window = 20, threshold = 5
|
|
60
|
+
constructor(window = 20, threshold = 5,
|
|
61
|
+
/** Revisits of one path needed to trip; defaults to the exact threshold. */
|
|
62
|
+
pathThreshold = threshold) {
|
|
31
63
|
this.window = window;
|
|
32
64
|
this.threshold = threshold;
|
|
65
|
+
this.pathThreshold = pathThreshold;
|
|
33
66
|
}
|
|
34
|
-
/** Record a tool call. Returns LoopHit if
|
|
67
|
+
/** Record a tool call. Returns LoopHit if either threshold is breached, else null. */
|
|
35
68
|
record(call) {
|
|
36
69
|
const key = `${call.name}\x00${stableStringify(call.args)}`;
|
|
37
|
-
this.buf.push(key);
|
|
70
|
+
this.buf.push({ key, path: primaryPath(call.args), offset: readOffset(call.args) });
|
|
38
71
|
if (this.buf.length > this.window)
|
|
39
72
|
this.buf.shift();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
// 1. Exact-match loop: identical (name, args) repeated past threshold.
|
|
74
|
+
let exact = 0;
|
|
75
|
+
for (const e of this.buf)
|
|
76
|
+
if (e.key === key)
|
|
77
|
+
exact++;
|
|
78
|
+
if (exact >= this.threshold)
|
|
79
|
+
return { call, count: exact, windowSize: this.buf.length };
|
|
80
|
+
// 2. Path-aware loop: the same file re-targeted without forward progress.
|
|
81
|
+
// Caught here precisely because varied offset/limit make the exact key
|
|
82
|
+
// miss it (TASK_0017: auth.ts read/grepped ~12× with changing args until
|
|
83
|
+
// the wall-clock timeout, never tripping the exact detector).
|
|
84
|
+
const path = this.buf[this.buf.length - 1].path;
|
|
85
|
+
if (path !== null) {
|
|
86
|
+
const revisits = this.countRevisits(path);
|
|
87
|
+
if (revisits >= this.pathThreshold) {
|
|
88
|
+
return { call, count: revisits, windowSize: this.buf.length };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Count same-path calls in the window that are "revisits" — accesses whose
|
|
95
|
+
* offset does not advance past the furthest offset already seen for that
|
|
96
|
+
* path. Linear paging (strictly increasing offsets) yields zero revisits and
|
|
97
|
+
* never trips; repeated whole-file re-reads (offset 0 each time), backward
|
|
98
|
+
* jumps, and path-targeting greps (offset 0) accumulate.
|
|
99
|
+
*/
|
|
100
|
+
countRevisits(path) {
|
|
101
|
+
let maxOffset = -1;
|
|
102
|
+
let revisits = 0;
|
|
103
|
+
for (const e of this.buf) {
|
|
104
|
+
if (e.path !== path)
|
|
105
|
+
continue;
|
|
106
|
+
if (e.offset > maxOffset)
|
|
107
|
+
maxOffset = e.offset; // progress: new ground
|
|
108
|
+
else
|
|
109
|
+
revisits++; // revisit: already-covered ground
|
|
110
|
+
}
|
|
111
|
+
return revisits;
|
|
45
112
|
}
|
|
46
113
|
}
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -40,6 +40,22 @@ export declare function phaseVerifyTooling(deps: PhaseDeps, research: string): P
|
|
|
40
40
|
export interface PhaseResearchDeps extends ExternalContextDeps {
|
|
41
41
|
getFileInventory?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The TOOLING worker only needs to know which verification commands the task
|
|
45
|
+
* cares about — never the per-file edit list. Big refined prompts embed a long
|
|
46
|
+
* bulleted "fix these files" checklist *inside* the GOAL block; handing that to
|
|
47
|
+
* a weak local model drags it into reading/grepping source it doesn't need and
|
|
48
|
+
* it loops (TASK_0017: read(sql-adapter.ts) ×5 → loop-kill → fails the phase).
|
|
49
|
+
* So scope TOOLING's view to the GOAL prose, truncated at the first bullet.
|
|
50
|
+
*
|
|
51
|
+
* Fallbacks, in order: no bare `GOAL` header (free-form refined) → the whole
|
|
52
|
+
* refined unchanged; a GOAL block with no bullets → the full GOAL block. The
|
|
53
|
+
* GOAL boundary is the next bare ALL-CAPS header (CONSTRAINTS, KNOWN-UNKNOWNS,
|
|
54
|
+
* …) or end of text. Verified against every recorded refined prompt: the
|
|
55
|
+
* bullet-heavy failure case drops 3319→329 chars with zero source-file
|
|
56
|
+
* mentions; the rest are unchanged or only lightly trimmed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function scopedToolingGoal(refined: string): string;
|
|
43
59
|
export declare function phaseResearch(deps: PhaseDeps, refined: string, researchDeps?: PhaseResearchDeps): Promise<string>;
|
|
44
60
|
export interface PhaseAutoAnswerDeps {
|
|
45
61
|
docsFocused?: typeof docsFocused;
|
package/dist/task/phases.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getFileInventory } from './file-inventory.js';
|
|
|
12
12
|
import { formatServiceBlock, formatFreshnessSkippedBlock } from './service-blocks.js';
|
|
13
13
|
import { gatherExternalContext } from './external-context.js';
|
|
14
14
|
import { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, MAX_GRILL_QUESTIONS, appendNoThink } from './prompts.js';
|
|
15
|
-
import { setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
15
|
+
import { readSection, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
16
16
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
17
17
|
import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
|
|
18
18
|
import { parseGrillQuestions, parseAutoAnswer, autoAnswerHasTag, parseVerifyToolingOutput, deriveTitle } from './parsers.js';
|
|
@@ -79,6 +79,68 @@ export async function phaseVerifyTooling(deps, research) {
|
|
|
79
79
|
return replaceToolingWithVerified(research, parsed.verified);
|
|
80
80
|
}
|
|
81
81
|
const DOCS_EXTENSION_PATH = new URL('../workers/docs-extension.js', import.meta.url).pathname;
|
|
82
|
+
/**
|
|
83
|
+
* Task-file heading under which a research worker's validated output is cached.
|
|
84
|
+
* A resumed research phase reads these to skip workers that already succeeded,
|
|
85
|
+
* instead of re-running all four from scratch when one of them fails — the
|
|
86
|
+
* expensive case (e.g. 3 healthy workers thrown away because the 4th looped).
|
|
87
|
+
*/
|
|
88
|
+
function researchWorkerCacheHeading(section) {
|
|
89
|
+
return `research worker ${section}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The TOOLING worker only needs to know which verification commands the task
|
|
93
|
+
* cares about — never the per-file edit list. Big refined prompts embed a long
|
|
94
|
+
* bulleted "fix these files" checklist *inside* the GOAL block; handing that to
|
|
95
|
+
* a weak local model drags it into reading/grepping source it doesn't need and
|
|
96
|
+
* it loops (TASK_0017: read(sql-adapter.ts) ×5 → loop-kill → fails the phase).
|
|
97
|
+
* So scope TOOLING's view to the GOAL prose, truncated at the first bullet.
|
|
98
|
+
*
|
|
99
|
+
* Fallbacks, in order: no bare `GOAL` header (free-form refined) → the whole
|
|
100
|
+
* refined unchanged; a GOAL block with no bullets → the full GOAL block. The
|
|
101
|
+
* GOAL boundary is the next bare ALL-CAPS header (CONSTRAINTS, KNOWN-UNKNOWNS,
|
|
102
|
+
* …) or end of text. Verified against every recorded refined prompt: the
|
|
103
|
+
* bullet-heavy failure case drops 3319→329 chars with zero source-file
|
|
104
|
+
* mentions; the rest are unchanged or only lightly trimmed.
|
|
105
|
+
*/
|
|
106
|
+
export function scopedToolingGoal(refined) {
|
|
107
|
+
const m = /^GOAL[ \t]*\n([\s\S]*?)(?=\n[A-Z][A-Z][A-Z -]*[ \t]*\n|$(?![\s\S]))/m.exec(refined);
|
|
108
|
+
if (!m)
|
|
109
|
+
return refined;
|
|
110
|
+
const goal = m[1].trim();
|
|
111
|
+
const firstBullet = goal.search(/\n[ \t]*[-*]\s/);
|
|
112
|
+
return firstBullet === -1 ? goal : goal.slice(0, firstBullet).trim();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Throw a descriptive error if a research worker's result is not trustworthy
|
|
116
|
+
* output, so only good output is ever cached and assembled. Precedence mirrors
|
|
117
|
+
* the original inline checks: loop/timeout exhaustion first (a loop-kill is a
|
|
118
|
+
* SIGTERM/exit 143 OR a clean exit 0 with partial text, so exitCode alone is
|
|
119
|
+
* ambiguous — the worker already burned its MAX_LOOP_RESTARTS restarts before
|
|
120
|
+
* setting these), then exit code, empty output, and finally a leaked
|
|
121
|
+
* (never-executed) tool call.
|
|
122
|
+
*/
|
|
123
|
+
function assertResearchWorkerOk(name, result) {
|
|
124
|
+
if (result.loopHit) {
|
|
125
|
+
const argsStr = JSON.stringify(result.loopHit.call.args);
|
|
126
|
+
throw new Error(`Research ${name} worker stuck in a loop — called `
|
|
127
|
+
+ `${result.loopHit.call.name}(${argsStr}) ×${result.loopHit.count} in the last `
|
|
128
|
+
+ `${result.loopHit.windowSize} calls and still looped after restarts`);
|
|
129
|
+
}
|
|
130
|
+
if (result.timedOut) {
|
|
131
|
+
throw new Error(`Research ${name} worker timed out after restarts`);
|
|
132
|
+
}
|
|
133
|
+
if (result.exitCode !== 0) {
|
|
134
|
+
throw new Error(`Research ${name} worker failed (exit ${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
135
|
+
}
|
|
136
|
+
if (result.text.trim().length === 0) {
|
|
137
|
+
throw new Error(`Research ${name} worker produced no output`);
|
|
138
|
+
}
|
|
139
|
+
if (result.leakedToolCall) {
|
|
140
|
+
throw new Error(`Research ${name} worker wrote a tool call as text instead of invoking it `
|
|
141
|
+
+ `(${result.leakedToolCall.trim()}) — it never ran`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
82
144
|
export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
83
145
|
const fileInventoryFn = researchDeps.getFileInventory ?? getFileInventory;
|
|
84
146
|
const externalContext = await gatherExternalContext(refined, deps, researchDeps);
|
|
@@ -152,11 +214,27 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
152
214
|
{
|
|
153
215
|
section: 'TOOLING',
|
|
154
216
|
label: 'worker:tooling',
|
|
155
|
-
|
|
217
|
+
// Scope to the goal prose only — not the per-file edit checklist that
|
|
218
|
+
// makes a weak model spelunk source and loop. See scopedToolingGoal.
|
|
219
|
+
prompt: appendNoThink(promptHeader + RESEARCH_TOOLING_PROMPT(scopedToolingGoal(refined)))
|
|
156
220
|
}
|
|
157
221
|
];
|
|
158
|
-
|
|
222
|
+
// Run workers one at a time, persisting each worker's validated output the
|
|
223
|
+
// moment it succeeds. On a resume, a worker whose cached output is already on
|
|
224
|
+
// disk is skipped — so when one worker fails and the phase is re-run, the
|
|
225
|
+
// others don't burn minutes regenerating work that was already good. Each
|
|
226
|
+
// worker is validated inline (not in a second pass) so a failure throws
|
|
227
|
+
// before later workers run, and only trustworthy text is ever cached.
|
|
228
|
+
const sections = [];
|
|
159
229
|
for (const spec of workerSpecs) {
|
|
230
|
+
const cacheHeading = researchWorkerCacheHeading(spec.section);
|
|
231
|
+
const cached = (await readSection(deps.cwd, deps.taskId, cacheHeading)) ?? '';
|
|
232
|
+
if (cached.trim().length > 0) {
|
|
233
|
+
deps.logDebug?.(`${spec.label}: cached — skipping re-run`);
|
|
234
|
+
updateProgress();
|
|
235
|
+
sections.push({ name: spec.section, text: cached.trim() });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
160
238
|
deps.logDebug?.(`${spec.label}: start`);
|
|
161
239
|
const r = await recordWorker(spec.label, runWorker({
|
|
162
240
|
prompt: spec.prompt,
|
|
@@ -174,39 +252,20 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
174
252
|
+ (r.stderr ? ` stderr=${r.stderr.slice(0, 300)}` : '')
|
|
175
253
|
+ (r.leakedToolCall ? ` leaked=${r.leakedToolCall.trim().slice(0, 80)}` : ''));
|
|
176
254
|
updateProgress();
|
|
177
|
-
|
|
255
|
+
// Throws (failing the phase) on a loop/timeout/exit/empty/leak — so the
|
|
256
|
+
// workers that already succeeded above stay cached for the resume.
|
|
257
|
+
assertResearchWorkerOk(spec.section, r);
|
|
258
|
+
await setTaskSection(deps.cwd, deps.taskId, cacheHeading, r.text);
|
|
259
|
+
sections.push({ name: spec.section, text: r.text.trim() });
|
|
178
260
|
}
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
for (const
|
|
184
|
-
|
|
185
|
-
// a loop-kill arrives as a SIGTERM (exit 143) AND sometimes as a clean
|
|
186
|
-
// exit 0 with partial text, so keying off exitCode alone would either give
|
|
187
|
-
// a useless "exit 143" message or silently accept truncated output. The
|
|
188
|
-
// worker already burned its MAX_LOOP_RESTARTS restarts before setting these.
|
|
189
|
-
if (result.loopHit) {
|
|
190
|
-
const argsStr = JSON.stringify(result.loopHit.call.args);
|
|
191
|
-
throw new Error(`Research ${name} worker stuck in a loop — called `
|
|
192
|
-
+ `${result.loopHit.call.name}(${argsStr}) ×${result.loopHit.count} in the last `
|
|
193
|
-
+ `${result.loopHit.windowSize} calls and still looped after restarts`);
|
|
194
|
-
}
|
|
195
|
-
if (result.timedOut) {
|
|
196
|
-
throw new Error(`Research ${name} worker timed out after restarts`);
|
|
197
|
-
}
|
|
198
|
-
if (result.exitCode !== 0) {
|
|
199
|
-
throw new Error(`Research ${name} worker failed (exit ${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
200
|
-
}
|
|
201
|
-
if (result.text.trim().length === 0) {
|
|
202
|
-
throw new Error(`Research ${name} worker produced no output`);
|
|
203
|
-
}
|
|
204
|
-
if (result.leakedToolCall) {
|
|
205
|
-
throw new Error(`Research ${name} worker wrote a tool call as text instead of invoking it `
|
|
206
|
-
+ `(${result.leakedToolCall.trim()}) — it never ran`);
|
|
207
|
-
}
|
|
261
|
+
// All workers succeeded — the assembled output below becomes the canonical
|
|
262
|
+
// 'research' section (written by the orchestrator). The per-worker caches
|
|
263
|
+
// exist only to survive a mid-phase failure, so drop them now to avoid
|
|
264
|
+
// carrying every section twice in the task file.
|
|
265
|
+
for (const spec of workerSpecs) {
|
|
266
|
+
await removeTaskSection(deps.cwd, deps.taskId, researchWorkerCacheHeading(spec.section));
|
|
208
267
|
}
|
|
209
|
-
return sections.map(({ name,
|
|
268
|
+
return sections.map(({ name, text }) => `${name}\n${text}`).join('\n\n');
|
|
210
269
|
}
|
|
211
270
|
export async function phaseAutoAnswer(deps, refined, research, question, autoDeps = {}) {
|
|
212
271
|
const docsFocusedFn = autoDeps.docsFocused ?? docsFocused;
|
package/dist/task/task-io.d.ts
CHANGED
|
@@ -17,3 +17,5 @@ export declare function writeTaskFile(cwd: string, fm: TaskFrontMatter, body: st
|
|
|
17
17
|
export declare function updateTaskFrontMatter(cwd: string, id: string, patch: Partial<TaskFrontMatter>): Promise<void>;
|
|
18
18
|
export declare function readSection(cwd: string, id: string, heading: string): Promise<string | null>;
|
|
19
19
|
export declare function setTaskSection(cwd: string, id: string, heading: string, content: string): Promise<void>;
|
|
20
|
+
/** Remove a section (heading + body) if present; a no-op when it's absent. */
|
|
21
|
+
export declare function removeTaskSection(cwd: string, id: string, heading: string): Promise<void>;
|
package/dist/task/task-io.js
CHANGED
|
@@ -76,3 +76,12 @@ export async function setTaskSection(cwd, id, heading, content) {
|
|
|
76
76
|
}
|
|
77
77
|
await writeTaskFile(cwd, { ...frontMatter, updated_at: new Date().toISOString() }, next);
|
|
78
78
|
}
|
|
79
|
+
/** Remove a section (heading + body) if present; a no-op when it's absent. */
|
|
80
|
+
export async function removeTaskSection(cwd, id, heading) {
|
|
81
|
+
const { frontMatter, body } = await readTaskFile(cwd, id);
|
|
82
|
+
const re = sectionRegex(heading);
|
|
83
|
+
if (!re.test(body))
|
|
84
|
+
return;
|
|
85
|
+
const next = body.replace(re, '');
|
|
86
|
+
await writeTaskFile(cwd, { ...frontMatter, updated_at: new Date().toISOString() }, next);
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.22",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|