mandrel 2.65.0 → 2.67.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/agents/acceptance-critic.md +7 -7
- package/.agents/agents/auditor.md +17 -18
- package/.agents/agents/plan-critic.md +5 -5
- package/.agents/agents/story-worker.md +5 -5
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/execution-reference.md +27 -5
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +12 -13
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +3 -2
- package/.agents/rules/git-conventions-reference.md +17 -8
- package/.agents/rules/git-conventions.md +10 -8
- package/.agents/rules/testing-standards.md +8 -7
- package/.agents/runtime-deps.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/scripts/boot-sweep.js +97 -9
- package/.agents/scripts/bootstrap.js +94 -89
- package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
- package/.agents/scripts/clean-temp.js +54 -0
- package/.agents/scripts/clean-worktrees.js +593 -0
- package/.agents/scripts/drain-pending-cleanup.js +5 -4
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
- package/.agents/scripts/lib/clean-temp.js +440 -0
- package/.agents/scripts/lib/cli/standard-args.js +60 -76
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/config/gates/shared.js +3 -3
- package/.agents/scripts/lib/config-settings-schema-delivery.js +11 -2
- package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
- package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
- package/.agents/scripts/lib/observability/signal-validator.js +17 -5
- package/.agents/scripts/lib/observability/source-classifier.js +3 -1
- package/.agents/scripts/lib/orchestration/code-review.js +22 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/single-story-sweep.js +2 -2
- package/.agents/scripts/lib/temp-removal.js +110 -0
- package/.agents/scripts/lib/temp-retention.js +122 -73
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/scripts/single-story-init.js +120 -17
- package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
- package/.agents/workflows/audit-architecture.md +5 -4
- package/.agents/workflows/audit-documentation.md +5 -5
- package/.agents/workflows/audit-performance.md +10 -10
- package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
- package/.agents/workflows/clean-temp.md +67 -0
- package/.agents/workflows/clean-worktrees.md +63 -0
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +11 -10
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +3 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
- package/.agents/workflows/helpers/deliver-story.md +6 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/mandrel-deliver.md +1 -1
- package/.agents/workflows/mandrel-plan.md +6 -5
- package/docs/CHANGELOG.md +39 -0
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- package/package.json +2 -2
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* plan-runner/worktree-sweep.js —
|
|
2
|
+
* plan-runner/worktree-sweep.js — boot sweep that removes
|
|
3
3
|
* `.worktrees/story-<id>/` entries whose Story is closed or `agent::done`,
|
|
4
|
-
* after draining the pending-cleanup manifest.
|
|
4
|
+
* after draining the pending-cleanup manifest. Called by `runBootSweep`
|
|
5
|
+
* (`boot-sweep.js`) under the shared merged-branch sweep lock.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Removal goes through `removeWorktreeWithRecovery` — the same seam close
|
|
8
|
+
* uses: Windows lock retries, an `fs.rm` fallback that discards residue a
|
|
9
|
+
* done Story's merged branch no longer needs, and the pending-cleanup
|
|
10
|
+
* hand-off when that exhausts. Branches are never deleted here; the
|
|
11
|
+
* merged-branch sweep owns them.
|
|
12
|
+
*
|
|
13
|
+
* Two trees are never touched: an open Story's, and the one the running
|
|
14
|
+
* process was loaded from or is working in (`findRunningCodeInside`).
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
import path from 'node:path';
|
|
@@ -13,8 +19,13 @@ import * as defaultGit from '../../git-utils.js';
|
|
|
13
19
|
import { NOOP_LOGGER } from '../../Logger.js';
|
|
14
20
|
import { AGENT_LABELS } from '../../label-constants.js';
|
|
15
21
|
import { concurrentMap } from '../../util/concurrent-map.js';
|
|
16
|
-
import {
|
|
22
|
+
import { canonicalPath } from '../../worktree/canonical-path.js';
|
|
23
|
+
import { parseWorktreePorcelain, samePath } from '../../worktree/inspector.js';
|
|
17
24
|
import { forceDrainPendingCleanup } from '../../worktree/lifecycle/force-drain.js';
|
|
25
|
+
import {
|
|
26
|
+
findRunningCodeInside,
|
|
27
|
+
removeWorktreeWithRecovery,
|
|
28
|
+
} from '../../worktree/lifecycle/reap.js';
|
|
18
29
|
|
|
19
30
|
const TICKET_READ_CONCURRENCY = 8;
|
|
20
31
|
|
|
@@ -27,33 +38,32 @@ function isStoryDone(ticket) {
|
|
|
27
38
|
return labels.includes(DONE_LABEL);
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The Story id of a `<worktreeRoot>/story-<id>` entry, else `null` — a
|
|
43
|
+
* `story-<id>` directory anywhere else (e.g. `.claude/worktrees/`) is not
|
|
44
|
+
* this sweep's to judge.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} wtPath
|
|
47
|
+
* @param {string} worktreeRoot
|
|
48
|
+
* @param {string} platform
|
|
49
|
+
* @returns {number|null}
|
|
50
|
+
*/
|
|
51
|
+
function storyIdFromPath(wtPath, worktreeRoot, platform) {
|
|
52
|
+
const parent = path.dirname(canonicalPath(wtPath));
|
|
53
|
+
if (!samePath(parent, canonicalPath(worktreeRoot), platform)) return null;
|
|
54
|
+
return defaultGit.parseStoryBranch(path.basename(path.resolve(wtPath)));
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
/**
|
|
37
|
-
*
|
|
58
|
+
* Validate and normalise the options bag.
|
|
38
59
|
*
|
|
39
60
|
* @param {object} opts
|
|
40
|
-
* @
|
|
41
|
-
* @param {string} opts.repoRoot Absolute path to the main checkout.
|
|
42
|
-
* @param {object} [opts.git]
|
|
43
|
-
* @param {object} [opts.logger]
|
|
44
|
-
* @returns {Promise<{
|
|
45
|
-
* reaped: Array<{ storyId: number, path: string }>,
|
|
46
|
-
* skipped: Array<{ storyId: number|null, path: string, reason: string }>,
|
|
47
|
-
* }>}
|
|
61
|
+
* @returns {object}
|
|
48
62
|
*/
|
|
49
|
-
|
|
63
|
+
function resolveSweepOptions(opts) {
|
|
50
64
|
const ctx = opts.ctx;
|
|
51
65
|
const provider = opts.provider ?? ctx?.provider;
|
|
52
66
|
const repoRoot = opts.repoRoot ?? ctx?.cwd;
|
|
53
|
-
const git = opts.git ?? defaultGit;
|
|
54
|
-
const logger = opts.logger ?? ctx?.logger ?? NOOP_LOGGER;
|
|
55
|
-
const fsRm = opts.fsRm;
|
|
56
|
-
const worktreeRoot = opts.worktreeRoot;
|
|
57
67
|
if (!provider || typeof provider.getTicket !== 'function') {
|
|
58
68
|
throw new Error(
|
|
59
69
|
'sweepStaleStoryWorktrees: provider with getTicket(id) is required',
|
|
@@ -62,107 +72,149 @@ export async function sweepStaleStoryWorktrees(opts = {}) {
|
|
|
62
72
|
if (!repoRoot || typeof repoRoot !== 'string') {
|
|
63
73
|
throw new Error('sweepStaleStoryWorktrees: repoRoot is required');
|
|
64
74
|
}
|
|
75
|
+
const platform = opts.platform ?? process.platform;
|
|
76
|
+
return {
|
|
77
|
+
provider,
|
|
78
|
+
repoRoot,
|
|
79
|
+
platform,
|
|
80
|
+
git: opts.git ?? defaultGit,
|
|
81
|
+
logger: opts.logger ?? ctx?.logger ?? NOOP_LOGGER,
|
|
82
|
+
fsRm: opts.fsRm,
|
|
83
|
+
sleepFn: opts.sleepFn,
|
|
84
|
+
worktreeRoot: path.resolve(
|
|
85
|
+
opts.worktreeRoot ?? path.join(repoRoot, '.worktrees'),
|
|
86
|
+
),
|
|
87
|
+
guardPaths: [process.cwd(), ...(opts.runningPaths ?? [])],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
65
90
|
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Read every Story worktree's ticket; each mapper captures its own error so
|
|
93
|
+
* one provider hiccup cannot abort the sweep via concurrentMap's
|
|
94
|
+
* first-rejection-wins policy.
|
|
95
|
+
*/
|
|
96
|
+
function readStoryTickets(entries, o) {
|
|
97
|
+
return concurrentMap(
|
|
98
|
+
entries,
|
|
99
|
+
async (entry) => {
|
|
100
|
+
const wtPath = entry.path;
|
|
101
|
+
const storyId = wtPath
|
|
102
|
+
? storyIdFromPath(wtPath, o.worktreeRoot, o.platform)
|
|
103
|
+
: null;
|
|
104
|
+
if (storyId === null) return { kind: 'non-story' };
|
|
105
|
+
try {
|
|
106
|
+
const ticket = await o.provider.getTicket(storyId);
|
|
107
|
+
return { kind: 'ok', wtPath, storyId, ticket };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return { kind: 'provider-error', wtPath, storyId, error: err };
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{ concurrency: TICKET_READ_CONCURRENCY },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Why a read entry must be kept, or `null` when it may be removed.
|
|
118
|
+
*
|
|
119
|
+
* @returns {string|null}
|
|
120
|
+
*/
|
|
121
|
+
function keepReason(r, o) {
|
|
122
|
+
if (r.kind === 'provider-error') {
|
|
123
|
+
o.logger.warn(
|
|
124
|
+
`worktree-sweep: provider.getTicket(#${r.storyId}) failed: ${r.error.message}`,
|
|
125
|
+
);
|
|
126
|
+
return `provider-error: ${r.error.message}`;
|
|
127
|
+
}
|
|
128
|
+
if (!isStoryDone(r.ticket)) return 'story-open';
|
|
129
|
+
const selfPath = findRunningCodeInside(o, r.wtPath, o.guardPaths);
|
|
130
|
+
if (selfPath) {
|
|
131
|
+
o.logger.warn(
|
|
132
|
+
`worktree-sweep: skipped storyId=${r.storyId} path=${r.wtPath} — the running process uses it (${selfPath})`,
|
|
133
|
+
);
|
|
134
|
+
return 'running-from-target-tree';
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function removeOne(r, o) {
|
|
140
|
+
const ctx = {
|
|
141
|
+
repoRoot: o.repoRoot,
|
|
142
|
+
git: o.git,
|
|
143
|
+
logger: o.logger,
|
|
144
|
+
platform: o.platform,
|
|
145
|
+
worktreeRoot: o.worktreeRoot,
|
|
146
|
+
listCache: { list: null, ts: 0 },
|
|
147
|
+
fsRm: o.fsRm,
|
|
148
|
+
};
|
|
149
|
+
const res = await removeWorktreeWithRecovery(ctx, r.wtPath, {
|
|
150
|
+
storyId: r.storyId,
|
|
151
|
+
...(o.sleepFn ? { sleepFn: o.sleepFn, retryDelay: 0 } : {}),
|
|
152
|
+
});
|
|
153
|
+
return res.removed
|
|
154
|
+
? null
|
|
155
|
+
: `remove-failed: ${res.reason ?? 'worktree-remove-failed'}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Never touches a worktree whose Story is still open, nor the one the
|
|
160
|
+
* running process uses.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} opts
|
|
163
|
+
* @param {object} opts.provider Only `getTicket(id)` is required.
|
|
164
|
+
* @param {string} opts.repoRoot Absolute path to the main checkout.
|
|
165
|
+
* @param {object} [opts.git]
|
|
166
|
+
* @param {object} [opts.logger]
|
|
167
|
+
* @param {string[]} [opts.runningPaths] Extra paths the running process owns.
|
|
168
|
+
* @returns {Promise<{
|
|
169
|
+
* reaped: Array<{ storyId: number, path: string }>,
|
|
170
|
+
* skipped: Array<{ storyId: number|null, path: string, reason: string }>,
|
|
171
|
+
* }>}
|
|
172
|
+
*/
|
|
173
|
+
export async function sweepStaleStoryWorktrees(opts = {}) {
|
|
174
|
+
const o = resolveSweepOptions(opts);
|
|
68
175
|
|
|
69
176
|
// Drain the pending-cleanup manifest first so entries whose Windows locks
|
|
70
177
|
// have released self-heal instead of accumulating.
|
|
71
178
|
const drainResult = await forceDrainPendingCleanup({
|
|
72
|
-
repoRoot,
|
|
73
|
-
worktreeRoot:
|
|
74
|
-
git,
|
|
75
|
-
fsRm,
|
|
76
|
-
logger,
|
|
179
|
+
repoRoot: o.repoRoot,
|
|
180
|
+
worktreeRoot: o.worktreeRoot,
|
|
181
|
+
git: o.git,
|
|
182
|
+
fsRm: o.fsRm,
|
|
183
|
+
logger: o.logger,
|
|
77
184
|
});
|
|
78
185
|
|
|
79
186
|
const reaped = [];
|
|
80
187
|
const skipped = [];
|
|
81
188
|
|
|
82
|
-
const listRes = git.gitSpawn(repoRoot, 'worktree', 'list', '--porcelain');
|
|
189
|
+
const listRes = o.git.gitSpawn(o.repoRoot, 'worktree', 'list', '--porcelain');
|
|
83
190
|
if (listRes.status !== 0) {
|
|
84
|
-
logger.warn(
|
|
191
|
+
o.logger.warn(
|
|
85
192
|
`worktree-sweep: git worktree list failed: ${listRes.stderr || listRes.stdout || 'unknown'}`,
|
|
86
193
|
);
|
|
87
194
|
return { reaped, skipped };
|
|
88
195
|
}
|
|
89
196
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// the sweep via concurrentMap's first-rejection-wins policy.
|
|
94
|
-
const reads = await concurrentMap(
|
|
95
|
-
entries,
|
|
96
|
-
async (entry) => {
|
|
97
|
-
const wtPath = entry.path;
|
|
98
|
-
if (!wtPath) return { kind: 'no-path' };
|
|
99
|
-
const storyId = storyIdFromPath(wtPath);
|
|
100
|
-
if (storyId === null) return { kind: 'non-story' };
|
|
101
|
-
try {
|
|
102
|
-
const ticket = await provider.getTicket(storyId);
|
|
103
|
-
return { kind: 'ok', wtPath, storyId, ticket };
|
|
104
|
-
} catch (err) {
|
|
105
|
-
return { kind: 'provider-error', wtPath, storyId, error: err };
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
{ concurrency: TICKET_READ_CONCURRENCY },
|
|
197
|
+
const reads = await readStoryTickets(
|
|
198
|
+
parseWorktreePorcelain(listRes.stdout || ''),
|
|
199
|
+
o,
|
|
109
200
|
);
|
|
110
201
|
|
|
111
202
|
// Removes stay sequential: they mutate .git/worktrees/, and racing git's
|
|
112
203
|
// locking on Windows can leave a partial admin dir the next remove trips on.
|
|
113
204
|
for (const r of reads) {
|
|
114
|
-
if (r.kind === '
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
path: r.wtPath,
|
|
119
|
-
reason: `provider-error: ${r.error.message}`,
|
|
120
|
-
});
|
|
121
|
-
logger.warn(
|
|
122
|
-
`worktree-sweep: provider.getTicket(#${r.storyId}) failed: ${r.error.message}`,
|
|
123
|
-
);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!isStoryDone(r.ticket)) {
|
|
128
|
-
skipped.push({
|
|
129
|
-
storyId: r.storyId,
|
|
130
|
-
path: r.wtPath,
|
|
131
|
-
reason: 'story-open',
|
|
132
|
-
});
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const res = git.gitSpawn(
|
|
137
|
-
repoRoot,
|
|
138
|
-
'worktree',
|
|
139
|
-
'remove',
|
|
140
|
-
'--force',
|
|
141
|
-
r.wtPath,
|
|
142
|
-
);
|
|
143
|
-
if (res.status !== 0) {
|
|
144
|
-
const reason = (
|
|
145
|
-
res.stderr ||
|
|
146
|
-
res.stdout ||
|
|
147
|
-
'worktree-remove-failed'
|
|
148
|
-
).trim();
|
|
149
|
-
skipped.push({
|
|
150
|
-
storyId: r.storyId,
|
|
151
|
-
path: r.wtPath,
|
|
152
|
-
reason: `remove-failed: ${reason}`,
|
|
153
|
-
});
|
|
154
|
-
logger.warn(
|
|
155
|
-
`worktree-sweep: failed to reap storyId=${r.storyId} path=${r.wtPath}: ${reason}`,
|
|
156
|
-
);
|
|
205
|
+
if (r.kind === 'non-story') continue;
|
|
206
|
+
const reason = keepReason(r, o) ?? (await removeOne(r, o));
|
|
207
|
+
if (reason) {
|
|
208
|
+
skipped.push({ storyId: r.storyId, path: r.wtPath, reason });
|
|
157
209
|
continue;
|
|
158
210
|
}
|
|
159
211
|
reaped.push({ storyId: r.storyId, path: r.wtPath });
|
|
160
|
-
logger.info(
|
|
212
|
+
o.logger.info(
|
|
161
213
|
`worktree-sweep: reaped stale worktree storyId=${r.storyId} path=${r.wtPath}`,
|
|
162
214
|
);
|
|
163
215
|
}
|
|
164
216
|
|
|
165
|
-
git.gitSpawn(repoRoot, 'worktree', 'prune');
|
|
217
|
+
o.git.gitSpawn(o.repoRoot, 'worktree', 'prune');
|
|
166
218
|
|
|
167
219
|
return {
|
|
168
220
|
reaped,
|
|
@@ -195,6 +195,16 @@ export function buildProviderChain(entries, ctx) {
|
|
|
195
195
|
return { inline, prompts };
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* @param {Record<string, number>} tally
|
|
200
|
+
* @param {string} name
|
|
201
|
+
* @param {Finding[]} findings
|
|
202
|
+
*/
|
|
203
|
+
function recordCritical(tally, name, findings) {
|
|
204
|
+
const critical = findings.filter((f) => f?.severity === 'critical').length;
|
|
205
|
+
if (critical > 0) tally[name] = critical;
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
/**
|
|
199
209
|
* Inline findings merge in declaration order; prompt entries render via
|
|
200
210
|
* `getPromptMessages`.
|
|
@@ -203,11 +213,14 @@ export function buildProviderChain(entries, ctx) {
|
|
|
203
213
|
* @param {{ logger?: { info?: Function, warn?: Function } }} [opts]
|
|
204
214
|
* @returns {ReviewProvider & {
|
|
205
215
|
* getPromptMessages: (input: ReviewInput) => Promise<string[]>,
|
|
216
|
+
* getCriticalByProvider: () => Record<string, number>,
|
|
206
217
|
* chain: ProviderChain,
|
|
207
218
|
* }}
|
|
208
219
|
*/
|
|
209
220
|
export function createChainProvider(chain, opts = {}) {
|
|
210
221
|
const logger = opts.logger;
|
|
222
|
+
/** Critical findings per entry from the last `runReview`, for attribution. */
|
|
223
|
+
let criticalByProvider = {};
|
|
211
224
|
|
|
212
225
|
return {
|
|
213
226
|
chain,
|
|
@@ -218,6 +231,7 @@ export function createChainProvider(chain, opts = {}) {
|
|
|
218
231
|
async runReview(input) {
|
|
219
232
|
/** @type {Finding[]} */
|
|
220
233
|
const merged = [];
|
|
234
|
+
criticalByProvider = {};
|
|
221
235
|
const ctx = {
|
|
222
236
|
scope: input?.scope,
|
|
223
237
|
labels: /** @type {ReadonlyArray<string>} */ (
|
|
@@ -238,9 +252,18 @@ export function createChainProvider(chain, opts = {}) {
|
|
|
238
252
|
);
|
|
239
253
|
}
|
|
240
254
|
for (const f of findings) merged.push(f);
|
|
255
|
+
recordCritical(criticalByProvider, entry.name, findings);
|
|
241
256
|
}
|
|
242
257
|
return merged;
|
|
243
258
|
},
|
|
259
|
+
/**
|
|
260
|
+
* Which entries raised the last run's critical findings, and how many.
|
|
261
|
+
*
|
|
262
|
+
* @returns {Record<string, number>}
|
|
263
|
+
*/
|
|
264
|
+
getCriticalByProvider() {
|
|
265
|
+
return { ...criticalByProvider };
|
|
266
|
+
},
|
|
244
267
|
/**
|
|
245
268
|
* Called after `runReview`.
|
|
246
269
|
*
|
|
@@ -10,6 +10,7 @@ import { selectAudits } from '../audit-suite/index.js';
|
|
|
10
10
|
import { graduateRetroProposals } from '../feedback-loop/retro-proposals-graduator.js';
|
|
11
11
|
import { gitSpawn } from '../git-utils.js';
|
|
12
12
|
import { Logger } from '../Logger.js';
|
|
13
|
+
import { gatherRunTelemetry } from '../observability/close-telemetry.js';
|
|
13
14
|
import { isEpicTicket } from './epic-container.js';
|
|
14
15
|
import { composeRoutedProposals } from './retro-proposals.js';
|
|
15
16
|
import {
|
|
@@ -657,6 +658,8 @@ async function executeFollowUpRollup({
|
|
|
657
658
|
graduateFn,
|
|
658
659
|
});
|
|
659
660
|
const categories = summarizeSignalCategories(signals);
|
|
661
|
+
// Step result only, never the roll-up comment: telemetry stays local.
|
|
662
|
+
const telemetry = await gatherRunTelemetry(stories, config);
|
|
660
663
|
const proposalCount = proposals.framework.length + proposals.consumer.length;
|
|
661
664
|
const outcome = assessRollupOutcome({
|
|
662
665
|
signalCount: signals.length,
|
|
@@ -687,6 +690,7 @@ async function executeFollowUpRollup({
|
|
|
687
690
|
categories,
|
|
688
691
|
outcome,
|
|
689
692
|
frictionWindow,
|
|
693
|
+
telemetry,
|
|
690
694
|
});
|
|
691
695
|
}
|
|
692
696
|
|
|
@@ -743,6 +747,7 @@ function buildRollupStepResult({
|
|
|
743
747
|
categories,
|
|
744
748
|
outcome,
|
|
745
749
|
frictionWindow,
|
|
750
|
+
telemetry = null,
|
|
746
751
|
}) {
|
|
747
752
|
return {
|
|
748
753
|
kind: 'follow-up-rollup',
|
|
@@ -766,6 +771,7 @@ function buildRollupStepResult({
|
|
|
766
771
|
fingerprint: item.fingerprint ?? null,
|
|
767
772
|
})),
|
|
768
773
|
emptyRollupSuspect: signals.length === 0 && storyCount > 1,
|
|
774
|
+
telemetry,
|
|
769
775
|
};
|
|
770
776
|
}
|
|
771
777
|
|
|
@@ -134,6 +134,7 @@ async function postStoryReviewCrossRef({
|
|
|
134
134
|
* halted: boolean,
|
|
135
135
|
* skipped?: boolean,
|
|
136
136
|
* severity?: { critical: number, high: number, medium: number, suggestion: number },
|
|
137
|
+
* criticalByProvider?: Record<string, number>,
|
|
137
138
|
* posted?: boolean,
|
|
138
139
|
* postedCommentId?: number|null,
|
|
139
140
|
* degraded?: boolean,
|
|
@@ -214,6 +215,7 @@ export async function runStoryScopeReview({
|
|
|
214
215
|
return {
|
|
215
216
|
halted: !!result.halted,
|
|
216
217
|
severity: sev,
|
|
218
|
+
criticalByProvider: result.criticalByProvider,
|
|
217
219
|
posted: result.posted,
|
|
218
220
|
postedCommentId: result.postedCommentId ?? null,
|
|
219
221
|
...degradationEnvelope(result.degradations),
|