mandrel 1.88.0 → 1.90.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 +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +62 -27
- package/.agents/docs/configuration.md +5 -4
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +10 -6
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +39 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/coverage-capture.js +17 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/close-validation/gates.js +64 -24
- package/.agents/scripts/lib/config/ci.js +12 -1
- package/.agents/scripts/lib/config/runners.js +13 -5
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -8
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +47 -15
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/npm-scripts.js +55 -0
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +193 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +248 -13
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +109 -12
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +46 -4
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +246 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +8 -1
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +116 -43
- package/.agents/workflows/helpers/deliver-epic.md +123 -54
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -366
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -224
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
storyTempDir,
|
|
43
43
|
} from '../config/temp-paths.js';
|
|
44
44
|
import { Logger } from '../Logger.js';
|
|
45
|
+
import { recordSignalReject, validateSignal } from './signal-validator.js';
|
|
45
46
|
import { classifyPathSource } from './source-classifier.js';
|
|
46
47
|
|
|
47
48
|
const TRACES_BASENAME = 'traces.ndjson';
|
|
@@ -60,20 +61,23 @@ function tracesFile(eid, sid, config) {
|
|
|
60
61
|
* Best-effort decoration of a signal record with a `source` field
|
|
61
62
|
* (`"framework"` or `"consumer"`) produced by `classifyPathSource`.
|
|
62
63
|
*
|
|
63
|
-
*
|
|
64
|
+
* Post the Epic #4406 cutover `source` is reserved **exclusively** for
|
|
65
|
+
* this framework/consumer classification — a record's originating tool
|
|
66
|
+
* lives in `emitter`, never `source`. That freed the classifier to run
|
|
67
|
+
* for every friction record (pre-cutover a provenance object under the
|
|
68
|
+
* `source` key blocked it via `Object.hasOwn`).
|
|
69
|
+
*
|
|
70
|
+
* Rules:
|
|
64
71
|
* - If the record is not a plain object (string, number, null,
|
|
65
|
-
* undefined), return it unchanged
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
74
|
-
* belt-and-braces a try/catch so an unexpected fault degrades to a
|
|
75
|
-
* `Logger.warn` and a passthrough of the original signal — never a
|
|
76
|
-
* dropped write.
|
|
72
|
+
* undefined), return it unchanged.
|
|
73
|
+
* - If the caller pre-set `source` to exactly `"framework"` or
|
|
74
|
+
* `"consumer"`, preserve it verbatim — some detectors classify
|
|
75
|
+
* upstream and we MUST NOT overwrite their intentional tag.
|
|
76
|
+
* - Otherwise (absent, or any other value — defense in depth against a
|
|
77
|
+
* stray non-canonical `source`), invoke `classifyPathSource` against
|
|
78
|
+
* the record's `failingPath` / `path` and `command` /
|
|
79
|
+
* `emitter.command` fields and inject/overwrite `source` with the
|
|
80
|
+
* result.
|
|
77
81
|
*
|
|
78
82
|
* @param {unknown} signal
|
|
79
83
|
* @returns {unknown}
|
|
@@ -82,16 +86,17 @@ function tagSignalSource(signal) {
|
|
|
82
86
|
if (signal === null || typeof signal !== 'object' || Array.isArray(signal)) {
|
|
83
87
|
return signal;
|
|
84
88
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (Object.hasOwn(signal, 'source')) {
|
|
89
|
-
return signal;
|
|
89
|
+
const record = /** @type {Record<string, unknown>} */ (signal);
|
|
90
|
+
if (record.source === 'framework' || record.source === 'consumer') {
|
|
91
|
+
return record;
|
|
90
92
|
}
|
|
91
93
|
try {
|
|
92
|
-
const record = /** @type {Record<string, unknown>} */ (signal);
|
|
93
94
|
const failingPath = record.failingPath ?? record.path;
|
|
94
|
-
const
|
|
95
|
+
const emitter =
|
|
96
|
+
record.emitter && typeof record.emitter === 'object'
|
|
97
|
+
? /** @type {Record<string, unknown>} */ (record.emitter)
|
|
98
|
+
: null;
|
|
99
|
+
const command = record.command ?? emitter?.command;
|
|
95
100
|
const source = classifyPathSource(failingPath, command);
|
|
96
101
|
return { ...record, source };
|
|
97
102
|
} catch (err) {
|
|
@@ -104,6 +109,27 @@ function tagSignalSource(signal) {
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Validate a record against the canonical `signal-event.schema.json`
|
|
114
|
+
* before it is appended. On failure the record is **dropped** (never
|
|
115
|
+
* appended), a `Logger.warn` names the violating field, and the per-Epic
|
|
116
|
+
* reject tally is incremented under the Epic temp tree. Never throws —
|
|
117
|
+
* the writer's best-effort contract is preserved.
|
|
118
|
+
*
|
|
119
|
+
* @param {unknown} record
|
|
120
|
+
* @param {{ epicId?: number|null, config?: object, label: string }} ctx
|
|
121
|
+
* @returns {Promise<boolean>} true when the record is valid (safe to append).
|
|
122
|
+
*/
|
|
123
|
+
async function validateOrDrop(record, { epicId, config, label }) {
|
|
124
|
+
const { valid, violatingField, message } = validateSignal(record);
|
|
125
|
+
if (valid) return true;
|
|
126
|
+
Logger.warn(
|
|
127
|
+
`signals-writer: dropping schema-invalid ${label} record — violating field '${violatingField}' (${message}).`,
|
|
128
|
+
);
|
|
129
|
+
await recordSignalReject({ epicId, config, field: violatingField });
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
/**
|
|
108
134
|
* Best-effort append of a single record as one newline-terminated JSON
|
|
109
135
|
* line. Caller-supplied record must be JSON-serialisable; circular refs
|
|
@@ -163,14 +189,24 @@ export async function appendSignal(args) {
|
|
|
163
189
|
);
|
|
164
190
|
return false;
|
|
165
191
|
}
|
|
166
|
-
|
|
192
|
+
const tagged = tagSignalSource(signal);
|
|
193
|
+
const ok = await validateOrDrop(tagged, {
|
|
194
|
+
epicId: Number.isInteger(epicId) ? epicId : null,
|
|
195
|
+
config,
|
|
196
|
+
label: 'signal',
|
|
197
|
+
});
|
|
198
|
+
if (!ok) return false;
|
|
199
|
+
return appendOne(target, tagged);
|
|
167
200
|
}
|
|
168
201
|
|
|
169
202
|
/**
|
|
170
203
|
* Append one signal record to the per-Epic stream at
|
|
171
|
-
* `temp/epic-<eid>/signals.ndjson` — used for
|
|
172
|
-
*
|
|
173
|
-
*
|
|
204
|
+
* `temp/epic-<eid>/signals.ndjson` — used for Epic-scoped signals not tied
|
|
205
|
+
* to an individual Story: the wave-window forensics kinds (`wave-start`,
|
|
206
|
+
* `wave-complete`), notification-derived anchors (`state-transition`,
|
|
207
|
+
* `notification.emitted`), and Epic-level `friction` (e.g.
|
|
208
|
+
* `lifecycle-emit.js`). The retired write-only kinds `wave-tick` /
|
|
209
|
+
* `epic-complete` are gone (Story #3909) and are NOT valid here.
|
|
174
210
|
*
|
|
175
211
|
* @param {{ epicId: number, signal: unknown, config?: object }} args
|
|
176
212
|
* @returns {Promise<boolean>}
|
|
@@ -188,7 +224,14 @@ export async function appendEpicSignal(args) {
|
|
|
188
224
|
);
|
|
189
225
|
return false;
|
|
190
226
|
}
|
|
191
|
-
|
|
227
|
+
const tagged = tagSignalSource(signal);
|
|
228
|
+
const ok = await validateOrDrop(tagged, {
|
|
229
|
+
epicId: Number.isInteger(epicId) ? epicId : null,
|
|
230
|
+
config,
|
|
231
|
+
label: 'epic signal',
|
|
232
|
+
});
|
|
233
|
+
if (!ok) return false;
|
|
234
|
+
return appendOne(target, tagged);
|
|
192
235
|
}
|
|
193
236
|
|
|
194
237
|
/**
|
|
@@ -211,41 +254,32 @@ export async function appendTrace(args) {
|
|
|
211
254
|
);
|
|
212
255
|
return false;
|
|
213
256
|
}
|
|
257
|
+
const ok = await validateOrDrop(trace, {
|
|
258
|
+
epicId: Number.isInteger(epicId) ? epicId : null,
|
|
259
|
+
config,
|
|
260
|
+
label: 'trace',
|
|
261
|
+
});
|
|
262
|
+
if (!ok) return false;
|
|
214
263
|
return appendOne(target, trace);
|
|
215
264
|
}
|
|
216
265
|
|
|
217
266
|
/**
|
|
218
|
-
* Stream `
|
|
219
|
-
* for each successfully parsed JSON line.
|
|
220
|
-
* match operator log expectations. Malformed
|
|
221
|
-
* `Logger.warn`. A missing file resolves with
|
|
222
|
-
*
|
|
223
|
-
* keeps walking.
|
|
267
|
+
* Stream any NDJSON `target` file line by line, invoking
|
|
268
|
+
* `cb(parsed, lineNumber)` for each successfully parsed JSON line.
|
|
269
|
+
* `lineNumber` is 1-based to match operator log expectations. Malformed
|
|
270
|
+
* lines are skipped with a `Logger.warn`. A missing file resolves with
|
|
271
|
+
* `missing: true` rather than throwing.
|
|
224
272
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
273
|
+
* Shared spine for `forEachLine` (per-Story stream) and `forEachEpicLine`
|
|
274
|
+
* (per-Epic stream) so the two readers cannot drift in their
|
|
275
|
+
* malformed-line / missing-file / cb-throw handling.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} target
|
|
227
278
|
* @param {(parsed: unknown, lineNumber: number) => unknown | Promise<unknown>} cb
|
|
228
|
-
* @param {
|
|
279
|
+
* @param {string} label Reader name used in warn messages.
|
|
229
280
|
* @returns {Promise<{ linesRead: number, linesParsed: number, missing: boolean }>}
|
|
230
281
|
*/
|
|
231
|
-
|
|
232
|
-
if (typeof cb !== 'function') {
|
|
233
|
-
Logger.warn('signals-writer: forEachLine called without a callback');
|
|
234
|
-
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let target;
|
|
238
|
-
try {
|
|
239
|
-
target = signalsFile(epicId, storyId, config);
|
|
240
|
-
} catch (err) {
|
|
241
|
-
Logger.warn(
|
|
242
|
-
`signals-writer: invalid epicId/storyId for forEachLine: ${
|
|
243
|
-
err instanceof Error ? err.message : String(err)
|
|
244
|
-
}`,
|
|
245
|
-
);
|
|
246
|
-
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
247
|
-
}
|
|
248
|
-
|
|
282
|
+
async function forEachLineIn(target, cb, label) {
|
|
249
283
|
try {
|
|
250
284
|
await fs.access(target);
|
|
251
285
|
} catch {
|
|
@@ -278,7 +312,7 @@ export async function forEachLine(epicId, storyId, cb, config) {
|
|
|
278
312
|
await cb(parsed, linesRead);
|
|
279
313
|
} catch (err) {
|
|
280
314
|
Logger.warn(
|
|
281
|
-
`signals-writer:
|
|
315
|
+
`signals-writer: ${label} cb threw at ${target}:${linesRead}: ${
|
|
282
316
|
err instanceof Error ? err.message : String(err)
|
|
283
317
|
}`,
|
|
284
318
|
);
|
|
@@ -286,7 +320,7 @@ export async function forEachLine(epicId, storyId, cb, config) {
|
|
|
286
320
|
}
|
|
287
321
|
} catch (err) {
|
|
288
322
|
Logger.warn(
|
|
289
|
-
`signals-writer:
|
|
323
|
+
`signals-writer: ${label} read failed for ${target}: ${
|
|
290
324
|
err instanceof Error ? err.message : String(err)
|
|
291
325
|
}`,
|
|
292
326
|
);
|
|
@@ -294,3 +328,72 @@ export async function forEachLine(epicId, storyId, cb, config) {
|
|
|
294
328
|
|
|
295
329
|
return { linesRead, linesParsed, missing: false };
|
|
296
330
|
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Stream a per-Story `signals.ndjson` line by line, invoking
|
|
334
|
+
* `cb(parsed, lineNumber)` for each successfully parsed JSON line. A
|
|
335
|
+
* missing file resolves with `missing: true` rather than throwing — the
|
|
336
|
+
* analyzer treats absence as "no signals yet" and keeps walking.
|
|
337
|
+
*
|
|
338
|
+
* @param {number} epicId
|
|
339
|
+
* @param {number} storyId
|
|
340
|
+
* @param {(parsed: unknown, lineNumber: number) => unknown | Promise<unknown>} cb
|
|
341
|
+
* @param {object} [config]
|
|
342
|
+
* @returns {Promise<{ linesRead: number, linesParsed: number, missing: boolean }>}
|
|
343
|
+
*/
|
|
344
|
+
export async function forEachLine(epicId, storyId, cb, config) {
|
|
345
|
+
if (typeof cb !== 'function') {
|
|
346
|
+
Logger.warn('signals-writer: forEachLine called without a callback');
|
|
347
|
+
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let target;
|
|
351
|
+
try {
|
|
352
|
+
target = signalsFile(epicId, storyId, config);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
Logger.warn(
|
|
355
|
+
`signals-writer: invalid epicId/storyId for forEachLine: ${
|
|
356
|
+
err instanceof Error ? err.message : String(err)
|
|
357
|
+
}`,
|
|
358
|
+
);
|
|
359
|
+
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return forEachLineIn(target, cb, 'forEachLine');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Stream the per-Epic `signals.ndjson` line by line, invoking
|
|
367
|
+
* `cb(parsed, lineNumber)` for each successfully parsed JSON line. This is
|
|
368
|
+
* the read side of `appendEpicSignal` — the wave-lifecycle stream at
|
|
369
|
+
* `temp/epic-<eid>/signals.ndjson` written by `appendEpicSignal` callers
|
|
370
|
+
* (e.g. `lifecycle-emit`). A missing file resolves with `missing: true`
|
|
371
|
+
* rather than throwing, mirroring the per-Story `forEachLine` contract so
|
|
372
|
+
* the retro's unified counts scan can fold the Epic stream in alongside the
|
|
373
|
+
* Story streams without a special-cased absence branch.
|
|
374
|
+
*
|
|
375
|
+
* @param {number} epicId
|
|
376
|
+
* @param {(parsed: unknown, lineNumber: number) => unknown | Promise<unknown>} cb
|
|
377
|
+
* @param {object} [config]
|
|
378
|
+
* @returns {Promise<{ linesRead: number, linesParsed: number, missing: boolean }>}
|
|
379
|
+
*/
|
|
380
|
+
export async function forEachEpicLine(epicId, cb, config) {
|
|
381
|
+
if (typeof cb !== 'function') {
|
|
382
|
+
Logger.warn('signals-writer: forEachEpicLine called without a callback');
|
|
383
|
+
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let target;
|
|
387
|
+
try {
|
|
388
|
+
target = epicArtifactPath(epicId, EPIC_SIGNALS_BASENAME, config);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
Logger.warn(
|
|
391
|
+
`signals-writer: invalid epicId for forEachEpicLine: ${
|
|
392
|
+
err instanceof Error ? err.message : String(err)
|
|
393
|
+
}`,
|
|
394
|
+
);
|
|
395
|
+
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return forEachLineIn(target, cb, 'forEachEpicLine');
|
|
399
|
+
}
|
|
@@ -220,10 +220,10 @@ function hashBashInput(toolInput) {
|
|
|
220
220
|
* (`tool_input.file_path`, `tool_input.path`, `tool_input.pattern`)
|
|
221
221
|
* before recording. The raw value never appears on disk.
|
|
222
222
|
*
|
|
223
|
-
* @param {{ tool: string, toolInput?: object, durationMs?: number|null }} args
|
|
223
|
+
* @param {{ tool: string, toolInput?: object, durationMs?: number|null, exitCode?: number|null }} args
|
|
224
224
|
* @returns {object}
|
|
225
225
|
*/
|
|
226
|
-
function buildDetails({ tool, toolInput, durationMs }) {
|
|
226
|
+
function buildDetails({ tool, toolInput, durationMs, exitCode }) {
|
|
227
227
|
const details = {};
|
|
228
228
|
if (typeof durationMs === 'number') {
|
|
229
229
|
details.durationMs = durationMs;
|
|
@@ -231,6 +231,14 @@ function buildDetails({ tool, toolInput, durationMs }) {
|
|
|
231
231
|
details.durationMs = null;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Bash exit code — the retry detector's failure predicate
|
|
235
|
+
// (`details.exitCode !== 0`) can only fire once this is recorded. Only
|
|
236
|
+
// Bash PostToolUse events carry a meaningful exit code; other tools omit
|
|
237
|
+
// the field entirely (Epic #4406 / Story #4413).
|
|
238
|
+
if (tool === 'Bash' && typeof exitCode === 'number') {
|
|
239
|
+
details.exitCode = exitCode;
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
if (toolInput && typeof toolInput === 'object') {
|
|
235
243
|
// Bash: hash `command` so a token-laden string never lands on disk.
|
|
236
244
|
// Story #1768 also records `normalizedHash` (paraphrase-collapsed)
|
|
@@ -273,6 +281,35 @@ function buildDetails({ tool, toolInput, durationMs }) {
|
|
|
273
281
|
return details;
|
|
274
282
|
}
|
|
275
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Extract the Bash exit code from a PostToolUse event. The harness reports
|
|
286
|
+
* the tool result under `tool_response` (occasionally at the event root);
|
|
287
|
+
* the exit-code field name is not contractually fixed across harness
|
|
288
|
+
* versions, so we probe the known aliases and return the first numeric
|
|
289
|
+
* hit. Returns `null` when no numeric exit code is present — the caller
|
|
290
|
+
* omits `details.exitCode` entirely rather than recording a guess.
|
|
291
|
+
*
|
|
292
|
+
* Exported for testing.
|
|
293
|
+
*
|
|
294
|
+
* @param {object} event
|
|
295
|
+
* @returns {number|null}
|
|
296
|
+
*/
|
|
297
|
+
export function extractExitCode(event) {
|
|
298
|
+
const candidates = [
|
|
299
|
+
event?.tool_response?.exitCode,
|
|
300
|
+
event?.tool_response?.exit_code,
|
|
301
|
+
event?.tool_response?.returnCode,
|
|
302
|
+
event?.tool_response?.code,
|
|
303
|
+
event?.tool_response?.status,
|
|
304
|
+
event?.exit_code,
|
|
305
|
+
event?.exitCode,
|
|
306
|
+
];
|
|
307
|
+
for (const c of candidates) {
|
|
308
|
+
if (typeof c === 'number' && Number.isFinite(c)) return c;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
276
313
|
/**
|
|
277
314
|
* PreToolUse handler. Stashes `{ startedAt, tool }` into the in-flight
|
|
278
315
|
* Map keyed by the harness `tool_use_id`. Does not append to
|
|
@@ -306,6 +343,7 @@ export async function handlePost(event, active) {
|
|
|
306
343
|
const id = event?.tool_use_id ?? event?.id ?? null;
|
|
307
344
|
const tool = event?.tool_name ?? event?.tool ?? 'unknown';
|
|
308
345
|
const toolInput = event?.tool_input;
|
|
346
|
+
const exitCode = extractExitCode(event);
|
|
309
347
|
|
|
310
348
|
let durationMs = null;
|
|
311
349
|
if (id && inflight.has(id)) {
|
|
@@ -317,13 +355,13 @@ export async function handlePost(event, active) {
|
|
|
317
355
|
const trace = {
|
|
318
356
|
ts: new Date().toISOString(),
|
|
319
357
|
kind: 'trace',
|
|
320
|
-
|
|
358
|
+
emitter: { tool: clamp(tool) },
|
|
321
359
|
epicId: active.epicId,
|
|
322
360
|
storyId: active.storyId,
|
|
323
361
|
taskId: null,
|
|
324
362
|
phase:
|
|
325
363
|
typeof process.env.CC_PHASE === 'string' ? process.env.CC_PHASE : null,
|
|
326
|
-
details: buildDetails({ tool, toolInput, durationMs }),
|
|
364
|
+
details: buildDetails({ tool, toolInput, durationMs, exitCode }),
|
|
327
365
|
};
|
|
328
366
|
|
|
329
367
|
await appendTrace({
|
|
@@ -174,7 +174,7 @@ export function buildAcceptanceEvalSignal({
|
|
|
174
174
|
epicId: epicId ?? null,
|
|
175
175
|
storyId,
|
|
176
176
|
phase,
|
|
177
|
-
|
|
177
|
+
emitter: { tool: 'acceptance-eval.js' },
|
|
178
178
|
details: {
|
|
179
179
|
decision: outcome.decision,
|
|
180
180
|
round: outcome.round,
|
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
* Behaviour:
|
|
23
23
|
* - Loads the configured review adapter via the factory; defaults to
|
|
24
24
|
* `native` when `delivery.codeReview.provider` is unset.
|
|
25
|
-
* - Always posts the
|
|
26
|
-
* issue (the adapter never posts; the orchestrator owns
|
|
25
|
+
* - Always posts the unified `verification-results` structured comment on
|
|
26
|
+
* the Epic issue (the adapter never posts; the orchestrator owns
|
|
27
|
+
* persistence). Story #4411 (Epic #4405) unified the former
|
|
28
|
+
* `code-review` and `audit-results` findings contracts into this one
|
|
29
|
+
* `verification-results` marker.
|
|
27
30
|
* - Treats severity.critical > 0 as a halting blocker — the merged
|
|
28
31
|
* `/deliver` runner consults `halted` and refuses to advance
|
|
29
32
|
* to Phase E (retro) when set.
|
|
@@ -32,6 +35,8 @@
|
|
|
32
35
|
* helper's "operator must remediate before /deliver" gate.
|
|
33
36
|
*/
|
|
34
37
|
|
|
38
|
+
import { hasSurvivingCritical } from '../audit-suite/findings.js';
|
|
39
|
+
import { resolveLensTier } from '../audit-suite/selector.js';
|
|
35
40
|
import { resolveConfig } from '../config-resolver.js';
|
|
36
41
|
import { selectAuditStrategy } from '../dynamic-workflow/capability.js';
|
|
37
42
|
import { gitSpawn } from '../git-utils.js';
|
|
@@ -228,6 +233,71 @@ export function resolveAuditLenses(envelope = {}) {
|
|
|
228
233
|
return LENS_ORDER.filter((lens) => matched.has(lens));
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Compose the Epic-close (gate3) lens roster from the change-set selection
|
|
238
|
+
* plus every risk-routed lens.
|
|
239
|
+
*
|
|
240
|
+
* STOPGAP (post-#4405 review finding): Story #4412 excluded every
|
|
241
|
+
* `local`-tier change-set lens here on the premise that the Story-scope
|
|
242
|
+
* local-lens pass (Story #4409) verifies those concerns shift-left. That
|
|
243
|
+
* premise does not hold yet — `runLocalLensReview`
|
|
244
|
+
* (story-close/phases/code-review.js) materializes lens bodies with no
|
|
245
|
+
* `artifactPrefix` (so no content persists) and no consumer acts on its
|
|
246
|
+
* result, which left change-set lenses (security, privacy, performance,
|
|
247
|
+
* quality, …) executed by NO reviewer at ANY tier. Local-tier lenses are
|
|
248
|
+
* therefore KEPT at Epic close until the story-scope pass has a real
|
|
249
|
+
* consumer; re-enable the exclusion then (tracked as a follow-up issue).
|
|
250
|
+
* The write-time maker checklists (#4410) are advisory self-checks, not
|
|
251
|
+
* verification. The `resolveLensTierFn` seam is retained so the
|
|
252
|
+
* re-exclusion is a one-line revert.
|
|
253
|
+
*
|
|
254
|
+
* The two inputs mirror the `epic-audit-prepare.js` envelope:
|
|
255
|
+
* - `changeSetAudits` — the raw change-set gate3 selection, kept in full
|
|
256
|
+
* (see the stopgap note above).
|
|
257
|
+
* - `riskRoutedAudits` — the verdict-routed high-risk lenses plus the
|
|
258
|
+
* route-glob navigability lens. Kept in **full**, regardless of tier: a
|
|
259
|
+
* high-risk axis (or a route-adding change set) that demands a local-tier
|
|
260
|
+
* lens still runs it at Epic close, because that demand is the whole point
|
|
261
|
+
* of risk routing.
|
|
262
|
+
*
|
|
263
|
+
* De-duplicated and order-preserving (kept change-set lenses first, then the
|
|
264
|
+
* risk-routed extras). Pure over the injected `resolveLensTierFn` seam; the
|
|
265
|
+
* default resolver reads `audit-rules.json` from disk.
|
|
266
|
+
*
|
|
267
|
+
* @param {{
|
|
268
|
+
* changeSetAudits?: string[],
|
|
269
|
+
* riskRoutedAudits?: string[],
|
|
270
|
+
* resolveLensTierFn?: typeof resolveLensTier,
|
|
271
|
+
* }} [params]
|
|
272
|
+
* @returns {string[]} The Epic-close roster (all change-set tiers +
|
|
273
|
+
* risk-routed; local-tier exclusion suspended per the stopgap above).
|
|
274
|
+
*/
|
|
275
|
+
export function selectEpicCloseLenses({
|
|
276
|
+
changeSetAudits = [],
|
|
277
|
+
riskRoutedAudits = [],
|
|
278
|
+
resolveLensTierFn = resolveLensTier,
|
|
279
|
+
} = {}) {
|
|
280
|
+
const kept = [];
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
const add = (lens) => {
|
|
283
|
+
if (typeof lens !== 'string' || lens.length === 0 || seen.has(lens)) return;
|
|
284
|
+
seen.add(lens);
|
|
285
|
+
kept.push(lens);
|
|
286
|
+
};
|
|
287
|
+
for (const lens of Array.isArray(changeSetAudits) ? changeSetAudits : []) {
|
|
288
|
+
if (typeof lens !== 'string' || lens.length === 0) continue;
|
|
289
|
+
// Stopgap: keep local-tier lenses (see docstring). The tier seam is
|
|
290
|
+
// deliberately untouched so restoring `=== 'local' → continue` is a
|
|
291
|
+
// one-line revert once the story-scope tier has a real consumer.
|
|
292
|
+
void resolveLensTierFn;
|
|
293
|
+
add(lens);
|
|
294
|
+
}
|
|
295
|
+
for (const lens of Array.isArray(riskRoutedAudits) ? riskRoutedAudits : []) {
|
|
296
|
+
add(lens);
|
|
297
|
+
}
|
|
298
|
+
return kept;
|
|
299
|
+
}
|
|
300
|
+
|
|
231
301
|
/**
|
|
232
302
|
* Build the post-delivery audit-lens execution plan for a judged risk
|
|
233
303
|
* envelope. Each routed lens (see {@link resolveAuditLenses}) is paired with a
|
|
@@ -551,7 +621,7 @@ async function postReviewComment({
|
|
|
551
621
|
const postResult = await upsertCommentFn(
|
|
552
622
|
provider,
|
|
553
623
|
commentTargetId,
|
|
554
|
-
'
|
|
624
|
+
'verification-results',
|
|
555
625
|
report,
|
|
556
626
|
);
|
|
557
627
|
const postedCommentId =
|
|
@@ -621,7 +691,7 @@ async function executeReviewPipeline({ opts, config, envelope }) {
|
|
|
621
691
|
);
|
|
622
692
|
|
|
623
693
|
const severity = countBySeverity(findings);
|
|
624
|
-
const halted = severity
|
|
694
|
+
const halted = hasSurvivingCritical(severity);
|
|
625
695
|
const report = renderFindingsFn({
|
|
626
696
|
scope,
|
|
627
697
|
ticketId,
|