@mjasnikovs/pi-task 0.18.24 → 0.18.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/config.d.ts +29 -0
- package/dist/config/config.js +25 -1
- package/dist/config/register.js +23 -2
- package/dist/index.js +2 -0
- package/dist/task/artifact-closure.d.ts +107 -0
- package/dist/task/artifact-closure.js +826 -0
- package/dist/task/auto-orchestrator.js +42 -3
- package/dist/task/command-watchdog.d.ts +67 -0
- package/dist/task/command-watchdog.js +114 -0
- package/dist/task/final-gate.js +22 -0
- package/dist/task/requirements.d.ts +6 -1
- package/dist/task/requirements.js +15 -2
- package/package.json +1 -1
package/dist/config/config.d.ts
CHANGED
|
@@ -42,7 +42,36 @@ export interface PiTaskConfig {
|
|
|
42
42
|
* /task-config, which enumerates the currently installed extensions.
|
|
43
43
|
*/
|
|
44
44
|
extensionWhitelist: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Wall-clock ceiling (ms) on a SINGLE tool execution in the MAIN session
|
|
47
|
+
* before the command watchdog cancels it and reminds the model to set its
|
|
48
|
+
* own timeout. Local models routinely run a command that never returns
|
|
49
|
+
* (e.g. `godot --headless` with no timeout, a dev server, a hung test) and
|
|
50
|
+
* the run wedges until the user manually aborts. pi's bash tool has an
|
|
51
|
+
* OPTIONAL timeout with NO default (bash.js), so a command the model didn't
|
|
52
|
+
* bound runs forever; this is the missing default, enforced from the host
|
|
53
|
+
* side via ctx.abort() (which kills the tool's whole process tree) plus an
|
|
54
|
+
* auto-reminder turn. Tool-agnostic: it arms on any tool execution, though
|
|
55
|
+
* in practice only bash runs long enough to trip it. 0 = off.
|
|
56
|
+
* DEFAULT 15 min: long enough for a real build/test suite, short enough that
|
|
57
|
+
* a true hang doesn't cost half an hour of dead time.
|
|
58
|
+
*/
|
|
59
|
+
requestTimeoutMs: number;
|
|
45
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
|
63
|
+
* the cycle order the picker shows. The stored config value is the ms number;
|
|
64
|
+
* the label is display-only (mirrors the searchProvider label/value split).
|
|
65
|
+
*/
|
|
66
|
+
export declare const COMMAND_TIMEOUT_OPTIONS: ReadonlyArray<{
|
|
67
|
+
label: string;
|
|
68
|
+
ms: number;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* A hand-edited or stale config could carry any number (or a string); pin it to
|
|
72
|
+
* one of the offered choices so the watchdog never arms on a nonsense value.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sanitizeRequestTimeoutMs(value: unknown): number;
|
|
46
75
|
/**
|
|
47
76
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
48
77
|
* object/number can't reach the child argv as `-e [object Object]`.
|
package/dist/config/config.js
CHANGED
|
@@ -3,6 +3,28 @@ import * as fsp from 'node:fs/promises';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import { isSearchProvider } from '../workers/search-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* The command-watchdog timeout choices offered by /task-config, newest-first in
|
|
8
|
+
* the cycle order the picker shows. The stored config value is the ms number;
|
|
9
|
+
* the label is display-only (mirrors the searchProvider label/value split).
|
|
10
|
+
*/
|
|
11
|
+
export const COMMAND_TIMEOUT_OPTIONS = [
|
|
12
|
+
{ label: '5 min', ms: 5 * 60_000 },
|
|
13
|
+
{ label: '10 min', ms: 10 * 60_000 },
|
|
14
|
+
{ label: '15 min', ms: 15 * 60_000 },
|
|
15
|
+
{ label: '30 min', ms: 30 * 60_000 },
|
|
16
|
+
{ label: 'off', ms: 0 }
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15 * 60_000;
|
|
19
|
+
/**
|
|
20
|
+
* A hand-edited or stale config could carry any number (or a string); pin it to
|
|
21
|
+
* one of the offered choices so the watchdog never arms on a nonsense value.
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizeRequestTimeoutMs(value) {
|
|
24
|
+
return COMMAND_TIMEOUT_OPTIONS.some(o => o.ms === value) ?
|
|
25
|
+
value
|
|
26
|
+
: DEFAULT_REQUEST_TIMEOUT_MS;
|
|
27
|
+
}
|
|
6
28
|
const DEFAULTS = {
|
|
7
29
|
remote: true,
|
|
8
30
|
compressReasoning: true,
|
|
@@ -15,7 +37,8 @@ const DEFAULTS = {
|
|
|
15
37
|
// 3/3, 0 collisions; ~14.5s of repeated docs lookups collapse to 0ms on a hit).
|
|
16
38
|
researchCache: true,
|
|
17
39
|
searchProvider: 'exa',
|
|
18
|
-
extensionWhitelist: []
|
|
40
|
+
extensionWhitelist: [],
|
|
41
|
+
requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS
|
|
19
42
|
};
|
|
20
43
|
/**
|
|
21
44
|
* A hand-edited config can hold anything; keep only string entries so a stray
|
|
@@ -43,6 +66,7 @@ if (!G.loaded) {
|
|
|
43
66
|
if (!isSearchProvider(parsed.searchProvider))
|
|
44
67
|
delete parsed.searchProvider;
|
|
45
68
|
parsed.extensionWhitelist = sanitizeExtensionWhitelist(parsed.extensionWhitelist);
|
|
69
|
+
parsed.requestTimeoutMs = sanitizeRequestTimeoutMs(parsed.requestTimeoutMs);
|
|
46
70
|
G.config = { ...DEFAULTS, ...parsed };
|
|
47
71
|
}
|
|
48
72
|
catch {
|
package/dist/config/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SettingsList, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import { registerBridgeCommand } from '../remote/bridge.js';
|
|
3
3
|
import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
|
|
4
|
-
import { getConfig, saveConfig } from './config.js';
|
|
4
|
+
import { COMMAND_TIMEOUT_OPTIONS, getConfig, saveConfig } from './config.js';
|
|
5
5
|
import { listInstalledExtensions } from './extension-list.js';
|
|
6
6
|
const CONFIG_TITLE = 'pi-task settings';
|
|
7
7
|
/**
|
|
@@ -95,12 +95,28 @@ const ITEMS = [
|
|
|
95
95
|
description: 'Engine behind web search (pi-worker-search + freshness checks). Exa and DuckDuckGo need no API key; Brave needs BRAVE_SEARCH_API_KEY',
|
|
96
96
|
// Display full engine names; the stored config value stays the short id.
|
|
97
97
|
values: SEARCH_PROVIDERS.map(p => SEARCH_PROVIDER_LABELS[p])
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'requestTimeoutMs',
|
|
101
|
+
label: 'command timeout',
|
|
102
|
+
description: 'Cancel a single command that runs longer than this and remind the model to set '
|
|
103
|
+
+ 'its own timeout. Catches a local model that runs a command which never returns '
|
|
104
|
+
+ '(hung build, dev server, no-timeout check) so the run stops itself instead of '
|
|
105
|
+
+ 'waiting for a manual abort. off disables it',
|
|
106
|
+
// Display human labels; the stored config value stays the ms number.
|
|
107
|
+
values: COMMAND_TIMEOUT_OPTIONS.map(o => o.label)
|
|
98
108
|
}
|
|
99
109
|
];
|
|
110
|
+
/** Human label for the stored command-timeout ms (falls back to the raw ms). */
|
|
111
|
+
function timeoutLabel(ms) {
|
|
112
|
+
return COMMAND_TIMEOUT_OPTIONS.find(o => o.ms === ms)?.label ?? `${ms}ms`;
|
|
113
|
+
}
|
|
100
114
|
/** What /task-config shows for a setting's current value. */
|
|
101
115
|
function displayValue(cfg, id, isEnum) {
|
|
102
116
|
if (id === 'searchProvider')
|
|
103
117
|
return SEARCH_PROVIDER_LABELS[cfg.searchProvider];
|
|
118
|
+
if (id === 'requestTimeoutMs')
|
|
119
|
+
return timeoutLabel(cfg.requestTimeoutMs);
|
|
104
120
|
if (isEnum)
|
|
105
121
|
return String(cfg[id]);
|
|
106
122
|
return cfg[id] ? 'on' : 'off';
|
|
@@ -173,6 +189,11 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
173
189
|
if (provider)
|
|
174
190
|
cfg.searchProvider = provider;
|
|
175
191
|
}
|
|
192
|
+
else if (id === 'requestTimeoutMs') {
|
|
193
|
+
const opt = COMMAND_TIMEOUT_OPTIONS.find(o => o.label === newValue);
|
|
194
|
+
if (opt)
|
|
195
|
+
cfg.requestTimeoutMs = opt.ms;
|
|
196
|
+
}
|
|
176
197
|
else {
|
|
177
198
|
;
|
|
178
199
|
cfg[id] = newValue === 'on';
|
|
@@ -185,7 +206,7 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
185
206
|
export function registerConfig(pi) {
|
|
186
207
|
registerBridgeCommand(pi, 'task-config', {
|
|
187
208
|
description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation, '
|
|
188
|
-
+ 'enforce guidelines, extension whitelist for child sessions).',
|
|
209
|
+
+ 'enforce guidelines, command timeout, extension whitelist for child sessions).',
|
|
189
210
|
handler: handleTaskConfig
|
|
190
211
|
});
|
|
191
212
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerTaskAuto } from './task/auto-orchestrator.js';
|
|
|
4
4
|
import { registerWorkers } from './workers/index.js';
|
|
5
5
|
import { registerRemote } from './remote/register.js';
|
|
6
6
|
import { registerThinkingCompression } from './thinking/compress.js';
|
|
7
|
+
import { registerCommandWatchdog } from './task/command-watchdog.js';
|
|
7
8
|
export default function (pi) {
|
|
8
9
|
registerConfig(pi);
|
|
9
10
|
registerTask(pi);
|
|
@@ -11,4 +12,5 @@ export default function (pi) {
|
|
|
11
12
|
registerWorkers(pi);
|
|
12
13
|
registerRemote(pi);
|
|
13
14
|
registerThinkingCompression(pi);
|
|
15
|
+
registerCommandWatchdog(pi);
|
|
14
16
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export interface RuntimeRef {
|
|
2
|
+
/** Repo-relative normalized path (posix separators, no leading ./). */
|
|
3
|
+
path: string;
|
|
4
|
+
/** Where the reference lives: repo-relative file, `package.json scripts.X`, or 'spec'. */
|
|
5
|
+
referencer: string;
|
|
6
|
+
/** The construct that matched (Bun.file, readFile, script src, …). */
|
|
7
|
+
construct: string;
|
|
8
|
+
/** file = must exist as a file; dir = a static root that must exist as a directory. */
|
|
9
|
+
kind: 'file' | 'dir';
|
|
10
|
+
}
|
|
11
|
+
export interface DanglingRef extends RuntimeRef {
|
|
12
|
+
/** Why the producer resolution came up empty (human- and prompt-readable). */
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
/** Everything the tree/scripts/build POSITIVELY produce. */
|
|
16
|
+
export interface ProducedOutputs {
|
|
17
|
+
/** Exact output files (tailwind -o, --outfile, redirects, Bun.write, cp dest…). */
|
|
18
|
+
files: Set<string>;
|
|
19
|
+
/** Enumerable outdir → output STEMS (basename sans extension) it emits —
|
|
20
|
+
* from parsed Bun.build entrypoints, explicit output files, source args. */
|
|
21
|
+
enumerable: Map<string, Set<string>>;
|
|
22
|
+
/** Dirs produced by machinery we could not enumerate (vite/tsc/next/unknown
|
|
23
|
+
* commands naming them) — everything under them steps aside. */
|
|
24
|
+
opaque: Set<string>;
|
|
25
|
+
/** Dirs known to be created (mkdir, outdirs) — satisfies dir-kind refs. */
|
|
26
|
+
dirs: Set<string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function emptyProducers(): ProducedOutputs;
|
|
29
|
+
/** Normalize a literal path: posix separators, strip ./ prefixes, query/hash
|
|
30
|
+
* tails (HTML), trailing slash. Returns null when the literal is not a
|
|
31
|
+
* checkable relative path (URL, absolute, template hole, glob — step aside). */
|
|
32
|
+
export declare function normalizeRefPath(raw: string): string | null;
|
|
33
|
+
/** Extract runtime refs from one JS/TS source. */
|
|
34
|
+
export declare function extractJsRefs(source: string, referencer: string): RuntimeRef[];
|
|
35
|
+
/** Extract local-asset refs from one HTML source. */
|
|
36
|
+
export declare function extractHtmlRefs(source: string, referencer: string): RuntimeRef[];
|
|
37
|
+
/** Script entrypoints: `bun x.ts`, `bun run x.ts`, `node x.js`, `tsx x.ts` —
|
|
38
|
+
* the first path-shaped source arg of a runner command. */
|
|
39
|
+
export declare function extractScriptEntrypoints(body: string, referencer: string): RuntimeRef[];
|
|
40
|
+
/**
|
|
41
|
+
* Producer facts from ONE shell command: output flags (`-o x`, `--outfile x`,
|
|
42
|
+
* `--outdir d`), redirects, `cp`/`mv`/`touch` destinations, `mkdir` dirs, known
|
|
43
|
+
* opaque bundlers. Leftover path tokens of an UNRECOGNIZED command escalate any
|
|
44
|
+
* directory they point into to opaque — that command may produce there, and
|
|
45
|
+
* inconclusive is never evidence. `escalate: false` disables that escalation
|
|
46
|
+
* for command text embedded in PROSE (a markdown bullet's surrounding words
|
|
47
|
+
* tokenize as junk "commands" and would opaque half the spec's paths).
|
|
48
|
+
*/
|
|
49
|
+
export declare function collectProducersFromCommand(cmd: string, prod: ProducedOutputs, opts?: {
|
|
50
|
+
escalate?: boolean;
|
|
51
|
+
}): void;
|
|
52
|
+
/**
|
|
53
|
+
* Producer facts from JS/TS source: write-side calls (`Bun.write`,
|
|
54
|
+
* `writeFile(Sync)`, `createWriteStream`, `copyFile` dest, `mkdir(Sync)`),
|
|
55
|
+
* `Bun.build({entrypoints, outdir})` (enumerable — unless a `naming` option
|
|
56
|
+
* makes the output names underivable, then opaque), `outfile:`, and `Bun.spawn`
|
|
57
|
+
* argv arrays re-fed through the shell-command collector (the mx5 build.ts
|
|
58
|
+
* shape: tailwind's `-o dist/app.css` lives in a spawn array).
|
|
59
|
+
*/
|
|
60
|
+
export declare function collectProducersFromSource(source: string, prod: ProducedOutputs): void;
|
|
61
|
+
/**
|
|
62
|
+
* Discover everything the project's own machinery produces: package.json script
|
|
63
|
+
* bodies, build files those scripts run (plus conventional root build files),
|
|
64
|
+
* tsconfig/vite outDirs.
|
|
65
|
+
*/
|
|
66
|
+
export declare function discoverProducers(cwd: string): ProducedOutputs;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve refs against existence + producers. DANGLING requires POSITIVE
|
|
69
|
+
* evidence (see the module doc): the ref sits under an ENUMERATED output dir
|
|
70
|
+
* and is not among its outputs, or is a missing source-only-extension file
|
|
71
|
+
* (nothing ever builds a `.ts`/`.tsx`). Everything inconclusive steps aside.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveDanglingRefs(refs: RuntimeRef[], prod: ProducedOutputs, exists: (rel: string) => boolean): DanglingRef[];
|
|
74
|
+
/**
|
|
75
|
+
* FINAL-GATE seam: scan the shipped tree for dangling runtime references.
|
|
76
|
+
* Deterministic, read-only, best-effort (throws nothing in normal operation;
|
|
77
|
+
* callers still guard). Producers are discovered first (scripts + build files +
|
|
78
|
+
* configs), then every authored source contributes refs AND runtime write-side
|
|
79
|
+
* producers (an app that writes its own cache file satisfies its own read).
|
|
80
|
+
*/
|
|
81
|
+
export declare function findDanglingArtifacts(cwd: string): DanglingRef[];
|
|
82
|
+
/** Ranked-failure text for the final gate (names referencer + missing path). */
|
|
83
|
+
export declare function danglingGateFailureText(d: DanglingRef): string;
|
|
84
|
+
/** Does the spec LIST the file as its own artifact — a file-tree entry or a
|
|
85
|
+
* bullet whose first token is (or ends with) the basename? Prose that merely
|
|
86
|
+
* mentions the name mid-sentence ("serves the built index.html") does NOT
|
|
87
|
+
* count: that is the CONSUMING side, exactly what must not self-satisfy. */
|
|
88
|
+
export declare function specListsFile(spec: string, refPath: string): boolean;
|
|
89
|
+
/** Backticked, asset-extension, path-shaped tokens on consuming-verb lines. */
|
|
90
|
+
export declare function extractSpecProseRefs(spec: string): RuntimeRef[];
|
|
91
|
+
/**
|
|
92
|
+
* PLAN-TIME seam: runtime refs in the spec's snippets AND consuming prose that
|
|
93
|
+
* neither the existing scaffold (`fileExists`), the spec's parsed build
|
|
94
|
+
* outputs, nor its own file tree produce. Each result becomes an UNOWNED
|
|
95
|
+
* coverage area until some task title claims the artifact.
|
|
96
|
+
*/
|
|
97
|
+
export declare function findSpecDanglingArtifacts(spec: string, fileExists: (rel: string) => boolean): DanglingRef[];
|
|
98
|
+
/** Does some task title claim the artifact (by basename or full path)? Titles
|
|
99
|
+
* are the one plan artifact the model cannot fake ownership INTO — mentioning
|
|
100
|
+
* the file is the grounded signal a producing task exists. */
|
|
101
|
+
export declare function titlesCoverArtifact(titles: string[], ref: {
|
|
102
|
+
path: string;
|
|
103
|
+
}): boolean;
|
|
104
|
+
/** Coverage-loop `missing` entry for an unowned dangling artifact. */
|
|
105
|
+
export declare function danglingMissingText(d: DanglingRef): string;
|
|
106
|
+
/** Carried-requirement line when still unowned at coverage exhaustion. */
|
|
107
|
+
export declare function danglingCarryText(d: DanglingRef): string;
|