@warpgogol/forge 5.3.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/cli.ts +7 -2
- package/dist/bin/cli.js +7 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +2 -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 +2 -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 +48 -16
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +39 -9
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
- package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-docs-validate.js +326 -0
- package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
- package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-inventory.js +5 -2
- package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
- package/dist/os/compass/handlers/summary-record.d.ts +17 -3
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +107 -10
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.d.ts +18 -0
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.d.ts.map +1 -0
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.js +64 -0
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.js.map +1 -0
- package/dist/os/compass/policy.d.ts +6 -1
- package/dist/os/compass/policy.d.ts.map +1 -1
- package/dist/os/compass/policy.js +8 -2
- package/dist/os/compass/policy.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +124 -73
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +3 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +1 -2
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +9 -2
- 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 +17 -6
- 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 +2 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts.map +1 -1
- package/dist/os/plugin/plugin.module.js +9 -2
- package/dist/os/plugin/plugin.module.js.map +1 -1
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +19 -4
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
- package/dist/os/queue/handlers/queue-validate.js +154 -7
- package/dist/os/queue/handlers/queue-validate.js.map +1 -1
- package/dist/os/queue/manifest.d.ts +17 -1
- package/dist/os/queue/manifest.d.ts.map +1 -1
- package/dist/os/queue/manifest.js +127 -6
- package/dist/os/queue/manifest.js.map +1 -1
- package/dist/os/queue/queue.module.d.ts.map +1 -1
- package/dist/os/queue/queue.module.js +32 -4
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/queue/types.d.ts +100 -1
- package/dist/os/queue/types.d.ts.map +1 -1
- package/dist/os/queue/types.js +44 -0
- package/dist/os/queue/types.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +28 -5
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/handlers/validate.js +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +27 -3
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +11 -0
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +1 -0
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/rfc/verification-evidence.d.ts +21 -0
- package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
- package/dist/os/rfc/verification-evidence.js +140 -23
- package/dist/os/rfc/verification-evidence.js.map +1 -1
- package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
- package/dist/os/rfc/verification-refresh.js +72 -37
- package/dist/os/rfc/verification-refresh.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +46 -5
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +5 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
- package/dist/os/spec/live-spec-merge.js +55 -134
- package/dist/os/spec/live-spec-merge.js.map +1 -1
- package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
- package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
- package/dist/os/spec/live-spec-rebuild.js +272 -0
- package/dist/os/spec/live-spec-rebuild.js.map +1 -0
- package/dist/os/spec/live-spec-shared.d.ts +62 -0
- package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
- package/dist/os/spec/live-spec-shared.js +315 -0
- package/dist/os/spec/live-spec-shared.js.map +1 -0
- package/dist/os/spec/live-spec-types.d.ts +17 -1
- package/dist/os/spec/live-spec-types.d.ts.map +1 -1
- package/dist/os/spec/live-spec-types.js +3 -0
- package/dist/os/spec/live-spec-types.js.map +1 -1
- package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
- package/dist/os/spec/live-spec-validate.js +105 -2
- package/dist/os/spec/live-spec-validate.js.map +1 -1
- package/dist/os/spec/spec-materialize.d.ts +2 -0
- package/dist/os/spec/spec-materialize.d.ts.map +1 -1
- package/dist/os/spec/spec-materialize.js +45 -5
- package/dist/os/spec/spec-materialize.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +19 -0
- package/dist/os/spec/spec-schema.d.ts.map +1 -1
- package/dist/os/spec/spec-schema.js +30 -0
- package/dist/os/spec/spec-schema.js.map +1 -1
- package/dist/os/spec/spec-status.d.ts +4 -0
- package/dist/os/spec/spec-status.d.ts.map +1 -1
- package/dist/os/spec/spec-status.js +19 -1
- package/dist/os/spec/spec-status.js.map +1 -1
- package/dist/os/spec/spec-validate.d.ts.map +1 -1
- package/dist/os/spec/spec-validate.js +55 -8
- package/dist/os/spec/spec-validate.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +84 -8
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +13 -4
- 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 +13 -5
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/cli-flags.d.ts.map +1 -1
- package/dist/src/cli-flags.js +25 -5
- package/dist/src/cli-flags.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +24 -3
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +38 -7
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/create.d.ts.map +1 -1
- package/dist/src/onboarding/create.js +26 -10
- 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 +49 -2
- 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 +8 -3
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +24 -8
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +14 -1
- package/dist/src/pipeline-status.d.ts.map +1 -1
- package/dist/src/pipeline-status.js +28 -8
- package/dist/src/pipeline-status.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/generated-marker.d.ts +8 -0
- package/dist/src/utils/generated-marker.d.ts.map +1 -1
- package/dist/src/utils/generated-marker.js +25 -0
- package/dist/src/utils/generated-marker.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +42 -15
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/os/adr/adr.module.ts +2 -0
- package/os/audit/audit.module.ts +2 -0
- package/os/compass/compass.module.ts +50 -16
- package/os/compass/handlers/compass-audit-handler.ts +50 -9
- package/os/compass/handlers/compass-docs-validate.ts +359 -0
- package/os/compass/handlers/compass-inventory.ts +6 -3
- package/os/compass/handlers/summary-record.ts +118 -10
- package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
- package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
- package/os/compass/handlers/tests/compass-policy.test.ts +15 -18
- package/os/compass/handlers/tests/compass-v2-contract.test.ts +38 -0
- package/os/compass/handlers/tests/forge-yaml-fixture.ts +69 -0
- package/os/compass/handlers/tests/summary-record.test.ts +152 -0
- package/os/compass/policy.ts +8 -2
- package/os/core/core.module.ts +147 -75
- package/os/core/docs-archive.test.ts +136 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/mission/handlers/archive.ts +1 -2
- package/os/naming/naming.module.ts +9 -2
- package/os/notes/notes.module.ts +17 -6
- package/os/plan/plan-0000-template.md +150 -0
- package/os/plan/plan.module.ts +2 -0
- package/os/plugin/plugin.module.ts +9 -2
- package/os/program/program.module.ts +19 -4
- package/os/queue/decision-ledger.test.ts +163 -0
- package/os/queue/handlers/queue-validate.ts +190 -8
- package/os/queue/manifest.ts +201 -7
- package/os/queue/queue-validate.test.ts +303 -0
- package/os/queue/queue.module.ts +32 -4
- package/os/queue/types.ts +56 -1
- package/os/rfc/handlers/implement-stamp.ts +35 -5
- package/os/rfc/handlers/validate.ts +1 -1
- package/os/rfc/rfc.module.ts +29 -3
- package/os/rfc/types.ts +17 -1
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +155 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +80 -38
- package/os/session/handlers/save.ts +41 -5
- package/os/session/session.module.ts +5 -0
- package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
- package/os/spec/live-spec-list-show-validate.test.ts +10 -6
- package/os/spec/live-spec-merge.test.ts +187 -7
- package/os/spec/live-spec-merge.ts +71 -155
- package/os/spec/live-spec-rebuild.test.ts +317 -0
- package/os/spec/live-spec-rebuild.ts +356 -0
- package/os/spec/live-spec-shared.ts +378 -0
- package/os/spec/live-spec-types.ts +21 -1
- package/os/spec/live-spec-validate.test.ts +317 -0
- package/os/spec/live-spec-validate.ts +106 -2
- package/os/spec/spec-materialize.test.ts +192 -0
- package/os/spec/spec-materialize.ts +48 -4
- package/os/spec/spec-schema.ts +49 -0
- package/os/spec/spec-status.ts +24 -0
- package/os/spec/spec-validate.test.ts +66 -0
- package/os/spec/spec-validate.ts +62 -8
- package/os/spec/spec.module.ts +85 -8
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
- package/os/werkstatt/werkstatt.module.ts +13 -4
- package/os/workflow/workflow.module.ts +13 -5
- package/package.json +1 -1
- package/skills/_shared/fo-pipeline-conventions.md +126 -1
- package/skills/fo/fo-compass-annotate/SKILL.md +1 -1
- package/skills/fo/fo-doc-audit/SKILL.md +4 -4
- package/skills/fo/fo-explore/SKILL.md +2 -1
- package/skills/fo/fo-fix/SKILL.md +11 -3
- package/skills/fo/fo-handoff/SKILL.md +6 -8
- package/skills/fo/fo-idea-audit/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/SKILL.md +3 -2
- package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
- package/skills/fo/fo-idea-implement/SKILL.md +57 -13
- package/skills/fo/fo-idea-plan/SKILL.md +28 -6
- package/skills/fo/fo-review/SKILL.md +69 -8
- package/skills/fo/fo-session-retro/SKILL.md +4 -3
- package/skills/fo/fo-session-save/SKILL.md +1 -1
- package/skills/fo/fo-step-commit/SKILL.md +2 -0
- package/skills/shared/grilling/SKILL.md +12 -0
- package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
- package/skills/shared/writing-great-skills/SKILL.md +2 -1
- package/src/cli-flags.ts +23 -5
- package/src/config/forge-config.ts +106 -57
- package/src/onboarding/create.ts +25 -9
- package/src/onboarding/doctor.ts +51 -2
- package/src/onboarding/init.ts +8 -3
- package/src/onboarding/upgrade.ts +36 -6
- package/src/pipeline-status.ts +32 -8
- package/src/tests/bindings-schema.test.ts +53 -1
- package/src/tests/cli-flags.test.ts +20 -0
- package/src/tests/cli-output.test.ts +13 -7
- package/src/tests/doctor-bindings.test.ts +7 -2
- package/src/tests/doctor-skills-unmanaged.test.ts +145 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
- package/src/tests/forge-config.test.ts +106 -2
- package/src/tests/implement-stamp.test.ts +38 -0
- package/src/tests/session-handlers.test.ts +33 -0
- package/src/tests/skill-validate.test.ts +180 -0
- package/src/tests/upgrade.test.ts +60 -0
- package/src/types.ts +13 -1
- package/src/utils/generated-marker.ts +28 -0
- package/src/validators/skill-validate.ts +43 -16
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge.doctor skills-unmanaged check (RFC-1226): SKILL.md-bearing dirs without .forge-managed are reported as warnings.</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-1226: initial tests — AC-1 warn on unmanaged, AC-2 pass when all marked, AC-3 skip non-skill dirs, AC-4 skip without config/skillsDir.</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
15
|
+
import { buildSkillMarkerContent } from "../onboarding/skill-markers.ts";
|
|
16
|
+
import type { ForgeRuntimeContext, ForgeLogger } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
const mockLogger: ForgeLogger = {
|
|
19
|
+
section: () => {},
|
|
20
|
+
info: () => {},
|
|
21
|
+
warn: () => {},
|
|
22
|
+
error: () => {},
|
|
23
|
+
success: () => {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const mockContext = (workspaceRoot: string): ForgeRuntimeContext => ({
|
|
27
|
+
workspaceRoot,
|
|
28
|
+
logger: mockLogger,
|
|
29
|
+
dryRun: false,
|
|
30
|
+
outputFormat: "json",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let tempDir: string;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-doctor-unmanaged-test-"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const FORGE_YAML = `schema: forge/config@1
|
|
44
|
+
project:
|
|
45
|
+
name: test
|
|
46
|
+
stack: []
|
|
47
|
+
packageManager: pnpm
|
|
48
|
+
paths:
|
|
49
|
+
rfcsDir: docs/rfcs
|
|
50
|
+
adrsDir: docs/adrs
|
|
51
|
+
plansDir: docs/plans
|
|
52
|
+
auditsDir: docs/audits
|
|
53
|
+
specsDir: docs/specs
|
|
54
|
+
skillsDir: .agents/skills
|
|
55
|
+
bindings:
|
|
56
|
+
schema: forge/bindings@1
|
|
57
|
+
commands:
|
|
58
|
+
validateRfc: null
|
|
59
|
+
validateAdr: null
|
|
60
|
+
implementStamp: null
|
|
61
|
+
typecheck: null
|
|
62
|
+
test: null
|
|
63
|
+
scopedBuild: null
|
|
64
|
+
specValidate: null
|
|
65
|
+
sessionSave: null
|
|
66
|
+
paths:
|
|
67
|
+
invariantsFile: null
|
|
68
|
+
compassDocs: []
|
|
69
|
+
reviewsDir: null
|
|
70
|
+
handoffsDir: null
|
|
71
|
+
sessionsDir: null
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const SKILL_MD = "---\nname: test-skill\ndescription: test\n---\n# Test\n";
|
|
75
|
+
|
|
76
|
+
async function writeSkill(dir: string, name: string, managed: boolean): Promise<void> {
|
|
77
|
+
const skillDir = join(dir, ".agents", "skills", name);
|
|
78
|
+
await mkdir(skillDir, { recursive: true });
|
|
79
|
+
await writeFile(join(skillDir, "SKILL.md"), SKILL_MD, "utf8");
|
|
80
|
+
if (managed) {
|
|
81
|
+
await writeFile(
|
|
82
|
+
join(skillDir, ".forge-managed"),
|
|
83
|
+
buildSkillMarkerContent(["SKILL.md"]),
|
|
84
|
+
"utf8",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findCheck(result: Awaited<ReturnType<typeof runDoctor>>) {
|
|
90
|
+
const check = result.data?.checks.find((c) => c.name === "skills-unmanaged");
|
|
91
|
+
expect(check, "skills-unmanaged check must be present").toBeDefined();
|
|
92
|
+
return check!;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test("AC-1: skills-unmanaged warns and names SKILL.md-bearing dirs without .forge-managed", async () => {
|
|
96
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
97
|
+
await mkdir(join(tempDir, ".agents", "skills"), { recursive: true });
|
|
98
|
+
await writeSkill(tempDir, "fo-managed-skill", true);
|
|
99
|
+
await writeSkill(tempDir, "ask-matt", false);
|
|
100
|
+
await writeSkill(tempDir, "tdd", false);
|
|
101
|
+
|
|
102
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
103
|
+
const check = findCheck(result);
|
|
104
|
+
expect(check.status).toBe("warn");
|
|
105
|
+
expect(check.message).toContain("2 unmanaged skill dir(s)");
|
|
106
|
+
expect(check.message).toContain("ask-matt");
|
|
107
|
+
expect(check.message).toContain("tdd");
|
|
108
|
+
expect(check.message).not.toContain("fo-managed-skill");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("AC-2: skills-unmanaged passes when every SKILL.md-bearing dir carries .forge-managed", async () => {
|
|
112
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
113
|
+
await mkdir(join(tempDir, ".agents", "skills"), { recursive: true });
|
|
114
|
+
await writeSkill(tempDir, "fo-managed-skill", true);
|
|
115
|
+
await writeSkill(tempDir, "grilling", true);
|
|
116
|
+
|
|
117
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
118
|
+
const check = findCheck(result);
|
|
119
|
+
expect(check.status).toBe("pass");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("AC-3: skills-unmanaged excludes dirs without SKILL.md regardless of markers", async () => {
|
|
123
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
124
|
+
const sharedDir = join(tempDir, ".agents", "skills", "_shared");
|
|
125
|
+
await mkdir(sharedDir, { recursive: true });
|
|
126
|
+
await writeFile(join(sharedDir, "notes.md"), "# support files\n", "utf8");
|
|
127
|
+
const knowledgeDir = join(tempDir, ".agents", "skills", "knowledge");
|
|
128
|
+
await mkdir(knowledgeDir, { recursive: true });
|
|
129
|
+
await writeFile(join(knowledgeDir, "learned-principles.md"), "# L2\n", "utf8");
|
|
130
|
+
|
|
131
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
132
|
+
const check = findCheck(result);
|
|
133
|
+
expect(check.status).toBe("pass");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("AC-4: skills-unmanaged passes without scanning when forge.yaml or skillsDir is absent", async () => {
|
|
137
|
+
// No forge.yaml at all
|
|
138
|
+
const noConfig = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
139
|
+
expect(findCheck(noConfig).status).toBe("pass");
|
|
140
|
+
|
|
141
|
+
// forge.yaml present, skillsDir absent
|
|
142
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
143
|
+
const noSkillsDir = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
144
|
+
expect(findCheck(noSkillsDir).status).toBe("pass");
|
|
145
|
+
});
|
|
@@ -110,29 +110,29 @@ When the operator expresses an intent in natural language, the agent routes to t
|
|
|
110
110
|
| "manage Compass headers", "annotate source files with Compass", "audit Compass module contracts" | `fo-compass-annotate` |
|
|
111
111
|
| "design summit", "multi-persona review", "party mode" | `fo-design-summit` |
|
|
112
112
|
| "audit documentation sync", "update docs after code changes", "check AGENTS.md and README are in sync" | `fo-doc-audit` |
|
|
113
|
-
| "explore this idea", "let me think about this", "what are the options for", "what if we"
|
|
113
|
+
| "нам надо закрыть открытые вопросы", "explore this idea", "let me think about this", "what are the options for", "what if we" | `fo-explore` |
|
|
114
114
|
| "extract DNA invariant", "discover architectural invariants", "formalize implicit architectural rules" | `fo-extract-dna` |
|
|
115
|
-
| "fix issues from review", "apply review findings and fix"
|
|
115
|
+
| "исправим", "примени findings", "fix all", "fix issues from review", "apply review findings and fix" | `fo-fix` |
|
|
116
116
|
| "create a handoff document", "compact conversation for next agent", "prepare handoff for another agent" | `fo-handoff` |
|
|
117
117
|
| "harvest portable patterns from code", "extract reusable code pattern to forge", "port this pattern to forge" | `fo-harvest` |
|
|
118
118
|
| "I have an idea for a change", "analyze this idea and route it", "decompose this feature idea", "does this need an RFC", "should I write an ADR for this" | `fo-idea` |
|
|
119
119
|
| "audit this RFC", "check RFC for ecosystem fit", "review RFC against DNA and forward-only rules" | `fo-idea-audit` |
|
|
120
120
|
| "record this architectural decision", "create an ADR", "document a local technical decision" | `fo-idea-create-adr` |
|
|
121
|
-
| "draft an RFC", "create a full RFC", "scaffold an RFC from template"
|
|
121
|
+
| "подготовим RFC", "draft an RFC", "create a full RFC", "scaffold an RFC from template" | `fo-idea-create-rfc` |
|
|
122
122
|
| "enhance this RFC", "fix audit findings in RFC", "improve RFC based on audit" | `fo-idea-enhance` |
|
|
123
123
|
| "I just want to see the plan", "plan this feature without implementing", "run idea to plan pipeline" | `fo-idea-i-just-want-to-see-the-plan` |
|
|
124
|
-
| "
|
|
124
|
+
| "по полному пайплайну", "на результат", "реализуй RFC", "I just want to see the result", "implement this end-to-end without pauses" | `fo-idea-i-just-want-to-see-the-result` |
|
|
125
125
|
| "implement this RFC", "execute the implementation plan", "realize this RFC end-to-end" | `fo-idea-implement` |
|
|
126
126
|
| "plan the implementation for this RFC", "create implementation plan", "map affected artifacts and draft steps" | `fo-idea-plan` |
|
|
127
127
|
| "show RFC and ADR status", "what is the status of this RFC", "list RFCs by status" | `fo-idea-status` |
|
|
128
128
|
| "distill knowledge", "compact skill knowledge", "knowledge lifecycle", "promote fix patterns" | `fo-knowledge-distill` |
|
|
129
129
|
| "sync memory from codex", "import external agent knowledge", "recall sessions from other tools", "sync memory from claude code" | `fo-memory-sync` |
|
|
130
130
|
| "I found a bug", "file a GitHub issue for this bug", "report a bug conversationally", "interactive QA session" | `fo-qa` |
|
|
131
|
-
| "review this code diff", "check code against Forge standards"
|
|
132
|
-
| "
|
|
131
|
+
| "проверим всё ли сделали правильно", "проверь изменения", "review this session", "review this code diff", "check code against Forge standards" | `fo-review` |
|
|
132
|
+
| "завершаем", "протокол завершения", "Завершаем эту сессию", "session retrospective", "End session" | `fo-session-retro` |
|
|
133
133
|
| "save this session", "enhance session transcript with annotations", "save session with quality checks" | `fo-session-save` |
|
|
134
134
|
| "ingest external specification", "vendor spec package into docs", "author spec skeleton" | `fo-spec-ingest` |
|
|
135
|
-
| "commit after request", "auto-commit changes", "step commit"
|
|
135
|
+
| "закоммитим", "commit after request", "auto-commit changes", "step commit" | `fo-step-commit` |
|
|
136
136
|
| "triage this issue", "categorize incoming bug report", "triage external pull request", "process feature request" | `fo-triage` |
|
|
137
137
|
|
|
138
138
|
The agent uses judgment to calibrate routing — minor edits (typo fixes, small CSS changes) do not require skill invocation, while significant changes (new features, architectural decisions) do.
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
<item>RFC-0391: initial config tests.</item>
|
|
7
7
|
<item>RFC-0538: added compass binding section tests.</item>
|
|
8
8
|
<item>RFC-0540: added CLI binding defaults, pm-runner, and package-manager-aware defaultForgeConfig tests.</item>
|
|
9
|
+
<item>RFC-1224: adrImplementStamp binding, schema passthrough retention, applyForgeYamlPatches tests.</item>
|
|
10
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
9
11
|
</CHANGE_SUMMARY>
|
|
10
12
|
*/
|
|
11
13
|
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
forgeConfigSchema,
|
|
21
23
|
forgeBindingsSchema,
|
|
22
24
|
resolveBinding,
|
|
25
|
+
applyForgeYamlPatches,
|
|
23
26
|
FORGE_CLI_BINDING_DEFAULTS,
|
|
24
27
|
resolvePmRunner,
|
|
25
28
|
applyCliBindingDefaults,
|
|
@@ -63,12 +66,13 @@ test("resolvePmRunner falls back to npx for unknown", () => {
|
|
|
63
66
|
expect(resolvePmRunner("")).toBe("npx");
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
test("FORGE_CLI_BINDING_DEFAULTS has
|
|
67
|
-
expect(FORGE_CLI_BINDING_DEFAULTS).toHaveLength(
|
|
69
|
+
test("FORGE_CLI_BINDING_DEFAULTS has 6 entries with correct keys", () => {
|
|
70
|
+
expect(FORGE_CLI_BINDING_DEFAULTS).toHaveLength(6);
|
|
68
71
|
const keys = FORGE_CLI_BINDING_DEFAULTS.map((e) => e.key);
|
|
69
72
|
expect(keys).toContain("commands.validateRfc");
|
|
70
73
|
expect(keys).toContain("commands.validateAdr");
|
|
71
74
|
expect(keys).toContain("commands.implementStamp");
|
|
75
|
+
expect(keys).toContain("commands.adrImplementStamp");
|
|
72
76
|
expect(keys).toContain("commands.specValidate");
|
|
73
77
|
expect(keys).toContain("commands.sessionSave");
|
|
74
78
|
});
|
|
@@ -134,6 +138,9 @@ test("applyCliBindingDefaults returns forge-CLI bindings non-null and stack bind
|
|
|
134
138
|
expect(commands.validateRfc).not.toBeNull();
|
|
135
139
|
expect(commands.validateAdr).not.toBeNull();
|
|
136
140
|
expect(commands.implementStamp).not.toBeNull();
|
|
141
|
+
expect(commands.adrImplementStamp).toBe(
|
|
142
|
+
"pnpm exec forge adr.implement.stamp --id {id} --implementation-commit {commit}",
|
|
143
|
+
);
|
|
137
144
|
expect(commands.specValidate).not.toBeNull();
|
|
138
145
|
expect(commands.sessionSave).not.toBeNull();
|
|
139
146
|
expect(commands.typecheck).toBeNull();
|
|
@@ -418,3 +425,100 @@ bindings:
|
|
|
418
425
|
expect(config.bindings?.compass?.fileExtensions).toEqual([".ts", ".astro"]);
|
|
419
426
|
expect(config.bindings?.compass?.testPatterns).toEqual(["*.test.ts", "*.spec.ts"]);
|
|
420
427
|
});
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// RFC-1224: schema passthrough + forge.yaml document patching
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
test("RFC-1224: schema retains adrImplementStamp and consumer-defined command keys", async () => {
|
|
434
|
+
const yaml = `schema: forge/config@1
|
|
435
|
+
project:
|
|
436
|
+
name: consumer-project
|
|
437
|
+
stack: []
|
|
438
|
+
packageManager: pnpm
|
|
439
|
+
paths:
|
|
440
|
+
rfcsDir: docs/rfcs
|
|
441
|
+
adrsDir: docs/adrs
|
|
442
|
+
plansDir: docs/plans
|
|
443
|
+
auditsDir: docs/audits
|
|
444
|
+
specsDir: docs/specs
|
|
445
|
+
skillsDir: .agents/skills
|
|
446
|
+
sessionsDir: docs/sessions
|
|
447
|
+
bindings:
|
|
448
|
+
schema: forge/bindings@1
|
|
449
|
+
commands:
|
|
450
|
+
validateRfc: null
|
|
451
|
+
adrImplementStamp: pnpm exec forge adr.implement.stamp --id {id} --implementation-commit {commit}
|
|
452
|
+
createRfc: pnpm exec forge rfc.create
|
|
453
|
+
manifestGenerate: pnpm exec forge manifest.generate
|
|
454
|
+
paths:
|
|
455
|
+
invariantsFile: null
|
|
456
|
+
compassDocs: []
|
|
457
|
+
reviewsDir: null
|
|
458
|
+
handoffsDir: null
|
|
459
|
+
sessionsDir: null
|
|
460
|
+
plugins:
|
|
461
|
+
- name: my-plugin
|
|
462
|
+
`;
|
|
463
|
+
await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
|
|
464
|
+
const config = loadForgeConfig(tempDir);
|
|
465
|
+
|
|
466
|
+
// Promoted key resolves through the typed interface
|
|
467
|
+
expect(config.bindings?.commands.adrImplementStamp).toBe(
|
|
468
|
+
"pnpm exec forge adr.implement.stamp --id {id} --implementation-commit {commit}",
|
|
469
|
+
);
|
|
470
|
+
// Consumer-defined keys stay resolvable via passthrough
|
|
471
|
+
expect(resolveBinding(config, "commands.createRfc")).toBe("pnpm exec forge rfc.create");
|
|
472
|
+
expect(resolveBinding(config, "commands.manifestGenerate")).toBe(
|
|
473
|
+
"pnpm exec forge manifest.generate",
|
|
474
|
+
);
|
|
475
|
+
// Unknown top-level sections survive the schema parse
|
|
476
|
+
expect((config as unknown as Record<string, unknown>)["plugins"]).toEqual([
|
|
477
|
+
{ name: "my-plugin" },
|
|
478
|
+
]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("RFC-1224: applyForgeYamlPatches preserves comments and consumer content", () => {
|
|
482
|
+
const raw = `schema: forge/config@1
|
|
483
|
+
# operator comment — must survive
|
|
484
|
+
project:
|
|
485
|
+
name: consumer-project
|
|
486
|
+
bindings:
|
|
487
|
+
schema: forge/bindings@1
|
|
488
|
+
commands:
|
|
489
|
+
validateRfc: null # inline comment
|
|
490
|
+
customKey: keep-me
|
|
491
|
+
plugins:
|
|
492
|
+
- name: my-plugin
|
|
493
|
+
forge:
|
|
494
|
+
syncedVersion: 0.1.0
|
|
495
|
+
`;
|
|
496
|
+
const patched = applyForgeYamlPatches(raw, [
|
|
497
|
+
{ path: ["forge", "syncedVersion"], value: "0.2.0" },
|
|
498
|
+
{ path: ["bindings", "commands", "adrImplementStamp"], value: "forge adr.implement.stamp" },
|
|
499
|
+
]);
|
|
500
|
+
|
|
501
|
+
expect(patched).not.toBeNull();
|
|
502
|
+
expect(patched).toContain("syncedVersion: 0.2.0");
|
|
503
|
+
expect(patched).toContain("adrImplementStamp: forge adr.implement.stamp");
|
|
504
|
+
expect(patched).toContain("customKey: keep-me");
|
|
505
|
+
expect(patched).toContain("# operator comment — must survive");
|
|
506
|
+
expect(patched).toContain("# inline comment");
|
|
507
|
+
expect(patched).toContain("my-plugin");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("RFC-1224: applyForgeYamlPatches creates missing intermediate maps", () => {
|
|
511
|
+
const patched = applyForgeYamlPatches("schema: forge/config@1\n", [
|
|
512
|
+
{ path: ["forge", "syncedVersion"], value: "0.2.0" },
|
|
513
|
+
{ path: ["bindings", "commands", "adrImplementStamp"], value: "x" },
|
|
514
|
+
]);
|
|
515
|
+
expect(patched).toContain("syncedVersion: 0.2.0");
|
|
516
|
+
expect(patched).toContain("adrImplementStamp: x");
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("RFC-1224: applyForgeYamlPatches returns null on invalid YAML", () => {
|
|
520
|
+
const patched = applyForgeYamlPatches("a: [unclosed\n : :", [
|
|
521
|
+
{ path: ["a"], value: 1 },
|
|
522
|
+
]);
|
|
523
|
+
expect(patched).toBeNull();
|
|
524
|
+
});
|
|
@@ -429,6 +429,44 @@ describe("rfc.implement.stamp", () => {
|
|
|
429
429
|
expect(result.data?.violations).toEqual([]);
|
|
430
430
|
});
|
|
431
431
|
|
|
432
|
+
it("auto-selects the sole implement/feat-prefixed commit among multiple candidates", async () => {
|
|
433
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
434
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
435
|
+
await commitAll(workspaceRoot, "docs: audit RFC-0001");
|
|
436
|
+
await writeFile(join(workspaceRoot, "src.txt"), "impl");
|
|
437
|
+
const implCommit = await commitAll(workspaceRoot, "implement: RFC-0001 — the change");
|
|
438
|
+
await writeFile(join(workspaceRoot, "src.txt"), "evidence");
|
|
439
|
+
await commitAll(workspaceRoot, "docs: RFC-0001 evidence sweep");
|
|
440
|
+
|
|
441
|
+
const result = await runRfcImplementStamp(
|
|
442
|
+
makeInputAutoDetect("RFC-0001"),
|
|
443
|
+
makeContext(workspaceRoot),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(result.exitCode).toBe(0);
|
|
447
|
+
expect(result.data?.status).toBe("pass");
|
|
448
|
+
expect(implCommit.startsWith(result.data?.data?.implementationCommit ?? "")).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("still blocks with annotated candidates when several implement-prefixed commits exist", async () => {
|
|
452
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
453
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
454
|
+
await commitAll(workspaceRoot, "implement: RFC-0001 step 1");
|
|
455
|
+
await writeFile(join(workspaceRoot, "src.txt"), "step 2");
|
|
456
|
+
await commitAll(workspaceRoot, "implement: RFC-0001 step 2");
|
|
457
|
+
|
|
458
|
+
const result = await runRfcImplementStamp(
|
|
459
|
+
makeInputAutoDetect("RFC-0001"),
|
|
460
|
+
makeContext(workspaceRoot),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
expect(result.exitCode).toBe(1);
|
|
464
|
+
const imp03 = result.data?.violations.find((v) => v.rule === "RFC-IMP-03");
|
|
465
|
+
expect(imp03?.message).toContain("Multiple commits reference RFC-0001");
|
|
466
|
+
expect(imp03?.message).toContain("[implementation-shaped]");
|
|
467
|
+
expect(imp03?.message).toContain("Likely:");
|
|
468
|
+
});
|
|
469
|
+
|
|
432
470
|
// ── RFC-0795: RFC-IMP-07 dependsOn dependency gate tests ──────────────────
|
|
433
471
|
|
|
434
472
|
it("blocks stamping when a dependsOn RFC is not implemented (RFC-IMP-07)", async () => {
|
|
@@ -99,6 +99,39 @@ describe("session.save", () => {
|
|
|
99
99
|
expect(data2.skipped[0]).toHaveProperty("reason", "already converted");
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
test("ignores non-.atif files — claim intermediates and writer temp files are skipped", async () => {
|
|
103
|
+
const rawDir = join(dir, "docs/sessions/.raw");
|
|
104
|
+
await writeFile(join(rawDir, "orphan.atif.claim-999"), SAMPLE_ATIF, "utf-8");
|
|
105
|
+
await writeFile(join(rawDir, "half-written.atif.tmp"), SAMPLE_ATIF, "utf-8");
|
|
106
|
+
|
|
107
|
+
const result = await runSessionSave(makeInput(), makeContext(dir));
|
|
108
|
+
const data = result.data as SessionSaveResult & { skipped: Array<{ rawFile: string }> };
|
|
109
|
+
|
|
110
|
+
// Neither junk file was converted, claimed, or deleted.
|
|
111
|
+
const touched = data.skipped.map((s) => s.rawFile);
|
|
112
|
+
expect(touched).not.toContain("orphan.atif.claim-999");
|
|
113
|
+
expect(touched).not.toContain("half-written.atif.tmp");
|
|
114
|
+
const remaining = await readdir(rawDir);
|
|
115
|
+
expect(remaining).toContain("orphan.atif.claim-999");
|
|
116
|
+
expect(remaining).toContain("half-written.atif.tmp");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("--keep-raw restores the original filename after the atomic claim", async () => {
|
|
120
|
+
const rawDir = join(dir, "docs/sessions/.raw");
|
|
121
|
+
const uniqueAtif = `2026-07-28T10:00:00+02:00
|
|
122
|
+
User: keep-raw claim restore test.
|
|
123
|
+
Assistant: Done.
|
|
124
|
+
Commands: session.save`;
|
|
125
|
+
await writeFile(join(rawDir, "keepraw-claim.atif"), uniqueAtif, "utf-8");
|
|
126
|
+
|
|
127
|
+
const result = await runSessionSave(makeInput({ "keep-raw": true }), makeContext(dir));
|
|
128
|
+
expect(result.data.id).not.toBe("");
|
|
129
|
+
|
|
130
|
+
const remaining = await readdir(rawDir);
|
|
131
|
+
expect(remaining).toContain("keepraw-claim.atif");
|
|
132
|
+
expect(remaining.filter((n) => n.startsWith("keepraw-claim.atif.claim-"))).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
102
135
|
test("deletes raw file on skip when --keep-raw is not set (idempotency cleanup)", async () => {
|
|
103
136
|
const uniqueAtif = `2026-07-27T09:15:00+02:00
|
|
104
137
|
User: Fix the session.save idempotency bug in save.ts.
|
|
@@ -259,3 +259,183 @@ describe("SKILL-21: knowledge layer token budget warnings (RFC-0661)", () => {
|
|
|
259
259
|
expect(skill21Warnings).toEqual([]);
|
|
260
260
|
});
|
|
261
261
|
});
|
|
262
|
+
|
|
263
|
+
describe("RFC-1225: forge root + workspace config resolution", () => {
|
|
264
|
+
function createConsumerWorkspace(): { tmpDir: string; forgePkg: string } {
|
|
265
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-skill-consumer-"));
|
|
266
|
+
const forgePkg = path.join(tmpDir, "node_modules", "@warpgogol", "forge");
|
|
267
|
+
fs.mkdirSync(forgePkg, { recursive: true });
|
|
268
|
+
fs.writeFileSync(
|
|
269
|
+
path.join(forgePkg, "package.json"),
|
|
270
|
+
JSON.stringify({ name: "@warpgogol/forge", version: "9.9.9" }),
|
|
271
|
+
"utf8",
|
|
272
|
+
);
|
|
273
|
+
return { tmpDir, forgePkg };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
test("AC-1: consumer layout validates skills under node_modules/@warpgogol/forge", async () => {
|
|
277
|
+
const { FORGE_SKILLS } = await import("../registry.ts");
|
|
278
|
+
const { tmpDir, forgePkg } = createConsumerWorkspace();
|
|
279
|
+
try {
|
|
280
|
+
// Materialize one registry skill inside the installed package copy —
|
|
281
|
+
// the others stay absent and produce SKILL-01, which is honest.
|
|
282
|
+
const entry = FORGE_SKILLS.find((s) => !(s.knowledge && s.knowledge.length > 0))!;
|
|
283
|
+
const skillPath = path.join(forgePkg, entry.path);
|
|
284
|
+
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
285
|
+
fs.writeFileSync(
|
|
286
|
+
skillPath,
|
|
287
|
+
`---\nname: ${entry.name}\ndescription: Fixture skill for consumer root resolution\ninvocation: ${entry.invocation}\ncategory: ${entry.category}\nconcerns: ${entry.concerns}\ndependsOn: []\nlanguagePolicy: ref(PREFERENCES.md)\n---\n\nBefore starting, read \`PREFERENCES.md\` at the repository root.\n`,
|
|
288
|
+
"utf8",
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const result = runSkillValidate({}, { workspaceRoot: tmpDir });
|
|
292
|
+
// The materialized skill was found → no SKILL-01 for it. Under the old
|
|
293
|
+
// hardcoded <workspaceRoot>/packages/forge path it could never exist.
|
|
294
|
+
const skill01 = result.violations.filter(
|
|
295
|
+
(v) => v.rule === "SKILL-01" && v.skill === entry.name,
|
|
296
|
+
);
|
|
297
|
+
expect(skill01).toEqual([]);
|
|
298
|
+
} finally {
|
|
299
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("AC-2: unresolvable forge root fails with a single SKILL-00", () => {
|
|
304
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-skill-noroot-"));
|
|
305
|
+
try {
|
|
306
|
+
const result = runSkillValidate({}, { workspaceRoot: tmpDir });
|
|
307
|
+
expect(result.status).toBe("fail");
|
|
308
|
+
expect(result.violations).toHaveLength(1);
|
|
309
|
+
expect(result.violations[0].rule).toBe("SKILL-00");
|
|
310
|
+
expect(result.violations[0].message).toContain("forge root not resolvable");
|
|
311
|
+
} finally {
|
|
312
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("AC-3: pack discovery reads forge.yaml from the workspace root", () => {
|
|
317
|
+
const { tmpDir } = createConsumerWorkspace();
|
|
318
|
+
try {
|
|
319
|
+
fs.writeFileSync(
|
|
320
|
+
path.join(tmpDir, "forge.yaml"),
|
|
321
|
+
`schema: forge/config@1
|
|
322
|
+
project:
|
|
323
|
+
name: consumer
|
|
324
|
+
stack: []
|
|
325
|
+
packageManager: pnpm
|
|
326
|
+
paths:
|
|
327
|
+
rfcsDir: docs/rfcs
|
|
328
|
+
adrsDir: docs/adrs
|
|
329
|
+
plansDir: docs/plans
|
|
330
|
+
auditsDir: docs/audits
|
|
331
|
+
specsDir: docs/specs
|
|
332
|
+
skillsDir: .agents/skills
|
|
333
|
+
sessionsDir: docs/sessions
|
|
334
|
+
skillPacks:
|
|
335
|
+
- prefix: wg
|
|
336
|
+
dir: packs/wg-skills
|
|
337
|
+
`,
|
|
338
|
+
"utf8",
|
|
339
|
+
);
|
|
340
|
+
const packDir = path.join(tmpDir, "packs", "wg-skills");
|
|
341
|
+
fs.mkdirSync(path.join(packDir, "wg-broken"), { recursive: true });
|
|
342
|
+
fs.writeFileSync(path.join(packDir, "forge.plugin.yaml"), "id: wg-skills\nversion: 1.0.0\n");
|
|
343
|
+
// Deliberately invalid pack skill — discovery proves itself by flagging it.
|
|
344
|
+
fs.writeFileSync(path.join(packDir, "wg-broken", "SKILL.md"), "# no frontmatter\n");
|
|
345
|
+
|
|
346
|
+
const result = runSkillValidate({}, { workspaceRoot: tmpDir });
|
|
347
|
+
const packViolations = result.violations.filter(
|
|
348
|
+
(v) => v.pack === "wg" && v.skill === "wg-broken",
|
|
349
|
+
);
|
|
350
|
+
expect(packViolations.length).toBeGreaterThan(0);
|
|
351
|
+
expect(packViolations.some((v) => v.rule === "SKILL-01")).toBe(true);
|
|
352
|
+
} finally {
|
|
353
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("AC-4: monorepo invocation resolves the real forge package root — no SKILL-01", async () => {
|
|
358
|
+
const { resolveForgeRoot } = await import("../config/forge-config.ts");
|
|
359
|
+
const root = process.cwd();
|
|
360
|
+
// cwd may be the repo root (packages/forge layout) or packages/forge
|
|
361
|
+
// itself (scoped test runs) — both must resolve to a root with skills/.
|
|
362
|
+
const forgeRoot = resolveForgeRoot(root);
|
|
363
|
+
expect(fs.existsSync(path.join(forgeRoot, "skills"))).toBe(true);
|
|
364
|
+
const result = runSkillValidate({}, { workspaceRoot: root });
|
|
365
|
+
const skill01 = result.violations.filter((v) => v.rule === "SKILL-01" && !v.pack);
|
|
366
|
+
expect(skill01).toEqual([]);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("RFC-1227: SKILL-17 comment-region exemption", () => {
|
|
371
|
+
// Access the checker through a fabricated skill on disk? checkSkill17 is not
|
|
372
|
+
// exported — exercise it via a consumer fixture containing a registry skill
|
|
373
|
+
// whose SKILL.md carries the content under test.
|
|
374
|
+
async function fixtureRun(bodyLines: string[]): Promise<ReturnType<typeof runSkillValidate>> {
|
|
375
|
+
const { FORGE_SKILLS } = await import("../registry.ts");
|
|
376
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-skill17-"));
|
|
377
|
+
const forgePkg = path.join(tmpDir, "node_modules", "@warpgogol", "forge");
|
|
378
|
+
fs.mkdirSync(forgePkg, { recursive: true });
|
|
379
|
+
fs.writeFileSync(
|
|
380
|
+
path.join(forgePkg, "package.json"),
|
|
381
|
+
JSON.stringify({ name: "@warpgogol/forge", version: "9.9.9" }),
|
|
382
|
+
"utf8",
|
|
383
|
+
);
|
|
384
|
+
const entry = FORGE_SKILLS.find((s) => !(s.knowledge && s.knowledge.length > 0))!;
|
|
385
|
+
const skillPath = path.join(forgePkg, entry.path);
|
|
386
|
+
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
387
|
+
fs.writeFileSync(
|
|
388
|
+
skillPath,
|
|
389
|
+
[
|
|
390
|
+
"---",
|
|
391
|
+
`name: ${entry.name}`,
|
|
392
|
+
"description: Fixture skill for SKILL-17 scope tests",
|
|
393
|
+
`invocation: ${entry.invocation}`,
|
|
394
|
+
`category: ${entry.category}`,
|
|
395
|
+
`concerns: ${entry.concerns}`,
|
|
396
|
+
"dependsOn: []",
|
|
397
|
+
"languagePolicy: ref(PREFERENCES.md)",
|
|
398
|
+
"---",
|
|
399
|
+
...bodyLines,
|
|
400
|
+
"Before starting, read `PREFERENCES.md` at the repository root.",
|
|
401
|
+
].join("\n"),
|
|
402
|
+
"utf8",
|
|
403
|
+
);
|
|
404
|
+
try {
|
|
405
|
+
return runSkillValidate({}, { workspaceRoot: tmpDir });
|
|
406
|
+
} finally {
|
|
407
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
test("AC-1: RFC ids inside HTML comment regions produce no SKILL-17 violation", async () => {
|
|
412
|
+
const result = await fixtureRun([
|
|
413
|
+
"<!--",
|
|
414
|
+
"<CHANGE_SUMMARY>",
|
|
415
|
+
" <item>RFC-1097: sweep — SKILL.md headers + classification fixes</item>",
|
|
416
|
+
"</CHANGE_SUMMARY>",
|
|
417
|
+
"-->",
|
|
418
|
+
"Run `forge doctor` to inspect bindings. <!-- operator note: see RFC-0553 -->",
|
|
419
|
+
]);
|
|
420
|
+
const idViolations = result.violations.filter((v) => v.rule === "SKILL-17");
|
|
421
|
+
expect(idViolations).toEqual([]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("AC-2: RFC ids in instruction text still violate SKILL-17", async () => {
|
|
425
|
+
const result = await fixtureRun([
|
|
426
|
+
"Stamp the implementation via `rfc.implement.stamp` (RFC-0756).",
|
|
427
|
+
]);
|
|
428
|
+
const idViolations = result.violations.filter((v) => v.rule === "SKILL-17");
|
|
429
|
+
expect(idViolations.length).toBeGreaterThan(0);
|
|
430
|
+
expect(idViolations[0].message).toContain("RFC-0756");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("AC-4: the file-level disable marker still suppresses all SKILL-17 violations", async () => {
|
|
434
|
+
const result = await fixtureRun([
|
|
435
|
+
"<!-- skill-lint-disable SKILL-17 -->",
|
|
436
|
+
"Referencing RFC-0996 in body text is suppressed by the marker.",
|
|
437
|
+
]);
|
|
438
|
+
const idViolations = result.violations.filter((v) => v.rule === "SKILL-17");
|
|
439
|
+
expect(idViolations).toEqual([]);
|
|
440
|
+
});
|
|
441
|
+
});
|