mandrel 1.69.0 → 1.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +1 -1
- package/.agents/docs/workflows.md +1 -1
- package/.agents/scripts/agents-update-preflight.js +235 -0
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +0 -4
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -84
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/story-body/story-body.js +110 -65
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/tick.js +177 -53
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/workflows/agents-update.md +205 -28
- package/README.md +20 -0
- package/docs/CHANGELOG.md +32 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
package/.agents/README.md
CHANGED
|
@@ -466,7 +466,7 @@ ticketing provider is GitHub, resolved by `provider-factory.js` from the
|
|
|
466
466
|
`orchestration.provider` config key. CLI scripts receive provider
|
|
467
467
|
instances from the SDK surface rather than importing provider
|
|
468
468
|
implementations directly. Execution is Claude-Code-in-session — there is
|
|
469
|
-
no separate adapter abstraction; `
|
|
469
|
+
no separate adapter abstraction; `manifest-builder.js` synthesizes the
|
|
470
470
|
dispatch record inline and the dispatch manifest (md + structured
|
|
471
471
|
comment) is the cross-runtime contract.
|
|
472
472
|
|
|
@@ -29,7 +29,7 @@ description, edit the workflow file’s front-matter and regenerate.
|
|
|
29
29
|
|
|
30
30
|
| Command | Description |
|
|
31
31
|
| --- | --- |
|
|
32
|
-
| `/agents-update` | npm-era upgrade wraparound for a Mandrel consumer. Runs `mandrel update` (resolve newest published version → install → re-materialize `.agents/` → migrate → doctor → surface changelog) as the single mechanical step, then walks the operator through the judgment wraparound the CLI deliberately leaves unowned: reconcile `.agentrc.json`, install the Epic #1386 quality-gate surface, refresh the harness permission allowlist, reconcile the consumer's `AGENTS.md` / runbooks against the surfaced changelog, and stage + commit the staged lockfile bump. |
|
|
32
|
+
| `/agents-update` | npm-era upgrade wraparound for a Mandrel consumer. Runs `npx mandrel update` (resolve newest published version → install → re-materialize `.agents/` → migrate → doctor → surface changelog) as the single mechanical step, then walks the operator through the judgment wraparound the CLI deliberately leaves unowned: reconcile `.agentrc.json`, install the Epic #1386 quality-gate surface, refresh the harness permission allowlist, reconcile the consumer's `AGENTS.md` / runbooks against the surfaced changelog, and stage + commit the staged lockfile bump. |
|
|
33
33
|
| `/audit-architecture` | Audit architectural boundaries, module coupling, and layering violations; emit a structured findings report keyed to High/Medium/Low severity. |
|
|
34
34
|
| `/audit-clean-code` | Audit code smells, dead code, complexity hotspots, and maintainability-index outliers; emit a structured findings report. |
|
|
35
35
|
| `/audit-dependencies` | Audit `package.json` for unused, outdated, and major-version-stale dependencies; surface Node-engine drift and propose upgrade batches. |
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agents-update-preflight.js — Story #4170
|
|
5
|
+
* (feat(agents-update): add a first-run preflight before the updater)
|
|
6
|
+
*
|
|
7
|
+
* A first-run preflight for the `/agents-update` workflow. The workflow
|
|
8
|
+
* otherwise jumps straight to `npx mandrel update` with no guard rails;
|
|
9
|
+
* this preflight catches three day-0 failure modes *before* the version
|
|
10
|
+
* bump:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Wrong project (hard stop).** Running the updater in the framework
|
|
13
|
+
* repo itself or in a non-consumer (no `mandrel` dependency in
|
|
14
|
+
* `package.json`, or no materialized `.agents/` directory) silently
|
|
15
|
+
* does the wrong thing. The consumer-shape check is a BLOCKER: it
|
|
16
|
+
* exits non-zero so the workflow halts before bumping anything.
|
|
17
|
+
* 2. **Dirty git index (warn).** `mandrel update` deliberately leaves the
|
|
18
|
+
* lockfile *staged* (see `lib/cli/update.js`); the workflow's commit
|
|
19
|
+
* step then `git add`s and commits. If the index already holds
|
|
20
|
+
* unrelated staged changes, that step would sweep them into the
|
|
21
|
+
* `chore: update mandrel` commit. Warn so the operator can unstage
|
|
22
|
+
* first.
|
|
23
|
+
* 3. **Offline (warn).** The CLI throws a usable error on a failed
|
|
24
|
+
* `npm view`, but a one-line up-front reachability check gives a
|
|
25
|
+
* friendlier "you're offline" signal before any version probe.
|
|
26
|
+
*
|
|
27
|
+
* Severity is consistent with framework preflight conventions
|
|
28
|
+
* (cf. `story-close.js` runStoryClosePreflight, `epic-deliver-preflight.js`):
|
|
29
|
+
* the consumer-shape check is a hard stop (blocker), dirty-index and offline
|
|
30
|
+
* are warn-only and never block the run.
|
|
31
|
+
*
|
|
32
|
+
* Out of scope (per the Story): folding these checks into
|
|
33
|
+
* `lib/cli/update.js` itself — the CLI stays git-free and
|
|
34
|
+
* side-effect-scoped; this is a workflow-layer concern.
|
|
35
|
+
*
|
|
36
|
+
* The detection logic is a pure function (`runAgentsUpdatePreflight`) that
|
|
37
|
+
* takes injectable probes so it is unit-testable without touching the real
|
|
38
|
+
* filesystem, git index, or network. The CLI wrapper wires the real probes
|
|
39
|
+
* and maps a blocker finding to a non-zero exit code.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { execFileSync } from 'node:child_process';
|
|
43
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
46
|
+
import { Logger } from './lib/Logger.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {object} PreflightFinding
|
|
50
|
+
* @property {string} id Stable check id.
|
|
51
|
+
* @property {'blocker'|'warning'} severity
|
|
52
|
+
* @property {string} summary One-line human-readable description.
|
|
53
|
+
* @property {string} [fix] Copy-pasteable remediation hint.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Real-world probes. Each is overridable in tests via the `probes` option.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} projectRoot Absolute consumer repo root.
|
|
60
|
+
*/
|
|
61
|
+
export function makeProbes(projectRoot) {
|
|
62
|
+
return {
|
|
63
|
+
/**
|
|
64
|
+
* Read + parse `package.json`. Returns the parsed object, or `null`
|
|
65
|
+
* when the file is missing or unparseable.
|
|
66
|
+
*/
|
|
67
|
+
readPackageJson() {
|
|
68
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
69
|
+
if (!existsSync(pkgPath)) return null;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
/** Does the materialized `.agents/` directory exist? */
|
|
77
|
+
agentsDirExists() {
|
|
78
|
+
return existsSync(path.join(projectRoot, '.agents'));
|
|
79
|
+
},
|
|
80
|
+
/**
|
|
81
|
+
* Are there staged (index) changes? Returns true when
|
|
82
|
+
* `git diff --cached --name-only` is non-empty. Returns false on any
|
|
83
|
+
* git error (not a repo, git missing) — a missing index is not a
|
|
84
|
+
* dirty index.
|
|
85
|
+
*/
|
|
86
|
+
hasStagedChanges() {
|
|
87
|
+
try {
|
|
88
|
+
const out = execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
89
|
+
cwd: projectRoot,
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
92
|
+
});
|
|
93
|
+
return out.trim().length > 0;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Is the npm registry reachable? Probes `npm ping` (a PM-agnostic
|
|
100
|
+
* reachability query that does not mutate anything). Returns true when
|
|
101
|
+
* the ping succeeds, false on any failure (offline / registry down).
|
|
102
|
+
*/
|
|
103
|
+
registryReachable() {
|
|
104
|
+
try {
|
|
105
|
+
execFileSync('npm', ['ping'], {
|
|
106
|
+
cwd: projectRoot,
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
109
|
+
timeout: 10_000,
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pure preflight evaluator. Runs the three checks against the supplied
|
|
121
|
+
* probes and returns a structured result. Never throws; never performs
|
|
122
|
+
* I/O directly (all I/O is behind `probes`).
|
|
123
|
+
*
|
|
124
|
+
* @param {object} options
|
|
125
|
+
* @param {ReturnType<typeof makeProbes>} options.probes
|
|
126
|
+
* @returns {{ ok: boolean, blocked: boolean, findings: PreflightFinding[] }}
|
|
127
|
+
* `blocked` is true when any blocker-severity finding fired (the
|
|
128
|
+
* consumer-shape hard stop). `ok` is true when there are no findings of
|
|
129
|
+
* any severity.
|
|
130
|
+
*/
|
|
131
|
+
export function runAgentsUpdatePreflight({ probes }) {
|
|
132
|
+
/** @type {PreflightFinding[]} */
|
|
133
|
+
const findings = [];
|
|
134
|
+
|
|
135
|
+
// 1. Consumer-shape check — BLOCKER (hard stop).
|
|
136
|
+
const pkg = probes.readPackageJson();
|
|
137
|
+
const deps = pkg
|
|
138
|
+
? {
|
|
139
|
+
...(pkg.dependencies ?? {}),
|
|
140
|
+
...(pkg.devDependencies ?? {}),
|
|
141
|
+
...(pkg.optionalDependencies ?? {}),
|
|
142
|
+
}
|
|
143
|
+
: {};
|
|
144
|
+
const hasMandrelDep = Object.hasOwn(deps, 'mandrel');
|
|
145
|
+
const hasAgentsDir = probes.agentsDirExists();
|
|
146
|
+
|
|
147
|
+
if (!pkg || !hasMandrelDep || !hasAgentsDir) {
|
|
148
|
+
const missing = [];
|
|
149
|
+
if (!pkg) missing.push('no readable package.json');
|
|
150
|
+
else if (!hasMandrelDep)
|
|
151
|
+
missing.push('package.json does not list "mandrel" as a dependency');
|
|
152
|
+
if (!hasAgentsDir) missing.push('no .agents/ directory');
|
|
153
|
+
findings.push({
|
|
154
|
+
id: 'consumer-shape',
|
|
155
|
+
severity: 'blocker',
|
|
156
|
+
summary: `Not a Mandrel consumer project (${missing.join('; ')}). Run /agents-update from a consumer repo that depends on "mandrel" and has a materialized .agents/ tree — not the framework repo itself or an unrelated project.`,
|
|
157
|
+
fix: 'cd into the consumer project root, or run `npm install -D mandrel && npx mandrel sync` to bootstrap one.',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 2. Dirty-index check — WARN only.
|
|
162
|
+
if (probes.hasStagedChanges()) {
|
|
163
|
+
findings.push({
|
|
164
|
+
id: 'dirty-index',
|
|
165
|
+
severity: 'warning',
|
|
166
|
+
summary:
|
|
167
|
+
"The git index already has staged changes. `mandrel update` leaves the lockfile staged, and the workflow's commit step (Step 5) would sweep these unrelated staged files into the `chore: update mandrel` commit.",
|
|
168
|
+
fix: 'Unstage unrelated changes first: `git restore --staged <path>` (or `git reset` to clear the whole index), then re-run the preflight.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3. Offline check — WARN only.
|
|
173
|
+
if (!probes.registryReachable()) {
|
|
174
|
+
findings.push({
|
|
175
|
+
id: 'offline',
|
|
176
|
+
severity: 'warning',
|
|
177
|
+
summary:
|
|
178
|
+
'The npm registry is not reachable. `npx mandrel update` resolves the newest published version via the registry and will fail its version probe while offline.',
|
|
179
|
+
fix: 'Check your network connection (or registry auth/proxy config) before running `npx mandrel update`.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const blocked = findings.some((f) => f.severity === 'blocker');
|
|
184
|
+
return { ok: findings.length === 0, blocked, findings };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Render the findings to a logger. Blockers go to `error`, warnings to
|
|
189
|
+
* `warn`. A clean result logs a single `info` line.
|
|
190
|
+
*
|
|
191
|
+
* @param {{ ok: boolean, blocked: boolean, findings: PreflightFinding[] }} result
|
|
192
|
+
* @param {{ info: Function, warn: Function, error: Function }} logger
|
|
193
|
+
*/
|
|
194
|
+
export function reportPreflight(result, logger) {
|
|
195
|
+
if (result.ok) {
|
|
196
|
+
logger.info(
|
|
197
|
+
'✅ [agents-update-preflight] All checks passed — safe to run `npx mandrel update`.',
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const f of result.findings) {
|
|
202
|
+
const line = `[${f.id}] ${f.summary}${f.fix ? `\n ↳ Fix: ${f.fix}` : ''}`;
|
|
203
|
+
if (f.severity === 'blocker') {
|
|
204
|
+
logger.error(`❌ ${line}`);
|
|
205
|
+
} else {
|
|
206
|
+
logger.warn(`⚠️ ${line}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (result.blocked) {
|
|
210
|
+
logger.error(
|
|
211
|
+
'[agents-update-preflight] Hard stop: do not run `npx mandrel update` until the blocker above is resolved (exit 2).',
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
logger.warn(
|
|
215
|
+
'[agents-update-preflight] Warnings only — review them, then proceed if intentional.',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function main() {
|
|
221
|
+
const projectRoot = process.cwd();
|
|
222
|
+
const probes = makeProbes(projectRoot);
|
|
223
|
+
const result = runAgentsUpdatePreflight({ probes });
|
|
224
|
+
reportPreflight(result, Logger);
|
|
225
|
+
// Machine-parsable JSON envelope on stdout for tooling / the workflow to
|
|
226
|
+
// read. Use process.stdout.write (not console.log) per the no-console
|
|
227
|
+
// enforcement boundary: human-facing output goes through Logger above.
|
|
228
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
229
|
+
return result.blocked ? 2 : 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
runAsCli(import.meta.url, main, {
|
|
233
|
+
source: 'agents-update-preflight',
|
|
234
|
+
propagateExitCode: true,
|
|
235
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* apply-quality-bootstrap.js — Story #4171
|
|
5
|
+
* (refactor(agents-update): extract the quality-bootstrap heredoc into a
|
|
6
|
+
* tested script)
|
|
7
|
+
*
|
|
8
|
+
* Replaces the inline `node -e "Promise.all([...])"` heredoc that Step 3.5 of
|
|
9
|
+
* the `/agents-update` workflow used to carry. That shape was fragile in three
|
|
10
|
+
* ways: it broke across shells (PowerShell vs bash quoting / backticks), it
|
|
11
|
+
* had no test so it silently drifted when the two helper signatures moved, and
|
|
12
|
+
* it could not be invoked or dry-run independently.
|
|
13
|
+
*
|
|
14
|
+
* This script runs the two Epic #1386 quality-gate installs in order against
|
|
15
|
+
* the consumer repo root:
|
|
16
|
+
*
|
|
17
|
+
* 1. `applyQualityBootstrap` — copies the code-quality-guardrails helper,
|
|
18
|
+
* installs the `.husky/pre-commit` quality:preview line, backfills the
|
|
19
|
+
* `quality:preview` / `quality:watch` npm scripts, and seeds the
|
|
20
|
+
* `delivery.quality.{codingGuardrails,autoRefresh}` defaults.
|
|
21
|
+
* 2. `migrateBaselinesLayout` — relocates per-Epic baseline snapshots into
|
|
22
|
+
* the `temp/epic/<id>/baselines/` namespace.
|
|
23
|
+
*
|
|
24
|
+
* Both helpers are idempotent by contract — a second run reports `no-change`
|
|
25
|
+
* on every install path — so this wrapper is safe to re-run. It prints the
|
|
26
|
+
* **same JSON result shape** the heredoc did: `{ quality, baselines }` to
|
|
27
|
+
* stdout, so any tooling that parsed the old output keeps working.
|
|
28
|
+
*
|
|
29
|
+
* The effectful work is a thin pure function (`applyBootstrapAndMigration`)
|
|
30
|
+
* that takes the two helpers and the project root, so the test suite can
|
|
31
|
+
* drive it against a tmp directory without spawning a child process. The CLI
|
|
32
|
+
* wrapper wires the real helpers and `process.cwd()`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { migrateBaselinesLayout } from './lib/bootstrap/baselines-layout-migration.js';
|
|
37
|
+
import { applyQualityBootstrap } from './lib/bootstrap/quality-bootstrap.js';
|
|
38
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run the quality-bootstrap install and the baselines-layout migration
|
|
42
|
+
* against `projectRoot`, returning the combined `{ quality, baselines }`
|
|
43
|
+
* envelope. Pure relative to its injected helpers: the default helpers touch
|
|
44
|
+
* the filesystem under `projectRoot`, but tests can pass stubs to exercise
|
|
45
|
+
* the composition in isolation.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} options
|
|
48
|
+
* @param {string} options.projectRoot Absolute consumer repo root.
|
|
49
|
+
* @param {typeof applyQualityBootstrap} [options.applyQualityBootstrap]
|
|
50
|
+
* @param {typeof migrateBaselinesLayout} [options.migrateBaselinesLayout]
|
|
51
|
+
* @returns {{ quality: object, baselines: object }}
|
|
52
|
+
*/
|
|
53
|
+
export function applyBootstrapAndMigration({
|
|
54
|
+
projectRoot,
|
|
55
|
+
applyQualityBootstrap: applyQuality = applyQualityBootstrap,
|
|
56
|
+
migrateBaselinesLayout: migrateBaselines = migrateBaselinesLayout,
|
|
57
|
+
}) {
|
|
58
|
+
const quality = applyQuality({ projectRoot });
|
|
59
|
+
const baselines = migrateBaselines({
|
|
60
|
+
baselinesDir: path.join(projectRoot, 'baselines'),
|
|
61
|
+
repoRoot: projectRoot,
|
|
62
|
+
});
|
|
63
|
+
return { quality, baselines };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
const projectRoot = process.cwd();
|
|
68
|
+
const result = applyBootstrapAndMigration({ projectRoot });
|
|
69
|
+
// Mirror the retired heredoc's output: pretty-printed `{ quality, baselines }`
|
|
70
|
+
// to stdout. Use process.stdout.write (not console.log) per the no-console
|
|
71
|
+
// enforcement boundary.
|
|
72
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
runAsCli(import.meta.url, main, {
|
|
77
|
+
source: 'apply-quality-bootstrap',
|
|
78
|
+
propagateExitCode: true,
|
|
79
|
+
});
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* audit-labels-bootstrap.js — Idempotently create the `audit::<
|
|
2
|
+
* audit-labels-bootstrap.js — Idempotently create the `audit::<lens>`
|
|
3
3
|
* label taxonomy in the configured GitHub repo.
|
|
4
4
|
*
|
|
5
5
|
* Run this once per repo before `/audit-to-stories` opens its first
|
|
6
6
|
* Story. Re-runs are safe — existing labels are skipped, only missing
|
|
7
7
|
* ones are created. Story #2583 acceptance criterion #6.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The lens list is the shared SSOT `AUDIT_LENSES`
|
|
10
|
+
* (`lib/audit-to-stories/audit-lenses.js`), one per `/audit-<lens>` workflow
|
|
11
|
+
* under `.agents/workflows/`. Sourcing the list from the same module that
|
|
12
|
+
* `build-story-body.js` derives `audit::<lens>` labels from guarantees the
|
|
13
|
+
* label producer (this bootstrap) and the label deriver (story-body) cannot
|
|
14
|
+
* drift — a finding from `audit-documentation-results.md` derives
|
|
15
|
+
* `audit::documentation`, and this bootstrap creates exactly that label
|
|
16
|
+
* (Story #4195). The per-lens colour/description metadata lives in
|
|
17
|
+
* `LENS_META` below; adding a new `audit-*` workflow means adding its lens to
|
|
18
|
+
* `AUDIT_LENSES` and (optionally) a `LENS_META` entry.
|
|
12
19
|
*
|
|
13
20
|
* Delegates to `gh label create` so the script works without any
|
|
14
21
|
* provider plumbing — `gh auth status` is the only prerequisite. Per
|
|
@@ -19,72 +26,87 @@
|
|
|
19
26
|
import process from 'node:process';
|
|
20
27
|
import { parseArgs } from 'node:util';
|
|
21
28
|
|
|
29
|
+
import { AUDIT_LENSES } from './lib/audit-to-stories/audit-lenses.js';
|
|
22
30
|
import { runAsCli } from './lib/cli-utils.js';
|
|
23
31
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
24
32
|
import { gh as defaultGh, GhExecError } from './lib/gh-exec.js';
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Per-lens label presentation. Keyed by canonical lens name. A lens absent
|
|
36
|
+
* from this map falls back to {@link DEFAULT_LENS_META} so a newly-added
|
|
37
|
+
* `AUDIT_LENSES` entry still gets a label without a hard requirement to
|
|
38
|
+
* register colour/description here first.
|
|
39
|
+
*/
|
|
40
|
+
const LENS_META = Object.freeze({
|
|
41
|
+
architecture: {
|
|
29
42
|
color: '6f42c1',
|
|
30
43
|
description: 'Audit-sourced finding: architectural concerns',
|
|
31
44
|
},
|
|
32
|
-
{
|
|
33
|
-
name: 'clean-code',
|
|
45
|
+
'clean-code': {
|
|
34
46
|
color: '0e8a16',
|
|
35
47
|
description: 'Audit-sourced finding: clean-code / maintainability',
|
|
36
48
|
},
|
|
37
|
-
{
|
|
38
|
-
name: 'dependencies',
|
|
49
|
+
dependencies: {
|
|
39
50
|
color: 'd4c5f9',
|
|
40
51
|
description: 'Audit-sourced finding: dependencies / supply chain',
|
|
41
52
|
},
|
|
42
|
-
{
|
|
43
|
-
name: 'devops',
|
|
53
|
+
devops: {
|
|
44
54
|
color: 'fbca04',
|
|
45
55
|
description: 'Audit-sourced finding: DevOps / CI / CD',
|
|
46
56
|
},
|
|
47
|
-
{
|
|
48
|
-
|
|
57
|
+
documentation: {
|
|
58
|
+
color: '1d76db',
|
|
59
|
+
description: 'Audit-sourced finding: documentation staleness / gaps',
|
|
60
|
+
},
|
|
61
|
+
lighthouse: {
|
|
49
62
|
color: 'c5def5',
|
|
50
63
|
description: 'Audit-sourced finding: Lighthouse score regressions',
|
|
51
64
|
},
|
|
52
|
-
{
|
|
53
|
-
|
|
65
|
+
navigability: {
|
|
66
|
+
color: 'bfdadc',
|
|
67
|
+
description: 'Audit-sourced finding: route / nav reachability',
|
|
68
|
+
},
|
|
69
|
+
performance: {
|
|
54
70
|
color: 'b60205',
|
|
55
71
|
description: 'Audit-sourced finding: performance / latency',
|
|
56
72
|
},
|
|
57
|
-
{
|
|
58
|
-
name: 'privacy',
|
|
73
|
+
privacy: {
|
|
59
74
|
color: 'fef2c0',
|
|
60
75
|
description: 'Audit-sourced finding: privacy / data handling',
|
|
61
76
|
},
|
|
62
|
-
{
|
|
63
|
-
name: 'quality',
|
|
77
|
+
quality: {
|
|
64
78
|
color: '0052cc',
|
|
65
79
|
description: 'Audit-sourced finding: test quality / coverage gaps',
|
|
66
80
|
},
|
|
67
|
-
{
|
|
68
|
-
name: 'security',
|
|
81
|
+
security: {
|
|
69
82
|
color: 'b60205',
|
|
70
83
|
description: 'Audit-sourced finding: security / OWASP',
|
|
71
84
|
},
|
|
72
|
-
{
|
|
73
|
-
name: 'seo',
|
|
85
|
+
seo: {
|
|
74
86
|
color: 'fbca04',
|
|
75
87
|
description: 'Audit-sourced finding: SEO / discoverability',
|
|
76
88
|
},
|
|
77
|
-
{
|
|
78
|
-
name: 'sre',
|
|
89
|
+
sre: {
|
|
79
90
|
color: '0052cc',
|
|
80
91
|
description: 'Audit-sourced finding: SRE / observability / reliability',
|
|
81
92
|
},
|
|
82
|
-
{
|
|
83
|
-
name: 'ux-ui',
|
|
93
|
+
'ux-ui': {
|
|
84
94
|
color: 'd4c5f9',
|
|
85
95
|
description: 'Audit-sourced finding: UX / UI concerns',
|
|
86
96
|
},
|
|
87
|
-
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const DEFAULT_LENS_META = Object.freeze({
|
|
100
|
+
color: 'ededed',
|
|
101
|
+
description: 'Audit-sourced finding',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const DIMENSIONS = Object.freeze(
|
|
105
|
+
AUDIT_LENSES.map((name) => ({
|
|
106
|
+
name,
|
|
107
|
+
...(LENS_META[name] ?? DEFAULT_LENS_META),
|
|
108
|
+
})),
|
|
109
|
+
);
|
|
88
110
|
|
|
89
111
|
async function labelExists(gh, owner, repo, name) {
|
|
90
112
|
try {
|
|
@@ -39,6 +39,7 @@ import { groupFindings } from './lib/audit-to-stories/group-findings.js';
|
|
|
39
39
|
import { parseAuditReports } from './lib/audit-to-stories/parse-audit-md.js';
|
|
40
40
|
import { buildEpicSeedMarkdown } from './lib/audit-to-stories/seed-epic-from-findings.js';
|
|
41
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
42
|
+
import { Logger } from './lib/Logger.js';
|
|
42
43
|
|
|
43
44
|
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
44
45
|
const DEFAULT_GLOB = 'temp/audits/audit-*-results.md';
|
|
@@ -109,6 +110,44 @@ async function loadProvider() {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Render the loud, operator-visible warning emitted when the Phase 6 dedup
|
|
115
|
+
* does NOT run against real GitHub issues. Two distinct reasons:
|
|
116
|
+
*
|
|
117
|
+
* - `'no-provider-port'` — the configured provider resolved but exposes no
|
|
118
|
+
* `searchIssues` port (or `loadProvider()` threw). This is the
|
|
119
|
+
* silent-no-op the workflow's "Never open a duplicate Issue" contract
|
|
120
|
+
* was failing on: every group classifies `create` and the operator gets
|
|
121
|
+
* zero automated dedup signal. Surfacing it loudly is the whole point.
|
|
122
|
+
* - `'disabled'` — the operator passed `--no-provider`, intentionally
|
|
123
|
+
* skipping dedup. Still warned (so a re-run that opens duplicates is
|
|
124
|
+
* never a surprise), but framed as a deliberate choice.
|
|
125
|
+
*
|
|
126
|
+
* Pure: returns the message string so `buildPlan` owns the single
|
|
127
|
+
* `Logger.warn` write site and the text stays unit-testable.
|
|
128
|
+
*
|
|
129
|
+
* @param {'no-provider-port'|'disabled'} reason
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
function dedupSkippedWarning(reason) {
|
|
133
|
+
if (reason === 'disabled') {
|
|
134
|
+
return (
|
|
135
|
+
'dedup skipped (--no-provider): every group is classified "create" ' +
|
|
136
|
+
'without checking GitHub for existing issues. A re-run may open ' +
|
|
137
|
+
'duplicates of already-tracked or already-closed findings. Drop ' +
|
|
138
|
+
'--no-provider to enable fingerprint dedup against real issues.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
'dedup skipped (no provider port): the configured provider exposes no ' +
|
|
143
|
+
'searchIssues() port, so Phase 6 dedup did NOT run. Every group is ' +
|
|
144
|
+
'classified "create" and existing/closed issues are NOT checked — a run ' +
|
|
145
|
+
'that creates Stories from this plan WILL open duplicates of ' +
|
|
146
|
+
'already-tracked work. Verify `gh auth status` and the github.{owner,repo} ' +
|
|
147
|
+
'config so a real provider resolves.'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
async function buildPlan({ glob: pattern, severity, useProvider }) {
|
|
113
152
|
const reportPaths = await collectReportPaths(pattern ?? DEFAULT_GLOB);
|
|
114
153
|
if (reportPaths.length === 0) {
|
|
@@ -143,6 +182,7 @@ async function buildPlan({ glob: pattern, severity, useProvider }) {
|
|
|
143
182
|
matchedFingerprints: [],
|
|
144
183
|
}));
|
|
145
184
|
let summary = { create: groups.length, skipOpen: 0, skipReoccurring: 0 };
|
|
185
|
+
let dedupApplied = false;
|
|
146
186
|
|
|
147
187
|
if (useProvider) {
|
|
148
188
|
const provider = await loadProvider();
|
|
@@ -150,7 +190,18 @@ async function buildPlan({ glob: pattern, severity, useProvider }) {
|
|
|
150
190
|
const result = await classifyGroupsAgainstGitHub({ groups, provider });
|
|
151
191
|
classifications = result.classifications;
|
|
152
192
|
summary = result.summary;
|
|
193
|
+
dedupApplied = true;
|
|
194
|
+
} else {
|
|
195
|
+
// The provider could not resolve a searchIssues port — the dedup gate
|
|
196
|
+
// is silently a no-op without this. Surface it loudly (stderr, so the
|
|
197
|
+
// --scan JSON on stdout stays clean) so the operator does not read a
|
|
198
|
+
// create-only plan as "no duplicates found".
|
|
199
|
+
Logger.warn(dedupSkippedWarning('no-provider-port'));
|
|
153
200
|
}
|
|
201
|
+
} else {
|
|
202
|
+
// Operator explicitly opted out via --no-provider. Still warn so a
|
|
203
|
+
// duplicate-opening re-run is never a surprise.
|
|
204
|
+
Logger.warn(dedupSkippedWarning('disabled'));
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
return {
|
|
@@ -165,6 +216,7 @@ async function buildPlan({ glob: pattern, severity, useProvider }) {
|
|
|
165
216
|
totalFindings: allFindings.length,
|
|
166
217
|
filtered: filtered.length,
|
|
167
218
|
tally: tallyBySeverity(filtered),
|
|
219
|
+
dedupApplied,
|
|
168
220
|
...summary,
|
|
169
221
|
},
|
|
170
222
|
};
|
|
@@ -188,6 +240,8 @@ export const __testing = {
|
|
|
188
240
|
meetsSeverity,
|
|
189
241
|
collectReportPaths,
|
|
190
242
|
buildPlan,
|
|
243
|
+
loadProvider,
|
|
244
|
+
dedupSkippedWarning,
|
|
191
245
|
};
|
|
192
246
|
|
|
193
247
|
async function main() {
|
|
@@ -534,6 +534,13 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
534
534
|
const reposList = lists.reposList;
|
|
535
535
|
const projectsList = lists.projectsList;
|
|
536
536
|
const pickerOwner = (answers) => answers?.owner || owner;
|
|
537
|
+
// `owner` / `repo` are GitHub-side answers. When `--skip-github` suppresses
|
|
538
|
+
// the entire GitHub bootstrap, they are not required — this lets a
|
|
539
|
+
// non-interactive `--assume-yes --skip-github` run materialize and configure
|
|
540
|
+
// a fresh non-git directory (no inferable remote) without hard-failing on
|
|
541
|
+
// `missing required answers: owner, repo`. With GitHub bootstrap active they
|
|
542
|
+
// remain required (the target repo must be resolvable).
|
|
543
|
+
const skipGithub = Boolean(flags?.['skip-github']);
|
|
537
544
|
return [
|
|
538
545
|
{
|
|
539
546
|
key: 'owner',
|
|
@@ -541,7 +548,7 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
541
548
|
env: 'GH_OWNER',
|
|
542
549
|
message: '\n\nGitHub repo owner',
|
|
543
550
|
default: defaults.owner,
|
|
544
|
-
required:
|
|
551
|
+
required: !skipGithub,
|
|
545
552
|
validate: (v) =>
|
|
546
553
|
/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(v) ? null : 'Invalid GitHub owner',
|
|
547
554
|
},
|
|
@@ -567,7 +574,7 @@ export function buildQuestions(defaults, flags, env = process.env, lists = {}) {
|
|
|
567
574
|
pickerMessage:
|
|
568
575
|
'GitHub repo name - Select existing or press ENTER to create',
|
|
569
576
|
default: defaults.repo,
|
|
570
|
-
required:
|
|
577
|
+
required: !skipGithub,
|
|
571
578
|
picker: {
|
|
572
579
|
list: (answers) => {
|
|
573
580
|
if (Array.isArray(reposList) && reposList.length > 0)
|
|
@@ -957,7 +964,10 @@ export async function collectAndConfirm(state) {
|
|
|
957
964
|
});
|
|
958
965
|
if (missing.length > 0) {
|
|
959
966
|
Logger.error(
|
|
960
|
-
`[Bootstrap] missing required answers: ${missing.join(', ')}
|
|
967
|
+
`[Bootstrap] missing required answers: ${missing.join(', ')}. ` +
|
|
968
|
+
'Pass them as flags (e.g. `--owner <name> --repo <name>`), or run ' +
|
|
969
|
+
'with `--skip-github` to configure the files/local setup only and ' +
|
|
970
|
+
'wire GitHub later.',
|
|
961
971
|
);
|
|
962
972
|
return { ok: false, exit: 1 };
|
|
963
973
|
}
|