mandrel 2.65.0 → 2.66.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 (56) hide show
  1. package/.agents/agents/acceptance-critic.md +5 -5
  2. package/.agents/agents/auditor.md +17 -18
  3. package/.agents/agents/plan-critic.md +5 -5
  4. package/.agents/agents/story-worker.md +5 -5
  5. package/.agents/docs/execution-reference.md +27 -5
  6. package/.agents/instructions.md +10 -12
  7. package/.agents/rules/ci-remediation.md +3 -3
  8. package/.agents/rules/gherkin-standards.md +3 -2
  9. package/.agents/rules/git-conventions-reference.md +12 -3
  10. package/.agents/rules/git-conventions.md +9 -7
  11. package/.agents/rules/testing-standards.md +8 -7
  12. package/.agents/runtime-deps.json +1 -1
  13. package/.agents/scripts/bootstrap.js +94 -89
  14. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  15. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
  16. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  17. package/.agents/scripts/lib/cli-args.js +26 -0
  18. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  19. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  20. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  21. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  22. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  23. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  24. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  25. package/.agents/scripts/lib/orchestration/code-review.js +22 -0
  26. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  27. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
  28. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
  30. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  32. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  33. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
  34. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  35. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  36. package/.agents/scripts/lib/transpile.js +28 -3
  37. package/.agents/scripts/single-story-close.js +10 -2
  38. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  39. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  40. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  41. package/.agents/workflows/audit-architecture.md +5 -4
  42. package/.agents/workflows/audit-documentation.md +5 -5
  43. package/.agents/workflows/audit-performance.md +10 -10
  44. package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
  45. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  46. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  47. package/.agents/workflows/helpers/deliver-reference.md +3 -1
  48. package/.agents/workflows/helpers/deliver-story.md +6 -1
  49. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  50. package/.agents/workflows/mandrel-deliver.md +1 -1
  51. package/.agents/workflows/mandrel-plan.md +6 -5
  52. package/docs/CHANGELOG.md +26 -0
  53. package/lib/cli/guarded-sync.js +87 -0
  54. package/lib/cli/sync-agents.js +9 -92
  55. package/lib/cli/sync-commands.js +9 -101
  56. package/package.json +2 -2
