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
|
@@ -18,48 +18,50 @@
|
|
|
18
18
|
* the codebase as of Epic #1181 (audit-snapshot 2026-05-11):
|
|
19
19
|
*
|
|
20
20
|
* Signals-writer `appendSignal` call sites:
|
|
21
|
-
* - `friction` — quality-gate / runtime friction (
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - `dispatched` — (no live emitter; retained pending Story #3908 sweep)
|
|
21
|
+
* - `friction` — quality-gate / runtime friction (diagnose-friction.js,
|
|
22
|
+
* lib/gates/friction.js, auto-refresh-runner.js,
|
|
23
|
+
* worktree-reap.js, lifecycle-emit.js)
|
|
24
|
+
* - `acceptance-eval` — acceptance-eval.js per-criterion terminus signal.
|
|
26
25
|
* - `wave-start` / `wave-complete` — `lib/wave-runner/tick.js` (read by
|
|
27
26
|
* perf-aggregator's `waveParallelism` report;
|
|
28
27
|
* `wave-start` also anchors span-tree Story spans)
|
|
29
|
-
* - `wave-end` — span-tree pairing anchor (
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* - `wave-end` — span-tree pairing anchor (retained for span-tree)
|
|
29
|
+
* - `state-transition` — notification-derived window anchor read by the
|
|
30
|
+
* waveParallelism bucketer.
|
|
32
31
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* rollup and nothing read them back.
|
|
32
|
+
* The `dispatched` kind was deleted in the Epic #4406 signal-contract
|
|
33
|
+
* cutover — it had no live emitter and no consumer.
|
|
36
34
|
*
|
|
37
35
|
* Signals-writer `appendTrace` call sites (traces.ndjson sibling, but
|
|
38
36
|
* sharing the same envelope shape — `tool-trace-hook.js`):
|
|
39
37
|
* - `trace` — per-tool-call timing record
|
|
40
38
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Detector-emitted / aggregator-consumed kinds (`rework`, `retry` are
|
|
40
|
+
* emitted by `lib/signals/detectors/*`; `hotspot`, `churn`, `idle` remain
|
|
41
|
+
* pinned in the enum for the aggregator's kind-count rollup even though no
|
|
42
|
+
* live detector emits them):
|
|
45
43
|
* - `hotspot`, `rework`, `churn`, `idle`, `retry`
|
|
46
44
|
*
|
|
47
|
-
* ## Common envelope
|
|
45
|
+
* ## Common envelope (canonical — Epic #4406 / Story #4413)
|
|
48
46
|
*
|
|
49
47
|
* Every signal MUST carry at minimum:
|
|
50
|
-
* - `ts` — ISO-8601 timestamp string
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `ts:` lands in a follow-on Story).
|
|
54
|
-
* - `epic` — integer Epic ID (writers historically used `epicId:`;
|
|
55
|
-
* same backward-compat note).
|
|
48
|
+
* - `ts` — ISO-8601 timestamp string. The single canonical
|
|
49
|
+
* timestamp key; the legacy `timestamp:` alias was deleted
|
|
50
|
+
* from every writer and this guard in the same PR.
|
|
56
51
|
* - `kind` — one of `EVENT_KINDS`.
|
|
57
52
|
*
|
|
53
|
+
* Scoped signals additionally carry:
|
|
54
|
+
* - `epicId` — integer Epic ID (or `null` for standalone-Story
|
|
55
|
+
* friction). The single canonical epic-id key; the legacy
|
|
56
|
+
* `epic:` alias was deleted.
|
|
57
|
+
*
|
|
58
58
|
* Optional but commonly carried:
|
|
59
|
-
* - `
|
|
60
|
-
* - `
|
|
61
|
-
* - `
|
|
62
|
-
* - `
|
|
59
|
+
* - `storyId` — integer Story ID
|
|
60
|
+
* - `taskId` — integer Task ID (nullable)
|
|
61
|
+
* - `emitter` — `{ tool: string, command?: string }` provenance
|
|
62
|
+
* - `source` — `"framework" | "consumer"` classifier tag
|
|
63
|
+
* - `category` — top-level friction category string
|
|
64
|
+
* - `details` — kind-specific payload (always an object)
|
|
63
65
|
*
|
|
64
66
|
* @module lib/signals/schema
|
|
65
67
|
*/
|
|
@@ -77,7 +79,6 @@ import { isPositiveInt } from './detectors/common.js';
|
|
|
77
79
|
export const EVENT_KINDS = Object.freeze({
|
|
78
80
|
FRICTION: 'friction',
|
|
79
81
|
TRACE: 'trace',
|
|
80
|
-
DISPATCHED: 'dispatched',
|
|
81
82
|
// Wave-window forensics signals: `wave-start` / `wave-end` anchor the
|
|
82
83
|
// span-tree's Story spans, and the perf-aggregator brackets each wave's
|
|
83
84
|
// wall-clock from `wave-start` → `wave-complete` (the `waveParallelism`
|
|
@@ -101,6 +102,13 @@ export const EVENT_KINDS = Object.freeze({
|
|
|
101
102
|
// the retro and /plan Phase 0 feedback fetch can see acceptance
|
|
102
103
|
// churn alongside friction/hotspot data.
|
|
103
104
|
ACCEPTANCE_EVAL: 'acceptance-eval',
|
|
105
|
+
// Forensic breadcrumb appended to the per-Epic stream by the notify
|
|
106
|
+
// dispatcher (lib/orchestration/lifecycle/listeners/notify-dispatcher.js)
|
|
107
|
+
// when a lifecycle event maps to a webhook notification — the resume
|
|
108
|
+
// suite reads it back to prove a dispatch survived a crash window without
|
|
109
|
+
// duplicating. Enumerated so the write-time validator (Story #4413) does
|
|
110
|
+
// not drop it; it is a deliberate write, not malformed data.
|
|
111
|
+
NOTIFICATION_EMITTED: 'notification.emitted',
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
/**
|
|
@@ -117,20 +125,17 @@ export const EVENT_KIND_VALUES = Object.freeze(
|
|
|
117
125
|
* spread string literals across the module graph.
|
|
118
126
|
*/
|
|
119
127
|
export const FIELDS = Object.freeze({
|
|
120
|
-
//
|
|
128
|
+
// Canonical envelope keys — there is exactly one key per concept. The
|
|
129
|
+
// legacy `timestamp` / `epic` / `story` / `task` aliases were deleted in
|
|
130
|
+
// the Epic #4406 signal-contract cutover; no reader tolerates them.
|
|
121
131
|
TS: 'ts',
|
|
122
|
-
EPIC: 'epic',
|
|
123
|
-
STORY: 'story',
|
|
124
|
-
TASK: 'task',
|
|
125
|
-
KIND: 'kind',
|
|
126
|
-
|
|
127
|
-
// Legacy envelope aliases still emitted by some writers
|
|
128
|
-
TIMESTAMP: 'timestamp',
|
|
129
132
|
EPIC_ID: 'epicId',
|
|
130
133
|
STORY_ID: 'storyId',
|
|
131
134
|
TASK_ID: 'taskId',
|
|
135
|
+
KIND: 'kind',
|
|
132
136
|
|
|
133
137
|
// Common payload fields
|
|
138
|
+
EMITTER: 'emitter',
|
|
134
139
|
SOURCE: 'source',
|
|
135
140
|
DETAILS: 'details',
|
|
136
141
|
CATEGORY: 'category',
|
|
@@ -150,8 +155,9 @@ function isTimestamp(v) {
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
/**
|
|
153
|
-
* Common envelope guard: every signal MUST carry `ts`
|
|
154
|
-
*
|
|
158
|
+
* Common envelope guard: every signal MUST carry `ts`, `epicId`, and a
|
|
159
|
+
* recognised `kind`. Canonical keys only — the legacy `timestamp` / `epic`
|
|
160
|
+
* aliases were deleted in the Epic #4406 cutover.
|
|
155
161
|
*
|
|
156
162
|
* Returns true when the envelope is well-formed. Used by `lib/signals/read`
|
|
157
163
|
* to discard malformed lines before yielding them to the consumer.
|
|
@@ -164,16 +170,24 @@ export function hasCommonEnvelope(evt) {
|
|
|
164
170
|
if (typeof evt.kind !== 'string' || !EVENT_KIND_VALUES.has(evt.kind)) {
|
|
165
171
|
return false;
|
|
166
172
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
if (!isTimestamp(evt.ts)) return false;
|
|
174
|
+
// The canonical `epicId` key MUST be present — a record carrying only the
|
|
175
|
+
// legacy `epic` alias is rejected (the cutover deleted that alias). Its
|
|
176
|
+
// value is nullable by contract: standalone-Story friction carries
|
|
177
|
+
// `epicId: null` (see signal-event.schema.json, where epicId is
|
|
178
|
+
// `["integer","null"]`). So accept an explicit null, reject a missing key
|
|
179
|
+
// or a present-but-non-positive-int value.
|
|
180
|
+
if (!Object.hasOwn(evt, 'epicId')) return false;
|
|
181
|
+
if (evt.epicId !== null && !isPositiveInt(evt.epicId)) return false;
|
|
171
182
|
return true;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
/**
|
|
175
186
|
* Generic per-kind guard. Returns true when the envelope is well-formed
|
|
176
|
-
* AND (when `kind` is supplied) the event's `kind` matches.
|
|
187
|
+
* AND (when `kind` is supplied) the event's `kind` matches. The full
|
|
188
|
+
* canonical-shape check lives in the AJV validator compiled from
|
|
189
|
+
* `signal-event.schema.json` (see `lib/observability/signal-validator.js`);
|
|
190
|
+
* this predicate is the cheap envelope gate the streaming reader uses.
|
|
177
191
|
*
|
|
178
192
|
* @param {unknown} evt
|
|
179
193
|
* @param {string} [kind] — optional kind to match (one of `EVENT_KINDS`).
|
|
@@ -184,42 +198,3 @@ export function isValidSignal(evt, kind) {
|
|
|
184
198
|
if (kind != null && evt.kind !== kind) return false;
|
|
185
199
|
return true;
|
|
186
200
|
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Per-kind shape guards. Each entry asserts the envelope plus any
|
|
190
|
-
* required per-kind fields. Unknown kinds fall back to the envelope
|
|
191
|
-
* check.
|
|
192
|
-
*
|
|
193
|
-
* The guards are intentionally lax — they reject records that are
|
|
194
|
-
* obviously malformed (missing `kind`, missing `ts`, missing `epic`),
|
|
195
|
-
* not records with extra fields or future schema extensions. The
|
|
196
|
-
* aggregator (perf-aggregator.js) carries its own per-kind narrowing.
|
|
197
|
-
*
|
|
198
|
-
* @type {Readonly<Record<string, (evt: unknown) => boolean>>}
|
|
199
|
-
*/
|
|
200
|
-
export const GUARDS = Object.freeze({
|
|
201
|
-
[EVENT_KINDS.FRICTION]: (evt) => {
|
|
202
|
-
if (!isValidSignal(evt, EVENT_KINDS.FRICTION)) return false;
|
|
203
|
-
// friction signals commonly carry a `category` field, but some
|
|
204
|
-
// writers (early in the migration) omit it. We accept both.
|
|
205
|
-
return true;
|
|
206
|
-
},
|
|
207
|
-
[EVENT_KINDS.TRACE]: (evt) => isValidSignal(evt, EVENT_KINDS.TRACE),
|
|
208
|
-
[EVENT_KINDS.DISPATCHED]: (evt) => isValidSignal(evt, EVENT_KINDS.DISPATCHED),
|
|
209
|
-
[EVENT_KINDS.WAVE_START]: (evt) => isValidSignal(evt, EVENT_KINDS.WAVE_START),
|
|
210
|
-
[EVENT_KINDS.WAVE_END]: (evt) => isValidSignal(evt, EVENT_KINDS.WAVE_END),
|
|
211
|
-
[EVENT_KINDS.WAVE_TICK]: (evt) => isValidSignal(evt, EVENT_KINDS.WAVE_TICK),
|
|
212
|
-
[EVENT_KINDS.WAVE_COMPLETE]: (evt) =>
|
|
213
|
-
isValidSignal(evt, EVENT_KINDS.WAVE_COMPLETE),
|
|
214
|
-
[EVENT_KINDS.EPIC_COMPLETE]: (evt) =>
|
|
215
|
-
isValidSignal(evt, EVENT_KINDS.EPIC_COMPLETE),
|
|
216
|
-
[EVENT_KINDS.STATE_TRANSITION]: (evt) =>
|
|
217
|
-
isValidSignal(evt, EVENT_KINDS.STATE_TRANSITION),
|
|
218
|
-
[EVENT_KINDS.HOTSPOT]: (evt) => isValidSignal(evt, EVENT_KINDS.HOTSPOT),
|
|
219
|
-
[EVENT_KINDS.REWORK]: (evt) => isValidSignal(evt, EVENT_KINDS.REWORK),
|
|
220
|
-
[EVENT_KINDS.CHURN]: (evt) => isValidSignal(evt, EVENT_KINDS.CHURN),
|
|
221
|
-
[EVENT_KINDS.IDLE]: (evt) => isValidSignal(evt, EVENT_KINDS.IDLE),
|
|
222
|
-
[EVENT_KINDS.RETRY]: (evt) => isValidSignal(evt, EVENT_KINDS.RETRY),
|
|
223
|
-
[EVENT_KINDS.ACCEPTANCE_EVAL]: (evt) =>
|
|
224
|
-
isValidSignal(evt, EVENT_KINDS.ACCEPTANCE_EVAL),
|
|
225
|
-
});
|
|
@@ -51,20 +51,21 @@
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
function tsOf(evt) {
|
|
54
|
-
//
|
|
55
|
-
|
|
54
|
+
// Canonical envelope only (Epic #4406) — the legacy `timestamp` alias
|
|
55
|
+
// was deleted from every writer in the same PR.
|
|
56
|
+
return evt?.ts ?? null;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function epicOf(evt) {
|
|
59
|
-
return evt?.
|
|
60
|
+
return evt?.epicId ?? null;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function storyOf(evt) {
|
|
63
|
-
return evt?.
|
|
64
|
+
return evt?.storyId ?? null;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function taskOf(evt) {
|
|
67
|
-
return evt?.
|
|
68
|
+
return evt?.taskId ?? null;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -193,6 +193,7 @@ export async function loadBodyTemplate(projectRoot) {
|
|
|
193
193
|
* bodyTemplate: string,
|
|
194
194
|
* duplicateCandidates: Array<object>,
|
|
195
195
|
* techStack?: string|null,
|
|
196
|
+
* corpusContext?: { docsDigest: string|null, relevantSections: Array<object> }|null,
|
|
196
197
|
* maxResults?: number,
|
|
197
198
|
* }} opts
|
|
198
199
|
*/
|
|
@@ -203,6 +204,7 @@ export function buildContextEnvelope({
|
|
|
203
204
|
bodyTemplate,
|
|
204
205
|
duplicateCandidates,
|
|
205
206
|
techStack = null,
|
|
207
|
+
corpusContext = null,
|
|
206
208
|
maxResults = DEFAULT_DUPLICATE_MAX_RESULTS,
|
|
207
209
|
}) {
|
|
208
210
|
return {
|
|
@@ -218,6 +220,7 @@ export function buildContextEnvelope({
|
|
|
218
220
|
candidates: duplicateCandidates,
|
|
219
221
|
},
|
|
220
222
|
techStack,
|
|
223
|
+
corpusContext,
|
|
221
224
|
deliverContract: {
|
|
222
225
|
workflow: '.agents/workflows/helpers/single-story-deliver.md',
|
|
223
226
|
requiredLabels: ['type::story', `persona::${persona}`],
|
|
@@ -425,7 +425,13 @@ export function planTick(state, records, inFlight) {
|
|
|
425
425
|
inFlight.length === 0
|
|
426
426
|
) {
|
|
427
427
|
signals.push({
|
|
428
|
+
// `index` is what the perf-aggregator's `bucketWaves` keys on to
|
|
429
|
+
// bracket a wave's wall-clock (Epic #4406 / Story #4413). Without
|
|
430
|
+
// it every wave-start collapsed to NaN and the waveParallelism
|
|
431
|
+
// table rendered structurally empty. The continuous ready-set
|
|
432
|
+
// scheduler runs a single logical wave, so `index: 0`.
|
|
428
433
|
kind: 'wave-start',
|
|
434
|
+
index: 0,
|
|
429
435
|
stories: records.map((s) => ({ id: s.id, title: s.title })),
|
|
430
436
|
});
|
|
431
437
|
}
|
|
@@ -443,7 +449,7 @@ export function planTick(state, records, inFlight) {
|
|
|
443
449
|
nextAction = { kind: 'observe', waitingOn };
|
|
444
450
|
} else if (allDone) {
|
|
445
451
|
// Every Story is done and nothing is in flight: the run is complete.
|
|
446
|
-
signals.push({ kind: 'wave-complete' });
|
|
452
|
+
signals.push({ kind: 'wave-complete', index: 0 });
|
|
447
453
|
nextAction = { kind: 'epic-complete' };
|
|
448
454
|
} else {
|
|
449
455
|
// Ready set empty, nothing in flight, but not all Stories are done — a
|
|
@@ -739,7 +745,9 @@ function defaultSignalEmit(epicId, ctx) {
|
|
|
739
745
|
return async (signal) => {
|
|
740
746
|
await appendEpicSignal({
|
|
741
747
|
epicId,
|
|
742
|
-
|
|
748
|
+
// Canonical envelope (Epic #4406 / Story #4413): the single
|
|
749
|
+
// `epicId` key — the legacy `epic` alias is gone.
|
|
750
|
+
signal: { ts: new Date().toISOString(), epicId, ...signal },
|
|
743
751
|
config: ctx?.config,
|
|
744
752
|
});
|
|
745
753
|
};
|
|
@@ -130,14 +130,27 @@ export function parseArgv(argv) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
133
|
+
* Argv keys that are runtime OPTIONS consumed by `main()` /
|
|
134
|
+
* `runLifecycleEmit()` directly rather than forwarded into the emitted
|
|
135
|
+
* bus payload. `--headless` (Story #4427) is the first member: every
|
|
136
|
+
* lifecycle event schema declares `additionalProperties: false`, so a
|
|
137
|
+
* flag that selects delivery-run behavior (rather than describing the
|
|
138
|
+
* event itself) would fail schema validation if it leaked into the
|
|
139
|
+
* payload.
|
|
140
|
+
*/
|
|
141
|
+
const RUNTIME_OPTION_KEYS = Object.freeze(['event', 'headless']);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build the bus emit payload from parsed argv. `event` and `headless`
|
|
145
|
+
* (see `RUNTIME_OPTION_KEYS`) are consumed as runtime options, not
|
|
146
|
+
* payload fields. `--epic` is mapped to `epicId` (integer). All other
|
|
147
|
+
* flags are mapped from kebab-case to camelCase with light value
|
|
148
|
+
* coercion.
|
|
136
149
|
*/
|
|
137
150
|
export function buildPayload(parsed) {
|
|
138
151
|
const payload = {};
|
|
139
152
|
for (const [key, raw] of Object.entries(parsed)) {
|
|
140
|
-
if (key
|
|
153
|
+
if (RUNTIME_OPTION_KEYS.includes(key)) continue;
|
|
141
154
|
if (key === 'epic') {
|
|
142
155
|
const n = Number.parseInt(raw, 10);
|
|
143
156
|
if (!Number.isInteger(n) || n < 1) {
|
|
@@ -261,11 +274,16 @@ export async function emitBlockedSignal({
|
|
|
261
274
|
config,
|
|
262
275
|
signal: {
|
|
263
276
|
kind: 'friction',
|
|
277
|
+
ts: new Date().toISOString(),
|
|
278
|
+
epicId,
|
|
279
|
+
category: 'lifecycle-listener-failure',
|
|
280
|
+
emitter: { tool: 'lifecycle-emit.js' },
|
|
264
281
|
severity: 'high',
|
|
265
282
|
event,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
283
|
+
details: {
|
|
284
|
+
message: `lifecycle-emit: ${event} produced failed listener classification(s): ${reasons}`,
|
|
285
|
+
outcomes: failedOutcomes,
|
|
286
|
+
},
|
|
269
287
|
},
|
|
270
288
|
});
|
|
271
289
|
} catch (err) {
|
|
@@ -321,6 +339,11 @@ export async function emitBlockedSignal({
|
|
|
321
339
|
* alongside an injected `bus` (when the caller owns listener wiring) —
|
|
322
340
|
* lets a test inject a bus + a chain carrying a `failed`-classifying
|
|
323
341
|
* listener to exercise the non-zero-exit path without the real roster.
|
|
342
|
+
* @param {boolean} [opts.headless] Explicit must-land signal (Story
|
|
343
|
+
* #4427), forwarded to `buildDefaultListenerChain({ headless })` (and
|
|
344
|
+
* from there to `MergeWatcher`) when this call constructs the default
|
|
345
|
+
* chain. Defaults to `false` — attended-mode behavior is unchanged.
|
|
346
|
+
* Ignored when the caller injects its own `bus`/`chain`.
|
|
324
347
|
*
|
|
325
348
|
* @returns {Promise<{ event: string, payload: object, seqId: number,
|
|
326
349
|
* outcomes: Array<object>, failed: boolean }>} The `outcomes[]` array
|
|
@@ -340,6 +363,7 @@ export async function runLifecycleEmit({
|
|
|
340
363
|
config,
|
|
341
364
|
emitBlockedSignalFn = emitBlockedSignal,
|
|
342
365
|
chain: injectedChain,
|
|
366
|
+
headless = false,
|
|
343
367
|
} = {}) {
|
|
344
368
|
if (typeof event !== 'string' || event.length === 0) {
|
|
345
369
|
throw new Error('lifecycle-emit: --event is required');
|
|
@@ -404,6 +428,7 @@ export async function runLifecycleEmit({
|
|
|
404
428
|
checkpointer: resolvedCheckpointer,
|
|
405
429
|
config: resolvedConfig,
|
|
406
430
|
logger,
|
|
431
|
+
headless,
|
|
407
432
|
});
|
|
408
433
|
}
|
|
409
434
|
const { seqId } = await targetBus.emit(event, payload ?? {});
|
|
@@ -464,7 +489,13 @@ async function main() {
|
|
|
464
489
|
}
|
|
465
490
|
const event = parsed.event;
|
|
466
491
|
const payload = buildPayload(parsed);
|
|
467
|
-
|
|
492
|
+
// `--headless true` is a runtime option (Story #4427), not a payload
|
|
493
|
+
// field — see RUNTIME_OPTION_KEYS. Threaded explicitly through
|
|
494
|
+
// runLifecycleEmit → buildDefaultListenerChain → MergeWatcher so the
|
|
495
|
+
// epic-path must-land terminal step reads a caller-supplied input
|
|
496
|
+
// rather than an ambient global.
|
|
497
|
+
const headless = parsed.headless === 'true';
|
|
498
|
+
const out = await runLifecycleEmit({ event, payload, headless });
|
|
468
499
|
process.stdout.write(`${JSON.stringify(out)}\n`);
|
|
469
500
|
// Exit non-zero when any listener classification came back `failed` so
|
|
470
501
|
// the workflow's "re-run on non-zero" loop closes the partial-finalize
|
|
@@ -150,6 +150,14 @@ export class IssuesGateway {
|
|
|
150
150
|
/**
|
|
151
151
|
* List Epic-typed issues. Filter shape preserved from the old code.
|
|
152
152
|
*
|
|
153
|
+
* `filters.pageCap` / `filters.perPage` pass through to `paginateRest`
|
|
154
|
+
* so a caller that only needs a bounded shortlist (e.g. the
|
|
155
|
+
* standalone-Story corpus lookup in `planning-corpus.js`, which ranks
|
|
156
|
+
* candidates down to a top-5 shortlist) can cap the scan instead of
|
|
157
|
+
* inheriting the default 50-page / 5000-item ceiling. Omitted, both
|
|
158
|
+
* fall back to `paginateRest`'s own defaults — unchanged behavior for
|
|
159
|
+
* existing callers (e.g. `duplicate-search.js`).
|
|
160
|
+
*
|
|
153
161
|
* @field-manifest /repos/{owner}/{repo}/issues?labels=type::epic: number,
|
|
154
162
|
* title, labels, state, state_reason, pull_request
|
|
155
163
|
*/
|
|
@@ -160,7 +168,10 @@ export class IssuesGateway {
|
|
|
160
168
|
labels: TYPE_LABELS.EPIC,
|
|
161
169
|
});
|
|
162
170
|
const endpoint = `/repos/${this.owner}/${this.repo}/issues?${params}`;
|
|
163
|
-
const issues = await paginateRest(this._gh, endpoint
|
|
171
|
+
const issues = await paginateRest(this._gh, endpoint, {
|
|
172
|
+
pageCap: filters.pageCap,
|
|
173
|
+
perPage: filters.perPage,
|
|
174
|
+
});
|
|
164
175
|
return issues
|
|
165
176
|
.filter((issue) => !issue.pull_request)
|
|
166
177
|
.map(issueToEpicListItem);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: resolve the repository's documentation read-tiers (Story #4438).
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over `lib/doc-tiers.js#resolveDocTiers` that prints the tier
|
|
5
|
+
* map — `{ tiers: { alwaysLoaded, mandatoryRead, digestVisible, onDemand } }`,
|
|
6
|
+
* every entry `{ path, bytes }` — as JSON. Consumed by the `audit-documentation`
|
|
7
|
+
* lens (read-tier severity weighting), the `check-context-budget.js` ratchet,
|
|
8
|
+
* and operators inspecting the always-loaded closure.
|
|
9
|
+
*
|
|
10
|
+
* Flags:
|
|
11
|
+
* --json emit the tier map as JSON to stdout (default rendering is also
|
|
12
|
+
* JSON; the flag is accepted for parity with the sibling ratchets
|
|
13
|
+
* and future non-JSON renderings).
|
|
14
|
+
* --root <path> resolve tiers against an explicit repo root (default: the
|
|
15
|
+
* resolved PROJECT_ROOT).
|
|
16
|
+
*
|
|
17
|
+
* Exit code is always 0 on success — this is a reporter, not a gate.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import process from 'node:process';
|
|
21
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
22
|
+
import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
|
|
23
|
+
import { resolveDocTiers } from './lib/doc-tiers.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse argv for `--root <path>` and `--json`.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]} argv
|
|
29
|
+
* @returns {{ rootPath: string | null, json: boolean }}
|
|
30
|
+
*/
|
|
31
|
+
export function parseArgv(argv = []) {
|
|
32
|
+
let rootPath = null;
|
|
33
|
+
let json = false;
|
|
34
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35
|
+
const a = argv[i];
|
|
36
|
+
if (a === '--root') {
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
if (next && !next.startsWith('--')) {
|
|
39
|
+
rootPath = next;
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
} else if (a === '--json') {
|
|
43
|
+
json = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { rootPath, json };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Top-level CLI entry. Exported so tests can drive it against a fixture root
|
|
51
|
+
* with an injected sink and config.
|
|
52
|
+
*
|
|
53
|
+
* @param {{
|
|
54
|
+
* argv?: string[],
|
|
55
|
+
* config?: object,
|
|
56
|
+
* root?: string,
|
|
57
|
+
* stdout?: { write: (s: string) => void },
|
|
58
|
+
* }} [opts]
|
|
59
|
+
* @returns {Promise<number>} always 0
|
|
60
|
+
*/
|
|
61
|
+
export async function runCli({
|
|
62
|
+
argv = process.argv.slice(2),
|
|
63
|
+
config,
|
|
64
|
+
root,
|
|
65
|
+
stdout = process.stdout,
|
|
66
|
+
} = {}) {
|
|
67
|
+
const { rootPath } = parseArgv(argv);
|
|
68
|
+
const resolvedConfig = config ?? resolveConfig();
|
|
69
|
+
const resolvedRoot = root ?? rootPath ?? PROJECT_ROOT;
|
|
70
|
+
const result = resolveDocTiers(resolvedConfig, { root: resolvedRoot });
|
|
71
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
return runCli();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runAsCli(import.meta.url, main, {
|
|
80
|
+
source: 'resolve-doc-tiers',
|
|
81
|
+
propagateExitCode: true,
|
|
82
|
+
errorPrefix: '[resolve-doc-tiers] ❌ Fatal error',
|
|
83
|
+
});
|
|
@@ -37,6 +37,46 @@ import { createLedgerWriter } from './lib/orchestration/lifecycle/ledger-writer.
|
|
|
37
37
|
import { runRetro } from './lib/orchestration/retro-runner.js';
|
|
38
38
|
import { createProvider } from './lib/provider-factory.js';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Pure: resolve the framework/consumer repo slugs (`"<owner>/<repo>"`)
|
|
42
|
+
* from a resolved config's `github` block, for threading into the retro's
|
|
43
|
+
* routed-proposal composer.
|
|
44
|
+
*
|
|
45
|
+
* - `consumerRepo` is the project's OWN repo (`github.owner/github.repo`).
|
|
46
|
+
* It has **no** silent fallback: when `github.owner` / `github.repo` are
|
|
47
|
+
* absent the slug is `''`, which `gatherRetroSignals` treats as "disable
|
|
48
|
+
* the consumer pane loudly" rather than routing consumer-tagged friction
|
|
49
|
+
* at the framework mirror (Story #4417).
|
|
50
|
+
* - `frameworkRepo` is the distinct framework mirror
|
|
51
|
+
* (`github.frameworkRepo.{owner,repo}`) when configured, otherwise `''`
|
|
52
|
+
* — `gatherRetroSignals` then falls back to its `DEFAULT_FRAMEWORK_REPO`
|
|
53
|
+
* constant (a stable, known default is legitimate for the framework).
|
|
54
|
+
*
|
|
55
|
+
* Exported for tests.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} [config]
|
|
58
|
+
* @returns {{ frameworkRepo: string, consumerRepo: string }}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveRetroRepos(config) {
|
|
61
|
+
const gh = config?.github ?? {};
|
|
62
|
+
const consumerRepo =
|
|
63
|
+
typeof gh.owner === 'string' &&
|
|
64
|
+
gh.owner.length > 0 &&
|
|
65
|
+
typeof gh.repo === 'string' &&
|
|
66
|
+
gh.repo.length > 0
|
|
67
|
+
? `${gh.owner}/${gh.repo}`
|
|
68
|
+
: '';
|
|
69
|
+
const fw = gh.frameworkRepo ?? {};
|
|
70
|
+
const frameworkRepo =
|
|
71
|
+
typeof fw.owner === 'string' &&
|
|
72
|
+
fw.owner.length > 0 &&
|
|
73
|
+
typeof fw.repo === 'string' &&
|
|
74
|
+
fw.repo.length > 0
|
|
75
|
+
? `${fw.owner}/${fw.repo}`
|
|
76
|
+
: '';
|
|
77
|
+
return { frameworkRepo, consumerRepo };
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
const HELP = `Usage: node .agents/scripts/retro-run.js --epic <epicId> [--full-retro]
|
|
41
81
|
|
|
42
82
|
Composes and posts the Epic retro structured comment for Epic #<epicId>,
|
|
@@ -125,11 +165,22 @@ export async function runRetroCli({
|
|
|
125
165
|
: createLedgerWriter({ epicId, tempRoot });
|
|
126
166
|
ledger.register(bus);
|
|
127
167
|
|
|
168
|
+
// Story #4417 — thread the framework/consumer repo slugs resolved from
|
|
169
|
+
// `config.github` into the retro so the routed-proposal composer files
|
|
170
|
+
// consumer-tagged friction at the project's own repo (not the framework
|
|
171
|
+
// mirror), and disables the consumer pane loudly when unresolved.
|
|
172
|
+
const { frameworkRepo, consumerRepo } = resolveRetroRepos(config);
|
|
173
|
+
|
|
128
174
|
const result = await runRetroFn({
|
|
129
175
|
epicId,
|
|
130
176
|
provider,
|
|
131
177
|
bus,
|
|
132
178
|
forceFull: fullRetro,
|
|
179
|
+
frameworkRepo,
|
|
180
|
+
consumerRepo,
|
|
181
|
+
// Story #4418 — thread the resolved config so the retro auto-filer can
|
|
182
|
+
// read the `delivery.feedbackLoop.retroProposals` toggle.
|
|
183
|
+
config,
|
|
133
184
|
logger,
|
|
134
185
|
});
|
|
135
186
|
|
|
@@ -161,7 +161,7 @@ function formatDuration(ms) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
function describeEvent(evt) {
|
|
164
|
-
const ts = evt?.ts ??
|
|
164
|
+
const ts = evt?.ts ?? '(no ts)';
|
|
165
165
|
const kind = evt?.kind ?? '(no kind)';
|
|
166
166
|
const phase = evt?.phase ? ` phase=${evt.phase}` : '';
|
|
167
167
|
const category = evt?.category ? ` category=${evt.category}` : '';
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
* the post-merge confirmation step,
|
|
24
24
|
* `single-story-confirm-merge.js`)
|
|
25
25
|
* 8. worktree-reap — drop the per-Story worktree
|
|
26
|
+
* 9. confirm-merge — Story #4428, headless-only (`--wait-merge`):
|
|
27
|
+
* poll the just-armed PR to merge confirmation
|
|
28
|
+
* (reusing `confirmStoryMerged`) or terminate
|
|
29
|
+
* `agent::blocked` with a classified
|
|
30
|
+
* `merge.unlanded` lifecycle event. Attended runs
|
|
31
|
+
* (the default, no `--wait-merge`) skip this
|
|
32
|
+
* phase entirely and keep resting at
|
|
33
|
+
* `agent::closing`, exactly as before.
|
|
26
34
|
*
|
|
27
35
|
* Existing tests import the re-exported helpers
|
|
28
36
|
* (`runSingleStoryClose`, `ensurePullRequest`, `parsePrNumber`,
|
|
@@ -33,8 +41,19 @@
|
|
|
33
41
|
* node single-story-close.js --story <STORY_ID> [--cwd <main-repo>]
|
|
34
42
|
* [--skip-validation] [--skip-sync]
|
|
35
43
|
* [--no-auto-merge] [--no-full-scope-crap]
|
|
44
|
+
* [--wait-merge | --no-wait-merge]
|
|
36
45
|
*
|
|
37
|
-
*
|
|
46
|
+
* `--wait-merge` is the headless must-land signal (Story #4428, Epic
|
|
47
|
+
* #4425): the invoking surface (a headless `/single-story-deliver` run, a
|
|
48
|
+
* CI-driven wrapper, or `/deliver`'s standalone multi-Story fan-out) opts
|
|
49
|
+
* in explicitly — attended runs never pass it, so the default exit shape
|
|
50
|
+
* (rest at `agent::closing`, issue OPEN) is unchanged. `--no-wait-merge` is
|
|
51
|
+
* the explicit opt-out that always wins over `--wait-merge`, for a caller
|
|
52
|
+
* that wants to manage merge confirmation externally even in an otherwise
|
|
53
|
+
* headless context.
|
|
54
|
+
*
|
|
55
|
+
* Exit codes: 0 ok, 1 error (including a headless `--wait-merge` run that
|
|
56
|
+
* gave up without a confirmed merge — see phase 9 above).
|
|
38
57
|
*
|
|
39
58
|
* @see .agents/workflows/helpers/single-story-deliver.md
|
|
40
59
|
*/
|