@moretea-labs/forge 1.7.0 → 1.7.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.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +85 -89
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +1 -1
  166. package/src/runtime/root/package-runtime-service.ts +33 -2
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -1,505 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, readdirSync, readFileSync } from "fs";
3
- import { relative, resolve } from "path";
4
- import { spawnSync } from "child_process";
2
+ import { runTrackedForgeHelper } from "./lib/run-forge-helper";
5
3
 
6
- type ContractFiles = {
7
- agents: string;
8
- claude: string;
9
- };
10
-
11
- type Capability = {
12
- id: string;
13
- domain: string;
14
- name: string;
15
- prefixes: string[];
16
- contract_files: ContractFiles;
17
- architecture_module: string;
18
- workstream_dir: string;
19
- lsp_profile: string;
20
- verification_hints: string[];
21
- };
22
-
23
- type CapabilityRegistry = {
24
- version: number;
25
- capabilities: Capability[];
26
- };
27
-
28
- type Format = "json" | "text" | "prefixes";
29
-
30
- type Args = {
31
- command: string;
32
- repo: string;
33
- path: string;
34
- pathsFrom: string;
35
- format: Format;
36
- };
37
-
38
- const DEFAULT_REGISTRY = ".ai/context/capabilities.json";
39
-
40
- function usage(): never {
41
- console.error(
42
- [
43
- "Usage:",
44
- " scripts/capability-resolver.ts list [--repo <repo>] [--format json|text|prefixes]",
45
- " scripts/capability-resolver.ts match --path <repo-relative-path> [--repo <repo>] [--format json|text]",
46
- " scripts/capability-resolver.ts match --paths-from <file|-> [--repo <repo>] [--format json|text]",
47
- " scripts/capability-resolver.ts validate [--repo <repo>] [--format json|text]",
48
- ].join("\n")
49
- );
50
- process.exit(2);
51
- }
52
-
53
- function parseArgs(argv: string[]): Args {
54
- const args: Args = {
55
- command: argv[0] || "",
56
- repo: ".",
57
- path: "",
58
- pathsFrom: "",
59
- format: "text",
60
- };
61
-
62
- for (let index = 1; index < argv.length; index += 1) {
63
- const arg = argv[index];
64
- switch (arg) {
65
- case "--repo":
66
- args.repo = argv[++index] || usage();
67
- break;
68
- case "--path":
69
- args.path = argv[++index] || usage();
70
- break;
71
- case "--paths-from":
72
- args.pathsFrom = argv[++index] || usage();
73
- break;
74
- case "--format": {
75
- const value = argv[++index] as Format;
76
- if (!["json", "text", "prefixes"].includes(value)) usage();
77
- args.format = value;
78
- break;
79
- }
80
- case "--help":
81
- case "-h":
82
- usage();
83
- break;
84
- default:
85
- console.error(`Unknown argument: ${arg}`);
86
- usage();
87
- }
88
- }
89
-
90
- if (!["list", "match", "validate"].includes(args.command)) usage();
91
- if (args.command === "match" && !args.path && !args.pathsFrom) usage();
92
- if (args.command === "match" && args.path && args.pathsFrom) usage();
93
- if (args.command === "match" && args.format === "prefixes") usage();
94
- return args;
95
- }
96
-
97
- function repoRoot(input: string): string {
98
- const cwd = resolve(input);
99
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
100
- cwd,
101
- encoding: "utf-8",
102
- });
103
- if (result.status === 0 && result.stdout.trim()) {
104
- return resolve(result.stdout.trim());
105
- }
106
- return cwd;
107
- }
108
-
109
- function normalizeRepoPath(value: string, repo: string): string {
110
- let next = value.trim().replace(/^file:\/\//, "").replaceAll("\\", "/");
111
- const normalizedRepo = repo.replaceAll("\\", "/");
112
-
113
- if (next.startsWith(`${normalizedRepo}/`)) {
114
- next = next.slice(normalizedRepo.length + 1);
115
- } else if (next.startsWith("/")) {
116
- throw new Error(`absolute path is outside repo: ${value}`);
117
- }
118
-
119
- next = next.replace(/^\.\//, "").replace(/\/+$/, "");
120
- const parts = next.split("/").filter(Boolean);
121
- if (parts.length === 0) throw new Error("path must not be empty");
122
- if (parts.some((part) => part === "." || part === "..")) {
123
- throw new Error(`path must not contain traversal: ${value}`);
124
- }
125
- return parts.join("/");
126
- }
127
-
128
- function safeToken(value: string, fallback = "capability"): string {
129
- const token = value
130
- .toLowerCase()
131
- .replace(/[^a-z0-9]+/g, "-")
132
- .replace(/^-+|-+$/g, "")
133
- .replace(/-{2,}/g, "-");
134
- return token || fallback;
135
- }
136
-
137
- function defaultCapabilityForPrefix(prefix: string): Capability {
138
- const parts = prefix.split("/");
139
- const domain =
140
- parts.length >= 2 ? safeToken(`${parts[0]}-${parts[1]}`, safeToken(prefix)) : safeToken(prefix);
141
- const name = parts.length > 2 ? safeToken(parts[parts.length - 1]) : safeToken(parts[parts.length - 1] || domain);
142
- const id = parts.length > 2 ? `${domain}-${name}` : domain;
143
-
144
- return {
145
- id,
146
- domain,
147
- name,
148
- prefixes: [prefix],
149
- contract_files: {
150
- agents: `${prefix}/AGENTS.md`,
151
- claude: `${prefix}/CLAUDE.md`,
152
- },
153
- architecture_module: `docs/architecture/modules/${domain}/${name}.md`,
154
- workstream_dir: `tasks/workstreams/${domain}/${name}`,
155
- lsp_profile: "typescript-lsp",
156
- verification_hints: ["record local commands here before implementation"],
157
- };
158
- }
159
-
160
- function legacyBlocks(repo: string): string[] {
161
- const configFile = resolve(repo, ".ai/context/agent-context-blocks.txt");
162
- const envBlocks =
163
- process.env.FORGE_CONTEXT_BLOCKS || "";
164
- const rawBlocks = envBlocks
165
- ? envBlocks.split(/[,:]/)
166
- : existsSync(configFile)
167
- ? readFileSync(configFile, "utf-8").split(/\r?\n/)
168
- : [];
169
-
170
- const blocks = rawBlocks
171
- .map((line) => line.replace(/#.*$/, "").trim())
172
- .filter(Boolean)
173
- .map((line) => normalizeRepoPath(line, repo))
174
- .filter((line) => existsSync(resolve(repo, line)));
175
-
176
- if (blocks.length > 0) {
177
- return [...new Set(blocks)].sort();
178
- }
179
-
180
- const discovered: string[] = [];
181
- const ignored = new Set([".git", "node_modules", ".ai", ".claude", ".worktrees", "_ops", "_ref"]);
182
- function walk(absDir: string) {
183
- for (const entry of readdirSync(absDir, { withFileTypes: true })) {
184
- if (ignored.has(entry.name)) continue;
185
- const absPath = resolve(absDir, entry.name);
186
- if (entry.isDirectory()) {
187
- walk(absPath);
188
- continue;
189
- }
190
- if (entry.isFile() && (entry.name === "AGENTS.md" || entry.name === "CLAUDE.md")) {
191
- const dir = relative(repo, absDir).replaceAll("\\", "/");
192
- if (dir && dir !== ".") discovered.push(normalizeRepoPath(dir, repo));
193
- }
194
- }
195
- }
196
- walk(repo);
197
- return [...new Set(discovered)].sort();
198
- }
199
-
200
- function readRegistry(repo: string): CapabilityRegistry {
201
- const registryPath = resolve(repo, DEFAULT_REGISTRY);
202
- if (!existsSync(registryPath)) {
203
- return {
204
- version: 1,
205
- capabilities: legacyBlocks(repo).map(defaultCapabilityForPrefix),
206
- };
207
- }
208
-
209
- const parsed = JSON.parse(readFileSync(registryPath, "utf-8")) as CapabilityRegistry;
210
- return {
211
- version: parsed.version ?? 1,
212
- capabilities: Array.isArray(parsed.capabilities) ? parsed.capabilities : [],
213
- };
214
- }
215
-
216
- function validateCapability(capability: Capability, repo: string): string[] {
217
- const errors: string[] = [];
218
- const requiredStrings: Array<[keyof Capability, string]> = [
219
- ["id", capability.id],
220
- ["domain", capability.domain],
221
- ["name", capability.name],
222
- ["architecture_module", capability.architecture_module],
223
- ["workstream_dir", capability.workstream_dir],
224
- ["lsp_profile", capability.lsp_profile],
225
- ];
226
-
227
- for (const [field, value] of requiredStrings) {
228
- if (typeof value !== "string" || value.trim() === "") {
229
- errors.push(`${capability.id || "(unknown)"}: ${String(field)} is required`);
230
- }
231
- }
232
-
233
- if (!Array.isArray(capability.prefixes) || capability.prefixes.length === 0) {
234
- errors.push(`${capability.id || "(unknown)"}: prefixes must contain at least one path`);
235
- } else {
236
- for (const prefix of capability.prefixes) {
237
- try {
238
- normalizeRepoPath(prefix, repo);
239
- } catch (error) {
240
- errors.push(`${capability.id}: invalid prefix ${prefix}: ${(error as Error).message}`);
241
- }
242
- }
243
- }
244
-
245
- const contractFiles = capability.contract_files;
246
- if (!contractFiles || typeof contractFiles !== "object") {
247
- errors.push(`${capability.id}: contract_files.agents and contract_files.claude are required`);
248
- } else {
249
- for (const field of ["agents", "claude"] as const) {
250
- const value = contractFiles[field];
251
- if (typeof value !== "string" || value.trim() === "") {
252
- errors.push(`${capability.id}: contract_files.${field} is required`);
253
- continue;
254
- }
255
- try {
256
- normalizeRepoPath(value, repo);
257
- } catch (error) {
258
- errors.push(`${capability.id}: invalid contract_files.${field}: ${(error as Error).message}`);
259
- }
260
- }
261
- }
262
-
263
- for (const [field, value] of [
264
- ["architecture_module", capability.architecture_module],
265
- ["workstream_dir", capability.workstream_dir],
266
- ] as const) {
267
- try {
268
- normalizeRepoPath(value, repo);
269
- } catch (error) {
270
- errors.push(`${capability.id}: invalid ${field}: ${(error as Error).message}`);
271
- }
272
- }
273
-
274
- if (!Array.isArray(capability.verification_hints)) {
275
- errors.push(`${capability.id}: verification_hints must be an array`);
276
- }
277
-
278
- return errors;
279
- }
280
-
281
- function validateRegistry(registry: CapabilityRegistry, repo: string): string[] {
282
- const errors: string[] = [];
283
- const ids = new Map<string, string>();
284
- const prefixes = new Map<string, string>();
285
- const architectureModules = new Set<string>();
286
- const workstreamDirs = new Set<string>();
287
-
288
- if (!Number.isInteger(registry.version)) {
289
- errors.push("version must be an integer");
290
- }
291
-
292
- for (const capability of registry.capabilities) {
293
- errors.push(...validateCapability(capability, repo));
294
- if (ids.has(capability.id)) {
295
- errors.push(`duplicate capability id: ${capability.id}`);
296
- }
297
- ids.set(capability.id, capability.id);
298
-
299
- for (const prefix of capability.prefixes || []) {
300
- let normalized = "";
301
- try {
302
- normalized = normalizeRepoPath(prefix, repo);
303
- } catch {
304
- continue;
305
- }
306
- const owner = prefixes.get(normalized);
307
- if (owner && owner !== capability.id) {
308
- errors.push(`duplicate capability prefix: ${normalized} (${owner}, ${capability.id})`);
309
- }
310
- prefixes.set(normalized, capability.id);
311
- }
312
-
313
- try {
314
- architectureModules.add(normalizeRepoPath(capability.architecture_module, repo));
315
- workstreamDirs.add(normalizeRepoPath(capability.workstream_dir, repo));
316
- } catch {
317
- // Field-specific validation already recorded the concrete error.
318
- }
319
- }
320
-
321
- const modulesRoot = resolve(repo, "docs/architecture/modules");
322
- if (existsSync(modulesRoot)) {
323
- const stack = [modulesRoot];
324
- while (stack.length > 0) {
325
- const current = stack.pop()!;
326
- for (const entry of readdirSync(current, { withFileTypes: true })) {
327
- const absPath = resolve(current, entry.name);
328
- if (entry.isDirectory()) {
329
- stack.push(absPath);
330
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
331
- const relPath = relative(repo, absPath).replaceAll("\\", "/");
332
- if (!architectureModules.has(relPath)) {
333
- errors.push(`orphan architecture module: ${relPath}`);
334
- }
335
- }
336
- }
337
- }
338
- }
339
-
340
- const workstreamsRoot = resolve(repo, "tasks/workstreams");
341
- if (existsSync(workstreamsRoot)) {
342
- const stack = [workstreamsRoot];
343
- while (stack.length > 0) {
344
- const current = stack.pop()!;
345
- for (const entry of readdirSync(current, { withFileTypes: true })) {
346
- const absPath = resolve(current, entry.name);
347
- if (entry.isDirectory()) {
348
- stack.push(absPath);
349
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
350
- const relPath = relative(repo, absPath).replaceAll("\\", "/");
351
- const owned = [...workstreamDirs].some((dir) => relPath === dir || relPath.startsWith(`${dir}/`));
352
- if (!owned) {
353
- errors.push(`orphan workstream: ${relPath}`);
354
- }
355
- }
356
- }
357
- }
358
- }
359
-
360
- return errors;
361
- }
362
-
363
- function findMatch(registry: CapabilityRegistry, repo: string, inputPath: string) {
364
- const relPath = normalizeRepoPath(inputPath, repo);
365
- const matches: Array<{ capability: Capability; prefix: string }> = [];
366
-
367
- for (const capability of registry.capabilities) {
368
- for (const rawPrefix of capability.prefixes || []) {
369
- const prefix = normalizeRepoPath(rawPrefix, repo);
370
- if (relPath === prefix || relPath.startsWith(`${prefix}/`)) {
371
- matches.push({ capability, prefix });
372
- }
373
- }
374
- }
375
-
376
- if (matches.length === 0) {
377
- return {
378
- matched: false,
379
- file_path: relPath,
380
- functional_block: "root",
381
- matched_prefix: "root",
382
- capability_id: "root",
383
- architecture_domain: "root",
384
- architecture_capability: "_root",
385
- architecture_module: "docs/architecture/index.md",
386
- workstream_dir: "tasks/workstreams/root/_root",
387
- };
388
- }
389
-
390
- matches.sort((left, right) => right.prefix.length - left.prefix.length);
391
- const longest = matches[0].prefix.length;
392
- const winners = matches.filter((match) => match.prefix.length === longest);
393
- const winnerKeys = new Set(winners.map((match) => `${match.capability.id}:${match.prefix}`));
394
- if (winnerKeys.size > 1) {
395
- throw new Error(
396
- `ambiguous capability match for ${relPath}: ${winners
397
- .map((match) => `${match.capability.id} (${match.prefix})`)
398
- .join(", ")}`
399
- );
400
- }
401
-
402
- const winner = winners[0];
403
- return {
404
- matched: true,
405
- file_path: relPath,
406
- functional_block: winner.prefix,
407
- matched_prefix: winner.prefix,
408
- capability_id: winner.capability.id,
409
- architecture_domain: winner.capability.domain,
410
- architecture_capability: winner.capability.name,
411
- architecture_module: winner.capability.architecture_module,
412
- workstream_dir: winner.capability.workstream_dir,
413
- contract_agents: winner.capability.contract_files.agents,
414
- contract_claude: winner.capability.contract_files.claude,
415
- lsp_profile: winner.capability.lsp_profile,
416
- verification_hints: winner.capability.verification_hints,
417
- };
418
- }
419
-
420
- function printJson(value: unknown): void {
421
- console.log(JSON.stringify(value, null, 2));
422
- }
423
-
424
- async function readPathLines(input: string): Promise<string[]> {
425
- const text = input === "-" ? await Bun.stdin.text() : readFileSync(input, "utf-8");
426
- return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
427
- }
428
-
429
- async function main(): Promise<void> {
430
- const args = parseArgs(process.argv.slice(2));
431
- const repo = repoRoot(args.repo);
432
- const registry = readRegistry(repo);
433
-
434
- if (args.command === "validate") {
435
- const errors = validateRegistry(registry, repo);
436
- if (args.format === "json") {
437
- printJson({ ok: errors.length === 0, errors });
438
- } else if (errors.length === 0) {
439
- console.log("[CapabilityResolver] OK");
440
- } else {
441
- for (const error of errors) console.log(`[CapabilityResolver] ${error}`);
442
- }
443
- process.exit(errors.length === 0 ? 0 : 1);
444
- }
445
-
446
- if (args.command === "list") {
447
- if (args.format === "json") {
448
- printJson(registry.capabilities);
449
- } else if (args.format === "prefixes") {
450
- for (const capability of registry.capabilities) {
451
- for (const prefix of capability.prefixes || []) {
452
- console.log(normalizeRepoPath(prefix, repo));
453
- }
454
- }
455
- } else {
456
- for (const capability of registry.capabilities) {
457
- console.log(`${capability.id}\t${capability.prefixes.join(",")}`);
458
- }
459
- }
460
- return;
461
- }
462
-
463
- const errors = validateRegistry(registry, repo);
464
- if (errors.length > 0) {
465
- throw new Error(`capability registry is invalid:\n${errors.join("\n")}`);
466
- }
467
- if (args.pathsFrom) {
468
- const paths = await readPathLines(args.pathsFrom);
469
- const seen = new Set<string>();
470
- const matches = [];
471
- for (const path of paths) {
472
- if (seen.has(path)) continue;
473
- seen.add(path);
474
- matches.push(findMatch(registry, repo, path));
475
- }
476
- if (args.format === "json") {
477
- printJson(matches);
478
- } else {
479
- for (const match of matches) {
480
- console.log(`${match.file_path}: ${match.capability_id} (${match.matched_prefix})`);
481
- }
482
- }
483
- return;
484
- }
485
-
486
- const match = findMatch(registry, repo, args.path);
487
- if (args.format === "json") {
488
- printJson(match);
489
- } else {
490
- for (const [key, value] of Object.entries(match)) {
491
- if (Array.isArray(value)) {
492
- console.log(`${key}: ${value.join(", ")}`);
493
- } else {
494
- console.log(`${key}: ${value}`);
495
- }
496
- }
497
- }
498
- }
499
-
500
- try {
501
- await main();
502
- } catch (error) {
503
- console.error(`[CapabilityResolver] ${(error as Error).message}`);
504
- process.exit(1);
505
- }
4
+ if (import.meta.main) runTrackedForgeHelper("capability-resolver.ts");