@mjasnikovs/pi-task 0.40.50 → 0.42.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.
Files changed (125) hide show
  1. package/README.md +7 -4
  2. package/dist/config/config.d.ts +61 -18
  3. package/dist/config/config.js +50 -6
  4. package/dist/config/register.js +18 -5
  5. package/dist/shared/content-tokens.d.ts +16 -0
  6. package/dist/shared/content-tokens.js +137 -0
  7. package/dist/task/accept-debt.d.ts +31 -1
  8. package/dist/task/accept-debt.js +49 -3
  9. package/dist/task/artifact-closure.js +7 -1
  10. package/dist/task/auto-io.d.ts +66 -7
  11. package/dist/task/auto-io.js +152 -47
  12. package/dist/task/auto-orchestrator.d.ts +22 -19
  13. package/dist/task/auto-orchestrator.js +184 -112
  14. package/dist/task/child-runner.d.ts +18 -0
  15. package/dist/task/child-runner.js +7 -8
  16. package/dist/task/command-run.d.ts +4 -0
  17. package/dist/task/command-run.js +4 -2
  18. package/dist/task/constraint-policy.d.ts +52 -0
  19. package/dist/task/constraint-policy.js +66 -0
  20. package/dist/task/context-silence.d.ts +5 -4
  21. package/dist/task/context-silence.js +13 -5
  22. package/dist/task/coverage-loop.d.ts +7 -4
  23. package/dist/task/coverage-loop.js +16 -138
  24. package/dist/task/debug-log.d.ts +25 -2
  25. package/dist/task/debug-log.js +64 -10
  26. package/dist/task/decompose-fidelity.d.ts +17 -4
  27. package/dist/task/decompose-fidelity.js +95 -55
  28. package/dist/task/decompose-granularity.d.ts +7 -7
  29. package/dist/task/decompose-granularity.js +8 -8
  30. package/dist/task/env-notes.d.ts +65 -19
  31. package/dist/task/env-notes.js +152 -50
  32. package/dist/task/external-context.js +25 -5
  33. package/dist/task/fix-context.d.ts +39 -0
  34. package/dist/task/fix-context.js +58 -0
  35. package/dist/task/gate-deps.d.ts +49 -0
  36. package/dist/task/gate-deps.js +201 -39
  37. package/dist/task/gate-evidence.d.ts +51 -0
  38. package/dist/task/gate-evidence.js +90 -0
  39. package/dist/task/gate-resolution.d.ts +119 -0
  40. package/dist/task/gate-resolution.js +120 -0
  41. package/dist/task/git-state-guard.js +3 -26
  42. package/dist/task/handoff.d.ts +35 -0
  43. package/dist/task/handoff.js +100 -0
  44. package/dist/task/health-baseline.d.ts +87 -0
  45. package/dist/task/health-baseline.js +158 -0
  46. package/dist/task/health-repair.d.ts +68 -0
  47. package/dist/task/health-repair.js +124 -0
  48. package/dist/task/ledger.d.ts +10 -0
  49. package/dist/task/ledger.js +22 -11
  50. package/dist/task/lint-fix.d.ts +11 -0
  51. package/dist/task/lint-fix.js +14 -8
  52. package/dist/task/loop-detector.d.ts +52 -3
  53. package/dist/task/loop-detector.js +81 -5
  54. package/dist/task/mentions.d.ts +17 -0
  55. package/dist/task/mentions.js +86 -0
  56. package/dist/task/orchestrator.d.ts +53 -9
  57. package/dist/task/orchestrator.js +104 -28
  58. package/dist/task/orientation.d.ts +135 -29
  59. package/dist/task/orientation.js +259 -67
  60. package/dist/task/owned-freeze-reassign.d.ts +7 -3
  61. package/dist/task/owned-freeze-reassign.js +13 -5
  62. package/dist/task/parsers.d.ts +6 -1
  63. package/dist/task/parsers.js +21 -4
  64. package/dist/task/phases.d.ts +1 -1
  65. package/dist/task/phases.js +125 -153
  66. package/dist/task/plan-orchestrator.js +5 -6
  67. package/dist/task/plan-rounds.d.ts +1 -0
  68. package/dist/task/plan-rounds.js +8 -1
  69. package/dist/task/prohibition-probe.d.ts +6 -1
  70. package/dist/task/prohibition-probe.js +26 -6
  71. package/dist/task/prompts.js +13 -2
  72. package/dist/task/qa-transcript.d.ts +17 -0
  73. package/dist/task/qa-transcript.js +28 -0
  74. package/dist/task/question-source.d.ts +16 -2
  75. package/dist/task/question-source.js +21 -8
  76. package/dist/task/repo-health-check.d.ts +27 -0
  77. package/dist/task/repo-health-check.js +39 -5
  78. package/dist/task/requirements.d.ts +67 -13
  79. package/dist/task/requirements.js +129 -67
  80. package/dist/task/research-worker.d.ts +43 -2
  81. package/dist/task/research-worker.js +103 -8
  82. package/dist/task/run-bracket.d.ts +9 -4
  83. package/dist/task/run-bracket.js +9 -0
  84. package/dist/task/run-context.d.ts +152 -0
  85. package/dist/task/run-context.js +277 -0
  86. package/dist/task/spec-doc.d.ts +78 -0
  87. package/dist/task/spec-doc.js +205 -0
  88. package/dist/task/spec-model.d.ts +70 -0
  89. package/dist/task/spec-model.js +139 -0
  90. package/dist/task/stall-detector.d.ts +1 -1
  91. package/dist/task/stall-detector.js +9 -5
  92. package/dist/task/state-dir.d.ts +29 -0
  93. package/dist/task/state-dir.js +80 -0
  94. package/dist/task/suppression-probe.d.ts +67 -0
  95. package/dist/task/suppression-probe.js +78 -0
  96. package/dist/task/task-gates.d.ts +52 -58
  97. package/dist/task/task-gates.js +161 -199
  98. package/dist/task/task-io.d.ts +10 -0
  99. package/dist/task/task-io.js +69 -18
  100. package/dist/task/task-parsers.js +6 -6
  101. package/dist/task/task-types.d.ts +7 -0
  102. package/dist/task/timings.d.ts +11 -0
  103. package/dist/task/timings.js +21 -0
  104. package/dist/task/tree-hash.d.ts +8 -0
  105. package/dist/task/tree-hash.js +55 -0
  106. package/dist/task/verify-reconcile.d.ts +4 -3
  107. package/dist/task/verify-reconcile.js +7 -8
  108. package/dist/task/verify-resolution.d.ts +32 -6
  109. package/dist/task/verify-resolution.js +78 -15
  110. package/dist/task/verify-work.d.ts +105 -35
  111. package/dist/task/verify-work.js +232 -60
  112. package/dist/task/yolo.d.ts +0 -13
  113. package/dist/task/yolo.js +9 -21
  114. package/dist/workers/pi-worker-core.d.ts +20 -0
  115. package/dist/workers/pi-worker-core.js +16 -11
  116. package/dist/workers/pi-worker-docs.d.ts +6 -5
  117. package/dist/workers/pi-worker-docs.js +8 -7
  118. package/dist/workers/pi-worker-fetch.d.ts +3 -2
  119. package/dist/workers/pi-worker-fetch.js +5 -4
  120. package/dist/workers/pi-worker-search.js +5 -4
  121. package/dist/workers/research-cache.d.ts +23 -1
  122. package/dist/workers/research-cache.js +63 -3
  123. package/dist/workers/shared.d.ts +18 -0
  124. package/dist/workers/shared.js +38 -22
  125. package/package.json +1 -1
