@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.
Files changed (123) 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 +15 -1
  8. package/dist/task/accept-debt.js +4 -0
  9. package/dist/task/artifact-closure.js +7 -1
  10. package/dist/task/auto-io.d.ts +59 -7
  11. package/dist/task/auto-io.js +140 -47
  12. package/dist/task/auto-orchestrator.d.ts +22 -19
  13. package/dist/task/auto-orchestrator.js +126 -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 +199 -38
  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/ledger.d.ts +10 -0
  47. package/dist/task/ledger.js +22 -11
  48. package/dist/task/lint-fix.d.ts +11 -0
  49. package/dist/task/lint-fix.js +14 -8
  50. package/dist/task/loop-detector.d.ts +52 -3
  51. package/dist/task/loop-detector.js +81 -5
  52. package/dist/task/mentions.d.ts +17 -0
  53. package/dist/task/mentions.js +86 -0
  54. package/dist/task/orchestrator.d.ts +53 -9
  55. package/dist/task/orchestrator.js +104 -28
  56. package/dist/task/orientation.d.ts +135 -29
  57. package/dist/task/orientation.js +259 -67
  58. package/dist/task/owned-freeze-reassign.d.ts +7 -3
  59. package/dist/task/owned-freeze-reassign.js +13 -5
  60. package/dist/task/parsers.d.ts +6 -1
  61. package/dist/task/parsers.js +21 -4
  62. package/dist/task/phases.d.ts +1 -1
  63. package/dist/task/phases.js +125 -153
  64. package/dist/task/plan-orchestrator.js +5 -6
  65. package/dist/task/plan-rounds.d.ts +1 -0
  66. package/dist/task/plan-rounds.js +8 -1
  67. package/dist/task/prohibition-probe.d.ts +6 -1
  68. package/dist/task/prohibition-probe.js +26 -6
  69. package/dist/task/prompts.js +13 -2
  70. package/dist/task/qa-transcript.d.ts +17 -0
  71. package/dist/task/qa-transcript.js +28 -0
  72. package/dist/task/question-source.d.ts +16 -2
  73. package/dist/task/question-source.js +21 -8
  74. package/dist/task/repo-health-check.d.ts +27 -0
  75. package/dist/task/repo-health-check.js +39 -5
  76. package/dist/task/requirements.d.ts +67 -13
  77. package/dist/task/requirements.js +129 -67
  78. package/dist/task/research-worker.d.ts +43 -2
  79. package/dist/task/research-worker.js +103 -8
  80. package/dist/task/run-bracket.d.ts +9 -4
  81. package/dist/task/run-bracket.js +9 -0
  82. package/dist/task/run-context.d.ts +152 -0
  83. package/dist/task/run-context.js +277 -0
  84. package/dist/task/spec-doc.d.ts +78 -0
  85. package/dist/task/spec-doc.js +205 -0
  86. package/dist/task/spec-model.d.ts +70 -0
  87. package/dist/task/spec-model.js +139 -0
  88. package/dist/task/stall-detector.d.ts +1 -1
  89. package/dist/task/stall-detector.js +9 -5
  90. package/dist/task/state-dir.d.ts +29 -0
  91. package/dist/task/state-dir.js +80 -0
  92. package/dist/task/suppression-probe.d.ts +67 -0
  93. package/dist/task/suppression-probe.js +78 -0
  94. package/dist/task/task-gates.d.ts +39 -57
  95. package/dist/task/task-gates.js +115 -195
  96. package/dist/task/task-io.d.ts +10 -0
  97. package/dist/task/task-io.js +69 -18
  98. package/dist/task/task-parsers.js +6 -6
  99. package/dist/task/task-types.d.ts +7 -0
  100. package/dist/task/timings.d.ts +11 -0
  101. package/dist/task/timings.js +21 -0
  102. package/dist/task/tree-hash.d.ts +8 -0
  103. package/dist/task/tree-hash.js +55 -0
  104. package/dist/task/verify-reconcile.d.ts +4 -3
  105. package/dist/task/verify-reconcile.js +7 -8
  106. package/dist/task/verify-resolution.d.ts +28 -5
  107. package/dist/task/verify-resolution.js +70 -13
  108. package/dist/task/verify-work.d.ts +98 -35
  109. package/dist/task/verify-work.js +227 -60
  110. package/dist/task/yolo.d.ts +0 -13
  111. package/dist/task/yolo.js +9 -21
  112. package/dist/workers/pi-worker-core.d.ts +20 -0
  113. package/dist/workers/pi-worker-core.js +16 -11
  114. package/dist/workers/pi-worker-docs.d.ts +6 -5
  115. package/dist/workers/pi-worker-docs.js +8 -7
  116. package/dist/workers/pi-worker-fetch.d.ts +3 -2
  117. package/dist/workers/pi-worker-fetch.js +5 -4
  118. package/dist/workers/pi-worker-search.js +5 -4
  119. package/dist/workers/research-cache.d.ts +23 -1
  120. package/dist/workers/research-cache.js +63 -3
  121. package/dist/workers/shared.d.ts +18 -0
  122. package/dist/workers/shared.js +38 -22
  123. package/package.json +1 -1