@@ -0,0 +1,205 @@
1
+ /**
2
+ * One finding's walk through the graduator, as route → dedup → file steps.
3
+ * Every step receives the gh context as the single `gh` value.
4
+ */
5
+
6
+ import { routeOwnership } from '../github/framework-repo.js';
7
+ import {
8
+ createFollowUpIssue,
9
+ ensureIssueLabels,
10
+ findExistingFollowUp,
11
+ probePathStatus,
12
+ searchFollowUpByMarker,
13
+ updateFollowUpIssue,
14
+ } from './graduator-gh.js';
15
+
16
+ /** @returns {Promise<string|null>} A skip reason, or `null` to continue. */
17
+ export async function probeFindingPath({ finding, gitRef, gh }) {
18
+ // A path-less finding would misclassify as `file-removed`; skip the probe.
19
+ if (typeof finding.path !== 'string' || finding.path.trim().length === 0) {
20
+ return null;
21
+ }
22
+ const { exists, probeError } = await probePathStatus({
23
+ ref: gitRef,
24
+ path: finding.path,
25
+ ...gh,
26
+ });
27
+ if (probeError) return 'probe-error';
28
+ return exists ? null : 'file-removed';
29
+ }
30
+
31
+ /**
32
+ * Unroutable findings are deferred and named, never re-pointed at the
33
+ * consumer's repo.
34
+ *
35
+ * @returns {{ source: string, routedRepo: object|null, deferred: object|null, skipReason: string|null }}
36
+ */
37
+ export function routeFinding({
38
+ finding,
39
+ classifier,
40
+ repos,
41
+ currentRepo,
42
+ logger,
43
+ spec,
44
+ }) {
45
+ const source = classifier(finding.path, null);
46
+ const routing = routeOwnership({ bucket: source, repos, currentRepo });
47
+ if (!routing.routable) {
48
+ const logLine = `[${spec.fnName}] unroutable ${source} finding (${routing.missingKey} is unset) — not filed: ${finding.title ?? finding.path ?? `finding ${finding.index}`}`;
49
+ logger?.warn?.(logLine);
50
+ const deferred = {
51
+ finding,
52
+ routedRepo: null,
53
+ source,
54
+ logLine,
55
+ missingKey: routing.missingKey,
56
+ };
57
+ return { source, routedRepo: null, deferred, skipReason: 'unroutable' };
58
+ }
59
+ const routedRepo = routing.routedRepo;
60
+ if (routing.crossRepo) {
61
+ const logLine = spec.buildCrossRepoLog({ finding, routedRepo, source });
62
+ logger?.info?.(logLine);
63
+ const deferred = { finding, routedRepo, source, logLine };
64
+ return { source, routedRepo, deferred, skipReason: 'cross-repo-deferred' };
65
+ }
66
+ return { source, routedRepo, deferred: null, skipReason: null };
67
+ }
68
+
69
+ /**
70
+ * Checks the content-hash marker, then the legacy ordinal marker so
71
+ * pre-fingerprint filings are not re-filed.
72
+ *
73
+ * @returns {Promise<{ number: number|null, state: string, url: string }|null>}
74
+ */
75
+ export async function dedupFinding({
76
+ finding,
77
+ epicId,
78
+ routedRepo,
79
+ contentMarker,
80
+ gh,
81
+ spec,
82
+ }) {
83
+ const probe = (marker) =>
84
+ searchFollowUpByMarker({
85
+ marker,
86
+ owner: routedRepo.owner,
87
+ repo: routedRepo.repo,
88
+ ...gh,
89
+ });
90
+ const hit = await probe(contentMarker);
91
+ if (hit) return hit;
92
+ const legacyMarker =
93
+ typeof spec.buildLegacyMarker === 'function'
94
+ ? spec.buildLegacyMarker(epicId, finding.index)
95
+ : null;
96
+ return legacyMarker ? probe(legacyMarker) : null;
97
+ }
98
+
99
+ /**
100
+ * An open, numbered match is refreshed and recorded on `filed` as
101
+ * `action: 'updated'` (a live write, counted like a creation). Anything else
102
+ * is `already-filed` and untouched: a closed follow-up is a human's decision.
103
+ */
104
+ export async function recordRecurrence(walk, existing, body) {
105
+ const { finding, source, routedRepo, envelope, decorate, skip, gh } = walk;
106
+ walk.filedMarkers?.add(walk.contentMarker);
107
+ if (existing.state !== 'open' || existing.number === null) {
108
+ skip('already-filed');
109
+ return;
110
+ }
111
+ const updated = await updateFollowUpIssue({
112
+ owner: routedRepo.owner,
113
+ repo: routedRepo.repo,
114
+ number: existing.number,
115
+ body,
116
+ ...gh,
117
+ });
118
+ if (updated.error) {
119
+ envelope.errors.push(
120
+ `finding ${finding.index} (${finding.path}): ${updated.error}`,
121
+ );
122
+ return;
123
+ }
124
+ envelope.filed.push(
125
+ decorate(
126
+ {
127
+ index: finding.index,
128
+ action: 'updated',
129
+ issueNumber: existing.number,
130
+ severity: finding.severity,
131
+ path: finding.path,
132
+ source,
133
+ repo: `${routedRepo.owner}/${routedRepo.repo}`,
134
+ url: updated.url || existing.url || null,
135
+ },
136
+ finding,
137
+ ),
138
+ );
139
+ }
140
+
141
+ function strongReadExisting(walk, labels) {
142
+ const { epicId, finding, contentMarker, routedRepo, spec } = walk;
143
+ return findExistingFollowUp({
144
+ markers:
145
+ typeof spec.buildMatchTokens === 'function'
146
+ ? spec.buildMatchTokens({ epicId, finding, contentMarker })
147
+ : [contentMarker],
148
+ owner: routedRepo.owner,
149
+ repo: routedRepo.repo,
150
+ labels,
151
+ ...walk.gh,
152
+ });
153
+ }
154
+
155
+ /** Cap → label mint → strong-read confirm → create. */
156
+ export async function fileFinding(walk, { title, body, labels }) {
157
+ const { finding, routedRepo, envelope, decorate, skip, gh } = walk;
158
+ if (envelope.filed.length >= walk.maxFilingsPerRun) {
159
+ return skip('cap-reached');
160
+ }
161
+ // Before the strong read too: `gh issue list --label <absent>` exits 0
162
+ // with `[]`, silently defeating the dedup confirm.
163
+ const ensured = await ensureIssueLabels({
164
+ owner: routedRepo.owner,
165
+ repo: routedRepo.repo,
166
+ labels,
167
+ labelCache: walk.labelCache,
168
+ ...gh,
169
+ });
170
+ envelope.errors.push(...ensured.errors);
171
+ if (ensured.missing.length > 0) return skip('label-ensure-failed');
172
+
173
+ const confirmed = await strongReadExisting(walk, labels);
174
+ if (confirmed) return recordRecurrence(walk, confirmed, body);
175
+
176
+ const created = await createFollowUpIssue({
177
+ owner: routedRepo.owner,
178
+ repo: routedRepo.repo,
179
+ title,
180
+ body,
181
+ labels,
182
+ ...gh,
183
+ });
184
+ if (created.error) {
185
+ envelope.errors.push(
186
+ `finding ${finding.index} (${finding.path}): ${created.error}`,
187
+ );
188
+ return;
189
+ }
190
+ walk.filedMarkers?.add(walk.contentMarker);
191
+ envelope.filed.push(
192
+ decorate(
193
+ {
194
+ index: finding.index,
195
+ action: 'created',
196
+ severity: finding.severity,
197
+ path: finding.path,
198
+ source: walk.source,
199
+ repo: `${routedRepo.owner}/${routedRepo.repo}`,
200
+ url: created.url,
201
+ },
202
+ finding,
203
+ ),
204
+ );
205
+ }