@mjasnikovs/pi-task 0.40.50 → 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/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- 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 +4 -0
- package/dist/task/command-run.js +4 -2
- 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
|
@@ -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
|
/**
|
package/dist/task/debug-log.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One place that decides whether a
|
|
2
|
+
* One place that decides whether a `*-debug.log` line gets written, and what
|
|
3
|
+
* reaches the file when it does. The files live in the run's state directory
|
|
4
|
+
* (state-dir.ts), outside the repository.
|
|
3
5
|
*
|
|
4
6
|
* THE TRAIL IS WRITE-ONLY in production. Nothing under src/ reads these files
|
|
5
7
|
* back: `task-io.ts` globs `TASK_NNNN.md` and skips everything else, and
|
|
6
|
-
* auto-commit's `snapshotTrail` copies
|
|
7
|
-
* ever parsing
|
|
8
|
-
* whose return value is discarded. So this gate cannot change what a run
|
|
9
|
-
* only what it can explain afterwards. (Tests do read the trail back,
|
|
10
|
-
* `flushPlanDebug` exists.)
|
|
8
|
+
* auto-commit's `snapshotTrail` copies `.pi-tasks/` across a `reset --hard`
|
|
9
|
+
* without ever parsing it. Every producer is a `logDebug?.(…)` / `log(…)` side
|
|
10
|
+
* effect whose return value is discarded. So this gate cannot change what a run
|
|
11
|
+
* DOES — only what it can explain afterwards. (Tests do read the trail back,
|
|
12
|
+
* which is why `flushPlanDebug` exists.)
|
|
11
13
|
*
|
|
12
14
|
* TWO KINDS OF LINE, and the distinction is the whole point of having three levels
|
|
13
15
|
* rather than a boolean:
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
* A machine-local `"debugLogs": "off"` still wins — this is a default, not a floor.
|
|
31
33
|
*/
|
|
32
34
|
import * as fsp from 'node:fs/promises';
|
|
35
|
+
import * as path from 'node:path';
|
|
33
36
|
import { getConfig, sanitizeDebugLogs } from '../config/config.js';
|
|
34
37
|
/**
|
|
35
38
|
* Escape hatch for reproducing a user's bug without walking them through
|
|
@@ -66,19 +69,70 @@ export function shouldLogDebug(kind, level) {
|
|
|
66
69
|
return true;
|
|
67
70
|
return kind === 'event';
|
|
68
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Longest line the trail keeps. A tool result the child pasted whole, a minified
|
|
74
|
+
* bundle, a base64 blob — one of them can be most of the file, and past a
|
|
75
|
+
* paragraph or so nothing is being explained any more. The overflow is reported
|
|
76
|
+
* rather than dropped silently, so a truncated line still says it was truncated.
|
|
77
|
+
*/
|
|
78
|
+
export const DEBUG_LINE_LIMIT = 4096;
|
|
79
|
+
const CONTROL_CHARS = /\p{Cc}/gu;
|
|
80
|
+
function capLine(line) {
|
|
81
|
+
const over = line.length - DEBUG_LINE_LIMIT;
|
|
82
|
+
return over > 0 ? `${line.slice(0, DEBUG_LINE_LIMIT)}…+${over} chars` : line;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What actually reaches the file: control characters stripped, every line capped.
|
|
86
|
+
*
|
|
87
|
+
* One NUL makes the whole file BINARY to grep, and the trail's only job is to be
|
|
88
|
+
* grepped — a 1.7 MB `verify-debug.log` full of terminal control bytes needed
|
|
89
|
+
* `grep -a` before it could be read at all. Newlines are the exception and stay:
|
|
90
|
+
* a multi-line message stays multi-line, with the timestamp on its first line.
|
|
91
|
+
*/
|
|
92
|
+
export function sanitizeDebugLine(msg) {
|
|
93
|
+
return msg
|
|
94
|
+
.split('\n')
|
|
95
|
+
.map(line => capLine(line.replace(CONTROL_CHARS, '')))
|
|
96
|
+
.join('\n');
|
|
97
|
+
}
|
|
98
|
+
const ensuredDirs = new Map();
|
|
99
|
+
/** A run's state directory does not exist until its first line — mkdir once per
|
|
100
|
+
* directory rather than once per line. */
|
|
101
|
+
function ensureDir(dir) {
|
|
102
|
+
let made = ensuredDirs.get(dir);
|
|
103
|
+
if (!made) {
|
|
104
|
+
made = fsp.mkdir(dir, { recursive: true }).catch(() => { });
|
|
105
|
+
ensuredDirs.set(dir, made);
|
|
106
|
+
}
|
|
107
|
+
return made;
|
|
108
|
+
}
|
|
109
|
+
async function appendToTrail(p, data) {
|
|
110
|
+
await ensureDir(path.dirname(p));
|
|
111
|
+
await fsp.appendFile(p, data);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Timestamp, sanitise and append one trail line, fire-and-forget. Errors are
|
|
115
|
+
* swallowed — an unwritable trail must never fail the run it is describing.
|
|
116
|
+
*
|
|
117
|
+
* An injected `appendFile` owns its own directory: a fake has none, and mkdir-ing
|
|
118
|
+
* a test's imaginary path would write to the real filesystem.
|
|
119
|
+
*/
|
|
120
|
+
export function appendDebugLine(logPath, msg, appendFile = appendToTrail) {
|
|
121
|
+
const line = `${new Date().toISOString()} ${sanitizeDebugLine(msg)}\n`;
|
|
122
|
+
void appendFile(logPath, line).catch(() => { });
|
|
123
|
+
}
|
|
69
124
|
/**
|
|
70
125
|
* A timestamped fire-and-forget appender for one trail file, level-gated.
|
|
71
126
|
*
|
|
72
127
|
* `kind` defaults to `'event'` so a new call site is quiet-by-default in the
|
|
73
128
|
* useful direction: forgetting to classify a marker keeps it in the audit trail,
|
|
74
|
-
* whereas forgetting to classify chatter would only make the log bigger.
|
|
75
|
-
* are swallowed — an unwritable trail must never fail the run it is describing.
|
|
129
|
+
* whereas forgetting to classify chatter would only make the log bigger.
|
|
76
130
|
*/
|
|
77
|
-
export function makeDebugAppender(logPath, appendFile =
|
|
131
|
+
export function makeDebugAppender(logPath, appendFile = appendToTrail) {
|
|
78
132
|
return (msg, kind = 'event') => {
|
|
79
133
|
if (!shouldLogDebug(kind, debugLogLevel()))
|
|
80
134
|
return;
|
|
81
|
-
|
|
135
|
+
appendDebugLine(logPath, msg, appendFile);
|
|
82
136
|
};
|
|
83
137
|
}
|
|
84
138
|
/**
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { type Block, type SpecDoc } from './spec-doc.js';
|
|
2
|
+
export interface GroundedSource {
|
|
3
|
+
/** The citation as the model wrote it. */
|
|
4
|
+
quote: string;
|
|
5
|
+
/** The document block it grounded in — null when it matches only ACROSS block
|
|
6
|
+
* boundaries (a model quoting two consecutive bullets as one line). */
|
|
7
|
+
block: Block | null;
|
|
8
|
+
}
|
|
1
9
|
export interface SourcedTitle {
|
|
2
10
|
/** The title with every source clause stripped. */
|
|
3
11
|
base: string;
|
|
4
12
|
/** The cited spec lines, in order, keeping only those GROUNDED in the doc. */
|
|
5
|
-
sources:
|
|
13
|
+
sources: GroundedSource[];
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Split a decompose title into its base and its GROUNDED source citations.
|
|
@@ -22,9 +30,9 @@ export interface SourcedTitle {
|
|
|
22
30
|
* An absent clause yields no sources; a fabricated (ungrounded) one is dropped
|
|
23
31
|
* — exactly like keepGroundedContracts rejects a paraphrased quote.
|
|
24
32
|
*/
|
|
25
|
-
export declare function extractTitleSource(title: string, sourceDoc: string): SourcedTitle;
|
|
33
|
+
export declare function extractTitleSource(title: string, sourceDoc: string | SpecDoc): SourcedTitle;
|
|
26
34
|
/**
|
|
27
|
-
* The `+`-joined trailing constraint fragments of
|
|
35
|
+
* The `+`-joined trailing constraint fragments of a spec block whose words are
|
|
28
36
|
* absent from `title`. "2. **Auth** — sessions, login/logout/me, guards + tests."
|
|
29
37
|
* yields the fragment "tests"; a title that never mentions tests gets it back.
|
|
30
38
|
* Fragments before the first `+` are the task's body — a title paraphrases those
|
|
@@ -32,8 +40,13 @@ export declare function extractTitleSource(title: string, sourceDoc: string): So
|
|
|
32
40
|
* ("+ Tailwind v4 tokens, nav, router" is three constraints), so a title missing
|
|
33
41
|
* one of them gets ONLY that one restored, not the whole phrase (measured live:
|
|
34
42
|
* whole-phrase restoration re-attached text the title already carried).
|
|
43
|
+
*
|
|
44
|
+
* A bare string is read as prose — the shape a caller that has no document has.
|
|
35
45
|
*/
|
|
36
|
-
export declare function findDroppedPlusFragments(
|
|
46
|
+
export declare function findDroppedPlusFragments(source: Block | string, title: string): string[];
|
|
47
|
+
/** Restored fragments as a DELIMITED list. A bare `join(', ')` is ambiguous the
|
|
48
|
+
* moment a fragment contains a comma of its own — and they routinely do. */
|
|
49
|
+
export declare function renderFragments(fragments: string[]): string;
|
|
37
50
|
export interface TitleRestoration {
|
|
38
51
|
/** Index into the reconciled titles array. */
|
|
39
52
|
index: number;
|