@@ -292,13 +292,16 @@ GOAL
292
292
  <one paragraph>
293
293
 
294
294
  CONSTRAINTS
295
- - <bullet>
295
+ - <bullet> [from: Q<n>] ← when the constraint comes from the Q&A answer numbered <n>
296
+ - <bullet> [from: spec] ← when it comes from the refined task itself
296
297
  - …
297
298
 
298
299
  ACCEPTANCE
299
300
  - <human-readable success criterion>
300
301
  - …
301
302
 
303
+ Every CONSTRAINTS bullet ends with a \`[from: …]\` tag naming where it came from: \`[from: Q3]\` if the Q&A's answer 3 is what states it, or \`[from: spec]\` if the refined task states it. Tag a bullet ONLY when you can point at that source — a constraint you inferred yourself carries no tag, and that is the correct answer, not a gap. The tag decides how much weight the verification gates give the constraint, so a tag that names a source which does not state it turns your inference into an unwaivable rule.
304
+
302
305
  VERIFY:
303
306
  \`\`\`sh
304
307
  <runnable shell command 1>
@@ -373,6 +376,7 @@ SCOPE RULES (equally critical — do not break these):
373
376
  - Do NOT introduce new requirements, deliverables, files, scripts, hooks, configs, or acceptance criteria that are not explicitly implied by the refined task or the Q&A.
374
377
  - Do NOT broaden scope. If the refined task says "run X and report", do not turn it into "build a toolchain around X with hooks, docs, and reports".
375
378
  - CONSTRAINTS from the refined task MUST be preserved in spirit. Do not silently drop or weaken them.
379
+ - Keep each CONSTRAINTS bullet's trailing \`[from: …]\` tag exactly as it stands, and do not add one to a bullet that has none. The tag is the constraint's provenance, and the verification gates weigh it; inventing or dropping one rewrites how binding the constraint is.
376
380
  - If the spec below is malformed, empty, or wrapped in a heredoc, reconstruct it from the refined task and Q&A — not from your own invention.
377
381
  - Your job is to tighten language, sharpen acceptance criteria, and ensure VERIFY is runnable. Not to redesign the task.
378
382
  - WIRING vs pinned facts: if the spec states interface wiring (a mount/route table, a module→path mapping, an exported signature, a file layout), reconcile EACH wiring specific against the design's pinned interface facts (the CROSS-SLICE CONTRACTS below, if present, are those facts quoted verbatim). Keep every wiring specific that reproduces the pinned facts exactly; CORRECT any that do not; and do NOT invent wiring the design leaves unspecified. Watch specifically for a "uniform" pattern (one module → one mount prefix, one naming scheme) applied to an interface whose pinned facts are NOT uniform — that is a seam bug, fix only the entry that breaks, and leave the conforming entries unchanged.
@@ -412,13 +416,20 @@ YOU MAY ONLY READ. Do NOT execute any of the listed commands, not even with --he
412
416
  Output exactly two sections:
413
417
 
414
418
  VERIFIED
415
- <command> <one-line evidence: where it was found>
419
+ <command> <class> <one-line evidence: where it was found>
416
420
  ...
417
421
 
418
422
  REJECTED
419
423
  <command> <one-line reason it can't be confirmed>
420
424
  ...
421
425
 
426
+ <class> is exactly one of:
427
+ check — analyses and EXITS on its own (lint, typecheck, test, format --check)
428
+ build — produces artifacts and EXITS on its own (build, compile, bundle)
429
+ serve — starts a process that KEEPS RUNNING until killed (dev server, watch mode, repl)
430
+ Any command that does not terminate by itself MUST be classed serve.
431
+ Separate every column with TWO OR MORE spaces.
432
+
422
433
  Do not add other sections, preamble, or commentary.
423
434
 
424
435
  TOOLING (one command per line):
@@ -38,6 +38,23 @@ export type QaKind =
38
38
  * gap — a human's own words are the baseline everything else is marked against.
39
39
  */
40
40
  export declare const QA_PROVENANCE: Record<QaKind, string>;
41
+ /**
42
+ * Recover each answer's KIND from a rendered record, so a spec's `[from: Q3]`
43
+ * tag resolves to a provenance long after the dialog's own object is gone — at
44
+ * verify time the transcript exists only as the `## grill Q&A` text in the task
45
+ * file.
46
+ *
47
+ * Index IS the question number minus one, the same statement `_render` makes.
48
+ * An answer with no suffix is `typed`: that is what the empty-string row above
49
+ * declares, and it is also the honest reading of a record whose policy stamped
50
+ * nothing — the host cannot tell a human's words from an unstamped kind, and
51
+ * `typed` is the reading that does not invent a provenance.
52
+ *
53
+ * YOLO's two kinds share one stamp, so a YOLO answer resolves to `yolo`. They
54
+ * carry the same weight in `constraint-policy.ts`, which is the only question
55
+ * this resolution is asked.
56
+ */
57
+ export declare function qaKindsFromRecord(record: string): Array<QaKind | undefined>;
41
58
  export interface QaPolicy {
42
59
  /** Kinds whose provenance appears in the RECORD — persisted, and handed on. */
43
60
  record: ReadonlySet<QaKind>;
@@ -31,6 +31,34 @@ export const QA_PROVENANCE = {
31
31
  accepted: '(accepted recommendation)',
32
32
  typed: ''
33
33
  };
34
+ /**
35
+ * Recover each answer's KIND from a rendered record, so a spec's `[from: Q3]`
36
+ * tag resolves to a provenance long after the dialog's own object is gone — at
37
+ * verify time the transcript exists only as the `## grill Q&A` text in the task
38
+ * file.
39
+ *
40
+ * Index IS the question number minus one, the same statement `_render` makes.
41
+ * An answer with no suffix is `typed`: that is what the empty-string row above
42
+ * declares, and it is also the honest reading of a record whose policy stamped
43
+ * nothing — the host cannot tell a human's words from an unstamped kind, and
44
+ * `typed` is the reading that does not invent a provenance.
45
+ *
46
+ * YOLO's two kinds share one stamp, so a YOLO answer resolves to `yolo`. They
47
+ * carry the same weight in `constraint-policy.ts`, which is the only question
48
+ * this resolution is asked.
49
+ */
50
+ export function qaKindsFromRecord(record) {
51
+ const kinds = [];
52
+ for (const line of record.split('\n')) {
53
+ const answer = /^A(\d+):\s*(.*)$/.exec(line);
54
+ if (!answer)
55
+ continue;
56
+ const text = answer[2].trimEnd();
57
+ const hit = Object.entries(QA_PROVENANCE).find(([, suffix]) => suffix.length > 0 && text.endsWith(suffix));
58
+ kinds[Number(answer[1]) - 1] = hit?.[0] ?? 'typed';
59
+ }
60
+ return kinds;
61
+ }
34
62
  /**
35
63
  * GRILL: stamps `auto` and both YOLO kinds in the record, and shows the generator
36
64
  * nothing.
@@ -52,6 +52,17 @@ export interface QuestionRule {
52
52
  /** Applied only when the defect survived its one re-prompt. */
53
53
  repair?: (q: ClarifyQuestion) => ClarifyQuestion;
54
54
  }