@@ -12,10 +12,11 @@
12
12
  * Mechanism (contracts.ts pattern, applied to decompose itself): the decompose
13
13
  * prompt asks each task line to cite its origin as a trailing
14
14
  * `[source: "<verbatim spec line>"]`. The host then:
15
- * 1. GROUNDS the quote — a citation that is not a (whitespace/case-normalised)
16
- * substring of the source doc is fabricated and is stripped, never trusted;
15
+ * 1. GROUNDS the quote in a BLOCK of the parsed spec doc — a citation that is
16
+ * not a (whitespace/case-normalised) substring is fabricated and is
17
+ * stripped, never trusted — and remembers which block it landed in;
17
18
  * 2. deterministically detects DROPPED ADDITIVE FRAGMENTS: the `+`-joined
18
- * trailing constraints of the cited line ("… + tests") whose words are
19
+ * trailing constraints of the cited block ("… + tests") whose words are
19
20
  * absent from the title;
20
21
  * 3. RE-ATTACHES the missing fragments to the title verbatim.
21
22
  *
@@ -30,41 +31,9 @@
30
31
  * presence is exact word membership (with a singular/plural `s` allowance).
31
32
  */
32
33
  import { normalise } from './contracts.js';
34
+ import { demark, groundIn, parseSpecDoc, specPlain } from './spec-doc.js';
33
35
  /** One trailing `[source: "…"]` clause, anchored so it is the WHOLE remainder. */
