@warpgogol/forge 5.1.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -2
- package/bin/cli.ts +4 -0
- package/dist/bin/cli.js +4 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/adr/handlers/implement-stamp.js +32 -2
- package/dist/os/adr/handlers/implement-stamp.js.map +1 -1
- package/dist/os/adr/types.d.ts +2 -1
- package/dist/os/adr/types.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +2 -0
- package/dist/os/naming/naming-convention.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts +4 -0
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -0
- package/dist/os/queue/handlers/queue-validate.js +62 -0
- package/dist/os/queue/handlers/queue-validate.js.map +1 -0
- package/dist/os/queue/index.d.ts +5 -0
- package/dist/os/queue/index.d.ts.map +1 -0
- package/dist/os/queue/index.js +18 -0
- package/dist/os/queue/index.js.map +1 -0
- package/dist/os/queue/manifest.d.ts +14 -0
- package/dist/os/queue/manifest.d.ts.map +1 -0
- package/dist/os/queue/manifest.js +110 -0
- package/dist/os/queue/manifest.js.map +1 -0
- package/dist/os/queue/queue.module.d.ts +3 -0
- package/dist/os/queue/queue.module.d.ts.map +1 -0
- package/dist/os/queue/queue.module.js +56 -0
- package/dist/os/queue/queue.module.js.map +1 -0
- package/dist/os/queue/types.d.ts +25 -0
- package/dist/os/queue/types.d.ts.map +1 -0
- package/dist/os/queue/types.js +28 -0
- package/dist/os/queue/types.js.map +1 -0
- package/dist/os/rfc/frontmatter-io.d.ts.map +1 -1
- package/dist/os/rfc/frontmatter-io.js +1 -0
- package/dist/os/rfc/frontmatter-io.js.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.d.ts +2 -7
- package/dist/os/rfc/handlers/pipeline-status.d.ts.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.js +15 -57
- package/dist/os/rfc/handlers/pipeline-status.js.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.js +6 -1
- package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
- package/dist/os/werkstatt/handlers/workspace-deps.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/workspace-deps.js +4 -3
- package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -1
- package/dist/src/onboarding/create.d.ts.map +1 -1
- package/dist/src/onboarding/create.js +5 -2
- package/dist/src/onboarding/create.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +15 -0
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts +1 -0
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/npm-token-check.d.ts +40 -0
- package/dist/src/onboarding/npm-token-check.d.ts.map +1 -0
- package/dist/src/onboarding/npm-token-check.js +104 -0
- package/dist/src/onboarding/npm-token-check.js.map +1 -0
- package/dist/src/onboarding/scaffold-project.d.ts.map +1 -1
- package/dist/src/onboarding/scaffold-project.js +10 -0
- package/dist/src/onboarding/scaffold-project.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +62 -0
- package/dist/src/pipeline-status.d.ts.map +1 -0
- package/dist/src/pipeline-status.js +220 -0
- package/dist/src/pipeline-status.js.map +1 -0
- package/dist/src/profiles/profile-schema.d.ts +2 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +1 -0
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +1 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +2 -0
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/os/adr/handlers/implement-stamp.ts +34 -2
- package/os/adr/types.ts +3 -1
- package/os/naming/naming-convention.ts +2 -0
- package/os/queue/handlers/queue-validate.ts +83 -0
- package/os/queue/index.ts +19 -0
- package/os/queue/manifest.ts +183 -0
- package/os/queue/queue-validate.test.ts +177 -0
- package/os/queue/queue.module.ts +59 -0
- package/os/queue/types.ts +49 -0
- package/os/rfc/frontmatter-io.ts +1 -0
- package/os/rfc/handlers/pipeline-status.ts +24 -67
- package/os/rfc/handlers/validate-rules.ts +8 -1
- package/os/werkstatt/handlers/workspace-deps.ts +4 -3
- package/package.json +17 -1
- package/profiles/site-workshop.yaml +654 -0
- package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +1 -1
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +46 -1
- package/src/onboarding/create.ts +5 -2
- package/src/onboarding/doctor-token-check.test.ts +124 -0
- package/src/onboarding/doctor.ts +17 -0
- package/src/onboarding/init.ts +1 -0
- package/src/onboarding/npm-token-check.ts +140 -0
- package/src/onboarding/scaffold-project.ts +11 -0
- package/src/onboarding/site-workshop-profile.test.ts +84 -0
- package/src/pipeline-status.test.ts +192 -0
- package/src/pipeline-status.ts +319 -0
- package/src/profiles/profile-schema.ts +2 -0
- package/src/profiles/stack-profile.ts +2 -0
- package/src/tests/adr-implement-stamp.test.ts +150 -0
- package/src/tests/profile-schema.test.ts +1 -1
|
@@ -8,6 +8,7 @@ dependsOn: ['my-preferences']
|
|
|
8
8
|
languagePolicy: ref(PREFERENCES.md)
|
|
9
9
|
triggers: ["I just want to see the result", "run the full pipeline automatically", "implement this end-to-end without pauses"]
|
|
10
10
|
---
|
|
11
|
+
|
|
11
12
|
<!--
|
|
12
13
|
<MODULE_CONTRACT>
|
|
13
14
|
<purpose>fo-idea-i-just-want-to-see-the-result skill — Orchestrate the full feature pipeline (idea, audit, enhance, plan, implement, review, fix) in one invocation. Accepts a raw idea or RFC/ADR id. Use when the operator wants the complete pipeline.</purpose>
|
|
@@ -19,10 +20,15 @@ triggers: ["I just want to see the result", "run the full pipeline automatically
|
|
|
19
20
|
<item>RFC-1097: sweep — SKILL.md headers + classification fixes
|
|
20
21
|
|
|
21
22
|
Sweep batch 1: add Compass v2 headers to 45 SKILL.md files (purpose derived from frontmatter description). Fix non-skill-markdown exclusion to check filename not workspace-relative path (packages/AGENTS.md escaped it). Add .coverage to ignoredDirs.</item>
|
|
23
|
+
<item>RFC-1140: step 5 — queue-mode section in orchestrator SKILL.md
|
|
24
|
+
|
|
25
|
+
Add Queue mode section (pre-flight queue.validate, loop semantics, failure handling, manifest immutability) to fo-idea-i-just-want-to-see-the-result SKILL.md, sync .agents copy, add forgeQueueModule row to forge AGENTS.md.</item>
|
|
26
|
+
<item>RFC-1140: queue mode — materialize manifest at invocation from pasted doc list
|
|
27
|
+
|
|
28
|
+
When the invocation carries >=2 RFC/ADR ids without a manifest path, the orchestrator builds docs/queues/session-<timestamp>.yaml from the pasted order, runs queue.validate, then processes queue mode. Unresolvable ids are named explicitly.</item>
|
|
22
29
|
</CHANGE_SUMMARY>
|
|
23
30
|
-->
|
|
24
31
|
|
|
25
|
-
|
|
26
32
|
# Full Pipeline — Just Want to See the Result
|
|
27
33
|
|
|
28
34
|
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.
|
|
@@ -48,8 +54,47 @@ The operator may provide either:
|
|
|
48
54
|
|
|
49
55
|
- **A raw idea** — natural-language description of a feature, change, or decision. The skill will invoke `fo-idea` as step 0 to create the RFC/ADR first.
|
|
50
56
|
- **An existing RFC/ADR id** — e.g. `RFC-XXXX` or `ADR-XXXX`. The skill skips idea creation and starts the pipeline from the appropriate step.
|
|
57
|
+
- **A queue manifest** — `--queue <path>` or a `docs/queues/*.yaml` path in the invocation text. The skill enters queue mode (see §Queue mode) and processes the manifest's `items[]` in order.
|
|
58
|
+
- **A pasted document list** — >=2 `RFC-XXXX`/`ADR-XXXX` ids in the invocation text (e.g. another agent's ordered implementation plan). The skill materializes a session manifest (see §Queue mode → Manifest materialization) and enters queue mode.
|
|
51
59
|
- **Nothing** — if neither is provided, check session context and IDE for a recently created document. If none found, ask the operator: "Какую идею реализуем? Опишите идею или укажите RFC-XXXX / ADR-XXXX."
|
|
52
60
|
|
|
61
|
+
## Queue mode
|
|
62
|
+
|
|
63
|
+
The orchestrator runs in **queue mode** when the invocation carries `--queue <path>`, a `docs/queues/*.yaml` path, OR a pasted document list (>=2 `RFC-XXXX`/`ADR-XXXX` ids — e.g. another agent's ordered implementation plan).
|
|
64
|
+
|
|
65
|
+
**Manifest materialization (mandatory when no manifest path is given):** Build the manifest from the invocation text before anything else:
|
|
66
|
+
|
|
67
|
+
1. Extract `RFC-\d{4}`/`ADR-\d{4}` tokens in order of appearance; dedupe preserving first-seen order. The pasted order IS the dependency order — preserve it even when the surrounding prose discusses a different grouping.
|
|
68
|
+
2. Write `docs/queues/session-<YYYYMMDD>-<HHmm>.yaml`:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
id: session-<YYYYMMDD>-<HHmm>
|
|
72
|
+
createdAt: <YYYY-MM-DD>
|
|
73
|
+
items:
|
|
74
|
+
- id: RFC-XXXX
|
|
75
|
+
- id: ADR-XXXX
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `id` field MUST equal the filename stem (`queue.validate` enforces this). Leave the file uncommitted — it is a session artifact; the operator may commit it for cross-session durability.
|
|
79
|
+
|
|
80
|
+
3. If the paste mixes ids with prose, ignore the prose — only the id sequence matters. Do not invent items that are not in the text.
|
|
81
|
+
|
|
82
|
+
**Pre-flight (mandatory):** Run `pnpm exec werkstatt run queue.validate --file <manifest> --json`. If the command exits non-zero (schema errors, unknown ids, duplicates, id↔filename mismatch), do NOT start the batch — report the errors and stop. When a pasted list references ids that do not resolve (QUEUE-04), name them explicitly — the operator must create those documents first or fix the list.
|
|
83
|
+
|
|
84
|
+
**Batch plan preview:** Emit the existing batch plan preview per `_shared/fo-pipeline-conventions.md` §Batch plan preview, listing items in manifest order.
|
|
85
|
+
|
|
86
|
+
**Loop semantics:** Process `items[]` in manifest order with no pauses between items:
|
|
87
|
+
|
|
88
|
+
1. **Skip terminal items** — items whose derived status is `implemented` or `skipped` (rejected/superseded frontmatter) are recorded in the batch summary and skipped.
|
|
89
|
+
2. **Resume mid-pipeline items** — an `in-progress` item resumes at its derived `pipelineStep`, not from step 1.
|
|
90
|
+
3. **Run the existing per-doc pipeline** — RFC items run audit → enhance → plan → implement; ADR items run implement only. All skill-internal interactions (grilling) stay inside the invoked skills.
|
|
91
|
+
4. **Batch-item checkpoint** — after each item, emit the context checkpoint per `_shared/fo-pipeline-conventions.md` §Context checkpoint between batch items.
|
|
92
|
+
5. **Clean-tree gate** — before starting the next item, verify the working tree has no uncommitted leftovers from the completed item; warn and stop if dirty.
|
|
93
|
+
|
|
94
|
+
**Failure:** If an item fails after its error checkpoint, stop the batch. The report names the blocked item id and the remaining item count. `blocked` is in-session report language only — never persist it into frontmatter, manifests, or files. Resume = re-invoke with the same manifest; `queue.validate` derives where to continue.
|
|
95
|
+
|
|
96
|
+
**Manifest immutability:** Do not edit `items[]` or the manifest during a queue run. Reordering requires stopping the batch and re-validating.
|
|
97
|
+
|
|
53
98
|
## Process
|
|
54
99
|
|
|
55
100
|
### 0. Idea creation (conditional)
|
package/src/onboarding/create.ts
CHANGED
|
@@ -94,6 +94,9 @@ function loadProfileDomainFields(profileId: string, forgeRoot: string): InitDoma
|
|
|
94
94
|
if (profile.compass) {
|
|
95
95
|
fields.compass = profile.compass;
|
|
96
96
|
}
|
|
97
|
+
if (profile.nextSteps) {
|
|
98
|
+
fields.nextSteps = profile.nextSteps;
|
|
99
|
+
}
|
|
97
100
|
return fields;
|
|
98
101
|
} catch {
|
|
99
102
|
return {};
|
|
@@ -384,9 +387,9 @@ export async function runCreate(
|
|
|
384
387
|
}
|
|
385
388
|
}
|
|
386
389
|
|
|
387
|
-
// 13. Write NEXT_STEPS.md (RFC-0550)
|
|
390
|
+
// 13. Write NEXT_STEPS.md (RFC-0550); RFC-1125: profile may override via nextSteps field
|
|
388
391
|
const nextStepsPath = path.join(targetDir, "NEXT_STEPS.md");
|
|
389
|
-
const nextStepsContent = `# Next Steps
|
|
392
|
+
const nextStepsContent = initDomainFields.nextSteps ?? `# Next Steps
|
|
390
393
|
|
|
391
394
|
Your Forge project is ready. The next step is mandatory: run \`/forge-bootstrap\` to configure your project interactively.
|
|
392
395
|
|
|
@@ -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
|
+
});
|
package/src/onboarding/doctor.ts
CHANGED
|
@@ -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
|
|
package/src/onboarding/init.ts
CHANGED
|
@@ -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
|
+
});
|