@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,168 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Werkstatt lock helpers. Moved from @warpgogol/site-kernel-handoff to
|
|
4
|
+
@warpgogol/forge for full autonomous mode (RFC-0556). Provides acquire, release,
|
|
5
|
+
heartbeat, status, and stale-lock removal.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not implement distributed locks — local process-level only.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0362: initial lock helpers (acquire, release, heartbeat, status).</item>
|
|
12
|
+
<item>RFC-0556: moved from @warpgogol/site-kernel-handoff to @warpgogol/forge for autonomous mode.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { werkstattLockSchema, type WerkstattLock } from "./schema.ts";
|
|
20
|
+
|
|
21
|
+
const LOCKS_DIR = path.join(".werkstatt", "locks");
|
|
22
|
+
const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
23
|
+
|
|
24
|
+
function resolveLocksDir(workspaceRoot: string): string {
|
|
25
|
+
return path.join(workspaceRoot, LOCKS_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveLockPath(workspaceRoot: string, scope: string): string {
|
|
29
|
+
const safeScope = scope.replace(/[^a-zA-Z0-9:_-]/g, "_");
|
|
30
|
+
return path.join(resolveLocksDir(workspaceRoot), `${safeScope}.lock.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isProcessAlive(pid: number): boolean {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isLockStale(lock: WerkstattLock, now: Date = new Date()): boolean {
|
|
43
|
+
if (!isProcessAlive(lock.pid)) return true;
|
|
44
|
+
const heartbeatAge = (now.getTime() - new Date(lock.heartbeatAt).getTime()) / 1000;
|
|
45
|
+
return heartbeatAge >= lock.timeoutSeconds;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function acquireLock(
|
|
49
|
+
workspaceRoot: string,
|
|
50
|
+
scope: string,
|
|
51
|
+
operationId: string,
|
|
52
|
+
command: string,
|
|
53
|
+
owner: string,
|
|
54
|
+
timeoutSeconds: number = DEFAULT_TIMEOUT_SECONDS,
|
|
55
|
+
): Promise<WerkstattLock> {
|
|
56
|
+
const lockPath = resolveLockPath(workspaceRoot, scope);
|
|
57
|
+
const locksDir = resolveLocksDir(workspaceRoot);
|
|
58
|
+
|
|
59
|
+
if (!existsSync(locksDir)) {
|
|
60
|
+
await fs.mkdir(locksDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (existsSync(lockPath)) {
|
|
64
|
+
const raw = await fs.readFile(lockPath, "utf8");
|
|
65
|
+
try {
|
|
66
|
+
const existing = werkstattLockSchema.parse(JSON.parse(raw));
|
|
67
|
+
if (existing.pid === process.pid && !isLockStale(existing)) {
|
|
68
|
+
const updated = {
|
|
69
|
+
...existing,
|
|
70
|
+
depth: (existing.depth ?? 1) + 1,
|
|
71
|
+
heartbeatAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
werkstattLockSchema.parse(updated);
|
|
74
|
+
await fs.writeFile(lockPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
75
|
+
return updated;
|
|
76
|
+
}
|
|
77
|
+
if (!isLockStale(existing)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[werkstatt.lock] lock '${scope}' held by operation '${existing.operationId}' (pid: ${existing.pid})`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof Error && err.message.includes("held by")) throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
const lock: WerkstattLock = {
|
|
89
|
+
schemaVersion: "1.0.0",
|
|
90
|
+
scope,
|
|
91
|
+
operationId,
|
|
92
|
+
command,
|
|
93
|
+
owner,
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
startedAt: now,
|
|
96
|
+
heartbeatAt: now,
|
|
97
|
+
timeoutSeconds,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
werkstattLockSchema.parse(lock);
|
|
101
|
+
await fs.writeFile(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
102
|
+
return lock;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function releaseLock(workspaceRoot: string, scope: string): Promise<void> {
|
|
106
|
+
const lockPath = resolveLockPath(workspaceRoot, scope);
|
|
107
|
+
if (!existsSync(lockPath)) return;
|
|
108
|
+
try {
|
|
109
|
+
const raw = await fs.readFile(lockPath, "utf8");
|
|
110
|
+
const lock = werkstattLockSchema.parse(JSON.parse(raw));
|
|
111
|
+
if (lock.pid === process.pid && (lock.depth ?? 1) > 1) {
|
|
112
|
+
const decremented = { ...lock, depth: (lock.depth ?? 1) - 1 };
|
|
113
|
+
werkstattLockSchema.parse(decremented);
|
|
114
|
+
await fs.writeFile(lockPath, JSON.stringify(decremented, null, 2) + "\n", "utf8");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await fs.unlink(lockPath);
|
|
118
|
+
} catch {
|
|
119
|
+
// corrupt or missing — try direct delete
|
|
120
|
+
try {
|
|
121
|
+
await fs.unlink(lockPath);
|
|
122
|
+
} catch {
|
|
123
|
+
/* already gone */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function heartbeatLock(workspaceRoot: string, scope: string): Promise<void> {
|
|
129
|
+
const lockPath = resolveLockPath(workspaceRoot, scope);
|
|
130
|
+
if (!existsSync(lockPath)) return;
|
|
131
|
+
const raw = await fs.readFile(lockPath, "utf8");
|
|
132
|
+
const lock = werkstattLockSchema.parse(JSON.parse(raw));
|
|
133
|
+
lock.heartbeatAt = new Date().toISOString();
|
|
134
|
+
await fs.writeFile(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function readAllLocks(
|
|
138
|
+
workspaceRoot: string,
|
|
139
|
+
): Promise<Array<WerkstattLock & { stale: boolean }>> {
|
|
140
|
+
const locksDir = resolveLocksDir(workspaceRoot);
|
|
141
|
+
if (!existsSync(locksDir)) return [];
|
|
142
|
+
|
|
143
|
+
const files = await fs.readdir(locksDir);
|
|
144
|
+
const locks: Array<WerkstattLock & { stale: boolean }> = [];
|
|
145
|
+
|
|
146
|
+
for (const file of files) {
|
|
147
|
+
if (!file.endsWith(".lock.json")) continue;
|
|
148
|
+
const raw = await fs.readFile(path.join(locksDir, file), "utf8");
|
|
149
|
+
try {
|
|
150
|
+
const lock = werkstattLockSchema.parse(JSON.parse(raw));
|
|
151
|
+
locks.push({ ...lock, stale: isLockStale(lock) });
|
|
152
|
+
} catch {
|
|
153
|
+
// skip corrupt lock files
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return locks;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function removeStaleLock(workspaceRoot: string, scope: string): Promise<boolean> {
|
|
161
|
+
const lockPath = resolveLockPath(workspaceRoot, scope);
|
|
162
|
+
if (!existsSync(lockPath)) return false;
|
|
163
|
+
const raw = await fs.readFile(lockPath, "utf8");
|
|
164
|
+
const lock = werkstattLockSchema.parse(JSON.parse(raw));
|
|
165
|
+
if (!isLockStale(lock)) return false;
|
|
166
|
+
await fs.unlink(lockPath);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Werkstatt Zod schemas inlined from @warpgogol/werkstatt-shared/ontology/operations for
|
|
4
|
+
full autonomous mode (RFC-0556). Provides werkstattLockSchema and
|
|
5
|
+
werkstattOperationRecordSchema without a dependency on @warpgogol/ontology.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not perform file IO — pure shape only.</item>
|
|
8
|
+
<item>Do not define hash-chain Bordbuch format — that is RFC-0276/RFC-0355.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>RFC-0362: initial Werkstatt schemas (WerkstattLock, WerkstattOperationRecord).</item>
|
|
13
|
+
<item>RFC-0556: inlined from @warpgogol/werkstatt-shared/ontology/operations into forge for autonomous mode.</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
export const werkstattLockSchema = z.object({
|
|
20
|
+
schemaVersion: z.literal("1.0.0"),
|
|
21
|
+
scope: z.string().min(1),
|
|
22
|
+
operationId: z.string().min(1),
|
|
23
|
+
command: z.string().min(1),
|
|
24
|
+
owner: z.string().min(1),
|
|
25
|
+
pid: z.number().int(),
|
|
26
|
+
startedAt: z.string().datetime(),
|
|
27
|
+
heartbeatAt: z.string().datetime(),
|
|
28
|
+
timeoutSeconds: z.number().positive(),
|
|
29
|
+
depth: z.number().int().positive().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const werkstattOperationRecordSchema = z.object({
|
|
33
|
+
schemaVersion: z.literal("1.0.0"),
|
|
34
|
+
operationId: z.string().min(1),
|
|
35
|
+
command: z.string().min(1),
|
|
36
|
+
scopes: z.array(z.string().min(1)),
|
|
37
|
+
state: z.enum(["started", "completed", "failed"]),
|
|
38
|
+
startedAt: z.string().datetime(),
|
|
39
|
+
completedAt: z.string().datetime().nullable(),
|
|
40
|
+
inputHash: z.string().min(1),
|
|
41
|
+
resultHash: z.string().nullable(),
|
|
42
|
+
artifacts: z.array(z.string()),
|
|
43
|
+
error: z.string().nullable(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type WerkstattLock = z.infer<typeof werkstattLockSchema>;
|
|
47
|
+
export type WerkstattOperationRecord = z.infer<typeof werkstattOperationRecordSchema>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>werkstatt.lock.recover command handler. Moved from
|
|
4
|
+
@warpgogol/site-kernel-handoff to @warpgogol/forge for full autonomous mode (RFC-0556).
|
|
5
|
+
Classifies and cleans stale locks and staging artifacts (RFC-0362 §8).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not introduce app-specific runtime composition or deployment behavior.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0362: initial lock.recover command handler.</item>
|
|
12
|
+
<item>RFC-0556: moved from @warpgogol/site-kernel-handoff to @warpgogol/forge for autonomous mode.</item>
|
|
13
|
+
<item>Replace fs.unlink/fs.rm with trashPath for artifact cleanup (trash bin for LLM-initiated deletions).</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs/promises";
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import type {
|
|
21
|
+
ForgeCommandInput,
|
|
22
|
+
ForgeCommandResult,
|
|
23
|
+
ForgeRuntimeContext,
|
|
24
|
+
} from "../../../src/types.ts";
|
|
25
|
+
import { trashPath } from "../../../src/utils/fs-trash.ts";
|
|
26
|
+
import { readAllLocks, removeStaleLock, acquireLock, releaseLock } from "./lock.ts";
|
|
27
|
+
|
|
28
|
+
export interface WerkstattLockRecoverData {
|
|
29
|
+
recovered: Array<{ scope?: string; artifact?: string; action: string }>;
|
|
30
|
+
failed: Array<{ scope?: string; artifact?: string; error: string }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function flagString(input: ForgeCommandInput, key: string): string | undefined {
|
|
34
|
+
const v = input.flags[key];
|
|
35
|
+
return typeof v === "string" ? v : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function classifyArtifacts(
|
|
39
|
+
workspaceRoot: string,
|
|
40
|
+
purge: boolean,
|
|
41
|
+
): Promise<{
|
|
42
|
+
recovered: Array<{ artifact: string; action: string }>;
|
|
43
|
+
failed: Array<{ artifact: string; error: string }>;
|
|
44
|
+
}> {
|
|
45
|
+
const recovered: Array<{ artifact: string; action: string }> = [];
|
|
46
|
+
const failed: Array<{ artifact: string; error: string }> = [];
|
|
47
|
+
|
|
48
|
+
const scanRoots = ["systems", "missions", "releases"];
|
|
49
|
+
for (const root of scanRoots) {
|
|
50
|
+
const rootPath = path.join(workspaceRoot, root);
|
|
51
|
+
if (!existsSync(rootPath)) continue;
|
|
52
|
+
|
|
53
|
+
let entries: string[];
|
|
54
|
+
try {
|
|
55
|
+
entries = await fs.readdir(rootPath);
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const entryPath = path.join(rootPath, entry);
|
|
62
|
+
const relPath = path.relative(workspaceRoot, entryPath).replace(/\\/g, "/");
|
|
63
|
+
|
|
64
|
+
if (entry.endsWith(".tmp")) {
|
|
65
|
+
if (purge) {
|
|
66
|
+
try {
|
|
67
|
+
await trashPath(entryPath);
|
|
68
|
+
recovered.push({ artifact: relPath, action: "trashed-tmp" });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
failed.push({ artifact: relPath, error: (err as Error).message });
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
recovered.push({ artifact: relPath, action: "classified-tmp (use --purge to remove)" });
|
|
74
|
+
}
|
|
75
|
+
} else if (entry.includes(".staging-")) {
|
|
76
|
+
if (purge) {
|
|
77
|
+
try {
|
|
78
|
+
await trashPath(entryPath);
|
|
79
|
+
recovered.push({ artifact: relPath, action: "trashed-staging" });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
failed.push({ artifact: relPath, error: (err as Error).message });
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
recovered.push({
|
|
85
|
+
artifact: relPath,
|
|
86
|
+
action: "classified-staging (use --purge to remove)",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} else if (entry.includes(".incomplete")) {
|
|
90
|
+
if (purge) {
|
|
91
|
+
const failedDir = entryPath.replace(".incomplete", `.failed-${Date.now()}`);
|
|
92
|
+
try {
|
|
93
|
+
await fs.rename(entryPath, failedDir);
|
|
94
|
+
recovered.push({ artifact: relPath, action: "renamed-to-failed" });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
failed.push({ artifact: relPath, error: (err as Error).message });
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
recovered.push({
|
|
100
|
+
artifact: relPath,
|
|
101
|
+
action: "classified-incomplete (use --purge to rename)",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { recovered, failed };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runWerkstattLockRecover(
|
|
112
|
+
input: ForgeCommandInput,
|
|
113
|
+
context: ForgeRuntimeContext,
|
|
114
|
+
): Promise<ForgeCommandResult<WerkstattLockRecoverData>> {
|
|
115
|
+
const { workspaceRoot, logger } = context;
|
|
116
|
+
const scopeFilter = flagString(input, "scope");
|
|
117
|
+
const purge = input.flags["purge"] === true;
|
|
118
|
+
|
|
119
|
+
const metaOperationId = `recover-${Date.now()}`;
|
|
120
|
+
try {
|
|
121
|
+
await acquireLock(
|
|
122
|
+
workspaceRoot,
|
|
123
|
+
"werkstatt-recovery",
|
|
124
|
+
metaOperationId,
|
|
125
|
+
"werkstatt.lock.recover",
|
|
126
|
+
"system",
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error("[werkstatt.lock.recover] another recovery operation is in progress");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const locks = await readAllLocks(workspaceRoot);
|
|
134
|
+
const staleLocks = locks.filter((l) => l.stale && (!scopeFilter || l.scope === scopeFilter));
|
|
135
|
+
|
|
136
|
+
const recovered: Array<{ scope?: string; artifact?: string; action: string }> = [];
|
|
137
|
+
const failed: Array<{ scope?: string; artifact?: string; error: string }> = [];
|
|
138
|
+
|
|
139
|
+
for (const lock of staleLocks) {
|
|
140
|
+
try {
|
|
141
|
+
await removeStaleLock(workspaceRoot, lock.scope);
|
|
142
|
+
recovered.push({ scope: lock.scope, action: "removed-stale-lock" });
|
|
143
|
+
logger.info(` [removed] lock '${lock.scope}'`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
failed.push({ scope: lock.scope, error: (err as Error).message });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { recovered: artifactRecovered, failed: artifactFailed } = await classifyArtifacts(
|
|
150
|
+
workspaceRoot,
|
|
151
|
+
purge,
|
|
152
|
+
);
|
|
153
|
+
for (const r of artifactRecovered) {
|
|
154
|
+
recovered.push({ artifact: r.artifact, action: r.action });
|
|
155
|
+
}
|
|
156
|
+
for (const f of artifactFailed) {
|
|
157
|
+
failed.push({ artifact: f.artifact, error: f.error });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (recovered.length === 0 && failed.length === 0) {
|
|
161
|
+
logger.success("[werkstatt.lock.recover] no stale locks or artifacts found");
|
|
162
|
+
} else {
|
|
163
|
+
logger.success(
|
|
164
|
+
`[werkstatt.lock.recover] ${recovered.length} recovered, ${failed.length} failed`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
data: { recovered, failed },
|
|
170
|
+
summary: `[werkstatt.lock.recover] ${recovered.length} artifact${recovered.length === 1 ? "" : "s"} recovered, ${failed.length} failure${failed.length === 1 ? "" : "s"}`,
|
|
171
|
+
};
|
|
172
|
+
} finally {
|
|
173
|
+
await releaseLock(workspaceRoot, "werkstatt-recovery");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>werkstatt.lock.status command handler. Moved from
|
|
4
|
+
@warpgogol/site-kernel-handoff to @warpgogol/forge for full autonomous mode (RFC-0556).
|
|
5
|
+
Reports all Werkstatt locks, their age, owner, and staleness (RFC-0362).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not introduce app-specific runtime composition or deployment behavior.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0362: initial lock.status command handler.</item>
|
|
12
|
+
<item>RFC-0556: moved from @warpgogol/site-kernel-handoff to @warpgogol/forge for autonomous mode.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ForgeCommandInput,
|
|
18
|
+
ForgeCommandResult,
|
|
19
|
+
ForgeRuntimeContext,
|
|
20
|
+
} from "../../../src/types.ts";
|
|
21
|
+
import { readAllLocks } from "./lock.ts";
|
|
22
|
+
|
|
23
|
+
export interface WerkstattLockStatusData {
|
|
24
|
+
locks: Array<{
|
|
25
|
+
scope: string;
|
|
26
|
+
operationId: string;
|
|
27
|
+
command: string;
|
|
28
|
+
owner: string;
|
|
29
|
+
pid: number;
|
|
30
|
+
startedAt: string;
|
|
31
|
+
heartbeatAt: string;
|
|
32
|
+
timeoutSeconds: number;
|
|
33
|
+
stale: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
count: number;
|
|
36
|
+
staleCount: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runWerkstattLockStatus(
|
|
40
|
+
_input: ForgeCommandInput,
|
|
41
|
+
context: ForgeRuntimeContext,
|
|
42
|
+
): Promise<ForgeCommandResult<WerkstattLockStatusData>> {
|
|
43
|
+
const { workspaceRoot, logger } = context;
|
|
44
|
+
const locks = await readAllLocks(workspaceRoot);
|
|
45
|
+
|
|
46
|
+
const staleCount = locks.filter((l) => l.stale).length;
|
|
47
|
+
|
|
48
|
+
for (const lock of locks) {
|
|
49
|
+
const status = lock.stale ? "STALE" : "active";
|
|
50
|
+
logger.info(
|
|
51
|
+
` ${lock.scope.padEnd(32)} ${status.padEnd(6)} pid:${lock.pid} op:${lock.operationId}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
data: {
|
|
57
|
+
locks: locks.map((l) => ({
|
|
58
|
+
scope: l.scope,
|
|
59
|
+
operationId: l.operationId,
|
|
60
|
+
command: l.command,
|
|
61
|
+
owner: l.owner,
|
|
62
|
+
pid: l.pid,
|
|
63
|
+
startedAt: l.startedAt,
|
|
64
|
+
heartbeatAt: l.heartbeatAt,
|
|
65
|
+
timeoutSeconds: l.timeoutSeconds,
|
|
66
|
+
stale: l.stale,
|
|
67
|
+
})),
|
|
68
|
+
count: locks.length,
|
|
69
|
+
staleCount,
|
|
70
|
+
},
|
|
71
|
+
summary: `[werkstatt.lock.status] ${locks.length} lock${locks.length === 1 ? "" : "s"}, ${staleCount} stale`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>werkstatt.operation.validate command handler. Moved from
|
|
4
|
+
@warpgogol/site-kernel-checks to @warpgogol/forge for full autonomous mode (RFC-0556).
|
|
5
|
+
Scans command source files for direct shared-state writes outside the allowlisted
|
|
6
|
+
helper module (RFC-0362 §6).</purpose>
|
|
7
|
+
<non-goals>
|
|
8
|
+
<item>Does not load command implementations — reads source files with regex only.</item>
|
|
9
|
+
<item>Does not scan files outside packages/os/site-kernel-handoff/src/.</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
<CHANGE_SUMMARY>
|
|
13
|
+
<item>RFC-0362: initial operation.validate command handler.</item>
|
|
14
|
+
<item>RFC-0556: moved from @warpgogol/site-kernel-checks to @warpgogol/forge for autonomous mode. Uses node:fs/promises directly instead of context.io.</item>
|
|
15
|
+
</CHANGE_SUMMARY>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { basename, join, relative } from "node:path";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { collectFiles } from "../../../src/utils/fs.ts";
|
|
21
|
+
import type {
|
|
22
|
+
ForgeCommandInput,
|
|
23
|
+
ForgeCommandResult,
|
|
24
|
+
ForgeRuntimeContext,
|
|
25
|
+
} from "../../../src/types.ts";
|
|
26
|
+
|
|
27
|
+
const SCAN_DIR = "packages/os/site-kernel-handoff/src";
|
|
28
|
+
const ALLOWLIST_DIR = "packages/os/site-kernel-handoff/src/werkstatt";
|
|
29
|
+
|
|
30
|
+
const DIRECT_WRITE_PATTERNS: Array<{ regex: RegExp; label: string }> = [
|
|
31
|
+
{ regex: /\bwriteFile\s*\(/g, label: "writeFile" },
|
|
32
|
+
{ regex: /\bappendFile\s*\(/g, label: "appendFile" },
|
|
33
|
+
{ regex: /\brename\s*\(/g, label: "rename" },
|
|
34
|
+
{ regex: /\bcp\s*\(/g, label: "cp" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export interface WerkstattOperationValidateData {
|
|
38
|
+
scannedFiles: number;
|
|
39
|
+
violations: Array<{ file: string; line: number; pattern: string }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runWerkstattOperationValidate(
|
|
43
|
+
_input: ForgeCommandInput,
|
|
44
|
+
context: ForgeRuntimeContext,
|
|
45
|
+
): Promise<ForgeCommandResult<WerkstattOperationValidateData>> {
|
|
46
|
+
const { workspaceRoot, logger } = context;
|
|
47
|
+
const scanPath = join(workspaceRoot, SCAN_DIR);
|
|
48
|
+
const allowlistPath = join(workspaceRoot, ALLOWLIST_DIR);
|
|
49
|
+
|
|
50
|
+
const files = await collectFiles(scanPath, {
|
|
51
|
+
ignore: (name) =>
|
|
52
|
+
name.startsWith(".") || name === "node_modules" || name === "dist" || name === "tests",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const violations: Array<{ file: string; line: number; pattern: string }> = [];
|
|
56
|
+
|
|
57
|
+
const PRE_EXISTING_ALLOWLIST = new Set(["handoff-pack.ts", "materialize.ts", "bundle-io.ts"]);
|
|
58
|
+
|
|
59
|
+
for (const filePath of files) {
|
|
60
|
+
if (filePath.startsWith(allowlistPath) || filePath.includes("werkstatt")) continue;
|
|
61
|
+
|
|
62
|
+
const fileName = basename(filePath);
|
|
63
|
+
if (fileName.endsWith(".test.ts") || fileName.endsWith(".spec.ts")) continue;
|
|
64
|
+
if (PRE_EXISTING_ALLOWLIST.has(fileName)) continue;
|
|
65
|
+
|
|
66
|
+
const relPath = relative(workspaceRoot, filePath).replace(/\\/g, "/");
|
|
67
|
+
|
|
68
|
+
let content: string;
|
|
69
|
+
try {
|
|
70
|
+
content = await readFile(filePath, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lines = content.split("\n");
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
80
|
+
|
|
81
|
+
for (const { regex, label } of DIRECT_WRITE_PATTERNS) {
|
|
82
|
+
const testRegex = new RegExp(regex.source, regex.flags);
|
|
83
|
+
if (testRegex.test(line)) {
|
|
84
|
+
violations.push({ file: relPath, line: i + 1, pattern: label });
|
|
85
|
+
logger.error(
|
|
86
|
+
`${relPath}:${i + 1}: direct ${label} — use the shared atomic-write helper in packages/os/site-kernel-handoff/src/werkstatt/`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
data: {
|
|
95
|
+
scannedFiles: files.length,
|
|
96
|
+
violations,
|
|
97
|
+
},
|
|
98
|
+
exitCode: violations.length > 0 ? 1 : 0,
|
|
99
|
+
summary:
|
|
100
|
+
violations.length > 0
|
|
101
|
+
? undefined
|
|
102
|
+
: `[werkstatt.operation.validate] ${files.length} files scanned, 0 violations`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Expose forge Werkstatt kernel commands through a stable package subpath.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement werkstatt handler logic.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0374: initial werkstatt module barrel.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { forgeWerkstattModule } from "./werkstatt.module.ts";
|
|
14
|
+
export {
|
|
15
|
+
werkstattLockSchema,
|
|
16
|
+
werkstattOperationRecordSchema,
|
|
17
|
+
type WerkstattLock,
|
|
18
|
+
type WerkstattOperationRecord,
|
|
19
|
+
} from "./handlers/schema.ts";
|
|
20
|
+
export {
|
|
21
|
+
acquireLock,
|
|
22
|
+
releaseLock,
|
|
23
|
+
heartbeatLock,
|
|
24
|
+
readAllLocks,
|
|
25
|
+
isLockStale,
|
|
26
|
+
removeStaleLock,
|
|
27
|
+
} from "./handlers/lock.ts";
|
|
28
|
+
export {
|
|
29
|
+
runWerkstattLockStatus,
|
|
30
|
+
type WerkstattLockStatusData,
|
|
31
|
+
} from "./handlers/werkstatt-lock-status.ts";
|
|
32
|
+
export {
|
|
33
|
+
runWerkstattLockRecover,
|
|
34
|
+
type WerkstattLockRecoverData,
|
|
35
|
+
} from "./handlers/werkstatt-lock-recover.ts";
|
|
36
|
+
export {
|
|
37
|
+
runWerkstattOperationValidate,
|
|
38
|
+
type WerkstattOperationValidateData,
|
|
39
|
+
} from "./handlers/werkstatt-operation-validate.ts";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Register forge Werkstatt lock and operation validation commands with the kernel registry.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement werkstatt handler logic — delegate to handlers/ inlined by RFC-0556.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0374: initial forgeWerkstattModule registering 3 werkstatt commands.</item>
|
|
10
|
+
<item>RFC-0556: removed dynamic imports of @warpgogol/site-kernel-handoff and @warpgogol/site-kernel-checks, all handlers now inlined in forge/os/werkstatt/handlers/.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ForgeModule } from "../../src/forge-module.ts";
|
|
15
|
+
import { runWerkstattLockStatus } from "./handlers/werkstatt-lock-status.ts";
|
|
16
|
+
import { runWerkstattLockRecover } from "./handlers/werkstatt-lock-recover.ts";
|
|
17
|
+
import { runWerkstattOperationValidate } from "./handlers/werkstatt-operation-validate.ts";
|
|
18
|
+
|
|
19
|
+
export const forgeWerkstattModule: ForgeModule = {
|
|
20
|
+
name: "forge-werkstatt",
|
|
21
|
+
version: "0.1.0",
|
|
22
|
+
async register(registry) {
|
|
23
|
+
registry.registerCommand({
|
|
24
|
+
name: "werkstatt.lock.status",
|
|
25
|
+
description: "Report all Werkstatt locks, their age, owner, and staleness (RFC-0362).",
|
|
26
|
+
scope: "workspace",
|
|
27
|
+
supportsAllSites: false,
|
|
28
|
+
flags: {},
|
|
29
|
+
reads: [".werkstatt/locks/**"],
|
|
30
|
+
execute: runWerkstattLockStatus,
|
|
31
|
+
});
|
|
32
|
+
registry.registerCommand({
|
|
33
|
+
name: "werkstatt.lock.recover",
|
|
34
|
+
description:
|
|
35
|
+
"Classify and clean stale locks and staging artifacts (RFC-0362). Flags: --scope, --purge.",
|
|
36
|
+
scope: "workspace",
|
|
37
|
+
supportsAllSites: false,
|
|
38
|
+
mutatesState: true,
|
|
39
|
+
flags: {
|
|
40
|
+
scope: { kind: "string", description: "Recover only a single lock scope." },
|
|
41
|
+
purge: {
|
|
42
|
+
kind: "boolean",
|
|
43
|
+
description: "Remove stale artifacts instead of classifying them.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
writes: [".werkstatt/locks/**", "systems/**", "missions/**", "releases/**"],
|
|
47
|
+
reads: [".werkstatt/locks/**"],
|
|
48
|
+
cacheable: false,
|
|
49
|
+
execute: runWerkstattLockRecover,
|
|
50
|
+
});
|
|
51
|
+
registry.registerCommand({
|
|
52
|
+
name: "werkstatt.operation.validate",
|
|
53
|
+
description:
|
|
54
|
+
"Validate that mutating Werkstatt commands use shared lock/idempotency/atomic-write helpers (RFC-0362).",
|
|
55
|
+
scope: "workspace",
|
|
56
|
+
supportsAllSites: false,
|
|
57
|
+
flags: {},
|
|
58
|
+
reads: ["packages/os/site-kernel-handoff/src/**/*.ts"],
|
|
59
|
+
execute: runWerkstattOperationValidate,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|