34
36
  const SOURCE_RE = /^\[source:\s*"([\s\S]*)"\s*\]$/i;
35
- /**
36
- * Markdown MARKUP dropped before grounding — emphasis runs, list and heading
37
- * markers, table pipes and CODE BACKTICKS. Not content: no word, number or
38
- * punctuation inside a sentence is touched, so this cannot make an invented
39
- * quote match.
40
- *
41
- * WHY. A model copies a spec line as it READS, and what it reads is rendered:
42
- * `2. **Auth** — sessions, login/logout/me, guards + tests.` comes back as
43
- * `Auth — sessions, login/logout/me, guards + tests.` That is a verbatim copy of
44
- * the line's TEXT, and the exact-substring test called it fabricated and threw
45
- * it away — including, as here, the "+ tests" line that is this module's own
46
- * worked example.
47
- *
48
- * BACKTICKS ARE THE SAME CLASS and were the larger half. A code span renders as
49
- * bare text, so `3. **Invites** — create/validate/redeem, \`/join/:token\` page.`
50
- * comes back as `Invites — create/validate/redeem, /join/:token page.` Screening
51
- * every spec line in its RENDERED form is what makes those quotes match at all.
52
- *
53
- * The two directions this has to hold in, both run:
54
- * FLOOR a real spec line with ONE content word altered must NOT be grounded —
55
- * changing `sessions` to `tokens`, or `redeem` to `revoke`, drops it.
56
- * CEILING a real spec line quoted without its markup MUST be grounded — both the
57
- * `2. **Auth** —` numbering-and-bold case and the backticked
58
- * `` `/join/:token` `` case still match.
59
- */
60
- function demark(s) {
61
- return s
62
- .replace(/\*\*|__/g, '')
63
- .replace(/^\s*(?:[-*+]|\d+\.)\s+/gm, '')
64
- .replace(/^#+\s*/gm, '')
65
- .replace(/`/g, '')
66
- .replace(/\|/g, ' ');
67
- }
68
37
  /**
69
38
  * Undo the backslash-escaping a model applies to a quote it is putting INSIDE a
70
39
  * double-quoted clause. `[source: "… \`import { sql } from \\"bun\\"\` gotcha …"]`
@@ -99,7 +68,8 @@ function unescapeQuotes(s) {
99
68
  * — exactly like keepGroundedContracts rejects a paraphrased quote.
100
69
  */
101
70
  export function extractTitleSource(title, sourceDoc) {
102
- const ref = normalise(demark(sourceDoc));
71
+ const doc = typeof sourceDoc === 'string' ? parseSpecDoc(sourceDoc) : sourceDoc;
72
+ const flat = normalise(specPlain(doc));
103
73
  let base = title.trim();
104
74
  const sources = [];
105
75
  for (;;) {
@@ -111,8 +81,16 @@ export function extractTitleSource(title, sourceDoc) {
111
81
  break;
112
82
  const quote = m[1].trim();
113
83
  base = base.slice(0, at).trim();
114
- if (quote.length > 0 && ref.includes(normalise(demark(unescapeQuotes(quote)))))
115
- sources.unshift(quote);
84
+ if (quote.length === 0)
85
+ continue;
86
+ const unescaped = unescapeQuotes(quote);
87
+ const block = groundIn(doc, unescaped);
88
+ if (block !== null)
89
+ sources.unshift({ quote, block });
90
+ // A citation that spans two blocks is still a faithful copy of the doc, so
91
+ // it stays grounded; it just has no single block to restore fragments from.
92
+ else if (flat.includes(normalise(demark(unescaped))))
93
+ sources.unshift({ quote, block: null });
116
94
  }
117
95
  return { base, sources };
118
96
  }
@@ -132,7 +110,57 @@ function fragmentPresent(fragment, titleWords) {
132
110
  });
133
111
  }
134
112
  /**
135
- * The `+`-joined trailing constraint fragments of `sourceLine` whose words are
113
+ * An additive suffix is a PROSE construct. Inside a table row a `+` joins one
114
+ * cell's own words while the row's other cells are unrelated columns; inside a
115
+ * code fence it is code. Live, those two shapes restored `password) | public |`
116
+ * (a router table's path, guard and its pipes) and `check` (the tail of a DDL
117
+ * `check (…)` constraint) into task titles as spec obligations.
118
+ */
119
+ const RESTORABLE_KINDS = new Set(['para', 'list-item']);
120
+ const BRACKET_PAIRS = [
121
+ ['(', ')'],
122
+ ['[', ']'],
123
+ ['{', '}']
124
+ ];
125
+ function occurrences(s, ch) {
126
+ let n = 0;
127
+ for (const c of s)
128
+ if (c === ch)
129
+ n++;
130
+ return n;
131
+ }
132
+ /** A fragment cut mid-expression: the cut is the defect, not the content. */
133
+ function isBalanced(s) {
134
+ return BRACKET_PAIRS.every(([open, close]) => occurrences(s, open) === occurrences(s, close));
135
+ }
136
+ /**
137
+ * Split on the separators that sit at bracket depth ZERO. A `+` or a comma inside
138
+ * a parenthetical belongs to the parenthetical — splitting there is what cut
139
+ * `(unused + not expired)` into a fragment with a dangling `)`.
140
+ *
141
+ * A stray closer floors the depth at zero rather than driving it negative, so a
142
+ * line whose brackets are already unbalanced still splits on its later separators.
143
+ */
144
+ function splitAtDepth(s, separators) {
145
+ const out = [];
146
+ let depth = 0;
147
+ let start = 0;
148
+ for (let i = 0; i < s.length; i++) {
149
+ const c = s[i];
150
+ if (BRACKET_PAIRS.some(([open]) => open === c))
151
+ depth++;
152
+ else if (BRACKET_PAIRS.some(([, close]) => close === c))
153
+ depth = Math.max(0, depth - 1);
154
+ else if (depth === 0 && separators.includes(c)) {
155
+ out.push(s.slice(start, i));
156
+ start = i + 1;
157
+ }
158
+ }
159
+ out.push(s.slice(start));
160
+ return out;
161
+ }
162
+ /**
163
+ * The `+`-joined trailing constraint fragments of a spec block whose words are
136
164
  * absent from `title`. "2. **Auth** — sessions, login/logout/me, guards + tests."
137
165
  * yields the fragment "tests"; a title that never mentions tests gets it back.
138
166
  * Fragments before the first `+` are the task's body — a title paraphrases those
@@ -140,31 +168,40 @@ function fragmentPresent(fragment, titleWords) {
140
168
  * ("+ Tailwind v4 tokens, nav, router" is three constraints), so a title missing
141
169
  * one of them gets ONLY that one restored, not the whole phrase (measured live:
142
170
  * whole-phrase restoration re-attached text the title already carried).
171
+ *
172
+ * A bare string is read as prose — the shape a caller that has no document has.
143
173
  */
144
- export function findDroppedPlusFragments(sourceLine, title) {
145
- const parts = sourceLine.split('+');
174
+ export function findDroppedPlusFragments(source, title) {
175
+ const block = typeof source === 'string' ?
176
+ { kind: 'para', text: source, plain: demark(source), line: 0 }
177
+ : source;
178
+ if (!RESTORABLE_KINDS.has(block.kind))
179
+ return [];
180
+ const parts = splitAtDepth(block.plain, '+');
146
181
  if (parts.length < 2)
147
182
  return [];
148
183
  const titleWords = new Set(words(title));
149
184
  const missing = [];
150
185
  for (const raw of parts.slice(1)) {
151
- for (const sub of raw.split(',')) {
152
- // A fragment runs to the next `+`/comma; strip trailing sentence
153
- // punctuation and markdown emphasis so "tests.**" compares as "tests".
154
- const fragment = sub
155
- .replace(/[*_`]/g, '')
156
- .replace(/[.,;:!?)\]]+\s*$/, '')
157
- .trim();
158
- if (fragment.length === 0)
159
- continue;
186
+ for (const sub of splitAtDepth(raw, ',')) {
187
+ // Trailing sentence punctuation is the line's, not the fragment's;
188
+ // brackets are left alone because they are what the balance test reads.
189
+ const fragment = sub.replace(/[.,;:!?]+\s*$/, '').trim();
160
190
  if (words(fragment).length === 0)
161
191
  continue;
192
+ if (!isBalanced(fragment))
193
+ continue;
162
194
  if (!fragmentPresent(fragment, titleWords))
163
195
  missing.push(fragment);
164
196
  }
