@mjasnikovs/pi-task 0.18.31 → 0.18.33
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.
|
@@ -32,7 +32,7 @@ import { describeDebt } from './accept-debt.js';
|
|
|
32
32
|
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';
|
|
33
33
|
import { getConfig } from '../config/config.js';
|
|
34
34
|
import { isYoloMode, yoloPickAnswer, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
|
|
35
|
-
import { configureResearchRun } from '../workers/research-cache.js';
|
|
35
|
+
import { configureResearchRun, resumeResearchRun } from '../workers/research-cache.js';
|
|
36
36
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
37
37
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
38
38
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
@@ -1396,9 +1396,14 @@ async function handleTaskAutoResume(_args, ctx) {
|
|
|
1396
1396
|
ctx.ui.notify(`Resuming ${id}…`, 'info');
|
|
1397
1397
|
await updateTaskFrontMatter(cwd, id, { state: 'in_progress' });
|
|
1398
1398
|
autoRunning = true;
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
1401
|
-
|
|
1399
|
+
// Reuse the interrupted run's research-cache id when the cache proves it still
|
|
1400
|
+
// describes the same dependency surface (F10). mx5 run 13 resumed three times and
|
|
1401
|
+
// each resume's fresh id discarded a working 201-entry cache; anything inconclusive
|
|
1402
|
+
// still falls back to a fresh id and a re-fetch. See resumeResearchRun.
|
|
1403
|
+
const research = await resumeResearchRun(cwd, getConfig().researchCache);
|
|
1404
|
+
if (research.reused) {
|
|
1405
|
+
logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies)`);
|
|
1406
|
+
}
|
|
1402
1407
|
const abort = new AbortController();
|
|
1403
1408
|
// Resume only runs the loop (runTask); no planning children, so the loader
|
|
1404
1409
|
// title is unused here — pass the id for clarity if that ever changes.
|
|
@@ -3,11 +3,23 @@ const DEFAULT_BUDGET = 24_000;
|
|
|
3
3
|
const MIN_TOKEN_LEN = 2;
|
|
4
4
|
const FALLBACK_DTS_CHARS = 12_000;
|
|
5
5
|
const FALLBACK_README_CHARS = 4_000;
|
|
6
|
+
/**
|
|
7
|
+
* Split on any run of non-identifier characters — NOT on whitespace alone.
|
|
8
|
+
*
|
|
9
|
+
* Splitting on /\s+/ and then stripping punctuation INSIDE each token silently welds
|
|
10
|
+
* multi-part identifiers into one string that occurs nowhere in the corpus:
|
|
11
|
+
* "src/server/routes/auth.ts" -> "srcserverroutesauthts"
|
|
12
|
+
* "Bun.password.hash" -> "Bunpasswordhash"
|
|
13
|
+
* Because buildFtsQuery ORs the tokens, such a token contributes no MATCH at all, so the
|
|
14
|
+
* single most informative term in the query — the path, or the dotted API symbol — was
|
|
15
|
+
* dropped and ranking fell to the surrounding prose. Measured on the 141 real project
|
|
16
|
+
* queries of mx5 run 13, that cost 18% of them any chunk from the file they named
|
|
17
|
+
* (82% -> 99% retrieved once split on punctuation); on the 44 gradable npm queries,
|
|
18
|
+
* discriminative-symbol recall 89.5% -> 96.4%.
|
|
19
|
+
* See scripts/live-project-docs-retrieval-ab.ts and scripts/live-npm-tokenizer-regression.ts.
|
|
20
|
+
*/
|
|
6
21
|
function tokenize(query) {
|
|
7
|
-
return query
|
|
8
|
-
.split(/\s+/)
|
|
9
|
-
.map(t => t.replace(/[^a-zA-Z0-9_]/g, ''))
|
|
10
|
-
.filter(t => t.length >= MIN_TOKEN_LEN);
|
|
22
|
+
return query.split(/[^a-zA-Z0-9_]+/).filter(t => t.length >= MIN_TOKEN_LEN);
|
|
11
23
|
}
|
|
12
24
|
function buildFtsQuery(tokens) {
|
|
13
25
|
return tokens.map(t => `"${t}"`).join(' OR ');
|
|
@@ -22,6 +22,33 @@ export declare function configureResearchRun(enabled: boolean): string | undefin
|
|
|
22
22
|
* of asking the same thing resolve to the same (correct) result.
|
|
23
23
|
*/
|
|
24
24
|
export declare function normalizeQuery(s: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* A stable fingerprint of the project's declared dependency surface — the only input a
|
|
27
|
+
* cached docs digest actually depends on (a digest summarises a package's INSTALLED
|
|
28
|
+
* types/README at a pinned version). Built from package.json's dependency blocks with
|
|
29
|
+
* keys sorted, so formatting churn or an unrelated field edit does not invalidate it,
|
|
30
|
+
* while any add/remove/version-bump does.
|
|
31
|
+
*
|
|
32
|
+
* Returns undefined when the manifest is missing or unparseable — the caller then has
|
|
33
|
+
* NO positive evidence of freshness and must not reuse. Deliberately NOT the lockfile's
|
|
34
|
+
* hash or mtime: a lockfile is rewritten by installs that do not change any resolved
|
|
35
|
+
* version, which would defeat reuse for no correctness gain.
|
|
36
|
+
*/
|
|
37
|
+
export declare function depsFingerprint(cwd: string): Promise<string | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* Resume hook: reuse the interrupted run's cache id when — and only when — the file
|
|
40
|
+
* proves it describes the same dependency surface. Returns the id now stamped into the
|
|
41
|
+
* environment (reused or fresh), or undefined when caching is off.
|
|
42
|
+
*
|
|
43
|
+
* Every uncertain path falls through to a fresh id, which merely re-fetches:
|
|
44
|
+
* caching disabled, no/corrupt cache file, a file with no fingerprint (written before
|
|
45
|
+
* this shipped), an unreadable manifest, or a fingerprint that no longer matches.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resumeResearchRun(cwd: string, enabled: boolean): Promise<{
|
|
48
|
+
runId: string | undefined;
|
|
49
|
+
reused: boolean;
|
|
50
|
+
entries: number;
|
|
51
|
+
}>;
|
|
25
52
|
/**
|
|
26
53
|
* Look up a cached result for `key` in the current run. Returns undefined on a miss,
|
|
27
54
|
* a stale-run file (different id ⇒ another run's digest, ignored), or any failure.
|
|
@@ -25,11 +25,29 @@
|
|
|
25
25
|
* a run started with the feature flag OFF (no id in the environment) does not cache
|
|
26
26
|
* at all — the cache is inert unless the orchestrator turned it on for this run.
|
|
27
27
|
*
|
|
28
|
+
* RESUME REUSE (mx5 run 13, measured from the file's own git history — the cache is
|
|
29
|
+
* committed with every task, so the whole run is recoverable): a 32-task run built the
|
|
30
|
+
* cache to 201 entries over 20 tasks under one run id, then three /task-auto-resume
|
|
31
|
+
* invocations each stamped a fresh id and the first store of each dropped everything —
|
|
32
|
+
* 201 → 11 → 3 → 5 → 8. The audit read the 8-entry tail and concluded the cache was
|
|
33
|
+
* near-useless; it was in fact working, and the resume threw the work away. The old
|
|
34
|
+
* comment called a resume re-fetch "only slightly less reuse", which holds for a
|
|
35
|
+
* 5-task run and fails badly for a 32-task one.
|
|
36
|
+
*
|
|
37
|
+
* So a resume now REUSES the interrupted run's id — but only on POSITIVE evidence that
|
|
38
|
+
* the digests still describe the same dependency surface. The staleness that matters is
|
|
39
|
+
* a package version moving under a cached answer (a docs digest summarises the
|
|
40
|
+
* INSTALLED types of a pinned package), so the file records a fingerprint of the
|
|
41
|
+
* manifest's dependency block and a resume reuses the id only when that fingerprint is
|
|
42
|
+
* unchanged. Missing, unparseable, or fingerprint-less file ⇒ fresh id and a re-fetch:
|
|
43
|
+
* every inconclusive path costs time, never correctness.
|
|
44
|
+
*
|
|
28
45
|
* Stored under `.pi-tasks/` (sibling of env-notes.md / contracts.md), which the
|
|
29
46
|
* git-state guard and discardEdits both exclude. Best-effort throughout: any I/O or
|
|
30
47
|
* parse failure falls back to a live fetch — the cache only ever saves time, it can
|
|
31
48
|
* never change an answer or block a worker.
|
|
32
49
|
*/
|
|
50
|
+
import { createHash } from 'node:crypto';
|
|
33
51
|
import * as fsp from 'node:fs/promises';
|
|
34
52
|
import * as path from 'node:path';
|
|
35
53
|
import { tasksDir } from '../task/task-io.js';
|
|
@@ -81,6 +99,72 @@ export function configureResearchRun(enabled) {
|
|
|
81
99
|
export function normalizeQuery(s) {
|
|
82
100
|
return s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
83
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A stable fingerprint of the project's declared dependency surface — the only input a
|
|
104
|
+
* cached docs digest actually depends on (a digest summarises a package's INSTALLED
|
|
105
|
+
* types/README at a pinned version). Built from package.json's dependency blocks with
|
|
106
|
+
* keys sorted, so formatting churn or an unrelated field edit does not invalidate it,
|
|
107
|
+
* while any add/remove/version-bump does.
|
|
108
|
+
*
|
|
109
|
+
* Returns undefined when the manifest is missing or unparseable — the caller then has
|
|
110
|
+
* NO positive evidence of freshness and must not reuse. Deliberately NOT the lockfile's
|
|
111
|
+
* hash or mtime: a lockfile is rewritten by installs that do not change any resolved
|
|
112
|
+
* version, which would defeat reuse for no correctness gain.
|
|
113
|
+
*/
|
|
114
|
+
export async function depsFingerprint(cwd) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await fsp.readFile(path.join(cwd, 'package.json'), 'utf8');
|
|
117
|
+
const pkg = JSON.parse(raw);
|
|
118
|
+
const blocks = [
|
|
119
|
+
'dependencies',
|
|
120
|
+
'devDependencies',
|
|
121
|
+
'peerDependencies',
|
|
122
|
+
'optionalDependencies'
|
|
123
|
+
];
|
|
124
|
+
const parts = [];
|
|
125
|
+
for (const block of blocks) {
|
|
126
|
+
const deps = pkg[block];
|
|
127
|
+
if (!deps || typeof deps !== 'object')
|
|
128
|
+
continue;
|
|
129
|
+
const entries = Object.entries(deps)
|
|
130
|
+
.filter(([, v]) => typeof v === 'string')
|
|
131
|
+
.sort(([a], [b]) => a < b ? -1
|
|
132
|
+
: a > b ? 1
|
|
133
|
+
: 0)
|
|
134
|
+
.map(([k, v]) => `${k}@${String(v)}`);
|
|
135
|
+
if (entries.length > 0)
|
|
136
|
+
parts.push(`${block}:${entries.join(',')}`);
|
|
137
|
+
}
|
|
138
|
+
// No dependency block at all is a real, stable state (a dependency-free repo) —
|
|
139
|
+
// fingerprint it as such rather than failing, so reuse still works there.
|
|
140
|
+
return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 32);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resume hook: reuse the interrupted run's cache id when — and only when — the file
|
|
148
|
+
* proves it describes the same dependency surface. Returns the id now stamped into the
|
|
149
|
+
* environment (reused or fresh), or undefined when caching is off.
|
|
150
|
+
*
|
|
151
|
+
* Every uncertain path falls through to a fresh id, which merely re-fetches:
|
|
152
|
+
* caching disabled, no/corrupt cache file, a file with no fingerprint (written before
|
|
153
|
+
* this shipped), an unreadable manifest, or a fingerprint that no longer matches.
|
|
154
|
+
*/
|
|
155
|
+
export async function resumeResearchRun(cwd, enabled) {
|
|
156
|
+
if (!enabled) {
|
|
157
|
+
delete process.env[RESEARCH_RUN_ID_ENV];
|
|
158
|
+
return { runId: undefined, reused: false, entries: 0 };
|
|
159
|
+
}
|
|
160
|
+
const file = await readCacheFile(cwd);
|
|
161
|
+
const current = await depsFingerprint(cwd);
|
|
162
|
+
if (file && file.deps !== undefined && current !== undefined && file.deps === current) {
|
|
163
|
+
process.env[RESEARCH_RUN_ID_ENV] = file.runId;
|
|
164
|
+
return { runId: file.runId, reused: true, entries: Object.keys(file.entries).length };
|
|
165
|
+
}
|
|
166
|
+
return { runId: configureResearchRun(true), reused: false, entries: 0 };
|
|
167
|
+
}
|
|
84
168
|
async function readCacheFile(cwd) {
|
|
85
169
|
try {
|
|
86
170
|
const raw = await fsp.readFile(researchCacheFile(cwd), 'utf8');
|
|
@@ -127,7 +211,16 @@ export async function storeResearch(cwd, runId, key, text, details) {
|
|
|
127
211
|
for (const k of ordered.slice(0, keys.length - MAX_ENTRIES))
|
|
128
212
|
delete entries[k];
|
|
129
213
|
}
|
|
130
|
-
|
|
214
|
+
// Stamp the dependency surface these entries were produced against, so a later
|
|
215
|
+
// resume can prove they are still fresh. Unreadable manifest ⇒ field omitted,
|
|
216
|
+
// which reads as "cannot prove freshness" and simply denies reuse.
|
|
217
|
+
//
|
|
218
|
+
// FROZEN at the run's first write, deliberately: if a task installs a package
|
|
219
|
+
// mid-run and we re-fingerprinted here, the new fingerprint would bless digests
|
|
220
|
+
// taken BEFORE the install as current. Keeping the original means a mid-run
|
|
221
|
+
// install makes a later resume mismatch and re-fetch — the safe direction.
|
|
222
|
+
const deps = existing && existing.runId === runId ? existing.deps : await depsFingerprint(cwd);
|
|
223
|
+
const out = deps === undefined ? { runId, entries } : { runId, entries, deps };
|
|
131
224
|
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
132
225
|
// Atomic-ish write so a concurrent reader never sees a half-written file.
|
|
133
226
|
const tmp = `${researchCacheFile(cwd)}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.33",
|
|
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",
|