@warpgogol/forge 2.8.0 → 2.8.2
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/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.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 +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- 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/__tests__/resolve-terminology.test.ts +45 -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/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -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/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -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/__tests__/workspace-discovery.test.ts +134 -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/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -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 +54 -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 +221 -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/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -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/__tests__/note-orphan-detect.test.ts +94 -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,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Discovery and parsing for program manifests and packet files.
|
|
4
|
+
Reads YAML frontmatter from program.yaml and packet .md files, validates
|
|
5
|
+
against schemas, and computes normative source hashes.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not import from @warpgogol/* — forge must remain dependency-free.</item>
|
|
8
|
+
<item>Do not mutate files — discovery is read-only.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>RFC-0856: initial program/packet discovery and parsing.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { parse as parseYaml } from "yaml";
|
|
21
|
+
import {
|
|
22
|
+
programManifestSchema,
|
|
23
|
+
programPacketSchema,
|
|
24
|
+
type ProgramManifest,
|
|
25
|
+
type ProgramPacket,
|
|
26
|
+
type ProgramPacketIndexEntry,
|
|
27
|
+
} from "./schemas.ts";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Program discovery
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface ProgramLocation {
|
|
34
|
+
programDir: string;
|
|
35
|
+
manifestPath: string;
|
|
36
|
+
manifest: ProgramManifest;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Discover a program by RFC id. Scans docs/plans/ for a program.yaml whose
|
|
41
|
+
* `program` field matches the given RFC id.
|
|
42
|
+
*/
|
|
43
|
+
export function discoverProgram(workspaceRoot: string, programRfc: string): ProgramLocation | null {
|
|
44
|
+
const plansDir = path.join(workspaceRoot, "docs", "plans");
|
|
45
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
46
|
+
|
|
47
|
+
for (const entry of fs.readdirSync(plansDir, { withFileTypes: true })) {
|
|
48
|
+
if (!entry.isDirectory()) continue;
|
|
49
|
+
const manifestPath = path.join(plansDir, entry.name, "program.yaml");
|
|
50
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
51
|
+
|
|
52
|
+
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
53
|
+
const parsed = parseYaml(raw) as Record<string, unknown>;
|
|
54
|
+
if (parsed.program !== programRfc) continue;
|
|
55
|
+
|
|
56
|
+
const result = programManifestSchema.safeParse(parsed);
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`PROGRAM-PACKET-01: malformed program manifest at ${manifestPath}: ${result.error.message}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
programDir: path.join(plansDir, entry.name),
|
|
65
|
+
manifestPath,
|
|
66
|
+
manifest: result.data,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Packet parsing
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse a packet .md file: extract YAML frontmatter and validate against
|
|
79
|
+
* the forge/program-packet@1 schema.
|
|
80
|
+
*/
|
|
81
|
+
export function parsePacketFile(packetPath: string): ProgramPacket {
|
|
82
|
+
const raw = fs.readFileSync(packetPath, "utf8");
|
|
83
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
84
|
+
if (!match) {
|
|
85
|
+
throw new Error(`PROGRAM-PACKET-01: missing frontmatter in ${packetPath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fm = parseYaml(match[1]) as Record<string, unknown>;
|
|
89
|
+
const result = programPacketSchema.safeParse(fm);
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`PROGRAM-PACKET-01: malformed packet at ${packetPath}: ${result.error.message}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find a packet by packetId within a program.
|
|
101
|
+
*/
|
|
102
|
+
export function findPacketEntry(
|
|
103
|
+
manifest: ProgramManifest,
|
|
104
|
+
packetId: string,
|
|
105
|
+
): ProgramPacketIndexEntry | null {
|
|
106
|
+
return manifest.packets.find((p) => p.packetId === packetId) ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the full path to a packet file.
|
|
111
|
+
*/
|
|
112
|
+
export function resolvePacketPath(programDir: string, entry: ProgramPacketIndexEntry): string {
|
|
113
|
+
return path.join(programDir, entry.file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Normative source hashing
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compute SHA-256 hex digest of a file's bytes.
|
|
122
|
+
*/
|
|
123
|
+
export function fileSha256(filePath: string): string {
|
|
124
|
+
const bytes = fs.readFileSync(filePath);
|
|
125
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Verify normative source hashes for a packet. Returns violations.
|
|
130
|
+
*/
|
|
131
|
+
export function verifyNormativeSources(
|
|
132
|
+
workspaceRoot: string,
|
|
133
|
+
packet: ProgramPacket,
|
|
134
|
+
): Array<{ path: string; expected: string; actual: string }> {
|
|
135
|
+
const violations: Array<{ path: string; expected: string; actual: string }> = [];
|
|
136
|
+
|
|
137
|
+
for (const source of packet.normativeSources) {
|
|
138
|
+
const fullPath = path.join(workspaceRoot, source.path);
|
|
139
|
+
if (!fs.existsSync(fullPath)) {
|
|
140
|
+
violations.push({
|
|
141
|
+
path: source.path,
|
|
142
|
+
expected: source.sha256,
|
|
143
|
+
actual: "<missing>",
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const actual = fileSha256(fullPath);
|
|
148
|
+
if (actual !== source.sha256) {
|
|
149
|
+
violations.push({
|
|
150
|
+
path: source.path,
|
|
151
|
+
expected: source.sha256,
|
|
152
|
+
actual,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return violations;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Git helpers (read-only)
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
export function gitHead(workspaceRoot: string): string {
|
|
165
|
+
return execSync("git rev-parse HEAD", {
|
|
166
|
+
cwd: workspaceRoot,
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
169
|
+
}).trim();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function gitBranch(workspaceRoot: string): string {
|
|
173
|
+
return execSync("git branch --show-current", {
|
|
174
|
+
cwd: workspaceRoot,
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
177
|
+
}).trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function gitIsClean(workspaceRoot: string): boolean {
|
|
181
|
+
const status = execSync("git status --porcelain", {
|
|
182
|
+
cwd: workspaceRoot,
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
185
|
+
}).trim();
|
|
186
|
+
return status.length === 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function gitAncestorOf(
|
|
190
|
+
workspaceRoot: string,
|
|
191
|
+
ancestor: string,
|
|
192
|
+
descendant: string,
|
|
193
|
+
): boolean {
|
|
194
|
+
try {
|
|
195
|
+
execSync(`git merge-base --is-ancestor ${ancestor} ${descendant}`, {
|
|
196
|
+
cwd: workspaceRoot,
|
|
197
|
+
encoding: "utf8",
|
|
198
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function gitChangedFilesBetween(
|
|
207
|
+
workspaceRoot: string,
|
|
208
|
+
base: string,
|
|
209
|
+
head: string,
|
|
210
|
+
): string[] {
|
|
211
|
+
try {
|
|
212
|
+
const output = execSync(`git diff --name-only ${base}..${head}`, {
|
|
213
|
+
cwd: workspaceRoot,
|
|
214
|
+
encoding: "utf8",
|
|
215
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
216
|
+
}).trim();
|
|
217
|
+
return output.length === 0 ? [] : output.split("\n");
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Path matching (cross-platform)
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Normalize a path to forward slashes for cross-platform comparison.
|
|
229
|
+
*/
|
|
230
|
+
export function normalizePath(p: string): string {
|
|
231
|
+
return p.replace(/\\/g, "/");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve ".." and "." segments in a path without touching the filesystem.
|
|
236
|
+
* Returns the lexical normalization (e.g. "a/b/../c" → "a/c").
|
|
237
|
+
*/
|
|
238
|
+
function resolveLexical(p: string): string {
|
|
239
|
+
const segments = p.split("/");
|
|
240
|
+
const result: string[] = [];
|
|
241
|
+
for (const seg of segments) {
|
|
242
|
+
if (seg === "..") {
|
|
243
|
+
result.pop();
|
|
244
|
+
} else if (seg !== "." && seg !== "") {
|
|
245
|
+
result.push(seg);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result.join("/");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if a path matches a glob pattern. Supports:
|
|
253
|
+
* - `**` for recursive directory matching
|
|
254
|
+
* - `*` for single-level wildcard
|
|
255
|
+
* - exact prefix matching with trailing `/**`
|
|
256
|
+
*/
|
|
257
|
+
export function pathMatchesGlob(filePath: string, pattern: string): boolean {
|
|
258
|
+
const normalized = resolveLexical(normalizePath(filePath));
|
|
259
|
+
const glob = resolveLexical(normalizePath(pattern));
|
|
260
|
+
|
|
261
|
+
// Exact match
|
|
262
|
+
if (normalized === glob) return true;
|
|
263
|
+
|
|
264
|
+
// /** suffix — prefix match
|
|
265
|
+
if (glob.endsWith("/**")) {
|
|
266
|
+
const prefix = glob.slice(0, -3);
|
|
267
|
+
return normalized === prefix || normalized.startsWith(prefix + "/");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// * wildcard matching (simplified)
|
|
271
|
+
const regexStr = glob
|
|
272
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
273
|
+
.replace(/\*\*/g, "<<<GLOBSTAR>>>")
|
|
274
|
+
.replace(/\*/g, "[^/]*")
|
|
275
|
+
.replace(/<<<GLOBSTAR>>>/g, ".*");
|
|
276
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
277
|
+
return regex.test(normalized);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check if a file path is within the allowed files list.
|
|
282
|
+
*/
|
|
283
|
+
export function isPathAllowed(filePath: string, allowedFiles: string[]): boolean {
|
|
284
|
+
const normalized = resolveLexical(normalizePath(filePath));
|
|
285
|
+
return allowedFiles.some((pattern) => pathMatchesGlob(normalized, pattern));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a file path matches any forbidden pattern.
|
|
290
|
+
*/
|
|
291
|
+
export function isPathForbidden(filePath: string, forbiddenFiles: string[]): boolean {
|
|
292
|
+
const normalized = resolveLexical(normalizePath(filePath));
|
|
293
|
+
return forbiddenFiles.some((pattern) => pathMatchesGlob(normalized, pattern));
|
|
294
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Command handler for program.packet.complete — Steward validates the
|
|
4
|
+
implementation range, writes the completion report, and updates the program
|
|
5
|
+
manifest. Supports --bootstrap for packet 000 (RFC-0856).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not commit — complete writes files only; canonical commit is separate.</item>
|
|
8
|
+
<item>Do not import from @warpgogol/* — forge must remain dependency-free.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>RFC-0856: initial complete command handler.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
19
|
+
import type {
|
|
20
|
+
ForgeCommandInput,
|
|
21
|
+
ForgeCommandResult,
|
|
22
|
+
ForgeRuntimeContext,
|
|
23
|
+
} from "../../../src/types.ts";
|
|
24
|
+
import { writeFileAtomic } from "../../../src/utils/fs-atomic.ts";
|
|
25
|
+
import { byteHash } from "../../../src/utils/hash.ts";
|
|
26
|
+
import {
|
|
27
|
+
discoverProgram,
|
|
28
|
+
findPacketEntry,
|
|
29
|
+
resolvePacketPath,
|
|
30
|
+
parsePacketFile,
|
|
31
|
+
gitHead,
|
|
32
|
+
gitIsClean,
|
|
33
|
+
gitAncestorOf,
|
|
34
|
+
gitChangedFilesBetween,
|
|
35
|
+
} from "../discovery.ts";
|
|
36
|
+
import { readLease, deleteLease, isLeaseStale, hashLeaseToken } from "../lease.ts";
|
|
37
|
+
import { validateCompleteTransition, type PacketViolation } from "../state.ts";
|
|
38
|
+
import type { ProgramManifest, ProgramPacketCompletion } from "../schemas.ts";
|
|
39
|
+
|
|
40
|
+
export interface CompleteResultData {
|
|
41
|
+
command: string;
|
|
42
|
+
status: "pass" | "fail";
|
|
43
|
+
program: string;
|
|
44
|
+
packetId: string;
|
|
45
|
+
filesModified: string[];
|
|
46
|
+
violations: PacketViolation[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runComplete(
|
|
50
|
+
input: ForgeCommandInput,
|
|
51
|
+
context: ForgeRuntimeContext,
|
|
52
|
+
): Promise<ForgeCommandResult<CompleteResultData>> {
|
|
53
|
+
const program = input.flags.program as string | undefined;
|
|
54
|
+
const packetId = input.flags.packet as string | undefined;
|
|
55
|
+
const steward = input.flags.steward as string | undefined;
|
|
56
|
+
const leaseToken = input.flags["lease-token"] as string | undefined;
|
|
57
|
+
const implementationHead = input.flags["implementation-head"] as string | undefined;
|
|
58
|
+
const _idempotencyKey = input.flags["idempotency-key"] as string | undefined;
|
|
59
|
+
const isBootstrap = input.flags.bootstrap === true;
|
|
60
|
+
|
|
61
|
+
if (!program || !packetId || !steward) {
|
|
62
|
+
return completeFail(program ?? "<missing>", packetId ?? "<missing>", [
|
|
63
|
+
{ rule: "PROGRAM-PACKET-01", message: "--program, --packet, and --steward are required" },
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { workspaceRoot } = context;
|
|
68
|
+
|
|
69
|
+
// Discover program
|
|
70
|
+
let programLocation;
|
|
71
|
+
try {
|
|
72
|
+
programLocation = discoverProgram(workspaceRoot, program);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return completeFail(program, packetId, [
|
|
75
|
+
{ rule: "PROGRAM-PACKET-01", message: String((err as Error).message) },
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
if (!programLocation) {
|
|
79
|
+
return completeFail(program, packetId, [
|
|
80
|
+
{ rule: "PROGRAM-PACKET-01", message: `program ${program} not found` },
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { manifest, manifestPath, programDir } = programLocation;
|
|
85
|
+
const entry = findPacketEntry(manifest, packetId);
|
|
86
|
+
if (!entry) {
|
|
87
|
+
return completeFail(program, packetId, [
|
|
88
|
+
{ rule: "PROGRAM-PACKET-01", message: `packet ${packetId} not found` },
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Bootstrap path (packet 000 only) ---
|
|
93
|
+
if (isBootstrap) {
|
|
94
|
+
return bootstrapComplete(input, context, program, packetId, steward, programLocation);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Normal completion path ---
|
|
98
|
+
if (!leaseToken || !implementationHead) {
|
|
99
|
+
return completeFail(program, packetId, [
|
|
100
|
+
{
|
|
101
|
+
rule: "PROGRAM-PACKET-01",
|
|
102
|
+
message: "--lease-token and --implementation-head are required (non-bootstrap)",
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Read lease
|
|
108
|
+
const lease = readLease(workspaceRoot, program, packetId);
|
|
109
|
+
if (!lease) {
|
|
110
|
+
return completeFail(program, packetId, [
|
|
111
|
+
{ rule: "PROGRAM-PACKET-05", message: "no active lease found" },
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Verify token
|
|
116
|
+
const tokenHash = hashLeaseToken(leaseToken);
|
|
117
|
+
if (tokenHash !== lease.tokenHash) {
|
|
118
|
+
return completeFail(program, packetId, [
|
|
119
|
+
{ rule: "PROGRAM-PACKET-11", message: "lease token mismatch" },
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check lease is not stale
|
|
124
|
+
if (isLeaseStale(lease)) {
|
|
125
|
+
return completeFail(program, packetId, [
|
|
126
|
+
{ rule: "PROGRAM-PACKET-05", message: "lease is stale — recover before completing" },
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse packet
|
|
131
|
+
let packet;
|
|
132
|
+
try {
|
|
133
|
+
packet = parsePacketFile(resolvePacketPath(programDir, entry));
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return completeFail(program, packetId, [
|
|
136
|
+
{ rule: "PROGRAM-PACKET-01", message: String((err as Error).message) },
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Git state
|
|
141
|
+
let head = "";
|
|
142
|
+
let isClean = false;
|
|
143
|
+
try {
|
|
144
|
+
head = gitHead(workspaceRoot);
|
|
145
|
+
isClean = gitIsClean(workspaceRoot);
|
|
146
|
+
} catch {
|
|
147
|
+
return completeFail(program, packetId, [
|
|
148
|
+
{ rule: "PROGRAM-PACKET-02", message: "failed to read git state" },
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check ancestry: implementation head must descend from seal commit
|
|
153
|
+
const sealCommit = lease.sealCommit ?? lease.baseCommit;
|
|
154
|
+
const ancestryValid = gitAncestorOf(workspaceRoot, sealCommit, implementationHead);
|
|
155
|
+
|
|
156
|
+
// Get changed files between seal and implementation head
|
|
157
|
+
const changedFiles = gitChangedFilesBetween(workspaceRoot, sealCommit, implementationHead);
|
|
158
|
+
|
|
159
|
+
// Validate transition
|
|
160
|
+
const violations = validateCompleteTransition(manifest, entry, lease, {
|
|
161
|
+
head,
|
|
162
|
+
isClean,
|
|
163
|
+
steward: steward as ProgramPacketCompletion["completedBy"],
|
|
164
|
+
implementationHead,
|
|
165
|
+
changedFiles,
|
|
166
|
+
allowedFiles: packet.allowedFiles,
|
|
167
|
+
forbiddenFiles: packet.forbiddenFiles,
|
|
168
|
+
ancestryValid,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (violations.length > 0) {
|
|
172
|
+
return completeFail(program, packetId, violations);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Write completion report
|
|
176
|
+
const completionsDir = path.join(programDir, "completions");
|
|
177
|
+
fs.mkdirSync(completionsDir, { recursive: true });
|
|
178
|
+
|
|
179
|
+
const completion: ProgramPacketCompletion = {
|
|
180
|
+
schema: "forge/program-packet-completion@1",
|
|
181
|
+
program,
|
|
182
|
+
packetId,
|
|
183
|
+
baseCommit: entry.baseCommit ?? lease.baseCommit,
|
|
184
|
+
sealCommit: lease.sealCommit ?? lease.baseCommit,
|
|
185
|
+
implementationCommits: [implementationHead],
|
|
186
|
+
implementationHead,
|
|
187
|
+
changedFiles,
|
|
188
|
+
validations: packet.requiredValidations.map((v) => ({
|
|
189
|
+
command: v.command,
|
|
190
|
+
status: "pass" as const,
|
|
191
|
+
evidenceDigest: byteHash(v.command),
|
|
192
|
+
})),
|
|
193
|
+
remainingTransitionDiagnostics: [],
|
|
194
|
+
unexpectedDiagnostics: [],
|
|
195
|
+
recoveryStatus: "not-applicable",
|
|
196
|
+
cleanTrees: true,
|
|
197
|
+
completedBy: steward as ProgramPacketCompletion["completedBy"],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const completionPath = path.join(completionsDir, `${packetId}.json`);
|
|
201
|
+
writeFileAtomic(completionPath, JSON.stringify(completion, null, 2) + "\n");
|
|
202
|
+
|
|
203
|
+
// Update manifest: packet state → completed, completion → head
|
|
204
|
+
const manifestRaw = fs.readFileSync(manifestPath, "utf8");
|
|
205
|
+
const manifestParsed = parseYaml(manifestRaw) as ProgramManifest;
|
|
206
|
+
const idx = manifestParsed.packets.findIndex((p) => p.packetId === packetId);
|
|
207
|
+
if (idx !== -1) {
|
|
208
|
+
manifestParsed.packets[idx].state = "completed";
|
|
209
|
+
manifestParsed.packets[idx].completion = head;
|
|
210
|
+
}
|
|
211
|
+
// Advance currentPacket to next packet
|
|
212
|
+
const nextEntry = manifest.packets.find((p) => p.dependsOnPacket === packetId);
|
|
213
|
+
if (nextEntry) {
|
|
214
|
+
manifestParsed.currentPacket = nextEntry.packetId;
|
|
215
|
+
} else {
|
|
216
|
+
// No next packet — program is complete
|
|
217
|
+
manifestParsed.state = "complete";
|
|
218
|
+
}
|
|
219
|
+
writeFileAtomic(manifestPath, stringifyYaml(manifestParsed));
|
|
220
|
+
|
|
221
|
+
// Delete lease
|
|
222
|
+
deleteLease(workspaceRoot, program, packetId);
|
|
223
|
+
|
|
224
|
+
const filesModified = [
|
|
225
|
+
path.relative(workspaceRoot, completionPath),
|
|
226
|
+
path.relative(workspaceRoot, manifestPath),
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
data: {
|
|
231
|
+
command: "program.packet.complete",
|
|
232
|
+
status: "pass",
|
|
233
|
+
program,
|
|
234
|
+
packetId,
|
|
235
|
+
filesModified,
|
|
236
|
+
violations: [],
|
|
237
|
+
},
|
|
238
|
+
exitCode: 0,
|
|
239
|
+
summary: `program.packet.complete: ${packetId} completed at ${head.slice(0, 8)}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Bootstrap completion (packet 000 only)
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
async function bootstrapComplete(
|
|
248
|
+
input: ForgeCommandInput,
|
|
249
|
+
context: ForgeRuntimeContext,
|
|
250
|
+
program: string,
|
|
251
|
+
packetId: string,
|
|
252
|
+
steward: string,
|
|
253
|
+
programLocation: NonNullable<ReturnType<typeof discoverProgram>>,
|
|
254
|
+
): Promise<ForgeCommandResult<CompleteResultData>> {
|
|
255
|
+
const { workspaceRoot } = context;
|
|
256
|
+
const { manifest, manifestPath, programDir } = programLocation;
|
|
257
|
+
|
|
258
|
+
// Bootstrap only for packet 000 (dependsOnPacket === null)
|
|
259
|
+
const entry = findPacketEntry(manifest, packetId);
|
|
260
|
+
if (!entry || entry.dependsOnPacket !== null) {
|
|
261
|
+
return completeFail(program, packetId, [
|
|
262
|
+
{
|
|
263
|
+
rule: "PROGRAM-PACKET-04",
|
|
264
|
+
message: "--bootstrap is only valid for packet 000 (no predecessor)",
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Program must be in preparing state
|
|
270
|
+
if (manifest.state !== "preparing") {
|
|
271
|
+
return completeFail(program, packetId, [
|
|
272
|
+
{
|
|
273
|
+
rule: "PROGRAM-PACKET-04",
|
|
274
|
+
message: `--bootstrap requires program state 'preparing', got '${manifest.state}'`,
|
|
275
|
+
},
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sealCommit = input.flags["seal-commit"] as string | undefined;
|
|
280
|
+
if (!sealCommit) {
|
|
281
|
+
return completeFail(program, packetId, [
|
|
282
|
+
{ rule: "PROGRAM-PACKET-01", message: "--seal-commit is required for --bootstrap" },
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let head = "";
|
|
287
|
+
let isClean = false;
|
|
288
|
+
try {
|
|
289
|
+
head = gitHead(workspaceRoot);
|
|
290
|
+
isClean = gitIsClean(workspaceRoot);
|
|
291
|
+
} catch {
|
|
292
|
+
return completeFail(program, packetId, [
|
|
293
|
+
{ rule: "PROGRAM-PACKET-02", message: "failed to read git state" },
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!isClean) {
|
|
298
|
+
return completeFail(program, packetId, [
|
|
299
|
+
{ rule: "PROGRAM-PACKET-10", message: "dirty tree at bootstrap completion" },
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Verify ancestry: head must descend from seal commit
|
|
304
|
+
const ancestryValid = gitAncestorOf(workspaceRoot, sealCommit, head);
|
|
305
|
+
if (!ancestryValid) {
|
|
306
|
+
return completeFail(program, packetId, [
|
|
307
|
+
{
|
|
308
|
+
rule: "PROGRAM-PACKET-09",
|
|
309
|
+
message: `HEAD does not descend from seal commit ${sealCommit}`,
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Get changed files
|
|
315
|
+
const changedFiles = gitChangedFilesBetween(workspaceRoot, sealCommit, head);
|
|
316
|
+
|
|
317
|
+
// Parse packet for allowed/forbidden files
|
|
318
|
+
let packet;
|
|
319
|
+
try {
|
|
320
|
+
packet = parsePacketFile(resolvePacketPath(programDir, entry));
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return completeFail(program, packetId, [
|
|
323
|
+
{ rule: "PROGRAM-PACKET-01", message: String((err as Error).message) },
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Validate paths
|
|
328
|
+
for (const file of changedFiles) {
|
|
329
|
+
if (packet.forbiddenFiles.length > 0 && isPathForbidden(file, packet.forbiddenFiles)) {
|
|
330
|
+
return completeFail(program, packetId, [
|
|
331
|
+
{ rule: "PROGRAM-PACKET-06", path: file, message: `changed file ${file} is forbidden` },
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Write completion report
|
|
337
|
+
const completionsDir = path.join(programDir, "completions");
|
|
338
|
+
fs.mkdirSync(completionsDir, { recursive: true });
|
|
339
|
+
|
|
340
|
+
const completion: ProgramPacketCompletion = {
|
|
341
|
+
schema: "forge/program-packet-completion@1",
|
|
342
|
+
program,
|
|
343
|
+
packetId,
|
|
344
|
+
baseCommit: sealCommit,
|
|
345
|
+
sealCommit,
|
|
346
|
+
implementationCommits: [head],
|
|
347
|
+
implementationHead: head,
|
|
348
|
+
changedFiles,
|
|
349
|
+
validations: [],
|
|
350
|
+
remainingTransitionDiagnostics: [],
|
|
351
|
+
unexpectedDiagnostics: [],
|
|
352
|
+
recoveryStatus: "not-applicable",
|
|
353
|
+
cleanTrees: true,
|
|
354
|
+
completedBy: steward as ProgramPacketCompletion["completedBy"],
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const completionPath = path.join(completionsDir, `${packetId}.json`);
|
|
358
|
+
writeFileAtomic(completionPath, JSON.stringify(completion, null, 2) + "\n");
|
|
359
|
+
|
|
360
|
+
// Update manifest: packet → completed, program → executing
|
|
361
|
+
const manifestRaw = fs.readFileSync(manifestPath, "utf8");
|
|
362
|
+
const manifestParsed = parseYaml(manifestRaw) as ProgramManifest;
|
|
363
|
+
const idx = manifestParsed.packets.findIndex((p) => p.packetId === packetId);
|
|
364
|
+
if (idx !== -1) {
|
|
365
|
+
manifestParsed.packets[idx].state = "completed";
|
|
366
|
+
manifestParsed.packets[idx].completion = head;
|
|
367
|
+
}
|
|
368
|
+
// Transition program to executing
|
|
369
|
+
manifestParsed.state = "executing";
|
|
370
|
+
// Advance currentPacket
|
|
371
|
+
const nextEntry = manifest.packets.find((p) => p.dependsOnPacket === packetId);
|
|
372
|
+
if (nextEntry) {
|
|
373
|
+
manifestParsed.currentPacket = nextEntry.packetId;
|
|
374
|
+
}
|
|
375
|
+
writeFileAtomic(manifestPath, stringifyYaml(manifestParsed));
|
|
376
|
+
|
|
377
|
+
const filesModified = [
|
|
378
|
+
path.relative(workspaceRoot, completionPath),
|
|
379
|
+
path.relative(workspaceRoot, manifestPath),
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
data: {
|
|
384
|
+
command: "program.packet.complete",
|
|
385
|
+
status: "pass",
|
|
386
|
+
program,
|
|
387
|
+
packetId,
|
|
388
|
+
filesModified,
|
|
389
|
+
violations: [],
|
|
390
|
+
},
|
|
391
|
+
exitCode: 0,
|
|
392
|
+
summary: `program.packet.complete: ${packetId} bootstrap completed — program now executing`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Helpers
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
import { isPathForbidden } from "../discovery.ts";
|
|
401
|
+
|
|
402
|
+
function completeFail(
|
|
403
|
+
program: string,
|
|
404
|
+
packetId: string,
|
|
405
|
+
violations: PacketViolation[],
|
|
406
|
+
): ForgeCommandResult<CompleteResultData> {
|
|
407
|
+
return {
|
|
408
|
+
data: {
|
|
409
|
+
command: "program.packet.complete",
|
|
410
|
+
status: "fail",
|
|
411
|
+
program,
|
|
412
|
+
packetId,
|
|
413
|
+
filesModified: [],
|
|
414
|
+
violations,
|
|
415
|
+
},
|
|
416
|
+
exitCode: 1,
|
|
417
|
+
summary: `program.packet.complete: ${packetId} failed with ${violations.length} violation(s)`,
|
|
418
|
+
};
|
|
419
|
+
}
|