165
197
  }
166
198
  return missing;
167
199
  }
200
+ /** Restored fragments as a DELIMITED list. A bare `join(', ')` is ambiguous the
201
+ * moment a fragment contains a comma of its own — and they routinely do. */
202
+ export function renderFragments(fragments) {
203
+ return fragments.map(f => `"${f}"`).join('; ');
204
+ }
168
205
  /**
169
206
  * Reconcile decompose output against the source doc: ground each citation, strip
170
207
  * the clause (its job ends here), and re-attach any dropped `+`-fragments to the
@@ -173,11 +210,12 @@ export function findDroppedPlusFragments(sourceLine, title) {
173
210
  * model that never cites degrades to exactly the old behavior.
174
211
  */
175
212
  export function reconcileTitleSources(titles, sourceDoc) {
213
+ const doc = parseSpecDoc(sourceDoc);
176
214
  const out = [];
177
215
  const restored = [];
178
216
  let sourced = 0;
179
217
  for (let i = 0; i < titles.length; i++) {
180
- const { base, sources } = extractTitleSource(titles[i], sourceDoc);
218
+ const { base, sources } = extractTitleSource(titles[i], doc);
181
219
  if (sources.length === 0) {
182
220
  out.push(base);
183
221
  continue;
@@ -190,7 +228,9 @@ export function reconcileTitleSources(titles, sourceDoc) {
190
228
  const seen = new Set();
191
229
  const missing = [];
192
230
  for (const src of sources) {
193
- for (const f of findDroppedPlusFragments(src, base)) {
231
+ if (src.block === null)
232
+ continue;
233
+ for (const f of findDroppedPlusFragments(src.block, base)) {
194
234
  const k = f.toLowerCase();
195
235
  if (seen.has(k))
196
236
  continue;
@@ -202,8 +242,8 @@ export function reconcileTitleSources(titles, sourceDoc) {
202
242
  out.push(base);
203
243
  continue;
204
244
  }
205
- restored.push({ index: i, fragments: missing, sources });
206
- out.push(`${base} — MUST also cover (restored from its spec line): ${missing.join(', ')}`);
245
+ restored.push({ index: i, fragments: missing, sources: sources.map(s => s.quote) });
246
+ out.push(`${base} — MUST also cover (restored from its spec line): ${renderFragments(missing)}`);
207
247
  }
208
248
  return { titles: out, restored, sourced };
209
249
  }
@@ -70,6 +70,8 @@ export declare function isTooCoarse(titles: number, floor: number): boolean;
70
70
  export declare const MIN_REQUIREMENTS_FOR_PLAN_SHAPE = 5;
71
71
  /** Does this feature have enough distinct deliverables for granularity to matter? */
72
72
  export declare function planShapeIsHostsToAnswer(ownable: number): boolean;
73
+ /** The clarify dialog's name for this fork — see `QuestionSource.settle`. */
74
+ export declare const PLAN_SHAPE_TOPIC = "plan-shape";
73
75
  /**
74
76
  * Does this clarify question decide how finely the feature is CUT into tasks?
75
77
  *
@@ -78,13 +80,11 @@ export declare function planShapeIsHostsToAnswer(ownable: number): boolean;
78
80
  * over-eager classifier would replace a real user decision with the host's.
79
81
  * Matched against the plain-text question.
80
82
  *
81
- * BOTH halves must hold — a breakdown phrase AND a plan-unit noun and the unit
82
- * list is SINGULAR except for tasks. Measured across the units it names:
83
- * milestone / section / step / phase / task / tasks fire
84
- * milestones / sections / steps / phases do NOT
85
- * So "one task per milestone, or split smaller?" fires, while the same fork
86
- * phrased "follow the milestones as-is, or split more granularly?" does not —
87
- * the breakdown half matches, the plural unit does not.
83
+ * BOTH halves must hold — a breakdown phrase AND a plan-unit noun, singular or
84
+ * plural. The plural used to be excluded, which left "follow the milestones
85
+ * as-is, or split more granularly?" unmatched: the same fork, redrawn in the
86
+ * plural, went to the user (or the triage) after the host had already settled it.
87
+ * Number is not a signal about what the question decides.
88
88
  */
89
89
  export declare function isPlanShapeQuestion(question: string): boolean;
90
90
  /**
@@ -78,6 +78,8 @@ export const MIN_REQUIREMENTS_FOR_PLAN_SHAPE = 5;
78
78
  export function planShapeIsHostsToAnswer(ownable) {
79
79
  return ownable >= MIN_REQUIREMENTS_FOR_PLAN_SHAPE;
80
80
  }
81
+ /** The clarify dialog's name for this fork — see `QuestionSource.settle`. */
82
+ export const PLAN_SHAPE_TOPIC = 'plan-shape';
81
83
  /**
82
84
  * Does this clarify question decide how finely the feature is CUT into tasks?
83
85
  *
@@ -86,13 +88,11 @@ export function planShapeIsHostsToAnswer(ownable) {
86
88
  * over-eager classifier would replace a real user decision with the host's.
87
89
  * Matched against the plain-text question.
88
90
  *
89
- * BOTH halves must hold — a breakdown phrase AND a plan-unit noun and the unit
90
- * list is SINGULAR except for tasks. Measured across the units it names:
91
- * milestone / section / step / phase / task / tasks fire
92
- * milestones / sections / steps / phases do NOT
93
- * So "one task per milestone, or split smaller?" fires, while the same fork
94
- * phrased "follow the milestones as-is, or split more granularly?" does not —
95
- * the breakdown half matches, the plural unit does not.
91
+ * BOTH halves must hold — a breakdown phrase AND a plan-unit noun, singular or
92
+ * plural. The plural used to be excluded, which left "follow the milestones
93
+ * as-is, or split more granularly?" unmatched: the same fork, redrawn in the
94
+ * plural, went to the user (or the triage) after the host had already settled it.
95
+ * Number is not a signal about what the question decides.
96
96
  */
97
97
  export function isPlanShapeQuestion(question) {
98
98
  const q = question.toLowerCase();
@@ -101,7 +101,7 @@ export function isPlanShapeQuestion(question) {
101
101
  if (!aboutBreakdown)
102
102
  return false;
103
103
  // …and offer a coarse/fine choice over the plan's own units.
104
- return /\b(milestone|section|step|phase|task|tasks)\b/.test(q);
104
+ return /\b(milestones?|sections?|steps?|phases?|tasks?)\b/.test(q);
105
105
  }
106
106
  /**
107
107
  * BELT — the host's own answer to that fork, recorded in the clarify transcript in
@@ -1,48 +1,94 @@
1
- /** One recorded fact plus the origin task that established it (may be ''). */
1
+ /**
2
+ * One recorded fact, the origin task that established it, the run it belongs to,
3
+ * and the SUBJECT it is about.
4
+ *
5
+ * The subject is the ledger key, and that is the whole point: twenty children
6
+ * re-measuring one database's reachability wrote twenty slots of one fact, and the
7
+ * oldest, least true of them was as loud as the newest. Under a subject key the
8
+ * latest measurement REPLACES its predecessor, which is also the only retraction
9
+ * mechanism a fact cache has.
10
+ */
2
11
  export interface EnvNote {
3
12
  fact: string;
4
13
  origin: string;
14
+ /** The run that recorded it; '' for a file written before runs were stamped. */
15
+ runId: string;
16
+ subject: string;
17
+ /** The task that retracted this claim. A resolved note stays as an audit line
18
+ * and is never carried into another child's prompt. */
19
+ resolvedBy?: string;
5
20
  }
6
- /**
7
- * Parse the stored file into fact+origin records. A line with no separator parses
8
- * with an empty origin rather than being dropped, so a cache written before
9
- * provenance existed still reads — and `buildEnvNotesBlock` renders those as
10
- * "origin unrecorded" instead of silently claiming an author.
11
- */
21
+ /** Stored line: `fact TAB origin TAB runId TAB subject TAB resolvedBy`, where every
22
+ * field after the fact may be absent a one- or two-field line is what earlier
23
+ * versions wrote, and it still reads. */
12
24
  export declare function parseEnvNotes(raw: string): EnvNote[];
13
25
  export declare function envNotesFile(cwd: string): string;
14
26
  /** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
15
27
  export declare function readEnvNotes(cwd: string): Promise<string>;
16
28
  /**
17
- * Pull `ENV-NOTE: <fact>` lines out of a child's answer text: trimmed,
18
- * deduplicated case-insensitively, and dropped when empty or over
19
- * MAX_NOTE_LENGTH. Run: a `VERDICT:` line and even a near-miss `ENV-NOTES:` line
29
+ * The subject of a fact the child did not name one for.
30
+ *
31
+ * A fact identifies what it is ABOUT by naming it first, and three rungs cover
32
+ * what an environment fact names: the SERVICE it reached (a URL authority), the
33
+ * COMMAND it ran (the first code span, by its basename so a tool named by path is
34
+ * the same tool), or — for the rest — its leading words.
35
+ *
36
+ * Measured against the 40-note cache a real 21-task run left behind, where 34
37
+ * slots restate two facts: it resolves them to nine subjects, so the twelfth
38
+ * re-measurement of one database replaces the eleventh instead of joining it.
39
+ */
40
+ export declare function deriveSubject(fact: string): string;
41
+ /** One thing a child shared: a fact, or the retraction of one. */
42
+ export interface EmittedNote {
43
+ subject: string;
44
+ fact: string;
45
+ /** The child observed this subject's recorded claim to be no longer true. */
46
+ resolved?: true;
47
+ }
48
+ /**
49
+ * Pull the facts out of a child's answer text. Three forms are accepted:
50
+ * `ENV-NOTE[<subject>]: <fact>` (what the instruction asks for), the bare
51
+ * `ENV-NOTE: <fact>` whose subject the host derives, and
52
+ * `ENV-NOTE-RESOLVED[<subject>]: <what was observed>`, which retracts a recorded
53
+ * claim. Facts are trimmed, deduped case-insensitively, and dropped when empty or
54
+ * over MAX_NOTE_LENGTH. A `VERDICT:` line and even a near-miss `ENV-NOTES:` line
20
55
  * match nothing, so a verdict cannot leak into the fact cache.
21
56
  */
22
- export declare function extractEnvNotes(text: string): string[];
57
+ export declare function extractEnvNotes(text: string): EmittedNote[];
23
58
  /** True when a fact reads like a standing excuse (see EXCUSE_PATTERNS). */
24
59
  export declare function isExcuseNote(fact: string): boolean;
25
60
  /**
26
- * Append newly discovered facts to the cache, deduplicated against what is
27
- * already there (case-insensitive fact match), keeping the newest MAX_NOTES.
28
- * Each new fact is stamped with the `origin` task that recorded it; a fact already
29
- * present keeps its ORIGINAL origin, so provenance traces to whoever first
30
- * established it. Confirmed by re-appending the same fact under a different task:
31
- * one entry survives, still carrying the first writer.
61
+ * Store what a child shared, keyed by SUBJECT: a fresh statement about a subject
62
+ * REPLACES the one the cache held, so the newest measurement is the one later
63
+ * children read and a stale fact cannot outlive its correction. Each note is
64
+ * stamped with the `origin` task and the `runId` that recorded it — the run is
65
+ * what the prompt block scopes on, the task is the provenance a reader is told to
66
+ * re-validate against.
67
+ *
68
+ * A retraction stores the observation that closed the subject and marks it
69
+ * RESOLVED, so it stays as an audit line without being carried into another
70
+ * child's prompt.
32
71
  *
33
72
  * Tabs in an emitted fact are normalised to spaces before storage, which is what
34
73
  * keeps the separator unambiguous. The cap holds too — sixty further facts leave
35
74
  * MAX_NOTES stored. Failures are swallowed: the cache is a sharpener, never a
36
75
  * blocker.
37
76
  */
38
- export declare function appendEnvNotes(cwd: string, notes: string[], origin?: string): Promise<void>;
77
+ export declare function appendEnvNotes(cwd: string, notes: readonly EmittedNote[], origin?: string, runId?: string): Promise<void>;
39
78
  /**
40
79
  * The prompt block a gate child receives when notes exist. Two things are
41
80
  * load-bearing: the no-waiver caveat (facts save re-discovery time but grant no
42
81
  * license to prepare/repair) and the trust discipline (a note is second-hand
43
82
  * until re-validated; an EXCUSE-CLASS note may not wave off a failure without a
44
83
  * live re-check; a grep of a generated artifact is not evidence of absence).
84
+ *
85
+ * THIS RUN leads. A fact measured by a sibling task minutes ago describes the tree
86
+ * the child is standing in; one from a run last week describes a tree that has
87
+ * moved since, and reading the two as one list is how a stale fact gets cited as
88
+ * current. Resolved subjects are dropped from both lists: a retracted claim is
89
+ * exactly the thing that must not be carried forward. `runId` '' (a cache written
90
+ * before runs were stamped) reads as an earlier run.
45
91
  */
46
- export declare function buildEnvNotesBlock(raw: string): string;
92
+ export declare function buildEnvNotesBlock(raw: string, runId?: string): string;
47
93
  /** The emit instruction appended to bash-capable gate-child prompts. */
48
94
  export declare const ENV_NOTE_EMIT_INSTRUCTION: string;