@mjasnikovs/pi-task 0.38.16 → 0.38.18
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 +37 -0
- package/dist/config/config.js +81 -18
- package/dist/task/accept-debt.js +2 -1
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +205 -214
- package/dist/task/boot-probe.d.ts +46 -0
- package/dist/task/boot-probe.js +41 -21
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/final-gate-fix.js +14 -24
- package/dist/task/final-gate.js +6 -1
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.js +9 -2
- package/dist/task/phases.d.ts +66 -4
- package/dist/task/phases.js +94 -34
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +1 -1
- package/dist/task/task-gates.js +4 -2
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/task/widget.js +41 -9
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/pi-worker-core.js +23 -8
- package/package.json +1 -1
package/dist/config/config.d.ts
CHANGED
|
@@ -189,5 +189,42 @@ export declare const DEFAULT_CONFIG: PiTaskConfig;
|
|
|
189
189
|
* object/number can't reach the child argv as `-e [object Object]`.
|
|
190
190
|
*/
|
|
191
191
|
export declare function sanitizeExtensionWhitelist(value: unknown): string[];
|
|
192
|
+
/**
|
|
193
|
+
* How each setting's STORED value becomes a safe in-memory value — one loader per
|
|
194
|
+
* key, keyed on the config's own type.
|
|
195
|
+
*
|
|
196
|
+
* `ConfigItem` (config/register.ts) already absorbed three of the four edits its
|
|
197
|
+
* own header names: *"Adding one enum setting meant four coordinated edits (row,
|
|
198
|
+
* format arm, parse arm, sanitizer) and NONE of them failed to compile if you
|
|
199
|
+
* forgot it."* The sanitizer was the fourth, and it stayed behind in this file as
|
|
200
|
+
* a hand-ordered statement ladder covering 7 of 14 keys. This is the mapped type
|
|
201
|
+
* that closes it: a new field on `PiTaskConfig` is a compile error until it
|
|
202
|
+
* declares how a hostile value becomes a safe one.
|
|
203
|
+
*/
|
|
204
|
+
export declare const CONFIG_LOADERS: {
|
|
205
|
+
[K in keyof PiTaskConfig]: (raw: unknown) => PiTaskConfig[K];
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Turn parsed config JSON into a `PiTaskConfig`. Pure — this is the seam the load
|
|
209
|
+
* block used to have none of, so every hostile-value case was reachable only
|
|
210
|
+
* through `getConfig()`, which reads the developer's own machine.
|
|
211
|
+
*
|
|
212
|
+
* A non-object `raw` yields the defaults. That is not merely defensive: the old
|
|
213
|
+
* block spread `parsed` directly, and `{...DEFAULT_CONFIG, ...'ab'}` produces
|
|
214
|
+
* numeric index keys. Keys absent from the table are dropped rather than carried
|
|
215
|
+
* into the config object.
|
|
216
|
+
*/
|
|
217
|
+
export declare function loadConfig(raw: unknown): PiTaskConfig;
|
|
218
|
+
/**
|
|
219
|
+
* Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
|
|
220
|
+
*
|
|
221
|
+
* The override exists because this module loads the real file at import time,
|
|
222
|
+
* which made every test that reads a config value depend on the developer's own
|
|
223
|
+
* `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
|
|
224
|
+
* 12 tests here while CI (no config file at all) stayed green. The test preload
|
|
225
|
+
* points this at a path under the tmp dir that never exists, so tests always see
|
|
226
|
+
* DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
|
|
227
|
+
*/
|
|
228
|
+
export declare const CONFIG_PATH_ENV = "PI_TASK_CONFIG_PATH";
|
|
192
229
|
export declare function getConfig(): PiTaskConfig;
|
|
193
230
|
export declare function saveConfig(config: PiTaskConfig): Promise<void>;
|
package/dist/config/config.js
CHANGED
|
@@ -115,7 +115,86 @@ export function sanitizeExtensionWhitelist(value) {
|
|
|
115
115
|
return [];
|
|
116
116
|
return value.filter((p) => typeof p === 'string' && p.trim().length > 0);
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* A boolean setting's loader. Only a REAL boolean counts; anything else falls
|
|
120
|
+
* back to the shipped default.
|
|
121
|
+
*
|
|
122
|
+
* This is `yoloMode`'s guard, generalised. Its comment — *"a hand-edited
|
|
123
|
+
* `"yoloMode": "false"` is a truthy string"* — was true verbatim of the other
|
|
124
|
+
* seven booleans, none of which had it, so a stale `"verifyWork": "off"` reached
|
|
125
|
+
* `getConfig().verifyWork` as truthy and a `"autoCommit": 0` reached it as falsy.
|
|
126
|
+
*/
|
|
127
|
+
function asBoolean(key) {
|
|
128
|
+
return raw => (typeof raw === 'boolean' ? raw : DEFAULT_CONFIG[key]);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* How each setting's STORED value becomes a safe in-memory value — one loader per
|
|
132
|
+
* key, keyed on the config's own type.
|
|
133
|
+
*
|
|
134
|
+
* `ConfigItem` (config/register.ts) already absorbed three of the four edits its
|
|
135
|
+
* own header names: *"Adding one enum setting meant four coordinated edits (row,
|
|
136
|
+
* format arm, parse arm, sanitizer) and NONE of them failed to compile if you
|
|
137
|
+
* forgot it."* The sanitizer was the fourth, and it stayed behind in this file as
|
|
138
|
+
* a hand-ordered statement ladder covering 7 of 14 keys. This is the mapped type
|
|
139
|
+
* that closes it: a new field on `PiTaskConfig` is a compile error until it
|
|
140
|
+
* declares how a hostile value becomes a safe one.
|
|
141
|
+
*/
|
|
142
|
+
export const CONFIG_LOADERS = {
|
|
143
|
+
remote: asBoolean('remote'),
|
|
144
|
+
autoCommit: asBoolean('autoCommit'),
|
|
145
|
+
orientation: asBoolean('orientation'),
|
|
146
|
+
enforceGuidelines: asBoolean('enforceGuidelines'),
|
|
147
|
+
verifyWork: asBoolean('verifyWork'),
|
|
148
|
+
parallelResearchWorkers: asBoolean('parallelResearchWorkers'),
|
|
149
|
+
researchCache: asBoolean('researchCache'),
|
|
150
|
+
yoloMode: asBoolean('yoloMode'),
|
|
151
|
+
// A hand-edited or stale enum value must not leak an unknown provider into
|
|
152
|
+
// the dispatch switch — fall back to the default.
|
|
153
|
+
searchProvider: raw => (isSearchProvider(raw) ? raw : DEFAULT_CONFIG.searchProvider),
|
|
154
|
+
extensionWhitelist: sanitizeExtensionWhitelist,
|
|
155
|
+
requestTimeoutMs: sanitizeRequestTimeoutMs,
|
|
156
|
+
commandTimeoutExemptTools: sanitizeCommandTimeoutExemptTools,
|
|
157
|
+
streamInactivityMs: sanitizeStreamInactivityMs,
|
|
158
|
+
debugLogs: sanitizeDebugLogs
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Turn parsed config JSON into a `PiTaskConfig`. Pure — this is the seam the load
|
|
162
|
+
* block used to have none of, so every hostile-value case was reachable only
|
|
163
|
+
* through `getConfig()`, which reads the developer's own machine.
|
|
164
|
+
*
|
|
165
|
+
* A non-object `raw` yields the defaults. That is not merely defensive: the old
|
|
166
|
+
* block spread `parsed` directly, and `{...DEFAULT_CONFIG, ...'ab'}` produces
|
|
167
|
+
* numeric index keys. Keys absent from the table are dropped rather than carried
|
|
168
|
+
* into the config object.
|
|
169
|
+
*/
|
|
170
|
+
export function loadConfig(raw) {
|
|
171
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
172
|
+
return { ...DEFAULT_CONFIG };
|
|
173
|
+
}
|
|
174
|
+
const stored = raw;
|
|
175
|
+
const out = { ...DEFAULT_CONFIG };
|
|
176
|
+
for (const key of Object.keys(CONFIG_LOADERS)) {
|
|
177
|
+
// `undefined` reaches the loader like any other hostile value: every
|
|
178
|
+
// loader answers a missing key with its own default, which is what the
|
|
179
|
+
// old `delete parsed.X` + spread did.
|
|
180
|
+
;
|
|
181
|
+
out[key] = CONFIG_LOADERS[key](stored[key]);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
|
|
187
|
+
*
|
|
188
|
+
* The override exists because this module loads the real file at import time,
|
|
189
|
+
* which made every test that reads a config value depend on the developer's own
|
|
190
|
+
* `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
|
|
191
|
+
* 12 tests here while CI (no config file at all) stayed green. The test preload
|
|
192
|
+
* points this at a path under the tmp dir that never exists, so tests always see
|
|
193
|
+
* DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
|
|
194
|
+
*/
|
|
195
|
+
export const CONFIG_PATH_ENV = 'PI_TASK_CONFIG_PATH';
|
|
196
|
+
const CONFIG_PATH = process.env[CONFIG_PATH_ENV]?.trim()
|
|
197
|
+
|| path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
119
198
|
const _g = globalThis;
|
|
120
199
|
if (!_g.__piTaskConfig) {
|
|
121
200
|
_g.__piTaskConfig = { config: { ...DEFAULT_CONFIG }, loaded: false };
|
|
@@ -125,23 +204,7 @@ const G = _g.__piTaskConfig;
|
|
|
125
204
|
// before any session_start handler fires.
|
|
126
205
|
if (!G.loaded) {
|
|
127
206
|
try {
|
|
128
|
-
|
|
129
|
-
const parsed = JSON.parse(raw);
|
|
130
|
-
// A hand-edited or stale enum value must not leak an unknown provider
|
|
131
|
-
// into the dispatch switch — fall back to the default.
|
|
132
|
-
if (!isSearchProvider(parsed.searchProvider))
|
|
133
|
-
delete parsed.searchProvider;
|
|
134
|
-
parsed.extensionWhitelist = sanitizeExtensionWhitelist(parsed.extensionWhitelist);
|
|
135
|
-
parsed.requestTimeoutMs = sanitizeRequestTimeoutMs(parsed.requestTimeoutMs);
|
|
136
|
-
parsed.commandTimeoutExemptTools = sanitizeCommandTimeoutExemptTools(parsed.commandTimeoutExemptTools);
|
|
137
|
-
parsed.streamInactivityMs = sanitizeStreamInactivityMs(parsed.streamInactivityMs);
|
|
138
|
-
// A hand-edited `"yoloMode": "false"` is a truthy string — it must not
|
|
139
|
-
// silently switch a watched run into unattended auto-pick. Only a real
|
|
140
|
-
// boolean counts; anything else falls back to the OFF default.
|
|
141
|
-
if (typeof parsed.yoloMode !== 'boolean')
|
|
142
|
-
delete parsed.yoloMode;
|
|
143
|
-
parsed.debugLogs = sanitizeDebugLogs(parsed.debugLogs);
|
|
144
|
-
G.config = { ...DEFAULT_CONFIG, ...parsed };
|
|
207
|
+
G.config = loadConfig(JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')));
|
|
145
208
|
}
|
|
146
209
|
catch {
|
|
147
210
|
G.config = { ...DEFAULT_CONFIG };
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -29,6 +29,7 @@ import { existsSync } from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import * as fsp from 'node:fs/promises';
|
|
31
31
|
import { runVerifyCommandLine, spawnCommand } from './command-run.js';
|
|
32
|
+
import { failClassOfReason, isStaticClass } from './verify-work.js';
|
|
32
33
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
33
34
|
import { makeLedger } from './ledger.js';
|
|
34
35
|
import { parseVerifyBlockStrict } from './spec-validation.js';
|
|
@@ -233,7 +234,7 @@ export async function writeAcceptDebts(cwd, debts) {
|
|
|
233
234
|
* behavioral and cannot be proven resolved without re-running the model.
|
|
234
235
|
*/
|
|
235
236
|
export function isStaticClassDebt(reason) {
|
|
236
|
-
return
|
|
237
|
+
return isStaticClass(failClassOfReason(reason));
|
|
237
238
|
}
|
|
238
239
|
/**
|
|
239
240
|
* The VERIFY-COMMAND class (nexttask 5): the ONE command a recorded reason itself
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
* c.notFound()`): an existence guard is exactly how the bug presents (permanent
|
|
41
41
|
* 404), so guarded reads deliberately do NOT step aside.
|
|
42
42
|
*/
|
|
43
|
-
import { existsSync, readdirSync, readFileSync
|
|
43
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
44
44
|
import * as path from 'node:path';
|
|
45
|
+
import { shippedSources, stripCommentLines, SOURCE_HTML_RE, SOURCE_JS_RE } from './shipped-source.js';
|
|
45
46
|
export function emptyProducers() {
|
|
46
47
|
return {
|
|
47
48
|
files: new Set(),
|
|
@@ -83,14 +84,6 @@ const hasExt = (p) => /\.[A-Za-z0-9]{1,8}$/.test(path.posix.basename(p));
|
|
|
83
84
|
/** Source-only extensions nothing ships un-built: a missing one of these is hard
|
|
84
85
|
* evidence on its own (a script entrypoint like `bun src/server/index.ts`). */
|
|
85
86
|
const SOURCE_ONLY_EXT_RE = /\.(?:ts|tsx|mts|cts|jsx)$/i;
|
|
86
|
-
/** Strip comment-only lines (`// …`, `* …`) — a ref quoted in a comment is not a
|
|
87
|
-
* runtime read. Inline comments are left alone (strings may contain `//`). */
|
|
88
|
-
function stripCommentLines(src) {
|
|
89
|
-
return src
|
|
90
|
-
.split('\n')
|
|
91
|
-
.filter(l => !/^\s*(?:\/\/|\*|\/\*)/.test(l))
|
|
92
|
-
.join('\n');
|
|
93
|
-
}
|
|
94
87
|
// Literal-first-argument read-side constructs. `[^'"\`\n]` keeps the argument on
|
|
95
88
|
// one line and free of a closing quote; normalizeRefPath rejects `${…}` holes.
|
|
96
89
|
const JS_READ_PATTERNS = [
|
|
@@ -229,7 +222,7 @@ export function collectEmittedHtml(source) {
|
|
|
229
222
|
const writeRe = /\b(?:Bun\.write|writeFileSync|writeFile|fs\.promises\.writeFile)\(\s*(['"`])([^'"`\n]+)\1\s*,\s*/g;
|
|
230
223
|
for (let m = writeRe.exec(src); m !== null; m = writeRe.exec(src)) {
|
|
231
224
|
const docPath = normalizeRefPath(m[2]);
|
|
232
|
-
if (docPath === null || !
|
|
225
|
+
if (docPath === null || !SOURCE_HTML_RE.test(docPath))
|
|
233
226
|
continue;
|
|
234
227
|
const at = m.index + m[0].length;
|
|
235
228
|
let html;
|
|
@@ -777,58 +770,20 @@ export function resolveDanglingRefs(refs, prod, exists) {
|
|
|
777
770
|
out.push({ ...ref, reason });
|
|
778
771
|
}
|
|
779
772
|
}
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const MAX_SCAN_FILES = 3000;
|
|
789
|
-
const MAX_FILE_BYTES = 400_000;
|
|
790
|
-
/** Walk the tree for scannable sources (bounded, deterministic order). */
|
|
773
|
+
// The tree walk, the caps and the skip sets live in task/shipped-source.ts —
|
|
774
|
+
// this was serve-entry's walker written a second time, and the two skip sets had
|
|
775
|
+
// drifted: `bench`/`benchmarks` and `*.bench.*` were skipped there and scanned
|
|
776
|
+
// here, so a dangling reference in a benchmark was a run-level finding while the
|
|
777
|
+
// same file was invisible to the sibling scan.
|
|
778
|
+
//
|
|
779
|
+
// `producedDirs` stays this scan's own: bundled output re-referencing its own
|
|
780
|
+
// chunks is noise, and which dirs are produced is discovered per run.
|
|
791
781
|
function scanCandidates(cwd, prod) {
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
let entries;
|
|
798
|
-
try {
|
|
799
|
-
entries = readdirSync(path.join(cwd, rel)).sort();
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
for (const name of entries) {
|
|
805
|
-
if (out.length >= MAX_SCAN_FILES)
|
|
806
|
-
return;
|
|
807
|
-
const relPath = rel === '' ? name : `${rel}/${name}`;
|
|
808
|
-
let st;
|
|
809
|
-
try {
|
|
810
|
-
st = statSync(path.join(cwd, relPath));
|
|
811
|
-
}
|
|
812
|
-
catch {
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
if (st.isDirectory()) {
|
|
816
|
-
if (name.startsWith('.') || SKIP_DIR_RE.test(name))
|
|
817
|
-
continue;
|
|
818
|
-
if (rel === '' && producedDirs.has(name))
|
|
819
|
-
continue;
|
|
820
|
-
walk(relPath);
|
|
821
|
-
}
|
|
822
|
-
else if (st.isFile() && st.size <= MAX_FILE_BYTES) {
|
|
823
|
-
if (SKIP_FILE_RE.test(name))
|
|
824
|
-
continue;
|
|
825
|
-
if (SCAN_JS_RE.test(name) || SCAN_HTML_RE.test(name))
|
|
826
|
-
out.push(relPath);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
walk('');
|
|
831
|
-
return out;
|
|
782
|
+
const producedRoots = new Set([...prod.dirs, ...prod.opaque, ...prod.enumerable.keys()].map(d => d.split('/')[0]));
|
|
783
|
+
return shippedSources(cwd, {
|
|
784
|
+
ext: new RegExp(`${SOURCE_JS_RE.source}|${SOURCE_HTML_RE.source}`, 'i'),
|
|
785
|
+
excludeRoots: producedRoots
|
|
786
|
+
});
|
|
832
787
|
}
|
|
833
788
|
/**
|
|
834
789
|
* FINAL-GATE seam: scan the shipped tree for dangling runtime references.
|
|
@@ -855,7 +810,7 @@ export function findDanglingArtifacts(cwd) {
|
|
|
855
810
|
catch {
|
|
856
811
|
continue;
|
|
857
812
|
}
|
|
858
|
-
if (
|
|
813
|
+
if (SOURCE_HTML_RE.test(rel)) {
|
|
859
814
|
refs.push(...extractHtmlRefs(src, rel));
|
|
860
815
|
}
|
|
861
816
|
else {
|
|
@@ -901,7 +856,7 @@ export function collectGeneratedHtmlRefs(cwd) {
|
|
|
901
856
|
const production = discoverProducers(cwd, { excludeDevScripts: true });
|
|
902
857
|
const sources = [];
|
|
903
858
|
for (const rel of scanCandidates(cwd, full)) {
|
|
904
|
-
if (
|
|
859
|
+
if (SOURCE_HTML_RE.test(rel))
|
|
905
860
|
continue;
|
|
906
861
|
let src;
|
|
907
862
|
try {
|