@warpgogol/forge 2.9.11 → 2.9.23
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/dist/bin/cli.js +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +1 -0
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/audit/audit.module.d.ts.map +1 -1
- package/dist/os/audit/audit.module.js +1 -0
- package/dist/os/audit/audit.module.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +1 -0
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-inventory-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-inventory-handler.js +82 -1
- package/dist/os/compass/handlers/compass-inventory-handler.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +16 -0
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/assets-helpers.js +1 -1
- package/dist/os/core/handlers/assets-helpers.js.map +1 -1
- package/dist/os/core/handlers/determinism-check.js +1 -1
- package/dist/os/core/handlers/determinism-check.js.map +1 -1
- package/dist/os/core/handlers/forge-autonomy-validate.d.ts +18 -0
- package/dist/os/core/handlers/forge-autonomy-validate.d.ts.map +1 -0
- package/dist/os/core/handlers/forge-autonomy-validate.js +78 -0
- package/dist/os/core/handlers/forge-autonomy-validate.js.map +1 -0
- package/dist/os/core/handlers/knowledge-compact.d.ts.map +1 -1
- package/dist/os/core/handlers/knowledge-compact.js +7 -1
- package/dist/os/core/handlers/knowledge-compact.js.map +1 -1
- package/dist/os/core/handlers/release-prepare.js +1 -1
- package/dist/os/core/handlers/release-prepare.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +1 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/mission/mission.module.d.ts.map +1 -1
- package/dist/os/mission/mission.module.js +1 -0
- package/dist/os/mission/mission.module.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +1 -0
- package/dist/os/naming/naming.module.js.map +1 -1
- package/dist/os/notes/notes.module.d.ts.map +1 -1
- package/dist/os/notes/notes.module.js +1 -0
- package/dist/os/notes/notes.module.js.map +1 -1
- package/dist/os/plan/plan.module.d.ts.map +1 -1
- package/dist/os/plan/plan.module.js +1 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts +25 -0
- package/dist/os/plugin/plugin.module.d.ts.map +1 -0
- package/dist/os/plugin/plugin.module.js +208 -0
- package/dist/os/plugin/plugin.module.js.map +1 -0
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +1 -0
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +1 -0
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +1 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +1 -0
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/workspace-deps.d.ts.map +1 -0
- package/dist/os/{core → werkstatt}/handlers/workspace-deps.js +4 -3
- package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -0
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +1 -0
- package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
- package/dist/os/workflow/workflow.module.d.ts.map +1 -1
- package/dist/os/workflow/workflow.module.js +1 -0
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/compass/contract-registry.d.ts +7 -0
- package/dist/src/compass/contract-registry.d.ts.map +1 -0
- package/dist/src/compass/contract-registry.js +93 -0
- package/dist/src/compass/contract-registry.js.map +1 -0
- package/dist/src/compass/types.d.ts +28 -0
- package/dist/src/compass/types.d.ts.map +1 -0
- package/dist/src/compass/types.js +14 -0
- package/dist/src/compass/types.js.map +1 -0
- package/dist/src/forge-module.d.ts +1 -0
- package/dist/src/forge-module.d.ts.map +1 -1
- package/dist/src/forge-module.js +2 -1
- package/dist/src/forge-module.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +89 -7
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +22 -1
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/scaffold.d.ts.map +1 -1
- package/dist/src/onboarding/scaffold.js +1 -0
- package/dist/src/onboarding/scaffold.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +7 -1
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/plugin/ForgePluginManifest.d.ts +38 -0
- package/dist/src/plugin/ForgePluginManifest.d.ts.map +1 -0
- package/dist/src/plugin/ForgePluginManifest.js +52 -0
- package/dist/src/plugin/ForgePluginManifest.js.map +1 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +28 -0
- package/dist/src/registry.js.map +1 -1
- package/dist/src/validators/port-validate.js +3 -3
- package/dist/src/validators/port-validate.js.map +1 -1
- package/os/adr/adr.module.ts +1 -0
- package/os/audit/audit.module.ts +1 -0
- package/os/compass/compass.module.ts +1 -0
- package/os/compass/handlers/compass-inventory-handler.ts +107 -1
- package/os/core/core.module.ts +18 -0
- package/os/core/handlers/assets-helpers.ts +1 -1
- package/os/core/handlers/determinism-check.ts +1 -1
- package/os/core/handlers/forge-autonomy-validate.ts +115 -0
- package/os/core/handlers/knowledge-compact.ts +6 -1
- package/os/core/handlers/release-prepare.ts +1 -1
- package/os/exploration/exploration.module.ts +1 -0
- package/os/mission/mission.module.ts +1 -0
- package/os/naming/naming.module.ts +1 -0
- package/os/notes/notes.module.ts +1 -0
- package/os/plan/plan.module.ts +1 -0
- package/os/plugin/plugin.module.ts +261 -0
- package/os/program/program.module.ts +1 -0
- package/os/rfc/rfc.module.ts +1 -0
- package/os/session/session.module.ts +1 -0
- package/os/spec/spec.module.ts +1 -0
- package/os/{core → werkstatt}/handlers/workspace-deps.ts +4 -3
- package/os/werkstatt/werkstatt.module.ts +1 -0
- package/os/workflow/workflow.module.ts +1 -0
- package/package.json +13 -3
- package/profiles/forge-shell.yaml +1 -1
- package/profiles/godot-csharp.yaml +13 -13
- package/profiles/knowledge-typescript-turborepo.yaml +1 -1
- package/profiles/phaser-turborepo.yaml +13 -13
- package/skills/fo/fo-add-tests/SKILL.md +54 -0
- package/skills/fo/fo-compass-annotate/SKILL.md +1 -0
- package/skills/fo/fo-idea-implement/SKILL.md +80 -0
- package/src/compass/contract-registry.ts +114 -0
- package/src/compass/types.ts +45 -0
- package/src/forge-module.ts +3 -1
- package/src/index.ts +17 -0
- package/src/onboarding/doctor.ts +89 -7
- package/src/onboarding/init.ts +20 -1
- package/src/onboarding/scaffold.ts +1 -0
- package/src/onboarding/templates/behavioral-layer-core.md +9 -0
- package/src/onboarding/upgrade.ts +6 -1
- package/src/plugin/ForgePluginManifest.ts +76 -0
- package/src/registry.ts +36 -0
- package/src/tests/contract-registry.test.ts +248 -0
- package/src/tests/discover-pack-skills.test.ts +112 -0
- package/src/tests/doctor-autonomy.test.ts +3 -3
- package/src/tests/fixtures/agents-generate-business-before.txt +9 -0
- package/src/tests/forge-autonomy-validate.test.ts +157 -0
- package/src/tests/plugin-manifest.test.ts +226 -0
- package/src/tests/upgrade.test.ts +5 -0
- package/src/validators/port-validate.ts +3 -3
- package/dist/os/core/handlers/workspace-deps.d.ts.map +0 -1
- package/dist/os/core/handlers/workspace-deps.js.map +0 -1
- /package/dist/os/{core → werkstatt}/handlers/workspace-deps.d.ts +0 -0
package/src/registry.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<item>RFC-0524: added optional knowledge?: string[] field for cumulative knowledge system. Added knowledge arrays to fo-site-scan and grilling.</item>
|
|
18
18
|
<item>RFC-0538: added fo-compass-annotate skill (22nd fo skill) — full-lifecycle Compass header management replacing removed compass.annotate, compass.clear, compass.markup.migrate, compass.invariant.add kernel commands.</item>
|
|
19
19
|
<item>RFC-0539: added discoverPackSkills helper and PackSkillEntry interface for project-declared skill packs. Removed mission-complete and fo-site-scan entries (relocated to warpgogol-skills as wg- pack skills).</item>
|
|
20
|
+
<item>RFC-0941: discoverPackSkills now requires forge.plugin.yaml manifest at each pack root — throws on missing or invalid manifest (fail-fast).</item>
|
|
20
21
|
</CHANGE_SUMMARY>
|
|
21
22
|
*/
|
|
22
23
|
|
|
@@ -24,6 +25,7 @@ import fs from "node:fs";
|
|
|
24
25
|
import path from "node:path";
|
|
25
26
|
import { parse as parseYaml } from "yaml";
|
|
26
27
|
import type { ForgeConfig } from "./config/forge-config.ts";
|
|
28
|
+
import { forgePluginManifestSchema } from "./plugin/ForgePluginManifest.ts";
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Every entry MUST be portable — it runs in any forge-bootstrapped project
|
|
@@ -69,6 +71,40 @@ export function discoverPackSkills(workspaceRoot: string, config: ForgeConfig):
|
|
|
69
71
|
const packDir = path.resolve(workspaceRoot, pack.dir);
|
|
70
72
|
if (!fs.existsSync(packDir)) continue;
|
|
71
73
|
|
|
74
|
+
const manifestPath = path.join(packDir, "forge.plugin.yaml");
|
|
75
|
+
if (!fs.existsSync(manifestPath)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`forge.plugin.yaml not found at ${packDir} for pack prefix "${pack.prefix}". ` +
|
|
78
|
+
"Run `forge init` to create manifests for declared skill packs (RFC-0941).",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let manifestRaw: string;
|
|
83
|
+
try {
|
|
84
|
+
manifestRaw = fs.readFileSync(manifestPath, "utf-8");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Failed to read forge.plugin.yaml at ${manifestPath}: ${(err as Error).message}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let manifestParsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
manifestParsed = parseYaml(manifestRaw);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`forge.plugin.yaml at ${manifestPath} is not valid YAML: ${(err as Error).message}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const manifestResult = forgePluginManifestSchema.safeParse(manifestParsed);
|
|
101
|
+
if (!manifestResult.success) {
|
|
102
|
+
const issues = manifestResult.error.issues
|
|
103
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
104
|
+
.join("; ");
|
|
105
|
+
throw new Error(`forge.plugin.yaml at ${manifestPath} failed schema validation: ${issues}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
const entries = fs.readdirSync(packDir, { withFileTypes: true });
|
|
73
109
|
for (const entry of entries) {
|
|
74
110
|
if (!entry.isDirectory()) continue;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
getBuiltInSpecs,
|
|
7
|
+
loadContractRegistry,
|
|
8
|
+
findApplicablePackSpecs,
|
|
9
|
+
fileMatchesGlobs,
|
|
10
|
+
} from "../compass/contract-registry.ts";
|
|
11
|
+
import type { ForgeConfig } from "../config/forge-config.ts";
|
|
12
|
+
|
|
13
|
+
function makeTmpWorkspace(): string {
|
|
14
|
+
return mkdtempSync(join(tmpdir(), "forge-contract-registry-"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeManifest(packDir: string, content: string): void {
|
|
18
|
+
mkdirSync(packDir, { recursive: true });
|
|
19
|
+
writeFileSync(join(packDir, "forge.plugin.yaml"), content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("contract-registry (RFC-0943)", () => {
|
|
23
|
+
describe("getBuiltInSpecs", () => {
|
|
24
|
+
test("returns MODULE_CONTRACT and CHANGE_SUMMARY as built-in specs", () => {
|
|
25
|
+
const specs = getBuiltInSpecs();
|
|
26
|
+
expect(specs).toHaveLength(2);
|
|
27
|
+
expect(specs[0]!.blockId).toBe("module-contract");
|
|
28
|
+
expect(specs[1]!.blockId).toBe("change-summary");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("module-contract has purpose tag with minWords 10", () => {
|
|
32
|
+
const specs = getBuiltInSpecs();
|
|
33
|
+
const moduleContract = specs.find((s) => s.blockId === "module-contract");
|
|
34
|
+
expect(moduleContract).toBeDefined();
|
|
35
|
+
expect(moduleContract!.requiredTags).toBeDefined();
|
|
36
|
+
const purposeTag = moduleContract!.requiredTags!.find((t) => t.name === "purpose");
|
|
37
|
+
expect(purposeTag).toBeDefined();
|
|
38
|
+
expect(purposeTag!.minWords).toBe(10);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("fileMatchesGlobs", () => {
|
|
43
|
+
test("matches a simple glob pattern", () => {
|
|
44
|
+
expect(fileMatchesGlobs("packages/my-pack/src/index.ts", ["packages/my-pack/**/*.ts"])).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("does not match when glob does not match", () => {
|
|
48
|
+
expect(fileMatchesGlobs("packages/other-pack/src/index.ts", ["packages/my-pack/**/*.ts"])).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("matches any of multiple globs", () => {
|
|
52
|
+
expect(
|
|
53
|
+
fileMatchesGlobs("services/my-service/src/index.ts", [
|
|
54
|
+
"packages/**/*.ts",
|
|
55
|
+
"services/**/*.ts",
|
|
56
|
+
]),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("handles dot files with dot option", () => {
|
|
61
|
+
expect(fileMatchesGlobs("packages/my-pack/.env.example", ["packages/**/*.example"])).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("loadContractRegistry", () => {
|
|
66
|
+
test("returns only built-in specs when no skillPacks declared", () => {
|
|
67
|
+
const registry = loadContractRegistry("/nonexistent", undefined);
|
|
68
|
+
expect(registry.builtIn).toHaveLength(2);
|
|
69
|
+
expect(registry.pack).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns only built-in specs when skillPacks is empty", () => {
|
|
73
|
+
const config: ForgeConfig = {
|
|
74
|
+
schema: "forge/bindings@1",
|
|
75
|
+
project: { name: "test", root: "." },
|
|
76
|
+
bindings: undefined,
|
|
77
|
+
skillPacks: [],
|
|
78
|
+
} as unknown as ForgeConfig;
|
|
79
|
+
const registry = loadContractRegistry("/nonexistent", config);
|
|
80
|
+
expect(registry.pack).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("loads pack-declared blocks from forge.plugin.yaml", () => {
|
|
84
|
+
const ws = makeTmpWorkspace();
|
|
85
|
+
try {
|
|
86
|
+
const packDir = join(ws, "my-pack");
|
|
87
|
+
writeManifest(
|
|
88
|
+
packDir,
|
|
89
|
+
"id: my-pack\nversion: 1.0.0\nextensionPoints:\n compass:\n contract:\n blocks:\n - blockId: api-contract\n requiredFor:\n - \"packages/my-pack/**/*.ts\"\n requiredTags:\n - name: endpoints\n minWords: 5\n",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const config: ForgeConfig = {
|
|
93
|
+
schema: "forge/bindings@1",
|
|
94
|
+
project: { name: "test", root: "." },
|
|
95
|
+
bindings: undefined,
|
|
96
|
+
skillPacks: [{ prefix: "my", dir: "my-pack" }],
|
|
97
|
+
} as unknown as ForgeConfig;
|
|
98
|
+
|
|
99
|
+
const registry = loadContractRegistry(ws, config);
|
|
100
|
+
expect(registry.pack).toHaveLength(1);
|
|
101
|
+
expect(registry.pack[0]!.blockId).toBe("api-contract");
|
|
102
|
+
expect(registry.pack[0]!.packId).toBe("my-pack");
|
|
103
|
+
expect(registry.pack[0]!.requiredTags).toEqual([{ name: "endpoints", minWords: 5 }]);
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(ws, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("throws on duplicate blockId across packs", () => {
|
|
110
|
+
const ws = makeTmpWorkspace();
|
|
111
|
+
try {
|
|
112
|
+
writeManifest(
|
|
113
|
+
join(ws, "pack-a"),
|
|
114
|
+
"id: pack-a\nversion: 1.0.0\nextensionPoints:\n compass:\n contract:\n blocks:\n - blockId: shared-block\n requiredFor:\n - \"packages/**/*.ts\"\n",
|
|
115
|
+
);
|
|
116
|
+
writeManifest(
|
|
117
|
+
join(ws, "pack-b"),
|
|
118
|
+
"id: pack-b\nversion: 1.0.0\nextensionPoints:\n compass:\n contract:\n blocks:\n - blockId: shared-block\n requiredFor:\n - \"services/**/*.ts\"\n",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const config: ForgeConfig = {
|
|
122
|
+
schema: "forge/bindings@1",
|
|
123
|
+
project: { name: "test", root: "." },
|
|
124
|
+
bindings: undefined,
|
|
125
|
+
skillPacks: [
|
|
126
|
+
{ prefix: "a", dir: "pack-a" },
|
|
127
|
+
{ prefix: "b", dir: "pack-b" },
|
|
128
|
+
],
|
|
129
|
+
} as unknown as ForgeConfig;
|
|
130
|
+
|
|
131
|
+
expect(() => loadContractRegistry(ws, config)).toThrow(/COMPASS-PLUGIN-DUP-01/);
|
|
132
|
+
} finally {
|
|
133
|
+
rmSync(ws, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("skips packs with no extensionPoints", () => {
|
|
138
|
+
const ws = makeTmpWorkspace();
|
|
139
|
+
try {
|
|
140
|
+
writeManifest(join(ws, "my-pack"), "id: my-pack\nversion: 1.0.0\n");
|
|
141
|
+
|
|
142
|
+
const config: ForgeConfig = {
|
|
143
|
+
schema: "forge/bindings@1",
|
|
144
|
+
project: { name: "test", root: "." },
|
|
145
|
+
bindings: undefined,
|
|
146
|
+
skillPacks: [{ prefix: "my", dir: "my-pack" }],
|
|
147
|
+
} as unknown as ForgeConfig;
|
|
148
|
+
|
|
149
|
+
const registry = loadContractRegistry(ws, config);
|
|
150
|
+
expect(registry.pack).toHaveLength(0);
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(ws, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("skips packs with empty blocks array", () => {
|
|
157
|
+
const ws = makeTmpWorkspace();
|
|
158
|
+
try {
|
|
159
|
+
writeManifest(
|
|
160
|
+
join(ws, "my-pack"),
|
|
161
|
+
"id: my-pack\nversion: 1.0.0\nextensionPoints:\n compass:\n contract:\n blocks: []\n",
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const config: ForgeConfig = {
|
|
165
|
+
schema: "forge/bindings@1",
|
|
166
|
+
project: { name: "test", root: "." },
|
|
167
|
+
bindings: undefined,
|
|
168
|
+
skillPacks: [{ prefix: "my", dir: "my-pack" }],
|
|
169
|
+
} as unknown as ForgeConfig;
|
|
170
|
+
|
|
171
|
+
const registry = loadContractRegistry(ws, config);
|
|
172
|
+
expect(registry.pack).toHaveLength(0);
|
|
173
|
+
} finally {
|
|
174
|
+
rmSync(ws, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("skips packs with missing manifest file", () => {
|
|
179
|
+
const ws = makeTmpWorkspace();
|
|
180
|
+
try {
|
|
181
|
+
const config: ForgeConfig = {
|
|
182
|
+
schema: "forge/bindings@1",
|
|
183
|
+
project: { name: "test", root: "." },
|
|
184
|
+
bindings: undefined,
|
|
185
|
+
skillPacks: [{ prefix: "my", dir: "nonexistent-pack" }],
|
|
186
|
+
} as unknown as ForgeConfig;
|
|
187
|
+
|
|
188
|
+
const registry = loadContractRegistry(ws, config);
|
|
189
|
+
expect(registry.pack).toHaveLength(0);
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(ws, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("findApplicablePackSpecs", () => {
|
|
197
|
+
test("returns specs whose requiredFor globs match the file path", () => {
|
|
198
|
+
const registry = {
|
|
199
|
+
builtIn: getBuiltInSpecs(),
|
|
200
|
+
pack: [
|
|
201
|
+
{
|
|
202
|
+
blockId: "api-contract",
|
|
203
|
+
requiredFor: ["packages/my-pack/**/*.ts"],
|
|
204
|
+
requiredTags: [],
|
|
205
|
+
packId: "my-pack",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
blockId: "service-contract",
|
|
209
|
+
requiredFor: ["services/**/*.ts"],
|
|
210
|
+
requiredTags: [],
|
|
211
|
+
packId: "other-pack",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const applicable = findApplicablePackSpecs(registry, "packages/my-pack/src/index.ts");
|
|
217
|
+
expect(applicable).toHaveLength(1);
|
|
218
|
+
expect(applicable[0]!.blockId).toBe("api-contract");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("returns empty array when no specs match", () => {
|
|
222
|
+
const registry = {
|
|
223
|
+
builtIn: getBuiltInSpecs(),
|
|
224
|
+
pack: [
|
|
225
|
+
{
|
|
226
|
+
blockId: "api-contract",
|
|
227
|
+
requiredFor: ["packages/my-pack/**/*.ts"],
|
|
228
|
+
requiredTags: [],
|
|
229
|
+
packId: "my-pack",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const applicable = findApplicablePackSpecs(registry, "packages/other-pack/src/index.ts");
|
|
235
|
+
expect(applicable).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("returns empty array when registry has no pack specs", () => {
|
|
239
|
+
const registry = {
|
|
240
|
+
builtIn: getBuiltInSpecs(),
|
|
241
|
+
pack: [],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const applicable = findApplicablePackSpecs(registry, "packages/my-pack/src/index.ts");
|
|
245
|
+
expect(applicable).toHaveLength(0);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { discoverPackSkills } from "../registry.ts";
|
|
6
|
+
import { loadForgeConfig } from "../config/forge-config.ts";
|
|
7
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
8
|
+
|
|
9
|
+
describe("discoverPackSkills fail-fast (RFC-0941)", () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-plugin-test-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function writeForgeYaml(skillPacks: Array<{ prefix: string; dir: string }>) {
|
|
21
|
+
const config = {
|
|
22
|
+
schema: "forge/config@1",
|
|
23
|
+
project: {
|
|
24
|
+
name: "test-project",
|
|
25
|
+
stack: [],
|
|
26
|
+
packageManager: "pnpm",
|
|
27
|
+
},
|
|
28
|
+
paths: {
|
|
29
|
+
rfcsDir: "docs/rfcs",
|
|
30
|
+
adrsDir: "docs/adrs",
|
|
31
|
+
plansDir: "docs/plans",
|
|
32
|
+
auditsDir: "docs/audits",
|
|
33
|
+
skillsDir: ".agents/skills",
|
|
34
|
+
},
|
|
35
|
+
skillPacks,
|
|
36
|
+
};
|
|
37
|
+
fs.writeFileSync(path.join(tmpDir, "forge.yaml"), stringifyYaml(config), "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createPackDir(prefix: string, dir: string) {
|
|
41
|
+
const packDir = path.resolve(tmpDir, dir);
|
|
42
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
43
|
+
return packDir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeManifest(packDir: string, id: string, version: string) {
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(packDir, "forge.plugin.yaml"),
|
|
49
|
+
stringifyYaml({ id, version }),
|
|
50
|
+
"utf8",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeSkill(packDir: string, prefix: string, skillName: string) {
|
|
55
|
+
const skillDir = path.join(packDir, `${prefix}-${skillName}`);
|
|
56
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(skillDir, "SKILL.md"),
|
|
59
|
+
`---\nname: ${prefix}-${skillName}\ndescription: Test skill\nconcerns: read-only\n---\n`,
|
|
60
|
+
"utf8",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("throws on missing forge.plugin.yaml manifest", () => {
|
|
65
|
+
const packDir = createPackDir("wg", "skills/wg");
|
|
66
|
+
writeSkill(packDir, "wg", "test");
|
|
67
|
+
writeForgeYaml([{ prefix: "wg", dir: "skills/wg" }]);
|
|
68
|
+
|
|
69
|
+
const config = loadForgeConfig(tmpDir);
|
|
70
|
+
expect(() => discoverPackSkills(tmpDir, config)).toThrow(/forge\.plugin\.yaml not found/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("throws on invalid manifest (bad id)", () => {
|
|
74
|
+
const packDir = createPackDir("wg", "skills/wg");
|
|
75
|
+
writeManifest(packDir, "Wg", "1.0.0");
|
|
76
|
+
writeSkill(packDir, "wg", "test");
|
|
77
|
+
writeForgeYaml([{ prefix: "wg", dir: "skills/wg" }]);
|
|
78
|
+
|
|
79
|
+
const config = loadForgeConfig(tmpDir);
|
|
80
|
+
expect(() => discoverPackSkills(tmpDir, config)).toThrow(/failed schema validation/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("throws on invalid manifest (bad version)", () => {
|
|
84
|
+
const packDir = createPackDir("wg", "skills/wg");
|
|
85
|
+
writeManifest(packDir, "wg", "not-semver");
|
|
86
|
+
writeSkill(packDir, "wg", "test");
|
|
87
|
+
writeForgeYaml([{ prefix: "wg", dir: "skills/wg" }]);
|
|
88
|
+
|
|
89
|
+
const config = loadForgeConfig(tmpDir);
|
|
90
|
+
expect(() => discoverPackSkills(tmpDir, config)).toThrow(/failed schema validation/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("discovers skills when manifest is valid", () => {
|
|
94
|
+
const packDir = createPackDir("wg", "skills/wg");
|
|
95
|
+
writeManifest(packDir, "wg", "1.0.0");
|
|
96
|
+
writeSkill(packDir, "wg", "test");
|
|
97
|
+
writeForgeYaml([{ prefix: "wg", dir: "skills/wg" }]);
|
|
98
|
+
|
|
99
|
+
const config = loadForgeConfig(tmpDir);
|
|
100
|
+
const skills = discoverPackSkills(tmpDir, config);
|
|
101
|
+
expect(skills.length).toBe(1);
|
|
102
|
+
expect(skills[0].name).toBe("wg-test");
|
|
103
|
+
expect(skills[0].pack).toBe("wg");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("returns empty array when no skillPacks declared", () => {
|
|
107
|
+
writeForgeYaml([]);
|
|
108
|
+
const config = loadForgeConfig(tmpDir);
|
|
109
|
+
const skills = discoverPackSkills(tmpDir, config);
|
|
110
|
+
expect(skills).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -67,7 +67,7 @@ test("doctor detects @warpgogol/* import specifiers", async () => {
|
|
|
67
67
|
);
|
|
68
68
|
await writeFile(
|
|
69
69
|
join(tempDir, "packages", "forge", "src", "bad.ts"),
|
|
70
|
-
'import { foo } from "@warpgogol/werkstatt/kernel";\nexport const x = foo;',
|
|
70
|
+
'import { foo } from "@warpgogol/werkstatt-engine/kernel";\nexport const x = foo;',
|
|
71
71
|
"utf8",
|
|
72
72
|
);
|
|
73
73
|
|
|
@@ -75,7 +75,7 @@ test("doctor detects @warpgogol/* import specifiers", async () => {
|
|
|
75
75
|
const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
|
|
76
76
|
expect(autonomyCheck?.status).toBe("fail");
|
|
77
77
|
expect(result.data?.forbiddenImports.length).toBe(1);
|
|
78
|
-
expect(result.data?.forbiddenImports[0]?.specifier).toBe("@warpgogol/werkstatt/kernel");
|
|
78
|
+
expect(result.data?.forbiddenImports[0]?.specifier).toBe("@warpgogol/werkstatt-engine/kernel");
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
test("doctor ignores @warpgogol/* in comments", async () => {
|
|
@@ -87,7 +87,7 @@ test("doctor ignores @warpgogol/* in comments", async () => {
|
|
|
87
87
|
);
|
|
88
88
|
await writeFile(
|
|
89
89
|
join(tempDir, "packages", "forge", "src", "clean.ts"),
|
|
90
|
-
"/* Do not import from @warpgogol/werkstatt/kernel */\nexport const x = 1;",
|
|
90
|
+
"/* Do not import from @warpgogol/werkstatt-engine/kernel */\nexport const x = 1;",
|
|
91
91
|
"utf8",
|
|
92
92
|
);
|
|
93
93
|
|
|
@@ -129,6 +129,15 @@ Session save mechanism:
|
|
|
129
129
|
|
|
130
130
|
The agent SHOULD auto-run `fo-review` after implementing a significant change. Review results are presented in creator language — only actionable issues are highlighted.
|
|
131
131
|
|
|
132
|
+
### Auto-testing
|
|
133
|
+
|
|
134
|
+
The agent SHOULD write tests for new code it produces. Tests are written for **agents**, not humans — when a test fails, the error message should tell an agent reading the console output what to fix and where, but only when the fix is not obvious from the assertion itself.
|
|
135
|
+
|
|
136
|
+
- **Significant changes** (new features, new functions, bug fixes) → write tests.
|
|
137
|
+
- **Minor edits** (typo fix, small CSS change, renaming a variable) → no new tests required.
|
|
138
|
+
- Use `fo-add-tests` for the full process and decision tree (example-based vs PBT).
|
|
139
|
+
- Do not weaken or delete existing tests without explicit operator direction.
|
|
140
|
+
|
|
132
141
|
### Context awareness
|
|
133
142
|
|
|
134
143
|
Before starting significant work, the agent SHOULD read recent ADRs, RFCs, and session transcripts (last 5-10 documents) to understand prior decisions and avoid conflicts. Minor edits do not require context reading.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge.autonomy.validate handler (RFC-0940, FORGE-AUTONOMY-01).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Does not test the pipeline wiring — that is covered by integration tests.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0940: initial unit tests for forge.autonomy.validate handler.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { runForgeAutonomyValidate } from "../../os/core/handlers/forge-autonomy-validate.ts";
|
|
18
|
+
import type { ForgeRuntimeContext } from "../types.ts";
|
|
19
|
+
|
|
20
|
+
function makeTmpWorkspace(): string {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), "forge-autonomy-"));
|
|
22
|
+
mkdirSync(join(dir, "packages", "forge", "os", "core", "handlers"), { recursive: true });
|
|
23
|
+
mkdirSync(join(dir, "packages", "forge", "os", "werkstatt", "handlers"), { recursive: true });
|
|
24
|
+
mkdirSync(join(dir, "packages", "forge", "os", "rfc"), { recursive: true });
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot,
|
|
31
|
+
logger: {
|
|
32
|
+
section: () => {},
|
|
33
|
+
info: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
success: () => {},
|
|
37
|
+
},
|
|
38
|
+
dryRun: false,
|
|
39
|
+
outputFormat: "pretty",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("forge.autonomy.validate", () => {
|
|
44
|
+
let workspace: string;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
workspace = makeTmpWorkspace();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("passes when no @warpgogol/werkstatt-engine imports exist", async () => {
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "clean.ts"),
|
|
57
|
+
`import { join } from "node:path";\nexport function foo() { return join("a", "b"); }\n`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
61
|
+
|
|
62
|
+
expect(result.data?.status).toBe("pass");
|
|
63
|
+
expect(result.data?.violations).toHaveLength(0);
|
|
64
|
+
expect(result.exitCode).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("fails when @warpgogol/werkstatt-engine is imported outside os/werkstatt/", async () => {
|
|
68
|
+
writeFileSync(
|
|
69
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "bad.ts"),
|
|
70
|
+
`import { foo } from "@warpgogol/werkstatt-engine/kernel";\nexport function bar() { return foo(); }\n`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
74
|
+
|
|
75
|
+
expect(result.data?.status).toBe("fail");
|
|
76
|
+
expect(result.data?.violations).toHaveLength(1);
|
|
77
|
+
expect(result.data?.violations[0]!.ruleId).toBe("FORGE-AUTONOMY-01");
|
|
78
|
+
expect(result.data?.violations[0]!.specifier).toBe("@warpgogol/werkstatt-engine/kernel");
|
|
79
|
+
expect(result.exitCode).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("passes when @warpgogol/werkstatt-engine is imported inside os/werkstatt/", async () => {
|
|
83
|
+
writeFileSync(
|
|
84
|
+
join(workspace, "packages", "forge", "os", "werkstatt", "handlers", "adapter.ts"),
|
|
85
|
+
`import { foo } from "@warpgogol/werkstatt-engine/kernel";\nexport function bar() { return foo(); }\n`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
89
|
+
|
|
90
|
+
expect(result.data?.status).toBe("pass");
|
|
91
|
+
expect(result.data?.violations).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("passes when @warpgogol/werkstatt-shared is imported (exempt)", async () => {
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "shared.ts"),
|
|
97
|
+
`import { scanDirectoryForImports } from "@warpgogol/werkstatt-shared/share/import-scan";\nexport function bar() { return scanDirectoryForImports; }\n`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
101
|
+
|
|
102
|
+
expect(result.data?.status).toBe("pass");
|
|
103
|
+
expect(result.data?.violations).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("passes for type-only imports (import type)", async () => {
|
|
107
|
+
writeFileSync(
|
|
108
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "type-only.ts"),
|
|
109
|
+
`import type { KernelModule } from "@warpgogol/werkstatt-engine/kernel";\nexport const x: KernelModule | null = null;\n`,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
113
|
+
|
|
114
|
+
expect(result.data?.status).toBe("pass");
|
|
115
|
+
expect(result.data?.violations).toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("passes for multi-line import type with specifier on different line", async () => {
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "multiline-type.ts"),
|
|
121
|
+
`import type {\n KernelModule,\n KernelPipelineStep,\n} from "@warpgogol/werkstatt-engine/kernel";\n`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
125
|
+
|
|
126
|
+
expect(result.data?.status).toBe("pass");
|
|
127
|
+
expect(result.data?.violations).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("does not scan test files", async () => {
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "bad.test.ts"),
|
|
133
|
+
`import { foo } from "@warpgogol/werkstatt-engine/kernel";\n`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
137
|
+
|
|
138
|
+
expect(result.data?.status).toBe("pass");
|
|
139
|
+
expect(result.data?.violations).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("reports multiple violations from different files", async () => {
|
|
143
|
+
writeFileSync(
|
|
144
|
+
join(workspace, "packages", "forge", "os", "core", "handlers", "bad1.ts"),
|
|
145
|
+
`import { foo } from "@warpgogol/werkstatt-engine";\n`,
|
|
146
|
+
);
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(workspace, "packages", "forge", "os", "rfc", "bad2.ts"),
|
|
149
|
+
`import { bar } from "@warpgogol/werkstatt-engine/release";\n`,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const result = await runForgeAutonomyValidate({ argv: [], flags: {} }, makeContext(workspace));
|
|
153
|
+
|
|
154
|
+
expect(result.data?.status).toBe("fail");
|
|
155
|
+
expect(result.data?.violations).toHaveLength(2);
|
|
156
|
+
});
|
|
157
|
+
});
|