@warpgogol/forge 2.8.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/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/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,433 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Command handler for program.packet.lease — manages the exclusive
|
|
4
|
+
local executor lease for a sealed packet. Supports start, heartbeat, release,
|
|
5
|
+
and recover actions (RFC-0856).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not store raw lease tokens — only the hash is persisted.</item>
|
|
8
|
+
<item>Do not commit — lease files are untracked.</item>
|
|
9
|
+
<item>Do not import from @warpgogol/* — forge must remain dependency-free.</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
<CHANGE_SUMMARY>
|
|
13
|
+
<item>RFC-0856: initial lease command handler.</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
20
|
+
import type {
|
|
21
|
+
ForgeCommandInput,
|
|
22
|
+
ForgeCommandResult,
|
|
23
|
+
ForgeRuntimeContext,
|
|
24
|
+
} from "../../../src/types.ts";
|
|
25
|
+
import { writeFileAtomic } from "../../../src/utils/fs-atomic.ts";
|
|
26
|
+
import {
|
|
27
|
+
discoverProgram,
|
|
28
|
+
findPacketEntry,
|
|
29
|
+
resolvePacketPath,
|
|
30
|
+
parsePacketFile,
|
|
31
|
+
gitHead,
|
|
32
|
+
} from "../discovery.ts";
|
|
33
|
+
import {
|
|
34
|
+
generateLeaseToken,
|
|
35
|
+
hashLeaseToken,
|
|
36
|
+
readLease,
|
|
37
|
+
writeLease,
|
|
38
|
+
deleteLease,
|
|
39
|
+
findActiveLeases,
|
|
40
|
+
isLeaseStale,
|
|
41
|
+
updateHeartbeat,
|
|
42
|
+
} from "../lease.ts";
|
|
43
|
+
import type { ProgramPacketLease } from "../schemas.ts";
|
|
44
|
+
import { validateLeaseStartTransition, type PacketViolation } from "../state.ts";
|
|
45
|
+
import type { ProgramManifest, RecoveryRecord } from "../schemas.ts";
|
|
46
|
+
|
|
47
|
+
export interface LeaseResultData {
|
|
48
|
+
command: string;
|
|
49
|
+
status: "pass" | "fail";
|
|
50
|
+
program: string;
|
|
51
|
+
packetId: string;
|
|
52
|
+
action: string;
|
|
53
|
+
leaseToken?: string;
|
|
54
|
+
violations: PacketViolation[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_TIMEOUT_SECONDS = 3600; // 1 hour
|
|
58
|
+
|
|
59
|
+
export async function runLease(
|
|
60
|
+
input: ForgeCommandInput,
|
|
61
|
+
context: ForgeRuntimeContext,
|
|
62
|
+
): Promise<ForgeCommandResult<LeaseResultData>> {
|
|
63
|
+
const program = input.flags.program as string | undefined;
|
|
64
|
+
const packetId = input.flags.packet as string | undefined;
|
|
65
|
+
const action = input.flags.action as string | undefined;
|
|
66
|
+
|
|
67
|
+
if (!program || !packetId || !action) {
|
|
68
|
+
return leaseFail(program ?? "<missing>", packetId ?? "<missing>", action ?? "<missing>", [
|
|
69
|
+
{ rule: "PROGRAM-PACKET-01", message: "--program, --packet, and --action are required" },
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
switch (action) {
|
|
74
|
+
case "start":
|
|
75
|
+
return leaseStart(input, context, program, packetId);
|
|
76
|
+
case "heartbeat":
|
|
77
|
+
return leaseHeartbeat(context, program, packetId, input);
|
|
78
|
+
case "release":
|
|
79
|
+
return leaseRelease(context, program, packetId, input);
|
|
80
|
+
case "recover":
|
|
81
|
+
return leaseRecover(input, context, program, packetId);
|
|
82
|
+
default:
|
|
83
|
+
return leaseFail(program, packetId, action, [
|
|
84
|
+
{ rule: "PROGRAM-PACKET-01", message: `unknown action: ${action}` },
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// start
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
async function leaseStart(
|
|
94
|
+
input: ForgeCommandInput,
|
|
95
|
+
context: ForgeRuntimeContext,
|
|
96
|
+
program: string,
|
|
97
|
+
packetId: string,
|
|
98
|
+
): Promise<ForgeCommandResult<LeaseResultData>> {
|
|
99
|
+
const executor = input.flags.executor as string | undefined;
|
|
100
|
+
if (!executor) {
|
|
101
|
+
return leaseFail(program, packetId, "start", [
|
|
102
|
+
{ rule: "PROGRAM-PACKET-01", message: "--executor is required for --action=start" },
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const phase = (input.flags.phase as string | undefined) ?? "execution";
|
|
107
|
+
|
|
108
|
+
const { workspaceRoot } = context;
|
|
109
|
+
|
|
110
|
+
// Discover program
|
|
111
|
+
let programLocation;
|
|
112
|
+
try {
|
|
113
|
+
programLocation = discoverProgram(workspaceRoot, program);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return leaseFail(program, packetId, "start", [
|
|
116
|
+
{ rule: "PROGRAM-PACKET-01", message: String((err as Error).message) },
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
if (!programLocation) {
|
|
120
|
+
return leaseFail(program, packetId, "start", [
|
|
121
|
+
{ rule: "PROGRAM-PACKET-01", message: `program ${program} not found` },
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { manifest, programDir } = programLocation;
|
|
126
|
+
const entry = findPacketEntry(manifest, packetId);
|
|
127
|
+
if (!entry) {
|
|
128
|
+
return leaseFail(program, packetId, "start", [
|
|
129
|
+
{ rule: "PROGRAM-PACKET-01", message: `packet ${packetId} not found` },
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let packet;
|
|
134
|
+
try {
|
|
135
|
+
packet = parsePacketFile(resolvePacketPath(programDir, entry));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return leaseFail(program, packetId, "start", [
|
|
138
|
+
{ rule: "PROGRAM-PACKET-01", message: String((err as Error).message) },
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for existing leases
|
|
143
|
+
const activeLeases = findActiveLeases(workspaceRoot, program);
|
|
144
|
+
const hasActiveLease = activeLeases.some(
|
|
145
|
+
(l) => l.packetId === packetId && !isLeaseStale(l.lease),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
let head = "";
|
|
149
|
+
try {
|
|
150
|
+
head = gitHead(workspaceRoot);
|
|
151
|
+
} catch {
|
|
152
|
+
return leaseFail(program, packetId, "start", [
|
|
153
|
+
{ rule: "PROGRAM-PACKET-02", message: "failed to read git HEAD" },
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const violations = validateLeaseStartTransition(manifest, entry, packet, {
|
|
158
|
+
head,
|
|
159
|
+
steward: packet.steward,
|
|
160
|
+
actor: executor as ProgramPacketLease["actor"],
|
|
161
|
+
hasActiveLease,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (violations.length > 0) {
|
|
165
|
+
return leaseFail(program, packetId, "start", violations);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Generate lease
|
|
169
|
+
const rawToken = generateLeaseToken();
|
|
170
|
+
const tokenHash = hashLeaseToken(rawToken);
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
|
|
173
|
+
const lease: ProgramPacketLease = {
|
|
174
|
+
schema: "forge/program-packet-lease@1",
|
|
175
|
+
program,
|
|
176
|
+
packetId,
|
|
177
|
+
phase: phase as ProgramPacketLease["phase"],
|
|
178
|
+
actor: executor as ProgramPacketLease["actor"],
|
|
179
|
+
baseCommit: head,
|
|
180
|
+
sealCommit: entry.sealCommit ?? null,
|
|
181
|
+
tokenHash,
|
|
182
|
+
startedAt: now,
|
|
183
|
+
heartbeatAt: now,
|
|
184
|
+
timeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
writeLease(workspaceRoot, program, lease);
|
|
188
|
+
|
|
189
|
+
// Return raw token ONLY in data.leaseToken — never in logs.
|
|
190
|
+
return {
|
|
191
|
+
data: {
|
|
192
|
+
command: "program.packet.lease",
|
|
193
|
+
status: "pass",
|
|
194
|
+
program,
|
|
195
|
+
packetId,
|
|
196
|
+
action: "start",
|
|
197
|
+
leaseToken: rawToken,
|
|
198
|
+
violations: [],
|
|
199
|
+
},
|
|
200
|
+
exitCode: 0,
|
|
201
|
+
summary: `program.packet.lease: ${packetId} lease started for ${executor}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// heartbeat
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
async function leaseHeartbeat(
|
|
210
|
+
context: ForgeRuntimeContext,
|
|
211
|
+
program: string,
|
|
212
|
+
packetId: string,
|
|
213
|
+
input: ForgeCommandInput,
|
|
214
|
+
): Promise<ForgeCommandResult<LeaseResultData>> {
|
|
215
|
+
const leaseToken = input.flags["lease-token"] as string | undefined;
|
|
216
|
+
if (!leaseToken) {
|
|
217
|
+
return leaseFail(program, packetId, "heartbeat", [
|
|
218
|
+
{ rule: "PROGRAM-PACKET-01", message: "--lease-token is required for --action=heartbeat" },
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { workspaceRoot } = context;
|
|
223
|
+
const existing = readLease(workspaceRoot, program, packetId);
|
|
224
|
+
if (!existing) {
|
|
225
|
+
return leaseFail(program, packetId, "heartbeat", [
|
|
226
|
+
{ rule: "PROGRAM-PACKET-05", message: "no lease found" },
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const tokenHash = hashLeaseToken(leaseToken);
|
|
231
|
+
if (tokenHash !== existing.tokenHash) {
|
|
232
|
+
return leaseFail(program, packetId, "heartbeat", [
|
|
233
|
+
{ rule: "PROGRAM-PACKET-11", message: "token mismatch" },
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
updateHeartbeat(workspaceRoot, program, packetId);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
data: {
|
|
241
|
+
command: "program.packet.lease",
|
|
242
|
+
status: "pass",
|
|
243
|
+
program,
|
|
244
|
+
packetId,
|
|
245
|
+
action: "heartbeat",
|
|
246
|
+
violations: [],
|
|
247
|
+
},
|
|
248
|
+
exitCode: 0,
|
|
249
|
+
summary: `program.packet.lease: ${packetId} heartbeat updated`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// release
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
async function leaseRelease(
|
|
258
|
+
context: ForgeRuntimeContext,
|
|
259
|
+
program: string,
|
|
260
|
+
packetId: string,
|
|
261
|
+
input: ForgeCommandInput,
|
|
262
|
+
): Promise<ForgeCommandResult<LeaseResultData>> {
|
|
263
|
+
const leaseToken = input.flags["lease-token"] as string | undefined;
|
|
264
|
+
if (!leaseToken) {
|
|
265
|
+
return leaseFail(program, packetId, "release", [
|
|
266
|
+
{ rule: "PROGRAM-PACKET-01", message: "--lease-token is required for --action=release" },
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const { workspaceRoot } = context;
|
|
271
|
+
const existing = readLease(workspaceRoot, program, packetId);
|
|
272
|
+
if (!existing) {
|
|
273
|
+
return leaseFail(program, packetId, "release", [
|
|
274
|
+
{ rule: "PROGRAM-PACKET-05", message: "no lease found" },
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const tokenHash = hashLeaseToken(leaseToken);
|
|
279
|
+
if (tokenHash !== existing.tokenHash) {
|
|
280
|
+
return leaseFail(program, packetId, "release", [
|
|
281
|
+
{ rule: "PROGRAM-PACKET-11", message: "token mismatch" },
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
deleteLease(workspaceRoot, program, packetId);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
data: {
|
|
289
|
+
command: "program.packet.lease",
|
|
290
|
+
status: "pass",
|
|
291
|
+
program,
|
|
292
|
+
packetId,
|
|
293
|
+
action: "release",
|
|
294
|
+
violations: [],
|
|
295
|
+
},
|
|
296
|
+
exitCode: 0,
|
|
297
|
+
summary: `program.packet.lease: ${packetId} lease released`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// recover
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
async function leaseRecover(
|
|
306
|
+
input: ForgeCommandInput,
|
|
307
|
+
context: ForgeRuntimeContext,
|
|
308
|
+
program: string,
|
|
309
|
+
packetId: string,
|
|
310
|
+
): Promise<ForgeCommandResult<LeaseResultData>> {
|
|
311
|
+
const steward = input.flags.steward as string | undefined;
|
|
312
|
+
const reason = input.flags.reason as string | undefined;
|
|
313
|
+
if (!steward || !reason) {
|
|
314
|
+
return leaseFail(program, packetId, "recover", [
|
|
315
|
+
{
|
|
316
|
+
rule: "PROGRAM-PACKET-01",
|
|
317
|
+
message: "--steward and --reason are required for --action=recover",
|
|
318
|
+
},
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const { workspaceRoot } = context;
|
|
323
|
+
const existing = readLease(workspaceRoot, program, packetId);
|
|
324
|
+
if (!existing) {
|
|
325
|
+
return leaseFail(program, packetId, "recover", [
|
|
326
|
+
{ rule: "PROGRAM-PACKET-05", message: "no lease to recover" },
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Only recover stale leases
|
|
331
|
+
if (!isLeaseStale(existing)) {
|
|
332
|
+
return leaseFail(program, packetId, "recover", [
|
|
333
|
+
{ rule: "PROGRAM-PACKET-05", message: "lease is not stale — cannot recover active lease" },
|
|
334
|
+
]);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Write recovery record
|
|
338
|
+
const recoveryDir = path.join(
|
|
339
|
+
workspaceRoot,
|
|
340
|
+
"docs",
|
|
341
|
+
"plans",
|
|
342
|
+
programLocationDir(workspaceRoot, program),
|
|
343
|
+
"recoveries",
|
|
344
|
+
);
|
|
345
|
+
fs.mkdirSync(recoveryDir, { recursive: true });
|
|
346
|
+
|
|
347
|
+
const head = gitHead(workspaceRoot);
|
|
348
|
+
const recovery: RecoveryRecord = {
|
|
349
|
+
schema: "forge/program-packet-recovery@1",
|
|
350
|
+
program,
|
|
351
|
+
packetId,
|
|
352
|
+
previousLeaseDigest: existing.tokenHash,
|
|
353
|
+
reason,
|
|
354
|
+
actor: steward as RecoveryRecord["actor"],
|
|
355
|
+
observedHead: head,
|
|
356
|
+
target: "blocked",
|
|
357
|
+
recoveredAt: new Date().toISOString(),
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const recoveryPath = path.join(recoveryDir, `${packetId}-${Date.now()}.json`);
|
|
361
|
+
writeFileAtomic(recoveryPath, JSON.stringify(recovery, null, 2) + "\n");
|
|
362
|
+
|
|
363
|
+
// Delete the stale lease
|
|
364
|
+
deleteLease(workspaceRoot, program, packetId);
|
|
365
|
+
|
|
366
|
+
// Update manifest: packet state → blocked
|
|
367
|
+
const manifestPath = path.join(
|
|
368
|
+
workspaceRoot,
|
|
369
|
+
"docs",
|
|
370
|
+
"plans",
|
|
371
|
+
programLocationDir(workspaceRoot, program),
|
|
372
|
+
"program.yaml",
|
|
373
|
+
);
|
|
374
|
+
const manifestRaw = fs.readFileSync(manifestPath, "utf8");
|
|
375
|
+
const manifestParsed = parseYaml(manifestRaw) as ProgramManifest;
|
|
376
|
+
const idx = manifestParsed.packets.findIndex((p) => p.packetId === packetId);
|
|
377
|
+
if (idx !== -1) {
|
|
378
|
+
manifestParsed.packets[idx].state = "blocked";
|
|
379
|
+
writeFileAtomic(manifestPath, stringifyYaml(manifestParsed));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
data: {
|
|
384
|
+
command: "program.packet.lease",
|
|
385
|
+
status: "pass",
|
|
386
|
+
program,
|
|
387
|
+
packetId,
|
|
388
|
+
action: "recover",
|
|
389
|
+
violations: [],
|
|
390
|
+
},
|
|
391
|
+
exitCode: 0,
|
|
392
|
+
summary: `program.packet.lease: ${packetId} recovered (stale lease cleared, recovery record written)`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Helpers
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
function programLocationDir(workspaceRoot: string, program: string): string {
|
|
401
|
+
// Find the directory name under docs/plans/ that contains this program
|
|
402
|
+
const plansDir = path.join(workspaceRoot, "docs", "plans");
|
|
403
|
+
if (!fs.existsSync(plansDir)) return program;
|
|
404
|
+
for (const entry of fs.readdirSync(plansDir, { withFileTypes: true })) {
|
|
405
|
+
if (!entry.isDirectory()) continue;
|
|
406
|
+
const manifestPath = path.join(plansDir, entry.name, "program.yaml");
|
|
407
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
408
|
+
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
409
|
+
const parsed = parseYaml(raw) as Record<string, unknown>;
|
|
410
|
+
if (parsed.program === program) return entry.name;
|
|
411
|
+
}
|
|
412
|
+
return program;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function leaseFail(
|
|
416
|
+
program: string,
|
|
417
|
+
packetId: string,
|
|
418
|
+
action: string,
|
|
419
|
+
violations: PacketViolation[],
|
|
420
|
+
): ForgeCommandResult<LeaseResultData> {
|
|
421
|
+
return {
|
|
422
|
+
data: {
|
|
423
|
+
command: "program.packet.lease",
|
|
424
|
+
status: "fail",
|
|
425
|
+
program,
|
|
426
|
+
packetId,
|
|
427
|
+
action,
|
|
428
|
+
violations,
|
|
429
|
+
},
|
|
430
|
+
exitCode: 1,
|
|
431
|
+
summary: `program.packet.lease: ${packetId} ${action} failed with ${violations.length} violation(s)`,
|
|
432
|
+
};
|
|
433
|
+
}
|