@mjasnikovs/pi-task 0.38.12 → 0.38.14
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 -4
- package/dist/config/config.d.ts +0 -1
- package/dist/config/config.js +0 -1
- package/dist/config/register.js +0 -2
- package/dist/index.js +0 -2
- package/package.json +2 -2
- package/dist/remote/state.d.ts +0 -2
- package/dist/remote/state.js +0 -7
- package/dist/thinking/compress.d.ts +0 -2
- package/dist/thinking/compress.js +0 -118
- package/dist/thinking/rewrite.d.ts +0 -29
- package/dist/thinking/rewrite.js +0 -53
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
12
|
-
[](#development)
|
|
13
13
|
[](./tsconfig.json)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
@@ -72,7 +72,7 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
72
72
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
73
73
|
| `/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. |
|
|
74
74
|
| `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
|
|
75
|
-
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control,
|
|
75
|
+
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control, auto-commit, verify work, enforce guidelines, project tour, parallel research, research cache, search engine, command timeout, stuck reply retry, yolo mode, debug logs, one `watch:` toggle per live tool, and one `ext:` toggle per installed host extension. |
|
|
76
76
|
| `/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. |
|
|
77
77
|
|
|
78
78
|
## The pipeline
|
|
@@ -204,7 +204,6 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
204
204
|
| Setting | Default | What it does |
|
|
205
205
|
| --- | --- | --- |
|
|
206
206
|
| **remote control** | on | The remote UI server (QR code, phone access). Turn off to never start it. |
|
|
207
|
-
| **compress thinking** | on | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
|
|
208
207
|
| **auto-commit** | on | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
|
|
209
208
|
| **verify work** | on | After each `/task` (and `/task-auto` task) implements — but **before** it's checked off or committed — actually **runs** the spec's own `VERIFY` block in the real workspace. pi-task otherwise only _authors_ a VERIFY block and never executes it, so a task that doesn't build is indistinguishable from one that works. A fresh `read` + `bash` child of the same local model runs the declared check, observes the real output, and reports **PASS/FAIL** (a legitimately no-op VERIFY is a PASS). On FAIL the run doesn't dead-stop: you get a boxed picker — **Autofix** (re-run the implementation turn against the failure, then re-verify; no attempt cap) or **Accept** (override a misjudged artifact) — and dismissing it pauses the run, resumable. A genuine clean pass is also the behavioral signal that lets **enforce guidelines** fix in place (see below). |
|
|
210
209
|
| **enforce guidelines** | on | After each `/task` (and `/task-auto` task) is committed, re-checks that commit's work against the project's `AGENTS.md` / `CLAUDE.md` (in the working directory). A bare fix-in-place pass trashes working code (A/B-proven), so enforcement is gated on the **verify work** signal. **With** a genuine verify pass: a fresh `read` + `edit` child of the same local model reads the **last commit's** diff and fixes violations in place; its fixes are committed **separately** as an `ENFORCE GUIDELINES` commit, then the verify signal is re-run against the enforced tree — a regression **reverts** the enforce commit and keeps the verified work. **Without** that signal (verify off, no spec, or an accept-override): the pass runs read-only and only **reports** violations, never rewrites logic. Either way a violation it can't clear (or a pass that can't run) only **warns** — the task commit already landed, so the run continues. Skipped when nothing was committed for the task. |
|
|
@@ -243,7 +242,7 @@ them checked in.
|
|
|
243
242
|
|
|
244
243
|
```sh
|
|
245
244
|
bun install
|
|
246
|
-
bun run test #
|
|
245
|
+
bun run test # 3772 tests across 212 files
|
|
247
246
|
bun run lint # prettier + eslint + tsc --noEmit
|
|
248
247
|
bun run build # tsc → dist/
|
|
249
248
|
```
|
package/dist/config/config.d.ts
CHANGED
package/dist/config/config.js
CHANGED
package/dist/config/register.js
CHANGED
|
@@ -94,8 +94,6 @@ function booleanItem(id, label, description) {
|
|
|
94
94
|
export const ITEMS = [
|
|
95
95
|
booleanItem('remote', 'remote control', 'Serve the task UI on your local network so you can follow and steer a run from '
|
|
96
96
|
+ 'your phone. Prints a QR code to scan when it starts'),
|
|
97
|
-
booleanItem('compressReasoning', 'compress thinking', "Shrink the model's thinking blocks once it has moved on, so a long run keeps more "
|
|
98
|
-
+ 'room for the work itself'),
|
|
99
97
|
booleanItem('autoCommit', 'auto-commit', 'Make a git commit before and after every sub-task, so each step is a checkpoint '
|
|
100
98
|
+ 'you can read back or roll back to'),
|
|
101
99
|
booleanItem('verifyWork', 'verify work', 'When a task says it is done, actually run the checks its spec asks for and report '
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { registerTaskAuto } from './task/auto-orchestrator.js';
|
|
|
4
4
|
import { registerTaskPlan } from './task/plan-orchestrator.js';
|
|
5
5
|
import { registerWorkers } from './workers/index.js';
|
|
6
6
|
import { registerRemote } from './remote/register.js';
|
|
7
|
-
import { registerThinkingCompression } from './thinking/compress.js';
|
|
8
7
|
import { registerCommandWatchdog } from './task/command-watchdog.js';
|
|
9
8
|
import { registerStreamWatchdog } from './task/stream-watchdog.js';
|
|
10
9
|
export default function (pi) {
|
|
@@ -14,7 +13,6 @@ export default function (pi) {
|
|
|
14
13
|
registerTaskPlan(pi);
|
|
15
14
|
registerWorkers(pi);
|
|
16
15
|
registerRemote(pi);
|
|
17
|
-
registerThinkingCompression(pi);
|
|
18
16
|
registerCommandWatchdog(pi);
|
|
19
17
|
registerStreamWatchdog(pi);
|
|
20
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.14",
|
|
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",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc -p tsconfig.build.json",
|
|
15
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
16
16
|
"lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit && tsc -p scripts/tsconfig.json --noEmit",
|
|
17
17
|
"test": "cross-env AGENT=1 bun test --isolate src/ scripts/",
|
|
18
18
|
"prepublishOnly": "bun run build"
|
package/dist/remote/state.d.ts
DELETED
package/dist/remote/state.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { getConfig } from '../config/config.js';
|
|
2
|
-
import { collectCompressible, MIN_THINKING_CHARS, rebuildWithCompressed } from './rewrite.js';
|
|
3
|
-
/** Hard cap so a stuck model request can never wedge a turn. */
|
|
4
|
-
const REQUEST_TIMEOUT_MS = 120_000;
|
|
5
|
-
const STATUS_KEY = 'pi-task-thinking';
|
|
6
|
-
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
-
const PROMPT = 'Compress this reasoning. Keep every decision/conclusion/constraint/fact relied on later. '
|
|
8
|
-
+ 'Drop restated questions, false starts, self-talk. Output only the compressed reasoning. /no_think';
|
|
9
|
-
async function compressOne(text, model, auth) {
|
|
10
|
-
const headers = { 'Content-Type': 'application/json', ...auth.headers };
|
|
11
|
-
if (auth.apiKey)
|
|
12
|
-
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
13
|
-
const res = await fetch(`${model.baseUrl}/chat/completions`, {
|
|
14
|
-
method: 'POST',
|
|
15
|
-
headers,
|
|
16
|
-
body: JSON.stringify({
|
|
17
|
-
model: model.id,
|
|
18
|
-
messages: [{ role: 'user', content: `${PROMPT}\n\n---\n\n${text}` }],
|
|
19
|
-
temperature: 0,
|
|
20
|
-
stream: false
|
|
21
|
-
}),
|
|
22
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
23
|
-
});
|
|
24
|
-
if (!res.ok)
|
|
25
|
-
throw new Error(`compress HTTP ${res.status}`);
|
|
26
|
-
const data = (await res.json());
|
|
27
|
-
const raw = data.choices?.[0]?.message?.content ?? '';
|
|
28
|
-
return raw.replaceAll('<think>', '').replaceAll('</think>', '').trim();
|
|
29
|
-
}
|
|
30
|
-
/** Animated footer loader. Safe in any mode — `setStatus` is a no-op outside the
|
|
31
|
-
* TUI. Each tick reports which block is compressing and its size. */
|
|
32
|
-
class Loader {
|
|
33
|
-
ui;
|
|
34
|
-
timer = null;
|
|
35
|
-
frame = 0;
|
|
36
|
-
constructor(ui) {
|
|
37
|
-
this.ui = ui;
|
|
38
|
-
}
|
|
39
|
-
start(label) {
|
|
40
|
-
this.stop();
|
|
41
|
-
const tick = () => {
|
|
42
|
-
this.ui.setStatus(STATUS_KEY, `${SPINNER[this.frame % SPINNER.length]} ${label()}`);
|
|
43
|
-
this.frame++;
|
|
44
|
-
};
|
|
45
|
-
tick();
|
|
46
|
-
this.timer = setInterval(tick, 120);
|
|
47
|
-
}
|
|
48
|
-
/** Show a final, non-animated line, then clear it after a short beat. */
|
|
49
|
-
finish(text) {
|
|
50
|
-
this.stop();
|
|
51
|
-
this.ui.setStatus(STATUS_KEY, text);
|
|
52
|
-
if (text !== undefined) {
|
|
53
|
-
setTimeout(() => this.ui.setStatus(STATUS_KEY, undefined), 4000);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
stop() {
|
|
57
|
-
if (this.timer) {
|
|
58
|
-
clearInterval(this.timer);
|
|
59
|
-
this.timer = null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function resolveAuth(ctx, model) {
|
|
64
|
-
try {
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ctx.model is Model<any>; the registry wants Model<Api>
|
|
66
|
-
const r = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
67
|
-
return r.ok ? { apiKey: r.apiKey, headers: r.headers } : {};
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return {};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const pct = (from, to) => Math.round((100 * (from - to)) / from);
|
|
74
|
-
export function registerThinkingCompression(pi) {
|
|
75
|
-
pi.on('message_end', async (event, ctx) => {
|
|
76
|
-
if (!getConfig().compressReasoning)
|
|
77
|
-
return;
|
|
78
|
-
const message = event.message;
|
|
79
|
-
const targets = collectCompressible(message, MIN_THINKING_CHARS);
|
|
80
|
-
if (targets.length === 0)
|
|
81
|
-
return;
|
|
82
|
-
const model = ctx.model;
|
|
83
|
-
if (!model)
|
|
84
|
-
return;
|
|
85
|
-
const loader = new Loader(ctx.ui);
|
|
86
|
-
const auth = await resolveAuth(ctx, model);
|
|
87
|
-
const modelRef = { id: model.id, baseUrl: model.baseUrl };
|
|
88
|
-
const replacements = new Map();
|
|
89
|
-
let origTotal = 0;
|
|
90
|
-
let newTotal = 0;
|
|
91
|
-
for (let i = 0; i < targets.length; i++) {
|
|
92
|
-
const t = targets[i];
|
|
93
|
-
const n = i + 1;
|
|
94
|
-
loader.start(() => targets.length > 1 ?
|
|
95
|
-
`compressing reasoning ${n}/${targets.length} (${t.text.length}c)…`
|
|
96
|
-
: `compressing reasoning (${t.text.length}c)…`);
|
|
97
|
-
try {
|
|
98
|
-
const compressed = await compressOne(t.text, modelRef, auth);
|
|
99
|
-
if (compressed.length > 0 && compressed.length < t.text.length) {
|
|
100
|
-
replacements.set(t.index, compressed);
|
|
101
|
-
origTotal += t.text.length;
|
|
102
|
-
newTotal += compressed.length;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// Leave this block verbatim; move on to the next.
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (replacements.size === 0) {
|
|
110
|
-
loader.finish(undefined);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
loader.finish(`✓ reasoning ${origTotal}→${newTotal}c (−${pct(origTotal, newTotal)}%)`);
|
|
114
|
-
// Cast back to the concrete AgentMessage type: the helpers work on a
|
|
115
|
-
// structural view, but the rewrite only swaps thinking-block text.
|
|
116
|
-
return { message: rebuildWithCompressed(message, replacements) };
|
|
117
|
-
});
|
|
118
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** Minimal structural view of a thinking content block. Kept structural (rather
|
|
2
|
-
* than importing pi-ai's `ThinkingContent`) so these helpers stay pure and are
|
|
3
|
-
* trivially unit-testable with plain objects. */
|
|
4
|
-
export interface ThinkingBlock {
|
|
5
|
-
type: 'thinking';
|
|
6
|
-
thinking: string;
|
|
7
|
-
thinkingSignature?: string;
|
|
8
|
-
redacted?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export interface AssistantMessageLike {
|
|
11
|
-
role?: string;
|
|
12
|
-
content?: unknown;
|
|
13
|
-
}
|
|
14
|
-
export interface CompressTarget {
|
|
15
|
-
index: number;
|
|
16
|
-
text: string;
|
|
17
|
-
}
|
|
18
|
-
/** Thinking blocks shorter than this aren't worth a model round-trip. */
|
|
19
|
-
export declare const MIN_THINKING_CHARS = 120;
|
|
20
|
-
export declare function isThinkingBlock(b: unknown): b is ThinkingBlock;
|
|
21
|
-
export declare function isCompressible(b: ThinkingBlock, minChars: number): boolean;
|
|
22
|
-
/** Compressible thinking blocks of an assistant message, with their positions. */
|
|
23
|
-
export declare function collectCompressible(message: AssistantMessageLike, minChars: number): CompressTarget[];
|
|
24
|
-
/** Rebuild an assistant message with compressed text swapped into the given
|
|
25
|
-
* block indices. The block `type` and `thinkingSignature` are preserved so the
|
|
26
|
-
* local provider still replays the (now shorter) reasoning, and a replacement is
|
|
27
|
-
* only applied when it actually shrinks the block. Returns the same object when
|
|
28
|
-
* nothing changed. */
|
|
29
|
-
export declare function rebuildWithCompressed<T extends AssistantMessageLike>(message: T, byIndex: ReadonlyMap<number, string>): T;
|
package/dist/thinking/rewrite.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/** Thinking blocks shorter than this aren't worth a model round-trip. */
|
|
2
|
-
export const MIN_THINKING_CHARS = 120;
|
|
3
|
-
/** In `openai-completions` (llama.cpp/local) the "signature" is a field *name*
|
|
4
|
-
* (`reasoning_content`) the reasoning is replayed under — not a crypto
|
|
5
|
-
* signature — so rewriting the text is safe. A long, non-sentinel signature is
|
|
6
|
-
* Anthropic-style extended thinking, where the signature cryptographically
|
|
7
|
-
* signs the original text and the block feeds the next turn's continuation;
|
|
8
|
-
* rewriting it would break that, so those blocks are skipped. */
|
|
9
|
-
const SENTINEL_SIGNATURES = new Set(['', 'reasoning_content', 'reasoning', 'reasoning_text']);
|
|
10
|
-
export function isThinkingBlock(b) {
|
|
11
|
-
return (typeof b === 'object'
|
|
12
|
-
&& b !== null
|
|
13
|
-
&& b.type === 'thinking'
|
|
14
|
-
&& typeof b.thinking === 'string');
|
|
15
|
-
}
|
|
16
|
-
export function isCompressible(b, minChars) {
|
|
17
|
-
if (b.redacted)
|
|
18
|
-
return false;
|
|
19
|
-
if (!SENTINEL_SIGNATURES.has(b.thinkingSignature ?? ''))
|
|
20
|
-
return false;
|
|
21
|
-
return b.thinking.trim().length >= minChars;
|
|
22
|
-
}
|
|
23
|
-
/** Compressible thinking blocks of an assistant message, with their positions. */
|
|
24
|
-
export function collectCompressible(message, minChars) {
|
|
25
|
-
if (message.role !== 'assistant' || !Array.isArray(message.content))
|
|
26
|
-
return [];
|
|
27
|
-
const out = [];
|
|
28
|
-
message.content.forEach((b, index) => {
|
|
29
|
-
if (isThinkingBlock(b) && isCompressible(b, minChars))
|
|
30
|
-
out.push({ index, text: b.thinking });
|
|
31
|
-
});
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
/** Rebuild an assistant message with compressed text swapped into the given
|
|
35
|
-
* block indices. The block `type` and `thinkingSignature` are preserved so the
|
|
36
|
-
* local provider still replays the (now shorter) reasoning, and a replacement is
|
|
37
|
-
* only applied when it actually shrinks the block. Returns the same object when
|
|
38
|
-
* nothing changed. */
|
|
39
|
-
export function rebuildWithCompressed(message, byIndex) {
|
|
40
|
-
if (byIndex.size === 0 || !Array.isArray(message.content))
|
|
41
|
-
return message;
|
|
42
|
-
let changed = false;
|
|
43
|
-
const content = message.content.map((b, index) => {
|
|
44
|
-
const compressed = byIndex.get(index);
|
|
45
|
-
if (compressed === undefined || !isThinkingBlock(b))
|
|
46
|
-
return b;
|
|
47
|
-
if (compressed.length >= b.thinking.length)
|
|
48
|
-
return b;
|
|
49
|
-
changed = true;
|
|
50
|
-
return { ...b, thinking: compressed };
|
|
51
|
-
});
|
|
52
|
-
return changed ? { ...message, content } : message;
|
|
53
|
-
}
|