mandrel 1.68.0 → 1.70.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 (75) hide show
  1. package/.agents/README.md +1 -1
  2. package/.agents/docs/agentrc-reference.json +1 -2
  3. package/.agents/docs/configuration.md +2 -4
  4. package/.agents/docs/workflows.md +1 -1
  5. package/.agents/schemas/agentrc.schema.json +1 -5
  6. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
  7. package/.agents/scripts/agents-update-preflight.js +235 -0
  8. package/.agents/scripts/apply-quality-bootstrap.js +79 -0
  9. package/.agents/scripts/audit-labels-bootstrap.js +52 -30
  10. package/.agents/scripts/audit-to-stories.js +54 -0
  11. package/.agents/scripts/bootstrap.js +13 -3
  12. package/.agents/scripts/epic-deliver-preflight.js +30 -13
  13. package/.agents/scripts/epic-deliver-prepare.js +40 -53
  14. package/.agents/scripts/epic-execute-record-wave.js +119 -133
  15. package/.agents/scripts/generate-config-docs.js +189 -94
  16. package/.agents/scripts/lib/audit-suite/findings.js +0 -4
  17. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
  18. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
  19. package/.agents/scripts/lib/baseline-snapshot.js +163 -4
  20. package/.agents/scripts/lib/baselines/refresh-service.js +13 -5
  21. package/.agents/scripts/lib/config/baselines.js +0 -20
  22. package/.agents/scripts/lib/config/explain.js +0 -2
  23. package/.agents/scripts/lib/config/limits.js +19 -8
  24. package/.agents/scripts/lib/config/temp-paths.js +0 -31
  25. package/.agents/scripts/lib/config-settings-schema.js +1 -2
  26. package/.agents/scripts/lib/crap-utils.js +281 -0
  27. package/.agents/scripts/lib/maintainability-utils.js +32 -9
  28. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
  29. package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
  30. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
  31. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
  32. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
  33. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -162
  34. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
  35. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
  36. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
  37. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
  38. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
  39. package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
  40. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
  41. package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
  42. package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
  43. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
  44. package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
  45. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
  46. package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
  47. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
  48. package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
  49. package/.agents/scripts/lib/story-adjacency.js +14 -10
  50. package/.agents/scripts/lib/story-body/story-body.js +142 -65
  51. package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
  52. package/.agents/scripts/lib/test-tiers.js +13 -7
  53. package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
  54. package/.agents/scripts/lib/wave-runner/tick.js +446 -216
  55. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
  56. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
  57. package/.agents/scripts/lint-label-vocabulary.js +1 -1
  58. package/.agents/scripts/providers/github/issues.js +48 -0
  59. package/.agents/scripts/providers/github.js +1 -0
  60. package/.agents/scripts/stories-wave-tick.js +262 -161
  61. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
  62. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
  63. package/.agents/skills/skills.index.json +2 -2
  64. package/.agents/workflows/agents-update.md +205 -28
  65. package/.agents/workflows/deliver.md +12 -9
  66. package/.agents/workflows/helpers/deliver-epic.md +126 -90
  67. package/.agents/workflows/helpers/deliver-stories.md +131 -85
  68. package/.agents/workflows/helpers/plan-epic.md +13 -10
  69. package/.agents/workflows/plan.md +1 -1
  70. package/README.md +20 -0
  71. package/docs/CHANGELOG.md +46 -0
  72. package/lib/cli/registry.js +49 -6
  73. package/lib/cli/update.js +335 -332
  74. package/package.json +16 -11
  75. package/.agents/scripts/lib/wave-runner/wave-checkpoint.js +0 -91
@@ -4,7 +4,8 @@
4
4
  * (blocked stories, gate failures) route through `WaveTickResult`.
5
5
  *
6
6
  * Callers classify via `err.phase`: `checkpoint-missing`,
7
- * `checkpoint-read`, `plan-missing`, `story-fetch`, `invalid-input`.
7
+ * `checkpoint-read`, `old-shape-checkpoint`, `story-fetch`,
8
+ * `invalid-input`.
8
9
  *
9
10
  * @module lib/wave-runner/wave-runner-error
10
11
  */
