@mjasnikovs/pi-task 0.22.0 → 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/task/auto-orchestrator.js +9 -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/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';
|
|
@@ -38,6 +38,7 @@ 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';
|
|
@@ -122,8 +123,16 @@ function mentionPath(token) {
|
|
|
122
123
|
* before any task file — hence any per-task `TASK_XXXX-debug.log` — exists. Writes
|
|
123
124
|
* to `.pi-tasks/plan-debug.log`; the `*-debug.log` suffix keeps it grep-compatible
|
|
124
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.
|
|
125
132
|
*/
|
|
126
133
|
function logPlanDebug(cwd, msg) {
|
|
134
|
+
if (!shouldLogDebug('event', debugLogLevel()))
|
|
135
|
+
return;
|
|
127
136
|
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
128
137
|
const dir = tasksDir(cwd);
|
|
129
138
|
fsp.mkdir(dir, { recursive: true })
|
|
@@ -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
|
+
}
|
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",
|