@warpgogol/forge 5.1.3 → 5.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -1
- 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/adr.module.js +1 -1
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/compass/compass.module.js +2 -2
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +3 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +87 -8
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/core/core.module.js +1 -1
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +22 -6
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/program/lease.js +1 -1
- package/dist/os/program/lease.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/handlers/list-create.d.ts.map +1 -1
- package/dist/os/rfc/handlers/list-create.js +79 -32
- package/dist/os/rfc/handlers/list-create.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/rfc.module.js +1 -1
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +2 -0
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/src/knowledge/index.d.ts +1 -0
- package/dist/src/knowledge/index.d.ts.map +1 -1
- package/dist/src/knowledge/index.js +1 -0
- package/dist/src/knowledge/index.js.map +1 -1
- package/dist/src/knowledge/sync.d.ts +24 -0
- package/dist/src/knowledge/sync.d.ts.map +1 -0
- package/dist/src/knowledge/sync.js +127 -0
- package/dist/src/knowledge/sync.js.map +1 -0
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +7 -6
- 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 +15 -8
- 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 +42 -13
- package/dist/src/onboarding/upgrade.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/types.d.ts +1 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/fs-atomic.js +1 -1
- package/dist/src/utils/fs-atomic.js.map +1 -1
- package/os/adr/adr.module.ts +1 -1
- package/os/compass/compass.module.ts +2 -2
- package/os/compass/handlers/compass-audit-handler.ts +116 -8
- package/os/compass/handlers/tests/compass-audit-plan.test.ts +98 -0
- package/os/compass/handlers/tests/compass-audit-record.test.ts +105 -0
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +133 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +124 -0
- package/os/core/core.module.ts +1 -1
- package/os/mission/handlers/archive.test.ts +49 -0
- package/os/mission/handlers/archive.ts +21 -6
- package/os/program/lease.ts +1 -1
- 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/handlers/list-create.ts +83 -35
- package/os/rfc/handlers/pipeline-status.ts +24 -67
- package/os/rfc/handlers/validate-rules.test.ts +50 -0
- package/os/rfc/rfc-create-concurrent.test.ts +118 -0
- package/os/rfc/rfc-create-hint.test.ts +90 -0
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +83 -0
- package/os/rfc/rfc.module.ts +1 -1
- package/os/spec/spec.module.ts +2 -0
- package/package.json +17 -1
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +46 -1
- package/src/knowledge/__tests__/sync.test.ts +227 -0
- package/src/knowledge/index.ts +7 -0
- package/src/knowledge/sync.ts +161 -0
- package/src/onboarding/doctor.ts +7 -6
- package/src/onboarding/init.ts +15 -8
- package/src/onboarding/upgrade.ts +47 -16
- package/src/pipeline-status.test.ts +192 -0
- package/src/pipeline-status.ts +319 -0
- package/src/tests/upgrade.test.ts +53 -0
- package/src/types.ts +2 -2
- package/src/utils/fs-atomic.ts +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
import type { ForgeRuntimeContext } from "../../src/types.ts";
|
|
7
|
+
import { runQueueValidate } from "./handlers/queue-validate.ts";
|
|
8
|
+
import { loadQueueManifest } from "./manifest.ts";
|
|
9
|
+
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "queue-validate-test-"));
|
|
14
|
+
await fs.mkdir(path.join(tmpDir, "docs/rfcs"), { recursive: true });
|
|
15
|
+
await fs.mkdir(path.join(tmpDir, "docs/adrs"), { recursive: true });
|
|
16
|
+
await fs.mkdir(path.join(tmpDir, "docs/queues"), { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function testContext(): ForgeRuntimeContext {
|
|
24
|
+
const logs: string[] = [];
|
|
25
|
+
return {
|
|
26
|
+
workspaceRoot: tmpDir,
|
|
27
|
+
dryRun: false,
|
|
28
|
+
outputFormat: "json",
|
|
29
|
+
logger: {
|
|
30
|
+
section: (m: string) => logs.push(m),
|
|
31
|
+
info: (m: string) => logs.push(m),
|
|
32
|
+
warn: (m: string) => logs.push(m),
|
|
33
|
+
error: (m: string) => logs.push(m),
|
|
34
|
+
success: (m: string) => logs.push(m),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function writeRfc(id: string, frontmatter = "status: draft\n"): Promise<void> {
|
|
40
|
+
await fs.writeFile(
|
|
41
|
+
path.join(tmpDir, `docs/rfcs/${id.toLowerCase()}-test.md`),
|
|
42
|
+
`---\nid: ${id}\n${frontmatter}---\n# ${id}\n`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function writeManifest(name: string, body: string): Promise<string> {
|
|
47
|
+
const rel = `docs/queues/${name}.yaml`;
|
|
48
|
+
await fs.writeFile(path.join(tmpDir, rel), body);
|
|
49
|
+
return rel;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("loadQueueManifest — validation rules (RFC-1140)", () => {
|
|
53
|
+
test("valid manifest loads with no errors", async () => {
|
|
54
|
+
await writeRfc("RFC-1300");
|
|
55
|
+
const file = await writeManifest(
|
|
56
|
+
"block-a",
|
|
57
|
+
"id: block-a\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-1300\n",
|
|
58
|
+
);
|
|
59
|
+
const { errors } = await loadQueueManifest(tmpDir, file);
|
|
60
|
+
expect(errors).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("id↔filename mismatch → QUEUE-02 blocking error", async () => {
|
|
64
|
+
await writeRfc("RFC-1301");
|
|
65
|
+
const file = await writeManifest(
|
|
66
|
+
"block-b",
|
|
67
|
+
"id: different-id\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-1301\n",
|
|
68
|
+
);
|
|
69
|
+
const { errors } = await loadQueueManifest(tmpDir, file);
|
|
70
|
+
expect(errors.some((e) => e.ruleId === "QUEUE-02")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("malformed item id → QUEUE-03", async () => {
|
|
74
|
+
const file = await writeManifest(
|
|
75
|
+
"block-c",
|
|
76
|
+
"id: block-c\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-13\n",
|
|
77
|
+
);
|
|
78
|
+
const { errors } = await loadQueueManifest(tmpDir, file);
|
|
79
|
+
expect(errors.some((e) => e.ruleId === "QUEUE-03")).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("unresolvable item id → QUEUE-04", async () => {
|
|
83
|
+
const file = await writeManifest(
|
|
84
|
+
"block-d",
|
|
85
|
+
"id: block-d\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-9999\n",
|
|
86
|
+
);
|
|
87
|
+
const { errors } = await loadQueueManifest(tmpDir, file);
|
|
88
|
+
expect(errors.some((e) => e.ruleId === "QUEUE-04")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("duplicate item id → QUEUE-05", async () => {
|
|
92
|
+
await writeRfc("RFC-1305");
|
|
93
|
+
const file = await writeManifest(
|
|
94
|
+
"block-e",
|
|
95
|
+
"id: block-e\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-1305\n - id: RFC-1305\n",
|
|
96
|
+
);
|
|
97
|
+
const { errors } = await loadQueueManifest(tmpDir, file);
|
|
98
|
+
expect(errors.some((e) => e.ruleId === "QUEUE-05")).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("dependsOn order violation → QUEUE-06 warning, not blocking", async () => {
|
|
102
|
+
await writeRfc("RFC-1310");
|
|
103
|
+
await writeRfc("RFC-1311", "status: draft\ndependsOn:\n - RFC-1310\n");
|
|
104
|
+
const file = await writeManifest(
|
|
105
|
+
"block-f",
|
|
106
|
+
"id: block-f\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-1311\n - id: RFC-1310\n",
|
|
107
|
+
);
|
|
108
|
+
const { errors, warnings } = await loadQueueManifest(tmpDir, file);
|
|
109
|
+
expect(errors).toEqual([]);
|
|
110
|
+
expect(warnings.some((w) => w.ruleId === "QUEUE-06")).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("empty items is valid", async () => {
|
|
114
|
+
const file = await writeManifest("block-g", "id: block-g\ncreatedAt: 2026-09-23\nitems: []\n");
|
|
115
|
+
const { errors } = await loadQueueManifest(tmpDir, file);
|
|
116
|
+
expect(errors).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("runQueueValidate — command contract (RFC-1140)", () => {
|
|
121
|
+
test("throws without --file", async () => {
|
|
122
|
+
await expect(
|
|
123
|
+
runQueueValidate({ argv: [], flags: {} }, testContext()),
|
|
124
|
+
).rejects.toThrow("--file");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("valid manifest → pass, items + next in JSON output", async () => {
|
|
128
|
+
await writeRfc("RFC-1400", "status: implemented\n");
|
|
129
|
+
await writeRfc("RFC-1401");
|
|
130
|
+
const file = await writeManifest(
|
|
131
|
+
"block-h",
|
|
132
|
+
"id: block-h\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-1400\n - id: RFC-1401\n",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = await runQueueValidate(
|
|
136
|
+
{ argv: [], flags: { file } },
|
|
137
|
+
testContext(),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result?.data?.status).toBe("pass");
|
|
141
|
+
expect(result?.data?.queue).toBe("block-h");
|
|
142
|
+
expect(result?.data?.items).toHaveLength(2);
|
|
143
|
+
expect(result?.data?.items[0]?.status).toBe("implemented");
|
|
144
|
+
expect(result?.data?.items[1]?.status).toBe("pending");
|
|
145
|
+
expect(result?.data?.next).toBe("RFC-1401");
|
|
146
|
+
expect(result?.exitCode).toBe(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("invalid manifest → fail + exitCode 1", async () => {
|
|
150
|
+
const file = await writeManifest(
|
|
151
|
+
"block-i",
|
|
152
|
+
"id: block-i\ncreatedAt: 2026-09-23\nitems:\n - id: RFC-9999\n",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const result = await runQueueValidate(
|
|
156
|
+
{ argv: [], flags: { file } },
|
|
157
|
+
testContext(),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(result?.data?.status).toBe("fail");
|
|
161
|
+
expect(result?.exitCode).toBe(1);
|
|
162
|
+
expect(result?.data?.errors.length).toBeGreaterThan(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("empty items → pass with next null", async () => {
|
|
166
|
+
const file = await writeManifest("block-j", "id: block-j\ncreatedAt: 2026-09-23\nitems: []\n");
|
|
167
|
+
|
|
168
|
+
const result = await runQueueValidate(
|
|
169
|
+
{ argv: [], flags: { file } },
|
|
170
|
+
testContext(),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(result?.data?.status).toBe("pass");
|
|
174
|
+
expect(result?.data?.items).toEqual([]);
|
|
175
|
+
expect(result?.data?.next).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Register the queue.validate command with the forge kernel registry (RFC-1140).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement handler logic here — delegate to handlers/queue-validate.ts.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-1140: initial forgeQueueModule registering queue.validate.</item>
|
|
10
|
+
<item>RFC-1140: steps 1-4 — shared resolver, queue module, registration
|
|
11
|
+
|
|
12
|
+
Extract pipeline-status derivation into packages/forge/src/pipeline-status.ts, refactor rfc.pipeline.status onto it, add os/queue module with queue.validate command, register in WORKSHOP_MODULE_MAP.forge + bin/cli.ts + package.json exports.</item>
|
|
13
|
+
<item>RFC-1140: queue.validate missing contract/rules metadata (DNA-91)
|
|
14
|
+
|
|
15
|
+
fo-review REVIEW-CODE-2026-09-23-01 finding: declare contract queue and emitted ruleIds QUEUE-01..06 so validator.inventory.generate does not fail closed. Manifest regenerated.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ForgeModule } from "../../src/forge-module.ts";
|
|
20
|
+
|
|
21
|
+
export async function createForgeQueueModule(): Promise<ForgeModule> {
|
|
22
|
+
const { runQueueValidate } = await import("./handlers/queue-validate.ts");
|
|
23
|
+
return {
|
|
24
|
+
name: "forge-queue",
|
|
25
|
+
version: "0.1.0",
|
|
26
|
+
runtime: "autonomous",
|
|
27
|
+
declarations: [],
|
|
28
|
+
commands: [
|
|
29
|
+
{
|
|
30
|
+
name: "queue.validate",
|
|
31
|
+
description:
|
|
32
|
+
"Validate a queue manifest (docs/queues/*.yaml) and report derived " +
|
|
33
|
+
"per-item pipeline status plus the next actionable item. Read-only — " +
|
|
34
|
+
"the pipeline orchestrator runs this before starting a queued batch (RFC-1140). Required flags: `--file`.",
|
|
35
|
+
scope: "workspace",
|
|
36
|
+
contract: "queue",
|
|
37
|
+
rules: ["QUEUE-01", "QUEUE-02", "QUEUE-03", "QUEUE-04", "QUEUE-05", "QUEUE-06"],
|
|
38
|
+
mutatesState: false,
|
|
39
|
+
reads: [
|
|
40
|
+
"docs/queues/*.yaml",
|
|
41
|
+
"docs/rfcs/**/*.md",
|
|
42
|
+
"docs/adrs/**/*.md",
|
|
43
|
+
"docs/audits/*.md",
|
|
44
|
+
"docs/plans/*.md",
|
|
45
|
+
],
|
|
46
|
+
cacheable: false,
|
|
47
|
+
flags: {
|
|
48
|
+
file: {
|
|
49
|
+
kind: "string",
|
|
50
|
+
required: true,
|
|
51
|
+
description: "Path to the queue manifest YAML file.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
execute: runQueueValidate,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
pipelines: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Queue manifest contract (RFC-1140) — the durable ordered document
|
|
4
|
+
block consumed by queue.validate and the pipeline orchestrator's queue mode.</purpose>
|
|
5
|
+
<non-goals>
|
|
6
|
+
<item>Do not store per-item progress state — status is derived, never persisted.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-1140: initial QueueManifest schema and queue.validate result types.</item>
|
|
11
|
+
<item>RFC-1140: steps 1-4 — shared resolver, queue module, registration
|
|
12
|
+
|
|
13
|
+
Extract pipeline-status derivation into packages/forge/src/pipeline-status.ts, refactor rfc.pipeline.status onto it, add os/queue module with queue.validate command, register in WORKSHOP_MODULE_MAP.forge + bin/cli.ts + package.json exports.</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
import type { Diagnostic } from "../../src/types.ts";
|
|
20
|
+
import type { QueueItemReport } from "../../src/pipeline-status.ts";
|
|
21
|
+
|
|
22
|
+
export const QUEUE_ITEM_ID_PATTERN = /^(RFC|ADR)-\d{4}$/;
|
|
23
|
+
|
|
24
|
+
export const queueManifestSchema = z.object({
|
|
25
|
+
id: z.string().min(1),
|
|
26
|
+
createdAt: z.string().min(1),
|
|
27
|
+
items: z
|
|
28
|
+
.array(
|
|
29
|
+
z.object({
|
|
30
|
+
id: z.string().min(1),
|
|
31
|
+
note: z.string().optional(),
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
.default([]),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type QueueManifest = z.infer<typeof queueManifestSchema>;
|
|
38
|
+
|
|
39
|
+
export interface QueueValidateResult {
|
|
40
|
+
command: "queue.validate";
|
|
41
|
+
status: "pass" | "fail";
|
|
42
|
+
queue: string;
|
|
43
|
+
file: string;
|
|
44
|
+
items: QueueItemReport[];
|
|
45
|
+
/** First item whose derived status is pending or in-progress; null when fully terminal. */
|
|
46
|
+
next: string | null;
|
|
47
|
+
errors: Diagnostic[];
|
|
48
|
+
warnings: Diagnostic[];
|
|
49
|
+
}
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
</non-goals>
|
|
7
7
|
</MODULE_CONTRACT>
|
|
8
8
|
<CHANGE_SUMMARY>
|
|
9
|
-
<item>RFC-0303: extracted list and create handlers from handlers.ts into handlers/list-create.ts.</item>
|
|
10
9
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
11
10
|
|
|
12
11
|
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.</item>
|
|
13
12
|
<item>RFC-1097: sweep — werkstatt-engine clean
|
|
14
13
|
|
|
15
14
|
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.</item>
|
|
15
|
+
<item>RFC-1138: pipeline hygiene — module-scoped exempt entries, archive gitignore guard, dns upsert to deploy phases, promote auto-sync, rfc.create claim protocol, siteHasRuntime filter</item>
|
|
16
|
+
<item>RFC-1139: CLI hint accuracy and agent-safety hygiene — rfc.create hint, EC-14-PARTIAL, amend delegation, ledger scope, sync footer, mission.open remnants</item>
|
|
17
|
+
<item>RFC-1139: second hint fix (rfc.list empty-state) + read-only test fix</item>
|
|
18
|
+
<history>RFC-0303</history>
|
|
16
19
|
</CHANGE_SUMMARY>
|
|
17
20
|
*/
|
|
18
21
|
|
|
@@ -140,7 +143,7 @@ export async function runRfcList(
|
|
|
140
143
|
entries.length === 0
|
|
141
144
|
? [
|
|
142
145
|
{
|
|
143
|
-
action: `Create a new RFC: pnpm exec
|
|
146
|
+
action: `Create a new RFC: pnpm exec werkstatt run rfc.create --title "..."`,
|
|
144
147
|
kind: "optional",
|
|
145
148
|
},
|
|
146
149
|
]
|
|
@@ -184,21 +187,6 @@ export async function runRfcCreate(
|
|
|
184
187
|
}
|
|
185
188
|
await assertSatisfiesIdsExist(workspaceRoot, satisfies);
|
|
186
189
|
|
|
187
|
-
const files = await listRfcFiles(rfcDirPath);
|
|
188
|
-
let maxId = 0;
|
|
189
|
-
for (const f of files) {
|
|
190
|
-
const basename = path.basename(f);
|
|
191
|
-
const match = basename.match(/^rfc-(\d{4})/);
|
|
192
|
-
if (match) {
|
|
193
|
-
const num = parseInt(match[1]!, 10);
|
|
194
|
-
if (num > maxId) maxId = num;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const nextNum = maxId + 1;
|
|
198
|
-
const paddedNum = String(nextNum).padStart(4, "0");
|
|
199
|
-
const nextSlug = `rfc-${paddedNum}`;
|
|
200
|
-
const nextId = `RFC-${paddedNum}`;
|
|
201
|
-
|
|
202
190
|
const templatePath = resolveRfcTemplate(workspaceRoot);
|
|
203
191
|
let templateContent: string;
|
|
204
192
|
try {
|
|
@@ -208,7 +196,6 @@ export async function runRfcCreate(
|
|
|
208
196
|
}
|
|
209
197
|
|
|
210
198
|
const kebabTitle = toKebabCase(title);
|
|
211
|
-
const fileName = `${nextSlug}-${kebabTitle}.md`;
|
|
212
199
|
|
|
213
200
|
// RFC-0329: consult decision log before scaffolding
|
|
214
201
|
let consultedDecisions: ConsultedDecision[] = [];
|
|
@@ -230,25 +217,86 @@ export async function runRfcCreate(
|
|
|
230
217
|
}
|
|
231
218
|
}
|
|
232
219
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
220
|
+
// RFC-1138: atomic id allocation — scan → claim the id via wx on a
|
|
221
|
+
// title-independent `rfc-NNNN.claim` file → write the real file → release.
|
|
222
|
+
// The claim must be on the id, not the filename: two creates with different
|
|
223
|
+
// titles produce different filenames, so a wx write on the target path can
|
|
224
|
+
// never collide. `.claim` files are invisible to listRfcFiles (.md-only),
|
|
225
|
+
// so they don't pollute rfc.list or the maxId scan. Stale claims (crashed
|
|
226
|
+
// create) are reclaimed after CLAIM_STALE_MS.
|
|
227
|
+
const MAX_CREATE_ATTEMPTS = 10;
|
|
228
|
+
const CLAIM_STALE_MS = 30_000;
|
|
229
|
+
let nextId = "";
|
|
230
|
+
let relativeFile = "";
|
|
231
|
+
let allocated = false;
|
|
232
|
+
|
|
233
|
+
const files = await listRfcFiles(rfcDirPath);
|
|
234
|
+
let maxId = 0;
|
|
235
|
+
for (const f of files) {
|
|
236
|
+
const basename = path.basename(f);
|
|
237
|
+
const match = basename.match(/^rfc-(\d{4})/);
|
|
238
|
+
if (match) {
|
|
239
|
+
const num = parseInt(match[1]!, 10);
|
|
240
|
+
if (num > maxId) maxId = num;
|
|
241
|
+
}
|
|
245
242
|
}
|
|
246
|
-
|
|
243
|
+
let candidate = maxId + 1;
|
|
244
|
+
|
|
245
|
+
for (let attempt = 1; attempt <= MAX_CREATE_ATTEMPTS; attempt++) {
|
|
246
|
+
const paddedNum = String(candidate).padStart(4, "0");
|
|
247
|
+
const nextSlug = `rfc-${paddedNum}`;
|
|
248
|
+
nextId = `RFC-${paddedNum}`;
|
|
249
|
+
const fileName = `${nextSlug}-${kebabTitle}.md`;
|
|
250
|
+
const claimPath = path.join(rfcDirPath, `${nextSlug}.claim`);
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await fs.writeFile(claimPath, `${process.pid}\n`, { flag: "wx" });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
256
|
+
// Stale claim (crashed create) → reclaim and retry the same id.
|
|
257
|
+
// Live claim → another create holds this id — move to the next.
|
|
258
|
+
const st = await fs.stat(claimPath).catch(() => null);
|
|
259
|
+
if (st && Date.now() - st.mtimeMs > CLAIM_STALE_MS) {
|
|
260
|
+
await fs.rm(claimPath, { force: true });
|
|
261
|
+
} else {
|
|
262
|
+
candidate++;
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
247
268
|
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
try {
|
|
270
|
+
let content = templateContent;
|
|
271
|
+
content = content.replace(/^id: RFC-0000$/m, `id: ${nextId}`);
|
|
272
|
+
content = content.replace(/^title: ".*"$/m, `title: "${title}"`);
|
|
273
|
+
content = content.replace(/^kind: \w+$/m, `kind: ${kind}`);
|
|
274
|
+
content = content.replace(/^scope: \w+$/m, `scope: ${scope}`);
|
|
275
|
+
content = content.replace(/^createdAt: YYYY-MM-DD$/m, `createdAt: ${today}`);
|
|
276
|
+
content = content.replace(/^updatedAt: YYYY-MM-DD$/m, `updatedAt: ${today}`);
|
|
277
|
+
if (satisfies.length > 0) {
|
|
278
|
+
content = content.replace(
|
|
279
|
+
/^satisfies: \[\]$/m,
|
|
280
|
+
`satisfies:\n${satisfies.map((id) => ` - ${id}`).join("\n")}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
content = content.replace(/^# RFC-0000: .+$/m, `# ${nextId}: ${title}`);
|
|
284
|
+
|
|
285
|
+
const targetPath = path.join(rfcDirPath, fileName);
|
|
286
|
+
await fs.writeFile(targetPath, content, { flag: "wx" });
|
|
287
|
+
relativeFile = path.join(RFC_DIR, fileName);
|
|
288
|
+
allocated = true;
|
|
289
|
+
} finally {
|
|
290
|
+
await fs.rm(claimPath, { force: true });
|
|
291
|
+
}
|
|
292
|
+
if (allocated) break;
|
|
293
|
+
}
|
|
250
294
|
|
|
251
|
-
|
|
295
|
+
if (!allocated) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`rfc.create: id allocation failed after ${MAX_CREATE_ATTEMPTS} attempts — concurrent creates keep claiming the next id`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
252
300
|
|
|
253
301
|
if (outputFormat === "pretty") {
|
|
254
302
|
logger.success(`Created ${nextId}: ${title}`);
|
|
@@ -269,7 +317,7 @@ export async function runRfcCreate(
|
|
|
269
317
|
summary: `Created ${nextId}: ${title}`,
|
|
270
318
|
nextSteps: [
|
|
271
319
|
{
|
|
272
|
-
action: `Validate the RFC: pnpm exec
|
|
320
|
+
action: `Validate the RFC: pnpm exec werkstatt run rfc.validate --id ${nextId}`,
|
|
273
321
|
kind: "optional",
|
|
274
322
|
},
|
|
275
323
|
],
|
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
</non-goals>
|
|
9
9
|
</MODULE_CONTRACT>
|
|
10
10
|
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-1140: refactored onto the shared resolver in src/pipeline-status.ts —
|
|
12
|
+
artifact finders, TERMINAL_STATUSES, and stage computation now imported from
|
|
13
|
+
the portable layer so queue.validate and the orchestrator share one derivation.</item>
|
|
11
14
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
12
15
|
|
|
13
16
|
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.</item>
|
|
14
17
|
<item>RFC-1097: sweep — werkstatt-engine clean
|
|
15
18
|
|
|
16
19
|
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.</item>
|
|
20
|
+
<item>RFC-1140: steps 1-4 — shared resolver, queue module, registration
|
|
21
|
+
|
|
22
|
+
Extract pipeline-status derivation into packages/forge/src/pipeline-status.ts, refactor rfc.pipeline.status onto it, add os/queue module with queue.validate command, register in WORKSHOP_MODULE_MAP.forge + bin/cli.ts + package.json exports.</item>
|
|
17
23
|
</CHANGE_SUMMARY>
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
|
-
import fs from "node:fs/promises";
|
|
21
26
|
import path from "node:path";
|
|
22
27
|
|
|
23
28
|
import { listRfcFiles, readAndParseRfc } from "../frontmatter-io.ts";
|
|
@@ -26,17 +31,18 @@ import type {
|
|
|
26
31
|
ForgeCommandResult,
|
|
27
32
|
ForgeRuntimeContext,
|
|
28
33
|
} from "../../../src/types.ts";
|
|
34
|
+
import {
|
|
35
|
+
TERMINAL_STATUSES,
|
|
36
|
+
computeRfcPipelineStages,
|
|
37
|
+
findAuditFile,
|
|
38
|
+
findPlanFile,
|
|
39
|
+
nextPipelineStep,
|
|
40
|
+
} from "../../../src/pipeline-status.ts";
|
|
41
|
+
import type { PipelineStage, RfcPipelineStageInfo } from "../../../src/pipeline-status.ts";
|
|
29
42
|
import type { RfcStatus } from "../types.ts";
|
|
30
43
|
import { RFC_DIR } from "../types.ts";
|
|
31
44
|
|
|
32
|
-
export type PipelineStage
|
|
33
|
-
|
|
34
|
-
export interface RfcPipelineStageInfo {
|
|
35
|
-
stage: PipelineStage;
|
|
36
|
-
done: boolean;
|
|
37
|
-
/** File or marker that confirms completion, when applicable. */
|
|
38
|
-
evidence?: string;
|
|
39
|
-
}
|
|
45
|
+
export type { PipelineStage, RfcPipelineStageInfo };
|
|
40
46
|
|
|
41
47
|
export interface RfcPipelineEntry {
|
|
42
48
|
id: string;
|
|
@@ -55,32 +61,6 @@ export interface RfcPipelineStatusResult {
|
|
|
55
61
|
entries: RfcPipelineEntry[];
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
const TERMINAL_STATUSES: ReadonlySet<string> = new Set(["implemented", "rejected", "superseded"]);
|
|
59
|
-
|
|
60
|
-
async function findAuditFile(workspaceRoot: string, rfcId: string): Promise<string | undefined> {
|
|
61
|
-
const auditsDir = path.join(workspaceRoot, "docs/audits");
|
|
62
|
-
try {
|
|
63
|
-
const entries = await fs.readdir(auditsDir);
|
|
64
|
-
const prefix = `audit-${rfcId.toLowerCase()}`;
|
|
65
|
-
const match = entries.find((e) => e.toLowerCase().startsWith(prefix) && e.endsWith(".md"));
|
|
66
|
-
return match ? path.join("docs/audits", match) : undefined;
|
|
67
|
-
} catch {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function findPlanFile(workspaceRoot: string, rfcId: string): Promise<string | undefined> {
|
|
73
|
-
const plansDir = path.join(workspaceRoot, "docs/plans");
|
|
74
|
-
try {
|
|
75
|
-
const entries = await fs.readdir(plansDir);
|
|
76
|
-
const prefix = `plan-${rfcId.toLowerCase()}`;
|
|
77
|
-
const match = entries.find((e) => e.toLowerCase().startsWith(prefix) && e.endsWith(".md"));
|
|
78
|
-
return match ? path.join("docs/plans", match) : undefined;
|
|
79
|
-
} catch {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
64
|
export async function runRfcPipelineStatus(
|
|
85
65
|
input: ForgeCommandInput,
|
|
86
66
|
context: ForgeRuntimeContext,
|
|
@@ -116,38 +96,15 @@ export async function runRfcPipelineStatus(
|
|
|
116
96
|
const auditFile = await findAuditFile(workspaceRoot, id);
|
|
117
97
|
const planFile = await findPlanFile(workspaceRoot, id);
|
|
118
98
|
|
|
119
|
-
const stages: RfcPipelineStageInfo[] =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
evidence: enhancedAt ? `enhancedAt: ${enhancedAt}` : undefined,
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
stage: "plan",
|
|
132
|
-
done: !!planFile,
|
|
133
|
-
evidence: planFile,
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
stage: "implement",
|
|
137
|
-
done: status === "implemented" || !!implementedAt,
|
|
138
|
-
evidence: implementedAt ? `implementedAt: ${implementedAt}` : undefined,
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
let nextStep: PipelineStage | null = null;
|
|
143
|
-
if (!isTerminal) {
|
|
144
|
-
for (const s of stages) {
|
|
145
|
-
if (!s.done) {
|
|
146
|
-
nextStep = s.stage;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
99
|
+
const stages: RfcPipelineStageInfo[] = computeRfcPipelineStages({
|
|
100
|
+
status,
|
|
101
|
+
enhancedAt,
|
|
102
|
+
implementedAt,
|
|
103
|
+
auditFile,
|
|
104
|
+
planFile,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const nextStep: PipelineStage | null = isTerminal ? null : nextPipelineStep(status, stages);
|
|
151
108
|
|
|
152
109
|
entries.push({
|
|
153
110
|
id,
|
|
@@ -385,6 +385,56 @@ describe("V-32: implementation commit drift detection", () => {
|
|
|
385
385
|
}
|
|
386
386
|
});
|
|
387
387
|
|
|
388
|
+
test("no V-32 or V-17 when implemented RFC is later superseded", async () => {
|
|
389
|
+
const dir = createGitRepoWithCommits([
|
|
390
|
+
{ message: "implement: RFC-9999 — step 1", date: "2026-01-02T10:00:00" },
|
|
391
|
+
]);
|
|
392
|
+
try {
|
|
393
|
+
const parsed = makeParsed("superseded", BASE_BODY, {
|
|
394
|
+
supersededBy: "RFC-0001",
|
|
395
|
+
});
|
|
396
|
+
const { add, violations } = makeViolationsCollector();
|
|
397
|
+
const allParsed = new Map([
|
|
398
|
+
[
|
|
399
|
+
"RFC-0001",
|
|
400
|
+
{
|
|
401
|
+
fileName: "rfc-0001-other.md",
|
|
402
|
+
parsed: makeParsed("accepted", BASE_BODY, { supersedes: ["RFC-9999"] }),
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
]);
|
|
406
|
+
await validateSingleRfc(
|
|
407
|
+
"rfc-9999-test.md",
|
|
408
|
+
parsed,
|
|
409
|
+
allParsed,
|
|
410
|
+
new Map(),
|
|
411
|
+
new Set(),
|
|
412
|
+
new Set(),
|
|
413
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
414
|
+
dir,
|
|
415
|
+
add,
|
|
416
|
+
);
|
|
417
|
+
expect(filterRule(violations, "V-32")).toHaveLength(0);
|
|
418
|
+
expect(filterRule(violations, "V-17")).toHaveLength(0);
|
|
419
|
+
} finally {
|
|
420
|
+
rmSync(dir, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("no V-32 when status is rejected", async () => {
|
|
425
|
+
const dir = createGitRepoWithCommits([
|
|
426
|
+
{ message: "implement: RFC-9999 — step 1", date: "2026-01-02T10:00:00" },
|
|
427
|
+
]);
|
|
428
|
+
try {
|
|
429
|
+
const parsed = makeParsed("rejected", BASE_BODY);
|
|
430
|
+
const violations = await runValidateInDir(parsed, dir);
|
|
431
|
+
const v32 = filterRule(violations, "V-32");
|
|
432
|
+
expect(v32).toHaveLength(0);
|
|
433
|
+
} finally {
|
|
434
|
+
rmSync(dir, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
388
438
|
test("no V-32 when no implement: commits exist", async () => {
|
|
389
439
|
const dir = createGitRepoWithCommits([
|
|
390
440
|
{ message: "feat: add some feature", date: "2026-01-02T10:00:00" },
|