@warpgogol/forge 2.7.0 → 2.8.1
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 +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/src/config/forge-config.d.ts +1 -1
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -2
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/agents-generate.d.ts +2 -1
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +24 -4
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/profiles/profile-schema.d.ts +2 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +1 -0
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +1 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +2 -0
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +5 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/profiles/root-agents-godot.md +133 -0
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +51 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +217 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-0727: adr.implement.stamp — the exclusive atomic path for
|
|
5
|
+
accepted/proposed → implemented ADR transitions. Validates preconditions
|
|
6
|
+
and atomically mutates ADR frontmatter.
|
|
7
|
+
</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Does not transition ADRs not in accepted or proposed status.</item>
|
|
10
|
+
<item>Does not add acceptance criteria or evidence checks — ADRs do not have these.</item>
|
|
11
|
+
<item>Does not call the GitHub API or require network access.</item>
|
|
12
|
+
</non-goals>
|
|
13
|
+
</MODULE_CONTRACT>
|
|
14
|
+
<CHANGE_SUMMARY>
|
|
15
|
+
<item>RFC-0727: initial implementation — mirrors rfc.implement.stamp for ADRs.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
20
|
+
import { readFile, unlink, mkdir } from "node:fs/promises";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { writeFileAtomic } from "../../../src/utils/fs-atomic.ts";
|
|
24
|
+
|
|
25
|
+
import { listAdrFiles, readAndParseAdr, adrFileMatchesId } from "../frontmatter-io.ts";
|
|
26
|
+
import { ADR_DIR } from "../types.ts";
|
|
27
|
+
import type { AdrStatus, AdrImplementStampViolation, AdrImplementStampResult } from "../types.ts";
|
|
28
|
+
import type {
|
|
29
|
+
ForgeCommandInput,
|
|
30
|
+
ForgeCommandResult,
|
|
31
|
+
ForgeRuntimeContext,
|
|
32
|
+
} from "../../../src/types.ts";
|
|
33
|
+
|
|
34
|
+
// ─── Git helpers ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function execGit(workspaceRoot: string, args: string[]): Promise<string> {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
execFile("git", args, { cwd: workspaceRoot, timeout: 10000 }, (err, stdout) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
resolve("");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
resolve(stdout.trim());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function isAdrFileClean(workspaceRoot: string, adrRelPath: string): Promise<boolean> {
|
|
49
|
+
const status = await execGit(workspaceRoot, ["status", "--porcelain", "--", adrRelPath]);
|
|
50
|
+
return status.length === 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function commitReachableFromHead(workspaceRoot: string, commitSha: string): Promise<boolean> {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
execFile(
|
|
56
|
+
"git",
|
|
57
|
+
["merge-base", "--is-ancestor", commitSha, "HEAD"],
|
|
58
|
+
{ cwd: workspaceRoot, timeout: 10000 },
|
|
59
|
+
(err) => {
|
|
60
|
+
resolve(!err);
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function commitReferencesAdr(
|
|
67
|
+
workspaceRoot: string,
|
|
68
|
+
commitSha: string,
|
|
69
|
+
adrId: string,
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
const commitMsg = await execGit(workspaceRoot, ["log", "-1", "--format=%B", commitSha]);
|
|
72
|
+
if (commitMsg.includes(adrId)) return true;
|
|
73
|
+
|
|
74
|
+
const changedFiles = await execGit(workspaceRoot, [
|
|
75
|
+
"show",
|
|
76
|
+
"--name-only",
|
|
77
|
+
"--format=",
|
|
78
|
+
commitSha,
|
|
79
|
+
]);
|
|
80
|
+
const slug = adrId.toLowerCase();
|
|
81
|
+
if (changedFiles.toLowerCase().includes(slug)) return true;
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── ADR-specific exclusive lock ─────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function lockFilePath(workspaceRoot: string, adrId: string): string {
|
|
89
|
+
const slug = adrId.toLowerCase();
|
|
90
|
+
return join(workspaceRoot, ".adr-locks", `${slug}.lock`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function acquireAdrLock(
|
|
94
|
+
workspaceRoot: string,
|
|
95
|
+
adrId: string,
|
|
96
|
+
): Promise<{ acquired: boolean; lockPath: string }> {
|
|
97
|
+
const lockPath = lockFilePath(workspaceRoot, adrId);
|
|
98
|
+
const lockDir = dirname(lockPath);
|
|
99
|
+
await mkdir(lockDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
const { open } = await import("node:fs/promises");
|
|
102
|
+
try {
|
|
103
|
+
const handle = await open(lockPath, "wx");
|
|
104
|
+
await handle.writeFile(
|
|
105
|
+
JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }),
|
|
106
|
+
);
|
|
107
|
+
await handle.close();
|
|
108
|
+
return { acquired: true, lockPath };
|
|
109
|
+
} catch {
|
|
110
|
+
return { acquired: false, lockPath };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function releaseAdrLock(lockPath: string): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(lockPath);
|
|
117
|
+
} catch {
|
|
118
|
+
// Lock file already removed — safe
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Frontmatter mutation ────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function mutateAdrFrontmatter(
|
|
125
|
+
source: string,
|
|
126
|
+
status: "implemented",
|
|
127
|
+
implementedAt: string,
|
|
128
|
+
updatedAt: string,
|
|
129
|
+
): string {
|
|
130
|
+
let content = source;
|
|
131
|
+
|
|
132
|
+
content = content.replace(/^status: \w+$/m, `status: ${status}`);
|
|
133
|
+
|
|
134
|
+
if (/^implementedAt:.*$/m.test(content)) {
|
|
135
|
+
content = content.replace(/^implementedAt:.*$/m, `implementedAt: ${implementedAt}`);
|
|
136
|
+
} else {
|
|
137
|
+
content = content.replace(
|
|
138
|
+
/^updatedAt: .*$/m,
|
|
139
|
+
`updatedAt: ${updatedAt}\nimplementedAt: ${implementedAt}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
content = content.replace(/^updatedAt: .*$/m, `updatedAt: ${updatedAt}`);
|
|
144
|
+
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toIsoDate(date: Date): string {
|
|
149
|
+
return date.toISOString().split("T")[0]!;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Main handler ────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export async function runAdrImplementStamp(
|
|
155
|
+
input: ForgeCommandInput,
|
|
156
|
+
context: ForgeRuntimeContext,
|
|
157
|
+
): Promise<ForgeCommandResult<AdrImplementStampResult>> {
|
|
158
|
+
const { workspaceRoot, logger, outputFormat, dryRun } = context;
|
|
159
|
+
const adrDirPath = join(workspaceRoot, ADR_DIR);
|
|
160
|
+
|
|
161
|
+
const targetId = input.flags["id"] as string | undefined;
|
|
162
|
+
const implementationCommit = input.flags["implementation-commit"] as string | undefined;
|
|
163
|
+
|
|
164
|
+
// ── Flag validation ───────────────────────────────────────────────────────
|
|
165
|
+
if (!targetId) {
|
|
166
|
+
throw new Error("adr.implement.stamp requires --id flag (e.g. --id ADR-0003).");
|
|
167
|
+
}
|
|
168
|
+
if (!implementationCommit) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"adr.implement.stamp requires --implementation-commit flag (the SHA of the implementation commit).",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const violations: AdrImplementStampViolation[] = [];
|
|
175
|
+
const isDryRun = dryRun || input.flags["dry-run"] === true;
|
|
176
|
+
|
|
177
|
+
// ── Find the target ADR ───────────────────────────────────────────────────
|
|
178
|
+
const files = await listAdrFiles(adrDirPath);
|
|
179
|
+
const targetFile = files.find((f) => adrFileMatchesId(f, targetId));
|
|
180
|
+
|
|
181
|
+
if (!targetFile) {
|
|
182
|
+
violations.push({
|
|
183
|
+
rule: "ADR-IMP-01",
|
|
184
|
+
message: `ADR ${targetId} not found in ${ADR_DIR}.`,
|
|
185
|
+
});
|
|
186
|
+
return stampFailResult(violations, isDryRun, outputFormat, logger);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const targetParsed = await readAndParseAdr(adrDirPath, targetFile);
|
|
190
|
+
if (!targetParsed) {
|
|
191
|
+
violations.push({
|
|
192
|
+
rule: "ADR-IMP-01",
|
|
193
|
+
message: `Could not parse target ADR ${targetId}.`,
|
|
194
|
+
});
|
|
195
|
+
return stampFailResult(violations, isDryRun, outputFormat, logger);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const fm = targetParsed.parsed.frontmatter;
|
|
199
|
+
const currentStatus = String(fm["status"] ?? "") as AdrStatus;
|
|
200
|
+
|
|
201
|
+
// ── ADR-IMP-01: must be in accepted or proposed status ─────────────────────
|
|
202
|
+
if (currentStatus !== "accepted" && currentStatus !== "proposed") {
|
|
203
|
+
violations.push({
|
|
204
|
+
rule: "ADR-IMP-01",
|
|
205
|
+
message: `ADR ${targetId} has status "${currentStatus}" — only "accepted" or "proposed" ADRs can be stamped to "implemented".`,
|
|
206
|
+
});
|
|
207
|
+
return stampFailResult(violations, isDryRun, outputFormat, logger);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── ADR-IMP-04: ADR file must be clean (no uncommitted edits to the target ADR) ──
|
|
211
|
+
const adrRelPath = join(ADR_DIR, targetFile);
|
|
212
|
+
const adrFileClean = await isAdrFileClean(workspaceRoot, adrRelPath);
|
|
213
|
+
if (!adrFileClean) {
|
|
214
|
+
violations.push({
|
|
215
|
+
rule: "ADR-IMP-04",
|
|
216
|
+
message: `ADR file ${adrRelPath} has uncommitted changes. Commit the ADR file before stamping.`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── ADR-IMP-03: implementation commit validation ──────────────────────────
|
|
221
|
+
const reachable = await commitReachableFromHead(workspaceRoot, implementationCommit);
|
|
222
|
+
if (!reachable) {
|
|
223
|
+
violations.push({
|
|
224
|
+
rule: "ADR-IMP-03",
|
|
225
|
+
message: `Implementation commit ${implementationCommit} is not reachable from HEAD. The commit must be an ancestor of the current HEAD.`,
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
const referencesAdr = await commitReferencesAdr(workspaceRoot, implementationCommit, targetId);
|
|
229
|
+
if (!referencesAdr) {
|
|
230
|
+
violations.push({
|
|
231
|
+
rule: "ADR-IMP-03",
|
|
232
|
+
message: `Implementation commit ${implementationCommit} does not reference ${targetId} in its message or changed files.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// If any violations found, return without mutation
|
|
238
|
+
if (violations.length > 0) {
|
|
239
|
+
return stampFailResult(violations, isDryRun, outputFormat, logger);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── ADR-IMP-05: acquire ADR-specific exclusive lock ───────────────────────
|
|
243
|
+
const { acquired, lockPath } = await acquireAdrLock(workspaceRoot, targetId);
|
|
244
|
+
if (!acquired) {
|
|
245
|
+
violations.push({
|
|
246
|
+
rule: "ADR-IMP-05",
|
|
247
|
+
message: `A concurrent stamp operation is in progress for ${targetId}. Wait for it to complete and retry.`,
|
|
248
|
+
});
|
|
249
|
+
return stampFailResult(violations, isDryRun, outputFormat, logger);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// ── Atomic mutation ──────────────────────────────────────────────────────
|
|
254
|
+
const adrFilePath = join(adrDirPath, targetFile);
|
|
255
|
+
const adrSource = await readFile(adrFilePath, "utf-8");
|
|
256
|
+
|
|
257
|
+
const today = toIsoDate(new Date());
|
|
258
|
+
const stampedAt = new Date().toISOString();
|
|
259
|
+
const mutatedSource = mutateAdrFrontmatter(adrSource, "implemented", today, today);
|
|
260
|
+
|
|
261
|
+
if (isDryRun) {
|
|
262
|
+
if (outputFormat === "pretty") {
|
|
263
|
+
logger.success(`[dry-run] ${targetId} would be stamped as implemented`);
|
|
264
|
+
logger.info(` implementation commit: ${implementationCommit}`);
|
|
265
|
+
logger.info(` stamped at: ${stampedAt}`);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
data: {
|
|
269
|
+
command: "adr.implement.stamp",
|
|
270
|
+
status: "pass",
|
|
271
|
+
data: {
|
|
272
|
+
adrId: targetId,
|
|
273
|
+
implementationCommit,
|
|
274
|
+
stampedAt,
|
|
275
|
+
},
|
|
276
|
+
violations: [],
|
|
277
|
+
},
|
|
278
|
+
exitCode: 0,
|
|
279
|
+
summary: `adr.implement.stamp: dry-run — ${targetId} would be stamped`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await writeFileAtomic(adrFilePath, mutatedSource);
|
|
284
|
+
|
|
285
|
+
if (outputFormat === "pretty") {
|
|
286
|
+
logger.success(`${targetId} stamped as implemented`);
|
|
287
|
+
logger.info(` implementation commit: ${implementationCommit}`);
|
|
288
|
+
logger.info(` stamped at: ${stampedAt}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
data: {
|
|
293
|
+
command: "adr.implement.stamp",
|
|
294
|
+
status: "pass",
|
|
295
|
+
data: {
|
|
296
|
+
adrId: targetId,
|
|
297
|
+
implementationCommit,
|
|
298
|
+
stampedAt,
|
|
299
|
+
},
|
|
300
|
+
violations: [],
|
|
301
|
+
},
|
|
302
|
+
exitCode: 0,
|
|
303
|
+
summary: `adr.implement.stamp: ${targetId} stamped as implemented`,
|
|
304
|
+
};
|
|
305
|
+
} finally {
|
|
306
|
+
await releaseAdrLock(lockPath);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function stampFailResult(
|
|
311
|
+
violations: AdrImplementStampViolation[],
|
|
312
|
+
isDryRun: boolean,
|
|
313
|
+
outputFormat: string,
|
|
314
|
+
logger: { warn: (msg: string) => void; info: (msg: string) => void },
|
|
315
|
+
): ForgeCommandResult<AdrImplementStampResult> {
|
|
316
|
+
const prefix = isDryRun ? "[dry-run] " : "";
|
|
317
|
+
if (outputFormat === "pretty") {
|
|
318
|
+
for (const v of violations) {
|
|
319
|
+
logger.warn(`${prefix}${v.rule}: ${v.message}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
data: {
|
|
324
|
+
command: "adr.implement.stamp",
|
|
325
|
+
status: "fail",
|
|
326
|
+
violations,
|
|
327
|
+
},
|
|
328
|
+
exitCode: 1,
|
|
329
|
+
summary: `${prefix}adr.implement.stamp: ${violations.length} violation(s)`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>ADR list and create handlers mirroring the RFC domain.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not introduce app-specific runtime composition or deployment behavior.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0366: create ADR list and create handlers mirroring the RFC domain.</item>
|
|
10
|
+
<item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { toKebabCase } from "../../../src/utils/string-utils.ts";
|
|
17
|
+
import { listAdrFiles, readAndParseAdr } from "../frontmatter-io.ts";
|
|
18
|
+
import type {
|
|
19
|
+
ForgeCommandInput,
|
|
20
|
+
ForgeCommandResult,
|
|
21
|
+
ForgeRuntimeContext,
|
|
22
|
+
} from "../../../src/types.ts";
|
|
23
|
+
import type {
|
|
24
|
+
AdrListEntry,
|
|
25
|
+
AdrListResult,
|
|
26
|
+
AdrCreateResult,
|
|
27
|
+
AdrStatus,
|
|
28
|
+
AdrScope,
|
|
29
|
+
} from "../types.ts";
|
|
30
|
+
import { ADR_DIR, ADR_TEMPLATE_FILE, ADR_SCOPES, ADR_STATUSES } from "../types.ts";
|
|
31
|
+
|
|
32
|
+
function toIsoDate(d: Date): string {
|
|
33
|
+
return d.toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runAdrList(
|
|
37
|
+
input: ForgeCommandInput,
|
|
38
|
+
context: ForgeRuntimeContext,
|
|
39
|
+
): Promise<ForgeCommandResult<AdrListResult>> {
|
|
40
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
41
|
+
const adrDirPath = path.join(workspaceRoot, ADR_DIR);
|
|
42
|
+
|
|
43
|
+
const filterStatus = input.flags["status"] as string | undefined;
|
|
44
|
+
const filterScope = input.flags["scope"] as string | undefined;
|
|
45
|
+
const filterDecider = input.flags["decider"] as string | undefined;
|
|
46
|
+
|
|
47
|
+
const files = await listAdrFiles(adrDirPath);
|
|
48
|
+
const entries: AdrListEntry[] = [];
|
|
49
|
+
|
|
50
|
+
for (const fileName of files) {
|
|
51
|
+
const result = await readAndParseAdr(adrDirPath, fileName);
|
|
52
|
+
if (!result) continue;
|
|
53
|
+
|
|
54
|
+
const fm = result.parsed.frontmatter;
|
|
55
|
+
const id = String(fm["id"] ?? "");
|
|
56
|
+
const status = String(fm["status"] ?? "") as AdrStatus;
|
|
57
|
+
const scope = String(fm["scope"] ?? "") as AdrScope;
|
|
58
|
+
const decider = String(fm["decider"] ?? "");
|
|
59
|
+
|
|
60
|
+
if (filterStatus && status !== filterStatus) continue;
|
|
61
|
+
if (filterScope && scope !== filterScope) continue;
|
|
62
|
+
if (filterDecider && decider !== filterDecider) continue;
|
|
63
|
+
|
|
64
|
+
entries.push({
|
|
65
|
+
id,
|
|
66
|
+
title: String(fm["title"] ?? ""),
|
|
67
|
+
status,
|
|
68
|
+
scope,
|
|
69
|
+
decider,
|
|
70
|
+
updatedAt: String(fm["updatedAt"] ?? ""),
|
|
71
|
+
file: path.join(ADR_DIR, fileName),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (outputFormat === "pretty") {
|
|
76
|
+
if (entries.length === 0) {
|
|
77
|
+
logger.info("No ADRs found matching the given filters.");
|
|
78
|
+
} else {
|
|
79
|
+
logger.section(`ADRs (${entries.length})`);
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
logger.info(
|
|
82
|
+
`${entry.id} ${entry.status.padEnd(12)} ${entry.scope.padEnd(10)} ${entry.decider.padEnd(20)} ${entry.title}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
data: {
|
|
90
|
+
command: "adr.list",
|
|
91
|
+
status: "ok",
|
|
92
|
+
count: entries.length,
|
|
93
|
+
entries,
|
|
94
|
+
},
|
|
95
|
+
summary: `${entries.length} ADR(s) found`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runAdrCreate(
|
|
100
|
+
input: ForgeCommandInput,
|
|
101
|
+
context: ForgeRuntimeContext,
|
|
102
|
+
): Promise<ForgeCommandResult<AdrCreateResult>> {
|
|
103
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
104
|
+
const adrDirPath = path.join(workspaceRoot, ADR_DIR);
|
|
105
|
+
|
|
106
|
+
const title = input.flags["title"] as string | undefined;
|
|
107
|
+
if (!title) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'adr.create requires --title flag, e.g. --title="Use DuckDB for local analytics"',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const scope = (input.flags["scope"] as string | undefined) ?? "package";
|
|
114
|
+
const decider = (input.flags["decider"] as string | undefined) ?? "architecture";
|
|
115
|
+
const status = (input.flags["status"] as string | undefined) ?? "proposed";
|
|
116
|
+
const relatedRaw = input.flags["related"] as string | undefined;
|
|
117
|
+
|
|
118
|
+
if (!ADR_SCOPES.includes(scope as AdrScope)) {
|
|
119
|
+
throw new Error(`Invalid scope: ${scope}. Must be one of: ${ADR_SCOPES.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
if (!ADR_STATUSES.includes(status as AdrStatus)) {
|
|
122
|
+
throw new Error(`Invalid status: ${status}. Must be one of: ${ADR_STATUSES.join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const files = await listAdrFiles(adrDirPath);
|
|
126
|
+
let maxId = 0;
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
const basename = path.basename(f);
|
|
129
|
+
const match = basename.match(/^adr-(\d{4})/);
|
|
130
|
+
if (match) {
|
|
131
|
+
const num = parseInt(match[1]!, 10);
|
|
132
|
+
if (num > maxId) maxId = num;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const nextNum = maxId + 1;
|
|
136
|
+
const paddedNum = String(nextNum).padStart(4, "0");
|
|
137
|
+
const nextSlug = `adr-${paddedNum}`;
|
|
138
|
+
const nextId = `ADR-${paddedNum}`;
|
|
139
|
+
|
|
140
|
+
const templateRelPath = ADR_TEMPLATE_FILE;
|
|
141
|
+
const templatePath = path.join(workspaceRoot, templateRelPath);
|
|
142
|
+
let templateContent: string;
|
|
143
|
+
try {
|
|
144
|
+
templateContent = await fs.readFile(templatePath, "utf-8");
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`ADR template not found at ${templateRelPath}. Ensure docs/adrs/ contains the template.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const today = toIsoDate(new Date());
|
|
152
|
+
const kebabTitle = toKebabCase(title);
|
|
153
|
+
const fileName = `${nextSlug}-${kebabTitle}.md`;
|
|
154
|
+
|
|
155
|
+
let content = templateContent;
|
|
156
|
+
content = content.replace(/^id: ADR-0000$/m, `id: ${nextId}`);
|
|
157
|
+
const escapedTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
158
|
+
content = content.replace(/^title: ".*"$/m, `title: "${escapedTitle}"`);
|
|
159
|
+
content = content.replace(/^status: \w+$/m, `status: ${status}`);
|
|
160
|
+
content = content.replace(/^scope: \w+$/m, `scope: ${scope}`);
|
|
161
|
+
content = content.replace(/^decider: \w+$/m, `decider: ${decider}`);
|
|
162
|
+
content = content.replace(/^createdAt: YYYY-MM-DD$/m, `createdAt: ${today}`);
|
|
163
|
+
content = content.replace(/^updatedAt: YYYY-MM-DD$/m, `updatedAt: ${today}`);
|
|
164
|
+
content = content.replace(/^# ADR-0000: .+$/m, `# ${nextId}: ${title}`);
|
|
165
|
+
|
|
166
|
+
const relatedIds = relatedRaw
|
|
167
|
+
? relatedRaw
|
|
168
|
+
.split(",")
|
|
169
|
+
.map((s) => s.trim())
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
: [];
|
|
172
|
+
if (relatedIds.length > 0) {
|
|
173
|
+
content = content.replace(
|
|
174
|
+
/^related:\s*\n\s*- RFC-0001\s*$/m,
|
|
175
|
+
`related:\n${relatedIds.map((id) => ` - ${id}`).join("\n")}`,
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
content = content.replace(/^related:\s*\n\s*- RFC-0001\s*$/m, `related: []`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await fs.mkdir(adrDirPath, { recursive: true });
|
|
183
|
+
} catch {
|
|
184
|
+
// ignore
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const targetPath = path.join(adrDirPath, fileName);
|
|
188
|
+
await fs.writeFile(targetPath, content, "utf-8");
|
|
189
|
+
|
|
190
|
+
const relativeFile = path.join(ADR_DIR, fileName);
|
|
191
|
+
|
|
192
|
+
if (outputFormat === "pretty") {
|
|
193
|
+
logger.success(`Created ${nextId}: ${title}`);
|
|
194
|
+
logger.info(`File: ${relativeFile}`);
|
|
195
|
+
logger.info(`Status: ${status} (only the named decider may change status)`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
data: {
|
|
200
|
+
command: "adr.create",
|
|
201
|
+
status: "ok",
|
|
202
|
+
file: relativeFile,
|
|
203
|
+
id: nextId,
|
|
204
|
+
},
|
|
205
|
+
summary: `Created ${nextId}: ${title}`,
|
|
206
|
+
};
|
|
207
|
+
}
|