@warpgogol/forge 5.1.2 → 5.1.3

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.
Files changed (62) hide show
  1. package/AGENTS.md +14 -2
  2. package/dist/os/adr/handlers/implement-stamp.d.ts.map +1 -1
  3. package/dist/os/adr/handlers/implement-stamp.js +32 -2
  4. package/dist/os/adr/handlers/implement-stamp.js.map +1 -1
  5. package/dist/os/adr/types.d.ts +2 -1
  6. package/dist/os/adr/types.d.ts.map +1 -1
  7. package/dist/os/naming/naming-convention.d.ts.map +1 -1
  8. package/dist/os/naming/naming-convention.js +2 -0
  9. package/dist/os/naming/naming-convention.js.map +1 -1
  10. package/dist/os/rfc/frontmatter-io.d.ts.map +1 -1
  11. package/dist/os/rfc/frontmatter-io.js +1 -0
  12. package/dist/os/rfc/frontmatter-io.js.map +1 -1
  13. package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
  14. package/dist/os/rfc/handlers/validate-rules.js +6 -1
  15. package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
  16. package/dist/os/werkstatt/handlers/workspace-deps.d.ts.map +1 -1
  17. package/dist/os/werkstatt/handlers/workspace-deps.js +4 -3
  18. package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -1
  19. package/dist/src/onboarding/create.d.ts.map +1 -1
  20. package/dist/src/onboarding/create.js +5 -2
  21. package/dist/src/onboarding/create.js.map +1 -1
  22. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  23. package/dist/src/onboarding/doctor.js +15 -0
  24. package/dist/src/onboarding/doctor.js.map +1 -1
  25. package/dist/src/onboarding/init.d.ts +1 -0
  26. package/dist/src/onboarding/init.d.ts.map +1 -1
  27. package/dist/src/onboarding/init.js.map +1 -1
  28. package/dist/src/onboarding/npm-token-check.d.ts +40 -0
  29. package/dist/src/onboarding/npm-token-check.d.ts.map +1 -0
  30. package/dist/src/onboarding/npm-token-check.js +104 -0
  31. package/dist/src/onboarding/npm-token-check.js.map +1 -0
  32. package/dist/src/onboarding/scaffold-project.d.ts.map +1 -1
  33. package/dist/src/onboarding/scaffold-project.js +10 -0
  34. package/dist/src/onboarding/scaffold-project.js.map +1 -1
  35. package/dist/src/profiles/profile-schema.d.ts +2 -0
  36. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  37. package/dist/src/profiles/profile-schema.js +1 -0
  38. package/dist/src/profiles/profile-schema.js.map +1 -1
  39. package/dist/src/profiles/stack-profile.d.ts +1 -0
  40. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  41. package/dist/src/profiles/stack-profile.js +2 -0
  42. package/dist/src/profiles/stack-profile.js.map +1 -1
  43. package/os/adr/handlers/implement-stamp.ts +34 -2
  44. package/os/adr/types.ts +3 -1
  45. package/os/naming/naming-convention.ts +2 -0
  46. package/os/rfc/frontmatter-io.ts +1 -0
  47. package/os/rfc/handlers/validate-rules.ts +8 -1
  48. package/os/werkstatt/handlers/workspace-deps.ts +4 -3
  49. package/package.json +1 -1
  50. package/profiles/site-workshop.yaml +654 -0
  51. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +1 -1
  52. package/src/onboarding/create.ts +5 -2
  53. package/src/onboarding/doctor-token-check.test.ts +124 -0
  54. package/src/onboarding/doctor.ts +17 -0
  55. package/src/onboarding/init.ts +1 -0
  56. package/src/onboarding/npm-token-check.ts +140 -0
  57. package/src/onboarding/scaffold-project.ts +11 -0
  58. package/src/onboarding/site-workshop-profile.test.ts +84 -0
  59. package/src/profiles/profile-schema.ts +2 -0
  60. package/src/profiles/stack-profile.ts +2 -0
  61. package/src/tests/adr-implement-stamp.test.ts +150 -0
  62. package/src/tests/profile-schema.test.ts +1 -1
