@outerlayer/cli 0.12.0 → 0.14.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/CHANGELOG.md +25 -0
- package/README.md +9 -4
- package/dist/agent-setup-CN2HUBEJ.js +26 -0
- package/dist/{build-LELVCWHV.js → build-BG2YENGK.js} +2 -2
- package/dist/build-info.json +1 -1
- package/dist/{builder-OF67QKDS.js → builder-2D3WSOOY.js} +4 -4
- package/dist/{check-RJOJU2WA.js → check-SRGR36KI.js} +37 -37
- package/dist/{chunk-XR3BJN33.js → chunk-22RMZBGW.js} +20 -1
- package/dist/{chunk-AHZQBGD2.js → chunk-2M63GG56.js} +1 -1
- package/dist/{chunk-SZOVKFWB.js → chunk-3NLOVYJQ.js} +4 -4
- package/dist/{chunk-USO2DKBD.js → chunk-4KSQKQXE.js} +8 -2
- package/dist/{chunk-QEESCZ26.js → chunk-4NPZVMJU.js} +2 -2
- package/dist/{chunk-DVT73QJO.js → chunk-4OGPT6I2.js} +14 -14
- package/dist/{chunk-ZQUCTFXC.js → chunk-FOJGZYFC.js} +11 -5
- package/dist/{chunk-G5XY6CVG.js → chunk-HFKKFTXU.js} +12 -16
- package/dist/{chunk-ANW7VHMH.js → chunk-HFKQIZXL.js} +95 -16
- package/dist/{chunk-325272HM.js → chunk-J5M7BSJQ.js} +11 -4
- package/dist/{chunk-NPXQXYVA.js → chunk-JJEGKEFV.js} +6 -8
- package/dist/{chunk-3ZPYPFNM.js → chunk-JXYVA45C.js} +1 -1
- package/dist/{chunk-IBB4D4KE.js → chunk-JYIEHQIF.js} +1 -1
- package/dist/{chunk-5TGQHOUX.js → chunk-KFRM23WD.js} +3 -3
- package/dist/{chunk-GGMRZPEA.js → chunk-KMDLFI4P.js} +1 -1
- package/dist/{chunk-G5IEVZGI.js → chunk-LCEXHCHD.js} +1 -1
- package/dist/{chunk-27Y7AX2O.js → chunk-MSASO5O4.js} +44 -8
- package/dist/chunk-MTF3PHVZ.js +161 -0
- package/dist/{chunk-G4RXNF2L.js → chunk-NHDVD7DU.js} +12 -12
- package/dist/{chunk-GATMPAMK.js → chunk-NPQWKGUA.js} +2 -2
- package/dist/{chunk-YEQL54G5.js → chunk-PQUM37BK.js} +1 -1
- package/dist/{chunk-GEBQXUFY.js → chunk-Q2UK3C4P.js} +24 -10
- package/dist/{chunk-SKHN72JO.js → chunk-RBKIH5ST.js} +13 -13
- package/dist/{chunk-2KLBKHT4.js → chunk-RLYZLUQ7.js} +1 -1
- package/dist/chunk-RMWLBUCJ.js +50 -0
- package/dist/{chunk-PDRYXERC.js → chunk-SMHOCDCI.js} +5 -5
- package/dist/{chunk-6FH2AHJY.js → chunk-SWHQ4LUM.js} +30 -17
- package/dist/{agent-setup-52LRZ6AY.js → chunk-SXWS25JC.js} +8 -21
- package/dist/{chunk-VK25O2ZM.js → chunk-T5VPP6H7.js} +1 -1
- package/dist/{chunk-JOXYKGUV.js → chunk-WUYTWNMA.js} +4 -2
- package/dist/{chunk-A56PYPDF.js → chunk-XCK2YAY5.js} +14 -4
- package/dist/{chunk-C7N4TDHW.js → chunk-YDSMROTB.js} +5 -5
- package/dist/{chunk-FO7KKPYG.js → chunk-ZA5CHAFY.js} +2 -2
- package/dist/{cli-JSSRGA6V.js → cli-UW2H3UUW.js} +138 -85
- package/dist/{command-3WYZ2ML5.js → command-5LD7G2QQ.js} +27 -27
- package/dist/{config-MKBX2ZQD.js → config-N3C6DFQA.js} +1 -1
- package/dist/{connect-cmd-A5NGODCJ.js → connect-cmd-JUAW6MU4.js} +5 -5
- package/dist/{context-adopt-BAHGXJZV.js → context-adopt-PJUMW2EZ.js} +6 -6
- package/dist/{context-materialize-45OVRRK2.js → context-materialize-FDHYKNDT.js} +6 -6
- package/dist/{dist-EQDW7MZW.js → dist-LKDWQWZ7.js} +2 -2
- package/dist/{docker-VGWEJWI7.js → docker-4J77Q2BF.js} +5 -5
- package/dist/{doctor-4FFXTQJ2.js → doctor-H7REBSLQ.js} +29 -29
- package/dist/{emit-artifact-cmd-QS7XNKBT.js → emit-artifact-cmd-FOGNJXVR.js} +18 -21
- package/dist/emit-cmd-ZEYWLC2C.js +11 -0
- package/dist/{emit-criteria-cmd-XN6AKA53.js → emit-criteria-cmd-HBP6XS5C.js} +8 -11
- package/dist/{emit-finding-cmd-SQJSEHYS.js → emit-finding-cmd-DLI4MKMN.js} +8 -11
- package/dist/{emit-result-cmd-56RHPHOA.js → emit-result-cmd-MUSL7GQA.js} +12 -15
- package/dist/{hook-fast-HLESEJAL.js → hook-fast-WYQD6D7T.js} +1 -1
- package/dist/{import-capture-cmd-NHMAFDM6.js → import-capture-cmd-6QVCOIHK.js} +10 -13
- package/dist/{import-ruler-cmd-UYB32YUT.js → import-ruler-cmd-KLRGRBCP.js} +7 -10
- package/dist/index.js +3 -3
- package/dist/{init-UZKD4RY7.js → init-NTXA3SN7.js} +28 -28
- package/dist/{init-6AY5T3P6.js → init-UBXK6MF7.js} +47 -37
- package/dist/{init-cmd-XGSX2BCC.js → init-cmd-NARDUPIG.js} +20 -13
- package/dist/init-update-O5YYFGF4.js +165 -0
- package/dist/{install-VXXI7UR6.js → install-HDBIPHTX.js} +2 -2
- package/dist/{login-browser-7XGSV7MA.js → login-browser-QZCS3WW6.js} +2 -1
- package/dist/{loop-4NMI2USY.js → loop-SW2M5VDD.js} +37 -37
- package/dist/{machine-4JX2D4TS.js → machine-U66FKVDP.js} +23 -23
- package/dist/mcp-install-cmd-FDTPQQS6.js +9 -0
- package/dist/{runner-env-WAXTJXZK.js → runner-env-XTC53CHN.js} +3 -3
- package/dist/runner-key-3JSMDRJA.js +154 -0
- package/dist/{session-sender-T7PN4YRC.js → session-sender-4M247XSV.js} +7 -7
- package/dist/starter-pack-IB4UUIX3.js +11 -0
- package/dist/{status-OXBIRWJ7.js → status-STZYPU3W.js} +30 -30
- package/dist/{sync-cmd-EVXNR47N.js → sync-cmd-UWD5JOFU.js} +6 -6
- package/dist/{watch-7NSFOAL5.js → watch-DBNB7R6X.js} +6 -6
- package/dist/{work-claim-cmd-KR7UR2AB.js → work-claim-cmd-LY4U6QHN.js} +13 -13
- package/dist/{work-cmd-JPM37MLF.js → work-cmd-RVRQ32EH.js} +7 -7
- package/dist/{work-comment-cmd-3DC6BC34.js → work-comment-cmd-FFUM4TJU.js} +10 -13
- package/dist/{work-open-pr-cmd-XZQRJBNZ.js → work-open-pr-cmd-HLDWP4T2.js} +10 -13
- package/dist/{work-pr-cmd-WHGZOP22.js → work-pr-cmd-L77K32WK.js} +7 -7
- package/package.json +1 -1
- package/skill-pack/maintained/emitting-evidence/SKILL.md +8 -2
- package/skill-pack/shipped.json +70 -0
- package/skill-pack/template/build/SKILL.md +66 -35
- package/skill-pack/template/build/references/agent-briefs.md +4 -2
- package/skill-pack/template/build/references/release.md +36 -8
- package/skill-pack/template/build/references/review-loop.md +6 -2
- package/skill-pack/template/build/scripts/record-criteria.mjs +109 -143
- package/skill-pack/template/spec/SKILL.md +33 -22
- package/skill-pack/template/writing-specs/SKILL.md +36 -3
- package/skill-pack/template/writing-specs/references/agents-snippet.md +57 -0
- package/skill-pack/template/writing-tests/SKILL.md +117 -0
- package/skill-pack/validators/acceptance-criteria.yaml +35 -0
- package/skill-pack/validators/code-review.yaml +4 -2
- package/dist/chunk-T6OIJQRT.js +0 -31
- package/dist/emit-cmd-LXEIDKLT.js +0 -10
- package/dist/mcp-install-cmd-XZARXNYT.js +0 -9
- package/dist/starter-pack-Z22VRFPF.js +0 -11
- package/dist/{chunk-3FMPZPJQ.js → chunk-R7PF5OOU.js} +1 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Records the criteria the build read from the issue, or drafted for an issue
|
|
3
|
+
// that has none, and notes the outcome in the build's state file. The agent
|
|
4
|
+
// reads the issue and writes the criteria file; this script checks it, gives
|
|
5
|
+
// the criteria ids, runs `outerlayer emit criteria`, and handles its answer.
|
|
6
|
+
// Needs only Node.
|
|
5
7
|
//
|
|
6
|
-
// node record-criteria.mjs --item <number> --
|
|
8
|
+
// node record-criteria.mjs --item <number> --criteria-file <path> [--drafted]
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// the
|
|
10
|
+
// The criteria file is `{ "criteria": [{ "id"?, "text", "proof"? }] }`, in
|
|
11
|
+
// the order the issue lists them. The ids are kept only when every criterion
|
|
12
|
+
// carries one that fits the id grammar; otherwise every criterion is numbered
|
|
13
|
+
// `AC-<item>-NN` in order. A drafted list is always numbered and is recorded
|
|
14
|
+
// with `drafted: true`. An empty list records nothing. The script never edits
|
|
15
|
+
// the issue, and never replaces a list the item already has (the gateway
|
|
16
|
+
// answers 409 and the item's list stands).
|
|
11
17
|
import { spawnSync } from "node:child_process";
|
|
12
18
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
19
|
import { dirname, join } from "node:path";
|
|
@@ -15,118 +21,64 @@ import { pathToFileURL } from "node:url";
|
|
|
15
21
|
|
|
16
22
|
/** Mirrors ARTIFACT_KINDS in @outerlayer/session-schema (a test pins them equal); this script imports no packages. */
|
|
17
23
|
export const PROOF_KINDS = ["video", "screenshot", "report", "log", "test", "file"];
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
24
|
+
/** Mirrors ArtifactCriterionIdSchema in @outerlayer/session-schema (a test pins them equal). */
|
|
25
|
+
export const CRITERION_ID = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
26
|
+
/** Mirrors CHECK_BOUND_ARTIFACT_IDS in @outerlayer/session-schema (a test pins them equal): ids that name a check, not a criterion. */
|
|
27
|
+
export const RESERVED_IDS = ["code-review-ran", "acceptance-criteria"];
|
|
28
|
+
const PROOF_IN_TEXT = /\(proof:\s*([a-z]+)\s*\)/i;
|
|
22
29
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
let fence = null;
|
|
26
|
-
return lines.map((line) => {
|
|
27
|
-
const opener = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
|
28
|
-
if (fence === null) {
|
|
29
|
-
if (opener === null) return line;
|
|
30
|
-
fence = opener[1];
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
if (opener !== null && opener[1][0] === fence[0] && opener[1].length >= fence.length) fence = null;
|
|
34
|
-
return "";
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** A bullet and the indented lines continuing it, as one string. */
|
|
39
|
-
function collectItems(lines, start) {
|
|
40
|
-
const items = [];
|
|
41
|
-
let i = start;
|
|
42
|
-
for (; i < lines.length; i++) {
|
|
43
|
-
const line = lines[i];
|
|
44
|
-
const bullet = BULLET.exec(line);
|
|
45
|
-
if (bullet) {
|
|
46
|
-
items.push(bullet[1].trim());
|
|
47
|
-
} else if (line.trim() === "") {
|
|
48
|
-
// a blank line ends the list unless another bullet follows
|
|
49
|
-
const next = lines.slice(i + 1).find((l) => l.trim() !== "");
|
|
50
|
-
if (next === undefined || !BULLET.test(next)) break;
|
|
51
|
-
} else if (/^\s+\S/.test(line) && items.length > 0) {
|
|
52
|
-
items[items.length - 1] += ` ${line.trim()}`;
|
|
53
|
-
} else {
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return items;
|
|
58
|
-
}
|
|
30
|
+
/** A criteria file the agent must fix before anything is recorded. */
|
|
31
|
+
export class CriteriaFileError extends Error {}
|
|
59
32
|
|
|
60
|
-
|
|
61
|
-
|
|
33
|
+
/** The proof a criterion declares: the file's `proof` when it is an artifact
|
|
34
|
+
* kind, else a `(proof: <kind>)` written in its text, else `null`. */
|
|
35
|
+
function proofOf(entry) {
|
|
36
|
+
if (PROOF_KINDS.includes(entry.proof)) return entry.proof;
|
|
37
|
+
const written = PROOF_IN_TEXT.exec(entry.text)?.[1]?.toLowerCase();
|
|
38
|
+
return PROOF_KINDS.includes(written) ? written : null;
|
|
62
39
|
}
|
|
63
40
|
|
|
64
|
-
/**
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (bullet) items.push(bullet[1].trim());
|
|
77
|
-
else if (/^\s+\S/.test(lines[i]) && items.length > 0) items[items.length - 1] += ` ${lines[i].trim()}`;
|
|
78
|
-
}
|
|
79
|
-
if (items.length === 0) return null;
|
|
80
|
-
const parsed = items.map((item) => ID_ITEM.exec(item));
|
|
81
|
-
const withId = parsed.filter((m) => m !== null && m[2].trim() !== "").length;
|
|
82
|
-
if (withId === 0) return null;
|
|
83
|
-
if (withId < parsed.length) return { mixed: true };
|
|
84
|
-
return {
|
|
85
|
-
criteria: parsed.map((m) => {
|
|
86
|
-
const kind = /\(proof:\s*([a-z]+)\s*\)/i.exec(m[2])?.[1]?.toLowerCase();
|
|
87
|
-
return { id: m[1], text: m[2].trim(), proof: PROOF_KINDS.includes(kind) ? kind : null };
|
|
88
|
-
}),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** The plain shape: an Acceptance / Acceptance criteria / Done when label, then a bullet list. */
|
|
93
|
-
function readLabelledBullets(lines) {
|
|
94
|
-
for (let i = 0; i < lines.length; i++) {
|
|
95
|
-
if (!LABEL.test(lines[i])) continue;
|
|
96
|
-
let start = i + 1;
|
|
97
|
-
while (start < lines.length && lines[start].trim() === "") start++;
|
|
98
|
-
const items = collectItems(lines, start).map(stripCheckbox).filter((t) => t !== "");
|
|
99
|
-
if (items.length > 0) return items;
|
|
100
|
-
}
|
|
41
|
+
/** Why the written ids cannot be kept, or `null` when they can. */
|
|
42
|
+
function idProblem(entries) {
|
|
43
|
+
const ids = entries.map((entry) => entry.id);
|
|
44
|
+
const missing = ids.filter((id) => typeof id !== "string" || id.trim() === "").length;
|
|
45
|
+
if (missing === ids.length) return "no criterion carried an id";
|
|
46
|
+
if (missing > 0) return "only some criteria carried an id";
|
|
47
|
+
const bad = ids.find((id) => !CRITERION_ID.test(id));
|
|
48
|
+
if (bad !== undefined) return `the id ${JSON.stringify(bad)} does not fit the id grammar`;
|
|
49
|
+
const reserved = ids.find((id) => RESERVED_IDS.includes(id));
|
|
50
|
+
if (reserved !== undefined) return `the id ${JSON.stringify(reserved)} names a check, not a criterion`;
|
|
51
|
+
const repeated = ids.find((id, n) => ids.indexOf(id) !== n);
|
|
52
|
+
if (repeated !== undefined) return `the id ${JSON.stringify(repeated)} appears twice`;
|
|
101
53
|
return null;
|
|
102
54
|
}
|
|
103
55
|
|
|
104
56
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
57
|
+
* Turns the agent's criteria file into the list `outerlayer emit criteria`
|
|
58
|
+
* takes. Throws {@link CriteriaFileError} for a file the agent must fix.
|
|
59
|
+
* @returns {{criteria: Array<{id:string,text:string,proof:string|null}>, numbered: boolean, numberedBecause?: string}}
|
|
107
60
|
*/
|
|
108
|
-
export function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (ids?.criteria) return { shape: "ids", criteria: ids.criteria };
|
|
112
|
-
const bullets = readLabelledBullets(lines);
|
|
113
|
-
if (ids?.mixed && bullets) {
|
|
114
|
-
return {
|
|
115
|
-
shape: "none",
|
|
116
|
-
criteria: [],
|
|
117
|
-
note: "some criteria carry an id and some do not; recording nothing rather than renumbering the written ids",
|
|
118
|
-
};
|
|
61
|
+
export function readCriteria(input, item, { drafted = false } = {}) {
|
|
62
|
+
if (typeof input !== "object" || input === null || !Array.isArray(input.criteria)) {
|
|
63
|
+
throw new CriteriaFileError('the criteria file must be { "criteria": [ ... ] }');
|
|
119
64
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
65
|
+
const entries = input.criteria.map((entry, n) => {
|
|
66
|
+
if (typeof entry !== "object" || entry === null || typeof entry.text !== "string" || entry.text.trim() === "") {
|
|
67
|
+
throw new CriteriaFileError(`criteria[${n}] needs the criterion's text`);
|
|
68
|
+
}
|
|
69
|
+
return { id: entry.id, text: entry.text.trim(), proof: entry.proof };
|
|
70
|
+
});
|
|
71
|
+
const problem = drafted ? "the build drafted them" : idProblem(entries);
|
|
72
|
+
const numbered = entries.length > 0 && problem !== null;
|
|
73
|
+
const criteria = entries.map((entry, n) => ({
|
|
74
|
+
id: numbered ? `AC-${item}-${String(n + 1).padStart(2, "0")}` : entry.id,
|
|
75
|
+
text: entry.text,
|
|
76
|
+
proof: proofOf(entry),
|
|
77
|
+
}));
|
|
78
|
+
return numbered ? { criteria, numbered, numberedBecause: problem } : { criteria, numbered };
|
|
127
79
|
}
|
|
128
80
|
|
|
129
|
-
/** Sorts a failed `emit criteria` into "the item already has a
|
|
81
|
+
/** Sorts a failed `emit criteria` into "the item already has a list" and any other refusal. */
|
|
130
82
|
export function classifyEmitFailure(message) {
|
|
131
83
|
// Only the CLI's replace refusal reads "refused (409):". Other answers that
|
|
132
84
|
// carry a 409, such as a lost race, read "emit failed (409):".
|
|
@@ -153,40 +105,39 @@ function readState(path) {
|
|
|
153
105
|
}
|
|
154
106
|
|
|
155
107
|
/**
|
|
156
|
-
* Records the
|
|
157
|
-
* `emit(file)` runs `outerlayer emit criteria
|
|
158
|
-
* `{ ok: true }` or `{ ok: false, message }`.
|
|
108
|
+
* Records the criteria and notes the outcome in the state file. `input` is
|
|
109
|
+
* the parsed criteria file. `emit(file)` runs `outerlayer emit criteria
|
|
110
|
+
* <file>` and returns `{ ok: true }` or `{ ok: false, message }`.
|
|
159
111
|
*/
|
|
160
|
-
export async function recordCriteria({
|
|
161
|
-
const
|
|
112
|
+
export async function recordCriteria({ input, item, drafted = false, reportsDir, statePath, emit }) {
|
|
113
|
+
const read = readCriteria(input, item, { drafted });
|
|
114
|
+
const numbering = read.numbered ? { numbered: true, numberedBecause: read.numberedBecause } : { numbered: false };
|
|
162
115
|
let outcome;
|
|
163
|
-
if (
|
|
164
|
-
outcome = { source: "none", recorded: false, count: 0, list: []
|
|
116
|
+
if (read.criteria.length === 0) {
|
|
117
|
+
outcome = { source: "none", recorded: false, count: 0, list: [] };
|
|
165
118
|
} else {
|
|
166
119
|
mkdirSync(reportsDir, { recursive: true });
|
|
167
120
|
const file = join(reportsDir, "criteria.json");
|
|
168
|
-
|
|
121
|
+
const list = drafted ? { criteria: read.criteria, drafted: true } : { criteria: read.criteria };
|
|
122
|
+
writeFileSync(file, `${JSON.stringify(list, null, 2)}\n`);
|
|
169
123
|
const result = await emit(file);
|
|
170
|
-
const
|
|
124
|
+
const source = drafted ? "drafted" : "issue";
|
|
125
|
+
const base = { count: read.criteria.length, list: read.criteria, ...numbering };
|
|
171
126
|
if (result.ok) {
|
|
172
|
-
outcome = { source
|
|
127
|
+
outcome = { source, recorded: true, ...base };
|
|
173
128
|
} else if (classifyEmitFailure(result.message) === "item-list-stands") {
|
|
174
|
-
// No command returns the item's own list, so `list` stays empty; the
|
|
129
|
+
// No command returns the item's own list, so `list` stays empty; the build's reading goes under `issueReading`.
|
|
175
130
|
outcome = {
|
|
176
131
|
source: "item",
|
|
177
132
|
recorded: false,
|
|
178
|
-
count:
|
|
133
|
+
count: read.criteria.length,
|
|
179
134
|
list: [],
|
|
180
|
-
issueReading:
|
|
135
|
+
issueReading: read.criteria,
|
|
136
|
+
...numbering,
|
|
181
137
|
note: "the item already has a recorded list; it stands",
|
|
182
138
|
};
|
|
183
139
|
} else {
|
|
184
|
-
outcome = {
|
|
185
|
-
source: found.shape === "ids" ? "issue-ids" : "issue-bullets",
|
|
186
|
-
recorded: false,
|
|
187
|
-
...base,
|
|
188
|
-
refusal: result.message,
|
|
189
|
-
};
|
|
140
|
+
outcome = { source, recorded: false, ...base, refusal: result.message };
|
|
190
141
|
}
|
|
191
142
|
}
|
|
192
143
|
mkdirSync(dirname(statePath), { recursive: true });
|
|
@@ -197,12 +148,13 @@ export async function recordCriteria({ body, item, reportsDir, statePath, emit }
|
|
|
197
148
|
export function summarize(outcome) {
|
|
198
149
|
const n = outcome.count;
|
|
199
150
|
const noun = n === 1 ? "criterion" : "criteria";
|
|
200
|
-
|
|
201
|
-
if (outcome.source === "
|
|
202
|
-
|
|
151
|
+
const ids = outcome.numbered ? `, numbered AC-<item>-NN because ${outcome.numberedBecause}` : ", with the issue's own ids";
|
|
152
|
+
if (outcome.source === "none") return "no criteria in the file; none recorded";
|
|
153
|
+
if (outcome.source === "item") return `the item already has a recorded list; it stands, and the ${n} ${noun} read here were not recorded`;
|
|
154
|
+
const what = outcome.source === "drafted" ? `drafted ${n} ${noun} from the issue's description` : `read ${n} ${noun} from the issue`;
|
|
203
155
|
return outcome.recorded
|
|
204
|
-
?
|
|
205
|
-
:
|
|
156
|
+
? `${what} and recorded them${outcome.source === "drafted" ? " as drafted" : ids}`
|
|
157
|
+
: `${what} but the record was refused: ${outcome.refusal}`;
|
|
206
158
|
}
|
|
207
159
|
|
|
208
160
|
async function main(argv) {
|
|
@@ -211,9 +163,16 @@ async function main(argv) {
|
|
|
211
163
|
return at === -1 ? undefined : argv[at + 1];
|
|
212
164
|
};
|
|
213
165
|
const item = arg("--item");
|
|
214
|
-
const
|
|
215
|
-
if (!item || !/^\d+$/.test(item) || !
|
|
216
|
-
process.stderr.write("usage: record-criteria.mjs --item <number> --
|
|
166
|
+
const criteriaFile = arg("--criteria-file");
|
|
167
|
+
if (!item || !/^\d+$/.test(item) || !criteriaFile) {
|
|
168
|
+
process.stderr.write("usage: record-criteria.mjs --item <number> --criteria-file <path> [--drafted]\n");
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
let input;
|
|
172
|
+
try {
|
|
173
|
+
input = JSON.parse(readFileSync(criteriaFile, "utf8"));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
process.stderr.write(`cannot read the criteria file "${criteriaFile}": ${err.message}\n`);
|
|
217
176
|
return 2;
|
|
218
177
|
}
|
|
219
178
|
const git = spawnSync("git", ["rev-parse", "--absolute-git-dir"], { encoding: "utf8" });
|
|
@@ -222,15 +181,22 @@ async function main(argv) {
|
|
|
222
181
|
return 2;
|
|
223
182
|
}
|
|
224
183
|
const gitDir = git.stdout.trim();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
184
|
+
try {
|
|
185
|
+
const outcome = await recordCriteria({
|
|
186
|
+
input,
|
|
187
|
+
item,
|
|
188
|
+
drafted: argv.includes("--drafted"),
|
|
189
|
+
reportsDir: join(gitDir, "build-reports"),
|
|
190
|
+
statePath: join(gitDir, "build-state.json"),
|
|
191
|
+
emit: makeEmit(spawnSync, item),
|
|
192
|
+
});
|
|
193
|
+
process.stdout.write(`${summarize(outcome)}\n`);
|
|
194
|
+
return 0;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (!(err instanceof CriteriaFileError)) throw err;
|
|
197
|
+
process.stderr.write(`${err.message}; fix the criteria file and run again\n`);
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
234
200
|
}
|
|
235
201
|
|
|
236
202
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = await main(process.argv.slice(2));
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
criteria with their proof forms,
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
Turns an idea into an approved issue: searches for duplicates, drafts
|
|
5
|
+
acceptance criteria with their proof forms, decides the docs and structural
|
|
6
|
+
verdicts, splits work that is too big for one story, and files it after you
|
|
7
|
+
approve it. Then offers to start the build. Use when you want to write up a
|
|
8
|
+
feature or change before building it. Invoke with
|
|
8
9
|
/spec <what you want built>.
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
Take one idea from a sentence to an issue a person can build from. This file
|
|
12
13
|
is the procedure. The content discipline is the `writing-specs` skill: summary
|
|
13
|
-
block, problem before solution, non-goals
|
|
14
|
-
it, and do not restate it here.
|
|
14
|
+
block, problem before solution, non-goals, Given/When/Then criteria and the
|
|
15
|
+
two verdicts. Follow it, and do not restate it here.
|
|
15
16
|
|
|
16
17
|
This skill files the issue. It does not branch, write code or run tests.
|
|
17
18
|
`/build` does that, on a work item that names the issue.
|
|
@@ -41,31 +42,41 @@ Run them in order. Do not skip the approval.
|
|
|
41
42
|
only where nothing visible changes. A criterion nobody can prove is a
|
|
42
43
|
criterion nobody can build to, so give every one either a proof form or a
|
|
43
44
|
named test.
|
|
44
|
-
3. **
|
|
45
|
+
3. **Verdicts.** Decide the docs verdict and the structural verdict as
|
|
46
|
+
`writing-specs` defines them. Give each a one-line reason. A docs-required
|
|
47
|
+
verdict adds a criterion naming the page to change or add and its proof
|
|
48
|
+
form. Record both verdicts in the issue body, so whoever builds it can
|
|
49
|
+
compare them with the finished change.
|
|
50
|
+
4. **Size.** More than twenty-four criteria is not one story. Above that, show
|
|
45
51
|
the work already split into slices that each stand alone. Slice one carries
|
|
46
52
|
the criteria the others depend on, and the rest become their own specs. The
|
|
47
53
|
user may redraw the split. Skipping it is not on offer.
|
|
48
|
-
|
|
54
|
+
5. **Reader pass.** Send the title and body through one cold rewrite agent. Its
|
|
49
55
|
only inputs are the draft and one writing rule: write for a reader who was
|
|
50
56
|
not in this conversation, lead with the outcome, keep sentences under about
|
|
51
|
-
twenty words, and use plain words.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
twenty words, and use plain words. When the instructions file carries the
|
|
58
|
+
writing rules from the `writing-specs` skill's
|
|
59
|
+
`references/agents-snippet.md`, pass that section verbatim instead. The
|
|
60
|
+
agent never sees this conversation. Criteria keep their exact wording.
|
|
61
|
+
Check that every path, identifier, number and link survived, and restore
|
|
62
|
+
anything the rewrite dropped.
|
|
63
|
+
6. **Checkpoint.** Show the drafted title and body, the criteria with their
|
|
64
|
+
proof forms, and both verdicts with their reasons. Wait for explicit
|
|
65
|
+
approval. Before showing them, check every criterion: it has an id, a
|
|
66
|
+
Given, When and Then, and a proof form or a named test. Check that both
|
|
67
|
+
verdicts are there, each with its reason. A criterion that fails the check
|
|
68
|
+
goes back to step 2, and a missing verdict to step 3. Apply requested
|
|
58
69
|
edits, run the reader pass again over the edited text, and show it again
|
|
59
|
-
until approved. Nothing is filed before this point. A spec the user
|
|
60
|
-
leaves no issue behind.
|
|
61
|
-
|
|
62
|
-
and full criteria. Then file the slices from step
|
|
70
|
+
until approved. Nothing is filed before this point. A spec the user
|
|
71
|
+
rejects leaves no issue behind.
|
|
72
|
+
7. **File it.** Through the team's tracker tool, file the approved title, body
|
|
73
|
+
and full criteria. Then file the slices from step 4, and only the ones the
|
|
63
74
|
user named. The issue is the source of truth from here on. Note its
|
|
64
75
|
reference exactly as the tracker gives it: a number, or a Linear or Jira key.
|
|
65
|
-
|
|
76
|
+
8. **Ask whether to build.** Ask the user exactly one question: build it now, or
|
|
66
77
|
not yet. Never start work unasked, because the user may want to hold the
|
|
67
78
|
issue.
|
|
68
|
-
|
|
79
|
+
9. **Start the build.** On a yes, run `outerlayer work build --issue <reference>`,
|
|
69
80
|
where the reference is the issue number or the Linear or Jira key exactly as
|
|
70
81
|
the user typed it. Print the item number the command reports and the launch
|
|
71
82
|
line:
|
|
@@ -80,5 +91,5 @@ Run them in order. Do not skip the approval.
|
|
|
80
91
|
|
|
81
92
|
## Scope limits
|
|
82
93
|
|
|
83
|
-
One `/spec` files one issue, plus the slices the user approves in step
|
|
94
|
+
One `/spec` files one issue, plus the slices the user approves in step 4. It
|
|
84
95
|
files nothing in another repository. It never creates a branch or edits code.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: writing-specs
|
|
3
|
-
description:
|
|
3
|
+
description: Describes how to write a feature spec before implementation begins - problem framing, goals and non-goals, assumptions, acceptance criteria in Given/When/Then, and the docs and structural verdicts. Use when asked to "write a spec", "spec this out", "write a PRD", "draft acceptance criteria", or when shaping the issue for a new feature.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Writing Specs
|
|
@@ -10,8 +10,9 @@ in the issue body: one clean spec, edited in place as the shape is agreed,
|
|
|
10
10
|
never a trail of amendment comments. Its acceptance criteria sit under an
|
|
11
11
|
`## Acceptance criteria` heading. Each criterion has an id, and an id is any
|
|
12
12
|
stable label the team chooses, such as `LOGIN-01` or `AC-12-03`. Pick a scheme
|
|
13
|
-
once and keep to it.
|
|
14
|
-
|
|
13
|
+
once and keep to it. The body also records the docs and structural verdicts,
|
|
14
|
+
each with a one-line reason. Planning scratch is local and disposable. Nothing
|
|
15
|
+
durable belongs there.
|
|
15
16
|
|
|
16
17
|
## Lead with a summary block
|
|
17
18
|
|
|
@@ -104,6 +105,30 @@ implementing. Never write them by reading the finished implementation back.
|
|
|
104
105
|
changes. The proof shows the running page, never a component render. The
|
|
105
106
|
emitting-evidence skill covers capture.
|
|
106
107
|
|
|
108
|
+
## The two verdicts
|
|
109
|
+
|
|
110
|
+
Every feature spec declares two verdicts, each with a one-line reason. They
|
|
111
|
+
are decided when the spec is written, and decided again from the finished
|
|
112
|
+
change before the pull request opens.
|
|
113
|
+
|
|
114
|
+
- **Docs.** `docs-required` when the change alters what a user sees or does.
|
|
115
|
+
Judge it against the repository's own docs: a `README.md`, a `docs/`
|
|
116
|
+
directory, a docs site. A new capability with no page yet is
|
|
117
|
+
`docs-required` too, and its criterion names the page to add. That no page
|
|
118
|
+
covers it is a reason to write one, not a reason to skip. Generated
|
|
119
|
+
reference, such as an API spec rendered as pages, is not a docs surface. A
|
|
120
|
+
new endpoint still needs a guide when using it takes a step the reference
|
|
121
|
+
cannot show, such as a credential to create or calls to make in order. When
|
|
122
|
+
required, the spec carries a criterion naming the page and its proof form.
|
|
123
|
+
- **Structural.** `structural-required` when the change adds a package or an
|
|
124
|
+
app, introduces a seam between components, or moves data across a
|
|
125
|
+
boundary. When in doubt, say required: approval is where it gets argued
|
|
126
|
+
down. A structural change is worth planning before it is implemented.
|
|
127
|
+
|
|
128
|
+
A verdict reversed when the change is finished is not a fault. The gap
|
|
129
|
+
between the two calls is the useful signal, so state it rather than quietly
|
|
130
|
+
adopting the later one.
|
|
131
|
+
|
|
107
132
|
## Honest limits
|
|
108
133
|
|
|
109
134
|
A criterion may carry prose admitting what it does not cover. Useful idioms:
|
|
@@ -132,3 +157,11 @@ the one most often skipped. Re-reading your own spec does not satisfy it. Run it
|
|
|
132
157
|
against the issue body as published, not the draft in the working tree. A spec
|
|
133
158
|
whose design lives in links the reader cannot follow fails this test, and that
|
|
134
159
|
failure is invisible to whoever wrote it.
|
|
160
|
+
|
|
161
|
+
## Writing rules for the instructions file
|
|
162
|
+
|
|
163
|
+
The reader test holds a spec to the rules in
|
|
164
|
+
[references/agents-snippet.md](references/agents-snippet.md): write for a
|
|
165
|
+
reader who was not in the session, and keep session history out of code
|
|
166
|
+
comments. Paste that file into the repository's instructions file, such as
|
|
167
|
+
`AGENTS.md`, to hold every agent's prose to the same rules.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- outerlayer:writing-rules -->
|
|
2
|
+
## Write for a reader who was not in the session
|
|
3
|
+
|
|
4
|
+
This applies to anything a person reads: replies, commit messages, pull
|
|
5
|
+
request titles and bodies, issue bodies, review comments, docs and code
|
|
6
|
+
comments.
|
|
7
|
+
|
|
8
|
+
The reader test: someone who knows this codebase but has never seen this
|
|
9
|
+
session, the skill that ran it, or any label coined while working. If they
|
|
10
|
+
would have to ask what a word means, explain it or cut it. Names made up
|
|
11
|
+
during a session never appear in anything published: reviewer names, round
|
|
12
|
+
labels, finding ids like C1 or N3, stage names, internal agent roles.
|
|
13
|
+
Describe the thing instead.
|
|
14
|
+
|
|
15
|
+
Lead with the outcome. One sentence: what changed, or what you found. Then
|
|
16
|
+
only the detail that changes what the reader does next. Keep exact paths,
|
|
17
|
+
numbers, identifiers and caveats. Cut the packaging around them.
|
|
18
|
+
|
|
19
|
+
A sentence carries one idea and stays under about 20 words. A sentence with
|
|
20
|
+
three clauses becomes three sentences. Break paragraphs often.
|
|
21
|
+
|
|
22
|
+
Say what you mean. Use the literal phrase, not a metaphor or a flourish:
|
|
23
|
+
"a parameter worth varying", not "a dial worth turning". Use everyday words:
|
|
24
|
+
"use", not "leverage"; "start", not "initiate"; "so", not "thereby".
|
|
25
|
+
|
|
26
|
+
Session telemetry, review-round tables, agent counts and raw command output
|
|
27
|
+
belong in the report to the person who asked. They never go in a pull
|
|
28
|
+
request or issue body.
|
|
29
|
+
|
|
30
|
+
Before posting, read the text as that outsider. If they would stall on a
|
|
31
|
+
term or a sentence, rewrite it.
|
|
32
|
+
|
|
33
|
+
## Code comments
|
|
34
|
+
|
|
35
|
+
A comment states a present-tense fact the code cannot express: a why, a
|
|
36
|
+
constraint, a gotcha or an invariant. Everything else is noise that leaks
|
|
37
|
+
the history of how the code was written.
|
|
38
|
+
|
|
39
|
+
Never put these in a comment, a test title or a user-facing string:
|
|
40
|
+
|
|
41
|
+
- Change narration: "previously…", "now uses…", "removed X", "renamed from
|
|
42
|
+
Y". Version control holds what changed. The comment says why the current
|
|
43
|
+
code is the way it is.
|
|
44
|
+
- Issue, pull request or ticket numbers. They mean nothing to a reader
|
|
45
|
+
without access. If the context matters, restate its substance.
|
|
46
|
+
- Project codenames, phase labels and spec section references. Describe
|
|
47
|
+
the behavior instead.
|
|
48
|
+
- Labels from a working session: reviewer names, round labels, finding ids.
|
|
49
|
+
|
|
50
|
+
One exception: an acceptance-criterion id may sit in a comment directly
|
|
51
|
+
above the test that proves it, so a reader can find the test from the
|
|
52
|
+
criterion. Never put it in the test's title.
|
|
53
|
+
|
|
54
|
+
Keep comments that document constraints, invariants, footguns, security
|
|
55
|
+
posture and real rationale, and do not weaken them when editing nearby. If
|
|
56
|
+
a comment narrates a change but carries a real reason, rewrite it as
|
|
57
|
+
present-tense rationale rather than deleting it.
|