@mmerterden/multi-agent-pipeline 20.23.1 → 20.24.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/CHANGELOG.md +30 -1
- package/README.md +7 -6
- package/README.tr.md +6 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +3 -3
- package/install/catalog-history.json +1 -1
- package/install/templates/claude-hooks.json +14 -1
- package/manifest.json +77 -56
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +11 -2
- package/pipeline/commands/multi-agent/postmortem/SKILL.md +78 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/lib/analysis-jira-write.sh +105 -22
- package/pipeline/lib/decisions.mjs +130 -0
- package/pipeline/lib/plan-todos.sh +156 -33
- package/pipeline/lib/task-graph.mjs +446 -0
- package/pipeline/lib/tdd-evidence.mjs +579 -0
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
- package/pipeline/multi-agent-refs/analysis-template.md +4 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/features/analysis-jira.md +51 -3
- package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +4 -2
- package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
- package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
- package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
- package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
- package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +5 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +13 -11
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
- package/pipeline/multi-agent-refs/picker-contract.md +3 -2
- package/pipeline/multi-agent-refs/response-shape.md +65 -0
- package/pipeline/rules/response-shape.md +18 -0
- package/pipeline/schemas/agent-state.schema.json +31 -1
- package/pipeline/schemas/plan-todos.schema.json +101 -0
- package/pipeline/schemas/planning-output.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +117 -1
- package/pipeline/scripts/_jsonl.mjs +151 -0
- package/pipeline/scripts/_plan-overlap.mjs +231 -0
- package/pipeline/scripts/_redact.mjs +124 -10
- package/pipeline/scripts/analysis-story-body.mjs +26 -10
- package/pipeline/scripts/analysis-story-tree.mjs +210 -9
- package/pipeline/scripts/autopilot-publish.mjs +12 -1
- package/pipeline/scripts/cost-analyze.mjs +6 -24
- package/pipeline/scripts/decisions-log.mjs +118 -0
- package/pipeline/scripts/edit-syntax-gate.mjs +240 -0
- package/pipeline/scripts/edit-syntax-gate.sh +29 -0
- package/pipeline/scripts/find-polluter.mjs +281 -0
- package/pipeline/scripts/gate-ledger.mjs +46 -13
- package/pipeline/scripts/mutation-probe.mjs +484 -0
- package/pipeline/scripts/plan-complexity.mjs +259 -0
- package/pipeline/scripts/review-decision-gate.mjs +33 -1
- package/pipeline/scripts/rework-escalation.mjs +264 -0
- package/pipeline/scripts/test-strength.mjs +5 -3
- package/pipeline/scripts/transcript-index.mjs +950 -0
- package/pipeline/scripts/validate-planning.mjs +48 -6
- package/pipeline/skills/.skill-manifest.json +11 -7
- package/pipeline/skills/.skills-index.json +12 -1
- package/pipeline/skills/shared/README.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +11 -2
- package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +66 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
- package/pipeline/skills/skills-index.md +2 -1
|
@@ -156,7 +156,8 @@ longer exists. A remembered choice is evidence about this user and this repo; an
|
|
|
156
156
|
order is a guess that happens to be sorted. Where the two agree nothing changes, and
|
|
157
157
|
where they disagree the remembered one is the answer with a reason behind it.
|
|
158
158
|
|
|
159
|
-
The run records which rule fired (`remembered` or `
|
|
159
|
+
The run records which rule fired (`remembered`, `default`, or `derived` when the answer follows from the task itself) and lists the choice in
|
|
160
|
+
`state.decisions[]` (`features/autopilot-decisions.md`). An autopilot run cannot be
|
|
160
161
|
asked anything, so the only thing that keeps it accountable is being readable afterwards.
|
|
161
162
|
|
|
162
163
|
## Background (unattended, not autopilot) contract
|
|
@@ -189,6 +190,6 @@ park it writes `agent-state.json` beside its tracker with `taskId`,
|
|
|
189
190
|
|
|
190
191
|
## Deterministic gates note
|
|
191
192
|
|
|
192
|
-
Claude Code's `PreToolUse` exit-2 hooks are the HARD blocking gates. Three ship, none needing run-specific arguments so they are naturally hookable: (1) `pre-commit-check.sh` scans the staged diff on every `git commit` and blocks on a detected secret; (2) `agent-guard.sh` runs on `git commit` + `git push` and blocks AI/assistant attribution in a commit message and force-push to a protected branch (main/master/develop); (3) `check-read-size.sh` runs on `Read` and on the shell commands that read a file whole, and routes an oversized read to a cheap worker (`bulk-read.sh`) instead of the caller's own rung. The first two inspect what a run WRITES; the third inspects what it pays to READ, and it is inert until `bulkRead.mode` is set to `observe` or `enforce`, so merging the block changes nothing until the user opts in. Its `observe` mode blocks nothing and only logs, which is how the baseline is measured before anything is routed. All three are self-contained and never execute the inspected command. On an internal error they let the call through in an attended session; `agent-guard.sh` blocks instead under `MULTI_AGENT_UNATTENDED=1`, where nobody is watching to notice a gap. Three capture hooks ship in the same block and block nothing: `SessionEnd` runs `capture-flush.sh --if-stale` (writing a killed run's findings into the per-repo stores, so they do not depend on the run reaching Phase 5) plus `note-session.sh` (the mechanical shape of a non-pipeline session: tools used, commands that failed, calls the user refused - never an argument, never any output), `PreCompact` runs `capture-flush.sh` without `--if-stale` (a compaction summarizes a long phase mid-flight, so it is the moment unflushed findings are at risk; the staleness test exists only so a session exit does not re-flush a finished run, and both store writes are idempotent), and `SessionStart` runs `capture-resume.sh`, at most two lines about an unfinished run and a stale observation queue. None of them calls a model; all exit 0 on every path. The recommended hook block ships at `install/templates/claude-hooks.json`; `multi-agent:setup` offers to merge it into `~/.claude/settings.json`. The other deterministic gates (evidence, consensus, intent, learnings) are invoked by the pipeline phases with per-run arguments (a build-log path, the triage JSON, the free-text input), so they are phase-enforced by contract, not OS-hookable.
|
|
193
|
+
Claude Code's `PreToolUse` exit-2 hooks are the HARD blocking gates. Three ship, none needing run-specific arguments so they are naturally hookable: (1) `pre-commit-check.sh` scans the staged diff on every `git commit` and blocks on a detected secret; (2) `agent-guard.sh` runs on `git commit` + `git push` and blocks AI/assistant attribution in a commit message and force-push to a protected branch (main/master/develop); (3) `check-read-size.sh` runs on `Read` and on the shell commands that read a file whole, and routes an oversized read to a cheap worker (`bulk-read.sh`) instead of the caller's own rung. The first two inspect what a run WRITES; the third inspects what it pays to READ, and it is inert until `bulkRead.mode` is set to `observe` or `enforce`, so merging the block changes nothing until the user opts in. Its `observe` mode blocks nothing and only logs, which is how the baseline is measured before anything is routed. All three are self-contained and never execute the inspected command. On an internal error they let the call through in an attended session; `agent-guard.sh` blocks instead under `MULTI_AGENT_UNATTENDED=1`, where nobody is watching to notice a gap. Three capture hooks ship in the same block and block nothing: `SessionEnd` runs `capture-flush.sh --if-stale` (writing a killed run's findings into the per-repo stores, so they do not depend on the run reaching Phase 5) plus `note-session.sh` (the mechanical shape of a non-pipeline session: tools used, commands that failed, calls the user refused - never an argument, never any output), `PreCompact` runs `capture-flush.sh` without `--if-stale` (a compaction summarizes a long phase mid-flight, so it is the moment unflushed findings are at risk; the staleness test exists only so a session exit does not re-flush a finished run, and both store writes are idempotent), and `SessionStart` runs `capture-resume.sh`, at most two lines about an unfinished run and a stale observation queue. None of them calls a model; all exit 0 on every path. One `PostToolUse` hook ships in the same block and blocks nothing either: `edit-syntax-gate.sh` on `Edit|Write|MultiEdit` parses the file the edit just wrote (`swiftc -parse`, `node --check`, `py_compile` into a temp dir, `JSON.parse`, `bash -n`; Kotlin is skipped, no parser answers inside its 2 s deadline) and hands a syntax error to the model as `additionalContext`; it is silent when a parser is missing or slow, and inert until `editSyntaxGate.enabled` is true. Every registration here is Claude Code's: Codex has a `PostToolUse` event and Copilot CLI a `postToolUse` one, but the installer registers none of the pipeline's hooks on either host, so this gate is Claude Code only for now. The recommended hook block ships at `install/templates/claude-hooks.json`; `multi-agent:setup` offers to merge it into `~/.claude/settings.json`. The other deterministic gates (evidence, consensus, intent, learnings) are invoked by the pipeline phases with per-run arguments (a build-log path, the triage JSON, the free-text input), so they are phase-enforced by contract, not OS-hookable.
|
|
193
194
|
|
|
194
195
|
Copilot CLI has no `PreToolUse` equivalent, so the secret scan there is workflow-enforced (run as a phase step, not OS-blocked) plus a CI smoke-gate step.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Reply shape
|
|
2
|
+
|
|
3
|
+
> Loaded on demand by `rules/response-shape.md`, which carries only the rules.
|
|
4
|
+
> This file holds the reasons, the word lists and the examples.
|
|
5
|
+
|
|
6
|
+
A reply is read by someone switching in from other work. The shape below lets
|
|
7
|
+
them act after reading the first and the last line, and find the detail in
|
|
8
|
+
between when they need it.
|
|
9
|
+
|
|
10
|
+
## Where it applies
|
|
11
|
+
|
|
12
|
+
| Output | Governed by |
|
|
13
|
+
|---|---|
|
|
14
|
+
| A reply in a conversation, inside or outside a run | this file |
|
|
15
|
+
| Run progress lines | the progress contract (`progress-contract.md`) |
|
|
16
|
+
| Phase 5 reports, PR bodies, Jira and Confluence text | their templates, then the humanizer skill |
|
|
17
|
+
|
|
18
|
+
The language is `prefs.global.outputLanguage`; nothing here depends on it.
|
|
19
|
+
|
|
20
|
+
## The rules, with their reasons
|
|
21
|
+
|
|
22
|
+
1. **Lead with the answer or the next action.** The first line is the one that
|
|
23
|
+
is always read. Openers that only announce the reply ("Great question",
|
|
24
|
+
"Let me take a look", "Sure! Here is...", "Harika soru", "Hemen bakıyorum")
|
|
25
|
+
spend it on nothing.
|
|
26
|
+
2. **Numbered steps, one action each.** A step that hides two actions is the
|
|
27
|
+
one that gets half done.
|
|
28
|
+
3. **At most five items per group, the rest as a count** ("and 7 more, listed
|
|
29
|
+
in the report"). This is display only. A finding, a failed check or a
|
|
30
|
+
verification caveat is never cut to fit; when they do not fit, they go to a
|
|
31
|
+
file and the reply names it. The verification rule in `CLAUDE.md` wins over
|
|
32
|
+
this one every time.
|
|
33
|
+
4. **Errors as cause, then fix.** "The build failed because X; fixed by Y" or
|
|
34
|
+
"...; needs Z from you". No apology paragraph.
|
|
35
|
+
5. **Finished work in concrete terms.** What changed (file, commit, version)
|
|
36
|
+
and how it was checked (which tests, which counts). "Done" alone is not a
|
|
37
|
+
report.
|
|
38
|
+
6. **Tangents at the end, one line.** Something noticed on the way is offered
|
|
39
|
+
("Also noticed X; want it fixed?"), not woven into the answer.
|
|
40
|
+
7. **Close with what happens next or what the user decides.** Not "Let me know
|
|
41
|
+
if you need anything else", "Hope this helps", "Başka bir şey olursa
|
|
42
|
+
söyle".
|
|
43
|
+
|
|
44
|
+
## When the task wins
|
|
45
|
+
|
|
46
|
+
- An explanation the user asked for runs as long as it needs, with headings.
|
|
47
|
+
- A destructive or outward-facing step is confirmed before it runs, whatever
|
|
48
|
+
that does to the shape.
|
|
49
|
+
- Real ambiguity gets one question, not a guess and not a questionnaire.
|
|
50
|
+
- Inside a run, the phase files and their templates decide the format; this
|
|
51
|
+
file only shapes the conversation around them.
|
|
52
|
+
|
|
53
|
+
## Example
|
|
54
|
+
|
|
55
|
+
Too long to act on:
|
|
56
|
+
|
|
57
|
+
> Great question! I looked into the failing build. It seems there might be an
|
|
58
|
+
> issue with the signing configuration, which could be caused by several
|
|
59
|
+
> things... Let me know if you want me to look further!
|
|
60
|
+
|
|
61
|
+
Shaped:
|
|
62
|
+
|
|
63
|
+
> The build fails on signing: the provisioning profile expired on 12.10.
|
|
64
|
+
> Renew it in the developer portal, then rerun `xcodebuild archive`.
|
|
65
|
+
> I can retry the archive once the new profile is installed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Reply shape
|
|
2
|
+
|
|
3
|
+
For replies in a conversation. Reports, PR, Jira and Confluence bodies keep
|
|
4
|
+
their own templates; the humanizer governs their wording.
|
|
5
|
+
|
|
6
|
+
- First line: the answer or the next action. No preamble, no recap of the ask.
|
|
7
|
+
- Steps: numbered, one action each. Items: at most five shown, the rest named
|
|
8
|
+
as a count. That limit shapes the display only; never drop a finding, a
|
|
9
|
+
failure or a verification note to fit it.
|
|
10
|
+
- Errors: the cause, then the fix. Finished work: what changed, how it was
|
|
11
|
+
checked.
|
|
12
|
+
- Tangents wait for the end, as one line.
|
|
13
|
+
- Last line: what happens next, or what the user has to decide. No closing
|
|
14
|
+
pleasantries.
|
|
15
|
+
- An explanation asked for runs as long as it needs; a destructive step is
|
|
16
|
+
confirmed first; real ambiguity gets one question.
|
|
17
|
+
|
|
18
|
+
Detail and examples: `$HOME/.claude/multi-agent-refs/response-shape.md`.
|
|
@@ -784,7 +784,7 @@
|
|
|
784
784
|
},
|
|
785
785
|
"plan": {
|
|
786
786
|
"type": ["object", "null"],
|
|
787
|
-
"description": "The Phase 1 plan as a Todo list, written by plan-todos.sh when prefs.global.planTodos.enabled is true. Shape: plan-todos.schema.json, which plan-todos.sh validates on write; absent when the opt-in is off."
|
|
787
|
+
"description": "The Phase 1 plan as a Todo list, written by plan-todos.sh when prefs.global.planTodos.enabled is true. Shape: plan-todos.schema.json, which plan-todos.sh validates on write, including the optional todos[].type, the todos[].tdd evidence record, the drift record (the todo ended off-plan) and the needsReplan flag (a step it depends on drifted; it does not start until confirm-replan); absent when the opt-in is off."
|
|
788
788
|
},
|
|
789
789
|
"analysis": {
|
|
790
790
|
"type": ["object", "null"],
|
|
@@ -1246,6 +1246,20 @@
|
|
|
1246
1246
|
"type": "string",
|
|
1247
1247
|
"enum": ["fix", "accept", "escalate"]
|
|
1248
1248
|
},
|
|
1249
|
+
"reworkEscalation": {
|
|
1250
|
+
"type": "object",
|
|
1251
|
+
"additionalProperties": false,
|
|
1252
|
+
"required": ["escalate", "fromTier", "toTier", "trigger", "reason"],
|
|
1253
|
+
"description": "Written by rework-escalation.mjs --state before the rework dispatch this review sent back: the rung it ran on and why, a refusal included (multi-agent-refs/features/rework-escalation.md).",
|
|
1254
|
+
"properties": {
|
|
1255
|
+
"escalate": { "type": "boolean" },
|
|
1256
|
+
"fromTier": { "type": "string" },
|
|
1257
|
+
"toTier": { "type": "string" },
|
|
1258
|
+
"trigger": { "enum": ["last-round", "still-present", null] },
|
|
1259
|
+
"reason": { "type": "string" },
|
|
1260
|
+
"at": { "type": "string", "format": "date-time" }
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1249
1263
|
"delta": {
|
|
1250
1264
|
"type": "object",
|
|
1251
1265
|
"additionalProperties": true,
|
|
@@ -2084,6 +2098,22 @@
|
|
|
2084
2098
|
"sha": { "type": "string" }
|
|
2085
2099
|
}
|
|
2086
2100
|
},
|
|
2101
|
+
"decisions": {
|
|
2102
|
+
"type": "array",
|
|
2103
|
+
"description": "Choices an autopilot run made where an attended run would have asked (multi-agent-refs/features/autopilot-decisions.md): parks (gate-ledger.mjs), blockers lowered to important (review-decision-gate.mjs), rework escalations (rework-escalation.mjs), pickers resolved without asking, split consensus taken on the triage verdict, a date-gate model fallback (decisions-log.mjs add). Written only while the quality gates are active. Rendered, redacted, in the Phase 5 report and appended to the autopilot PR body; an empty or absent list renders nothing.",
|
|
2104
|
+
"items": {
|
|
2105
|
+
"type": "object",
|
|
2106
|
+
"additionalProperties": false,
|
|
2107
|
+
"required": ["what", "why", "costIfWrong", "at", "phase"],
|
|
2108
|
+
"properties": {
|
|
2109
|
+
"what": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
2110
|
+
"why": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
2111
|
+
"costIfWrong": { "type": "string", "minLength": 1, "maxLength": 300 },
|
|
2112
|
+
"at": { "type": "string", "format": "date-time" },
|
|
2113
|
+
"phase": { "type": "integer", "minimum": 0, "maximum": 5 }
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2087
2117
|
"verificationFailed": {
|
|
2088
2118
|
"type": "object",
|
|
2089
2119
|
"additionalProperties": false,
|
|
@@ -58,10 +58,111 @@
|
|
|
58
58
|
"enum": ["Reuse", "Add new", "Modify"],
|
|
59
59
|
"description": "Carried from the analysis Section 14 row this step came from (Locked 16). Phase 2 reads it as an instruction: a Reuse step binds the existing file and must not write a new one. Phase 3 checks the diff against it. Absent when the step did not originate from a Files-to-Add row."
|
|
60
60
|
},
|
|
61
|
+
"type": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"enum": ["test", "code", "refactor", "docs", "chore"],
|
|
64
|
+
"description": "Carried from the planning-output task type. A docs or chore todo is exempt from the TDD evidence check (lib/tdd-evidence.mjs)."
|
|
65
|
+
},
|
|
66
|
+
"tdd": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": false,
|
|
69
|
+
"description": "TDD evidence, written by plan-todos.sh tdd-red / tdd-green / complete through lib/tdd-evidence.mjs. complete reads it according to prefs.global.tddEvidence.mode.",
|
|
70
|
+
"properties": {
|
|
71
|
+
"red": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": false,
|
|
74
|
+
"required": ["at", "failed", "total"],
|
|
75
|
+
"description": "The run before the change: at least one failing test, or none when outcome is already-passing.",
|
|
76
|
+
"properties": {
|
|
77
|
+
"at": { "type": "string", "format": "date-time" },
|
|
78
|
+
"failed": { "type": "integer", "minimum": 0 },
|
|
79
|
+
"total": { "type": "integer", "minimum": 1 },
|
|
80
|
+
"tests": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"items": { "type": "string" },
|
|
83
|
+
"description": "Failing test ids the todo named; every failing id when it named none; empty when the results carry no names."
|
|
84
|
+
},
|
|
85
|
+
"testsUnverifiable": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"description": "The todo named tests (--tests) but the results name no failing test, so the names went unchecked. Recorded in warn and off mode; enforce refuses the run instead."
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"green": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"required": ["at", "failed", "total"],
|
|
95
|
+
"description": "The run after the change, with no failures.",
|
|
96
|
+
"properties": {
|
|
97
|
+
"at": { "type": "string", "format": "date-time" },
|
|
98
|
+
"failed": { "const": 0 },
|
|
99
|
+
"total": { "type": "integer", "minimum": 1 }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"outcome": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"enum": ["red", "verified", "already-passing", "exempt", "unverified"],
|
|
105
|
+
"description": "red: RED recorded. verified: RED then GREEN. already-passing: the behaviour existed and one passing run stands for both. exempt: no evidence needed. unverified: completed in warn mode without evidence; Phase 3 notes it."
|
|
106
|
+
},
|
|
107
|
+
"reason": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Why the outcome is already-passing, exempt or unverified."
|
|
110
|
+
},
|
|
111
|
+
"selfDeclared": {
|
|
112
|
+
"type": "boolean",
|
|
113
|
+
"description": "The exemption came from the todo itself (complete --no-tests), not from its type, the task type or the test policy. Phase 3 notes it."
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
61
117
|
"estimatedMinutes": {
|
|
62
118
|
"type": "integer",
|
|
63
119
|
"minimum": 1,
|
|
64
120
|
"description": "Optional Phase 1 estimate for the step. Phase 5 compares against actual duration."
|
|
121
|
+
},
|
|
122
|
+
"files": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"items": { "type": "string" },
|
|
125
|
+
"description": "The planning-output task's files[], carried by plan-todos.sh set. The drift check compares the files a step changed against it."
|
|
126
|
+
},
|
|
127
|
+
"creates": {
|
|
128
|
+
"type": "array",
|
|
129
|
+
"items": { "type": "string" },
|
|
130
|
+
"description": "The planning-output task's creates[], carried by plan-todos.sh set. The drift check counts these files as planned, like files[]."
|
|
131
|
+
},
|
|
132
|
+
"drift": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"additionalProperties": false,
|
|
135
|
+
"required": ["outside", "notDone", "at"],
|
|
136
|
+
"description": "Set by plan-todos.sh drift when this step ended off-plan: the files it changed outside files[] and the number of planned items it did not do.",
|
|
137
|
+
"properties": {
|
|
138
|
+
"outside": { "type": "array", "items": { "type": "string" } },
|
|
139
|
+
"notDone": { "type": "integer", "minimum": 0 },
|
|
140
|
+
"at": { "type": "string", "format": "date-time" }
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"needsReplan": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"required": ["because", "at"],
|
|
147
|
+
"description": "Set by plan-todos.sh drift on every pending step that depends, directly or transitively, on a step that drifted. Until confirmedAt is set, task-graph.mjs reports the step as waiting (reason needs-replan) and plan-todos.sh start refuses it; confirm-replan sets confirmedAt.",
|
|
148
|
+
"properties": {
|
|
149
|
+
"because": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"pattern": "^t[0-9]+$",
|
|
152
|
+
"description": "The step that drifted."
|
|
153
|
+
},
|
|
154
|
+
"at": { "type": "string", "format": "date-time" },
|
|
155
|
+
"reason": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "What the drift was: files outside the plan, planned items not done, or a stated reason."
|
|
158
|
+
},
|
|
159
|
+
"confirmedAt": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"format": "date-time",
|
|
162
|
+
"description": "When someone confirmed the step's plan still holds."
|
|
163
|
+
},
|
|
164
|
+
"note": { "type": "string", "description": "What the confirmation decided." }
|
|
165
|
+
}
|
|
65
166
|
}
|
|
66
167
|
}
|
|
67
168
|
}
|
|
@@ -50,6 +50,52 @@
|
|
|
50
50
|
"type": "array",
|
|
51
51
|
"items": { "type": "string" },
|
|
52
52
|
"description": "Analysis requirement ids this task implements or tests (BR-<slug>-NN, FG-NN). spec-consistency-gate.mjs reads them, with any id in the title or acceptance criteria, to check that every requirement has a task and a test and that every task traces to a requirement."
|
|
53
|
+
},
|
|
54
|
+
"creates": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": { "type": "string", "minLength": 1 },
|
|
57
|
+
"description": "Files this task brings into existence. validate-planning.mjs fails a plan in which two tasks create the same file, and warns when a task that edits a created file is not ordered after its creator."
|
|
58
|
+
},
|
|
59
|
+
"removes": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": { "type": "string", "minLength": 1 },
|
|
62
|
+
"description": "Files or symbols this task deletes, or renames away (the old name). A name with a '/' or a file extension is a file. validate-planning.mjs warns when another task edits, consumes or creates what this task removes."
|
|
63
|
+
},
|
|
64
|
+
"produces": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "type": "string", "minLength": 1 },
|
|
67
|
+
"description": "Interfaces or symbols this task introduces or changes the shape of. validate-planning.mjs warns when a task that consumes one is not ordered after its producer."
|
|
68
|
+
},
|
|
69
|
+
"consumes": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": { "type": "string", "minLength": 1 },
|
|
72
|
+
"description": "Interfaces or symbols this task calls or relies on without changing them. Paired against other tasks' produces and removes."
|
|
73
|
+
},
|
|
74
|
+
"complexity": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": ["score"],
|
|
78
|
+
"description": "Grounded size of the task, written by plan-complexity.mjs --write. A task with recommendSplit is split in Phase 1 before approval.",
|
|
79
|
+
"properties": {
|
|
80
|
+
"score": { "type": "integer", "minimum": 1, "maximum": 10 },
|
|
81
|
+
"recommendSplit": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"description": "score is above prefs.global.planComplexity.splitAbove."
|
|
84
|
+
},
|
|
85
|
+
"factors": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"description": "The counts the score was built from. fanIn, crossModuleEdges and notIndexed are null when no code graph was available.",
|
|
89
|
+
"properties": {
|
|
90
|
+
"files": { "type": "integer", "minimum": 0 },
|
|
91
|
+
"fanIn": { "type": ["integer", "null"], "minimum": 0 },
|
|
92
|
+
"modules": { "type": "integer", "minimum": 0 },
|
|
93
|
+
"crossModuleEdges": { "type": ["integer", "null"], "minimum": 0 },
|
|
94
|
+
"testSurface": { "type": "integer", "minimum": 0 },
|
|
95
|
+
"notIndexed": { "type": ["integer", "null"], "minimum": 0 }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
53
99
|
}
|
|
54
100
|
}
|
|
55
101
|
}
|
|
@@ -577,6 +577,24 @@
|
|
|
577
577
|
}
|
|
578
578
|
}
|
|
579
579
|
},
|
|
580
|
+
"reworkEscalation": {
|
|
581
|
+
"type": "object",
|
|
582
|
+
"additionalProperties": false,
|
|
583
|
+
"description": "Rework model escalation (multi-agent-refs/features/rework-escalation.md). On the last rework round the cap allows, or when review-delta.mjs reports findings still present after a fix, scripts/rework-escalation.mjs moves that one dev dispatch one rung up the model-fallback ladder, as a fresh agent given the findings and the prior report as files. It never escalates against the cost budget (warn or exceeded), never re-upgrades a run that fell back, never reaches fable while modelFallback.fableEnabled is off, and never passes maxTier; a refusal is recorded with its reason.",
|
|
584
|
+
"properties": {
|
|
585
|
+
"enabled": {
|
|
586
|
+
"type": "boolean",
|
|
587
|
+
"default": true,
|
|
588
|
+
"description": "Master switch. On by default: it fires only on the rounds where a failure costs most, moves one rung, and stops at maxTier. false keeps every rework round on the dev dispatch's rung."
|
|
589
|
+
},
|
|
590
|
+
"maxTier": {
|
|
591
|
+
"type": "string",
|
|
592
|
+
"enum": ["fable", "opus", "sonnet", "haiku"],
|
|
593
|
+
"default": "opus",
|
|
594
|
+
"description": "Highest rung an escalation may reach. fable also needs modelFallback.fableEnabled true."
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
},
|
|
580
598
|
"modelRouting": {
|
|
581
599
|
"type": "object",
|
|
582
600
|
"description": "Policy-driven model selection. Ships DISABLED and changes no dispatch behaviour while enabled is false. Full contract and the reason `scope` has no host-session member: pipeline/schemas/route-config.schema.json. Written by /multi-agent:route-on, cleared of its on-state (not its rules) by /multi-agent:route-off, reported by /multi-agent:route-status.",
|
|
@@ -1151,6 +1169,19 @@
|
|
|
1151
1169
|
}
|
|
1152
1170
|
}
|
|
1153
1171
|
},
|
|
1172
|
+
"tddEvidence": {
|
|
1173
|
+
"type": "object",
|
|
1174
|
+
"additionalProperties": false,
|
|
1175
|
+
"description": "Whether completing a plan todo needs a recorded RED run and GREEN run (plan-todos.sh tdd-red / tdd-green). Applies where Phase 2 iterates plan todos (prefs.global.planTodos.enabled); without it Phase 2 walks tasks[] and nothing is recorded. Docs and chore todos, component tasks, test policy none and complete --no-tests are exempt and recorded with a reason.",
|
|
1176
|
+
"properties": {
|
|
1177
|
+
"mode": {
|
|
1178
|
+
"type": "string",
|
|
1179
|
+
"enum": ["off", "warn", "enforce"],
|
|
1180
|
+
"default": "warn",
|
|
1181
|
+
"description": "off: no check. warn: complete warns and records tdd.outcome unverified, which Phase 3 notes. enforce: complete refuses a todo without the evidence."
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1154
1185
|
"planTodos": {
|
|
1155
1186
|
"type": "object",
|
|
1156
1187
|
"additionalProperties": false,
|
|
@@ -1163,6 +1194,20 @@
|
|
|
1163
1194
|
}
|
|
1164
1195
|
}
|
|
1165
1196
|
},
|
|
1197
|
+
"planComplexity": {
|
|
1198
|
+
"type": "object",
|
|
1199
|
+
"additionalProperties": false,
|
|
1200
|
+
"description": "Grounded complexity score per Phase 1 plan task (pipeline/scripts/plan-complexity.mjs): counted from the files a task touches, their code-graph fan-in, the modules they span and their test surface, never from the planner's opinion. Phase 1 splits a task scoring above the threshold before the plan is approved.",
|
|
1201
|
+
"properties": {
|
|
1202
|
+
"splitAbove": {
|
|
1203
|
+
"type": "integer",
|
|
1204
|
+
"minimum": 1,
|
|
1205
|
+
"maximum": 10,
|
|
1206
|
+
"default": 7,
|
|
1207
|
+
"description": "A task whose score (1-10) is above this gets recommendSplit and is split before plan approval. Lower it to get smaller tasks; 10 never splits."
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1166
1211
|
"clarifyAmbiguous": {
|
|
1167
1212
|
"type": "object",
|
|
1168
1213
|
"additionalProperties": false,
|
|
@@ -1793,6 +1838,52 @@
|
|
|
1793
1838
|
"tailLines": 20
|
|
1794
1839
|
}
|
|
1795
1840
|
},
|
|
1841
|
+
"editSyntaxGate": {
|
|
1842
|
+
"type": "object",
|
|
1843
|
+
"additionalProperties": false,
|
|
1844
|
+
"description": "Parse the file an Edit / Write / MultiEdit just wrote and tell the model when it no longer parses: swiftc -parse, node --check, python3 py_compile (bytecode to a temp dir), JSON.parse, bash -n, chosen by extension, each under a 2 s deadline. Kotlin and every other extension are skipped. The hook is pipeline/scripts/edit-syntax-gate.sh (PostToolUse, Claude Code, installed from install/templates/claude-hooks.json). It never blocks: the message arrives as PostToolUse context and the hook exits 0. A parser that is not installed or runs past the deadline means no message.",
|
|
1845
|
+
"properties": {
|
|
1846
|
+
"enabled": {
|
|
1847
|
+
"type": "boolean",
|
|
1848
|
+
"default": false,
|
|
1849
|
+
"description": "false = the hook is a no-op. true = every edited file with a supported extension is parsed after the edit."
|
|
1850
|
+
}
|
|
1851
|
+
},
|
|
1852
|
+
"default": {
|
|
1853
|
+
"enabled": false
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
"mutationProbe": {
|
|
1857
|
+
"type": "object",
|
|
1858
|
+
"additionalProperties": false,
|
|
1859
|
+
"description": "Phase 3 optional review signal: pipeline/scripts/mutation-probe.mjs makes small textual mutants (a flipped comparison, a negated boolean literal, a swapped + / -, a changed returned constant) on lines the diff added or changed, runs the test command against each in a disposable worktree, and reports every mutant the tests did not catch as a finding. The caller's checkout is never written.",
|
|
1860
|
+
"properties": {
|
|
1861
|
+
"enabled": {
|
|
1862
|
+
"type": "boolean",
|
|
1863
|
+
"default": false,
|
|
1864
|
+
"description": "false = Phase 3 skips the probe. true = Phase 3 runs it when a test command is known and merges each survivor as an important finding (the tests do not catch that change)."
|
|
1865
|
+
},
|
|
1866
|
+
"maxMutants": {
|
|
1867
|
+
"type": "integer",
|
|
1868
|
+
"minimum": 1,
|
|
1869
|
+
"maximum": 50,
|
|
1870
|
+
"default": 5,
|
|
1871
|
+
"description": "Upper bound on mutants per run. Each one costs a full test-command run, so the bound is the cost bound."
|
|
1872
|
+
},
|
|
1873
|
+
"timeoutSec": {
|
|
1874
|
+
"type": "integer",
|
|
1875
|
+
"minimum": 5,
|
|
1876
|
+
"maximum": 3600,
|
|
1877
|
+
"default": 120,
|
|
1878
|
+
"description": "Deadline for one test-command run. A mutant whose run times out is reported as timed out, not as a survivor."
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
"default": {
|
|
1882
|
+
"enabled": false,
|
|
1883
|
+
"maxMutants": 5,
|
|
1884
|
+
"timeoutSec": 120
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1796
1887
|
"bulkRead": {
|
|
1797
1888
|
"type": "object",
|
|
1798
1889
|
"additionalProperties": false,
|
|
@@ -2205,7 +2296,27 @@
|
|
|
2205
2296
|
"type": ["string", "null", "object"],
|
|
2206
2297
|
"additionalProperties": { "type": "string" },
|
|
2207
2298
|
"default": null,
|
|
2208
|
-
"description": "The sub-task type name, or a map from a subtaskRoles entry to this site's type name (\"*\" for any other role), e.g. {\"develop\": \"
|
|
2299
|
+
"description": "The sub-task type name, or a map from a subtaskRoles entry to this site's type name (\"*\" for any other role), e.g. {\"develop\": \"Build Sub-task\", \"test\": \"QA Sub-task\"}. Null means discover from createmeta (the per-project endpoint, else the classic one) rather than assume a name; when the site has several sub-task types the first is used and the run says so."
|
|
2300
|
+
},
|
|
2301
|
+
"subtaskRolesDeclined": {
|
|
2302
|
+
"type": "boolean",
|
|
2303
|
+
"default": false,
|
|
2304
|
+
"description": "true once the person answered the sub-task question with no sub-tasks, so /multi-agent:analysis-jira does not ask again. Written by analysis-story-tree.mjs --save-subtasks none."
|
|
2305
|
+
},
|
|
2306
|
+
"storyPanels": {
|
|
2307
|
+
"oneOf": [
|
|
2308
|
+
{ "const": false },
|
|
2309
|
+
{
|
|
2310
|
+
"type": "object",
|
|
2311
|
+
"additionalProperties": false,
|
|
2312
|
+
"properties": {
|
|
2313
|
+
"story": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
2314
|
+
"criteria": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
2315
|
+
"other": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
],
|
|
2319
|
+
"description": "Border colours of a Story's wiki panels, as written by /multi-agent:create-jira and /multi-agent:analysis-jira: story (the user story), criteria (the acceptance criteria), other (every other section). Unset means #d4d39e, #d4a19e and #9eb6d4; a colour left out keeps its default; false writes plain h3. sections."
|
|
2209
2320
|
},
|
|
2210
2321
|
"subtaskRoles": {
|
|
2211
2322
|
"type": "array",
|
|
@@ -2256,6 +2367,11 @@
|
|
|
2256
2367
|
"type": "string",
|
|
2257
2368
|
"default": "Relates",
|
|
2258
2369
|
"description": "The link type between a story and its channel clones, by this site's name."
|
|
2370
|
+
},
|
|
2371
|
+
"dependencyLinkType": {
|
|
2372
|
+
"type": "string",
|
|
2373
|
+
"default": "Blocks",
|
|
2374
|
+
"description": "The link type written from a prerequisite story to the story that depends on it (a `Depends on:` clause in the analysis document), by this site's name. The prerequisite is the link's source."
|
|
2259
2375
|
}
|
|
2260
2376
|
}
|
|
2261
2377
|
},
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file _jsonl.mjs - read a JSON-lines file one record at a time, by line number.
|
|
3
|
+
*
|
|
4
|
+
* Session transcripts are append-only logs that can run to hundreds of
|
|
5
|
+
* megabytes, are sometimes cut mid-line by a crash, and occasionally carry a
|
|
6
|
+
* record far larger than the rest (a whole file pasted into a tool result).
|
|
7
|
+
* So the reader streams, stops at a byte cap, refuses a single line over a
|
|
8
|
+
* per-line cap without parsing it, and reports every line it could not use
|
|
9
|
+
* instead of throwing. Line numbers are physical lines of the file, blank ones
|
|
10
|
+
* included, so `file:line` points at what an editor shows.
|
|
11
|
+
*
|
|
12
|
+
* Each yielded row is `{ line, record }` for a parsed object, or
|
|
13
|
+
* `{ line, reason }` for a line that was not used: `parse` (not JSON),
|
|
14
|
+
* `not-object` (JSON, but not an object), `oversized` (over the per-line cap)
|
|
15
|
+
* or `truncated` (the line the byte cap cut through). `stats` is filled in as
|
|
16
|
+
* the file is read.
|
|
17
|
+
*
|
|
18
|
+
* @module pipeline/scripts/_jsonl
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createReadStream, statSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_MAX_BYTES = 256 * 1024 * 1024;
|
|
24
|
+
export const DEFAULT_MAX_LINE_BYTES = 8 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} path
|
|
28
|
+
* @param {{maxBytes?:number, maxLineBytes?:number, stats?:object}} [opts]
|
|
29
|
+
*/
|
|
30
|
+
export async function* readJsonl(path, opts = {}) {
|
|
31
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
32
|
+
const maxLineBytes = opts.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
|
|
33
|
+
const stats = opts.stats ?? {};
|
|
34
|
+
Object.assign(stats, {
|
|
35
|
+
lines: 0,
|
|
36
|
+
malformed: 0,
|
|
37
|
+
oversized: 0,
|
|
38
|
+
truncated: false,
|
|
39
|
+
unreadable: false,
|
|
40
|
+
bytes: 0,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let size;
|
|
44
|
+
try {
|
|
45
|
+
const st = statSync(path);
|
|
46
|
+
if (!st.isFile()) throw new Error("not a regular file");
|
|
47
|
+
size = st.size;
|
|
48
|
+
} catch {
|
|
49
|
+
stats.unreadable = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const capped = size > maxBytes;
|
|
53
|
+
stats.bytes = Math.min(size, maxBytes);
|
|
54
|
+
if (stats.bytes === 0) return;
|
|
55
|
+
|
|
56
|
+
const classify = (n, text) => {
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(text);
|
|
60
|
+
} catch {
|
|
61
|
+
stats.malformed++;
|
|
62
|
+
return { line: n, reason: "parse" };
|
|
63
|
+
}
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
65
|
+
stats.malformed++;
|
|
66
|
+
return { line: n, reason: "not-object" };
|
|
67
|
+
}
|
|
68
|
+
return { line: n, record: value };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// A line becomes {n, text} or {n, oversized}. Bytes of the current line are
|
|
72
|
+
// kept only while it is within the cap; past it they are dropped as they
|
|
73
|
+
// arrive, so a single huge line costs one chunk of memory, not its length.
|
|
74
|
+
// One line is held back so the last one is known before it is classified:
|
|
75
|
+
// when the byte cap cut the file, that line is incomplete.
|
|
76
|
+
const NL = 0x0a;
|
|
77
|
+
let parts = [];
|
|
78
|
+
let partBytes = 0;
|
|
79
|
+
let over = false;
|
|
80
|
+
let n = 0;
|
|
81
|
+
let held = null;
|
|
82
|
+
const finish = () => {
|
|
83
|
+
n++;
|
|
84
|
+
let line;
|
|
85
|
+
if (over) line = { n, oversized: true };
|
|
86
|
+
else {
|
|
87
|
+
let text = Buffer.concat(parts, partBytes).toString("utf8");
|
|
88
|
+
if (text.endsWith("\r")) text = text.slice(0, -1);
|
|
89
|
+
line = Buffer.byteLength(text) > maxLineBytes ? { n, oversized: true } : { n, text };
|
|
90
|
+
}
|
|
91
|
+
parts = [];
|
|
92
|
+
partBytes = 0;
|
|
93
|
+
over = false;
|
|
94
|
+
return line;
|
|
95
|
+
};
|
|
96
|
+
const emit = (line) => {
|
|
97
|
+
if (line.oversized) {
|
|
98
|
+
stats.oversized++;
|
|
99
|
+
return { line: line.n, reason: "oversized" };
|
|
100
|
+
}
|
|
101
|
+
return line.text.trim() ? classify(line.n, line.text) : null;
|
|
102
|
+
};
|
|
103
|
+
const append = (buf) => {
|
|
104
|
+
if (over || buf.length === 0) return;
|
|
105
|
+
if (partBytes + buf.length > maxLineBytes + 1) {
|
|
106
|
+
over = true;
|
|
107
|
+
parts = [];
|
|
108
|
+
partBytes = 0;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
parts.push(buf);
|
|
112
|
+
partBytes += buf.length;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const input = createReadStream(path, { start: 0, end: stats.bytes - 1 });
|
|
116
|
+
try {
|
|
117
|
+
for await (const chunk of input) {
|
|
118
|
+
let from = 0;
|
|
119
|
+
for (let at = chunk.indexOf(NL); at !== -1; at = chunk.indexOf(NL, from)) {
|
|
120
|
+
append(chunk.subarray(from, at));
|
|
121
|
+
from = at + 1;
|
|
122
|
+
const line = finish();
|
|
123
|
+
if (held) {
|
|
124
|
+
const row = emit(held);
|
|
125
|
+
if (row) yield row;
|
|
126
|
+
}
|
|
127
|
+
held = line;
|
|
128
|
+
}
|
|
129
|
+
append(chunk.subarray(from));
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
stats.unreadable = true;
|
|
133
|
+
}
|
|
134
|
+
if (partBytes > 0 || over) {
|
|
135
|
+
const line = finish();
|
|
136
|
+
if (held) {
|
|
137
|
+
const row = emit(held);
|
|
138
|
+
if (row) yield row;
|
|
139
|
+
}
|
|
140
|
+
held = line;
|
|
141
|
+
}
|
|
142
|
+
stats.lines = n;
|
|
143
|
+
if (!held || (!held.oversized && !held.text.trim())) return;
|
|
144
|
+
if (capped) {
|
|
145
|
+
stats.truncated = true;
|
|
146
|
+
yield { line: held.n, reason: "truncated" };
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const row = emit(held);
|
|
150
|
+
if (row) yield row;
|
|
151
|
+
}
|