@@ -0,0 +1,124 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for the npm-token probe (RFC-1125, AC-2) — .npmrc parsing, token presence detection, and the fail-fast fixHint contract used by forge create and forge.doctor.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-1125: initial npm-token probe tests (AC-2 evidence).</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect, beforeEach, afterEach } from "vitest";
11
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import {
15
+ checkNpmToken,
16
+ readNpmrcTokenStatus,
17
+ workshopNeedsWarpgogolToken,
18
+ } from "./npm-token-check.ts";
19
+
20
+ let tempDir: string;
21
+
22
+ beforeEach(async () => {
23
+ tempDir = await mkdtemp(join(tmpdir(), "npm-token-check-"));
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await rm(tempDir, { recursive: true, force: true });
28
+ });
29
+
30
+ const NPMRC_WITH_TOKEN = `@warpgogol:registry=https://registry.npmjs.org/
31
+ //registry.npmjs.org/:_authToken=npm_real_token_123
32
+ `;
33
+
34
+ const NPMRC_PLACEHOLDER = `@warpgogol:registry=https://registry.npmjs.org/
35
+ //registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN
36
+ `;
37
+
38
+ const NPMRC_ENV_REF = `@warpgogol:registry=https://registry.npmjs.org/
39
+ //registry.npmjs.org/:_authToken=\${NPM_TOKEN}
40
+ `;
41
+
42
+ // --- readNpmrcTokenStatus ---
43
+
44
+ test("detects registry and literal token from .npmrc", async () => {
45
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_WITH_TOKEN);
46
+ const status = readNpmrcTokenStatus(tempDir, {});
47
+ expect(status.registry).toBe("https://registry.npmjs.org/");
48
+ expect(status.tokenPresent).toBe(true);
49
+ });
50
+
51
+ test("YOUR_NPM_TOKEN placeholder does not count as a token", async () => {
52
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_PLACEHOLDER);
53
+ const status = readNpmrcTokenStatus(tempDir, {});
54
+ expect(status.tokenPresent).toBe(false);
55
+ });
56
+
57
+ test("NPM_TOKEN env satisfies the probe even with placeholder .npmrc", async () => {
58
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_PLACEHOLDER);
59
+ const status = readNpmrcTokenStatus(tempDir, { NPM_TOKEN: "npm_env_token" });
60
+ expect(status.tokenPresent).toBe(true);
61
+ });
62
+
63
+ test("${NPM_TOKEN} reference resolves against env", async () => {
64
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_ENV_REF);
65
+ expect(readNpmrcTokenStatus(tempDir, { NPM_TOKEN: "x" }).tokenPresent).toBe(true);
66
+ expect(readNpmrcTokenStatus(tempDir, {}).tokenPresent).toBe(false);
67
+ });
68
+
69
+ test("missing .npmrc → no registry, no token", () => {
70
+ const status = readNpmrcTokenStatus(tempDir, {});
71
+ expect(status.registry).toBeNull();
72
+ expect(status.tokenPresent).toBe(false);
73
+ });
74
+
75
+ test("detects _authToken when it is not the last line (scaffolded .npmrc shape)", async () => {
76
+ // Regression: scaffolded .npmrc appends dangerously-allow-all-builds and
77
+ // ignore-workspace-root-check AFTER the token line — the regex must match mid-file.
78
+ await writeFile(
79
+ join(tempDir, ".npmrc"),
80
+ `@warpgogol:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=npm_real_token\ndangerously-allow-all-builds=true\nignore-workspace-root-check=true\n`,
81
+ );
82
+ const status = readNpmrcTokenStatus(tempDir, {});
83
+ expect(status.tokenPresent).toBe(true);
84
+ });
85
+
86
+ // --- workshopNeedsWarpgogolToken ---
87
+
88
+ test("needs token when .npmrc declares @warpgogol scope", async () => {
89
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_PLACEHOLDER);
90
+ expect(workshopNeedsWarpgogolToken(tempDir, [])).toBe(true);
91
+ });
92
+
93
+ test("needs token when install pulls @warpgogol packages", () => {
94
+ expect(workshopNeedsWarpgogolToken(tempDir, ["pnpm add -wD @warpgogol/forge"])).toBe(true);
95
+ });
96
+
97
+ test("skips probe when no @warpgogol usage", () => {
98
+ expect(workshopNeedsWarpgogolToken(tempDir, ["pnpm add -wD prettier"])).toBe(false);
99
+ });
100
+
101
+ // --- checkNpmToken ---
102
+
103
+ test("canFetch=true → fixHint is null", async () => {
104
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_WITH_TOKEN);
105
+ const result = await checkNpmToken(tempDir, { fetchVersion: () => true, env: {} });
106
+ expect(result.canFetch).toBe(true);
107
+ expect(result.fixHint).toBeNull();
108
+ });
109
+
110
+ test("canFetch=false with token → fixHint mentions expiry/revocation", async () => {
111
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_WITH_TOKEN);
112
+ const result = await checkNpmToken(tempDir, { fetchVersion: () => false, env: {} });
113
+ expect(result.canFetch).toBe(false);
114
+ expect(result.fixHint).toContain("expired or revoked");
115
+ });
116
+
117
+ test("canFetch=false without token → fixHint names NPM_TOKEN and .npmrc", async () => {
118
+ await writeFile(join(tempDir, ".npmrc"), NPMRC_PLACEHOLDER);
119
+ const result = await checkNpmToken(tempDir, { fetchVersion: () => false, env: {} });
120
+ expect(result.canFetch).toBe(false);
121
+ // AC-2: the fixHint must be actionable — name the exact remediation path
122
+ expect(result.fixHint).toContain("NPM_TOKEN");
123
+ expect(result.fixHint).toContain(".npmrc");
124
+ });
@@ -49,6 +49,7 @@ import type { DuplicatePair } from "../knowledge/index.ts";
49
49
  import { checkMemoryLayerHealth } from "./memory-scaffold.ts";