@@ -0,0 +1,226 @@
1
+ /**
2
+ * lib/workers/combined-mi-crap-worker.js — CPU-pool worker entry for the
3
+ * combined MI + CRAP single-pass scan (`scanAndScoreCombined`).
4
+ *
5
+ * One file in, BOTH the maintainability score and the per-method CRAP rows
6
+ * out — derived from a SINGLE `escomplex.analyzeModule` parse via
7
+ * `analyzeOnce`. This collapses the two independent worker-pool passes the
8
+ * full-tree baseline regenerator used to run (the MI worker parsed the AST
9
+ * once for the module score, the CRAP worker parsed the same file's AST
10
+ * again for the method rows) into one parse per file.
11
+ *
12
+ * The MI score and the CRAP rows have independent skip policies, mirroring
13
+ * the two separate passes this worker replaces:
14
+ * - **MI** never requires coverage. The module score is emitted for every
15
+ * file that reads + transpiles + parses. A read failure yields
16
+ * `miScore: null` (the host drops the file from the MI map, matching
17
+ * `calculateAll`'s `score === null` filter). A transpile failure or a
18
+ * parse error yields `miScore: 0` (matching `calculateForFile` /
19
+ * `calculateForSource`, which return 0 on transpile-null / parse-error).
20
+ * - **CRAP** honours `requireCoverage`. A file with no coverage entry is
21
+ * reported as `skippedFileNoCoverage: true` (the host increments its own
22
+ * counter and emits no CRAP rows for it) — but the MI score is STILL
23
+ * computed and returned, because the MI pass would have scored it.
24
+ *
25
+ * Message contract — see lib/cpu-pool.js:
26
+ * IN : { item: { abs: string, relPath: string, requireCoverage: boolean,
27
+ * coverageEntry: object | null } }
28
+ * { exit: true }
29
+ * OUT : { ok: true, result: {
30
+ * relPath,
31
+ * miScore: number | null,
32
+ * skippedFileNoCoverage: boolean,
33
+ * crapRows: Array<{ method, startLine, cyclomatic, coverage, crap }> | null,
34
+ * skippedMethodsNoCoverage: number,
35
+ * } }
36
+ *
37
+ * A read/transpile/parse failure surfaces as `crapRows: null` so the host
38
+ * loop drops the file's CRAP contribution (matching the crap-worker's
39
+ * `rows: null` contract) — never aborts the whole scan. On a read failure
40
+ * `miScore` is `null`; on a transpile/parse failure `miScore` is `0`.
41
+ */
42
+
43
+ import fs from 'node:fs';
44
+ import { parentPort } from 'node:worker_threads';
45
+ import { analyzeOnce } from '../crap-utils.js';
46
+ import { transpileIfNeeded } from '../transpile.js';
47
+
48
+ /**
49
+ * Pure handler for a single inbound worker message. Exported so unit tests
50
+ * can exercise every branch (bad-shape rejection, coverage gate, read /
51
+ * transpile / parse failures, success rows, skipped methods, and the
52
+ * MI-computed-even-when-coverage-skipped invariant) without spawning a real
53
+ * `Worker` thread.
54
+ *
55
+ * Side effects (fs, transpile, analyzeOnce) are wired through `deps` so
56
+ * tests pass deterministic stubs.
57
+ *
58
+ * @param {unknown} msg
59
+ * @param {{
60
+ * readFile?: (abs: string) => string,
61
+ * transpile?: (abs: string, source: string) => string | null,
62
+ * analyze?: (source: string, entry: object|null) => {
63
+ * miScore: number,
64
+ * crapRows: Array<object>,
65
+ * parseError: boolean,
66
+ * },
67
+ * }} [deps]
68
+ * @returns {{kind: 'exit'} | {kind: 'reply', message: object}}
69
+ */
70
+ export function handleCombinedMiCrapWorkerMessage(msg, deps = {}) {
71
+ if (msg && msg.exit === true) return { kind: 'exit' };
72
+
73
+ const item = msg?.item;
74
+ if (
75
+ !item ||
76
+ typeof item.abs !== 'string' ||
77
+ typeof item.relPath !== 'string'
78
+ ) {
79
+ return {
80
+ kind: 'reply',
81
+ message: {
82
+ ok: false,
83
+ error: `bad worker message: ${JSON.stringify(msg)}`,
84
+ },
85
+ };
86
+ }
87
+ const { abs, relPath, requireCoverage } = item;
88
+ const readFile = deps.readFile ?? ((p) => fs.readFileSync(p, 'utf-8'));
89
+ const transpile = deps.transpile ?? transpileIfNeeded;
90
+ const analyze = deps.analyze ?? analyzeOnce;
91
+
92
+ // Coverage entry is pre-resolved on the host and attached to the item.
93
+ // `item.coverageEntry` may be explicitly `null` when the file has no
94
+ // coverage, or `undefined` when the caller did not supply it (treat as null).
95
+ const entry = item.coverageEntry ?? null;
96
+
97
+ // Read the source once. A read failure means neither MI nor CRAP can be
98
+ // computed — MI drops (null), CRAP drops (rows null) — matching the two
99
+ // passes' read-failure contracts (calculateAll → score null; crap worker
100
+ // → rows null).
101
+ let source;
102
+ try {
103
+ source = readFile(abs);
104
+ } catch {
105
+ return {
106
+ kind: 'reply',
107
+ message: {
108
+ ok: true,
109
+ result: {
110
+ relPath,
111
+ miScore: null,
112
+ skippedFileNoCoverage: false,
113
+ crapRows: null,
114
+ skippedMethodsNoCoverage: 0,
115
+ },
116
+ },
117
+ };
118
+ }
119
+
120
+ // TS/TSX → strip-then-analyze. A transpile failure yields miScore 0
121
+ // (calculateForFile returns 0 when transpileIfNeeded returns null) and a
122
+ // null CRAP contribution (crap worker returns rows: null).
123
+ const prepared = transpile(abs, source);
124
+ if (prepared === null) {
125
+ return {
126
+ kind: 'reply',
127
+ message: {
128
+ ok: true,
129
+ result: {
130
+ relPath,
131
+ miScore: 0,
132
+ skippedFileNoCoverage: false,
133
+ crapRows: null,
134
+ skippedMethodsNoCoverage: 0,
135
+ },
136
+ },
137
+ };
138
+ }
139
+
140
+ // ONE parse: analyzeOnce derives both the module MI score and the raw
141
+ // per-method CRAP rows from a single escomplex report. On a parse error it
142
+ // returns miScore 0 and an empty crapRows with parseError true.
143
+ const {
144
+ miScore,
145
+ crapRows: rawCrapRows,
146
+ parseError,
147
+ } = analyze(prepared, entry);
148
+ if (parseError) {
149
+ // Parse error: MI scores 0 (parity with calculateForSource's catch →
150
+ // returns 0), CRAP drops the file (rows null, parity with the crap
151
+ // worker's calculateCrap-throw branch).
152
+ return {
153
+ kind: 'reply',
154
+ message: {
155
+ ok: true,
156
+ result: {
157
+ relPath,
158
+ miScore: 0,
159
+ skippedFileNoCoverage: false,
160
+ crapRows: null,
161
+ skippedMethodsNoCoverage: 0,
162
+ },
163
+ },
164
+ };
165
+ }
166
+
167
+ // CRAP coverage gate runs AFTER the parse so the MI score is always
168
+ // available. When the file has no coverage under requireCoverage, the CRAP
169
+ // pass would have skipped it at the file level (no rows, counted) — but the
170
+ // MI pass would still have scored it, so miScore is returned regardless.
171
+ if (requireCoverage && entry === null) {
172
+ return {
173
+ kind: 'reply',
174
+ message: {
175
+ ok: true,
176
+ result: {
177
+ relPath,
178
+ miScore,
179
+ skippedFileNoCoverage: true,
180
+ crapRows: [],
181
+ skippedMethodsNoCoverage: 0,
182
+ },
183
+ },
184
+ };
185
+ }
186
+
187
+ const crapRows = [];
188
+ let skippedMethodsNoCoverage = 0;
189
+ for (const mr of rawCrapRows) {
190
+ if (mr.crap === null || mr.coverage === null) {
191
+ skippedMethodsNoCoverage += 1;
192
+ continue;
193
+ }
194
+ crapRows.push({
195
+ method: mr.method,
196
+ startLine: mr.startLine,
197
+ cyclomatic: mr.cyclomatic,
198
+ coverage: mr.coverage,
199
+ crap: mr.crap,
200
+ });
201
+ }
202
+ return {
203
+ kind: 'reply',
204
+ message: {
205
+ ok: true,
206
+ result: {
207
+ relPath,
208
+ miScore,
209
+ skippedFileNoCoverage: false,
210
+ crapRows,
211
+ skippedMethodsNoCoverage,
212
+ },
213
+ },
214
+ };
215
+ }
216
+
217
+ if (parentPort) {
218
+ parentPort.on('message', (msg) => {
219
+ const out = handleCombinedMiCrapWorkerMessage(msg);
220
+ if (out.kind === 'exit') {
221
+ parentPort.close();
222
+ return;
223
+ }
224
+ parentPort.postMessage(out.message);
225
+ });
226
+ }
@@ -159,7 +159,7 @@ export function findVocabularyViolations(src, axes = KNOWN_AXES) {
159
159
  // - `context::acceptance-spec` — canonical label, value contains a
160
160
  // hyphen but is not at axis position.
161
161
  // - `<type>/<slug>` — template placeholders.
162
- // - `planning.maxTickets` — config-key paths with `.`.
162
+ // - `delivery.maxTokenBudget` — config-key paths with `.`.
163
163
  // - `context.{prdId,techSpecId}` — JS destructure shapes.
164
164
  // - `agent-protocol.md` — filenames / concept slugs.
165
165
  //
@@ -99,6 +99,54 @@ export class IssuesGateway {
99
99
  return issues.filter((issue) => !issue?.pull_request);
100
100
  }
101
101
 
102
+ /**
103
+ * Search issues by a free-text query via the REST search API
104
+ * (`GET /search/issues`). Deliberately REST, **not** GraphQL: transient
105
+ * GraphQL 401s are a known failure mode in this repo (the dedup port that
106
+ * consumes this method must not silently no-op on an auth blip), so the
107
+ * search rides the same `gh api` REST surface + transient-retry shim as
108
+ * every other read here.
109
+ *
110
+ * The caller (`audit-to-stories.js` `loadProvider()`) passes a 40-char
111
+ * fingerprint sha as the query so the search resolves the handful of
112
+ * issues whose fingerprint footer carries that sha; `route-finding.js`
113
+ * then confirms identity against the footer. Both open and closed issues
114
+ * are returned (no `state:` qualifier is appended) so a closed-fingerprint
115
+ * match can surface as `regression-of-closed`.
116
+ *
117
+ * Returns the trimmed `[{ number, state, body }]` projection the dedup
118
+ * port expects. `state` is normalised to the REST lowercase form
119
+ * (`open` / `closed`).
120
+ *
121
+ * @param {{ query: string, owner?: string, repo?: string }} params
122
+ * @returns {Promise<Array<{ number: number, state: string, body: string }>>}
123
+ * @field-manifest GET /search/issues: total_count, items[number, state, body]
124
+ */
125
+ async searchIssues({ query, owner, repo } = {}) {
126
+ if (typeof query !== 'string' || query.trim().length === 0) {
127
+ throw new Error('searchIssues: a non-empty query string is required');
128
+ }
129
+ const scopeOwner = owner ?? this.owner;
130
+ const scopeRepo = repo ?? this.repo;
131
+ // Constrain the search to this repo and to issues (not PRs). The
132
+ // fingerprint sha is the free-text term; GitHub matches it against the
133
+ // issue body where the `<!-- audit-fingerprints: ... -->` footer lives.
134
+ const qualifiers = [`repo:${scopeOwner}/${scopeRepo}`, 'type:issue'];
135
+ const q = `${query.trim()} ${qualifiers.join(' ')}`;
136
+ const endpoint = `/search/issues?q=${encodeURIComponent(q)}`;
137
+ const result = await withTransientRetry(
138
+ () => this._gh.api({ method: 'GET', endpoint }),
139
+ { label: `searchIssues ${query}`, onRetry: defaultRetryWarn },
140
+ );
141
+ const json = parseApiJson(result);
142
+ const items = Array.isArray(json?.items) ? json.items : [];
143
+ return items.map((item) => ({
144
+ number: item.number,
145
+ state: item.state ?? 'open',
146
+ body: item.body ?? '',
147
+ }));
148
+ }
149
+
102
150
  /**
103
151
  * List Epic-typed issues. Filter shape preserved from the old code.
104
152
  *
@@ -98,6 +98,7 @@ export class GitHubProvider extends ITicketingProvider {
98
98
  */
99
99
  const DELEGATIONS = [
100
100
  ['graphql', 'issues.ghGraphql'],
101
+ ['searchIssues', 'issues.searchIssues'],
101
102
  ['listIssuesByLabel', 'issues.listIssuesByLabel'],
102
103
  ['getEpics', 'issues.getEpics'],
103
104
  ['getEpic', 'issues.getEpic'],