@warpgogol/forge 2.21.6 → 2.21.7
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.md +27 -1
- package/os/adr/adr-0000-template.md +8 -0
- package/os/adr/handlers/validate.test.ts +203 -0
- package/os/adr/handlers/validate.ts +54 -1
- package/os/adr/types.ts +7 -0
- package/os/compass/handlers/compass-inventory-handler.ts +11 -1
- package/os/compass/handlers/compass-inventory.ts +10 -0
- package/os/core/handlers/validate.ts +56 -5
- package/os/naming/naming-convention.ts +9 -0
- package/os/plugin/plugin.module.ts +1 -1
- package/os/rfc/acceptance.ts +133 -4
- package/os/rfc/handlers/implement-stamp.ts +14 -1
- package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
- package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
- package/os/rfc/handlers/validate-rules.ts +450 -9
- package/os/rfc/handlers/validate.ts +20 -2
- package/os/rfc/rfc-0000-template.md +24 -8
- package/os/rfc/rfc.module.ts +28 -0
- package/os/rfc/types.ts +72 -6
- package/os/rfc/verification-evidence.ts +5 -4
- package/os/rfc/verification-refresh.test.ts +320 -0
- package/os/rfc/verification-refresh.ts +216 -0
- package/os/session/handlers/save.ts +10 -0
- package/os/spec/spec-validate.test.ts +59 -0
- package/os/spec/spec-validate.ts +6 -4
- package/package.json +2 -1
- package/skills/fo/fo-handoff/SKILL.md +15 -6
- package/skills/fo/fo-idea-audit/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
- package/skills/fo/fo-idea-implement/SKILL.md +3 -2
- package/src/compass/contract-registry.ts +25 -6
- package/src/index.ts +1 -1
- package/src/onboarding/doctor.ts +1 -1
- package/src/registry.ts +1 -1
- package/src/tests/acceptance-probe-kinds.test.ts +262 -0
- package/src/tests/plugin-manifest.test.ts +1 -1
- package/src/tests/session-handlers.test.ts +29 -0
- package/src/types/werkstatt-engine-shims.d.ts +0 -21
- package/src/types/werkstatt-shared-shims.d.ts +68 -147
- /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-0999: re-run acceptance probes for implemented RFCs and update their
|
|
5
|
+
verification evidence envelopes in-place. Preserves emittedAt, adds
|
|
6
|
+
lastRefreshedAt, replaces probes[] with fresh results. Supports --id,
|
|
7
|
+
--all, and --dry-run flags.
|
|
8
|
+
</purpose>
|
|
9
|
+
<non-goals>
|
|
10
|
+
<item>Do not duplicate probe execution — reuse runProbe from acceptance.ts.</item>
|
|
11
|
+
<item>Do not create new envelopes — refresh only updates existing ones.</item>
|
|
12
|
+
<item>Do not run for non-implemented RFCs — only implemented status has evidence.</item>
|
|
13
|
+
</non-goals>
|
|
14
|
+
</MODULE_CONTRACT>
|
|
15
|
+
<CHANGE_SUMMARY>
|
|
16
|
+
<item>RFC-0999: initial implementation.</item>
|
|
17
|
+
</CHANGE_SUMMARY>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFile } from "node:fs/promises";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { performance } from "node:perf_hooks";
|
|
23
|
+
import { parse as yamlParse } from "yaml";
|
|
24
|
+
|
|
25
|
+
import { runProbe } from "./acceptance.ts";
|
|
26
|
+
import { listRfcFiles, readAndParseRfc } from "./frontmatter-io.ts";
|
|
27
|
+
import { writeFileAtomic } from "../../src/utils/fs-atomic.ts";
|
|
28
|
+
import { buildGeneratedHeader } from "../../src/utils/generated-marker.ts";
|
|
29
|
+
import { stringify as yamlStringify } from "yaml";
|
|
30
|
+
import {
|
|
31
|
+
captureGitContext,
|
|
32
|
+
getKernelVersion,
|
|
33
|
+
byteHashHex,
|
|
34
|
+
VERIFICATION_DIR,
|
|
35
|
+
buildEvidenceEnvelope,
|
|
36
|
+
} from "./verification-evidence.ts";
|
|
37
|
+
import { RFC_DIR } from "./types.ts";
|
|
38
|
+
import type {
|
|
39
|
+
AcceptanceProbe,
|
|
40
|
+
RfcStatus,
|
|
41
|
+
VerificationEvidence,
|
|
42
|
+
VerificationEvidenceProbeRecord,
|
|
43
|
+
RfcVerificationRefreshResult,
|
|
44
|
+
} from "./types.ts";
|
|
45
|
+
import type {
|
|
46
|
+
Diagnostic,
|
|
47
|
+
ForgeCommandInput,
|
|
48
|
+
ForgeCommandResult,
|
|
49
|
+
ForgeRuntimeContext,
|
|
50
|
+
} from "../../src/types.ts";
|
|
51
|
+
|
|
52
|
+
function normalizeProbes(probes: AcceptanceProbe[]): string {
|
|
53
|
+
return JSON.stringify(probes, Object.keys(probes[0] ?? {}).sort());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runRfcVerificationRefresh(
|
|
57
|
+
input: ForgeCommandInput,
|
|
58
|
+
context: ForgeRuntimeContext,
|
|
59
|
+
): Promise<ForgeCommandResult<RfcVerificationRefreshResult>> {
|
|
60
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
61
|
+
const rfcDirPath = join(workspaceRoot, RFC_DIR);
|
|
62
|
+
const targetId = input.flags["id"] as string | undefined;
|
|
63
|
+
const allMode = input.flags["all"] === true;
|
|
64
|
+
const dryRun = input.flags["dry-run"] === true;
|
|
65
|
+
|
|
66
|
+
if (!targetId && !allMode) {
|
|
67
|
+
return {
|
|
68
|
+
data: {
|
|
69
|
+
command: "rfc.verification.refresh",
|
|
70
|
+
status: "pass",
|
|
71
|
+
refreshed: [],
|
|
72
|
+
skipped: [],
|
|
73
|
+
diagnostics: [],
|
|
74
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 },
|
|
75
|
+
},
|
|
76
|
+
exitCode: 0,
|
|
77
|
+
summary:
|
|
78
|
+
"rfc.verification.refresh: pass --id <rfc-id> or --all to select target RFC(s)",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allFiles = await listRfcFiles(rfcDirPath);
|
|
83
|
+
const refreshed: RfcVerificationRefreshResult["refreshed"] = [];
|
|
84
|
+
const skipped: RfcVerificationRefreshResult["skipped"] = [];
|
|
85
|
+
const diagnostics: Diagnostic[] = [];
|
|
86
|
+
|
|
87
|
+
const gitContext = await captureGitContext(workspaceRoot);
|
|
88
|
+
const kernelVersion = await getKernelVersion(workspaceRoot);
|
|
89
|
+
|
|
90
|
+
for (const fileName of allFiles) {
|
|
91
|
+
const parsedFile = await readAndParseRfc(rfcDirPath, fileName);
|
|
92
|
+
if (!parsedFile) continue;
|
|
93
|
+
if ("error" in parsedFile) continue;
|
|
94
|
+
const fm = parsedFile.parsed.frontmatter;
|
|
95
|
+
const rfcId = String(fm["id"] ?? "");
|
|
96
|
+
const status = String(fm["status"] ?? "");
|
|
97
|
+
|
|
98
|
+
if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) continue;
|
|
99
|
+
|
|
100
|
+
if (status !== "implemented") {
|
|
101
|
+
skipped.push({ rfcId, reason: "not implemented" });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const slug = rfcId.toLowerCase();
|
|
106
|
+
const evidenceFileName = `${slug}.generated.yaml`;
|
|
107
|
+
const evidenceRelPath = join(VERIFICATION_DIR, evidenceFileName);
|
|
108
|
+
const evidenceAbsPath = join(workspaceRoot, evidenceRelPath);
|
|
109
|
+
|
|
110
|
+
let existingEnvelope: VerificationEvidence | null = null;
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readFile(evidenceAbsPath, "utf-8");
|
|
113
|
+
const parsed = yamlParse(raw) as VerificationEvidence;
|
|
114
|
+
if (parsed && typeof parsed === "object" && parsed.rfcId) {
|
|
115
|
+
existingEnvelope = parsed;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// file doesn't exist or can't be parsed
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!existingEnvelope) {
|
|
122
|
+
skipped.push({ rfcId, reason: "no evidence envelope" });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const acceptance = fm["acceptance"];
|
|
127
|
+
if (!Array.isArray(acceptance) || acceptance.length === 0) {
|
|
128
|
+
skipped.push({ rfcId, reason: "no evidence envelope" });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const probes = acceptance as AcceptanceProbe[];
|
|
133
|
+
const probeRecords: VerificationEvidenceProbeRecord[] = [];
|
|
134
|
+
const rfcFilePath = join(rfcDirPath, fileName);
|
|
135
|
+
const rfcMarkdown = await readFile(rfcFilePath, "utf-8");
|
|
136
|
+
const now = new Date().toISOString();
|
|
137
|
+
|
|
138
|
+
for (const probe of probes) {
|
|
139
|
+
const start = performance.now();
|
|
140
|
+
const result = await runProbe(probe, workspaceRoot, context.commandRegistry);
|
|
141
|
+
const durationMs = Math.round(performance.now() - start);
|
|
142
|
+
probeRecords.push({
|
|
143
|
+
probe,
|
|
144
|
+
ok: result.ok,
|
|
145
|
+
detail: result.detail,
|
|
146
|
+
durationMs,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const envelope = buildEvidenceEnvelope(
|
|
151
|
+
rfcId,
|
|
152
|
+
String(fm["title"] ?? ""),
|
|
153
|
+
String(fm["status"] ?? "") as RfcStatus,
|
|
154
|
+
rfcMarkdown,
|
|
155
|
+
probes,
|
|
156
|
+
probeRecords,
|
|
157
|
+
gitContext,
|
|
158
|
+
kernelVersion,
|
|
159
|
+
existingEnvelope.emittedAt,
|
|
160
|
+
);
|
|
161
|
+
envelope.lastRefreshedAt = now;
|
|
162
|
+
|
|
163
|
+
if (!dryRun) {
|
|
164
|
+
const yamlContent = `${buildGeneratedHeader({ filePath: evidenceRelPath, ownerCommand: "rfc.verification.refresh" })}${yamlStringify(envelope)}\n`;
|
|
165
|
+
await writeFileAtomic(evidenceAbsPath, yamlContent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const probesFailed = probeRecords.filter((r) => !r.ok).length;
|
|
169
|
+
refreshed.push({
|
|
170
|
+
rfcId,
|
|
171
|
+
file: evidenceRelPath,
|
|
172
|
+
overall: envelope.overall,
|
|
173
|
+
probesTotal: probeRecords.length,
|
|
174
|
+
probesFailed,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (envelope.overall === "fail") {
|
|
178
|
+
diagnostics.push({
|
|
179
|
+
ruleId: "RFC-REFRESH-01",
|
|
180
|
+
severity: "error",
|
|
181
|
+
file: evidenceRelPath,
|
|
182
|
+
message: `${rfcId}: refresh overall is "fail" — ${probesFailed}/${probeRecords.length} probe(s) failed.`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (outputFormat === "pretty") {
|
|
187
|
+
logger.info(
|
|
188
|
+
`[refresh] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes${dryRun ? ", dry-run" : ""})`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const passed = refreshed.filter((r) => r.overall === "pass").length;
|
|
194
|
+
const failed = refreshed.filter((r) => r.overall === "fail").length;
|
|
195
|
+
const hasFailures = failed > 0;
|
|
196
|
+
const hasSkippedNoEnvelope = skipped.some((s) => s.reason === "no evidence envelope");
|
|
197
|
+
const status: RfcVerificationRefreshResult["status"] = hasFailures ? "fail" : "pass";
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
data: {
|
|
201
|
+
command: "rfc.verification.refresh",
|
|
202
|
+
status,
|
|
203
|
+
refreshed,
|
|
204
|
+
skipped,
|
|
205
|
+
diagnostics,
|
|
206
|
+
summary: {
|
|
207
|
+
total: refreshed.length,
|
|
208
|
+
passed,
|
|
209
|
+
failed,
|
|
210
|
+
skipped: skipped.length,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
exitCode: hasFailures || hasSkippedNoEnvelope ? 1 : 0,
|
|
214
|
+
summary: `rfc.verification.refresh: ${refreshed.length} refreshed, ${skipped.length} skipped`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -265,6 +265,16 @@ export async function runSessionSave(
|
|
|
265
265
|
if (outputFormat === "pretty") {
|
|
266
266
|
logger.info(`Skipped ${rawFileName}: already converted to ${outputRel}`);
|
|
267
267
|
}
|
|
268
|
+
// Delete raw file even when skipping — otherwise .atif files accumulate
|
|
269
|
+
if (!dryRun && !keepRaw) {
|
|
270
|
+
try {
|
|
271
|
+
await trashPath(rawFilePath);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
268
278
|
continue;
|
|
269
279
|
} catch {
|
|
270
280
|
// Output doesn't exist — proceed
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Prove vendored spec validation resolves materialized RFCs across the canonical RFC archive topology.</purpose>
|
|
4
|
+
<non-goals><item>Do not duplicate individual integrity, graph, or amendment rule tests.</item></non-goals>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY><item>Gap fix: cover SPEC-07 after terminal RFC archival.</item></CHANGE_SUMMARY>
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
13
|
+
import type { ForgeRuntimeContext } from "../../src/types.ts";
|
|
14
|
+
import { runSpecValidate } from "./spec-validate.ts";
|
|
15
|
+
|
|
16
|
+
const roots: string[] = [];
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("spec.validate SPEC-07", () => {
|
|
23
|
+
it("accepts a materialized RFC after docs.archive moves it below archive/implemented", async () => {
|
|
24
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-archive-"));
|
|
25
|
+
roots.push(workspaceRoot);
|
|
26
|
+
const specDir = path.join(workspaceRoot, "docs/specs/example");
|
|
27
|
+
const archiveDir = path.join(workspaceRoot, "docs/rfcs/archive/implemented");
|
|
28
|
+
await fs.mkdir(specDir, { recursive: true }); await fs.mkdir(archiveDir, { recursive: true });
|
|
29
|
+
await fs.writeFile(path.join(archiveDir, "rfc-9001-example.md"), "---\nid: RFC-9001\n---\n");
|
|
30
|
+
await fs.writeFile(path.join(specDir, "forge-spec.yaml"), `schema: forge/spec@1
|
|
31
|
+
id: example
|
|
32
|
+
title: Example
|
|
33
|
+
version: 1.0.0
|
|
34
|
+
status: accepted
|
|
35
|
+
reviewers: []
|
|
36
|
+
sourceNote: test fixture
|
|
37
|
+
vendoredAt: 2026-09-03
|
|
38
|
+
documents: {}
|
|
39
|
+
decisions: []
|
|
40
|
+
rfcs:
|
|
41
|
+
- id: EX-001
|
|
42
|
+
title: Archived RFC
|
|
43
|
+
dependsOn: []
|
|
44
|
+
wave: 1
|
|
45
|
+
sources: []
|
|
46
|
+
materializedAs: RFC-9001
|
|
47
|
+
waves:
|
|
48
|
+
- id: 1
|
|
49
|
+
name: Test
|
|
50
|
+
goal: Exercise archived resolution
|
|
51
|
+
`);
|
|
52
|
+
await fs.writeFile(path.join(specDir, "integrity.yaml"), "schema: forge/spec-integrity@1\nfiles: {}\n");
|
|
53
|
+
const context = { workspaceRoot, dryRun: false, outputFormat: "json",
|
|
54
|
+
logger: { section() {}, info() {}, warn() {}, error() {}, success() {} } } as ForgeRuntimeContext;
|
|
55
|
+
const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, context);
|
|
56
|
+
expect(result).toMatchObject({ exitCode: 0, data: { status: "pass",
|
|
57
|
+
specs: [{ id: "example", violations: [] }] } });
|
|
58
|
+
});
|
|
59
|
+
});
|
package/os/spec/spec-validate.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
</MODULE_CONTRACT>
|
|
10
10
|
<CHANGE_SUMMARY>
|
|
11
11
|
<item>RFC-0394: initial spec.validate handler with SPEC-01..07 rules.</item>
|
|
12
|
+
<item>Gap fix: resolve materialized RFCs recursively so terminal RFC archival does not invalidate accepted specs.</item>
|
|
12
13
|
</CHANGE_SUMMARY>
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
19
20
|
import { byteHash } from "../../src/utils/hash.ts";
|
|
20
21
|
import { collectFiles } from "../../src/utils/fs.ts";
|
|
21
22
|
import { loadForgeConfig } from "../../src/config/forge-config.ts";
|
|
23
|
+
import { listRfcFiles } from "../rfc/frontmatter-io.ts";
|
|
22
24
|
import type {
|
|
23
25
|
ForgeCommandInput,
|
|
24
26
|
ForgeCommandResult,
|
|
@@ -260,15 +262,15 @@ function checkDuplicates(spec: ForgeSpec, violations: SpecViolation[]): void {
|
|
|
260
262
|
|
|
261
263
|
async function checkMaterializedAs(
|
|
262
264
|
spec: ForgeSpec,
|
|
263
|
-
workspaceRoot: string,
|
|
264
265
|
rfcDir: string,
|
|
265
266
|
violations: SpecViolation[],
|
|
266
267
|
): Promise<void> {
|
|
268
|
+
const rfcFiles = await listRfcFiles(rfcDir);
|
|
267
269
|
for (const node of spec.rfcs) {
|
|
268
270
|
if (!node.materializedAs) continue;
|
|
269
271
|
const rfcId = node.materializedAs;
|
|
270
|
-
const
|
|
271
|
-
const found = rfcFiles.some((
|
|
272
|
+
const prefix = rfcId.toLowerCase().replace(/^rfc-/, "rfc-");
|
|
273
|
+
const found = rfcFiles.some((file) => path.basename(file).startsWith(prefix));
|
|
272
274
|
if (!found) {
|
|
273
275
|
violations.push({
|
|
274
276
|
rule: "SPEC-07",
|
|
@@ -467,7 +469,7 @@ export async function runSpecValidate(
|
|
|
467
469
|
checkDuplicates(spec, violations);
|
|
468
470
|
|
|
469
471
|
// SPEC-07: materializedAs
|
|
470
|
-
await checkMaterializedAs(spec,
|
|
472
|
+
await checkMaterializedAs(spec, rfcDir, violations);
|
|
471
473
|
|
|
472
474
|
// SPEC-08..11: Amendments (RFC-0397)
|
|
473
475
|
const amendments = await loadAmendments(specDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warpgogol/forge",
|
|
3
|
-
"version": "2.21.
|
|
3
|
+
"version": "2.21.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
},
|
|
170
170
|
"dependencies": {
|
|
171
171
|
"@aws-sdk/client-s3": "^3.1116.0",
|
|
172
|
+
"ajv": "^8.20.0",
|
|
172
173
|
"picomatch": "^4.0.5",
|
|
173
174
|
"trash": "^10.1.1",
|
|
174
175
|
"yaml": "^2.9.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fo-handoff
|
|
3
|
-
description: Compact the current conversation into a handoff document for another agent to pick up. Saves to
|
|
3
|
+
description: Compact the current conversation into a handoff document for another agent to pick up. Saves to docs/handoffs/ (resolved from forge.yaml paths.handoffsDir) and commits via ecosystem.commit.
|
|
4
4
|
invocation: user
|
|
5
5
|
category: fo
|
|
6
6
|
concerns: document-only
|
|
@@ -13,7 +13,7 @@ triggers: ["create a handoff document", "compact conversation for next agent", "
|
|
|
13
13
|
|
|
14
14
|
Before starting, read `PREFERENCES.md` at the repository root. If the file is missing or `aiLanguage` is unset, ask the operator once and create the file using the `my-preferences` skill semantics.
|
|
15
15
|
|
|
16
|
-
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the
|
|
16
|
+
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the project's handoffs directory (resolved from `forge.yaml` `paths.handoffsDir`, defaulting to `docs/handoffs/`), not the OS temp directory.
|
|
17
17
|
|
|
18
18
|
## Process
|
|
19
19
|
|
|
@@ -47,18 +47,27 @@ Redact any sensitive information, such as API keys, passwords, or personally ide
|
|
|
47
47
|
|
|
48
48
|
### 3. Save
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Resolve the target directory from `forge.yaml` `paths.handoffsDir` (falling back to `docs/handoffs/` if unset). Filename pattern: `handoff-YYYY-MM-DD-session-<brief-description>.md`.
|
|
51
51
|
|
|
52
52
|
If the operator passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.
|
|
53
53
|
|
|
54
|
-
### 4.
|
|
54
|
+
### 4. Commit
|
|
55
|
+
|
|
56
|
+
Commit the handoff document via `ecosystem.commit` so it survives stash operations and is available to the next session.
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
rtk pnpm exec werkstatt run ecosystem.commit --message "docs: add handoff document"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 5. Report
|
|
55
63
|
|
|
56
64
|
Tell the operator the absolute path of the handoff document and suggest opening a fresh session that references it. Report in `aiLanguage`.
|
|
57
65
|
|
|
58
66
|
## Constraints
|
|
59
67
|
|
|
60
|
-
- **
|
|
68
|
+
- **Save to `docs/handoffs/`** (or the directory resolved from `forge.yaml` `paths.handoffsDir`). Never save to `/tmp/` or other temporary directories.
|
|
69
|
+
- **Commit the handoff document** via `ecosystem.commit` after saving.
|
|
61
70
|
- **Do not duplicate existing artifacts.** Reference them by path or URL.
|
|
62
71
|
- **Redact sensitive information.** API keys, passwords, PII.
|
|
63
|
-
- **
|
|
72
|
+
- **Stage only the handoff file.** See `_shared/fo-pipeline-conventions.md` §Commit discipline.
|
|
64
73
|
- **Session summary.** End every session with the closing block defined in `_shared/fo-session-summary.md`.
|
|
@@ -91,7 +91,7 @@ Beyond V-13 (required sections exist) and V-14 (≥3 acceptance items), check th
|
|
|
91
91
|
- **Rollout** describes default behavior, adoption path for existing apps, and new-app compliance.
|
|
92
92
|
- **Alternatives considered** is honest — at least one real alternative with a rejection reason.
|
|
93
93
|
- **Risks** includes agent misinterpretation risk and false-positive rate for validators.
|
|
94
|
-
- **Acceptance criteria** items are checkable (can an agent or human verify each one?) and sufficient (do they cover the decision's full scope?).
|
|
94
|
+
- **Acceptance criteria** items are checkable (can an agent or human verify each one?) and sufficient (do they cover the decision's full scope?). Run the reject checklist (RFC-1006): no non-atomic "and"-joined claims (V-39), no unbounded quantities like "fast" or "scalable" (V-40), no weasel verbs like "handle gracefully" (V-41). Check that document-readiness criteria (DR-N) are separated from system-conformance criteria (AC-N) if a `## Document readiness` section is present.
|
|
95
95
|
- **Implementation notes for agents** are explicit behavioral rules, not vague guidance.
|
|
96
96
|
|
|
97
97
|
#### Axis B — DNA alignment
|
|
@@ -88,7 +88,7 @@ Read the generated file and the full template (`docs/rfcs/rfc-0000-template.md`)
|
|
|
88
88
|
- **Rollout** — implementation order, generated-file refresh, migration path for existing apps.
|
|
89
89
|
- **Alternatives considered** — at least one real alternative with a rejection reason.
|
|
90
90
|
- **Risks** — including agent-misinterpretation risk and false-positive rates for validators.
|
|
91
|
-
- **Acceptance criteria** —
|
|
91
|
+
- **Acceptance criteria** — EARS-form criteria with stable `AC-N:` identifiers, following the authoring standard in `packages/forge/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md` (RFC-0996 + RFC-1006). Required mix: at least one behavior, one contract, one negative, and one sync criterion. 3–10 items. Each criterion is a falsifiable claim about an observable artifact, not an effort claim. One claim = one checking mechanism. Run the reject checklist (RFC-1006): no non-atomic "and"-joined claims, no unbounded quantities, no weasel verbs.
|
|
92
92
|
- **Implementation notes for agents** — explicit MAY/MUST NOT rules, status-gate reminders, escalation triggers.
|
|
93
93
|
|
|
94
94
|
When a section cannot be filled with confidence, insert `> NEEDS CLARIFICATION: <question>` instead of guessing. Do not leave sections empty or fill them with speculative content. Markers are resolved during the enhance step.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Acceptance Criteria Authoring Standard (EARS-based)
|
|
2
|
+
|
|
3
|
+
> Source of truth: RFC-0996 (EARS form, required mix, evidence discipline) and RFC-1006 (phase separation, reject checklist, criterion versioning). This document is the canonical reference — templates and skills summarize and link to it, never fork the normative wording.
|
|
4
|
+
|
|
5
|
+
## Agent instruction
|
|
6
|
+
|
|
7
|
+
A criterion is a _falsifiable claim about an observable artifact_, not a description of work performed. Before writing each criterion, answer: _what command, test, or probe would turn red if this claim were false?_ If you cannot answer, the criterion is not ready — rewrite it or insert `> NEEDS CLARIFICATION`.
|
|
8
|
+
|
|
9
|
+
**Form.** Each criterion carries a stable identifier `AC-N:` and uses EARS-style phrasing:
|
|
10
|
+
|
|
11
|
+
- Invariant: `AC-N: THE <artifact> SHALL <observable property>`
|
|
12
|
+
- Event-driven: `AC-N: WHEN <trigger>, THE <system> SHALL <response> [within <bound>]`
|
|
13
|
+
- Failure-path: `AC-N: IF <violation>, THEN THE <system> SHALL <explicit failure behavior>`
|
|
14
|
+
|
|
15
|
+
**One criterion = one claim = one checking mechanism.** Never bundle ("types defined AND command registered AND docs updated" is three criteria). Each criterion names its mechanism inline or via a probe.
|
|
16
|
+
|
|
17
|
+
**Required mix.** A well-formed set (3–10 items) covers:
|
|
18
|
+
|
|
19
|
+
1. **Behavior** — at least one criterion per user-visible or agent-visible behavior change, backed by a `run`/`test` probe or a named test file.
|
|
20
|
+
2. **Contract** — schema/type/output-format claims, backed by `file-contains`, schema validation, or a compile check.
|
|
21
|
+
3. **Negative** — at least one criterion asserting what _fails_ (wrong input rejected, gate blocks, error escalates). Specs that only describe the happy path are half-specs.
|
|
22
|
+
4. **Sync** — docs/AGENTS.md/DNA updates, backed by `file-contains`.
|
|
23
|
+
|
|
24
|
+
**Forbidden patterns:**
|
|
25
|
+
|
|
26
|
+
- Effort claims: "code written", "X implemented", "refactored Y".
|
|
27
|
+
- Unfalsifiable adverbs: "works correctly", "handles gracefully", "is robust".
|
|
28
|
+
- Tautologies: "`rfc.validate` passes on this file" as the only substantive criterion.
|
|
29
|
+
- Mind-reading: criteria whose truth requires asking the implementer.
|
|
30
|
+
- Copied template boilerplate left unedited.
|
|
31
|
+
|
|
32
|
+
**Probes.** Every criterion checkable by an existing probe kind MUST get a probe in `acceptance:` frontmatter. Criteria without a feasible probe must state their manual verification procedure explicitly — "manually verified" without a procedure is not evidence.
|
|
33
|
+
|
|
34
|
+
**Evidence discipline (at check-off time).** `(evidence: ...)` must point to the _mechanism_, not the _artifact_: prefer `probe:AC-N` or `test: path/to/file.test.ts`, then `file:line`. Never check a box based on intention; run the mechanism first.
|
|
35
|
+
|
|
36
|
+
**Self-test before submitting:** hand the criteria set to a hostile reviewer who has _only_ the repo and a shell. If they cannot verify every box without talking to you, the spec is prose, not a spec.
|
|
37
|
+
|
|
38
|
+
## Phase separation (RFC-1006)
|
|
39
|
+
|
|
40
|
+
RFCs have two phases of criteria:
|
|
41
|
+
|
|
42
|
+
- **RFC-phase (document readiness)** — criteria that gate whether the document itself is ready for acceptance. These live in `## Document readiness` with `DR-N:` identifiers. They are NOT EARS statements about system behavior. Examples: "every considered alternative has a rejection reason", "the RFC states reversibility". Evidence is typically `file:line` pointing to the RFC itself.
|
|
43
|
+
- **ADR-phase (acceptance criteria)** — criteria that verify the decision's implementation in the running system. These live in `## Acceptance criteria` with `AC-N:` identifiers. They ARE EARS statements with verification mapping (probe, test, or file:line).
|
|
44
|
+
|
|
45
|
+
ADRs have only acceptance criteria (no document-readiness section).
|
|
46
|
+
|
|
47
|
+
## Reject checklist (RFC-1006)
|
|
48
|
+
|
|
49
|
+
Before submitting criteria, run this line-level reject check. A criterion that triggers any of these is rejected:
|
|
50
|
+
|
|
51
|
+
1. **Not atomic** — two independently testable behaviors joined by "and" in a single criterion. Split into separate `AC-N` items. (V-39)
|
|
52
|
+
2. **Unbounded quantity** — unitless numeric concepts: "fast", "scalable", "reasonable load", "efficiently", "high performance", "low latency". Replace with a specific number or reference the decision that sets it. (V-40)
|
|
53
|
+
3. **Weasel verb** — closed set: "handle gracefully", "behave correctly", "as appropriate", "robust", "user-friendly", "works correctly", "is reliable". Replace with a specific observable behavior. (V-41)
|
|
54
|
+
4. **No verification mapping** — a checked criterion without `(evidence: ...)` pointing to a mechanism. Already enforced by V-27 and V-37.
|
|
55
|
+
|
|
56
|
+
The trigger lists for V-40 and V-41 are configuration constants in `validate-rules.ts` — they can be extended without an RFC.
|
|
57
|
+
|
|
58
|
+
## Criterion versioning (RFC-1006)
|
|
59
|
+
|
|
60
|
+
Criteria are append-only. To change an accepted criterion:
|
|
61
|
+
|
|
62
|
+
1. Add a `> Superseded AC-N (YYYY-MM-DD): <reason>` annotation line above the original criterion.
|
|
63
|
+
2. Add the new criterion with a new `AC-N` identifier.
|
|
64
|
+
3. Never edit an accepted criterion in place — that erases the trace of what shipped.
|
|
65
|
+
|
|
66
|
+
Superseded criteria are excluded from the unchecked count (V-26) but preserved in the document. Malformed supersession annotations are rejected by V-42.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
> Superseded AC-3 (2026-09-15): original criterion was too broad, split into AC-3a and AC-3b
|
|
72
|
+
- [x] AC-3: THE command SHALL return exit code 0 for valid input (evidence: test: test.ts:42)
|
|
73
|
+
- [x] AC-3a: THE command SHALL return exit code 0 when input matches the schema (evidence: test: test.ts:42)
|
|
74
|
+
- [x] AC-3b: IF input does not match the schema, THEN THE command SHALL return exit code 1 (evidence: test: test.ts:58)
|
|
75
|
+
```
|
|
@@ -173,11 +173,12 @@ Read the RFC's `## Acceptance criteria` section. For each checkbox:
|
|
|
173
173
|
1. **Verify the criterion is met semantically** — check the code does what the criterion says, run the relevant command, or inspect the artifact. Mechanical existence (command registered, test passes) is NOT sufficient. The criterion must describe observable behavior that the RFC defines, not just that a command exists.
|
|
174
174
|
2. **Check for stubs** — if the code contains TODO, stub, not-implemented, or placeholder logic in the path the criterion covers, the criterion is NOT met. Implement the real logic before marking it.
|
|
175
175
|
3. **If a criterion is not met**, implement the missing work, commit it, and re-verify.
|
|
176
|
-
4. **Annotate every `[x]` with inline evidence** — add `(evidence:
|
|
176
|
+
4. **Annotate every `[x]` with inline evidence** — add `(evidence: ...)` to each checked criterion, pointing to the _checking mechanism_, not just the artifact. Prefer `probe:AC-N` or `test: path/to/file.test.ts`, then `file:line`. This is enforced by V-27 (RFC-0996 evidence discipline).
|
|
177
177
|
5. **If a criterion cannot be met** (e.g., requires an external dependency not yet available, requires a pilot that is not registered), do NOT mark it `[x]` and do NOT stamp `implemented`. Instead, split the deferred work into a follow-up RFC via `rfc.supersede.propose`. An RFC with unchecked `[ ]` criteria cannot transition to `implemented` — this is enforced by V-26.
|
|
178
178
|
6. **Ensure `reviewers` is non-empty** — `rfc.validate` enforces V-25: implemented RFCs with an empty `reviewers` field fail validation. Add at least one reviewer (e.g. `human:<name>`) before stamping `implemented`.
|
|
179
|
+
7. **Check document readiness** (RFC-1006) — if the RFC has a `## Document readiness` section, verify every `DR-N` checkbox is checked `[x]`. Unchecked `DR-N` items block `accepted`/`implemented` status via V-38. Document readiness criteria cover document quality (sections present, risks described, alternatives considered), not system behavior — they are checked by inspecting the RFC itself, not by running code.
|
|
179
180
|
|
|
180
|
-
Do not proceed to step 4.7 until every acceptance criterion checkbox is checked with evidence.
|
|
181
|
+
Do not proceed to step 4.7 until every acceptance criterion checkbox AND every document readiness checkbox is checked with evidence.
|
|
181
182
|
|
|
182
183
|
#### 4.7. Run acceptance probes and emit evidence
|
|
183
184
|
|
|
@@ -17,7 +17,7 @@ import path from "node:path";
|
|
|
17
17
|
import { parse as parseYaml } from "yaml";
|
|
18
18
|
import picomatch from "picomatch";
|
|
19
19
|
import type { ForgeConfig } from "../config/forge-config.ts";
|
|
20
|
-
import { forgePluginManifestSchema } from "../plugin/
|
|
20
|
+
import { forgePluginManifestSchema } from "../plugin/forge-plugin-manifest.ts";
|
|
21
21
|
import type {
|
|
22
22
|
CompassContractBlockSpec,
|
|
23
23
|
CompassContractRegistry,
|
|
@@ -27,15 +27,34 @@ import type {
|
|
|
27
27
|
const BUILT_IN_SPECS: CompassContractBlockSpec[] = [
|
|
28
28
|
{
|
|
29
29
|
blockId: "module-contract",
|
|
30
|
-
requiredFor: [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
requiredFor: [
|
|
31
|
+
"packages/**/*.ts",
|
|
32
|
+
"packages/**/*.tsx",
|
|
33
|
+
"packages/**/*.astro",
|
|
34
|
+
"packages/**/*.js",
|
|
35
|
+
"packages/**/*.mjs",
|
|
36
|
+
"packages/**/SKILL.md",
|
|
37
|
+
"services/**/*.ts",
|
|
38
|
+
"apps/**/*.ts",
|
|
39
|
+
"apps/**/*.tsx",
|
|
40
|
+
"apps/**/*.astro",
|
|
34
41
|
],
|
|
42
|
+
requiredTags: [{ name: "purpose", minWords: 10 }, { name: "non-goals" }],
|
|
35
43
|
},
|
|
36
44
|
{
|
|
37
45
|
blockId: "change-summary",
|
|
38
|
-
requiredFor: [
|
|
46
|
+
requiredFor: [
|
|
47
|
+
"packages/**/*.ts",
|
|
48
|
+
"packages/**/*.tsx",
|
|
49
|
+
"packages/**/*.astro",
|
|
50
|
+
"packages/**/*.js",
|
|
51
|
+
"packages/**/*.mjs",
|
|
52
|
+
"packages/**/SKILL.md",
|
|
53
|
+
"services/**/*.ts",
|
|
54
|
+
"apps/**/*.ts",
|
|
55
|
+
"apps/**/*.tsx",
|
|
56
|
+
"apps/**/*.astro",
|
|
57
|
+
],
|
|
39
58
|
requiredTags: [],
|
|
40
59
|
},
|
|
41
60
|
];
|
package/src/index.ts
CHANGED
package/src/onboarding/doctor.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
|
|
|
31
31
|
import { readFileSync, existsSync } from "node:fs";
|
|
32
32
|
import { join, relative, dirname, resolve } from "node:path";
|
|
33
33
|
import { parse as parseYaml } from "yaml";
|
|
34
|
-
import { forgePluginManifestSchema } from "../plugin/
|
|
34
|
+
import { forgePluginManifestSchema } from "../plugin/forge-plugin-manifest.ts";
|
|
35
35
|
import type {
|
|
36
36
|
ForgeCommandInput,
|
|
37
37
|
ForgeCommandResult,
|
package/src/registry.ts
CHANGED
|
@@ -25,7 +25,7 @@ import fs from "node:fs";
|
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { parse as parseYaml } from "yaml";
|
|
27
27
|
import type { ForgeConfig } from "./config/forge-config.ts";
|
|
28
|
-
import { forgePluginManifestSchema } from "./plugin/
|
|
28
|
+
import { forgePluginManifestSchema } from "./plugin/forge-plugin-manifest.ts";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Every entry MUST be portable — it runs in any forge-bootstrapped project
|