50
50
  import { checkInvariants } from "./invariant-engine.ts";
51
51
  import type { InvariantViolation } from "./invariant-engine.ts";
52
+ import { checkNpmToken, workshopNeedsWarpgogolToken } from "./npm-token-check.ts";
52
53
  import { execSync } from "node:child_process";
53
54
  import fs from "node:fs";
54
55
  import type { ProfilePrerequisite } from "../profiles/profile-schema.ts";
@@ -1272,6 +1273,22 @@ export async function runDoctor(
1272
1273
  : "docs/adrs/ directory not found — run 'forge create' to create ADR directory",
1273
1274
  });
1274
1275
 
1276
+ // RFC-1125: re-verify the @warpgogol npm token post-install when the workshop
1277
+ // uses the private scope (.npmrc declares it or package.json has @warpgogol deps)
1278
+ const pkgJsonPath = join(workspaceRoot, "package.json");
1279
+ const pkgHasWarpgogol =
1280
+ fs.existsSync(pkgJsonPath) && fs.readFileSync(pkgJsonPath, "utf8").includes("@warpgogol/");
1281
+ if (workshopNeedsWarpgogolToken(workspaceRoot, []) || pkgHasWarpgogol) {
1282
+ const tokenCheck = await checkNpmToken(workspaceRoot);
1283
+ checks.push({
1284
+ name: "npm-token (@warpgogol)",
1285
+ status: tokenCheck.canFetch ? "pass" : "fail",
1286
+ message: tokenCheck.canFetch
1287
+ ? `npm token fetches @warpgogol/werkstatt-engine (${tokenCheck.registry ?? "default registry"})`
1288
+ : tokenCheck.fixHint!,
1289
+ });
1290
+ }
1291
+
1275
1292
  // Check git branch is 'main' (not 'master')
1276
1293
  checks.push(checkGitBranch(workspaceRoot));
1277
1294
 
