mandrel 1.69.0 → 1.71.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 +7 -7
- package/.agents/docs/SDLC.md +4 -5
- package/.agents/docs/configuration.md +9 -9
- package/.agents/docs/workflows.md +4 -6
- package/.agents/schemas/qa-finding.schema.json +1 -1
- 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/bootstrap/ci-workflow-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +1 -1
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/defaults.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-resolver.js +1 -1
- 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/qa/qa-context-hydrator.js +1 -1
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +1 -1
- 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/mandrel-update-preflight.js +235 -0
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/sync-agentrc.js +2 -2
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +3 -3
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +4 -4
- package/.agents/workflows/git-deliver.md +298 -0
- package/.agents/workflows/helpers/epic-testing.md +6 -6
- package/.agents/workflows/helpers/{agents-sync-config.md → mandrel-sync-config.md} +5 -4
- package/.agents/workflows/{agents-update.md → mandrel-update.md} +210 -33
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/{qa-run-harness.md → qa-run.md} +5 -5
- package/README.md +40 -0
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- package/.agents/workflows/git-commit-all.md +0 -15
- package/.agents/workflows/git-pr-all.md +0 -281
- package/.agents/workflows/git-push.md +0 -63
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mandrel-update-preflight.js — Story #4170
|
|
5
|
+
* (feat(mandrel-update): add a first-run preflight before the updater)
|
|
6
|
+
*
|
|
7
|
+
* A first-run preflight for the `/mandrel-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 (`runMandrelUpdatePreflight`) 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 runMandrelUpdatePreflight({ 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 /mandrel-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
|
+
'✅ [mandrel-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
|
+
'[mandrel-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
|
+
'[mandrel-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 = runMandrelUpdatePreflight({ 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: 'mandrel-update-preflight',
|
|
234
|
+
propagateExitCode: true,
|
|
235
|
+
});
|
|
@@ -99,6 +99,54 @@ export class IssuesGateway {
|
|
|
99
99
|
return issues.filter((issue) => !issue?.pull_request);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Search issues by a free-text query via the REST search API
|
|
104
|
+
* (`GET /search/issues`). Deliberately REST, **not** GraphQL: transient
|
|
105
|
+
* GraphQL 401s are a known failure mode in this repo (the dedup port that
|
|
106
|
+
* consumes this method must not silently no-op on an auth blip), so the
|
|
107
|
+
* search rides the same `gh api` REST surface + transient-retry shim as
|
|
108
|
+
* every other read here.
|
|
109
|
+
*
|
|
110
|
+
* The caller (`audit-to-stories.js` `loadProvider()`) passes a 40-char
|
|
111
|
+
* fingerprint sha as the query so the search resolves the handful of
|
|
112
|
+
* issues whose fingerprint footer carries that sha; `route-finding.js`
|
|
113
|
+
* then confirms identity against the footer. Both open and closed issues
|
|
114
|
+
* are returned (no `state:` qualifier is appended) so a closed-fingerprint
|
|
115
|
+
* match can surface as `regression-of-closed`.
|
|
116
|
+
*
|
|
117
|
+
* Returns the trimmed `[{ number, state, body }]` projection the dedup
|
|
118
|
+
* port expects. `state` is normalised to the REST lowercase form
|
|
119
|
+
* (`open` / `closed`).
|
|
120
|
+
*
|
|
121
|
+
* @param {{ query: string, owner?: string, repo?: string }} params
|
|
122
|
+
* @returns {Promise<Array<{ number: number, state: string, body: string }>>}
|
|
123
|
+
* @field-manifest GET /search/issues: total_count, items[number, state, body]
|
|
124
|
+
*/
|
|
125
|
+
async searchIssues({ query, owner, repo } = {}) {
|
|
126
|
+
if (typeof query !== 'string' || query.trim().length === 0) {
|
|
127
|
+
throw new Error('searchIssues: a non-empty query string is required');
|
|
128
|
+
}
|
|
129
|
+
const scopeOwner = owner ?? this.owner;
|
|
130
|
+
const scopeRepo = repo ?? this.repo;
|
|
131
|
+
// Constrain the search to this repo and to issues (not PRs). The
|
|
132
|
+
// fingerprint sha is the free-text term; GitHub matches it against the
|
|
133
|
+
// issue body where the `<!-- audit-fingerprints: ... -->` footer lives.
|
|
134
|
+
const qualifiers = [`repo:${scopeOwner}/${scopeRepo}`, 'type:issue'];
|
|
135
|
+
const q = `${query.trim()} ${qualifiers.join(' ')}`;
|
|
136
|
+
const endpoint = `/search/issues?q=${encodeURIComponent(q)}`;
|
|
137
|
+
const result = await withTransientRetry(
|
|
138
|
+
() => this._gh.api({ method: 'GET', endpoint }),
|
|
139
|
+
{ label: `searchIssues ${query}`, onRetry: defaultRetryWarn },
|
|
140
|
+
);
|
|
141
|
+
const json = parseApiJson(result);
|
|
142
|
+
const items = Array.isArray(json?.items) ? json.items : [];
|
|
143
|
+
return items.map((item) => ({
|
|
144
|
+
number: item.number,
|
|
145
|
+
state: item.state ?? 'open',
|
|
146
|
+
body: item.body ?? '',
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
/**
|
|
103
151
|
* List Epic-typed issues. Filter shape preserved from the old code.
|
|
104
152
|
*
|
|
@@ -98,6 +98,7 @@ export class GitHubProvider extends ITicketingProvider {
|
|
|
98
98
|
*/
|
|
99
99
|
const DELEGATIONS = [
|
|
100
100
|
['graphql', 'issues.ghGraphql'],
|
|
101
|
+
['searchIssues', 'issues.searchIssues'],
|
|
101
102
|
['listIssuesByLabel', 'issues.listIssuesByLabel'],
|
|
102
103
|
['getEpics', 'issues.getEpics'],
|
|
103
104
|
['getEpic', 'issues.getEpic'],
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* sync-agentrc.js — default-aware `.agentrc.json` reconciliation (Story #1995).
|
|
5
5
|
*
|
|
6
6
|
* Replaces the manual procedure formerly described in
|
|
7
|
-
* `.agents/workflows/helpers/
|
|
8
|
-
* `/
|
|
7
|
+
* `.agents/workflows/helpers/mandrel-sync-config.md`. Invoked by
|
|
8
|
+
* `/mandrel-update` Step 3 after the package upgrade re-materializes `.agents/`.
|
|
9
9
|
*
|
|
10
10
|
* Contract:
|
|
11
11
|
* - Validates the project config against the framework schema. On
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-06-
|
|
2
|
+
"generatedAt": "2026-06-16T16:35:27.286Z",
|
|
3
3
|
"generator": "generate-skills-index.js@1",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
@@ -577,7 +577,7 @@
|
|
|
577
577
|
"tier": "stack",
|
|
578
578
|
"category": "qa",
|
|
579
579
|
"path": ".agents/skills/stack/qa/qa-harness/SKILL.md",
|
|
580
|
-
"description": "Conventions for the agent-driven QA harness that drives Gherkin scenarios through a real browser. Use when executing `/qa-run
|
|
580
|
+
"description": "Conventions for the agent-driven QA harness that drives Gherkin scenarios through a real browser. Use when executing `/qa-run` or instrumenting a live surface — covers navigation-first execution, per-surface console and network capture, design-token visual checks, and the framework-generic heuristic cards for turning signal into findings. The harness procedure lives in `.agents/workflows/qa-run.md`; this skill is the conventions reference it leans on.",
|
|
581
581
|
"policyCapsuleBullets": 8,
|
|
582
582
|
"allowedTools": null,
|
|
583
583
|
"vendor": null
|
|
@@ -71,7 +71,7 @@ authoring.
|
|
|
71
71
|
single `defineBddConfig` block that lists `features` and `steps` paths.
|
|
72
72
|
- Register the Cucumber HTML/JSON reporter alongside the Playwright HTML
|
|
73
73
|
reporter so a headless CI invocation emits machine-readable evidence
|
|
74
|
-
alongside the agent-driven `/qa-run
|
|
74
|
+
alongside the agent-driven `/qa-run` sweep.
|
|
75
75
|
- Use Playwright projects (not Cucumber profiles) for browser matrix fan-out —
|
|
76
76
|
keeps sharding, retries, and trace config in one place.
|
|
77
77
|
|
|
@@ -99,7 +99,7 @@ authoring.
|
|
|
99
99
|
tag vocabularies in the runner config; extend via `@domain-*` only.
|
|
100
100
|
- Wire tag-filtered headless runs to a single npm script so operators never
|
|
101
101
|
reconstruct the generate-then-run sequence by hand; the agent-driven
|
|
102
|
-
`/qa-run
|
|
102
|
+
`/qa-run` selector mirrors the same tag expressions for browser sweeps.
|
|
103
103
|
- Fail the run if generation produces zero matching scenarios — a silent empty
|
|
104
104
|
suite is worse than a red build.
|
|
105
105
|
|
|
@@ -184,5 +184,5 @@ those outcomes.
|
|
|
184
184
|
|
|
185
185
|
- Scenario authoring rules: `.agents/rules/gherkin-standards.md`.
|
|
186
186
|
- Browser-level conventions: `.agents/skills/stack/qa/playwright/SKILL.md`.
|
|
187
|
-
- Operator entry point: `.agents/workflows/qa-run
|
|
187
|
+
- Operator entry point: `.agents/workflows/qa-run.md`.
|
|
188
188
|
- Evidence handoff: `.agents/workflows/helpers/epic-testing.md`.
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
name: qa-harness
|
|
3
3
|
description:
|
|
4
4
|
Conventions for the agent-driven QA harness that drives Gherkin scenarios
|
|
5
|
-
through a real browser. Use when executing `/qa-run
|
|
5
|
+
through a real browser. Use when executing `/qa-run` or instrumenting
|
|
6
6
|
a live surface — covers navigation-first execution, per-surface console and
|
|
7
7
|
network capture, design-token visual checks, and the framework-generic
|
|
8
8
|
heuristic cards for turning signal into findings. The harness procedure lives
|
|
9
|
-
in `.agents/workflows/qa-run
|
|
9
|
+
in `.agents/workflows/qa-run.md`; this skill is the conventions
|
|
10
10
|
reference it leans on.
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@ description:
|
|
|
26
26
|
Guidance for executing the agent-driven QA harness through a real browser (the
|
|
27
27
|
chrome-devtools MCP surface). The harness **procedure** — argument parsing,
|
|
28
28
|
step ordering, contract resolution sequence — is the SSOT in
|
|
29
|
-
[`.agents/workflows/qa-run
|
|
29
|
+
[`.agents/workflows/qa-run.md`](../../../../workflows/qa-run.md);
|
|
30
30
|
this skill shows **how** to apply the instrumentation and inspection
|
|
31
31
|
conventions that procedure depends on. The assertion-tier rules it enforces
|
|
32
32
|
live in [`testing-standards.md`](../../../../rules/testing-standards.md)
|
|
@@ -212,7 +212,7 @@ QA evidence ticket.
|
|
|
212
212
|
|
|
213
213
|
## 7. Cross-References
|
|
214
214
|
|
|
215
|
-
- Run procedure (SSOT): [`qa-run
|
|
215
|
+
- Run procedure (SSOT): [`qa-run.md`](../../../../workflows/qa-run.md).
|
|
216
216
|
- Console filter module: [`console-allowlist.js`](../../../../scripts/lib/qa/console-allowlist.js).
|
|
217
217
|
- Assertion-tier rules: [`testing-standards.md`](../../../../rules/testing-standards.md).
|
|
218
218
|
- Scenario prose: [`gherkin-authoring`](../gherkin-authoring/SKILL.md).
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
Single ad-hoc delivery command for working-tree changes. Detects the git
|
|
4
|
+
setup and escalates to the right terminal step — commit only, commit + push,
|
|
5
|
+
or commit + push + open a PR with native auto-merge — picking the default
|
|
6
|
+
from observable state and letting flags pin any level explicitly. Replaces
|
|
7
|
+
the retired git-commit-all, git-push, and git-pr-all trio.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /git-deliver [Message] [--no-push] [--pr] [--draft] [--no-auto-merge] [--branch <name>] [--base <branch>]
|
|
11
|
+
|
|
12
|
+
This workflow is the **single source of truth** for getting outstanding
|
|
13
|
+
working-tree changes out the door when they do not belong to a planned Epic
|
|
14
|
+
(typo fixes, file deletions, doc tweaks, dependency bumps, operator
|
|
15
|
+
housekeeping). It is the ad-hoc counterpart to the heavyweight `/deliver`
|
|
16
|
+
pipeline.
|
|
17
|
+
|
|
18
|
+
It replaces the retired `/git-commit-all`, `/git-push`, and `/git-pr-all`
|
|
19
|
+
commands: instead of choosing a command by how far you want to go, you run
|
|
20
|
+
one command and it **detects the git setup** and escalates to the correct
|
|
21
|
+
terminal step. Flags pin any level explicitly; the interactive choice prompt
|
|
22
|
+
fires **only** when the detected state is genuinely ambiguous, so the common
|
|
23
|
+
path stays non-interactive and scriptable.
|
|
24
|
+
|
|
25
|
+
> **Persona**: `devops-engineer` · **Skills**:
|
|
26
|
+
> `core/git-workflow-and-versioning`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Terminal levels
|
|
31
|
+
|
|
32
|
+
| Level | Terminal action | Default trigger |
|
|
33
|
+
| ----- | --------------- | --------------- |
|
|
34
|
+
| **commit** | stage + commit on the current branch | `--no-push`, **or** no git remote is configured |
|
|
35
|
+
| **push** | + push the current branch to its upstream | on a feature branch (current ≠ base branch) with a remote |
|
|
36
|
+
| **pr** | + cut/push a feature branch, open a PR, arm auto-merge | on the base branch (a direct push would bounce off branch protection), **or** `--pr` is set |
|
|
37
|
+
|
|
38
|
+
The detection only sets the **default**. Every level is reachable by an
|
|
39
|
+
explicit flag, and the command **announces what it detected and which level
|
|
40
|
+
it is about to run** before it acts.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Arguments
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/git-deliver [Message] [--no-push] [--pr] [--draft] [--no-auto-merge] [--branch <name>] [--base <branch>]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `Message` — the commit subject. First line becomes the commit subject (and,
|
|
51
|
+
at the **pr** level, the PR title); if the message contains a blank line,
|
|
52
|
+
everything after it becomes the commit/PR body. When omitted, a timestamped
|
|
53
|
+
fallback (`chore: ad-hoc changes <ISO 8601>`) is used so the commit is never
|
|
54
|
+
unmessageable.
|
|
55
|
+
- `--no-push` — force the **commit** level: stage and commit only, no push.
|
|
56
|
+
Useful when chaining several commits or deferring the push.
|
|
57
|
+
- `--pr` — force the **pr** level even from a feature branch where a plain
|
|
58
|
+
push would otherwise be the default.
|
|
59
|
+
- `--draft` — (pr level) open the PR in draft state and skip arming
|
|
60
|
+
auto-merge. Useful when you want CI to run before flipping to
|
|
61
|
+
ready-for-review.
|
|
62
|
+
- `--no-auto-merge` — (pr level) open a normal (non-draft) PR but do not enable
|
|
63
|
+
GitHub's native auto-merge queue. The operator merges through the UI.
|
|
64
|
+
Default at the pr level is `gh pr merge --auto --squash --delete-branch`.
|
|
65
|
+
- `--branch <name>` — (pr level) override the auto-generated feature branch
|
|
66
|
+
name. When omitted, the branch is slugged from the commit subject (Step 3).
|
|
67
|
+
- `--base <branch>` — override the base branch used for detection and as the PR
|
|
68
|
+
merge target. When omitted, reads `project.baseBranch` from `.agentrc.json`
|
|
69
|
+
(default `main`).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Step 0 — Detect Git Setup & Resolve Level
|
|
74
|
+
|
|
75
|
+
1. Resolve `[BASE_BRANCH]` from `--base` or `.agentrc.json` →
|
|
76
|
+
`project.baseBranch` (default `main`).
|
|
77
|
+
2. Read the current branch: `git rev-parse --abbrev-ref HEAD`.
|
|
78
|
+
3. Verify the working tree has outstanding changes with
|
|
79
|
+
`git status --porcelain`. If the output is empty: **STOP** and tell the
|
|
80
|
+
operator there is nothing to deliver.
|
|
81
|
+
4. Detect whether a remote is configured: `git remote`.
|
|
82
|
+
5. Resolve the **terminal level** from flags + state:
|
|
83
|
+
- `--no-push` set → **commit**.
|
|
84
|
+
- No remote configured → **commit** (warn there is nowhere to push).
|
|
85
|
+
- `--pr` set → **pr**.
|
|
86
|
+
- Current branch equals `[BASE_BRANCH]` → **pr** (a direct push to the
|
|
87
|
+
protected base would be rejected, so the PR flow is the only safe path).
|
|
88
|
+
- Otherwise (a feature branch with a remote) → **push**.
|
|
89
|
+
6. **Ambiguity gate.** Surface an interactive choice **only** when the state is
|
|
90
|
+
genuinely under-determined — for example a **detached HEAD**, or a feature
|
|
91
|
+
branch with a remote but no upstream tracking ref where pushing would need
|
|
92
|
+
`-u`. Present the operator the candidate levels (e.g. "push to a new
|
|
93
|
+
upstream" vs. "open a PR") and proceed with their pick. In every
|
|
94
|
+
non-ambiguous case, do **not** prompt — announce the detected level and
|
|
95
|
+
continue.
|
|
96
|
+
7. Echo a one-line plan to the operator before acting, e.g.
|
|
97
|
+
`detected: on feature branch 'fix/foo' with upstream → level: push`.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Step 1 — Compose Commit Message
|
|
102
|
+
|
|
103
|
+
If the operator passed `[Message]`, use it verbatim. Otherwise fall back to
|
|
104
|
+
`chore: ad-hoc changes <ISO 8601 timestamp>`.
|
|
105
|
+
|
|
106
|
+
Split the message on the first blank line:
|
|
107
|
+
|
|
108
|
+
- **Subject** — the first line; commit subject and (pr level) PR title.
|
|
109
|
+
- **Body** — everything after the first blank line; commit body and (pr level)
|
|
110
|
+
PR body. May be empty.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Step 2 — Stage + Commit (all levels)
|
|
115
|
+
|
|
116
|
+
Stage all outstanding changes:
|
|
117
|
+
|
|
118
|
+
```powershell
|
|
119
|
+
git add -A
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Commit:
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
git commit -m "<subject>" -m "<body>"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If the body is empty, omit the second `-m`. If the pre-commit hook fails:
|
|
129
|
+
|
|
130
|
+
1. Read the failure output.
|
|
131
|
+
2. Fix the issue (run `npm run format`, fix lint errors, etc.).
|
|
132
|
+
3. `git add -A` again.
|
|
133
|
+
4. Re-run `git commit` — do **not** pass `--no-verify`.
|
|
134
|
+
|
|
135
|
+
**If the level is `commit`, stop here** and print the commit summary.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Step 3 — Cut Feature Branch (pr level, from-base only)
|
|
140
|
+
|
|
141
|
+
Only when the level is **pr** *and* the current branch equals `[BASE_BRANCH]`.
|
|
142
|
+
Skip when already on a feature branch.
|
|
143
|
+
|
|
144
|
+
When `--branch <name>` is set, use it verbatim. Otherwise generate a branch
|
|
145
|
+
slug from the commit subject:
|
|
146
|
+
|
|
147
|
+
1. Detect the Conventional Commit type prefix (`<type>(<scope>): …`). If
|
|
148
|
+
matched, use `<type>` as the branch namespace. Allowed types: `feat`,
|
|
149
|
+
`fix`, `chore`, `docs`, `refactor`, `test`, `build`, `ci`, `perf`,
|
|
150
|
+
`style`. Anything else (or no prefix) → `chore`.
|
|
151
|
+
2. Strip the type prefix and any leading punctuation from the subject.
|
|
152
|
+
3. Lowercase, replace non-alphanumeric runs with `-`, collapse repeated
|
|
153
|
+
hyphens, trim leading/trailing hyphens.
|
|
154
|
+
4. Truncate to 50 chars on a word boundary.
|
|
155
|
+
5. Combine: `<type>/<slug>`. Example: `"Delete unused files"` →
|
|
156
|
+
`chore/delete-unused-files`.
|
|
157
|
+
|
|
158
|
+
Cut and check out the branch **before committing** — that is, when this step
|
|
159
|
+
applies, run it ahead of Step 2's commit so the commit lands on the feature
|
|
160
|
+
branch, never on the base branch:
|
|
161
|
+
|
|
162
|
+
```powershell
|
|
163
|
+
git checkout -b <branch-name>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
If a local branch with that name already exists, append `-2` (then `-3`, …)
|
|
167
|
+
until `git rev-parse --verify` returns non-zero, and check that out instead.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Step 4 — Push (push and pr levels)
|
|
172
|
+
|
|
173
|
+
Push the current branch. At the **push** level, push to the existing upstream:
|
|
174
|
+
|
|
175
|
+
```powershell
|
|
176
|
+
git push
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
At the **pr** level (or any branch lacking an upstream), set the upstream:
|
|
180
|
+
|
|
181
|
+
```powershell
|
|
182
|
+
git push -u origin <branch-name>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If the pre-push hook fails:
|
|
186
|
+
|
|
187
|
+
1. Read the failure output.
|
|
188
|
+
2. Fix the offending baseline / test / lint issue in the working tree.
|
|
189
|
+
3. `git add -A`, then create a **new follow-up commit** (do not amend a commit
|
|
190
|
+
that has already been pushed; amending an unpushed commit is fine).
|
|
191
|
+
4. Re-run the push. Never bypass the hook with `--no-verify`.
|
|
192
|
+
|
|
193
|
+
If the push is rejected because the remote has work you do not have locally,
|
|
194
|
+
`git pull --rebase`, resolve conflicts, and push again.
|
|
195
|
+
|
|
196
|
+
**If the level is `push`, stop here** and print the push summary.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Step 5 — Open PR (pr level)
|
|
201
|
+
|
|
202
|
+
```powershell
|
|
203
|
+
gh pr create --base <BASE_BRANCH> --head <branch-name> \
|
|
204
|
+
--title "<subject>" --body "<body-or-default>"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
When the body would otherwise be empty, fall back to a single line:
|
|
208
|
+
`Opened via /git-deliver`. Pass `--draft` to `gh pr create` when the operator
|
|
209
|
+
set `--draft`. Capture the PR URL from stdout for the summary.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Step 6 — Arm Auto-Merge (pr level, default)
|
|
214
|
+
|
|
215
|
+
Skip when `--draft` or `--no-auto-merge` is set.
|
|
216
|
+
|
|
217
|
+
```powershell
|
|
218
|
+
gh pr merge <PR_NUMBER> --auto --squash --delete-branch
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This queues the PR to merge as soon as required checks turn green and schedules
|
|
222
|
+
head-branch deletion on merge. Auto-merge requires `allow_auto_merge: true` on
|
|
223
|
+
the repo. If `gh pr merge --auto` fails (missing repo feature, insufficient
|
|
224
|
+
token scope), log the failure and surface it — the PR stays open and mergeable
|
|
225
|
+
through the GitHub UI.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Step 7 — Summary
|
|
230
|
+
|
|
231
|
+
Print a single block matched to the level that ran:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
# commit level
|
|
235
|
+
✅ Committed on <branch>: <subject>
|
|
236
|
+
|
|
237
|
+
# push level
|
|
238
|
+
✅ Committed + pushed <branch> → origin: <subject>
|
|
239
|
+
|
|
240
|
+
# pr level
|
|
241
|
+
✅ Opened PR #<PR_NUMBER>: <subject>
|
|
242
|
+
<PR_URL>
|
|
243
|
+
branch: <branch-name> → <BASE_BRANCH>
|
|
244
|
+
auto-merge: <enabled | draft | disabled>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Do **not** poll CI. That is the `/deliver` Phase 7 job and is overkill for
|
|
248
|
+
ad-hoc changes. The operator (or GitHub's email notification) is the next
|
|
249
|
+
watcher.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Troubleshooting
|
|
254
|
+
|
|
255
|
+
- **Hook failures**: Read the output, fix the underlying issue, never
|
|
256
|
+
`--no-verify`. The pre-push hook (lint + format + maintainability + audit +
|
|
257
|
+
coverage + CRAP) is the same gate every PR has to pass eventually; failing
|
|
258
|
+
here lets you fix it before opening the PR rather than after CI fails.
|
|
259
|
+
- **Branch already exists locally**: appended `-2`/`-3` per Step 3; pass
|
|
260
|
+
`--branch <name>` for a specific name.
|
|
261
|
+
- **`gh pr create` fails with "no commits between branches"**: the push did not
|
|
262
|
+
move the branch (e.g. it was already at the same SHA as `[BASE_BRANCH]`).
|
|
263
|
+
Verify `git log <BASE_BRANCH>..HEAD` shows commits before re-running.
|
|
264
|
+
- **PR template wins over `--body`**: if `.github/pull_request_template.md`
|
|
265
|
+
exists, `gh pr create --body` overrides it. For ad-hoc PRs the explicit body
|
|
266
|
+
is the right default.
|
|
267
|
+
- **Auto-merge does not fire after CI green**: confirm the PR's required checks
|
|
268
|
+
match the auto-merge requirements. The framework's quality gate
|
|
269
|
+
(`Validate and Test`) is the canonical required check.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Constraint
|
|
274
|
+
|
|
275
|
+
- **Never** push directly to `[BASE_BRANCH]`. At the pr level Step 3's branch
|
|
276
|
+
cut is mandatory in from-base mode; remove it and the workflow becomes a
|
|
277
|
+
silent bypass of the PR-required policy.
|
|
278
|
+
- **Never** pass `--no-verify` to `git commit` or `git push` to bypass the
|
|
279
|
+
quality gate. Fix the failure at the source.
|
|
280
|
+
- **Never** force-push from `/git-deliver`. This workflow opens new PRs, it
|
|
281
|
+
does not rewrite history. Force-pushes belong to `/git-merge-pr` (with
|
|
282
|
+
`--force-with-lease` after a rebase) and `/deliver` Phase 7.
|
|
283
|
+
- **Always** prefer `--auto --squash --delete-branch` at the pr level unless
|
|
284
|
+
the operator opts out, so `main`'s commit history stays uniform across the
|
|
285
|
+
`/git-deliver` and `/deliver` surfaces.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## ⚠️ Parallel Story Execution
|
|
290
|
+
|
|
291
|
+
Do **not** use this workflow from inside a parallel story-execution context
|
|
292
|
+
(`/deliver #<storyId>`, `/deliver` wave dispatch). `git add -A` sweeps any
|
|
293
|
+
untracked files in the working tree, which in a shared working directory may
|
|
294
|
+
belong to another agent. In those contexts stage explicit paths only and
|
|
295
|
+
confirm `git branch --show-current` reports the expected `story-<id>` branch
|
|
296
|
+
before committing — see
|
|
297
|
+
[`helpers/worktree-lifecycle.md`](helpers/worktree-lifecycle.md) for the
|
|
298
|
+
shared-tree hazard and the worktree-isolation model that contains it.
|