@mjasnikovs/pi-task 0.40.49 → 0.41.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 +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/child-process.js +8 -2
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/shared/leftovers.d.ts +6 -1
- package/dist/shared/leftovers.js +6 -1
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +12 -2
- package/dist/task/command-run.js +45 -20
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
package/dist/task/command-run.js
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* the gate's boot half, while its command half had none.
|
|
30
30
|
*/
|
|
31
31
|
import { spawn } from 'node:child_process';
|
|
32
|
-
import { EXIT_DRAIN_MS, ownGroupSpawnOptions, reapGroupAfterExit, reapProcessGroup } from '../shared/child-process.js';
|
|
32
|
+
import { EXIT_DRAIN_MS, KILL_GRACE_MS, ownGroupSpawnOptions, reapGroupAfterExit, reapProcessGroup } from '../shared/child-process.js';
|
|
33
|
+
import { trackLeftovers } from '../shared/leftovers.js';
|
|
33
34
|
import { isCommandNotFound, resolveRunner, runnerEnv } from './runner-resolve.js';
|
|
34
35
|
/**
|
|
35
36
|
* How much of ONE stream may be held in the HOST process, and how it is split.
|
|
@@ -72,6 +73,7 @@ class BoundedOutput {
|
|
|
72
73
|
: this.head + this.tail;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
const HOST_SPAWNER = { spawn, platform: process.platform };
|
|
75
77
|
/**
|
|
76
78
|
* The real runner: one bounded child, output collected, never rejects.
|
|
77
79
|
*
|
|
@@ -89,41 +91,56 @@ class BoundedOutput {
|
|
|
89
91
|
* pipe. Waiting for `close` is therefore waiting for something no timeout can
|
|
90
92
|
* reach. `exit` settles the run, and the deadline settles it itself.
|
|
91
93
|
*/
|
|
92
|
-
export const spawnCommand = spec => new Promise(resolve => {
|
|
94
|
+
export const spawnCommand = (spec, spawner = HOST_SPAWNER) => new Promise(resolve => {
|
|
95
|
+
const { platform } = spawner;
|
|
93
96
|
const out = new BoundedOutput();
|
|
94
97
|
const err = new BoundedOutput();
|
|
98
|
+
let result;
|
|
95
99
|
let settled = false;
|
|
100
|
+
let cutShort = false;
|
|
96
101
|
let exitStatus = null;
|
|
97
102
|
let exited = false;
|
|
98
103
|
let killed = false;
|
|
99
104
|
let endedStreams = 0;
|
|
100
105
|
let drain;
|
|
101
|
-
// Its own process group
|
|
102
|
-
//
|
|
103
|
-
|
|
106
|
+
// Its own process group, and descendants found by what they inherit: a pretest
|
|
107
|
+
// that backgrounds a daemon, a build that leaves a watcher, would otherwise
|
|
108
|
+
// hold their ports into the boot check.
|
|
109
|
+
const leftovers = trackLeftovers(platform, spec.env ?? process.env, KILL_GRACE_MS);
|
|
110
|
+
const child = spawner.spawn(spec.bin, spec.args, {
|
|
104
111
|
cwd: spec.cwd,
|
|
105
112
|
// stdin CLOSED. `spawnSync` gave the child none; the default `spawn`
|
|
106
113
|
// stdio is a live pipe nobody ever ends, so a check that reads stdin —
|
|
107
114
|
// a `cat`-style pipeline, a tool that prompts, a pager — would block
|
|
108
115
|
// until the kill timer fires instead of returning at once.
|
|
109
116
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
-
...ownGroupSpawnOptions(
|
|
111
|
-
|
|
117
|
+
...ownGroupSpawnOptions(platform),
|
|
118
|
+
env: leftovers.env
|
|
112
119
|
});
|
|
113
|
-
const
|
|
114
|
-
if (settled)
|
|
120
|
+
const settle = () => {
|
|
121
|
+
if (settled || !result)
|
|
115
122
|
return;
|
|
116
123
|
settled = true;
|
|
117
124
|
clearTimeout(timer);
|
|
118
|
-
clearTimeout(drain);
|
|
119
125
|
spec.signal?.removeEventListener('abort', killAndSettle);
|
|
120
|
-
resolve(
|
|
126
|
+
resolve(result);
|
|
127
|
+
};
|
|
128
|
+
const done = (status, failure) => {
|
|
129
|
+
if (result)
|
|
130
|
+
return;
|
|
131
|
+
clearTimeout(drain);
|
|
132
|
+
result = {
|
|
121
133
|
failedToStart: failure !== undefined,
|
|
122
134
|
...(failure === undefined ? {} : { failureMessage: failure }),
|
|
123
135
|
status,
|
|
124
136
|
stdout: out.toString(),
|
|
125
137
|
stderr: err.toString()
|
|
126
|
-
}
|
|
138
|
+
};
|
|
139
|
+
// Not settled before the leftovers are gone: the boot check binds their
|
|
140
|
+
// ports. The deadline and the cancel still end the wait.
|
|
141
|
+
void leftovers.reap().then(settle);
|
|
142
|
+
if (cutShort)
|
|
143
|
+
settle();
|
|
127
144
|
};
|
|
128
145
|
const expectedStreams = (child.stdout ? 1 : 0) + (child.stderr ? 1 : 0);
|
|
129
146
|
const settleIfDrained = () => {
|
|
@@ -132,7 +149,7 @@ export const spawnCommand = spec => new Promise(resolve => {
|
|
|
132
149
|
};
|
|
133
150
|
const kill = () => {
|
|
134
151
|
if (child.pid)
|
|
135
|
-
reapProcessGroup(child.pid, 'SIGKILL', { leaderExited: exited });
|
|
152
|
+
reapProcessGroup(child.pid, 'SIGKILL', { platform, leaderExited: exited });
|
|
136
153
|
try {
|
|
137
154
|
child.kill('SIGKILL');
|
|
138
155
|
}
|
|
@@ -143,9 +160,14 @@ export const spawnCommand = spec => new Promise(resolve => {
|
|
|
143
160
|
/**
|
|
144
161
|
* The deadline and the cancel both END the run. This cannot wait to observe
|
|
145
162
|
* the kill's effect — it kills, gives the pipes one drain, and reports
|
|
146
|
-
* `status: null` regardless.
|
|
163
|
+
* `status: null` regardless. A leader that already exited keeps its status.
|
|
147
164
|
*/
|
|
148
165
|
const killAndSettle = () => {
|
|
166
|
+
cutShort = true;
|
|
167
|
+
if (result)
|
|
168
|
+
return settle();
|
|
169
|
+
if (exited)
|
|
170
|
+
return;
|
|
149
171
|
killed = true;
|
|
150
172
|
kill();
|
|
151
173
|
clearTimeout(drain);
|
|
@@ -177,15 +199,16 @@ export const spawnCommand = spec => new Promise(resolve => {
|
|
|
177
199
|
child.on('error', (e) => done(null, e.message));
|
|
178
200
|
child.on('exit', (code) => {
|
|
179
201
|
exited = true;
|
|
180
|
-
//
|
|
181
|
-
|
|
202
|
+
// A win32 kill ends the child with exit code 1, the code a failed check exits
|
|
203
|
+
// with too. A POSIX kill leaves no code, so a code there is the child's own.
|
|
204
|
+
exitStatus = killed && platform === 'win32' && code === 1 ? null : code;
|
|
182
205
|
if (child.pid)
|
|
183
|
-
reapGroupAfterExit(child.pid);
|
|
206
|
+
reapGroupAfterExit(child.pid, platform);
|
|
184
207
|
// Both ends of the same question: settle now if the pipes are already
|
|
185
208
|
// at EOF, otherwise settle after one short drain rather than waiting on
|
|
186
209
|
// whoever else is holding them.
|
|
187
210
|
settleIfDrained();
|
|
188
|
-
if (!
|
|
211
|
+
if (!result) {
|
|
189
212
|
clearTimeout(drain);
|
|
190
213
|
drain = setTimeout(() => done(exitStatus), EXIT_DRAIN_MS);
|
|
191
214
|
}
|
|
@@ -285,8 +308,10 @@ export function classifyCommandRun(run, gapPatterns = [], opts = {}) {
|
|
|
285
308
|
tail: outputTail(run.stdout, run.stderr)
|
|
286
309
|
};
|
|
287
310
|
}
|
|
288
|
-
/** The command word of a shell line, past any leading `VAR=value` assignments.
|
|
289
|
-
|
|
311
|
+
/** The command word of a shell line, past any leading `VAR=value` assignments.
|
|
312
|
+
* Exported for gate-evidence, which spawns the same shape of line and must
|
|
313
|
+
* resolve the same runner against the same stripped PATH. */
|
|
314
|
+
export function leadingBin(line) {
|
|
290
315
|
for (const tok of line.trim().split(/\s+/)) {
|
|
291
316
|
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok))
|
|
292
317
|
continue;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* constraint-policy — how much a constraint's PROVENANCE is worth, as one table
|
|
3
|
+
* both gate children render from.
|
|
4
|
+
*
|
|
5
|
+
* The verify child was told a spec constraint is unwaivable, full stop. The
|
|
6
|
+
* resolution judge was told to judge by function and treat over-literal wording
|
|
7
|
+
* as a false alarm. Neither was wrong on its own; together they made the gate's
|
|
8
|
+
* answer depend on which child happened to speak. 0043 is what that costs: a
|
|
9
|
+
* constraint a grill AUTO-ANSWER invented ("do NOT touch `test/ct/template/
|
|
10
|
+
* index.html`") was held as unwaivable as one the user typed, and the task failed
|
|
11
|
+
* on it.
|
|
12
|
+
*
|
|
13
|
+
* A constraint a human stated, or one the refined task carried in, BINDS. A
|
|
14
|
+
* constraint some automation produced — an auto-answer, a YOLO pick, an untagged
|
|
15
|
+
* line the composer wrote itself — is ADVISORY: it sharpens the work, and it
|
|
16
|
+
* fails a gate only when an ACCEPTANCE bullet also fails. Acceptance is the bar;
|
|
17
|
+
* an advisory constraint is a hint about how to clear it.
|
|
18
|
+
*
|
|
19
|
+
* One table, one renderer, two call sites (verify rule 4b, the judge's rules).
|
|
20
|
+
*/
|
|
21
|
+
import type { Constraint, ConstraintProvenance } from './spec-model.js';
|
|
22
|
+
export type ConstraintWeight = 'binding' | 'advisory';
|
|
23
|
+
/**
|
|
24
|
+
* What each provenance is worth. A new `ConstraintProvenance` member is a compile
|
|
25
|
+
* error until it declares its weight — which is the point, since the cheapest way
|
|
26
|
+
* back to 0043 is a new automated kind quietly defaulting to `binding`.
|
|
27
|
+
*
|
|
28
|
+
* `accepted` binds because a human read the recommendation and took it; `typed`
|
|
29
|
+
* and `host-set` for the same reason at either end (a person's own words, and a
|
|
30
|
+
* decision the host makes deterministically rather than leaving to a model).
|
|
31
|
+
* Everything else was produced by automation reasoning about the task, and
|
|
32
|
+
* automation may not mint an unwaivable rule.
|
|
33
|
+
*/
|
|
34
|
+
export declare const CONSTRAINT_WEIGHTS: Record<ConstraintProvenance, ConstraintWeight>;
|
|
35
|
+
export declare function constraintWeight(provenance: ConstraintProvenance): ConstraintWeight;
|
|
36
|
+
/** The weight marker a child sees on a constraint line, and probes match on. */
|
|
37
|
+
export declare function weightTag(provenance: ConstraintProvenance): string;
|
|
38
|
+
/** Does this collection of findings name at least one BINDING constraint? */
|
|
39
|
+
export declare function anyBinding(findings: readonly string[]): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The spec's CONSTRAINTS, annotated with each line's weight, for a child that
|
|
42
|
+
* must judge them. Returns null when the spec states none — the caller then emits
|
|
43
|
+
* no block rather than an empty heading.
|
|
44
|
+
*/
|
|
45
|
+
export declare function annotateConstraints(constraints: readonly Constraint[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* The policy, as prompt lines. ONE renderer: the verify pass and the resolution
|
|
48
|
+
* judge cite the same paragraph, so the two children cannot hold opposite
|
|
49
|
+
* policies again. `heading` differs because the two prompts number their rules
|
|
50
|
+
* differently — that is the only thing a call site may vary.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderConstraintPolicy(heading: string): string[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each provenance is worth. A new `ConstraintProvenance` member is a compile
|
|
3
|
+
* error until it declares its weight — which is the point, since the cheapest way
|
|
4
|
+
* back to 0043 is a new automated kind quietly defaulting to `binding`.
|
|
5
|
+
*
|
|
6
|
+
* `accepted` binds because a human read the recommendation and took it; `typed`
|
|
7
|
+
* and `host-set` for the same reason at either end (a person's own words, and a
|
|
8
|
+
* decision the host makes deterministically rather than leaving to a model).
|
|
9
|
+
* Everything else was produced by automation reasoning about the task, and
|
|
10
|
+
* automation may not mint an unwaivable rule.
|
|
11
|
+
*/
|
|
12
|
+
export const CONSTRAINT_WEIGHTS = {
|
|
13
|
+
typed: 'binding',
|
|
14
|
+
'host-set': 'binding',
|
|
15
|
+
spec: 'binding',
|
|
16
|
+
accepted: 'binding',
|
|
17
|
+
auto: 'advisory',
|
|
18
|
+
yolo: 'advisory',
|
|
19
|
+
'yolo-skip': 'advisory',
|
|
20
|
+
'auto-resolved': 'advisory',
|
|
21
|
+
derived: 'advisory'
|
|
22
|
+
};
|
|
23
|
+
export function constraintWeight(provenance) {
|
|
24
|
+
return CONSTRAINT_WEIGHTS[provenance];
|
|
25
|
+
}
|
|
26
|
+
/** The weight marker a child sees on a constraint line, and probes match on. */
|
|
27
|
+
export function weightTag(provenance) {
|
|
28
|
+
return `[${constraintWeight(provenance)}: ${provenance}]`;
|
|
29
|
+
}
|
|
30
|
+
/** Does this collection of findings name at least one BINDING constraint? */
|
|
31
|
+
export function anyBinding(findings) {
|
|
32
|
+
return findings.some(f => f.includes('[binding:'));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The spec's CONSTRAINTS, annotated with each line's weight, for a child that
|
|
36
|
+
* must judge them. Returns null when the spec states none — the caller then emits
|
|
37
|
+
* no block rather than an empty heading.
|
|
38
|
+
*/
|
|
39
|
+
export function annotateConstraints(constraints) {
|
|
40
|
+
if (constraints.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return constraints.map(c => `- ${c.text} ${weightTag(c.provenance)}`).join('\n');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The policy, as prompt lines. ONE renderer: the verify pass and the resolution
|
|
46
|
+
* judge cite the same paragraph, so the two children cannot hold opposite
|
|
47
|
+
* policies again. `heading` differs because the two prompts number their rules
|
|
48
|
+
* differently — that is the only thing a call site may vary.
|
|
49
|
+
*/
|
|
50
|
+
export function renderConstraintPolicy(heading) {
|
|
51
|
+
return [
|
|
52
|
+
`${heading} A CONSTRAINT IS WORTH ITS PROVENANCE. Each CONSTRAINTS line is tagged with`,
|
|
53
|
+
' its weight and where it came from:',
|
|
54
|
+
` - ${weightTag('typed')} / ${weightTag('spec')} — BINDING. A person stated it, or the`,
|
|
55
|
+
' task itself carried it in. Shipped work that violates one is a FAIL naming the',
|
|
56
|
+
' constraint; you have no waiver authority over it, and "it works anyway" is exactly',
|
|
57
|
+
' the waiver you do not have.',
|
|
58
|
+
` - ${weightTag('auto')} / ${weightTag('derived')} — ADVISORY. Automation produced it`,
|
|
59
|
+
' (an auto-answered question, an unattended pick, a line the composer wrote itself),',
|
|
60
|
+
' so it records intent, not a contract. A violated advisory constraint is a FAIL only',
|
|
61
|
+
' when an ACCEPTANCE bullet ALSO fails; on its own it is a note in your report and',
|
|
62
|
+
' the verdict stands on the acceptance criteria.',
|
|
63
|
+
' ACCEPTANCE is the bar in both cases. Never soften a binding constraint, and never',
|
|
64
|
+
' fail work solely for an advisory one.'
|
|
65
|
+
];
|
|
66
|
+
}
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* TWO SILENT SHAPES ARE LOSSES, and both are recognisable from the output alone:
|
|
9
9
|
*
|
|
10
|
-
* LOOP_DEGRADE: the worker thrashed
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* LOOP_DEGRADE: the worker thrashed until a runaway guard fired, leaving the
|
|
11
|
+
* degrade banner in place of a section. research-worker.ts writes that banner
|
|
12
|
+
* from `describeLoopHit` (loop-detector.ts) — "stuck in a loop — called
|
|
13
|
+
* <tool>(<args>) …" or "stopped covering new ground — …" — which is what
|
|
14
|
+
* LOOP_BANNER keys on.
|
|
14
15
|
*
|
|
15
16
|
* GENERATION_GARBAGE: the worker exited 0 and emitted a non-bullet fragment
|
|
16
17
|
* instead of context — a stray sentence, or a hallucinated system note.
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* TWO SILENT SHAPES ARE LOSSES, and both are recognisable from the output alone:
|
|
9
9
|
*
|
|
10
|
-
* LOOP_DEGRADE: the worker thrashed
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* LOOP_DEGRADE: the worker thrashed until a runaway guard fired, leaving the
|
|
11
|
+
* degrade banner in place of a section. research-worker.ts writes that banner
|
|
12
|
+
* from `describeLoopHit` (loop-detector.ts) — "stuck in a loop — called
|
|
13
|
+
* <tool>(<args>) …" or "stopped covering new ground — …" — which is what
|
|
14
|
+
* LOOP_BANNER keys on.
|
|
14
15
|
*
|
|
15
16
|
* GENERATION_GARBAGE: the worker exited 0 and emitted a non-bullet fragment
|
|
16
17
|
* instead of context — a stray sentence, or a hallucinated system note.
|
|
@@ -26,7 +27,14 @@
|
|
|
26
27
|
export function countBullets(contextText) {
|
|
27
28
|
return contextText.split('\n').filter(l => /^\s*[-*]\s+/.test(l)).length;
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The runaway-degrade banner, in either wording: an exact-repeat loop, or the
|
|
32
|
+
* progress rule that kills a worker for covering no new ground
|
|
33
|
+
* (loop-detector.ts `describeLoopHit`). Both are the same loss — a worker killed
|
|
34
|
+
* mid-answer — and keying on the loop phrase alone filed the second as a
|
|
35
|
+
* hallucinated fragment.
|
|
36
|
+
*/
|
|
37
|
+
const LOOP_BANNER = /stuck in a loop|stopped covering new ground|tokens of tool output/i;
|
|
30
38
|
/** Honest "nothing to surface" — the ONLY non-loss silent shape. */
|
|
31
39
|
const EMPTY_DECLARATION = /^\s*(none|n\/a|no relevant (context|architectural)|nothing\b)/i;
|
|
32
40
|
/**
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* noun, no web-app assumption. A CLI, a data pipeline, a library, a refactor, a
|
|
29
29
|
* docs task all flow through the same integers.
|
|
30
30
|
*/
|
|
31
|
-
/** A scored plan candidate — the minimum the adoption rule needs. */
|
|
32
31
|
import type { CoverageAccounting } from './requirements.js';
|
|
32
|
+
/** A scored plan candidate — the minimum the adoption rule needs. */
|
|
33
33
|
export interface CoveragePlan {
|
|
34
34
|
titles: string[];
|
|
35
35
|
/**
|
|
@@ -80,6 +80,8 @@ export interface AdoptionDecision {
|
|
|
80
80
|
reason: string;
|
|
81
81
|
/** Owned-requirement indices the retry would drop (for the debug trail). */
|
|
82
82
|
dropped: number[];
|
|
83
|
+
/** No further round can improve on the current plan — stop reprompting. */
|
|
84
|
+
terminal?: boolean;
|
|
83
85
|
}
|
|
84
86
|
/**
|
|
85
87
|
* Whether a coverage-retry should REPLACE the current plan.
|
|
@@ -91,9 +93,10 @@ export interface AdoptionDecision {
|
|
|
91
93
|
* guarantee; it holds regardless of how the un-ownable requirements were
|
|
92
94
|
* classified, so it backstops the cross-cutting classifier completely.
|
|
93
95
|
* 2b. WITH requirement signal: reject a retry that grows the plan while covering
|
|
94
|
-
* NOTHING new
|
|
95
|
-
* smallest among equals". Without it the guard is
|
|
96
|
-
* the reprompt asks for; see the long note at the
|
|
96
|
+
* NOTHING new, and mark the loop TERMINAL — the tiebreak that makes "ship the
|
|
97
|
+
* best" also mean "ship the smallest among equals". Without it the guard is
|
|
98
|
+
* inert against the superset the reprompt asks for; see the long note at the
|
|
99
|
+
* branch.
|
|
97
100
|
* 3. WITHOUT requirement signal: fall back to the count floor, and additionally
|
|
98
101
|
* refuse a retry that leaves MORE areas uncovered than the current plan — so
|
|
99
102
|
* the no-requirements path also ships the best, not the last.
|
|
@@ -28,133 +28,7 @@
|
|
|
28
28
|
* noun, no web-app assumption. A CLI, a data pipeline, a library, a refactor, a
|
|
29
29
|
* docs task all flow through the same integers.
|
|
30
30
|
*/
|
|
31
|
-
|
|
32
|
-
// titles and requirement quotes, so overlap on them would falsely connect a
|
|
33
|
-
// requirement to any plan. Stopped so grounding keys on the DISTINCTIVE nouns
|
|
34
|
-
// (json, dead-letter, serialize, symlink…) that actually name a deliverable.
|
|
35
|
-
//
|
|
36
|
-
// Confirmed: titles built only from these words own NOTHING, while titles naming
|
|
37
|
-
// `JSON output` and `dead-letter queue` own the matching requirements.
|
|
38
|
-
//
|
|
39
|
-
// English function words + generic task verbs + generic project nouns — all
|
|
40
|
-
// domain-agnostic.
|
|
41
|
-
const COVERAGE_STOPWORDS = new Set([
|
|
42
|
-
// function words
|
|
43
|
-
'the',
|
|
44
|
-
'a',
|
|
45
|
-
'an',
|
|
46
|
-
'and',
|
|
47
|
-
'or',
|
|
48
|
-
'of',
|
|
49
|
-
'to',
|
|
50
|
-
'in',
|
|
51
|
-
'on',
|
|
52
|
-
'for',
|
|
53
|
-
'with',
|
|
54
|
-
'by',
|
|
55
|
-
'at',
|
|
56
|
-
'as',
|
|
57
|
-
'is',
|
|
58
|
-
'are',
|
|
59
|
-
'be',
|
|
60
|
-
'it',
|
|
61
|
-
'its',
|
|
62
|
-
'that',
|
|
63
|
-
'this',
|
|
64
|
-
'from',
|
|
65
|
-
'into',
|
|
66
|
-
'out',
|
|
67
|
-
'up',
|
|
68
|
-
'per',
|
|
69
|
-
'via',
|
|
70
|
-
'not',
|
|
71
|
-
'no',
|
|
72
|
-
'but',
|
|
73
|
-
'if',
|
|
74
|
-
'then',
|
|
75
|
-
'than',
|
|
76
|
-
'so',
|
|
77
|
-
'such',
|
|
78
|
-
'each',
|
|
79
|
-
'any',
|
|
80
|
-
'all',
|
|
81
|
-
'every',
|
|
82
|
-
'when',
|
|
83
|
-
'where',
|
|
84
|
-
'must',
|
|
85
|
-
'should',
|
|
86
|
-
'shall',
|
|
87
|
-
'may',
|
|
88
|
-
'can',
|
|
89
|
-
'will',
|
|
90
|
-
'end',
|
|
91
|
-
'new',
|
|
92
|
-
// generic task verbs
|
|
93
|
-
'add',
|
|
94
|
-
'implement',
|
|
95
|
-
'create',
|
|
96
|
-
'build',
|
|
97
|
-
'scaffold',
|
|
98
|
-
'setup',
|
|
99
|
-
'set',
|
|
100
|
-
'support',
|
|
101
|
-
'handle',
|
|
102
|
-
'apply',
|
|
103
|
-
'use',
|
|
104
|
-
'used',
|
|
105
|
-
'using',
|
|
106
|
-
'make',
|
|
107
|
-
'makes',
|
|
108
|
-
'made',
|
|
109
|
-
'enable',
|
|
110
|
-
'provide',
|
|
111
|
-
'ensure',
|
|
112
|
-
'allow',
|
|
113
|
-
'run',
|
|
114
|
-
'runs',
|
|
115
|
-
'get',
|
|
116
|
-
'gets',
|
|
117
|
-
'define',
|
|
118
|
-
'configure',
|
|
119
|
-
'init',
|
|
120
|
-
'update',
|
|
121
|
-
'manage',
|
|
122
|
-
// generic project nouns
|
|
123
|
-
'cli',
|
|
124
|
-
'tool',
|
|
125
|
-
'app',
|
|
126
|
-
'application',
|
|
127
|
-
'project',
|
|
128
|
-
'feature',
|
|
129
|
-
'task',
|
|
130
|
-
'tasks',
|
|
131
|
-
'user',
|
|
132
|
-
'users',
|
|
133
|
-
'mode',
|
|
134
|
-
'flag',
|
|
135
|
-
'flags',
|
|
136
|
-
'option',
|
|
137
|
-
'options',
|
|
138
|
-
'system',
|
|
139
|
-
'code',
|
|
140
|
-
'thing',
|
|
141
|
-
'things',
|
|
142
|
-
'work'
|
|
143
|
-
]);
|
|
144
|
-
/** Distinctive content tokens of a phrase: lowercased alphanumeric words ≥3 chars,
|
|
145
|
-
* minus the ubiquitous stopwords. `--json` → `json`, `dead-letter` → `dead`,`letter`.
|
|
146
|
-
* A single trailing `s` is stripped (len ≥4) so `scan`/`scans`, `file`/`files`,
|
|
147
|
-
* `serialize`/`serializes` match — plain plural/3rd-person, no full stemmer. */
|
|
148
|
-
function contentTokens(s) {
|
|
149
|
-
const out = new Set();
|
|
150
|
-
for (const raw of s.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
151
|
-
if (raw.length < 3 || COVERAGE_STOPWORDS.has(raw))
|
|
152
|
-
continue;
|
|
153
|
-
const w = raw.length >= 4 && raw.endsWith('s') && !raw.endsWith('ss') ? raw.slice(0, -1) : raw;
|
|
154
|
-
out.add(w);
|
|
155
|
-
}
|
|
156
|
-
return out;
|
|
157
|
-
}
|
|
31
|
+
import { contentTokens } from '../shared/content-tokens.js';
|
|
158
32
|
/**
|
|
159
33
|
* DETERMINISTIC owned-set for the monotonic guard — grounded in requirement↔title
|
|
160
34
|
* token overlap, NOT the coverage-map model's `TASK n` verdict.
|
|
@@ -224,9 +98,10 @@ export function droppedCoverage(current, retry) {
|
|
|
224
98
|
* guarantee; it holds regardless of how the un-ownable requirements were
|
|
225
99
|
* classified, so it backstops the cross-cutting classifier completely.
|
|
226
100
|
* 2b. WITH requirement signal: reject a retry that grows the plan while covering
|
|
227
|
-
* NOTHING new
|
|
228
|
-
* smallest among equals". Without it the guard is
|
|
229
|
-
* the reprompt asks for; see the long note at the
|
|
101
|
+
* NOTHING new, and mark the loop TERMINAL — the tiebreak that makes "ship the
|
|
102
|
+
* best" also mean "ship the smallest among equals". Without it the guard is
|
|
103
|
+
* inert against the superset the reprompt asks for; see the long note at the
|
|
104
|
+
* branch.
|
|
230
105
|
* 3. WITHOUT requirement signal: fall back to the count floor, and additionally
|
|
231
106
|
* refuse a retry that leaves MORE areas uncovered than the current plan — so
|
|
232
107
|
* the no-requirements path also ships the best, not the last.
|
|
@@ -265,23 +140,26 @@ export function decideAdoption(current, retry, hasRequirements) {
|
|
|
265
140
|
// always adopted; an independently-sampled one often is not — the guard has
|
|
266
141
|
// power, just not against the shape the prompt requests).
|
|
267
142
|
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
143
|
+
// TERMINAL, not merely rejected. The reprompt asks for every title the
|
|
144
|
+
// previous list had PLUS the missing areas, so the next round is drawn from
|
|
145
|
+
// the same instruction that just produced a superset covering nothing new;
|
|
146
|
+
// "reject and ask again" spends the remaining rounds re-asking a question
|
|
147
|
+
// already answered. The measured shape is a plan inflating round after
|
|
148
|
+
// round with the owned-set pinned, every step logged as "preserves owned
|
|
149
|
+
// coverage".
|
|
271
150
|
//
|
|
272
151
|
// Safety is structural, not statistical: this branch is reachable only when
|
|
273
152
|
// the retry covers NO MORE than the current plan, so it can never decline a
|
|
274
|
-
// strictly better one
|
|
275
|
-
//
|
|
276
|
-
// win is removing pathological
|
|
277
|
-
// inflation, not shrinking plans generally.
|
|
153
|
+
// strictly better one — and the plan that ships is the current one, which is
|
|
154
|
+
// the best seen.
|
|
278
155
|
if (retry.covered.size <= current.covered.size
|
|
279
156
|
&& retry.titles.length > current.titles.length) {
|
|
280
157
|
return {
|
|
281
158
|
adopt: false,
|
|
282
159
|
reason: `no coverage gain for +${retry.titles.length - current.titles.length} titles `
|
|
283
160
|
+ `(${current.covered.size} owned, unchanged)`,
|
|
284
|
-
dropped: []
|
|
161
|
+
dropped: [],
|
|
162
|
+
terminal: true
|
|
285
163
|
};
|
|
286
164
|
}
|
|
287
165
|
return { adopt: true, reason: 'preserves owned coverage', dropped: [] };
|
package/dist/task/debug-log.d.ts
CHANGED
|
@@ -22,13 +22,36 @@ export declare function debugLogLevel(getEnv?: (k: string) => string | undefined
|
|
|
22
22
|
* `off` writes nothing, `full` writes both kinds, `events` writes events and
|
|
23
23
|
* drops stream. */
|
|
24
24
|
export declare function shouldLogDebug(kind: DebugLine, level: DebugLogLevel): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Longest line the trail keeps. A tool result the child pasted whole, a minified
|
|
27
|
+
* bundle, a base64 blob — one of them can be most of the file, and past a
|
|
28
|
+
* paragraph or so nothing is being explained any more. The overflow is reported
|
|
29
|
+
* rather than dropped silently, so a truncated line still says it was truncated.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEBUG_LINE_LIMIT = 4096;
|
|
32
|
+
/**
|
|
33
|
+
* What actually reaches the file: control characters stripped, every line capped.
|
|
34
|
+
*
|
|
35
|
+
* One NUL makes the whole file BINARY to grep, and the trail's only job is to be
|
|
36
|
+
* grepped — a 1.7 MB `verify-debug.log` full of terminal control bytes needed
|
|
37
|
+
* `grep -a` before it could be read at all. Newlines are the exception and stay:
|
|
38
|
+
* a multi-line message stays multi-line, with the timestamp on its first line.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sanitizeDebugLine(msg: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Timestamp, sanitise and append one trail line, fire-and-forget. Errors are
|
|
43
|
+
* swallowed — an unwritable trail must never fail the run it is describing.
|
|
44
|
+
*
|
|
45
|
+
* An injected `appendFile` owns its own directory: a fake has none, and mkdir-ing
|
|
46
|
+
* a test's imaginary path would write to the real filesystem.
|
|
47
|
+
*/
|
|
48
|
+
export declare function appendDebugLine(logPath: string, msg: string, appendFile?: (p: string, data: string) => Promise<unknown>): void;
|
|
25
49
|
/**
|
|
26
50
|
* A timestamped fire-and-forget appender for one trail file, level-gated.
|
|
27
51
|
*
|
|
28
52
|
* `kind` defaults to `'event'` so a new call site is quiet-by-default in the
|
|
29
53
|
* useful direction: forgetting to classify a marker keeps it in the audit trail,
|
|
30
|
-
* whereas forgetting to classify chatter would only make the log bigger.
|
|
31
|
-
* are swallowed — an unwritable trail must never fail the run it is describing.
|
|
54
|
+
* whereas forgetting to classify chatter would only make the log bigger.
|
|
32
55
|
*/
|
|
33
56
|
export declare function makeDebugAppender(logPath: string, appendFile?: (p: string, data: string) => Promise<unknown>): (msg: string, kind?: DebugLine) => void;
|
|
34
57
|
/**
|