@@ -49,6 +49,7 @@ export interface InitDomainFields {
49
49
  terminology?: Record<string, string>;
50
50
  semanticBindings?: Record<string, string | null>;
51
51
  profileId?: string;
52
+ nextSteps?: string;
52
53
  compass?: {
53
54
  fileExtensions?: string[];
54
55
  testPatterns?: string[];
@@ -0,0 +1,140 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Non-interactive npm-token probe for the private @warpgogol registry scope (RFC-1125) — parses .npmrc for the @warpgogol scope registry and auth token presence, probes the registry via `npm view @warpgogol/werkstatt-engine version` (injectable for tests) before pnpm install runs in forge create, re-verifies post-install in forge.doctor, and returns a structured NpmTokenCheck with an actionable fixHint on failure.</purpose>
4
+ <non-goals>
5
+ <item>Do not prompt the operator — the probe is non-interactive (create.ts forbids prompts).</item>
6
+ <item>Do not accept tokens via CLI flags — tokens must not appear in argv/history.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-1125: initial npm-token probe — checkNpmToken + readNpmrcTokenStatus, wired into scaffold-project (pre-install fail-fast) and doctor (post-install re-verify).</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { execSync } from "node:child_process";
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+
18
+ /** Result of the npm-token probe (RFC-1125). */
19
+ export interface NpmTokenCheck {
20
+ /** Registry URL from `.npmrc` `@warpgogol:registry=` (null when undeclared). */
21
+ registry: string | null;
22
+ /** A usable token is configured — NPM_TOKEN env or a real _authToken value. */
23
+ tokenPresent: boolean;
24
+ /** `npm view @warpgogol/werkstatt-engine version` succeeded from workspaceRoot. */
25
+ canFetch: boolean;
26
+ /** Actionable remediation when canFetch is false; null on success. */
27
+ fixHint: string | null;
28
+ }
29
+
30
+ /** Injectable dependencies for testing. */
31
+ export interface NpmTokenProbeDeps {
32
+ /** Override the registry probe — return true when the fetch succeeds. */
33
+ fetchVersion?: (cwd: string) => boolean;
34
+ /** Override the environment (defaults to process.env). */
35
+ env?: Record<string, string | undefined>;
36
+ }
37
+
38
+ const WARPGOGOL_SCOPE = "@warpgogol";
39
+ const PROBE_PACKAGE = "@warpgogol/werkstatt-engine";
40
+ const PROBE_TIMEOUT_MS = 15_000;
41
+ const PLACEHOLDER_TOKENS = new Set(["YOUR_NPM_TOKEN", ""]);
42
+
43
+ /**
44
+ * Parse the workspace `.npmrc` for the @warpgogol scope: registry URL and
45
+ * whether a usable auth token is configured. A token counts as present when
46
+ * NPM_TOKEN is set in the env, or when `_authToken` holds a real value
47
+ * (not the YOUR_NPM_TOKEN placeholder, not an unset ${VAR} reference).
48
+ */
49
+ export function readNpmrcTokenStatus(
50
+ workspaceRoot: string,
51
+ env: Record<string, string | undefined> = process.env,
52
+ ): { registry: string | null; tokenPresent: boolean } {
53
+ const npmrcPath = path.join(workspaceRoot, ".npmrc");
54
+ const npmrc = fs.existsSync(npmrcPath) ? fs.readFileSync(npmrcPath, "utf8") : "";
55
+
56
+ const registryMatch = npmrc.match(/^@warpgogol:registry=(\S+)\s*$/m);
57
+ const registry = registryMatch?.[1] ?? null;
58
+
59
+ // Token sources, in precedence order:
60
+ // 1. NPM_TOKEN env (agent-driven installs; also satisfies ${NPM_TOKEN} refs)
61
+ if (env.NPM_TOKEN && env.NPM_TOKEN.trim().length > 0) {
62
+ return { registry, tokenPresent: true };
63
+ }
64
+
65
+ // 2. Literal _authToken in .npmrc for the scope's registry host.
66
+ // `m` flag required — the token line is never last in a scaffolded .npmrc
67
+ // (dangerously-allow-all-builds + appended ignore-workspace-root-check follow it).
68
+ const tokenMatch = npmrc.match(/_authToken=(\S+)[ \t]*$/m);
69
+ if (tokenMatch) {
70
+ const value = tokenMatch[1];
71
+ // ${VAR} references resolve at install time — count as present only when set
72
+ const varRef = value.match(/^\$\{(\w+)\}$/);
73
+ if (varRef) {
74
+ const envValue = env[varRef[1]];
75
+ return { registry, tokenPresent: !!envValue && envValue.trim().length > 0 };
76
+ }
77
+ return { registry, tokenPresent: !PLACEHOLDER_TOKENS.has(value) };
78
+ }
79
+
80
+ return { registry, tokenPresent: false };
81
+ }
82
+
83
+ /**
84
+ * Whether the workspace needs the @warpgogol token at all — true when `.npmrc`
85
+ * declares the scope or any install command pulls `@warpgogol/` packages.
86
+ * Profiles without @warpgogol deps skip the probe entirely.
87
+ */
88
+ export function workshopNeedsWarpgogolToken(workspaceRoot: string, installCmds: string[]): boolean {
89
+ const npmrcPath = path.join(workspaceRoot, ".npmrc");
90
+ if (fs.existsSync(npmrcPath)) {
91
+ const npmrc = fs.readFileSync(npmrcPath, "utf8");
92
+ if (npmrc.includes(`${WARPGOGOL_SCOPE}:registry`) || npmrc.includes(`${WARPGOGOL_SCOPE}/`)) {
93
+ return true;
94
+ }
95
+ }
96
+ return installCmds.some((cmd) => cmd.includes(`${WARPGOGOL_SCOPE}/`));
97
+ }
98
+
99
+ function defaultFetchVersion(cwd: string): boolean {
100
+ try {
101
+ execSync(`npm view ${PROBE_PACKAGE} version`, {
102
+ cwd,
103
+ stdio: "pipe",
104
+ timeout: PROBE_TIMEOUT_MS,
105
+ });
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ function buildFixHint(tokenPresent: boolean): string {
113
+ const base =
114
+ `npm token for the @warpgogol scope cannot fetch ${PROBE_PACKAGE}. ` +
115
+ `Set NPM_TOKEN env or replace YOUR_NPM_TOKEN in .npmrc with a valid npm read token ` +
116
+ `(issued at workshop provisioning — see NEXT_STEPS.md §Token lifecycle).`;
117
+ return tokenPresent
118
+ ? `${base} A token is configured but was rejected — it may be expired or revoked; request a fresh one.`
119
+ : base;
120
+ }
121
+
122
+ /**
123
+ * Run the npm-token probe: parse `.npmrc`, then verify the registry actually
124
+ * serves the package. Non-interactive — never prompts, never reads argv.
125
+ */
126
+ export async function checkNpmToken(
127
+ workspaceRoot: string,
128
+ deps: NpmTokenProbeDeps = {},
129
+ ): Promise<NpmTokenCheck> {
130
+ const env = deps.env ?? process.env;
131
+ const fetchVersion = deps.fetchVersion ?? defaultFetchVersion;
132
+ const { registry, tokenPresent } = readNpmrcTokenStatus(workspaceRoot, env);
133
+ const canFetch = fetchVersion(workspaceRoot);
134
+ return {
135
+ registry,
136
+ tokenPresent,
137
+ canFetch,
138
+ fixHint: canFetch ? null : buildFixHint(tokenPresent),
139
+ };
140
+ }
@@ -22,6 +22,7 @@ import fs from "node:fs";
22
22
  import path from "node:path";
23
23
  import { execSync } from "node:child_process";
24
24
  import { listStackProfiles, type StackProfile } from "../profiles/stack-profile.ts";
25
+ import { checkNpmToken, workshopNeedsWarpgogolToken } from "./npm-token-check.ts";
25
26
  import { resolveForgeRoot } from "../config/forge-config.ts";
26
27
  import type {
27
28
  ForgeCommandInput,
@@ -166,6 +167,16 @@ export async function runScaffoldProject(
166
167
  }
167
168
  }
168
169
 
170
+ // RFC-1125: non-interactive npm-token probe — fail fast before pnpm install when
171
+ // the @warpgogol scope is needed but the token cannot fetch @warpgogol/werkstatt-engine.
172
+ // Runs after .npmrc is materialized so the probe sees the final token state.
173
+ if (profile.install.length > 0 && workshopNeedsWarpgogolToken(workspaceRoot, profile.install)) {
174
+ const tokenCheck = await checkNpmToken(workspaceRoot);
175
+ if (!tokenCheck.canFetch) {
176
+ return fail("forge.scaffold", profileId, tokenCheck.fixHint!, errors, outputFormat, logger);
177
+ }
178
+ }
179
+
169
180
  // Run ALL install commands AFTER all files are created (atomicity: no partial state if install fails)
170
181
  // Root workspace install commands
171
182
  if (outputFormat === "pretty" && profile.install.length > 0) {
@@ -0,0 +1,84 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for the site-workshop stack profile (RFC-1125) — verifies the profile parses, scaffolds the workshop contract, and never scaffolds systems-cache inside the workshop root.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-1125: initial site-workshop profile tests (AC-1 evidence).</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect } from "vitest";
11
+ import { join } from "node:path";
12
+ import { listStackProfiles } from "../profiles/stack-profile.ts";
13
+
14
+ // Resolve forge root from this test file's location
15
+ const FORGE_ROOT = join(import.meta.dirname, "..", "..");
16
+
17
+ function loadSiteWorkshop() {
18
+ const profiles = listStackProfiles(FORGE_ROOT);
19
+ const profile = profiles.find((p) => p.id === "site-workshop");
20
+ expect(profile, "site-workshop profile must exist in packages/forge/profiles/").toBeDefined();
21
+ return profile!;
22
+ }
23
+
24
+ test("site-workshop profile parses and is discoverable", () => {
25
+ const profile = loadSiteWorkshop();
26
+ expect(profile.schema).toBe("forge/stack-profile@1");
27
+ expect(profile.displayName).toBeTruthy();
28
+ });
29
+
30
+ test("scaffolds kernel wiring, missions/, and governance dirs — but never systems-cache", () => {
31
+ const profile = loadSiteWorkshop();
32
+ const dirs = profile.workspace.dirs;
33
+ const filePaths = profile.workspace.files.map((f) => f.path);
34
+
35
+ // AC-1: kernel wiring + missions + governance dirs present
36
+ expect(dirs).toContain("missions");
37
+ expect(dirs).toContain("docs");
38
+ expect(dirs).toContain("tools");
39
+ expect(filePaths).toContain("tools/kernel.config.ts");
40
+ expect(filePaths).toContain("missions/.gitkeep");
41
+
42
+ // AC-1: systems-cache is a sibling (../systems-cache), never scaffolded inside root
43
+ expect(
44
+ dirs,
45
+ "systems-cache must not be a scaffolded dir — sibling convention per RFC-1125",
46
+ ).not.toContain("systems-cache");
47
+ expect(
48
+ filePaths.some((p) => p.startsWith("systems-cache")),
49
+ "no systems-cache files may be scaffolded — created by sternsystem.register on demand",
50
+ ).toBe(false);
51
+ });
52
+
53
+ test("scaffolded kernel.config.ts consumes the single-sourced WorkshopModuleMap", () => {
54
+ const profile = loadSiteWorkshop();
55
+ const kernelConfig = profile.workspace.files.find((f) => f.path === "tools/kernel.config.ts");
56
+ expect(kernelConfig, "profile must scaffold tools/kernel.config.ts").toBeDefined();
57
+ // AC-5: the scaffolded config spreads workshopModuleLoaders() — no hand-copied loader list
58
+ expect(kernelConfig!.content).toContain("workshopModuleLoaders");
59
+ expect(kernelConfig!.content).toContain("@warpgogol/werkstatt-engine/kernel/module-map");
60
+ expect(
61
+ kernelConfig!.content,
62
+ "kernel.config.ts must not embed per-module loader thunks — they live in WORKSHOP_MODULE_MAP",
63
+ ).not.toContain("createMissionModule");
64
+ });
65
+
66
+ test("install set includes engine, site plugin, and forge", () => {
67
+ const profile = loadSiteWorkshop();
68
+ const install = profile.install.join(" ");
69
+ expect(install).toContain("@warpgogol/werkstatt-engine");
70
+ expect(install).toContain("@warpgogol/werkstatt-site");
71
+ expect(install).toContain("@warpgogol/forge");
72
+ });
73
+
74
+ test("ships .npmrc token template and site-workshop nextSteps guidance", () => {
75
+ const profile = loadSiteWorkshop();
76
+ const npmrc = profile.workspace.files.find((f) => f.path === ".npmrc");
77
+ expect(npmrc, "profile must scaffold .npmrc for the private @warpgogol scope").toBeDefined();
78
+ expect(npmrc!.content).toContain("@warpgogol:registry");
79
+
80
+ // RFC-1125: token lifecycle + identity.bootstrap guidance lives in NEXT_STEPS.md
81
+ expect(profile.nextSteps, "profile must provide nextSteps for token/identity onboarding").toBeTruthy();
82
+ expect(profile.nextSteps).toContain("NPM_TOKEN");
83
+ expect(profile.nextSteps).toContain("identity.bootstrap");
84
+ });
@@ -363,6 +363,7 @@ export const stackProfileDomainFieldsSchema = z.object({
363
363
  templates: z.array(profileTemplateSchema).optional(),
364
364
  scriptDir: z.string().min(1).optional(),
365
365
  rootAgentsMdTemplate: z.string().optional(),
366
+ nextSteps: z.string().optional(),
366
367
  compass: compassPolicyOverridesSchema.optional(),
367
368
  });
368
369
 
@@ -380,5 +381,6 @@ export interface StackProfileDomainFields {
380
381
  templates?: ProfileTemplate[];
381
382
  scriptDir?: string;
382
383
  rootAgentsMdTemplate?: string;
384
+ nextSteps?: string;
383
385
  compass?: CompassPolicyOverrides;
384
386
  }
@@ -80,6 +80,8 @@ export const stackProfileSchema = z.object({
80
80
  scriptDir: stackProfileDomainFieldsSchema.shape.scriptDir,
81
81
  // Profile-driven root AGENTS.md template
82
82
  rootAgentsMdTemplate: stackProfileDomainFieldsSchema.shape.rootAgentsMdTemplate,
83
+ // RFC-1125: profile-driven NEXT_STEPS.md content override
84
+ nextSteps: stackProfileDomainFieldsSchema.shape.nextSteps,
83
85
  // Profile-driven Compass file extensions
84
86
  compass: stackProfileDomainFieldsSchema.shape.compass,
85
87
  });
@@ -70,6 +70,48 @@ Test consequences.
70
70
  Test evolution.
71
71
  `;
72
72
 
73
+ const ADR_BODY_WITH_AC = (
74
+ id: string,
75
+ status: string,
76
+ createdAt: string,
77
+ acBlock: string,
78
+ ): string => `---
79
+ id: ${id}
80
+ title: "Test ADR"
81
+ status: ${status}
82
+ scope: workspace
83
+ decider: human:test
84
+ createdAt: ${createdAt}
85
+ updatedAt: ${createdAt}
86
+ ---
87
+
88
+ # ${id}: Test ADR
89
+
90
+ ## Context
91
+
92
+ Test context.
93
+
94
+ ## Decision
95
+
96
+ Test decision.
97
+
98
+ ## Justification
99
+
100
+ Test justification.
101
+
102
+ ## Consequences
103
+
104
+ Test consequences.
105
+
106
+ ## Evolution
107
+
108
+ Test evolution.
109
+
110
+ ## Acceptance criteria
111
+
112
+ ${acBlock}
113
+ `;
114
+
73
115
  async function makeGitRepo(dir: string): Promise<void> {
74
116
  execFileSync("git", ["init"], { cwd: dir, timeout: 5000 });
75
117
  execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir, timeout: 5000 });
@@ -295,6 +337,114 @@ describe("adr.implement.stamp", () => {
295
337
  expect(result.data?.violations.some((v) => v.rule === "ADR-IMP-05")).toBe(true);
296
338
  });
297
339
 
340
+ // ── ADR-IMP-02: acceptance criteria completeness (post-cutoff, mirrors AV-17) ──
341
+
342
+ it("rejects post-cutoff ADR with unchecked acceptance criteria", async () => {
343
+ await writeFile(
344
+ join(workspaceRoot, "docs", "adrs", "adr-0001-test.md"),
345
+ ADR_BODY_WITH_AC(
346
+ "ADR-0001",
347
+ "accepted",
348
+ "2026-09-10",
349
+ "- [ ] AC-1: THE artifact SHALL exist (evidence: file: x.ts:1)\n- [x] AC-2: THE other SHALL pass (evidence: file: y.ts:2)",
350
+ ),
351
+ );
352
+ const sha = await commitAll(workspaceRoot, "implement: ADR-0001 add feature");
353
+
354
+ const result = await runAdrImplementStamp(
355
+ makeInput("ADR-0001", sha),
356
+ makeContext(workspaceRoot),
357
+ );
358
+
359
+ expect(result.data?.status).toBe("fail");
360
+ expect(result.data?.violations.some((v) => v.rule === "ADR-IMP-02")).toBe(true);
361
+ expect(result.exitCode).toBe(1);
362
+ });
363
+
364
+ it("rejects post-cutoff ADR with checked criteria lacking evidence annotation", async () => {
365
+ await writeFile(
366
+ join(workspaceRoot, "docs", "adrs", "adr-0001-test.md"),
367
+ ADR_BODY_WITH_AC(
368
+ "ADR-0001",
369
+ "accepted",
370
+ "2026-09-10",
371
+ "- [x] AC-1: THE artifact SHALL exist",
372
+ ),
373
+ );
374
+ const sha = await commitAll(workspaceRoot, "implement: ADR-0001 add feature");
375
+
376
+ const result = await runAdrImplementStamp(
377
+ makeInput("ADR-0001", sha),
378
+ makeContext(workspaceRoot),
379
+ );
380
+
381
+ expect(result.data?.status).toBe("fail");
382
+ expect(result.data?.violations.some((v) => v.rule === "ADR-IMP-02")).toBe(true);
383
+ });
384
+
385
+ it("stamps post-cutoff ADR when all criteria are checked with evidence", async () => {
386
+ const adrPath = join(workspaceRoot, "docs", "adrs", "adr-0001-test.md");
387
+ await writeFile(
388
+ adrPath,
389
+ ADR_BODY_WITH_AC(
390
+ "ADR-0001",
391
+ "accepted",
392
+ "2026-09-10",
393
+ "- [x] AC-1: THE artifact SHALL exist (evidence: file: x.ts:1)\n- [x] AC-2: THE other SHALL pass (evidence: file: y.ts:2)",
394
+ ),
395
+ );
396
+ const sha = await commitAll(workspaceRoot, "implement: ADR-0001 add feature");
397
+
398
+ const result = await runAdrImplementStamp(
399
+ makeInput("ADR-0001", sha),
400
+ makeContext(workspaceRoot),
401
+ );
402
+
403
+ expect(result.data?.status).toBe("pass");
404
+ expect(result.data?.data?.criteriaChecked).toBe(2);
405
+
406
+ const afterContent = await readFile(adrPath, "utf-8");
407
+ expect(afterContent).toContain("status: implemented");
408
+ });
409
+
410
+ it("stamps pre-cutoff ADR with unchecked criteria (AV-17 exempt)", async () => {
411
+ await writeFile(
412
+ join(workspaceRoot, "docs", "adrs", "adr-0001-test.md"),
413
+ ADR_BODY_WITH_AC(
414
+ "ADR-0001",
415
+ "accepted",
416
+ "2026-08-01",
417
+ "- [ ] AC-1: THE artifact SHALL exist (evidence: file: x.ts:1)",
418
+ ),
419
+ );
420
+ const sha = await commitAll(workspaceRoot, "implement: ADR-0001 add feature");
421
+
422
+ const result = await runAdrImplementStamp(
423
+ makeInput("ADR-0001", sha),
424
+ makeContext(workspaceRoot),
425
+ );
426
+
427
+ expect(result.data?.status).toBe("pass");
428
+ expect(result.data?.data?.criteriaChecked).toBeUndefined();
429
+ });
430
+
431
+ it("stamps post-cutoff ADR without an acceptance criteria section", async () => {
432
+ const adrPath = join(workspaceRoot, "docs", "adrs", "adr-0001-test.md");
433
+ const body = ADR_BODY("ADR-0001", "accepted").replace(
434
+ "createdAt: 2026-08-01",
435
+ "createdAt: 2026-09-10",
436
+ );
437
+ await writeFile(adrPath, body);
438
+ const sha = await commitAll(workspaceRoot, "implement: ADR-0001 add feature");
439
+
440
+ const result = await runAdrImplementStamp(
441
+ makeInput("ADR-0001", sha),
442
+ makeContext(workspaceRoot),
443
+ );
444
+
445
+ expect(result.data?.status).toBe("pass");
446
+ });
447
+
298
448
  // ── Dry-run ──────────────────────────────────────────────────────────────────
299
449
 
300
450
  it("dry-run checks preconditions without mutating file", async () => {
@@ -157,7 +157,7 @@ test("artifacts with missing required extensions field fails", () => {
157
157
 
158
158
  test("all shipped profiles parse without changes", () => {
159
159
  const profiles = listStackProfiles(FORGE_ROOT);
160
- expect(profiles.length).toBe(5);
160
+ expect(profiles.length).toBe(6);
161
161
  for (const profile of profiles) {
162
162
  expect(profile.id).toBeDefined();
163
163
  expect(profile.workspace.dirs.length).toBeGreaterThan(0);