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.
- package/.agents/README.md +1 -1
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +2 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/schemas/agentrc.schema.json +1 -5
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
- package/.agents/scripts/agents-update-preflight.js +235 -0
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/epic-deliver-preflight.js +30 -13
- package/.agents/scripts/epic-deliver-prepare.js +40 -53
- package/.agents/scripts/epic-execute-record-wave.js +119 -133
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +13 -5
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/explain.js +0 -2
- package/.agents/scripts/lib/config/limits.js +19 -8
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-settings-schema.js +1 -2
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/maintainability-utils.js +32 -9
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -162
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
- package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
- package/.agents/scripts/lib/story-adjacency.js +14 -10
- package/.agents/scripts/lib/story-body/story-body.js +142 -65
- package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
- package/.agents/scripts/lib/wave-runner/tick.js +446 -216
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/lint-label-vocabulary.js +1 -1
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/stories-wave-tick.js +262 -161
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/agents-update.md +205 -28
- package/.agents/workflows/deliver.md +12 -9
- package/.agents/workflows/helpers/deliver-epic.md +126 -90
- package/.agents/workflows/helpers/deliver-stories.md +131 -85
- package/.agents/workflows/helpers/plan-epic.md +13 -10
- package/.agents/workflows/plan.md +1 -1
- package/README.md +20 -0
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- 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`, `
|
|
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
|
-
// - `
|
|
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'],
|