@mjasnikovs/pi-task 0.21.10 → 0.23.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 +3 -1
- package/dist/config/config.d.ts +37 -0
- package/dist/config/config.js +22 -1
- package/dist/config/register.js +17 -1
- package/dist/shared/child-process.js +34 -0
- package/dist/task/auto-orchestrator.js +85 -31
- package/dist/task/auto-prompts.d.ts +6 -0
- package/dist/task/auto-prompts.js +6 -0
- package/dist/task/child-runner.d.ts +11 -2
- package/dist/task/debug-log.d.ts +34 -0
- package/dist/task/debug-log.js +87 -0
- package/dist/task/decompose-granularity.d.ts +98 -0
- package/dist/task/decompose-granularity.js +124 -0
- package/dist/task/gate-deps.js +16 -21
- package/dist/task/orchestrator.js +6 -3
- package/dist/task/phases.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
65
65
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
66
66
|
| `/task-auto-resume [--unattended]` | Resume the active `/task-auto` run at the next unfinished task. `--unattended` is the boot-hook form: in-flight runs only. |
|
|
67
67
|
| `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
|
|
68
|
-
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control, compress thinking, auto-commit, verify work, enforce guidelines, project tour, command timeout, stuck reply retry, and one `ext:` toggle per installed host extension. |
|
|
68
|
+
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control, compress thinking, auto-commit, verify work, enforce guidelines, project tour, command timeout, stuck reply retry, debug logs, and one `ext:` toggle per installed host extension. |
|
|
69
69
|
| `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
|
|
70
70
|
|
|
71
71
|
## The pipeline
|
|
@@ -183,6 +183,7 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
183
183
|
| **command timeout** | 15 min | Wall-clock ceiling on a **single** tool execution. Local models routinely run a command that never returns (a hung build, a dev server, a check with no timeout) and the run wedges until you abort by hand — pi's bash tool has an optional timeout with no default, so this is the missing one. One knob, two surfaces: in the main session the overrun call is cancelled (killing the tool's whole process tree) plus a reminder turn; in the verify/fix gate children the child is killed and re-spawned with a hint, halving the ceiling on repeat hangs. Choices: 5/10/15/30 min or **off** — off unguards both surfaces, gates included. |
|
|
184
184
|
| **stuck reply retry** | 10 min | Inactivity ceiling on the **model stream**. A hung or silently-dropped stream throws nothing at all, so neither the connection-error retry (it needs a reported error) nor the **command timeout** (tool calls only) nor the dead-backend stall guard (a reachable endpoint reads as proof of life) can see it — an mx5 run lost ~2.9h to three of them while the model server stayed healthy. Measured as time since the **last stream event of any kind**, so a slow model emitting one token every 30s is never touched, and it pauses while a tool runs. On expiry the main session aborts the turn (through the same channel the command watchdog uses) and posts a resume reminder; a child is killed and routed into the existing connection-error retry. Choices: 5/10/20/30 min or **off**. Keep it generous on local backends — prompt processing on a large context legitimately emits nothing for minutes. |
|
|
185
185
|
| **yolo mode** | off | **Unattended runs.** Wherever pi-task would stop and ask, it takes the option already marked RECOMMENDED, stamps the artifact `(YOLO)` so an audit can tell a machine decided, and shows no prompt at all — clarify/grill answers, the verify-FAIL picker (auto-**Accept**, recorded as a yolo debt), and the final-gate picker (autofix while the budget lasts, then leave the run FAILED). A question with no recommendation is **skipped**, never invented. For throwaway/test projects nobody is watching; a real run should decide these itself. |
|
|
186
|
+
| **debug logs** | events | How much of a run is written to `.pi-tasks/*-debug.log`. **`events`** keeps decisions and guard actions — which phase ran, why a worker was retried, what the git-state guard restored, what a write-capable child changed on disk, why a gate returned FAIL — a few lines per task. **`full`** adds every line the child model emitted and every tool result; that's ~85% of the bytes (a real 247 KB `verify-debug.log` is 1315 lines, 521 of them tool dumps) and is what you want while actively debugging. **`off`** writes nothing. Nothing in pi-task ever reads these files back, so the setting cannot change how a run behaves — only whether you can explain it afterwards, and a log not written can't be recovered later. |
|
|
186
187
|
| **ext: …** | all off | One toggle per installed host `pi` extension, loading it into every child session by explicit path. Children otherwise run with extensions off, so a provider registered by an extension (e.g. `pi-lmstudio`) doesn't exist in them and they can't resolve the default model. Children also inherit the extension's tools and hooks, so only enable ones you trust. The list is strictly additive (discovery stays off), and an entry whose file is gone is skipped at spawn time, never fatal. |
|
|
187
188
|
|
|
188
189
|
## Configuration
|
|
@@ -195,6 +196,7 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
195
196
|
| `PI_REMOTE_PUSH_SUBJECT` | remote push | VAPID JWT `sub` contact. Defaults to the project URL; set your own `mailto:you@domain.com` or `https://…`. |
|
|
196
197
|
| `PI_REMOTE_PUSH_DEBUG` | remote push | When set (e.g. `1`), logs push delivery and push-service HTTP status. Off by default. |
|
|
197
198
|
| `PI_REMOTE_PUSH_LOG` | remote push | Path for the debug log (defaults to `/tmp/pi-task-push.log`). |
|
|
199
|
+
| `PI_TASK_DEBUG_LOG` | task trail | Overrides the **debug logs** setting for one session: `off`, `events`, or `full`. For reproducing a report without walking someone through `/task-config`. An unrecognised value is ignored, not treated as `off`. |
|
|
198
200
|
|
|
199
201
|
Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your `.gitignore` if you don't want them checked in.
|
|
200
202
|
|
package/dist/config/config.d.ts
CHANGED
|
@@ -96,7 +96,44 @@ export interface PiTaskConfig {
|
|
|
96
96
|
* DEFAULT OFF — this is never the behaviour of a normal, watched run.
|
|
97
97
|
*/
|
|
98
98
|
yoloMode: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* How much the run writes to its `.pi-tasks/*-debug.log` forensic trail
|
|
101
|
+
* (task/debug-log.ts). NOTHING in pi-task ever reads these files back —
|
|
102
|
+
* `task-io.ts` only globs `TASK_NNNN.md`, and auto-commit's trail snapshot
|
|
103
|
+
* copies bytes without parsing them — so this knob is behaviour-neutral by
|
|
104
|
+
* construction. It trades disk and repo noise against the ability to explain
|
|
105
|
+
* a run after it has finished.
|
|
106
|
+
*
|
|
107
|
+
* `full` is every line the child model emitted plus every tool result;
|
|
108
|
+
* `events` keeps only decisions and guard actions; `off` writes nothing.
|
|
109
|
+
*
|
|
110
|
+
* DEFAULT `events`, not `off`, because the two levels are not the same kind
|
|
111
|
+
* of record. Measured on a real 247 KB `verify-debug.log` (IAR1, 1315 lines):
|
|
112
|
+
* the child's own output and the `↳` tool dumps are 85% of the bytes, while
|
|
113
|
+
* the `=== … ===` markers are 15% — and that 15% is the ONLY record of what
|
|
114
|
+
* the guards did (`GIT-STATE GUARD — child mutated graded state`, the
|
|
115
|
+
* write-capable child's `tree changes`, the FAIL reason). mx5 run 11's
|
|
116
|
+
* final-fix child deleted a source file; the tree-changes line is why that
|
|
117
|
+
* was findable at all. Silencing the chatter costs nothing; silencing the
|
|
118
|
+
* guard record makes the next incident unreconstructible, and a debug log
|
|
119
|
+
* cannot be recovered after the fact.
|
|
120
|
+
*/
|
|
121
|
+
debugLogs: DebugLogLevel;
|
|
99
122
|
}
|
|
123
|
+
/** How verbose the `.pi-tasks/*-debug.log` trail is. See {@link PiTaskConfig.debugLogs}. */
|
|
124
|
+
export type DebugLogLevel = 'off' | 'events' | 'full';
|
|
125
|
+
/**
|
|
126
|
+
* The debug-log choices offered by /task-config, in cycle order (quietest →
|
|
127
|
+
* loudest, so the cycle reads as a volume dial). Unlike the timeout options the
|
|
128
|
+
* stored value IS the label — the level is already a word.
|
|
129
|
+
*/
|
|
130
|
+
export declare const DEBUG_LOG_OPTIONS: readonly DebugLogLevel[];
|
|
131
|
+
/**
|
|
132
|
+
* Same pinning as the timeout sanitizers: a hand-edited `"debugLogs": true` or a
|
|
133
|
+
* level from a future version must not reach the writer as an unknown string —
|
|
134
|
+
* it falls back to the default rather than silently disabling the trail.
|
|
135
|
+
*/
|
|
136
|
+
export declare function sanitizeDebugLogs(value: unknown): DebugLogLevel;
|
|
100
137
|
/**
|
|
101
138
|
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
|
102
139
|
* the cycle order the picker shows. The stored config value is the ms number;
|
package/dist/config/config.js
CHANGED
|
@@ -4,6 +4,23 @@ import * as path from 'node:path';
|
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import { isSearchProvider } from '../workers/search-types.js';
|
|
6
6
|
import { DEFAULT_STREAM_INACTIVITY_MS } from '../shared/stream-watchdog.js';
|
|
7
|
+
/**
|
|
8
|
+
* The debug-log choices offered by /task-config, in cycle order (quietest →
|
|
9
|
+
* loudest, so the cycle reads as a volume dial). Unlike the timeout options the
|
|
10
|
+
* stored value IS the label — the level is already a word.
|
|
11
|
+
*/
|
|
12
|
+
export const DEBUG_LOG_OPTIONS = ['off', 'events', 'full'];
|
|
13
|
+
const DEFAULT_DEBUG_LOGS = 'events';
|
|
14
|
+
/**
|
|
15
|
+
* Same pinning as the timeout sanitizers: a hand-edited `"debugLogs": true` or a
|
|
16
|
+
* level from a future version must not reach the writer as an unknown string —
|
|
17
|
+
* it falls back to the default rather than silently disabling the trail.
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeDebugLogs(value) {
|
|
20
|
+
return DEBUG_LOG_OPTIONS.includes(value) ?
|
|
21
|
+
value
|
|
22
|
+
: DEFAULT_DEBUG_LOGS;
|
|
23
|
+
}
|
|
7
24
|
/**
|
|
8
25
|
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
|
9
26
|
* the cycle order the picker shows. The stored config value is the ms number;
|
|
@@ -61,7 +78,10 @@ const DEFAULTS = {
|
|
|
61
78
|
requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
|
|
62
79
|
streamInactivityMs: DEFAULT_STREAM_INACTIVITY_MS,
|
|
63
80
|
// OFF: auto-answering is for unattended throwaway runs only.
|
|
64
|
-
yoloMode: false
|
|
81
|
+
yoloMode: false,
|
|
82
|
+
// EVENTS: the model chatter is 85% of the bytes and nobody reads it; the
|
|
83
|
+
// guard/verdict markers are the 15% that explains a failed run.
|
|
84
|
+
debugLogs: DEFAULT_DEBUG_LOGS
|
|
65
85
|
};
|
|
66
86
|
/**
|
|
67
87
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
@@ -96,6 +116,7 @@ if (!G.loaded) {
|
|
|
96
116
|
// boolean counts; anything else falls back to the OFF default.
|
|
97
117
|
if (typeof parsed.yoloMode !== 'boolean')
|
|
98
118
|
delete parsed.yoloMode;
|
|
119
|
+
parsed.debugLogs = sanitizeDebugLogs(parsed.debugLogs);
|
|
99
120
|
G.config = { ...DEFAULTS, ...parsed };
|
|
100
121
|
}
|
|
101
122
|
catch {
|
package/dist/config/register.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SettingsList, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi
|
|
|
2
2
|
import { registerBridgeCommand } from '../remote/bridge.js';
|
|
3
3
|
import { readPkgVersion } from '../shared/pkg-version.js';
|
|
4
4
|
import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
|
|
5
|
-
import { COMMAND_TIMEOUT_OPTIONS, getConfig, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
|
|
5
|
+
import { COMMAND_TIMEOUT_OPTIONS, DEBUG_LOG_OPTIONS, getConfig, sanitizeDebugLogs, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
|
|
6
6
|
import { listInstalledExtensions } from './extension-list.js';
|
|
7
7
|
// Version in the title so a bug report or screenshot says which build it came
|
|
8
8
|
// from without anyone having to go look it up.
|
|
@@ -160,6 +160,16 @@ const ITEMS = [
|
|
|
160
160
|
+ 'check is accepted and written down as debt, and a failed final check is retried '
|
|
161
161
|
+ 'until the budget runs out. Each auto-answer is marked (YOLO) in the task file. '
|
|
162
162
|
+ 'For throwaway projects you are not watching'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'debugLogs',
|
|
166
|
+
label: 'debug logs',
|
|
167
|
+
description: 'How much of a run gets written to .pi-tasks/*-debug.log. "events" keeps the '
|
|
168
|
+
+ 'decisions and the guard actions — what a checking step changed, why something '
|
|
169
|
+
+ 'failed — a few lines per task. "full" adds everything the model said and every '
|
|
170
|
+
+ 'command it ran, which is most of the size and only useful while you are digging '
|
|
171
|
+
+ 'into a problem. "off" writes nothing, and nothing can be reconstructed later',
|
|
172
|
+
values: [...DEBUG_LOG_OPTIONS]
|
|
163
173
|
}
|
|
164
174
|
];
|
|
165
175
|
/** Human label for the stored command-timeout ms (falls back to the raw ms). */
|
|
@@ -295,6 +305,12 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
295
305
|
if (opt)
|
|
296
306
|
cfg.streamInactivityMs = opt.ms;
|
|
297
307
|
}
|
|
308
|
+
else if (id === 'debugLogs') {
|
|
309
|
+
// The stored value IS the label here, but it must still go
|
|
310
|
+
// through the sanitizer — the generic `else` below would
|
|
311
|
+
// write the boolean `newValue === 'on'` into an enum field.
|
|
312
|
+
cfg.debugLogs = sanitizeDebugLogs(newValue);
|
|
313
|
+
}
|
|
298
314
|
else {
|
|
299
315
|
;
|
|
300
316
|
cfg[id] = newValue === 'on';
|
|
@@ -335,6 +335,40 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
335
335
|
else
|
|
336
336
|
stdout += chunk;
|
|
337
337
|
});
|
|
338
|
+
// This accumulation is deliberately unbounded, unlike `discardStdout`
|
|
339
|
+
// above. The asymmetry was investigated on 2026-07-28 and the "stderr can
|
|
340
|
+
// overflow V8's max string length" hypothesis is REFUTED at ~5 orders of
|
|
341
|
+
// magnitude — do not re-open it without new volume evidence.
|
|
342
|
+
//
|
|
343
|
+
// Measured ceiling (binary search on `'a'.repeat`, not cited from memory):
|
|
344
|
+
// node v26.2.0 536,870,888 chars (512 MiB, 2^29-24), bun 1.3.14
|
|
345
|
+
// 2,147,483,647 (2 GiB). Both throw RangeError, not OOM — node's default
|
|
346
|
+
// heap is 4192 MiB, well clear of its own string ceiling. If it were ever
|
|
347
|
+
// reached the severity would be high: a throw in this handler escapes as
|
|
348
|
+
// an uncaughtException and the enclosing promise never settles (measured),
|
|
349
|
+
// so it would kill the host, not fail the child.
|
|
350
|
+
//
|
|
351
|
+
// Measured volume, real runs not synthetic: the largest single tool output
|
|
352
|
+
// in a full mx5 run is 5,043 bytes and that whole run's verify-debug.log is
|
|
353
|
+
// 112,216 bytes. The heaviest commands in the system are far under —
|
|
354
|
+
// pi-task's own 2,452-test suite emits 162 bytes of stderr, mx5's
|
|
355
|
+
// prettier+eslint+tsc lint 90, a cached `bun install` 0. Observed max is
|
|
356
|
+
// ~106,000x below the node ceiling; reaching it inside the 15-min command
|
|
357
|
+
// bound would need ~596 KB/s sustained on stderr for the full window.
|
|
358
|
+
//
|
|
359
|
+
// The structural reason is the population of children that reach here:
|
|
360
|
+
// `pi` model children (stderr near-empty), `git`, and one
|
|
361
|
+
// `npm install --loglevel=error`. Genuinely verbose work — test runners,
|
|
362
|
+
// builds, boot probes — does not use runChild; final-gate.ts spawns its own
|
|
363
|
+
// and already caps at 8000 chars. So `discardStdout`'s single caller
|
|
364
|
+
// (docs-core.ts) is not an oversight to mirror: it discards the verbose
|
|
365
|
+
// stream and KEEPS stderr precisely because stderr is that call's payload.
|
|
366
|
+
//
|
|
367
|
+
// A symmetric `discardStderr` would therefore have no caller, and a naive
|
|
368
|
+
// cap here would be actively unsafe: consumers disagree about which end
|
|
369
|
+
// carries the cause — phases.ts:474 takes the tail (-500), phases.ts:936
|
|
370
|
+
// takes the head (0, 300), and child-runner.ts:172 feeds the whole string
|
|
371
|
+
// into failure classification.
|
|
338
372
|
proc.stderr?.on('data', (d) => {
|
|
339
373
|
lastActivity = Date.now();
|
|
340
374
|
streamWatch?.note();
|
|
@@ -38,10 +38,12 @@ import { describeDebt, recordFinalGateUnobservedDebt } from './accept-debt.js';
|
|
|
38
38
|
import { applyDemotions, isNonProgress, normalizeFailureDetail, rankedFirstFailure, unobservedDebtReason } from './final-gate-progress.js';
|
|
39
39
|
import { classifyFinalGateAnswer, MAX_FINAL_GATE_AUTOFIX, FINAL_LEAVE_LABEL, FINAL_LEAVE_VALUE, FINAL_ACCEPT_LABEL, FINAL_ACCEPT_VALUE, FINAL_AUTOFIX_LABEL, FINAL_AUTOFIX_VALUE, STRANDED_FIX_COMMIT, strandedFixNote } from './final-gate-fix.js';
|
|
40
40
|
import { getConfig } from '../config/config.js';
|
|
41
|
+
import { debugLogLevel, shouldLogDebug } from './debug-log.js';
|
|
41
42
|
import { isYoloMode, yoloPickAnswer, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
|
|
42
43
|
import { configureResearchRun, resumeResearchRun } from '../workers/research-cache.js';
|
|
43
44
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
44
45
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
46
|
+
import { granularityFloor, granularitySplitHint, isPlanShapeQuestion, isTooCoarse, planShapeIsHostsToAnswer, PLAN_SHAPE_ANSWER } from './decompose-granularity.js';
|
|
45
47
|
import { mandatesTestsInSameChange, rewriteBatchTestPlan } from './batch-test-task.js';
|
|
46
48
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, writeOwnedRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
47
49
|
import { decideAdoption, groundedCoverage } from './coverage-loop.js';
|
|
@@ -121,8 +123,16 @@ function mentionPath(token) {
|
|
|
121
123
|
* before any task file — hence any per-task `TASK_XXXX-debug.log` — exists. Writes
|
|
122
124
|
* to `.pi-tasks/plan-debug.log`; the `*-debug.log` suffix keeps it grep-compatible
|
|
123
125
|
* with the per-task logs. Never throws (mkdir + append are best-effort).
|
|
126
|
+
*
|
|
127
|
+
* Every call site here records a plan DECISION (how many titles a round produced,
|
|
128
|
+
* whether a retry was adopted, which clarify answer was auto-resolved), so all of
|
|
129
|
+
* them are `'event'` — this file carries no model chatter and survives at the
|
|
130
|
+
* default level. It is also the only channel the plan phase has: it runs before
|
|
131
|
+
* any task file, hence any `TASK_NNNN-debug.log`, exists.
|
|
124
132
|
*/
|
|
125
133
|
function logPlanDebug(cwd, msg) {
|
|
134
|
+
if (!shouldLogDebug('event', debugLogLevel()))
|
|
135
|
+
return;
|
|
126
136
|
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
127
137
|
const dir = tasksDir(cwd);
|
|
128
138
|
fsp.mkdir(dir, { recursive: true })
|
|
@@ -424,6 +434,54 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
424
434
|
taskId: '',
|
|
425
435
|
signal: new AbortController().signal
|
|
426
436
|
}).catch(() => '');
|
|
437
|
+
// Requirement extraction (mx5 run 11, goal A): grounded requirement units,
|
|
438
|
+
// extracted from whatever structure the spec has, BEFORE decompose — they ride
|
|
439
|
+
// into the decompose prompt as a ledger (structure-mirroring can't discharge
|
|
440
|
+
// them) and drive the per-requirement coverage accounting below.
|
|
441
|
+
//
|
|
442
|
+
// Runs BEFORE clarify (it depends only on the inlined feature, never on the
|
|
443
|
+
// answers) so the plan-shape gate below has a real count to judge with: the
|
|
444
|
+
// host must not seize the granularity fork on a spec that has no breakdown to
|
|
445
|
+
// speak of. Best-effort:
|
|
446
|
+
// a fault leaves reqEntries empty and the whole channel degrades to the old
|
|
447
|
+
// behavior (one-liners / doc-less features naturally yield few or none).
|
|
448
|
+
let reqEntries = [];
|
|
449
|
+
try {
|
|
450
|
+
// Recall floor: the obligation-marked passages ride into the prompt as a
|
|
451
|
+
// checklist, and a marked passage that produced NO quote is hard evidence
|
|
452
|
+
// for one forced re-extraction (measured live: 1/5 extractions missed the
|
|
453
|
+
// entire marked testing section without this).
|
|
454
|
+
const passages = enumerateObligationPassages(featureForModel);
|
|
455
|
+
const extractOnce = async (hint) => keepGroundedRequirements(parseRequirementLines(await deps.runChild('requirement-extract', '', prependHint(hint, REQUIREMENT_EXTRACT_PROMPT(featureForModel, passages)))), featureForModel);
|
|
456
|
+
reqEntries = await extractOnce(null);
|
|
457
|
+
const uncovered = uncoveredPassages(passages, reqEntries);
|
|
458
|
+
if (uncovered.length > 0) {
|
|
459
|
+
logPlanDebug(cwd, `requirement extraction: ${uncovered.length} obligation-marked passage(s) `
|
|
460
|
+
+ 'uncovered — forcing one re-extraction');
|
|
461
|
+
const retry = await extractOnce(extractionRetryHint(uncovered));
|
|
462
|
+
// Union of both grounded passes (keepGrounded dedupes).
|
|
463
|
+
reqEntries = keepGroundedRequirements([...reqEntries, ...retry], featureForModel);
|
|
464
|
+
}
|
|
465
|
+
// Bound with marked-passage priority — a plain first-N cap truncates the
|
|
466
|
+
// doc's tail sections (measured live: an eager model fills 40 top-down).
|
|
467
|
+
reqEntries = capRequirements(reqEntries, passages, featureForModel);
|
|
468
|
+
logPlanDebug(cwd, `requirement extraction: ${reqEntries.length} grounded requirement(s) kept`);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// best-effort channel
|
|
472
|
+
}
|
|
473
|
+
// Granularity floor (mx5 Jul 25 vs Jul 27): the plan's task COUNT was being set
|
|
474
|
+
// by an auto-resolved clarify line the user never saw — the same spec planned
|
|
475
|
+
// into 41 tasks one day and 11 the next, with identical code. Derive the floor
|
|
476
|
+
// from the requirements a task can own, so an unreviewable "one task per
|
|
477
|
+
// milestone" decision cannot collapse the plan; it also gates whether the
|
|
478
|
+
// plan-shape fork below is the host's to answer at all. 0 ownable ⇒ no channel.
|
|
479
|
+
const ownableRequirements = reqEntries.filter(e => !isCrossCuttingRequirement(e.quote)).length;
|
|
480
|
+
const coarseFloor = granularityFloor(ownableRequirements);
|
|
481
|
+
if (coarseFloor > 0) {
|
|
482
|
+
logPlanDebug(cwd, `granularity floor: ${ownableRequirements} ownable requirement(s) ⇒ at least `
|
|
483
|
+
+ `${coarseFloor} task(s)`);
|
|
484
|
+
}
|
|
427
485
|
const answers = [];
|
|
428
486
|
// Plain text of every question already shown, for the duplicate backstop.
|
|
429
487
|
const askedQuestions = [];
|
|
@@ -458,6 +516,19 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
458
516
|
const shownQ = renderInlineMarkdown(question, theme);
|
|
459
517
|
const plainQ = stripInlineMarkdown(question);
|
|
460
518
|
askedQuestions.push(plainQ);
|
|
519
|
+
// PLAN SHAPE is the host's call, not the triage's (mx5 41→11 tasks on the
|
|
520
|
+
// same spec, same base commit, same code — see decompose-granularity.ts).
|
|
521
|
+
// The triage answers this fork for itself in 8/8 live reps and stamps it
|
|
522
|
+
// "already settled by the spec" while the spec settles no such thing, so the
|
|
523
|
+
// single most load-bearing decision in a run was an invisible coin flip.
|
|
524
|
+
// Answer it deterministically instead: same channel, same transcript, but a
|
|
525
|
+
// fixed value the user can read in the AUTO file and override next run.
|
|
526
|
+
if (planShapeIsHostsToAnswer(ownableRequirements) && isPlanShapeQuestion(plainQ)) {
|
|
527
|
+
logPlanDebug(cwd, `plan-shape question answered host-side (not the triage): ${plainQ.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
528
|
+
answers.push(`Q${answers.length + 1}: ${plainQ}\n`
|
|
529
|
+
+ `A${answers.length + 1}: ${PLAN_SHAPE_ANSWER} (host-set — plan granularity is not left to chance)`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
461
532
|
// Answer-side triage (grill parity): if the inlined spec already settles
|
|
462
533
|
// this question, auto-resolve it and never show it. The resolved value is
|
|
463
534
|
// recorded so decompose sees the decision and the next gen call's priorQA
|
|
@@ -544,37 +615,6 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
544
615
|
ctx.ui.notify('No clarifying questions needed — planning tasks…', 'info');
|
|
545
616
|
}
|
|
546
617
|
const clarifications = answers.join('\n');
|
|
547
|
-
// Requirement extraction (mx5 run 11, goal A): grounded requirement units,
|
|
548
|
-
// extracted from whatever structure the spec has, BEFORE decompose — they ride
|
|
549
|
-
// into the decompose prompt as a ledger (structure-mirroring can't discharge
|
|
550
|
-
// them) and drive the per-requirement coverage accounting below. Best-effort:
|
|
551
|
-
// a fault leaves reqEntries empty and the whole channel degrades to the old
|
|
552
|
-
// behavior (one-liners / doc-less features naturally yield few or none).
|
|
553
|
-
let reqEntries = [];
|
|
554
|
-
try {
|
|
555
|
-
// Recall floor: the obligation-marked passages ride into the prompt as a
|
|
556
|
-
// checklist, and a marked passage that produced NO quote is hard evidence
|
|
557
|
-
// for one forced re-extraction (measured live: 1/5 extractions missed the
|
|
558
|
-
// entire marked testing section without this).
|
|
559
|
-
const passages = enumerateObligationPassages(featureForModel);
|
|
560
|
-
const extractOnce = async (hint) => keepGroundedRequirements(parseRequirementLines(await deps.runChild('requirement-extract', '', prependHint(hint, REQUIREMENT_EXTRACT_PROMPT(featureForModel, passages)))), featureForModel);
|
|
561
|
-
reqEntries = await extractOnce(null);
|
|
562
|
-
const uncovered = uncoveredPassages(passages, reqEntries);
|
|
563
|
-
if (uncovered.length > 0) {
|
|
564
|
-
logPlanDebug(cwd, `requirement extraction: ${uncovered.length} obligation-marked passage(s) `
|
|
565
|
-
+ 'uncovered — forcing one re-extraction');
|
|
566
|
-
const retry = await extractOnce(extractionRetryHint(uncovered));
|
|
567
|
-
// Union of both grounded passes (keepGrounded dedupes).
|
|
568
|
-
reqEntries = keepGroundedRequirements([...reqEntries, ...retry], featureForModel);
|
|
569
|
-
}
|
|
570
|
-
// Bound with marked-passage priority — a plain first-N cap truncates the
|
|
571
|
-
// doc's tail sections (measured live: an eager model fills 40 top-down).
|
|
572
|
-
reqEntries = capRequirements(reqEntries, passages, featureForModel);
|
|
573
|
-
logPlanDebug(cwd, `requirement extraction: ${reqEntries.length} grounded requirement(s) kept`);
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
576
|
-
// best-effort channel
|
|
577
|
-
}
|
|
578
618
|
// Artifact-production closure, plan side (mx5 run 13, PROMPT 2): runtime
|
|
579
619
|
// files the spec REFERENCES (server snippets, prose "serve the built
|
|
580
620
|
// index.html") that neither its file tree, its parsed build outputs, nor the
|
|
@@ -636,6 +676,20 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
636
676
|
const listRaw = await deps.runChild('auto-decompose', 'read', decomposePrompt);
|
|
637
677
|
let planTitles = parsePlan(listRaw);
|
|
638
678
|
logPlanDebug(cwd, `decompose produced ${planTitles.length} title(s)`);
|
|
679
|
+
// BRACES for the floor: the prompt clause alone is a preference the model can
|
|
680
|
+
// ignore, so a plan under the floor is sent back ONCE to be split (never
|
|
681
|
+
// regenerated — a fresh roll can drop a covered area, mx5 run 12). Longer plan
|
|
682
|
+
// wins; a still-coarse plan falls through to the coverage judge as before, so
|
|
683
|
+
// this can never block planning.
|
|
684
|
+
if (isTooCoarse(planTitles.length, coarseFloor)) {
|
|
685
|
+
logPlanDebug(cwd, `plan under the granularity floor (${planTitles.length} < ${coarseFloor}) — `
|
|
686
|
+
+ 'reprompting once to split');
|
|
687
|
+
const splitRaw = await deps.runChild('auto-decompose', 'read', prependHint(granularitySplitHint(planTitles.length, ownableRequirements), decomposePrompt));
|
|
688
|
+
const splitTitles = parsePlan(splitRaw);
|
|
689
|
+
logPlanDebug(cwd, `granularity split-retry produced ${splitTitles.length} title(s)`);
|
|
690
|
+
if (splitTitles.length > planTitles.length)
|
|
691
|
+
planTitles = splitTitles;
|
|
692
|
+
}
|
|
639
693
|
// Distrust floor (see isSuspectPlan): a ≤2-title plan for a multi-KB spec is
|
|
640
694
|
// regenerated once BEFORE the judge runs — the judge cannot be trusted to
|
|
641
695
|
// catch it (3/10 live false-pass) and a hinted retry heals it reliably
|
|
@@ -16,6 +16,12 @@ export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) =>
|
|
|
16
16
|
* `noBatchTests` adds the anti-batch-test rule (batch-test-task.ts) — emitted ONLY
|
|
17
17
|
* when the decisions mandate tests-in-the-same-change, so every other run sees the
|
|
18
18
|
* prompt it always saw. It is the belt; the host-side rewrite is the lever.
|
|
19
|
+
*
|
|
20
|
+
* Plan GRANULARITY is deliberately NOT a rule here — it rides in CLARIFICATIONS
|
|
21
|
+
* (decompose-granularity.ts). Live A/B: as a RULES line replacing "prefer a
|
|
22
|
+
* handful", it made plan size explode (81 and 85 titles for a spec whose healthy
|
|
23
|
+
* plan is ~30, one 120k-context blowup); as a clarification, with the counterweight
|
|
24
|
+
* below left intact, the same directive holds 20–39 across 16 reps.
|
|
19
25
|
*/
|
|
20
26
|
export declare const AUTO_DECOMPOSE_PROMPT: (feature: string, clarifications: string, requirementsLedger?: string, noBatchTests?: boolean) => string;
|
|
21
27
|
/**
|
|
@@ -76,6 +76,12 @@ NONE`;
|
|
|
76
76
|
* `noBatchTests` adds the anti-batch-test rule (batch-test-task.ts) — emitted ONLY
|
|
77
77
|
* when the decisions mandate tests-in-the-same-change, so every other run sees the
|
|
78
78
|
* prompt it always saw. It is the belt; the host-side rewrite is the lever.
|
|
79
|
+
*
|
|
80
|
+
* Plan GRANULARITY is deliberately NOT a rule here — it rides in CLARIFICATIONS
|
|
81
|
+
* (decompose-granularity.ts). Live A/B: as a RULES line replacing "prefer a
|
|
82
|
+
* handful", it made plan size explode (81 and 85 titles for a spec whose healthy
|
|
83
|
+
* plan is ~30, one 120k-context blowup); as a clarification, with the counterweight
|
|
84
|
+
* below left intact, the same directive holds 20–39 across 16 reps.
|
|
79
85
|
*/
|
|
80
86
|
export const AUTO_DECOMPOSE_PROMPT = (feature, clarifications, requirementsLedger = '', noBatchTests = false) => `Split this feature into an ordered list of implementation tasks. Each task
|
|
81
87
|
will be handed, by its title, to a separate pipeline that does its own research
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* for phase-level child pi invocations.
|
|
7
7
|
*/
|
|
8
8
|
import { type SpawnFn, type ContextSnapshot, type ToolCall, type LoopHit } from '../shared/child-process.js';
|
|
9
|
+
import type { DebugLine } from './debug-log.js';
|
|
9
10
|
export declare const LOOP_WINDOW = 20;
|
|
10
11
|
export declare const LOOP_THRESHOLD = 5;
|
|
11
12
|
export declare const MAX_LOOP_RESTARTS = 2;
|
|
@@ -44,8 +45,16 @@ interface PhaseDeps {
|
|
|
44
45
|
*/
|
|
45
46
|
recordSubStep?: (label: string, ms: number) => void;
|
|
46
47
|
spawn?: SpawnFn;
|
|
47
|
-
/**
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Write a timestamped line to the per-task debug log. Fire-and-forget, and
|
|
50
|
+
* UNSET entirely when the trail is off — so a caller must keep the `?.` and
|
|
51
|
+
* must not do work to build a message outside the call.
|
|
52
|
+
*
|
|
53
|
+
* `kind` defaults to `'event'` (a decision or guard action, kept at the
|
|
54
|
+
* default level). Pass `'stream'` for raw child output and tool results,
|
|
55
|
+
* which only the `full` level keeps. See debug-log.ts.
|
|
56
|
+
*/
|
|
57
|
+
logDebug?: (msg: string, kind?: DebugLine) => void;
|
|
49
58
|
/** Injectable delay for connection-error backoff; defaults to a real timer.
|
|
50
59
|
* Tests override it with a no-op so retries don't actually sleep. */
|
|
51
60
|
sleepFor?: (ms: number) => Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type DebugLogLevel } from '../config/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Escape hatch for reproducing a user's bug without walking them through
|
|
4
|
+
* /task-config. Follows the existing `PI_TASK_*` instrumentation convention
|
|
5
|
+
* (`PI_TASK_TYPEONLY_LOG`, `PI_REMOTE_PUSH_DEBUG`). An unrecognised value is
|
|
6
|
+
* ignored rather than treated as `off` — a typo in an env var must not silently
|
|
7
|
+
* throw the trail away.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEBUG_LOG_ENV = "PI_TASK_DEBUG_LOG";
|
|
10
|
+
/** A trail line's kind — see the module note. Producers default to `'event'`. */
|
|
11
|
+
export type DebugLine = 'event' | 'stream';
|
|
12
|
+
/**
|
|
13
|
+
* The level in force: env override first, then the saved config. Read per call
|
|
14
|
+
* rather than cached, so flipping the setting mid-run takes effect on the next
|
|
15
|
+
* line instead of at the next restart.
|
|
16
|
+
*/
|
|
17
|
+
export declare function debugLogLevel(getEnv?: (k: string) => string | undefined): DebugLogLevel;
|
|
18
|
+
/** Whether a line of this kind should be written at `level`. */
|
|
19
|
+
export declare function shouldLogDebug(kind: DebugLine, level: DebugLogLevel): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* A timestamped fire-and-forget appender for one trail file, level-gated.
|
|
22
|
+
*
|
|
23
|
+
* `kind` defaults to `'event'` so a new call site is quiet-by-default in the
|
|
24
|
+
* useful direction: forgetting to classify a marker keeps it in the audit trail,
|
|
25
|
+
* whereas forgetting to classify chatter would only make the log bigger. Errors
|
|
26
|
+
* are swallowed — an unwritable trail must never fail the run it is describing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function makeDebugAppender(logPath: string, appendFile?: (p: string, data: string) => Promise<unknown>): (msg: string, kind?: DebugLine) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a raw append in the level gate. Returns `undefined` when the level is
|
|
31
|
+
* `off`, so callers that hold an OPTIONAL `logDebug` leave it unset and every
|
|
32
|
+
* `logDebug?.(…)` in the pipeline short-circuits before it formats a string.
|
|
33
|
+
*/
|
|
34
|
+
export declare function gateDebugWriter(write: (msg: string) => void, getEnv?: (k: string) => string | undefined): ((msg: string, kind?: DebugLine) => void) | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that decides whether a `.pi-tasks/*-debug.log` line gets written.
|
|
3
|
+
*
|
|
4
|
+
* THE TRAIL IS WRITE-ONLY. Nothing in pi-task reads these files back: `task-io.ts`
|
|
5
|
+
* globs `TASK_NNNN.md` and skips everything else, and auto-commit's `snapshotTrail`
|
|
6
|
+
* copies the bytes across a `reset --hard` without ever parsing them. Every producer
|
|
7
|
+
* is a `logDebug?.(…)` / `log(…)` side effect whose return value is discarded. So
|
|
8
|
+
* this gate cannot change what a run DOES — only what it can explain afterwards.
|
|
9
|
+
*
|
|
10
|
+
* TWO KINDS OF LINE, and the distinction is the whole point of having three levels
|
|
11
|
+
* rather than a boolean:
|
|
12
|
+
*
|
|
13
|
+
* 'stream' — what the child model said, and what its tools returned. Reproducible
|
|
14
|
+
* by re-running, useful while you are actively debugging, and 85% of the
|
|
15
|
+
* bytes (measured: a 247 KB IAR1 `verify-debug.log` is 1315 lines, of
|
|
16
|
+
* which 521 are `↳` tool dumps and most of the rest is raw model text).
|
|
17
|
+
*
|
|
18
|
+
* 'event' — a decision or a guard action: which phase started, why a worker was
|
|
19
|
+
* retried or degraded, what the git-state guard restored, what a
|
|
20
|
+
* write-capable child changed on disk, why a gate returned FAIL. Ten-ish
|
|
21
|
+
* lines per task, and NOT reproducible — it is the only record that the
|
|
22
|
+
* guard fired at all. mx5 run 11's final-fix child deleted a source file
|
|
23
|
+
* and the `tree changes:` line is the reason anyone could tell.
|
|
24
|
+
*
|
|
25
|
+
* Hence the default is `events`, not `off`: the quiet default users want costs the
|
|
26
|
+
* chatter, not the audit trail.
|
|
27
|
+
*/
|
|
28
|
+
import * as fsp from 'node:fs/promises';
|
|
29
|
+
import { getConfig, sanitizeDebugLogs } from '../config/config.js';
|
|
30
|
+
/**
|
|
31
|
+
* Escape hatch for reproducing a user's bug without walking them through
|
|
32
|
+
* /task-config. Follows the existing `PI_TASK_*` instrumentation convention
|
|
33
|
+
* (`PI_TASK_TYPEONLY_LOG`, `PI_REMOTE_PUSH_DEBUG`). An unrecognised value is
|
|
34
|
+
* ignored rather than treated as `off` — a typo in an env var must not silently
|
|
35
|
+
* throw the trail away.
|
|
36
|
+
*/
|
|
37
|
+
export const DEBUG_LOG_ENV = 'PI_TASK_DEBUG_LOG';
|
|
38
|
+
/**
|
|
39
|
+
* The level in force: env override first, then the saved config. Read per call
|
|
40
|
+
* rather than cached, so flipping the setting mid-run takes effect on the next
|
|
41
|
+
* line instead of at the next restart.
|
|
42
|
+
*/
|
|
43
|
+
export function debugLogLevel(getEnv = k => process.env[k]) {
|
|
44
|
+
const raw = getEnv(DEBUG_LOG_ENV)?.trim();
|
|
45
|
+
// sanitizeDebugLogs falls back to the DEFAULT for anything unrecognised, so
|
|
46
|
+
// an unset/typo'd var must be filtered out here rather than handed to it —
|
|
47
|
+
// otherwise `PI_TASK_DEBUG_LOG=verbose` would quietly override a saved `off`.
|
|
48
|
+
if (raw && sanitizeDebugLogs(raw) === raw)
|
|
49
|
+
return raw;
|
|
50
|
+
return getConfig().debugLogs;
|
|
51
|
+
}
|
|
52
|
+
/** Whether a line of this kind should be written at `level`. */
|
|
53
|
+
export function shouldLogDebug(kind, level) {
|
|
54
|
+
if (level === 'off')
|
|
55
|
+
return false;
|
|
56
|
+
if (level === 'full')
|
|
57
|
+
return true;
|
|
58
|
+
return kind === 'event';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A timestamped fire-and-forget appender for one trail file, level-gated.
|
|
62
|
+
*
|
|
63
|
+
* `kind` defaults to `'event'` so a new call site is quiet-by-default in the
|
|
64
|
+
* useful direction: forgetting to classify a marker keeps it in the audit trail,
|
|
65
|
+
* whereas forgetting to classify chatter would only make the log bigger. Errors
|
|
66
|
+
* are swallowed — an unwritable trail must never fail the run it is describing.
|
|
67
|
+
*/
|
|
68
|
+
export function makeDebugAppender(logPath, appendFile = (p, data) => fsp.appendFile(p, data)) {
|
|
69
|
+
return (msg, kind = 'event') => {
|
|
70
|
+
if (!shouldLogDebug(kind, debugLogLevel()))
|
|
71
|
+
return;
|
|
72
|
+
void appendFile(logPath, `${new Date().toISOString()} ${msg}\n`).catch(() => { });
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wrap a raw append in the level gate. Returns `undefined` when the level is
|
|
77
|
+
* `off`, so callers that hold an OPTIONAL `logDebug` leave it unset and every
|
|
78
|
+
* `logDebug?.(…)` in the pipeline short-circuits before it formats a string.
|
|
79
|
+
*/
|
|
80
|
+
export function gateDebugWriter(write, getEnv) {
|
|
81
|
+
if (debugLogLevel(getEnv) === 'off')
|
|
82
|
+
return undefined;
|
|
83
|
+
return (msg, kind = 'event') => {
|
|
84
|
+
if (shouldLogDebug(kind, debugLogLevel(getEnv)))
|
|
85
|
+
write(msg);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decompose-granularity — the deterministic FLOOR on how finely /task-auto cuts
|
|
3
|
+
* a feature into tasks.
|
|
4
|
+
*
|
|
5
|
+
* The failure this closes (mx5, Jul 25 vs Jul 27): the SAME design doc, from the
|
|
6
|
+
* SAME base commit, with a byte-identical planning path, planned once into 41
|
|
7
|
+
* tasks and once into 11. The whole 4x difference is one line of clarify text.
|
|
8
|
+
* /task-auto's clarify head asks a plan-shape question first ("one task per
|
|
9
|
+
* milestone, or split smaller?" — 8/8 live reps), the answer-side triage
|
|
10
|
+
* auto-resolves it 8/8 and stamps it "already settled by the spec", so the user
|
|
11
|
+
* never sees the fork; and the answer decides the whole plan. Live A/B, run 18's
|
|
12
|
+
* transcript with ONLY that line swapped (n=8/arm): coarse mean 11.4 titles vs
|
|
13
|
+
* fine mean 28.6, 63.5/64 pairwise wins, p<0.001.
|
|
14
|
+
*
|
|
15
|
+
* The spec does NOT settle it. mx5's §12 is titled "Build order (milestones)" —
|
|
16
|
+
* an ORDER, 9 items, not a task breakdown. So the plan's granularity, the single
|
|
17
|
+
* highest-leverage decision in a run (each title is handed to its own pipeline
|
|
18
|
+
* that researches and specs it alone), was being decided by a coin flip nobody
|
|
19
|
+
* could see, review, or reproduce.
|
|
20
|
+
*
|
|
21
|
+
* Surfacing the question does not fix it: under YOLO/unattended `yoloPickAnswer`
|
|
22
|
+
* takes the recommendation, which is the same stochastic line. The fix has to be
|
|
23
|
+
* host-side and deterministic, so this module derives the floor from integers the
|
|
24
|
+
* run already has — the count of grounded requirements that a task can OWN.
|
|
25
|
+
*
|
|
26
|
+
* floor = ceil(ownable requirements / MAX_REQUIREMENTS_PER_TASK)
|
|
27
|
+
*
|
|
28
|
+
* MAX_REQUIREMENTS_PER_TASK = 2 is anchored on the two real mx5 plans, not on
|
|
29
|
+
* taste: the 41-task plan carried 0.8 ownable requirements per task, the collapsed
|
|
30
|
+
* 11-task plan carried 2.8. A ceiling of 2 sits between them — it rejects the
|
|
31
|
+
* collapse without demanding the finest plan ever observed.
|
|
32
|
+
*
|
|
33
|
+
* Spec-shape-agnostic: the only inputs are two integers. A CLI, a library, a
|
|
34
|
+
* refactor, a docs job all flow through the same arithmetic, and a feature with
|
|
35
|
+
* no extracted requirements (a one-liner, a doc-less request) yields floor 0 —
|
|
36
|
+
* the whole channel degrades to exactly the previous behaviour.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* The most distinct grounded requirements one task may carry before the plan is
|
|
40
|
+
* judged too coarse. See the module docstring for the mx5 anchoring.
|
|
41
|
+
*/
|
|
42
|
+
export declare const MAX_REQUIREMENTS_PER_TASK = 2;
|
|
43
|
+
/**
|
|
44
|
+
* Fewest task titles a plan may have for `ownable` requirements. Zero when the
|
|
45
|
+
* requirement channel produced nothing, which disables every check below.
|
|
46
|
+
*/
|
|
47
|
+
export declare function granularityFloor(ownable: number): number;
|
|
48
|
+
/** Is this plan too coarse for the requirements it has to carry? */
|
|
49
|
+
export declare function isTooCoarse(titles: number, floor: number): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Fewest ownable requirements a feature needs before the host takes the
|
|
52
|
+
* plan-shape fork away from the triage.
|
|
53
|
+
*
|
|
54
|
+
* Below this the fork is not load-bearing — the whole plan is one or two tasks
|
|
55
|
+
* either way — and seizing it does harm: live smoke over 24 prompts, the host
|
|
56
|
+
* directive turned "rename the `foo()` helper across the repo" (1 requirement)
|
|
57
|
+
* into a 6-task plan and "add a .editorconfig" (4) into 4. Every case at or above
|
|
58
|
+
* this cut planned inside its expected range. So: a feature with real breadth
|
|
59
|
+
* gets the deterministic answer, a chore keeps the old triage path untouched.
|
|
60
|
+
*/
|
|
61
|
+
export declare const MIN_REQUIREMENTS_FOR_PLAN_SHAPE = 5;
|
|
62
|
+
/** Does this feature have enough distinct deliverables for granularity to matter? */
|
|
63
|
+
export declare function planShapeIsHostsToAnswer(ownable: number): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Does this clarify question decide how finely the feature is CUT into tasks?
|
|
66
|
+
*
|
|
67
|
+
* Deterministic and narrow on purpose. It must fire on the fork the triage keeps
|
|
68
|
+
* answering for itself ("follow the milestones as-is, or split more granularly?")
|
|
69
|
+
* and stay off ordinary scope questions — an over-eager classifier would replace
|
|
70
|
+
* a real user decision with the host's. Matched against the plain-text question.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isPlanShapeQuestion(question: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* BELT — the host's own answer to that fork, recorded in the clarify transcript in
|
|
75
|
+
* place of the triage's.
|
|
76
|
+
*
|
|
77
|
+
* WHY A CLARIFICATION AND NOT A DECOMPOSE RULE. Both were measured live. As a
|
|
78
|
+
* RULES line replacing "prefer a handful of substantial tasks", the same directive
|
|
79
|
+
* removed the collapse but destroyed plan-size control: 66, then 81 and 85 titles
|
|
80
|
+
* for a spec whose healthy plan is ~30, plus one decompose child that blew the
|
|
81
|
+
* model's 120k context window and killed the planning phase (the baseline produced
|
|
82
|
+
* no such failure in 27 reps). Naming a target count made it worse, not better.
|
|
83
|
+
* In the CLARIFICATIONS block, with the "prefer a handful" counterweight left
|
|
84
|
+
* intact, the identical directive held 20–39 titles across 16 reps. The channel is
|
|
85
|
+
* part of the lever, not a detail.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately count-free: the spec-derived floor stays host-side, where it is
|
|
88
|
+
* enforced silently and cannot be chased.
|
|
89
|
+
*/
|
|
90
|
+
export declare const PLAN_SHAPE_QUESTION = "How finely should this feature be split into tasks?";
|
|
91
|
+
export declare const PLAN_SHAPE_ANSWER: string;
|
|
92
|
+
/**
|
|
93
|
+
* BRACES — the reprompt when the returned plan lands under the floor. Also
|
|
94
|
+
* countless, for the reason above: it asks for a SPLIT of the plan in hand rather
|
|
95
|
+
* than a fresh roll (a regeneration is a new stochastic draw over the whole plan
|
|
96
|
+
* and can drop an area the current one covers — mx5 run 12).
|
|
97
|
+
*/
|
|
98
|
+
export declare function granularitySplitHint(titles: number, ownable: number): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decompose-granularity — the deterministic FLOOR on how finely /task-auto cuts
|
|
3
|
+
* a feature into tasks.
|
|
4
|
+
*
|
|
5
|
+
* The failure this closes (mx5, Jul 25 vs Jul 27): the SAME design doc, from the
|
|
6
|
+
* SAME base commit, with a byte-identical planning path, planned once into 41
|
|
7
|
+
* tasks and once into 11. The whole 4x difference is one line of clarify text.
|
|
8
|
+
* /task-auto's clarify head asks a plan-shape question first ("one task per
|
|
9
|
+
* milestone, or split smaller?" — 8/8 live reps), the answer-side triage
|
|
10
|
+
* auto-resolves it 8/8 and stamps it "already settled by the spec", so the user
|
|
11
|
+
* never sees the fork; and the answer decides the whole plan. Live A/B, run 18's
|
|
12
|
+
* transcript with ONLY that line swapped (n=8/arm): coarse mean 11.4 titles vs
|
|
13
|
+
* fine mean 28.6, 63.5/64 pairwise wins, p<0.001.
|
|
14
|
+
*
|
|
15
|
+
* The spec does NOT settle it. mx5's §12 is titled "Build order (milestones)" —
|
|
16
|
+
* an ORDER, 9 items, not a task breakdown. So the plan's granularity, the single
|
|
17
|
+
* highest-leverage decision in a run (each title is handed to its own pipeline
|
|
18
|
+
* that researches and specs it alone), was being decided by a coin flip nobody
|
|
19
|
+
* could see, review, or reproduce.
|
|
20
|
+
*
|
|
21
|
+
* Surfacing the question does not fix it: under YOLO/unattended `yoloPickAnswer`
|
|
22
|
+
* takes the recommendation, which is the same stochastic line. The fix has to be
|
|
23
|
+
* host-side and deterministic, so this module derives the floor from integers the
|
|
24
|
+
* run already has — the count of grounded requirements that a task can OWN.
|
|
25
|
+
*
|
|
26
|
+
* floor = ceil(ownable requirements / MAX_REQUIREMENTS_PER_TASK)
|
|
27
|
+
*
|
|
28
|
+
* MAX_REQUIREMENTS_PER_TASK = 2 is anchored on the two real mx5 plans, not on
|
|
29
|
+
* taste: the 41-task plan carried 0.8 ownable requirements per task, the collapsed
|
|
30
|
+
* 11-task plan carried 2.8. A ceiling of 2 sits between them — it rejects the
|
|
31
|
+
* collapse without demanding the finest plan ever observed.
|
|
32
|
+
*
|
|
33
|
+
* Spec-shape-agnostic: the only inputs are two integers. A CLI, a library, a
|
|
34
|
+
* refactor, a docs job all flow through the same arithmetic, and a feature with
|
|
35
|
+
* no extracted requirements (a one-liner, a doc-less request) yields floor 0 —
|
|
36
|
+
* the whole channel degrades to exactly the previous behaviour.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* The most distinct grounded requirements one task may carry before the plan is
|
|
40
|
+
* judged too coarse. See the module docstring for the mx5 anchoring.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_REQUIREMENTS_PER_TASK = 2;
|
|
43
|
+
/**
|
|
44
|
+
* Fewest task titles a plan may have for `ownable` requirements. Zero when the
|
|
45
|
+
* requirement channel produced nothing, which disables every check below.
|
|
46
|
+
*/
|
|
47
|
+
export function granularityFloor(ownable) {
|
|
48
|
+
return ownable <= 0 ? 0 : Math.ceil(ownable / MAX_REQUIREMENTS_PER_TASK);
|
|
49
|
+
}
|
|
50
|
+
/** Is this plan too coarse for the requirements it has to carry? */
|
|
51
|
+
export function isTooCoarse(titles, floor) {
|
|
52
|
+
return floor > 0 && titles < floor;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fewest ownable requirements a feature needs before the host takes the
|
|
56
|
+
* plan-shape fork away from the triage.
|
|
57
|
+
*
|
|
58
|
+
* Below this the fork is not load-bearing — the whole plan is one or two tasks
|
|
59
|
+
* either way — and seizing it does harm: live smoke over 24 prompts, the host
|
|
60
|
+
* directive turned "rename the `foo()` helper across the repo" (1 requirement)
|
|
61
|
+
* into a 6-task plan and "add a .editorconfig" (4) into 4. Every case at or above
|
|
62
|
+
* this cut planned inside its expected range. So: a feature with real breadth
|
|
63
|
+
* gets the deterministic answer, a chore keeps the old triage path untouched.
|
|
64
|
+
*/
|
|
65
|
+
export const MIN_REQUIREMENTS_FOR_PLAN_SHAPE = 5;
|
|
66
|
+
/** Does this feature have enough distinct deliverables for granularity to matter? */
|
|
67
|
+
export function planShapeIsHostsToAnswer(ownable) {
|
|
68
|
+
return ownable >= MIN_REQUIREMENTS_FOR_PLAN_SHAPE;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Does this clarify question decide how finely the feature is CUT into tasks?
|
|
72
|
+
*
|
|
73
|
+
* Deterministic and narrow on purpose. It must fire on the fork the triage keeps
|
|
74
|
+
* answering for itself ("follow the milestones as-is, or split more granularly?")
|
|
75
|
+
* and stay off ordinary scope questions — an over-eager classifier would replace
|
|
76
|
+
* a real user decision with the host's. Matched against the plain-text question.
|
|
77
|
+
*/
|
|
78
|
+
export function isPlanShapeQuestion(question) {
|
|
79
|
+
const q = question.toLowerCase();
|
|
80
|
+
// The fork has to be about the BREAKDOWN itself…
|
|
81
|
+
const aboutBreakdown = /\b(task breakdown|break(ing)? (it|this|the \w+) down|decompos\w*|split\w*|subdivid\w*|granular\w*|fine[- ]grained|one task per|task per (milestone|section|step|phase|feature)|per[- ](milestone|route|component|page|module)\b|(own|separate|standalone|dedicated|self[- ]contained|single)\s+(\w+\s+)?tasks?\b)/.test(q);
|
|
82
|
+
if (!aboutBreakdown)
|
|
83
|
+
return false;
|
|
84
|
+
// …and offer a coarse/fine choice over the plan's own units.
|
|
85
|
+
return /\b(milestone|section|step|phase|task|tasks)\b/.test(q);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* BELT — the host's own answer to that fork, recorded in the clarify transcript in
|
|
89
|
+
* place of the triage's.
|
|
90
|
+
*
|
|
91
|
+
* WHY A CLARIFICATION AND NOT A DECOMPOSE RULE. Both were measured live. As a
|
|
92
|
+
* RULES line replacing "prefer a handful of substantial tasks", the same directive
|
|
93
|
+
* removed the collapse but destroyed plan-size control: 66, then 81 and 85 titles
|
|
94
|
+
* for a spec whose healthy plan is ~30, plus one decompose child that blew the
|
|
95
|
+
* model's 120k context window and killed the planning phase (the baseline produced
|
|
96
|
+
* no such failure in 27 reps). Naming a target count made it worse, not better.
|
|
97
|
+
* In the CLARIFICATIONS block, with the "prefer a handful" counterweight left
|
|
98
|
+
* intact, the identical directive held 20–39 titles across 16 reps. The channel is
|
|
99
|
+
* part of the lever, not a detail.
|
|
100
|
+
*
|
|
101
|
+
* Deliberately count-free: the spec-derived floor stays host-side, where it is
|
|
102
|
+
* enforced silently and cannot be chased.
|
|
103
|
+
*/
|
|
104
|
+
export const PLAN_SHAPE_QUESTION = 'How finely should this feature be split into tasks?';
|
|
105
|
+
export const PLAN_SHAPE_ANSWER = 'subdivide into smaller per-deliverable tasks — one task per route, page, screen,'
|
|
106
|
+
+ ' module, schema, or pipeline stage — rather than one task per milestone or spec'
|
|
107
|
+
+ ' section. A milestone or section that spans several deliverables becomes several'
|
|
108
|
+
+ ' tasks. (host-set: the spec fixes the build ORDER, not the task breakdown, so'
|
|
109
|
+
+ ' pi-task settles granularity deterministically instead of guessing per run)';
|
|
110
|
+
/**
|
|
111
|
+
* BRACES — the reprompt when the returned plan lands under the floor. Also
|
|
112
|
+
* countless, for the reason above: it asks for a SPLIT of the plan in hand rather
|
|
113
|
+
* than a fresh roll (a regeneration is a new stochastic draw over the whole plan
|
|
114
|
+
* and can drop an area the current one covers — mx5 run 12).
|
|
115
|
+
*/
|
|
116
|
+
export function granularitySplitHint(titles, ownable) {
|
|
117
|
+
return (`[SYSTEM NOTE: your plan of ${titles} task(s) is too coarse for the`
|
|
118
|
+
+ ` ${ownable} required contents this spec lists — several tasks each bundle work`
|
|
119
|
+
+ ' that belongs in separate ones. Emit the SAME plan with those tasks SPLIT: keep'
|
|
120
|
+
+ ' every task that is already one deliverable, and break each one that bundles'
|
|
121
|
+
+ ' several routes, pages, modules, or pipeline stages into one task per piece.'
|
|
122
|
+
+ ' Do not drop anything, do not merge, do not pad with trivia — split what is'
|
|
123
|
+
+ ' already there.]');
|
|
124
|
+
}
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -43,6 +43,7 @@ import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
|
43
43
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
44
44
|
import { formatLoopHint } from './child-runner.js';
|
|
45
45
|
import { getConfig } from '../config/config.js';
|
|
46
|
+
import { makeDebugAppender } from './debug-log.js';
|
|
46
47
|
import { startAutoLoader } from './widget.js';
|
|
47
48
|
import { resolveContextUsage } from './context-usage.js';
|
|
48
49
|
/** Max chars of a tool result kept in the gate debug log (mx5 run 10 item 6). */
|
|
@@ -352,10 +353,11 @@ export function buildGateDeps(params) {
|
|
|
352
353
|
contextUsage = undefined;
|
|
353
354
|
lastGuardReconcile = null;
|
|
354
355
|
const startedAt = Date.now();
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
// `kind` defaults to 'event': every marker below (start/end, the
|
|
357
|
+
// git-state guard's restore, the loop warning, a write-capable child's
|
|
358
|
+
// tree changes) is a guard record that survives at the default level.
|
|
359
|
+
// Only the child's own stdout and its tool results pass 'stream'.
|
|
360
|
+
const log = makeDebugAppender(path.join(tasksDir(cwd2), logFile));
|
|
359
361
|
log(`=== ${kind} start: ${taskTitle} ===`);
|
|
360
362
|
// GIT-STATE GUARD: these children are read-only BY CONTRACT, but the
|
|
361
363
|
// contract is prompt-level and the live model breaks it (mx5 run 6: the
|
|
@@ -399,14 +401,17 @@ export function buildGateDeps(params) {
|
|
|
399
401
|
streamInactivityMs: getConfig().streamInactivityMs,
|
|
400
402
|
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
401
403
|
onLine: line => {
|
|
404
|
+
// `lastLine` feeds the LIVE status widget and is not
|
|
405
|
+
// logging — it stays outside the gate, or a quiet
|
|
406
|
+
// trail would also blank the progress display.
|
|
402
407
|
lastLine = line;
|
|
403
|
-
log(line);
|
|
408
|
+
log(line, 'stream');
|
|
404
409
|
},
|
|
405
410
|
// Log tool OUTPUTS, not just the command (mx5 run 10 item 6):
|
|
406
411
|
// without the result "verify claimed curl PASS on a server that
|
|
407
412
|
// cannot serve" is undecidable from the log. Truncated, tail-kept
|
|
408
413
|
// (a bind failure / status usually lands at the end), error-flagged.
|
|
409
|
-
onToolResult: ({ name, isError, text }) => log(`↳ ${name} [${isError ? 'ERR' : 'ok'}]: ${truncateToolResult(text)}
|
|
414
|
+
onToolResult: ({ name, isError, text }) => log(`↳ ${name} [${isError ? 'ERR' : 'ok'}]: ${truncateToolResult(text)}`, 'stream'),
|
|
410
415
|
onContextUsage: snapshot => {
|
|
411
416
|
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
412
417
|
}
|
|
@@ -549,12 +554,7 @@ export function buildGateDeps(params) {
|
|
|
549
554
|
contextUsage = undefined;
|
|
550
555
|
const startedAt = Date.now();
|
|
551
556
|
// Per-pass debug log; the enforce child is otherwise unobservable.
|
|
552
|
-
const
|
|
553
|
-
const logEnforce = (msg) => {
|
|
554
|
-
void fsp
|
|
555
|
-
.appendFile(enforceLogPath, `${new Date().toISOString()} ${msg}\n`)
|
|
556
|
-
.catch(() => { });
|
|
557
|
-
};
|
|
557
|
+
const logEnforce = makeDebugAppender(path.join(tasksDir(cwd2), 'enforce-debug.log'));
|
|
558
558
|
logEnforce(`=== enforce start: ${taskTitle} ===`);
|
|
559
559
|
const stopLoader = startAutoLoader(enforceCtx, () => ({
|
|
560
560
|
title: taskTitle,
|
|
@@ -587,8 +587,9 @@ export function buildGateDeps(params) {
|
|
|
587
587
|
// literally-identical call repeated past threshold does.
|
|
588
588
|
loop: { pathThreshold: Number.POSITIVE_INFINITY },
|
|
589
589
|
onLine: line => {
|
|
590
|
+
// `lastLine` drives the live widget, not the trail.
|
|
590
591
|
lastLine = line;
|
|
591
|
-
logEnforce(line);
|
|
592
|
+
logEnforce(line, 'stream');
|
|
592
593
|
},
|
|
593
594
|
onContextUsage: snapshot => {
|
|
594
595
|
contextUsage = resolveContextUsage(snapshot, contextUsage, parentContextWindow);
|
|
@@ -666,9 +667,7 @@ export function buildGateDeps(params) {
|
|
|
666
667
|
// resolves; the remainder is injected under rule 4e, whose point is
|
|
667
668
|
// that such a path breaks the BUILD — so the checks that would have
|
|
668
669
|
// caught it report nothing rather than failing.
|
|
669
|
-
foreignPathProbe: () => collectForeignPathFindings(cwd2, signal,
|
|
670
|
-
.appendFile(path.join(tasksDir(cwd2), 'verify-debug.log'), `${new Date().toISOString()} ${msg}\n`)
|
|
671
|
-
.catch(() => { })),
|
|
670
|
+
foreignPathProbe: () => collectForeignPathFindings(cwd2, signal, makeDebugAppender(path.join(tasksDir(cwd2), 'verify-debug.log'))),
|
|
672
671
|
// Deterministic neutered-check-script probe (mx5 run 13 PROMPT 4
|
|
673
672
|
// item 4): a check script this task authored that cannot fail
|
|
674
673
|
// (`… || true`, an inverted-grep launder). Injected under rule 4f,
|
|
@@ -785,11 +784,7 @@ export function buildGateDeps(params) {
|
|
|
785
784
|
// preserve registry), never a per-task union.
|
|
786
785
|
treeChanges: () => collectTreeChanges(cwd2, signal),
|
|
787
786
|
probeScan: () => collectAddedLines(cwd2, signal).then(findProbeGaming),
|
|
788
|
-
log:
|
|
789
|
-
void fsp
|
|
790
|
-
.appendFile(path.join(tasksDir(cwd2), 'final-gate-debug.log'), `${new Date().toISOString()} ${msg}\n`)
|
|
791
|
-
.catch(() => { });
|
|
792
|
-
}
|
|
787
|
+
log: makeDebugAppender(path.join(tasksDir(cwd2), 'final-gate-debug.log'))
|
|
793
788
|
}),
|
|
794
789
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
795
790
|
// Read the same composed spec the verify gate judged against, so the
|
|
@@ -28,6 +28,7 @@ import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.
|
|
|
28
28
|
import { publishViewer, publishNotify, publishLifecycleNotice, registerBridgeCommand, getBridge, SessionUI } from '../remote/bridge.js';
|
|
29
29
|
import { pushNotify } from '../remote/push.js';
|
|
30
30
|
import { getConfig } from '../config/config.js';
|
|
31
|
+
import { gateDebugWriter } from './debug-log.js';
|
|
31
32
|
import { consumeWatchdogAbort, WATCHDOG_CANCEL_MARKER } from './command-watchdog.js';
|
|
32
33
|
import { buildGateDeps } from './gate-deps.js';
|
|
33
34
|
import { runGatesForTask } from './task-gates.js';
|
|
@@ -191,13 +192,15 @@ export class TaskRunner {
|
|
|
191
192
|
await this._onStart(id);
|
|
192
193
|
// Wire up per-task debug log (<cwd>/.pi-tasks/TASK_XXXX-debug.log).
|
|
193
194
|
const debugLogPath = path.join(tasksDir(cwd), `${id}-debug.log`);
|
|
194
|
-
|
|
195
|
+
// Left UNSET at level `off`, so the ~39 `logDebug?.(…)` sites downstream
|
|
196
|
+
// short-circuit before they format a string and the file is never created.
|
|
197
|
+
this._deps.logDebug = gateDebugWriter((msg) => {
|
|
195
198
|
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
196
199
|
fsp.appendFile(debugLogPath, line).catch(() => {
|
|
197
200
|
/* ignore */
|
|
198
201
|
});
|
|
199
|
-
};
|
|
200
|
-
this._deps.logDebug(`run: start phase=${resumePhase}`);
|
|
202
|
+
});
|
|
203
|
+
this._deps.logDebug?.(`run: start phase=${resumePhase}`);
|
|
201
204
|
// Register as active.
|
|
202
205
|
this._widgetState.taskId = id;
|
|
203
206
|
this._widgetState.title = title;
|
package/dist/task/phases.js
CHANGED
|
@@ -630,7 +630,10 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
630
630
|
...(spec.tools ? { tools: spec.tools } : {}),
|
|
631
631
|
...(spec.extensions ? { extensions: spec.extensions } : {}),
|
|
632
632
|
onLine: line => {
|
|
633
|
-
|
|
633
|
+
// The one 'stream' site in this file: raw research-worker
|
|
634
|
+
// output. Every other logDebug here records a decision.
|
|
635
|
+
// onChildOutput drives the widget and is not gated.
|
|
636
|
+
deps.logDebug?.(`${spec.label}: ${line}`, 'stream');
|
|
634
637
|
deps.onChildOutput?.(`${spec.label}: ${line}`);
|
|
635
638
|
}
|
|
636
639
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|