55
+ /**
56
+ * A decision the caller may SETTLE outside the dialog. The generator is
57
+ * stateless, so being told "already answered" in the transcript does not stop it
58
+ * re-drawing the same fork reworded; `match` is how the source recognises the
59
+ * re-draw and drops it.
60
+ */
61
+ export interface QuestionTopic {
62
+ id: string;
63
+ /** Matched against the plain-text question. */
64
+ match: (plain: string) => boolean;
65
+ }
55
66
  export interface QuestionSourceDeps {
56
67
  /**
57
68
  * Ask the model for the next question. `hint` is the corrective re-prompt to
@@ -64,6 +75,8 @@ export interface QuestionSourceDeps {
64
75
  /** Ordered quality rules. At most ONE fires per question: they share a single
65
76
  * corrective re-prompt, spent by the first rule that detects a defect. */
66
77
  rules?: ReadonlyArray<QuestionRule>;
78
+ /** Topics `settle` can close. A topic the caller never settles costs nothing. */
79
+ topics?: ReadonlyArray<QuestionTopic>;
67
80
  cap?: number;
68
81
  log?: (msg: string) => void;
69
82
  }
@@ -81,11 +94,12 @@ export type NextQuestion = {
81
94
  *
82
95
  * The interface is one method. Behind it: the cap, the duplicate backstop and its
83
96
  * strike budget, the NONE-vs-unparseable distinction, `pickQuestion`, the one-shot
84
- * budget shared by every quality rule, and the hint precedence between a format
85
- * re-prompt and a duplicate re-prompt.
97
+ * budget shared by every quality rule, the hint precedence between a format
98
+ * re-prompt and a duplicate re-prompt, and the settled-topic filter.
86
99
  */
87
100
  export declare function makeQuestionSource(deps: QuestionSourceDeps): {
88
101
  next: () => Promise<NextQuestion>;
89
102
  asked: () => ReadonlyArray<string>;
90
103
  reopen: () => void;
104
+ settle: (topic: string) => void;
91
105
  };
@@ -47,12 +47,14 @@ export function pickQuestion(parsed) {
47
47
  *
48
48
  * The interface is one method. Behind it: the cap, the duplicate backstop and its
49
49
  * strike budget, the NONE-vs-unparseable distinction, `pickQuestion`, the one-shot
50
- * budget shared by every quality rule, and the hint precedence between a format
51
- * re-prompt and a duplicate re-prompt.
50
+ * budget shared by every quality rule, the hint precedence between a format
51
+ * re-prompt and a duplicate re-prompt, and the settled-topic filter.
52
52
  */
53
53
  export function makeQuestionSource(deps) {
54
54
  const cap = deps.cap ?? MAX_DIALOG_QUESTIONS;
55
55
  const rules = deps.rules ?? [];
56
+ const topics = deps.topics ?? [];
57
+ const settled = new Set();
56
58
  const asked = [];
57
59
  let dupStrikes = 0;
58
60
  let dupHint = null;
@@ -92,12 +94,18 @@ export function makeQuestionSource(deps) {
92
94
  }
93
95
  let picked = pickQuestion(parsed);
94
96
  const plain = stripInlineMarkdown(picked.question);
95
- // The duplicate backstop runs BEFORE any quality re-prompt: a question
96
- // about to be discarded as a re-ask must not first buy itself an extra
97
- // child call to be polished.
98
- if (isDuplicateQuestion(asked, plain)) {
97
+ // A SETTLED topic is filtered here, ahead of both the dedupe backstop
98
+ // and any quality re-prompt: the caller has already recorded an answer,
99
+ // so this draw must not be shown, polished, or charged to the cap. It
100
+ // does spend a duplicate strike, because it IS one — a re-ask of a
101
+ // decision already in the transcript — and that budget is what stops a
102
+ // generator that can only redraw this fork from looping forever.
103
+ const settledTopic = topics.find(t => settled.has(t.id) && t.match(plain));
104
+ if (settledTopic || isDuplicateQuestion(asked, plain)) {
99
105
  dupStrikes++;
100
- deps.log?.(`duplicate question, strike ${dupStrikes}/${MAX_DUP_STRIKES}`);
106
+ deps.log?.(settledTopic ?
107
+ `settled topic "${settledTopic.id}" re-asked, strike ${dupStrikes}/${MAX_DUP_STRIKES}`
108
+ : `duplicate question, strike ${dupStrikes}/${MAX_DUP_STRIKES}`);
101
109
  hint = null;
102
110
  if (dupStrikes >= MAX_DUP_STRIKES)
103
111
  return { kind: 'exhausted', why: 'dups' };
@@ -141,5 +149,10 @@ export function makeQuestionSource(deps) {
141
149
  dupStrikes = 0;
142
150
  dupHint = null;
143
151
  }
144
- return { next, asked: () => asked, reopen };
152
+ /** Close a topic the caller answered itself. Idempotent; an unknown id is a
153
+ * no-op, so a caller may settle a topic it declared nothing for. */
154
+ function settle(topic) {
155
+ settled.add(topic);
156
+ }
157
+ return { next, asked: () => asked, reopen, settle };
145
158
  }
@@ -1,4 +1,21 @@
1
1
  import { type CommandRunner } from './command-run.js';
2
+ /**
3
+ * What ONE discovered command did. `outcome` is `classifyCommandRun`'s verdict, so
4
+ * a tool that could not run at all is `skip` rather than a zero-exit pass.
5
+ *
6
+ * This exists for the DIFFERENTIAL (health-baseline.ts): "was the repo already
7
+ * failing?" is per command, not per overall verdict. Two runs can both be `ok:
8
+ * false` while a different command failed in each — a task that broke typecheck in
9
+ * a repo whose lint was already red — and the overall boolean calls that
10
+ * pre-existing.
11
+ */
12
+ export interface HealthCommandResult {
13
+ /** The command line as run, e.g. `bun run lint`. The differential's join key. */
14
+ cmd: string;
15
+ outcome: 'pass' | 'fail' | 'skip';
16
+ /** Real exit status on a `fail`; null when nothing conclusive ran. */
17
+ exitCode: number | null;
18
+ }
2
19
  export interface HealthOutcome {
3
20
  /** true → every discovered static check passed, or there was nothing to run.
4
21
  * false → a discovered command actually ran and exited non-zero. */
@@ -7,6 +24,9 @@ export interface HealthOutcome {
7
24
  reason: string;
8
25
  /** Which manifest drove discovery, or null when none was found. */
9
26
  ecosystem: string | null;
27
+ /** Every command that was REACHED, in run order. The run short-circuits on the
28
+ * first failure, so commands after it are absent rather than passing. */
29
+ commands: HealthCommandResult[];
10
30
  /**
11
31
  * First lines of the failing command's combined stderr+stdout — captured so a
12
32
  * FAIL is explainable from artifacts alone. The exit code alone does not say
@@ -20,6 +40,13 @@ export interface HealthOutcome {
20
40
  export declare function captureHealthOutput(stdout: string, stderr: string): string;
21
41
  /** One discovered command: the binary and its args, run from the repo root. */
22
42
  export type HealthCommand = [bin: string, args: string[]];
43
+ /**
44
+ * Every file `discoverHealthCommands` consults. Exported because a verified
45
+ * command is only as good as the manifest that vouched for it: `manifestHash`
46
+ * (run-context.ts) hashes exactly this set, so a project that gains a `lint`
47
+ * script invalidates the run's tooling verdicts and nothing else does.
48
+ */
49
+ export declare const HEALTH_MANIFEST_FILES: readonly ["package.json", "Makefile", "Cargo.toml", "pyproject.toml", "deno.json", "deno.jsonc", "go.mod"];
23
50
  /**
24
51
  * Discover the project's OWN whole-repo static-analysis commands. First manifest
25
52
  * that exists wins; returns only the STATIC commands actually available for that
@@ -66,6 +66,21 @@ function makeHasTarget(cwd, target) {
66
66
  return false;
67
67
  }
68
68
  }
69
+ /**
70
+ * Every file `discoverHealthCommands` consults. Exported because a verified
71
+ * command is only as good as the manifest that vouched for it: `manifestHash`
72
+ * (run-context.ts) hashes exactly this set, so a project that gains a `lint`
73
+ * script invalidates the run's tooling verdicts and nothing else does.
74
+ */
75
+ export const HEALTH_MANIFEST_FILES = [
76
+ 'package.json',
77
+ 'Makefile',
78
+ 'Cargo.toml',
79
+ 'pyproject.toml',
80
+ 'deno.json',
81
+ 'deno.jsonc',
82
+ 'go.mod'
83
+ ];
69
84
  /**
70
85
  * Discover the project's OWN whole-repo static-analysis commands. First manifest
71
86
  * that exists wins; returns only the STATIC commands actually available for that
@@ -105,7 +120,13 @@ export function discoverHealthCommands(cwd) {
105
120
  }
106
121
  /** The nothing-to-run outcome, shared by both runners. */
107
122
  function noCommandOutcome(ecosystem) {
108
- return { ok: true, reason: 'no repo-wide static-analysis command found', ecosystem, output: '' };
123
+ return {
124
+ ok: true,
125
+ reason: 'no repo-wide static-analysis command found',
126
+ ecosystem,
127
+ commands: [],
128
+ output: ''
129
+ };
109
130
  }
110
131
  /**
111
132
  * Run the discovered static checks whole-repo and let the real exit codes decide.
@@ -133,8 +154,10 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
133
154
  if (!ecosystem || cmds.length === 0)
134
155
  return noCommandOutcome(ecosystem);
135
156
  const run = opts.run ?? spawnCommand;
157
+ const commands = [];
136
158
  for (const [bin, args] of cmds) {
137
- opts.onCommand?.(`${bin} ${args.join(' ')}`);
159
+ const cmd = `${bin} ${args.join(' ')}`;
160
+ opts.onCommand?.(cmd);
138
161
  // Runner resolution: a PATH-stripped environment must not
139
162
  // silently skip the statics when the runner sits at a known install
140
163
  // location; the resolved dir also rides on PATH for the script chain.
@@ -158,14 +181,25 @@ export async function runRepoHealthCheck(cwd, opts = {}) {
158
181
  // English, so a genuine report quoting "browsers are not installed" would
159
182
  // skip the static check and certify the repo healthy.
160
183
  const verdict = classifyCommandRun(r, [], { runtimeGap: false });
161
- if (verdict.outcome !== 'fail')
184
+ if (verdict.outcome !== 'fail') {
185
+ const passed = verdict.outcome === 'pass';
186
+ commands.push({ cmd, outcome: passed ? 'pass' : 'skip', exitCode: passed ? 0 : null });
162
187
  continue;
188
+ }
189
+ commands.push({ cmd, outcome: 'fail', exitCode: verdict.status });
163
190
  return {
164
191
  ok: false,
165
- reason: `\`${bin} ${args.join(' ')}\` exited ${verdict.status}`,
192
+ reason: `\`${cmd}\` exited ${verdict.status}`,
166
193
  ecosystem,
194
+ commands,
167
195
  output: captureHealthOutput(r.stdout, r.stderr)
168
196
  };
169
197
  }
170
- return { ok: true, reason: `${ecosystem}: static checks passed`, ecosystem, output: '' };
198
+ return {
199
+ ok: true,
200
+ reason: `${ecosystem}: static checks passed`,
201
+ ecosystem,
202
+ commands,
203
+ output: ''
204
+ };
171
205
  }
@@ -1,17 +1,38 @@
1
+ import { type BlockKind, type SpecDoc } from './spec-doc.js';
1
2
  export interface RequirementEntry {
2
3
  /** The verbatim quote from the source doc — the obligation. */
3
4
  quote: string;
4
- /** Where it came from (heading/section, or 'prose'). */
5
+ /** Where it came from (heading/section, or 'prose'). Model-authored. */
5
6
  anchor: string;
7
+ /** 1-based source line of the block it grounded in — the HOST's anchor. */
8
+ line?: number;
9
+ /** Grounded above the first heading, where a doc says what the thing IS. */
10
+ preamble?: boolean;
6
11
  }
12
+ /**
13
+ * Which blocks may carry a requirement.
14
+ *
15
+ * The model still proposes whatever it likes; this decides what GROUNDING will
16
+ * accept. A fenced block is code or a file tree, and a table row is a grid of
17
+ * columns whose cells only read as a sentence by accident — both produced
18
+ * "requirements" like a DDL column definition or a router-table guard cell,
19
+ * which no task can own and which then held the coverage verdict INCOMPLETE.
20
+ */
21
+ export interface RequirementPolicy {
22
+ candidateKinds: ReadonlySet<BlockKind>;
23
+ excludePreamble: boolean;
24
+ }
25
+ export declare const REQUIREMENT_POLICY: RequirementPolicy;
7
26
  export declare function requirementsFile(cwd: string): string;
8
27
  /** Parse `REQUIREMENT: "<quote>" [anchor: …]` lines (mirrors parseContractLines). */
9
28
  export declare function parseRequirementLines(text: string): RequirementEntry[];
10
- /** THE ANTI-SYNTHESIS GUARD: keep only entries whose quote is a normalised
11
- * substring of the source doc (same rule as keepGroundedContracts). Does NOT
12
- * cap capping is capRequirements' job, which protects obligation-marked
13
- * passages from doc-order truncation. */
14
- export declare function keepGroundedRequirements(entries: RequirementEntry[], sourceDoc: string): RequirementEntry[];
29
+ /** THE ANTI-SYNTHESIS GUARD: keep only entries whose quote grounds in a CANDIDATE
30
+ * block of the source doc (same rule as keepGroundedContracts, narrowed by the
31
+ * policy). A quote whose only match lies in an excluded block is dropped — the
32
+ * model is free to propose it, the document decides. Does NOT cap — capping is
33
+ * capRequirements' job, which protects obligation-marked passages from doc-order
34
+ * truncation. */
35
+ export declare function keepGroundedRequirements(entries: RequirementEntry[], sourceDoc: string | SpecDoc, policy?: RequirementPolicy): RequirementEntry[];
15
36
  /**
16
37
  * Bound the list WITHOUT doc-order truncation. An extractor that works top-down
17
38
  * yields more entries than the cap from the doc's early sections alone, so a
@@ -48,7 +69,7 @@ export declare function isLowValueQuote(quote: string): boolean;
48
69
  * uncoveredPassages() below turns "a marked passage produced no quote" into hard
49
70
  * evidence for one forced re-extraction.
50
71
  */
51
- export declare function enumerateObligationPassages(doc: string): string[];
72
+ export declare function enumerateObligationPassages(doc: string | SpecDoc, policy?: RequirementPolicy): string[];
52
73
  /** Marked passages none of the kept quotes came from — the hard evidence that
53
74
  * extraction recall failed there (a kept quote "covers" a passage when the
54
75
  * passage contains it, normalised). */
@@ -67,6 +88,23 @@ export type ReqMapping = {
67
88
  } | {
68
89
  kind: 'none';
69
90
  };
91
+ export type RequirementClass = 'prohibition' | 'global-policy' | 'descriptive' | 'ownable';
92
+ /**
93
+ * `descriptive` is the class the coverage loop must not chase: a line that says
94
+ * what the product IS rather than what the work must do. It cost two whole
95
+ * rejected decompose rounds live — "Invite-only used-parts marketplace for a
96
+ * local Mazda MX-5 club." was extracted as a requirement, mapped NONE by every
97
+ * round because no task delivers a sentence, and held the verdict INCOMPLETE.
98
+ *
99
+ * Both marks are required, not either: preamble POSITION (above every heading,
100
+ * where a spec states its subject) and the absence of any modal. A preamble
101
+ * sentence that does carry a modal is a real obligation stated up front, and a
102
+ * modal-free sentence anywhere else is the ordinary shape of a feature statement
103
+ * — treating either alone as descriptive would empty the coverage gate.
104
+ */
105
+ export declare function classifyRequirement(quote: string, fromPreamble?: boolean): RequirementClass;
106
+ /** A requirement no single task can ever OWN, judged on the quote alone — the
107
+ * shape `groundedCoverage` and the granularity floor get. */
70
108
  export declare function isCrossCuttingRequirement(quote: string): boolean;
71
109
  /** Requirement INDICES a task owns (a `TASK n` verdict), the monotonic-replacement
72
110
  * signal (coverage-loop.ts). Index-aligned with the requirements list. */
@@ -125,9 +163,15 @@ export interface OwnedRequirement {
125
163
  /** The verbatim design quote (the obligation). */
126
164
  quote: string;
127
165
  anchor: string;
128
- /** The plan title of the task the coverage map assigned it to — matched
129
- * against the executing task's title at phase time (ids don't exist yet at
130
- * plan time, and spliced repair tasks shift them). */
166
+ /**
167
+ * The plan key (`TaskEntry.key`) of the task the coverage map assigned it to —
168
+ * the join. Allocated at plan time and stable under every later rewrite of the
169
+ * title. Absent on a ledger written before keys existed, which still joins on
170
+ * the title.
171
+ */
172
+ key?: string;
173
+ /** The plan title of the owning task. Display and legacy join only — a title
174
+ * is prose, and a task that rewords its own is still the same task. */
131
175
  title: string;
132
176
  /**
133
177
  * DETACHED: the files this obligation
@@ -149,10 +193,20 @@ export declare function readOwnedRequirements(cwd: string): Promise<OwnedRequire
149
193
  export declare function parseOwnedRequirements(text: string): OwnedRequirement[];
150
194
  /** The owned entries whose plan title matches THIS task's title (normalised
151
195
  * equality — titles travel verbatim from the plan list into task creation;
152
- * spliced repair tasks simply match nothing). A DETACHED entry (`pending`) is
153
- * owned by nobody until a task claims it, so it is never returned here — its
154
- * `title` is provenance, not ownership. */
196
+ * spliced repair tasks simply match nothing). */
155
197
  export declare function ownedForTitle(owned: OwnedRequirement[], title: string): OwnedRequirement[];
198
+ /** The owned entries assigned to THIS task's plan entry. */
199
+ export declare function ownedForKey(owned: OwnedRequirement[], key: string): OwnedRequirement[];
200
+ /**
201
+ * THIS task's owned entries. The key is the join; the title is the fallback for a
202
+ * ledger written before keys existed, and only then — once ANY entry carries a
203
+ * key the ledger is a keyed one, and a task the plan did not key (a spliced
204
+ * repair step) owns nothing, which is the same answer the title join gave it.
205
+ */
206
+ export declare function ownedForTask(owned: OwnedRequirement[], join: {
207
+ key?: string;
208
+ title: string;
209
+ }): OwnedRequirement[];
156
210
  /** The injection block for a task's OWN mapped obligations. Mirrors
157
211
  * buildRequirementsBlock (the directive pattern that measurably works) but is
158
212
  * singular in address: these are not "wherever they touch", they ARE this