mandrel 1.87.0 → 1.89.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 (140) hide show
  1. package/.agents/README.md +18 -13
  2. package/.agents/audit-checklists/architecture.md +24 -0
  3. package/.agents/audit-checklists/clean-code.md +24 -0
  4. package/.agents/audit-checklists/dependencies.md +14 -0
  5. package/.agents/audit-checklists/devops.md +17 -0
  6. package/.agents/audit-checklists/documentation.md +22 -0
  7. package/.agents/audit-checklists/lighthouse.md +15 -0
  8. package/.agents/audit-checklists/navigability.md +14 -0
  9. package/.agents/audit-checklists/performance.md +22 -0
  10. package/.agents/audit-checklists/privacy.md +21 -0
  11. package/.agents/audit-checklists/quality.md +18 -0
  12. package/.agents/audit-checklists/security.md +22 -0
  13. package/.agents/audit-checklists/seo.md +16 -0
  14. package/.agents/audit-checklists/sre.md +24 -0
  15. package/.agents/audit-checklists/ux-ui.md +21 -0
  16. package/.agents/docs/SDLC.md +63 -16
  17. package/.agents/docs/configuration.md +5 -3
  18. package/.agents/instructions.md +51 -21
  19. package/.agents/personas/architect.md +10 -7
  20. package/.agents/personas/engineer.md +4 -3
  21. package/.agents/personas/project-manager.md +5 -2
  22. package/.agents/personas/refactorer.md +5 -3
  23. package/.agents/rules/git-conventions.md +77 -0
  24. package/.agents/schemas/agentrc.schema.json +16 -4
  25. package/.agents/schemas/audit-rules.json +16 -2
  26. package/.agents/schemas/audit-rules.schema.json +7 -6
  27. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
  28. package/.agents/schemas/signal-event.schema.json +28 -13
  29. package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
  30. package/.agents/scripts/check-context-budget.js +320 -0
  31. package/.agents/scripts/diagnose-friction.js +4 -4
  32. package/.agents/scripts/epic-audit-prepare.js +30 -2
  33. package/.agents/scripts/epic-audit-recheck.js +46 -13
  34. package/.agents/scripts/epic-deliver-prepare.js +80 -8
  35. package/.agents/scripts/epic-plan-spec.js +4 -8
  36. package/.agents/scripts/generate-lens-checklists.js +180 -0
  37. package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
  38. package/.agents/scripts/lib/audit-suite/findings.js +27 -0
  39. package/.agents/scripts/lib/audit-suite/index.js +9 -0
  40. package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
  41. package/.agents/scripts/lib/audit-suite/selector.js +136 -5
  42. package/.agents/scripts/lib/checks/loop-health.js +340 -0
  43. package/.agents/scripts/lib/cli-args.js +8 -0
  44. package/.agents/scripts/lib/config/explain.js +4 -0
  45. package/.agents/scripts/lib/config/runners.js +21 -2
  46. package/.agents/scripts/lib/config/temp-paths.js +24 -0
  47. package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
  48. package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
  49. package/.agents/scripts/lib/doc-tiers.js +291 -0
  50. package/.agents/scripts/lib/epic-body-sections.js +5 -2
  51. package/.agents/scripts/lib/epic-merge-lock.js +83 -0
  52. package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
  53. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
  54. package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
  55. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
  56. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
  57. package/.agents/scripts/lib/gates/friction.js +15 -5
  58. package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
  59. package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
  60. package/.agents/scripts/lib/observability/signal-validator.js +204 -0
  61. package/.agents/scripts/lib/observability/signals-writer.js +157 -54
  62. package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
  63. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  64. package/.agents/scripts/lib/orchestration/code-review.js +74 -4
  65. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
  66. package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
  67. package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
  68. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
  69. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
  70. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
  71. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
  72. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
  73. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
  74. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
  75. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
  76. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
  77. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
  78. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
  79. package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
  80. package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
  81. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
  82. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
  83. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
  84. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
  85. package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
  86. package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
  87. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
  88. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
  89. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
  90. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
  91. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
  92. package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
  93. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
  94. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
  95. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
  96. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
  97. package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
  98. package/.agents/scripts/lib/planning-corpus.js +306 -0
  99. package/.agents/scripts/lib/signals/detectors/common.js +10 -10
  100. package/.agents/scripts/lib/signals/detectors/index.js +4 -4
  101. package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
  102. package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
  103. package/.agents/scripts/lib/signals/schema.js +56 -81
  104. package/.agents/scripts/lib/signals/span-tree.js +6 -5
  105. package/.agents/scripts/lib/story-plan.js +3 -0
  106. package/.agents/scripts/lib/wave-runner/tick.js +10 -2
  107. package/.agents/scripts/lifecycle-emit.js +39 -8
  108. package/.agents/scripts/providers/github/issues.js +12 -1
  109. package/.agents/scripts/resolve-doc-tiers.js +83 -0
  110. package/.agents/scripts/retro-run.js +51 -0
  111. package/.agents/scripts/signals-view.js +1 -1
  112. package/.agents/scripts/single-story-close.js +20 -1
  113. package/.agents/scripts/standalone-feedback-rollup.js +188 -0
  114. package/.agents/scripts/story-close.js +48 -0
  115. package/.agents/scripts/story-plan.js +51 -12
  116. package/.agents/scripts/validate-docs-freshness.js +69 -15
  117. package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
  118. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
  119. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
  120. package/.agents/skills/core/scope-triage/SKILL.md +61 -0
  121. package/.agents/skills/skills.index.json +3 -3
  122. package/.agents/workflows/audit-documentation.md +82 -2
  123. package/.agents/workflows/helpers/code-review.md +193 -44
  124. package/.agents/workflows/helpers/deliver-epic.md +128 -39
  125. package/.agents/workflows/helpers/deliver-stories.md +26 -0
  126. package/.agents/workflows/helpers/epic-audit.md +116 -283
  127. package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
  128. package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
  129. package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
  130. package/.agents/workflows/helpers/plan-epic.md +141 -105
  131. package/.agents/workflows/helpers/plan-story.md +32 -0
  132. package/.agents/workflows/helpers/single-story-deliver.md +43 -0
  133. package/.agents/workflows/loops/nightly-audit.md +9 -7
  134. package/docs/CHANGELOG.md +29 -0
  135. package/lib/cli/doctor.js +44 -0
  136. package/package.json +4 -3
  137. package/.agents/scripts/epic-plan-spec-validate.js +0 -111
  138. package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
  139. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
  140. package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * epic-plan-spec-validate.js — Phase 7.5 Tech Spec post-authoring gate CLI.
