mandrel 2.54.0 → 2.56.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/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +8 -2
- package/.agents/docs/configuration.md +5 -0
- package/.agents/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +34 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +374 -76
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/file-ci-gap.js +306 -0
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +80 -29
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +61 -115
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +52 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +42 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +233 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -0
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +63 -42
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/pr-watch-with-update.js +3 -2
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/audit-to-stories.md +63 -27
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +35 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +26 -8
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +30 -0
- package/.agents/workflows/mandrel-plan.md +10 -13
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +37 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* file-ci-gap.js — the CI-remediation Option-2 filing command.
|
|
4
|
+
*
|
|
5
|
+
* `rules/ci-remediation.md` sends three verdicts here — `pre-existing`,
|
|
6
|
+
* `capacity`, `unreproducible-tier` — each meaning "this red check is real,
|
|
7
|
+
* and fixing it is not this delivery's job". The rule used to say "file a
|
|
8
|
+
* `meta::framework-gap` issue" and stop, leaving the agent to hand-run
|
|
9
|
+
* `gh issue create` wherever it was standing. This is the mechanism behind
|
|
10
|
+
* that sentence: evidence from the CI digest, ownership routing, fingerprint
|
|
11
|
+
* dedup, the `friction` comment, and the `agent::blocked` flip in one call.
|
|
12
|
+
*
|
|
13
|
+
* It files an **intake** issue, never a Story: `/mandrel-plan <id>` graduates
|
|
14
|
+
* it on the next planning pass. Delivery never blocks on planning — see
|
|
15
|
+
* `lib/orchestration/ci-gap-intake.js` for why that split is load-bearing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { parseArgs } from 'node:util';
|
|
19
|
+
|
|
20
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
21
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
22
|
+
import {
|
|
23
|
+
createFollowUpIssue,
|
|
24
|
+
ensureIssueLabels,
|
|
25
|
+
updateFollowUpIssue,
|
|
26
|
+
} from './lib/feedback-loop/graduator-core.js';
|
|
27
|
+
import { issueNumberFromUrl } from './lib/feedback-loop/retro-proposals-graduator.js';
|
|
28
|
+
import {
|
|
29
|
+
resolveOwnershipRepos,
|
|
30
|
+
routeOwnership,
|
|
31
|
+
} from './lib/github/framework-repo.js';
|
|
32
|
+
import { Logger } from './lib/Logger.js';
|
|
33
|
+
import {
|
|
34
|
+
fileCiGapIntake,
|
|
35
|
+
INTAKE_VERDICTS,
|
|
36
|
+
REFUSED_VERDICT,
|
|
37
|
+
} from './lib/orchestration/ci-gap-intake.js';
|
|
38
|
+
import { readCiDigest } from './lib/orchestration/ci-rerun-guard.js';
|
|
39
|
+
import {
|
|
40
|
+
STATE_LABELS,
|
|
41
|
+
transitionTicketState,
|
|
42
|
+
upsertStructuredComment,
|
|
43
|
+
} from './lib/orchestration/ticketing.js';
|
|
44
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
45
|
+
|
|
46
|
+
const USAGE = {
|
|
47
|
+
invocation:
|
|
48
|
+
'node .agents/scripts/file-ci-gap.js --story <id> --verdict <verdict> --owner <bucket> [--evidence "<proof reading>"] [--pr <n>] [--block] [--dry-run]',
|
|
49
|
+
summary:
|
|
50
|
+
'File (or update) the CI-gap intake issue for an Option-2 verdict in .agents/rules/ci-remediation.md, routed to the repository that owns the fault and deduped by failure signature.',
|
|
51
|
+
flags: [
|
|
52
|
+
[
|
|
53
|
+
'--story <id>',
|
|
54
|
+
'Story the red check blocked (required — keys the CI digest).',
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'--verdict <verdict>',
|
|
58
|
+
`One of ${INTAKE_VERDICTS.join(' | ')}. "${REFUSED_VERDICT}" is refused: it routes to Option 1, fix at source.`,
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
'--owner <bucket>',
|
|
62
|
+
'Who owns the fault: consumer | framework | platform. Resolves through github.followUpRepos.*.',
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'--evidence <text>',
|
|
66
|
+
"The verdict's proof reading (the exhausted-limit log line, the failed attach). Recorded in the body.",
|
|
67
|
+
],
|
|
68
|
+
['--pr <n>', 'PR number the red check ran on; recorded as an occurrence.'],
|
|
69
|
+
[
|
|
70
|
+
'--block',
|
|
71
|
+
'Also flip the Story to agent::blocked. Without it the friction comment is posted but the Story is left where it is.',
|
|
72
|
+
],
|
|
73
|
+
['--dry-run', 'Compose the filing and print it; write nothing.'],
|
|
74
|
+
],
|
|
75
|
+
notes: [
|
|
76
|
+
'Requires a CI digest at temp/story-<id>-ci-digest.json — pr-watch-with-update.js --story <id> writes it on the first red.',
|
|
77
|
+
'A repeat occurrence of a known signature UPDATES the existing intake issue rather than opening a second one.',
|
|
78
|
+
'The issue it files is intake, not an executable Story: graduate it with /mandrel-plan <issue number>.',
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Wire the live GitHub ports the intake filer writes through.
|
|
84
|
+
*
|
|
85
|
+
* `gh` is the transport rather than the provider facade because a filing can
|
|
86
|
+
* target a repository other than the configured one, and `gh issue create
|
|
87
|
+
* --repo` is the surface that already does that (the graduators file
|
|
88
|
+
* cross-repo the same way).
|
|
89
|
+
*
|
|
90
|
+
* @param {object} opts
|
|
91
|
+
* @returns {object} ports for `fileCiGapIntake`
|
|
92
|
+
*/
|
|
93
|
+
function liveIntakePorts({ provider, searchRepo, cwd, logger }) {
|
|
94
|
+
const labelCache = new Map();
|
|
95
|
+
return {
|
|
96
|
+
searchIssues: (query) =>
|
|
97
|
+
provider.searchIssues({
|
|
98
|
+
query,
|
|
99
|
+
owner: searchRepo.owner,
|
|
100
|
+
repo: searchRepo.repo,
|
|
101
|
+
}),
|
|
102
|
+
createIssue: async ({ owner, repo, title, body, labels }) => {
|
|
103
|
+
// `gh issue create --label <absent>` fails outright, so a brand-new
|
|
104
|
+
// routing label (meta::platform-gap, friction::unreproducible-tier)
|
|
105
|
+
// has to exist before the create — not after it errors.
|
|
106
|
+
const ensured = await ensureIssueLabels({
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
labels,
|
|
110
|
+
labelCache,
|
|
111
|
+
cwd,
|
|
112
|
+
});
|
|
113
|
+
for (const err of ensured.errors) logger?.warn?.(`[file-ci-gap] ${err}`);
|
|
114
|
+
const created = await createFollowUpIssue({
|
|
115
|
+
owner,
|
|
116
|
+
repo,
|
|
117
|
+
title,
|
|
118
|
+
body,
|
|
119
|
+
labels,
|
|
120
|
+
ghPath: 'gh',
|
|
121
|
+
cwd,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
url: created.url,
|
|
125
|
+
number: created.url ? issueNumberFromUrl(created.url) : null,
|
|
126
|
+
error: created.error,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
updateIssue: ({ owner, repo, number, body }) =>
|
|
130
|
+
updateFollowUpIssue({ owner, repo, number, body, ghPath: 'gh', cwd }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render the `friction` comment the Story carries so the blocker is legible
|
|
136
|
+
* on the ticket itself, not only in the intake issue.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} opts
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function renderFrictionComment({ verdict, result, digest }) {
|
|
142
|
+
const target = result.issue?.url ?? result.issue?.number ?? '(not filed)';
|
|
143
|
+
const lines = [
|
|
144
|
+
`### CI gap filed — verdict \`${verdict}\``,
|
|
145
|
+
'',
|
|
146
|
+
`- **Failing check:** \`${digest?.failingCheck ?? 'unknown'}\``,
|
|
147
|
+
`- **Run:** ${digest?.runUrl ?? `run id ${digest?.runId ?? 'unresolved'}`}`,
|
|
148
|
+
`- **Intake issue:** ${target} (${result.decision})`,
|
|
149
|
+
`- **Owner:** \`${result.routing.bucket}\` → \`${result.routing.routedRepo.owner}/${result.routing.routedRepo.repo}\``,
|
|
150
|
+
];
|
|
151
|
+
if (!result.routing.routable) {
|
|
152
|
+
lines.push(
|
|
153
|
+
`- **Routing:** \`unroutable\` — \`${result.routing.missingKey}\` is unset, so the intake issue was filed locally.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (result.routing.deferredFrom) {
|
|
157
|
+
lines.push(
|
|
158
|
+
`- **Routing:** deferred from \`${result.routing.deferredFrom}\` (${result.routing.deferralReason}) — filed locally instead.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
lines.push(
|
|
162
|
+
'',
|
|
163
|
+
'This verdict does **not** license a re-run of the failed job. Graduate the',
|
|
164
|
+
'intake issue with `/mandrel-plan <issue number>` to turn it into a Story.',
|
|
165
|
+
);
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* File the CI-gap intake issue for one Story, post the `friction` comment,
|
|
171
|
+
* and optionally flip the Story to `agent::blocked`.
|
|
172
|
+
*
|
|
173
|
+
* Every port is injectable so the unit tests exercise the whole command with
|
|
174
|
+
* no network and no live tracker.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} opts
|
|
177
|
+
* @returns {Promise<object>} the intake result, plus what the command did.
|
|
178
|
+
*/
|
|
179
|
+
export async function runFileCiGap({
|
|
180
|
+
storyId,
|
|
181
|
+
verdict,
|
|
182
|
+
owner: bucket,
|
|
183
|
+
evidence = '',
|
|
184
|
+
prNumber = null,
|
|
185
|
+
dryRun = false,
|
|
186
|
+
block = false,
|
|
187
|
+
config,
|
|
188
|
+
provider,
|
|
189
|
+
ports,
|
|
190
|
+
digest,
|
|
191
|
+
tempRoot,
|
|
192
|
+
cwd = process.cwd(),
|
|
193
|
+
logger = Logger,
|
|
194
|
+
now,
|
|
195
|
+
} = {}) {
|
|
196
|
+
const sid = Number(storyId);
|
|
197
|
+
if (!Number.isInteger(sid) || sid <= 0) {
|
|
198
|
+
throw new Error('--story <id> is required (a positive issue number).');
|
|
199
|
+
}
|
|
200
|
+
const resolved = config ?? resolveConfig();
|
|
201
|
+
const ciDigest = digest ?? readCiDigest({ storyId: sid, tempRoot, cwd });
|
|
202
|
+
if (!ciDigest) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`no CI digest for Story #${sid}. The digest is written by \`pr-watch-with-update.js --story ${sid}\` on the first red; without it there is no run link or failure signature to file.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const repos = resolveOwnershipRepos(resolved);
|
|
209
|
+
const currentRepo = repos.consumer ?? { owner: 'unknown', repo: 'unknown' };
|
|
210
|
+
// Dedup searches the repository the filing will land in — routing is a pure
|
|
211
|
+
// function, so computing it here and inside the filer cannot disagree.
|
|
212
|
+
const routed = routeOwnership({ bucket, repos, currentRepo });
|
|
213
|
+
const searchRepo = routed.routable ? routed.routedRepo : currentRepo;
|
|
214
|
+
|
|
215
|
+
const ticketing =
|
|
216
|
+
provider ?? (dryRun ? null : (createProvider(resolved) ?? null));
|
|
217
|
+
const livePorts =
|
|
218
|
+
ports ??
|
|
219
|
+
liveIntakePorts({
|
|
220
|
+
provider: ticketing ?? createProvider(resolved),
|
|
221
|
+
searchRepo,
|
|
222
|
+
cwd,
|
|
223
|
+
logger,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = await fileCiGapIntake({
|
|
227
|
+
digest: ciDigest,
|
|
228
|
+
verdict,
|
|
229
|
+
bucket,
|
|
230
|
+
evidence,
|
|
231
|
+
repos,
|
|
232
|
+
currentRepo,
|
|
233
|
+
prNumber,
|
|
234
|
+
dryRun,
|
|
235
|
+
ports: livePorts,
|
|
236
|
+
logger,
|
|
237
|
+
now,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const actions = { commented: false, blocked: false };
|
|
241
|
+
if (!dryRun && ticketing) {
|
|
242
|
+
const body = renderFrictionComment({ verdict, result, digest: ciDigest });
|
|
243
|
+
try {
|
|
244
|
+
await upsertStructuredComment(ticketing, sid, 'friction', body);
|
|
245
|
+
actions.commented = true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
result.errors.push(`friction comment failed: ${err?.message ?? err}`);
|
|
248
|
+
}
|
|
249
|
+
if (block) {
|
|
250
|
+
try {
|
|
251
|
+
await transitionTicketState(ticketing, sid, STATE_LABELS.BLOCKED, {});
|
|
252
|
+
actions.blocked = true;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
result.errors.push(
|
|
255
|
+
`agent::blocked transition failed: ${err?.message ?? err}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { storyId: sid, verdict, ...result, actions };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* CLI entrypoint.
|
|
266
|
+
*
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async function main() {
|
|
270
|
+
const { values } = parseArgs({
|
|
271
|
+
args: process.argv.slice(2),
|
|
272
|
+
options: {
|
|
273
|
+
story: { type: 'string' },
|
|
274
|
+
verdict: { type: 'string' },
|
|
275
|
+
owner: { type: 'string' },
|
|
276
|
+
evidence: { type: 'string' },
|
|
277
|
+
pr: { type: 'string' },
|
|
278
|
+
block: { type: 'boolean', default: false },
|
|
279
|
+
'dry-run': { type: 'boolean', default: false },
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const result = await runFileCiGap({
|
|
284
|
+
storyId: values.story,
|
|
285
|
+
verdict: values.verdict,
|
|
286
|
+
owner: values.owner,
|
|
287
|
+
evidence: values.evidence ?? '',
|
|
288
|
+
prNumber: values.pr ? Number(values.pr) : null,
|
|
289
|
+
dryRun: values['dry-run'],
|
|
290
|
+
block: values.block,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Single-line JSON per the script-output contract — an orchestrator parses
|
|
294
|
+
// this, and a pretty dump is noise in a delivery transcript.
|
|
295
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
296
|
+
for (const err of result.errors) {
|
|
297
|
+
Logger.error(`[file-ci-gap] ${err}`);
|
|
298
|
+
}
|
|
299
|
+
if (result.errors.length > 0) process.exitCode = 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
runAsCli(import.meta.url, main, {
|
|
303
|
+
source: 'file-ci-gap',
|
|
304
|
+
errorPrefix: '[file-ci-gap]',
|
|
305
|
+
usage: USAGE,
|
|
306
|
+
});
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* current workflow set.
|
|
23
23
|
* --check — exits 0 when the on-disk file matches the freshly generated
|
|
24
24
|
* content, throws (→ exit 1) with a regeneration hint otherwise.
|
|
25
|
+
* --root — read and write under another checkout's `.agents/` tree.
|
|
26
|
+
* See {@link resolveTargets} for why this seam exists.
|
|
25
27
|
*
|
|
26
28
|
* Per `.agents/rules/orchestration-error-handling.md`, unrecoverable failures
|
|
27
29
|
* surface via `throw new Error(...)` so `runAsCli` maps the throw to
|
|
@@ -39,8 +41,43 @@ import { buildCatalog, buildLoopCatalog } from './lib/mandrel-catalog.js';
|
|
|
39
41
|
const __filename = fileURLToPath(import.meta.url);
|
|
40
42
|
const __dirname = path.dirname(__filename);
|
|
41
43
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the workflow source directory and the generated doc for one
|
|
47
|
+
* repository root.
|
|
48
|
+
*
|
|
49
|
+
* The `--root` seam this backs exists for the drift gate's own test. That
|
|
50
|
+
* test used to prove the gate by editing the *real*
|
|
51
|
+
* `.agents/workflows/mandrel-deliver.md`, running `--check`, and restoring
|
|
52
|
+
* the file in `afterEach`. The proof was sound; the blast radius was not.
|
|
53
|
+
* `node --test` runs test files in parallel against one shared checkout, so
|
|
54
|
+
* for the ~1s the real file sat mutated, every other test file observed a
|
|
55
|
+
* dirty tree — and `tests/enforcement/workflow-script-help.test.js`, whose
|
|
56
|
+
* final assertion is a repo-wide `git status --porcelain`, reported it as
|
|
57
|
+
* "`--help` mutated the working tree" on the Windows Smoke job, where the
|
|
58
|
+
* wider process-spawn cost stretches that window far enough to collide.
|
|
59
|
+
*
|
|
60
|
+
* A generator that can only ever be pointed at its own checkout forces that
|
|
61
|
+
* choice. Pointing it at a fixture root removes it.
|
|
62
|
+
*
|
|
63
|
+
* The `--root` default is applied here rather than at the call site so the
|
|
64
|
+
* one branch it costs lives with the resolution it belongs to.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} [root] Repository root to render against; defaults to this
|
|
67
|
+
* checkout. A relative path is resolved against the process cwd.
|
|
68
|
+
* @returns {{ root: string, workflowsDir: string, docPath: string }}
|
|
69
|
+
*/
|
|
70
|
+
export function resolveTargets(root) {
|
|
71
|
+
const resolved = root ? path.resolve(root) : PROJECT_ROOT;
|
|
72
|
+
return {
|
|
73
|
+
root: resolved,
|
|
74
|
+
workflowsDir: path.join(resolved, '.agents', 'workflows'),
|
|
75
|
+
docPath: path.join(resolved, '.agents', 'docs', 'workflows.md'),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** This checkout's own targets — the default when `--root` is absent. */
|
|
80
|
+
const { workflowsDir: WORKFLOWS_DIR, docPath: DOC_PATH } = resolveTargets();
|
|
44
81
|
|
|
45
82
|
/**
|
|
46
83
|
* Collapse a catalog description to a single Markdown table-cell-safe line.
|
|
@@ -149,16 +186,24 @@ export function renderWorkflowsDoc(catalog, loopCatalog = []) {
|
|
|
149
186
|
/**
|
|
150
187
|
* Build the canonical generated content and read the on-disk file (if any).
|
|
151
188
|
*
|
|
152
|
-
* @
|
|
189
|
+
* @param {string} [root] Repository root to render against; see
|
|
190
|
+
* {@link resolveTargets}.
|
|
191
|
+
* @returns {{
|
|
192
|
+
* generated: string,
|
|
193
|
+
* original: string | null,
|
|
194
|
+
* root: string,
|
|
195
|
+
* docPath: string,
|
|
196
|
+
* }}
|
|
153
197
|
*/
|
|
154
|
-
export function buildExpected() {
|
|
155
|
-
const
|
|
156
|
-
const
|
|
198
|
+
export function buildExpected(root) {
|
|
199
|
+
const { root: resolvedRoot, workflowsDir, docPath } = resolveTargets(root);
|
|
200
|
+
const catalog = buildCatalog(workflowsDir);
|
|
201
|
+
const loopCatalog = buildLoopCatalog(workflowsDir);
|
|
157
202
|
const generated = renderWorkflowsDoc(catalog, loopCatalog);
|
|
158
|
-
const original = fs.existsSync(
|
|
159
|
-
? fs.readFileSync(
|
|
203
|
+
const original = fs.existsSync(docPath)
|
|
204
|
+
? fs.readFileSync(docPath, 'utf8')
|
|
160
205
|
: null;
|
|
161
|
-
return { generated, original };
|
|
206
|
+
return { generated, original, root: resolvedRoot, docPath };
|
|
162
207
|
}
|
|
163
208
|
|
|
164
209
|
/**
|
|
@@ -169,12 +214,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
169
214
|
args: argv,
|
|
170
215
|
options: {
|
|
171
216
|
check: { type: 'boolean', default: false },
|
|
217
|
+
root: { type: 'string' },
|
|
172
218
|
},
|
|
173
219
|
allowPositionals: false,
|
|
174
220
|
});
|
|
175
221
|
|
|
176
|
-
const { generated, original } = buildExpected();
|
|
177
|
-
const rel = path.relative(
|
|
222
|
+
const { generated, original, root, docPath } = buildExpected(values.root);
|
|
223
|
+
const rel = path.relative(root, docPath).split(path.sep).join('/');
|
|
178
224
|
|
|
179
225
|
if (values.check) {
|
|
180
226
|
if (original === generated) {
|
|
@@ -191,8 +237,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
191
237
|
Logger.info(`generate-workflows-doc: ${rel} already current — no write.`);
|
|
192
238
|
return;
|
|
193
239
|
}
|
|
194
|
-
fs.mkdirSync(path.dirname(
|
|
195
|
-
fs.writeFileSync(
|
|
240
|
+
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
241
|
+
fs.writeFileSync(docPath, generated, 'utf8');
|
|
196
242
|
Logger.info(`generate-workflows-doc: wrote ${rel}.`);
|
|
197
243
|
}
|
|
198
244
|
|
|
@@ -201,7 +247,8 @@ export { DOC_PATH, WORKFLOWS_DIR };
|
|
|
201
247
|
runAsCli(import.meta.url, main, {
|
|
202
248
|
source: 'generate-workflows-doc',
|
|
203
249
|
usage: {
|
|
204
|
-
invocation:
|
|
250
|
+
invocation:
|
|
251
|
+
'node .agents/scripts/generate-workflows-doc.js [--check] [--root <dir>]',
|
|
205
252
|
summary:
|
|
206
253
|
'Regenerate the workflow catalog from .agents/workflows/. Writes only when the generated content differs.',
|
|
207
254
|
flags: [
|
|
@@ -209,6 +256,10 @@ runAsCli(import.meta.url, main, {
|
|
|
209
256
|
'--check',
|
|
210
257
|
'Verify the doc is current and fail if stale; write nothing.',
|
|
211
258
|
],
|
|
259
|
+
[
|
|
260
|
+
'--root <dir>',
|
|
261
|
+
"Render against another checkout's .agents/ tree instead of this one (test seam).",
|
|
262
|
+
],
|
|
212
263
|
],
|
|
213
264
|
},
|
|
214
265
|
});
|
|
@@ -157,6 +157,10 @@ runAsCli(import.meta.url, main, {
|
|
|
157
157
|
'Never consider branches matching the glob (repeatable).',
|
|
158
158
|
],
|
|
159
159
|
['--drop-stashes <ref>', 'Stash ref approved for dropping (repeatable).'],
|
|
160
|
+
[
|
|
161
|
+
'--include-content-merged',
|
|
162
|
+
'Under --yes, also delete remote refs detected only by content-equivalence.',
|
|
163
|
+
],
|
|
160
164
|
['--base <branch>', 'Base branch (default: project.baseBranch).'],
|
|
161
165
|
['--cwd <path>', 'Repository root (default: process cwd).'],
|
|
162
166
|
],
|
|
@@ -98,6 +98,84 @@ export class ITicketingProvider {
|
|
|
98
98
|
// Intentional no-op. Concrete providers that maintain a cache override.
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* List every ticket carrying `labels`, in the **mapped** ticket shape.
|
|
103
|
+
*
|
|
104
|
+
* This is the declared read for a label scan, and the only one callers
|
|
105
|
+
* should reach for. Implementations MUST map every issue the way every
|
|
106
|
+
* other read on this interface does — in particular `id` is the **issue
|
|
107
|
+
* number**, not the backend's internal database id.
|
|
108
|
+
*
|
|
109
|
+
* That single rule is the whole reason the method exists. The raw REST
|
|
110
|
+
* payload names the issue number `number` and the database id `id`, so a
|
|
111
|
+
* consumer handed either shape wrote `number ?? id` and appeared to cope —
|
|
112
|
+
* while silently addressing issues by database id on the mapped shape,
|
|
113
|
+
* because there `id` is already the number and the fallback never fires.
|
|
114
|
+
* A declared shape removes the choice rather than documenting it.
|
|
115
|
+
*
|
|
116
|
+
* `state` selects `open` (default), `closed` or `all`. Implementations MUST
|
|
117
|
+
* honour it: a caller asking for `all` is asking a question — "did this
|
|
118
|
+
* child reopen?" — that an open-only listing answers wrongly rather than
|
|
119
|
+
* partially.
|
|
120
|
+
*
|
|
121
|
+
* @param {{ state?: 'open'|'closed'|'all', labels?: string }} [_opts]
|
|
122
|
+
* @returns {Promise<Array<{
|
|
123
|
+
* id: number,
|
|
124
|
+
* title: string,
|
|
125
|
+
* body: string,
|
|
126
|
+
* labels: string[],
|
|
127
|
+
* assignees: string[],
|
|
128
|
+
* state: string,
|
|
129
|
+
* url?: string|null,
|
|
130
|
+
* }>>}
|
|
131
|
+
*/
|
|
132
|
+
async listTicketsByLabel(_opts = {}) {
|
|
133
|
+
throw new Error('Not implemented: listTicketsByLabel');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read a parent's native sub-issue children as issue numbers.
|
|
138
|
+
*
|
|
139
|
+
* Takes **both** identifiers because they address different things: the
|
|
140
|
+
* backend's child edge is keyed by the parent's opaque node id, while
|
|
141
|
+
* `number` exists only so a degraded read can name the parent it failed on.
|
|
142
|
+
* Passing the number where the node id belongs is not a type error — it is
|
|
143
|
+
* a successful call about the wrong issue — which is why the parameter
|
|
144
|
+
* order is fixed here rather than left to each call site.
|
|
145
|
+
*
|
|
146
|
+
* Implementations MUST return `[]` rather than throw when the sub-issue
|
|
147
|
+
* feature is unavailable on the backend: absence of the feature is not a
|
|
148
|
+
* failed read, and callers union this with a body checklist that still
|
|
149
|
+
* answers the question.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} _nodeId Opaque node id of the parent.
|
|
152
|
+
* @param {number} _number Parent's issue number, for diagnostics only.
|
|
153
|
+
* @returns {Promise<number[]>}
|
|
154
|
+
*/
|
|
155
|
+
async getNativeSubIssues(_nodeId, _number) {
|
|
156
|
+
throw new Error('Not implemented: getNativeSubIssues');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a ticket's container parent in **one** call.
|
|
161
|
+
*
|
|
162
|
+
* Exists so a child→parent lookup is a read, not a search. Without it the
|
|
163
|
+
* only way to find a container was to list every candidate parent and read
|
|
164
|
+
* each one's children — O(containers) requests to answer what the backend
|
|
165
|
+
* knows directly.
|
|
166
|
+
*
|
|
167
|
+
* Returns `null` when the ticket has no parent, and `null` rather than
|
|
168
|
+
* throwing when the backend cannot answer. Callers treat a null as "no
|
|
169
|
+
* parent resolved *here*" and may fall back to a body-declared link; an
|
|
170
|
+
* exception would turn a degraded lookup into a failed lifecycle edge.
|
|
171
|
+
*
|
|
172
|
+
* @param {number} _number Issue number whose parent to resolve.
|
|
173
|
+
* @returns {Promise<object|null>} Mapped parent ticket, or null.
|
|
174
|
+
*/
|
|
175
|
+
async getParentIssue(_number) {
|
|
176
|
+
throw new Error('Not implemented: getParentIssue');
|
|
177
|
+
}
|
|
178
|
+
|
|
101
179
|
/**
|
|
102
180
|
* Return the dependency graph edges for a ticket.
|
|
103
181
|
* Parses `blocked by #NNN` patterns from the ticket body.
|