5
- *
6
- * `/plan` Phase 7 authors the Tech Spec; Phase 8.3 (Holistic
7
- * Consolidation) reconciles the draft ticket array against the Tech Spec's
8
- * `## Delivery Slicing` section, which the decompose-author skill uses as the
9
- * capability-boundary anchor. When that section is absent the consolidation
10
- * pass runs against a void and produces groupings that reflect technical
11
- * shape rather than capability boundaries.
12
- *
13
- * This CLI is the hard gate between Phase 7 and Phase 8: it reads the authored
14
- * `techspec.md`, runs {@link ./lib/orchestration/spec-section-validator.js#validateSpecSections},
15
- * and exits non-zero when the required section is missing so decomposition
16
- * cannot proceed against an un-anchored spec. It is the Phase 8-side
17
- * counterpart to the Phase 6 Epic Clarity Gate (`epic-plan-clarity.js`) —
18
- * same detect-then-prompt pattern, one phase later.
19
- *
20
- * Usage:
21
- * epic-plan-spec-validate.js --techspec <path> [--json]
22
- *
23
- * Exit codes:
24
- * 0 — every required section present.
25
- * 1 — at least one required section missing, or a fatal error (bad path,
26
- * unreadable file). The failure message names the missing section(s)
27
- * and tells the operator how to recover.
28
- */
29
-
30
- import { readFile } from 'node:fs/promises';
31
- import { parseArgs } from 'node:util';
32
- import { runAsCli } from './lib/cli-utils.js';
33
- import { Logger } from './lib/Logger.js';
34
- import { validateSpecSections } from './lib/orchestration/spec-section-validator.js';
35
-
36
- /**
37
- * Build the operator-facing failure message for a missing-section result.
38
- * Names each missing section and tells the operator whether to re-author the
39
- * spec or add the section by hand before continuing to Phase 8.
40
- *
41
- * @param {{ techspecPath: string, missing: string[] }} args
42
- * @returns {string}
43
- */
44
- export function formatMissingSectionMessage({ techspecPath, missing }) {
45
- const list = missing.map((name) => `## ${name}`).join(', ');
46
- return [
47
- `[epic-plan-spec-validate] Tech Spec is missing required section(s): ${list}`,
48
- ` Spec file: ${techspecPath}`,
49
- '',
50
- ` Phase 8 (decomposition) reconciles the draft ticket array against the`,
51
- ` Tech Spec's "## Delivery Slicing" section — without it, the Phase 8.3`,
52
- ` consolidation pass has no capability-boundary anchor and groups by`,
53
- ` technical shape instead.`,
54
- '',
55
- ' To continue, do ONE of the following before re-running Phase 8:',
56
- ` 1. Re-author the Tech Spec (re-run the Phase 7 spec-author step) so it`,
57
- ` emits a "## Delivery Slicing" section, OR`,
58
- ` 2. Add a "## Delivery Slicing" section to the Tech Spec by hand,`,
59
- ` describing the capability boundaries the work should be sliced along.`,
60
- ].join('\n');
61
- }
62
-
63
- /**
64
- * Validate an authored Tech Spec file for the required post-authoring
65
- * sections. Pure-ish wrapper around `validateSpecSections` that owns the file
66
- * read so the CLI `main` stays a thin arg-parse shell.
67
- *
68
- * @param {{ techspecPath: string }} args
69
- * @returns {Promise<{ ok: boolean, missing: string[], present: string[] }>}
70
- */
71
- export async function validateSpecFile({ techspecPath }) {
72
- const body = await readFile(techspecPath, 'utf8');
73
- return validateSpecSections({ body });
74
- }
75
-
76
- /* node:coverage ignore next */
77
- async function main() {
78
- const { values } = parseArgs({
79
- options: {
80
- techspec: { type: 'string' },
81
- json: { type: 'boolean', default: false },
82
- },
83
- });
84
-
85
- if (!values.techspec) {
86
- throw new Error(
87
- 'Usage: epic-plan-spec-validate.js --techspec <path> [--json]',
88
- );
89
- }
90
-
91
- const techspecPath = values.techspec;
92
- const result = await validateSpecFile({ techspecPath });
93
-
94
- if (values.json) {
95
- process.stdout.write(`${JSON.stringify({ techspecPath, ...result })}\n`);
96
- }
97
-
98
- if (!result.ok) {
99
- throw new Error(
100
- formatMissingSectionMessage({ techspecPath, missing: result.missing }),
101
- );
102
- }
103
-
104
- Logger.info(
105
- `[epic-plan-spec-validate] Tech Spec section gate passed: ${result.present
106
- .map((name) => `## ${name}`)
107
- .join(', ')} present.`,
108
- );
109
- }
110
-
111
- runAsCli(import.meta.url, main, { source: 'epic-plan-spec-validate' });
@@ -1,207 +0,0 @@
1
- /**
2
- * code-review-graduator.js — Auto-graduate non-blocking code-review
3
- * findings from the Epic's `code-review` structured comment into routed
4
- * GitHub follow-up issues.
5
- *
6
- * Story #2555 / Epic #2547. As of Story #3845 / Epic #3823 the spawn
7
- * helper, the path/idempotency probes, the `gh issue create` filer, the
8
- * toggle reader, and the route → probe → file walk all live in the
9
- * shared [`graduator-core.js`](./graduator-core.js). This module is the
10
- * thin code-review-specific shell: it owns the code-review finding parser
11
- * (the 🟢→low severity mapping, no lens), the code-review label / title /
12
- * body shape, and the code-review idempotency marker. Behaviour is
13
- * identical to the pre-consolidation graduator.
14
- *
15
- * - Read the `code-review` structured comment off the Epic ticket via
16
- * the injected provider (findStructuredComment surface).
17
- * - For each non-blocking finding (severity high/medium/low — i.e.
18
- * anything that is NOT a 🔴 Critical Blocker), check that the cited
19
- * file still exists in the merged tree (`git cat-file -e <ref>:<path>`)
20
- * via the injected spawn seam.
21
- * - Route by source classification (framework vs consumer) using
22
- * `classifyPathSource` (S1 helper). When the routed repo differs
23
- * from the current repo, record under `skipped: 'cross-repo-deferred'`
24
- * and log the would-be `gh issue create` invocation — do NOT actually
25
- * shell out against a different repo.
26
- * - File a follow-up issue with `gh issue create --repo <routed-repo>`
27
- * carrying a `code-review::<severity>` label plus the matching
28
- * `meta::<framework-gap|consumer-improvement>` label.
29
- * - Embed an idempotency marker in each body:
30
- * <!-- code-review-followup: epic-<id>-finding-<idx> -->
31
- * Before filing, probe via `gh search issues "<marker>" --repo …`
32
- * and skip findings whose marker is already present in any issue.
33
- * - Short-circuit when `config.delivery.feedbackLoop.codeReviewAutoFile`
34
- * is `false` — return `{ filed: [], skipped: [{reason:
35
- * 'toggle-disabled'}], errors: [] }`.
36
- * - NEVER throw. Every failure path (missing comment, parse failure,
37
- * gh/git spawn error, non-zero exit) is captured in `errors[]`.
38
- *
39
- * Tests inject `provider`, `classifier`, and `spawnImpl` to drive every
40
- * branch deterministically.
41
- */
42
-
43
- import {
44
- graduate,
45
- makeIsAutoFileEnabled,
46
- probePathExists,
47
- } from './graduator-core.js';
48
-
49
- /**
50
- * Resolve the toggle from the resolved agentrc config. Defaults to `true`
51
- * — the feature is opt-out, not opt-in.
52
- *
53
- * @param {object|undefined|null} config
54
- * @returns {boolean}
55
- */
56
- export const isAutoFileEnabled = makeIsAutoFileEnabled('codeReviewAutoFile');
57
-
58
- // Re-export the shared path probe so existing importers keep working.
59
- export { probePathExists };
60
-
61
- /**
62
- * Severity → label mapping. Only non-blocking severities have a route;
63
- * 🔴 Critical Blocker is explicitly filtered out upstream.
64
- */
65
- const SEVERITY_LABEL = Object.freeze({
66
- high: 'code-review::high',
67
- medium: 'code-review::medium',
68
- low: 'code-review::low',
69
- });
70
-
71
- /**
72
- * Map a classification source to its meta routing label.
73
- *
74
- * @param {string} source
75
- * @returns {string}
76
- */
77
- function metaSourceLabel(source) {
78
- return source === 'framework'
79
- ? 'meta::framework-gap'
80
- : 'meta::consumer-improvement';
81
- }
82
-
83
- /**
84
- * Compile a marker for a given epicId / finding index. The marker is an
85
- * HTML comment so it survives GitHub markdown rendering without leaking
86
- * into the visible body — but it's still indexable via `gh search`.
87
- *
88
- * @param {number} epicId
89
- * @param {number} index — zero-based finding ordinal within the Epic.
90
- * @returns {string}
91
- */
92
- export function buildIdempotencyMarker(epicId, index) {
93
- return `<!-- code-review-followup: epic-${epicId}-finding-${index} -->`;
94
- }
95
-
96
- /**
97
- * Parse the rendered code-review markdown into a list of findings. Each
98
- * finding has `{ severity, path, summary, index }`. Pure. Exported so
99
- * the parser can be unit-tested in isolation.
100
- *
101
- * The structured `code-review` comment emits findings as bullet lines under the
102
- * "🚨 Critical Findings" and "🟡 Warnings" sections. Each line begins
103
- * with a severity emoji and embeds the cited path inside backticks. We
104
- * filter 🔴 (Critical Blocker — blocking) out; 🟠/🟡/🟢 are non-blocking
105
- * and graduate to follow-up issues.
106
- *
107
- * @param {string} body
108
- * @returns {Array<{ severity: 'high'|'medium'|'low', path: string, summary: string, index: number }>}
109
- */
110
- export function parseFindings(body) {
111
- if (typeof body !== 'string' || body.length === 0) return [];
112
- const findings = [];
113
- const lines = body.split(/\r?\n/);
114
- let idx = 0;
115
- for (const line of lines) {
116
- const trimmed = line.trim();
117
- if (trimmed.length === 0) continue;
118
- let severity = null;
119
- if (trimmed.startsWith('🟠')) severity = 'high';
120
- else if (trimmed.startsWith('🟡')) severity = 'medium';
121
- else if (trimmed.startsWith('🟢')) severity = 'low';
122
- else continue;
123
- // Path is the first backticked token on the line.
124
- const pathMatch = trimmed.match(/`([^`]+)`/);
125
- if (!pathMatch) continue;
126
- const path = pathMatch[1];
127
- // Summary is the line itself, stripped of the leading emoji bullet.
128
- findings.push({
129
- severity,
130
- path,
131
- summary: trimmed,
132
- index: idx,
133
- });
134
- idx += 1;
135
- }
136
- return findings;
137
- }
138
-
139
- /**
140
- * Auto-graduate non-blocking code-review findings into routed follow-up
141
- * issues. Never throws. Thin wrapper around the shared `graduate()` walk
142
- * with the code-review-specific behaviour bundle.
143
- *
144
- * @param {object} opts
145
- * @param {number} opts.epicId
146
- * @param {object} opts.provider — ticketing provider exposing
147
- * `getTicketComments(ticketId)`.
148
- * @param {object} [opts.config] — resolved agentrc.
149
- * @param {{owner: string, repo: string}} opts.currentRepo — the repo the
150
- * listener is running inside; used for the cross-repo guard.
151
- * @param {{owner: string, repo: string}} [opts.frameworkRepo] — where
152
- * framework-tagged findings get routed. Defaults to
153
- * `currentRepo` when this is the framework repo, otherwise typically
154
- * `{ owner: 'dsj1984', repo: 'mandrel' }`.
155
- * @param {string} [opts.gitRef='HEAD'] — ref against which to probe path
156
- * existence.
157
- * @param {Function} [opts.classifier=classifyPathSource] — S1 helper.
158
- * @param {string} [opts.ghPath='gh']
159
- * @param {Function} [opts.spawnImpl]
160
- * @param {string} [opts.cwd]
161
- * @param {{info?: Function, warn?: Function, debug?: Function}} [opts.logger]
162
- * @returns {Promise<{
163
- * filed: Array<{ index: number, severity: string, path: string, source: string, repo: string, url: string|null }>,
164
- * skipped: Array<{ index?: number, reason: string, path?: string, severity?: string }>,
165
- * errors: string[],
166
- * }>}
167
- */
168
- export async function graduateFindings(opts = {}) {
169
- return graduate({
170
- ...opts,
171
- spec: {
172
- fnName: 'graduateFindings',
173
- isAutoFileEnabled,
174
- commentMarker: '<!-- structured-comment: code-review -->',
175
- noCommentReason: 'no-code-review-comment',
176
- parseFindings,
177
- buildIdempotencyMarker,
178
- buildCrossRepoLog: ({ finding, routedRepo, source }) => {
179
- const metaLabel = metaSourceLabel(source);
180
- return `[code-review-graduator] cross-repo skip (would file in ${routedRepo.owner}/${routedRepo.repo}): gh issue create --repo ${routedRepo.owner}/${routedRepo.repo} --title "Code review follow-up: ${finding.path}" --label "${metaLabel},${SEVERITY_LABEL[finding.severity]}"`;
181
- },
182
- buildFollowUp: ({ finding, source, epicId, idMarker }) => {
183
- const metaLabel = metaSourceLabel(source);
184
- const title = `Code review follow-up: ${finding.path}`;
185
- const body = [
186
- idMarker,
187
- '',
188
- `Auto-filed from the Epic #${epicId} code-review pass.`,
189
- '',
190
- `**Severity**: ${finding.severity}`,
191
- `**Source**: ${source}`,
192
- `**Path**: \`${finding.path}\``,
193
- '',
194
- '### Finding',
195
- '',
196
- finding.summary,
197
- '',
198
- `_See Epic #${epicId} for the full code-review report._`,
199
- ].join('\n');
200
- const labels = [metaLabel, SEVERITY_LABEL[finding.severity]];
201
- return { title, body, labels };
202
- },
203
- },
204
- });
205
- }
206
-
207
- export default graduateFindings;
@@ -1,58 +0,0 @@
1
- /**
2
- * phases/prompts.js — Canonical Tech Spec / Acceptance Spec system prompts for
3
- * the spec phase of `/plan`.
4
- *
5
- * These ride along on the `--emit-context` envelope as a backstop. The
6
- * `epic-plan-spec-author` Skill
7
- * (`.agents/skills/core/epic-plan-spec-author/SKILL.md`) embeds the
8
- * authoritative copies of these strings — keep the two surfaces in sync when
9
- * either is edited.
10
- *
11
- * Story #4314: the PRD artifact class is retired. The Epic body (which now
12
- * carries its `## User Stories` section inline) is the sole authoring input;
13
- * both prompts consume the Epic body directly rather than a paraphrased PRD.
14
- *
15
- * Story #4324: the Tech Spec and Acceptance Spec are no longer separate
16
- * `context::*` tickets — the authored content lands as managed sections of
17
- * the same Epic body (`## Delivery Slicing`-led Tech Spec sections, and the
18
- * `## Acceptance Table` AC-ID table). Content semantics are unchanged; only
19
- * WHERE the output lives moved.
20
- */
21
-
22
- export const TECH_SPEC_SYSTEM_PROMPT = `You are an expert Engineering Architect.
23
- Your job is to convert an Epic into a Technical Specification for implementation.
24
-
25
- The Tech Spec should outline:
26
- 1. Delivery Slicing — propose how the Epic's enumerated capabilities cluster into shippable Stories. This count is a CEILING, not a target: the Phase 8 consolidation pass may merge below your proposed count when slices form dependent single-consumer chains, but never splits above it. Do NOT coarsen the Epic enumeration to produce this; the grouping recommendation is the granularity lever.
27
- 2. Architecture & Design
28
- 3. Data Models (if any)
29
- 4. API Changes (if any)
30
- 5. Core Components
31
- 6. Security & Privacy Considerations
32
-
33
- CRITICAL REQUIREMENTS:
34
- - Respond ONLY with valid Markdown.
35
- - Do not use top-level <h1> (# ) tags. Open the document with the \`## Delivery Slicing\` section — it is the primary input to Phase 8 consolidation, so author it first and hang the rest of the spec off it.
36
- - Do NOT restate the Epic's Context, Goal, or Scope — your output lands as sections of the same Epic body, which travels into every downstream story agent's prompt, so any restatement is pure duplication and a drift risk. If a brief technical orientation is genuinely useful, add an optional \`## Technical Overview\` of no more than 2–3 sentences that names the *technical approach* only (which subsystems are touched and reused); never re-narrate the problem statement, goals, or scope.
37
- - Format architectural decisions clearly with bullet points.
38
- - Author the \`## Delivery Slicing\` section as a markdown table with columns \`Slice | What ships | Independent?\`, using noun-phrase slice names (e.g. "Foundation", "Transport seam", "Send helper") that map onto Feature titles. "Independent?" answers: can this slice ship to production and provide value without the next slice landing? A slice you mark "Independent? No" MUST carry a one-line justification (parallelism, risk isolation, or delivery-envelope pressure); an unjustified dependent single-consumer slice folds into its consumer by default rather than shipping as its own Story.`;
39
-
40
- export const ACCEPTANCE_SPEC_SYSTEM_PROMPT = `You are an expert Acceptance Engineer.
41
- Your job is to convert an Epic and a Tech Spec into a structured Acceptance Specification that drives features-first BDD authoring.
42
-
43
- The Acceptance Spec should outline:
44
- 1. Acceptance Table — one row per user-visible outcome, expressed as a Markdown table with columns: AC ID | Outcome | Feature File | Scenario | Disposition
45
- 2. Stable AC IDs — assign AC-1, AC-2, ... in document order; reuse the same ID across re-plans when an Outcome is materially unchanged so scenario tags (@ac-N) stay aligned
46
- 3. Disposition — tag each row with one of: new | updated | unchanged
47
-
48
- The Epic body's \`## Acceptance Criteria\` bullets are the single source of truth for what the spec verifies. Your table does not re-invent criteria — it anchors each one to a specific Epic AC bullet.
49
-
50
- CRITICAL REQUIREMENTS:
51
- - Respond ONLY with valid Markdown.
52
- - Do not use top-level <h1> (# ) tags. Start with ## Acceptance Table — the table lands as a section of the Epic body, so it must NOT reuse the Epic's own ## Acceptance Criteria heading.
53
- - Every AC row MUST have a stable AC ID of the form AC-<n> (AC-1, AC-2, ...) — do not reorder IDs across re-plans; new ACs get fresh sequential IDs.
54
- - Every AC row MUST carry a Disposition value from the enum: new | updated | unchanged. (At Epic close, the acceptance reconciler overwrites Disposition with the verification outcome — satisfied | pending | missing — inside this section only; on re-plan, reset each row to the authoring enum.)
55
- - Each Outcome MUST be a **terse restatement keyed to a specific Epic \`## Acceptance Criteria\` bullet** — lead the Outcome with the bullet's anchor (its quoted lead phrase or an explicit "Epic AC N" index) and keep the rest to a single user-visible behaviour. Do NOT re-elaborate the Epic bullet in independent words: a free-standing Outcome that paraphrases the criterion without naming the bullet it verifies is forbidden, because it drifts from the Epic silently. No DB assertions, no HTTP status codes, no internal implementation details.
56
- - Where one Epic AC bullet genuinely expands into several user-visible outcomes, emit one row per outcome and declare the split on each — e.g. lead with "splits Epic AC 3" — so the fan-out is explicit rather than hidden.
57
- - Anchor coverage MUST be complete and auditable: every Epic AC bullet MUST be covered by at least one row, and every row MUST anchor to an Epic AC bullet. Flag divergence in the authored spec instead of dropping it — if an Epic AC bullet has no corresponding row, or a row has no Epic anchor, call it out explicitly (a note beneath the table) rather than silently omitting the bullet or emitting an unanchored row.
58
- - Cite proposed feature file paths under tests/features/** so Phase 8 can scaffold matching scenarios.`;
@@ -1,292 +0,0 @@
1
- /**
2
- * Hotspot detector — pure module (Epic #1721 / Story #1769 / Task #1776).
3
- *
4
- * Walks every Story directory under `temp/epic-<eid>/`, aggregates edit
5
- * counts per `details.targetHash` for file-mutating tools across Stories,
6
- * and emits one `kind: 'hotspot'` SignalEvent per hash whose total edit
7
- * count exceeds `p95 * multiplier`. Pure: takes config-shaped args in,
8
- * returns events out. Caller persists via `appendEpicSignal` (the signal
9
- * is Epic-scope; there is no single owning Story).
10
- *
11
- * ## Counting rule
12
- *
13
- * Only file-mutating tools are counted:
14
- * - `Edit`
15
- * - `Write`
16
- * - `MultiEdit`
17
- * - `NotebookEdit`
18
- *
19
- * Every other tool (Read, Bash, Grep, Glob, …) is ignored. Trace records
20
- * without a `details.targetHash` are also skipped — without a stable key
21
- * we cannot group repeats.
22
- *
23
- * ## Cross-Story-only percentile pool
24
- *
25
- * Hashes that appear in **fewer than 2 Stories** are excluded from the
26
- * p95 calculation. This prevents a single large Story from manufacturing
27
- * its own hotspot when no other Story touches the same file. The pool
28
- * for the percentile is therefore the set of `totalEdits` values for
29
- * hashes seen in ≥ 2 Stories. Hashes outside that pool also do not
30
- * emit a signal — by construction they are not cross-Story.
31
- *
32
- * ## p95 algorithm
33
- *
34
- * Nearest-rank method (no interpolation): for a sorted array of length
35
- * `n`, the p95 index is `ceil(0.95 * n) - 1` (0-based). For
36
- * `[1,2,3,4,5,6,7,8,9,10]` the index is `ceil(9.5) - 1 = 9` → value `10`.
37
- * For a single-element array `[10]` the index is `ceil(0.95) - 1 = 0` →
38
- * value `10`. Nearest-rank keeps fixture-driven tests deterministic
39
- * across Node versions and avoids the floating-point ambiguity of
40
- * linear-interpolation variants.
41
- *
42
- * ## Privacy contract
43
- *
44
- * Trace records key off `details.targetHash` (a sha256 of the file path,
45
- * see `lib/observability/tool-trace-hook.js`). The detector groups by
46
- * the hash, never the raw path, so the privacy boundary established by
47
- * the hook is preserved end-to-end.
48
- *
49
- * ## Threshold semantics
50
- *
51
- * `totalEdits > p95 * multiplier` (strictly greater than). A hash with
52
- * exactly `p95 * multiplier` edits does NOT emit; only counts past the
53
- * threshold trip the detector. The multiplier comes from
54
- * `delivery.signals.hotspot.p95Multiplier` (default 1.25) — pass it in
55
- * via `args.multiplier`. The detector itself does not import the config
56
- * resolver.
57
- *
58
- * ## Robustness
59
- *
60
- * - Missing `temp/epic-<eid>/` → returns `[]`. Never throws.
61
- * - Story directories with no `traces.ndjson` → contribute zero edits.
62
- * - Malformed JSON lines → silently skipped (consistent with
63
- * `lib/signals/read.js` and `detectors/rework.js`).
64
- * - Non-trace records → ignored (the file may legitimately interleave
65
- * other kinds in future).
66
- *
67
- * @module lib/signals/detectors/hotspot
68
- */
69
-
70
- import { createReadStream } from 'node:fs';
71
- import fs from 'node:fs/promises';
72
- import path from 'node:path';
73
- import { createInterface } from 'node:readline';
74
- import { epicTempDir } from '../../config/temp-paths.js';
75
- import { parseStoryBranch } from '../../git-utils.js';
76
- import { extractTool, validateDetectorArgs } from './common.js';
77
-
78
- /**
79
- * Tools that mutate files. Only these contribute to the per-target edit
80
- * count. Anything outside this set is ignored.
81
- *
82
- * @type {ReadonlySet<string>}
83
- */
84
- const FILE_MUTATING_TOOLS = Object.freeze(
85
- new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']),
86
- );
87
-
88
- function isPositiveNumber(v) {
89
- return typeof v === 'number' && Number.isFinite(v) && v > 0;
90
- }
91
-
92
- /**
93
- * Stream a single `traces.ndjson` line-by-line and emit per-targetHash
94
- * edit counts as a `Map<targetHash, count>`. Missing file → empty map.
95
- *
96
- * @param {string} tracesPath
97
- * @returns {Promise<Map<string, number>>}
98
- */
99
- async function tallyEditsByTarget(tracesPath) {
100
- const counts = new Map();
101
-
102
- // Existence check before opening the stream — `createReadStream`
103
- // defers ENOENT until the first read, which leaves the iterator in a
104
- // bad state on some Node versions.
105
- try {
106
- await fs.access(tracesPath);
107
- } catch {
108
- return counts;
109
- }
110
-
111
- const stream = createReadStream(tracesPath, { encoding: 'utf8' });
112
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
113
-
114
- try {
115
- for await (const rawLine of rl) {
116
- if (rawLine.length === 0) continue;
117
- let parsed;
118
- try {
119
- parsed = JSON.parse(rawLine);
120
- } catch {
121
- continue;
122
- }
123
- if (parsed == null || typeof parsed !== 'object') continue;
124
- if (parsed.kind !== 'trace') continue;
125
-
126
- const tool = extractTool(parsed);
127
- if (tool == null || !FILE_MUTATING_TOOLS.has(tool)) continue;
128
-
129
- const hash = parsed.details?.targetHash;
130
- if (typeof hash !== 'string' || hash.length === 0) continue;
131
-
132
- counts.set(hash, (counts.get(hash) ?? 0) + 1);
133
- }
134
- } finally {
135
- rl.close();
136
- if (!stream.destroyed) stream.destroy();
137
- }
138
-
139
- return counts;
140
- }
141
-
142
- /**
143
- * Enumerate `story-<id>` subdirectories of `epicDir`. Returns an empty
144
- * array when `epicDir` does not exist. The ordering is by Story ID
145
- * ascending so `storiesAffected` and any future audit output are stable.
146
- *
147
- * @param {string} epicDir
148
- * @returns {Promise<string[]>} absolute paths to each story directory
149
- */
150
- async function listStoryDirs(epicDir) {
151
- let entries;
152
- try {
153
- entries = await fs.readdir(epicDir, { withFileTypes: true });
154
- } catch {
155
- return [];
156
- }
157
- const stories = [];
158
- for (const ent of entries) {
159
- if (!ent.isDirectory()) continue;
160
- const id = parseStoryBranch(ent.name);
161
- if (id == null) continue;
162
- stories.push({ id, dir: path.join(epicDir, ent.name) });
163
- }
164
- stories.sort((a, b) => a.id - b.id);
165
- return stories.map((s) => s.dir);
166
- }
167
-
168
- /**
169
- * Nearest-rank p95 of a numeric array. Returns `0` for an empty input.
170
- *
171
- * The pool is sorted ascending in-place on a defensive copy and the
172
- * value at index `ceil(0.95 * n) - 1` (0-based) is returned. No
173
- * interpolation — keeps fixture tests deterministic.
174
- *
175
- * @param {readonly number[]} values
176
- * @returns {number}
177
- */
178
- export function nearestRankP95(values) {
179
- if (!Array.isArray(values) || values.length === 0) return 0;
180
- const sorted = [...values].sort((a, b) => a - b);
181
- const idx = Math.ceil(0.95 * sorted.length) - 1;
182
- return sorted[Math.max(0, Math.min(sorted.length - 1, idx))];
183
- }
184
-
185
- /**
186
- * Detect Epic-scope hotspots. Returns one `kind: 'hotspot'` SignalEvent
187
- * per `targetHash` whose cross-Story `totalEdits` strictly exceeds
188
- * `p95(pool) * multiplier`, where `pool` is the set of `totalEdits`
189
- * values for hashes appearing in ≥ 2 Stories.
190
- *
191
- * Pure — emission to disk is the caller's responsibility (use
192
- * `appendEpicSignal` since the signal is Epic-scope).
193
- *
194
- * @param {object} args
195
- * @param {number} args.epicId — positive integer Epic ID.
196
- * @param {number} args.multiplier — positive number; p95 multiplier.
197
- * @param {string} [args.tempRoot] — override `temp/` root (mostly for
198
- * tests that point at a synthetic fixture tree).
199
- * @param {() => string} [args.nowFn] — optional clock seam returning the
200
- * ISO-8601 `ts` stamped onto every emitted SignalEvent. Defaults to
201
- * `() => new Date().toISOString()`. Inject a fixed-return function in
202
- * tests to make the emitted `ts` deterministic. MUST, when provided, be
203
- * a function.
204
- * @returns {Promise<object[]>} array of hotspot SignalEvents conforming
205
- * to `.agents/schemas/signal-event.schema.json`'s envelope. Emitted
206
- * `details` payload: `{ targetHash, totalEdits, storiesAffected,
207
- * p95Threshold, multiplier }`.
208
- */
209
- export async function detectHotspot(args) {
210
- // Hotspot shares only the args-shape, nowFn, and epicId guards with the
211
- // Story-scoped detectors — it has no tracesPath/storyId/taskId/threshold.
212
- // Gate those three off and validate multiplier/tempRoot (hotspot-specific)
213
- // inline below.
214
- const { epicId, nowFn } = validateDetectorArgs(args, {
215
- fnName: 'detectHotspot',
216
- requireTracesPath: false,
217
- requireStoryId: false,
218
- requireThreshold: false,
219
- });
220
- const { multiplier, tempRoot } = args;
221
-
222
- if (!isPositiveNumber(multiplier)) {
223
- throw new RangeError(
224
- `detectHotspot: multiplier must be a positive number (got ${multiplier})`,
225
- );
226
- }
227
- if (
228
- tempRoot != null &&
229
- (typeof tempRoot !== 'string' || tempRoot.length === 0)
230
- ) {
231
- throw new TypeError(
232
- `detectHotspot: tempRoot, when provided, must be a non-empty string (got ${tempRoot})`,
233
- );
234
- }
235
-
236
- // Resolve the Epic temp dir. When the caller supplies an explicit
237
- // `tempRoot`, splice it into the `{ project: { paths: { tempRoot } } }`
238
- // shape that `epicTempDir` understands without forcing a config load.
239
- const epicDir = tempRoot
240
- ? epicTempDir(epicId, { project: { paths: { tempRoot } } })
241
- : epicTempDir(epicId);
242
-
243
- const storyDirs = await listStoryDirs(epicDir);
244
- if (storyDirs.length === 0) return [];
245
-
246
- // Per-Story edit-count maps, then aggregate per-hash totals plus the
247
- // count of distinct Stories each hash was seen in.
248
- const totals = new Map(); // hash -> totalEdits across Stories
249
- const storyHits = new Map(); // hash -> count of distinct Stories
250
-
251
- for (const storyDir of storyDirs) {
252
- const tracesPath = path.join(storyDir, 'traces.ndjson');
253
- const perStory = await tallyEditsByTarget(tracesPath);
254
- for (const [hash, count] of perStory) {
255
- totals.set(hash, (totals.get(hash) ?? 0) + count);
256
- storyHits.set(hash, (storyHits.get(hash) ?? 0) + 1);
257
- }
258
- }
259
-
260
- // Cross-Story-only pool: keep hashes seen in ≥ 2 Stories. Single-Story
261
- // hashes never qualify as hotspots regardless of their absolute count.
262
- const crossStoryHashes = [];
263
- for (const [hash, hits] of storyHits) {
264
- if (hits >= 2) crossStoryHashes.push(hash);
265
- }
266
-
267
- if (crossStoryHashes.length === 0) return [];
268
-
269
- const pool = crossStoryHashes.map((h) => totals.get(h));
270
- const p95 = nearestRankP95(pool);
271
- const threshold = p95 * multiplier;
272
-
273
- // Sort offenders by hash ascending for stable emission order.
274
- const offenders = crossStoryHashes
275
- .filter((h) => totals.get(h) > threshold)
276
- .sort();
277
-
278
- const ts = nowFn();
279
- return offenders.map((targetHash) => ({
280
- ts,
281
- kind: 'hotspot',
282
- source: { tool: 'hotspot-detector' },
283
- epicId,
284
- details: {
285
- targetHash,
286
- totalEdits: totals.get(targetHash),
287
- storiesAffected: storyHits.get(targetHash),
288
- p95Threshold: threshold,
289
- multiplier,
290
- },
291
- }));
292
- }