forge-workflow 0.1.0-beta.5 → 0.1.0-beta.6

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 (128) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +36 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +108 -6
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +65 -21
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/workflow/enforce-stage.js +44 -0
  107. package/lib/workflow/plan-authority.js +225 -0
  108. package/package.json +8 -4
  109. package/scripts/commitlint.js +13 -15
  110. package/scripts/generate-risk-manifest.js +91 -0
  111. package/scripts/github-context-bridge.sh +10 -0
  112. package/scripts/legacy-claim-repair.js +145 -0
  113. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  114. package/scripts/process-tree.js +14 -2
  115. package/scripts/protected-state-check.js +440 -17
  116. package/scripts/sync-agent-skills.js +333 -34
  117. package/scripts/test-full-suite.js +704 -18
  118. package/scripts/test-profile.js +13 -3
  119. package/scripts/test.js +95 -14
  120. package/skills/coverage.json +1 -0
  121. package/skills/review/SKILL.md +2 -0
  122. package/skills/review/evals/scorecard.json +2 -2
  123. package/skills/setup/SKILL.md +18 -0
  124. package/skills/setup/evals/scorecard.json +3 -3
  125. package/skills/shepherd/SKILL.md +19 -2
  126. package/skills/shepherd/evals/scorecard.json +3 -3
  127. package/skills/validate/SKILL.md +3 -0
  128. package/skills/validate/evals/scorecard.json +1 -1
@@ -109,20 +109,106 @@ function isLinkPermissionError(error) {
109
109
  // inspect, and `yarn check` was removed in Berry, so there is nothing to verify.
110
110
  const PROBE_LESS_MANAGERS = new Set(['yarn']);
111
111
 
112
+ function samePath(left, right) {
113
+ return path.relative(left, right) === '';
114
+ }
115
+
112
116
  /**
113
- * Direct dependencies the worktree declares. Optional/peer deps are excluded —
114
- * they may legitimately be absent. An unreadable package.json yields none, so an
115
- * unverifiable worktree is left alone rather than force-reinstalled.
117
+ * Direct dependencies the root and its workspaces declare. Optional/peer deps
118
+ * are excluded — they may legitimately be absent. Workspace discovery fails
119
+ * closed so an incomplete manifest set cannot prove an install complete.
116
120
  * @param {string} worktreePath - Absolute path to the worktree
117
121
  * @param {object} fsApi - fs module (for DI)
118
- * @returns {string[]} Declared package names
122
+ * @returns {Array<{dir: string, name: string, workspaceDir: string|null}>} Declaring directory and package name
119
123
  */
120
124
  function declaredDependencies(worktreePath, fsApi) {
125
+ let rootPkg;
126
+ try {
127
+ rootPkg = JSON.parse(fsApi.readFileSync(path.join(worktreePath, 'package.json'), 'utf8'));
128
+ } catch (error) {
129
+ throw new Error(`Cannot verify dependencies: unreadable worktree package.json: ${error.message}`);
130
+ }
131
+
132
+ let workspacePatterns = [];
133
+ if (rootPkg.workspaces != null) {
134
+ workspacePatterns = Array.isArray(rootPkg.workspaces)
135
+ ? rootPkg.workspaces
136
+ : rootPkg.workspaces.packages;
137
+ if (!Array.isArray(workspacePatterns)) {
138
+ throw new Error('Cannot verify dependencies: workspaces must be an array or an object with a packages array');
139
+ }
140
+ }
141
+
142
+ if (workspacePatterns.length && typeof fsApi.globSync !== 'function') {
143
+ throw new Error('Cannot verify dependencies: this runtime does not provide workspace glob support');
144
+ }
145
+
146
+ const manifestPaths = new Set();
147
+ for (const rawPattern of workspacePatterns) {
148
+ if (typeof rawPattern !== 'string' || !rawPattern.trim()) {
149
+ throw new Error(`Cannot verify dependencies: invalid workspace pattern ${JSON.stringify(rawPattern)}`);
150
+ }
151
+ const exclude = rawPattern.startsWith('!');
152
+ const pattern = (exclude ? rawPattern.slice(1) : rawPattern).replace(/\\/g, '/').replace(/\/+$/, '');
153
+ const resolvedPattern = resolveRepoRelativePath(worktreePath, pattern);
154
+ if (!pattern || pattern.includes('\0') || path.isAbsolute(pattern) || !resolvedPattern.insideRoot) {
155
+ throw new Error(`Cannot verify dependencies: workspace pattern resolves outside the worktree: ${rawPattern}`);
156
+ }
157
+
158
+ let matches;
159
+ try {
160
+ matches = fsApi.globSync(`${pattern}/package.json`, {
161
+ cwd: worktreePath,
162
+ exclude: candidate => String(candidate).split(/[\\/]/).includes('node_modules'),
163
+ });
164
+ } catch (error) {
165
+ throw new Error(`Cannot verify dependencies: workspace glob failed for ${rawPattern}: ${error.message}`);
166
+ }
167
+ for (const match of matches) {
168
+ const resolved = resolveRepoRelativePath(worktreePath, String(match));
169
+ if (!resolved.insideRoot) {
170
+ throw new Error(`Cannot verify dependencies: workspace glob resolved outside the worktree: ${match}`);
171
+ }
172
+ const existing = [...manifestPaths].find(manifestPath => samePath(manifestPath, resolved.target));
173
+ if (exclude && existing) manifestPaths.delete(existing);
174
+ else if (!exclude && !existing) manifestPaths.add(resolved.target);
175
+ }
176
+ }
177
+
178
+ const manifests = [{ dir: worktreePath, pkg: rootPkg }];
179
+ for (const manifestPath of manifestPaths) {
180
+ try {
181
+ manifests.push({
182
+ dir: path.dirname(manifestPath),
183
+ pkg: JSON.parse(fsApi.readFileSync(manifestPath, 'utf8')),
184
+ });
185
+ } catch (error) {
186
+ if (error && ['ENOENT', 'ENOTDIR'].includes(error.code)) continue;
187
+ throw new Error(`Cannot verify dependencies: unreadable workspace manifest ${manifestPath}: ${error.message}`);
188
+ }
189
+ }
190
+
191
+ const workspaceDirs = new Map();
192
+ for (const { dir, pkg } of manifests.slice(1)) {
193
+ if (!pkg.name) continue;
194
+ if (workspaceDirs.has(pkg.name) && !samePath(workspaceDirs.get(pkg.name), dir)) {
195
+ throw new Error(`Cannot verify dependencies: duplicate workspace package name ${pkg.name}`);
196
+ }
197
+ workspaceDirs.set(pkg.name, dir);
198
+ }
199
+ return manifests.flatMap(({ dir, pkg }) => [
200
+ ...Object.keys(pkg.dependencies || {}),
201
+ ...Object.keys(pkg.devDependencies || {}),
202
+ ].map(name => ({ dir, name, workspaceDir: workspaceDirs.get(name) || null })));
203
+ }
204
+
205
+ function dependencyCandidatePresent(candidate, workspaceDir, fsApi) {
206
+ if (!fsApi.existsSync(path.join(candidate, 'package.json'))) return false;
207
+ if (!workspaceDir) return true;
121
208
  try {
122
- const pkg = JSON.parse(fsApi.readFileSync(path.join(worktreePath, 'package.json'), 'utf8'));
123
- return [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})];
124
- } catch (_err) { /* intentional: no readable package.json → nothing to verify */ // NOSONAR S2486
125
- return [];
209
+ return path.relative(fsApi.realpathSync(workspaceDir), fsApi.realpathSync(candidate)) === '';
210
+ } catch (_err) { /* intentional: an unresolvable workspace link is not installed */ // NOSONAR S2486
211
+ return false;
126
212
  }
127
213
  }
128
214
 
@@ -138,11 +224,33 @@ function declaredDependencies(worktreePath, fsApi) {
138
224
  *
139
225
  * @param {string} worktreePath - Absolute path to the worktree
140
226
  * @param {object} fsApi - fs module (for DI)
227
+ * @param {string|null} pkgManager - Detected package manager
141
228
  * @returns {boolean} true when nothing is missing
142
229
  */
143
- function dependenciesPresent(worktreePath, fsApi) {
230
+ function dependenciesPresent(worktreePath, fsApi, pkgManager) {
231
+ if (!pkgManager) return true;
144
232
  const deps = declaredDependencies(worktreePath, fsApi);
145
- return deps.every((name) => fsApi.existsSync(path.join(worktreePath, 'node_modules', name, 'package.json')));
233
+ return deps.every(({ dir, name, workspaceDir }) => {
234
+ const localPackage = path.join(dir, 'node_modules', name);
235
+ if (fsApi.existsSync(localPackage)) {
236
+ return dependencyCandidatePresent(localPackage, workspaceDir, fsApi);
237
+ }
238
+
239
+ const rootPackage = path.join(worktreePath, 'node_modules', name);
240
+ if (dir === worktreePath || (pkgManager === 'bun' && !workspaceDir)) return false;
241
+ return dependencyCandidatePresent(rootPackage, workspaceDir, fsApi);
242
+ });
243
+ }
244
+
245
+ function detachModulesLink(modulesPath, fsApi) {
246
+ try {
247
+ if (!fsApi.lstatSync(modulesPath).isSymbolicLink()) return false;
248
+ fsApi.unlinkSync(modulesPath);
249
+ return true;
250
+ } catch (error) {
251
+ if (error && error.code === 'ENOENT') return false;
252
+ throw error;
253
+ }
146
254
  }
147
255
 
148
256
  const STDERR_DETAIL_LIMIT = 400;
@@ -199,21 +307,21 @@ function spawnInstall(pkgManager, args, worktreePath, spawnFn) {
199
307
  * @param {string} projectRoot - Absolute path to the project root
200
308
  * @param {Function} spawnFn - spawnSync-compatible function
201
309
  * @param {object} fsApi - fs module (for DI)
310
+ * @param {string|null} [pkgManager] - Detected package manager
202
311
  * @returns {{ linked: boolean, installed: boolean, healed: boolean }}
203
312
  * @throws {Error} when the install fails, or the tree is still broken after --force.
204
313
  */
205
- function runInstall(worktreePath, projectRoot, spawnFn, fsApi) {
206
- const pkgManager = detectPackageManager(projectRoot, fsApi);
314
+ function runInstall(worktreePath, projectRoot, spawnFn, fsApi, pkgManager = detectPackageManager(projectRoot, fsApi)) {
207
315
  if (!pkgManager) return { linked: false, installed: false, healed: false };
208
316
 
209
317
  spawnInstall(pkgManager, ['install'], worktreePath, spawnFn);
210
318
 
211
- if (PROBE_LESS_MANAGERS.has(pkgManager) || dependenciesPresent(worktreePath, fsApi)) {
319
+ if (PROBE_LESS_MANAGERS.has(pkgManager) || dependenciesPresent(worktreePath, fsApi, pkgManager)) {
212
320
  return { linked: false, installed: true, healed: false };
213
321
  }
214
322
 
215
323
  spawnInstall(pkgManager, ['install', '--force'], worktreePath, spawnFn);
216
- if (!dependenciesPresent(worktreePath, fsApi)) {
324
+ if (!dependenciesPresent(worktreePath, fsApi, pkgManager)) {
217
325
  throw new Error(`Dependency install could not be verified: packages are still missing in ${worktreePath} after '${pkgManager} install --force'. Run it manually there and inspect the output.`);
218
326
  }
219
327
  return { linked: false, installed: true, healed: true };
@@ -237,24 +345,38 @@ function runInstall(worktreePath, projectRoot, spawnFn, fsApi) {
237
345
  function setupWorktreeDeps(worktreePath, projectRoot, { spawnFn, fsApi, platform }) {
238
346
  const srcModules = path.join(projectRoot, 'node_modules');
239
347
  const destModules = path.join(worktreePath, 'node_modules');
348
+ const pkgManager = detectPackageManager(projectRoot, fsApi);
349
+
350
+ // Keep the zero-install fast path only when root and workspace dependencies
351
+ // are complete; Bun's isolated linker keeps workspace packages below each workspace.
352
+ if (fsApi.existsSync(destModules)) {
353
+ if (dependenciesPresent(worktreePath, fsApi, pkgManager)) return { linked: false, installed: false, healed: false };
354
+ detachModulesLink(destModules, fsApi);
355
+ return runInstall(worktreePath, projectRoot, spawnFn, fsApi, pkgManager);
356
+ }
240
357
 
241
- // Already populated (e.g. git checkout carried it) — nothing to do.
242
- if (fsApi.existsSync(destModules)) return { linked: false, installed: false, healed: false };
358
+ // existsSync follows links, so remove a dangling directory entry before
359
+ // either replacing it with the shared install or installing in its place.
360
+ detachModulesLink(destModules, fsApi);
243
361
 
244
362
  // Fast path: link to the shared install when the main repo has one.
245
363
  if (fsApi.existsSync(srcModules)) {
246
364
  const linkType = platform === 'win32' ? 'junction' : 'dir';
247
365
  try {
248
366
  fsApi.symlinkSync(srcModules, destModules, linkType);
249
- return { linked: true, installed: false, healed: false };
250
367
  } catch (error) {
251
368
  // Degrade to a real install only when the link was refused for lack of
252
369
  // privilege/support; any other failure is surfaced to the caller.
253
370
  if (!isLinkPermissionError(error)) throw error;
371
+ return runInstall(worktreePath, projectRoot, spawnFn, fsApi, pkgManager);
372
+ }
373
+ if (dependenciesPresent(worktreePath, fsApi, pkgManager)) {
374
+ return { linked: true, installed: false, healed: false };
254
375
  }
376
+ fsApi.unlinkSync(destModules);
255
377
  }
256
378
 
257
- return runInstall(worktreePath, projectRoot, spawnFn, fsApi);
379
+ return runInstall(worktreePath, projectRoot, spawnFn, fsApi, pkgManager);
258
380
  }
259
381
 
260
382
  function readExistingWorktreeLinkage(driver, worktreePath, reuse) {
@@ -504,12 +626,26 @@ async function handleCreate(slug, flags, projectRoot, opts) {
504
626
  if (linkage.conflict) {
505
627
  return { success: false, reused: true, error: linkage.reason, worktreePath, linkage };
506
628
  }
629
+ let deps;
630
+ try {
631
+ deps = setupWorktreeDeps(worktreePath, projectRoot, {
632
+ spawnFn: runSpawn,
633
+ fsApi,
634
+ platform: opts._platform || process.platform,
635
+ });
636
+ } catch (error) {
637
+ return { success: false, reused: true, error: error.message, worktreePath, linkage };
638
+ }
507
639
  const backing = await autoFileBackingIssue({ projectRoot, worktreePath, branch: existingBranch, issueId, opts });
640
+ const healNote = deps.healed ? ' Healed dependencies (store was stale): reinstalled with --force.' : '';
508
641
  return {
509
642
  success: true,
510
643
  reused: true,
511
- message: `Worktree already exists at ${worktreePath}`,
644
+ message: `Worktree already exists at ${worktreePath}.${healNote}`,
512
645
  worktreePath,
646
+ depsLinked: deps.linked,
647
+ depsInstalled: deps.installed,
648
+ depsHealed: deps.healed,
513
649
  linkage,
514
650
  backing,
515
651
  };
@@ -0,0 +1,9 @@
1
+ # Forge 0.1.0-beta.5 compatibility corpus v1
2
+
3
+ This immutable corpus is derived from release commit
4
+ `ebeb4e5b31fc2dacdf23c5936c02fb2656990f49`. Contract snapshots retain only
5
+ public package, binary, command, alias, and workflow facts. State rows are
6
+ synthetic and contain no user, device, prompt, tool-log, or repository data.
7
+
8
+ `manifest.json` hashes every corpus member. Change requires a new corpus
9
+ version; never rewrite v1 after it becomes release evidence.
@@ -0,0 +1,26 @@
1
+ {
2
+ "commands": ["adapter", "add", "audit", "blocked", "claim", "claims", "clean", "close", "comment", "control", "create", "dev", "doc-gate", "doctor", "explain", "export", "gate", "hooks", "inbox", "init", "insights", "issue", "issues", "lint", "list", "memory", "merge", "migrate", "new", "options", "orient", "orphans", "patch", "plan", "pr", "preflight", "prime", "push", "ready", "recall", "recap", "recommend", "release", "remember", "role", "serve", "setup", "shepherd", "ship", "show", "skill", "stage", "stale", "status", "sync", "team", "test", "update", "upgrade", "validate", "worktree"],
3
+ "aliases": {
4
+ "blocked": { "canonical": "issue blocked", "visible": false },
5
+ "claim": { "canonical": "issue claim", "visible": false },
6
+ "claims": { "canonical": "issue claims", "visible": false },
7
+ "close": { "canonical": "issue close", "visible": false },
8
+ "create": { "canonical": "issue create", "visible": false },
9
+ "doc-gate": { "canonical": "gate doc", "visible": false },
10
+ "insights": { "canonical": "memory insights", "visible": true },
11
+ "issues": { "canonical": "issue list", "visible": false },
12
+ "lint": { "canonical": "issue lint", "visible": false },
13
+ "list": { "canonical": "issue list", "visible": false },
14
+ "merge": { "canonical": "pr merge", "visible": false },
15
+ "orphans": { "canonical": "issue orphans", "visible": false },
16
+ "preflight": { "canonical": "pr preflight", "visible": true },
17
+ "ready": { "canonical": "issue ready", "visible": false },
18
+ "recall": { "canonical": "memory recall", "visible": true },
19
+ "remember": { "canonical": "memory add", "visible": true },
20
+ "shepherd": { "canonical": "pr shepherd", "visible": false },
21
+ "ship": { "canonical": "pr ship", "visible": true },
22
+ "show": { "canonical": "issue show", "visible": false },
23
+ "stale": { "canonical": "issue stale", "visible": false },
24
+ "update": { "canonical": "issue update", "visible": false }
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "forge-workflow",
3
+ "version": "0.1.0-beta.5",
4
+ "release_commit": "ebeb4e5b31fc2dacdf23c5936c02fb2656990f49",
5
+ "binaries": {
6
+ "forge": "bin/forge.js",
7
+ "forge-workflow": "bin/forge.js",
8
+ "forge-preflight": "bin/forge-preflight.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=22.16.0"
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "critical": ["plan", "dev", "validate", "ship", "review", "verify"],
3
+ "standard": ["plan", "dev", "validate", "ship", "review"],
4
+ "refactor": ["plan", "dev", "validate", "ship"],
5
+ "simple": ["dev", "validate", "ship"],
6
+ "hotfix": ["dev", "validate", "ship"],
7
+ "docs": ["verify", "ship"]
8
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "schema_version": "forge.beta5-corpus.v1",
3
+ "corpus_version": 1,
4
+ "release_version": "0.1.0-beta.5",
5
+ "release_commit": "ebeb4e5b31fc2dacdf23c5936c02fb2656990f49",
6
+ "source_blobs": {
7
+ "package.json": "6f3f112341cd50e0e76fad615beb007b9e6dff2c",
8
+ "lib/commands/_manifest.js": "62595c38b66b139244ca1f54cb034f67d849247d",
9
+ "lib/commands/_aliases.js": "2b16d61d1cdb9ef3e594b5a5093206d05844b460",
10
+ "lib/workflow/stages.js": "0b7ae32bddcbb9bd6dd52a07ca22e43364621918"
11
+ },
12
+ "hash_algorithm": "sha256",
13
+ "content_hash": "c6978eed757c6255f2d25ea6d49ada44e5b3da91830d348016267981d13afd18",
14
+ "files": [
15
+ { "path": "README.md", "sha256": "3cfcc78856e397f2cfd3fee9fa2fffb630e27ea3e76ee8d8b5d9a77cb366a709" },
16
+ { "path": "contract/command-contract.json", "sha256": "8b4f885091fa8afe72b7b5a5a8039869c871f4fb1dc3344728b2c4322f491a84" },
17
+ { "path": "contract/package-contract.json", "sha256": "a65280ede51d62c1913140c83b40e6c3312c991c7a173e7a9ad52d3d5ea0fd47" },
18
+ { "path": "contract/workflow-stage-matrix.json", "sha256": "2aadafdb5f5c1817c8289dd6134d034e3ccc6579438935520bd3c6b90401e5bf" },
19
+ { "path": "state/comments.jsonl", "sha256": "ae4f694f207a11a22b6798943cc6d07487d1dc2be83a78ceadad947a3ff40113" },
20
+ { "path": "state/config.yaml", "sha256": "0b877d9180cf14951657254b50cd771e131970e92582a7c0337f36418a35e1c0" },
21
+ { "path": "state/dependencies.jsonl", "sha256": "cb43cc053d6dd885254a26f6d6b024883f770eb68f50b715cc2f0f05e2cd31e5" },
22
+ { "path": "state/issues.jsonl", "sha256": "425567c5c7b70fd6bbdf9ead3aff78c09a77ebe7b78d3c856c045743b8cb924e" },
23
+ { "path": "state/kernel.sql", "sha256": "4208aabcadc6cec3234ebb5abd4cbf32cc74805433d04a05801e0b6354b53d27" }
24
+ ]
25
+ }
@@ -0,0 +1 @@
1
+ {"id":"synthetic-comment-1","issue_id":"synthetic-issue-1","body":"Synthetic comment","actor":"fixture"}
@@ -0,0 +1,6 @@
1
+ workflow:
2
+ profile: default
3
+ gates:
4
+ gate.issue_verify: true
5
+ rails:
6
+ rail.kernel_tracking: true
@@ -0,0 +1 @@
1
+ {"issue_id":"synthetic-issue-1","blocks_issue_id":"synthetic-issue-2"}
@@ -0,0 +1,2 @@
1
+ {"id":"synthetic-issue-1","title":"Synthetic issue","status":"open","entity_revision":2}
2
+ {"id":"synthetic-issue-2","title":"Synthetic dependency","status":"done","entity_revision":1}
@@ -0,0 +1,20 @@
1
+ PRAGMA user_version = 1;
2
+ CREATE TABLE kernel_migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL);
3
+ CREATE TABLE kernel_issues (id TEXT PRIMARY KEY, title TEXT NOT NULL, status TEXT NOT NULL, entity_revision INTEGER NOT NULL);
4
+ CREATE TABLE kernel_comments (id TEXT PRIMARY KEY, issue_id TEXT NOT NULL, body TEXT NOT NULL, actor TEXT NOT NULL);
5
+ CREATE TABLE kernel_dependencies (id TEXT PRIMARY KEY, issue_id TEXT NOT NULL, blocks_issue_id TEXT NOT NULL);
6
+ CREATE TABLE kernel_claims (id TEXT PRIMARY KEY, issue_id TEXT NOT NULL, actor TEXT NOT NULL, state TEXT NOT NULL);
7
+ CREATE TABLE kernel_worktrees (id TEXT PRIMARY KEY, path TEXT NOT NULL, branch TEXT NOT NULL, state TEXT NOT NULL);
8
+ CREATE TABLE kernel_stage_runs (id TEXT PRIMARY KEY, issue_id TEXT NOT NULL, stage TEXT NOT NULL, status TEXT NOT NULL);
9
+ CREATE TABLE kernel_projections (id TEXT PRIMARY KEY, target TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, status TEXT NOT NULL);
10
+ CREATE TABLE kernel_events (id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, event_type TEXT NOT NULL);
11
+ INSERT INTO kernel_migrations VALUES ('001_initial_kernel_schema', '2026-08-09T00:00:00.000Z');
12
+ INSERT INTO kernel_issues VALUES ('synthetic-issue-1', 'Synthetic issue', 'open', 2); -- NOSONAR S1192: repeated synthetic foreign key preserves fixture readability.
13
+ INSERT INTO kernel_issues VALUES ('synthetic-issue-2', 'Synthetic dependency', 'done', 1);
14
+ INSERT INTO kernel_comments VALUES ('synthetic-comment-1', 'synthetic-issue-1', 'Synthetic comment', 'fixture');
15
+ INSERT INTO kernel_dependencies VALUES ('synthetic-dependency-1', 'synthetic-issue-1', 'synthetic-issue-2');
16
+ INSERT INTO kernel_claims VALUES ('synthetic-claim-1', 'synthetic-issue-1', 'fixture', 'active');
17
+ INSERT INTO kernel_worktrees VALUES ('synthetic-worktree-1', '/synthetic/worktree', 'fixture/branch', 'active');
18
+ INSERT INTO kernel_stage_runs VALUES ('synthetic-run-1', 'synthetic-issue-1', 'dev', 'done');
19
+ INSERT INTO kernel_projections VALUES ('synthetic-projection-1', 'jsonl', 'issue', 'synthetic-issue-1', 'delivered');
20
+ INSERT INTO kernel_events VALUES ('synthetic-event-1', 'issue', 'synthetic-issue-1', 'issue.updated');
@@ -7,6 +7,8 @@ const { createLocalBroker, buildLocalBrokerConfig } = require('./kernel/broker')
7
7
  const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
8
8
  const { detectWorktree } = require('./detect-worktree');
9
9
  const { DEFAULT_LEASE_TTL_MS } = require('./kernel/lease-enforcer');
10
+ const { ISSUE_TYPES } = require('./kernel/issue-command-contract');
11
+ const { loadRuntimeGraphConfig } = require('./core/runtime-graph');
10
12
 
11
13
  const OPERATION_METHOD_ALIASES = {
12
14
  'dep.add': 'depAdd',
@@ -199,6 +201,75 @@ function resolveLeaseTtlMs(env = {}) {
199
201
  return ttl;
200
202
  }
201
203
 
204
+ function readIssueContractValues(loaded, fail) {
205
+ const missing = Symbol('missing issue contract config');
206
+ const ownDataValue = (object, key, path) => {
207
+ if (!object || (typeof object !== 'object' && typeof object !== 'function')) return missing;
208
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
209
+ if (!descriptor) return missing;
210
+ if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
211
+ fail(`${path} must be a data property`);
212
+ }
213
+ return descriptor.value;
214
+ };
215
+ const issues = ownDataValue(loaded?.config, 'issues', 'issues');
216
+ const readiness = issues === missing ? missing : ownDataValue(issues, 'readiness', 'issues.readiness');
217
+ const configured = readiness === missing
218
+ ? missing
219
+ : ownDataValue(readiness, 'contracts', 'issues.readiness.contracts');
220
+ if (configured === missing || configured == null) return null;
221
+ if (typeof configured !== 'object'
222
+ || Array.isArray(configured)
223
+ || Object.getPrototypeOf(configured) !== Object.prototype) {
224
+ fail('issues.readiness.contracts must be a plain JSON object');
225
+ }
226
+ const allowedKeys = new Set(['enabled', 'workClasses']);
227
+ const ownKeys = Reflect.ownKeys(configured);
228
+ const unknownKeys = ownKeys.filter(key => typeof key !== 'string' || !allowedKeys.has(key));
229
+ if (unknownKeys.length > 0) {
230
+ fail(`unknown issues.readiness.contracts keys: ${unknownKeys.map(String).sort((left, right) => left.localeCompare(right)).join(', ')}`);
231
+ }
232
+ const values = Object.create(null);
233
+ for (const key of ownKeys) {
234
+ const descriptor = Object.getOwnPropertyDescriptor(configured, key);
235
+ if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
236
+ fail(`issues.readiness.contracts.${key} must be a data property`);
237
+ }
238
+ values[key] = descriptor.value;
239
+ }
240
+ return values;
241
+ }
242
+
243
+ function resolveIssueContractPolicy(projectRoot, deps = {}) {
244
+ const loadConfig = deps.loadRuntimeGraphConfig || loadRuntimeGraphConfig;
245
+ const loaded = loadConfig({ projectRoot });
246
+ const fail = message => {
247
+ const error = new Error(`FORGE_ISSUE_CONTRACT_CONFIG_INVALID: ${message}`);
248
+ error.code = 'FORGE_ISSUE_CONTRACT_CONFIG_INVALID';
249
+ throw error;
250
+ };
251
+ if (Array.isArray(loaded?.errors) && loaded.errors.length > 0) {
252
+ fail(loaded.errors.map(error => error.message || error.code || String(error)).join('; '));
253
+ }
254
+ const values = readIssueContractValues(loaded, fail);
255
+ if (values === null) return null;
256
+ if (values.enabled !== undefined && typeof values.enabled !== 'boolean') {
257
+ fail('issues.readiness.contracts.enabled must be a boolean');
258
+ }
259
+ if (values.enabled !== true) return { enabled: false, workClasses: [] };
260
+ if (!Array.isArray(values.workClasses) || values.workClasses.length === 0) {
261
+ fail('issues.readiness.contracts.workClasses must be a non-empty array');
262
+ }
263
+ const allowedClasses = new Set(ISSUE_TYPES);
264
+ if (values.workClasses.some(value => typeof value !== 'string' || !allowedClasses.has(value))) {
265
+ fail('issues.readiness.contracts.workClasses contains an unknown work class');
266
+ }
267
+ return {
268
+ enabled: true,
269
+ workClasses: [...new Set(values.workClasses)],
270
+ };
271
+ }
272
+
202
273
  async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
203
274
  const injectedService = deps.createService;
204
275
 
@@ -206,6 +277,7 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
206
277
  backend: createKernelIssueBackend({ projectRoot, deps }),
207
278
  }));
208
279
 
280
+ const contractPolicy = resolveIssueContractPolicy(projectRoot, deps);
209
281
  const service = createService();
210
282
  // Thread a distinct per-agent actor (and, when present, a stable session id) into the
211
283
  // mutation context so the Kernel's claim idempotency key is scoped per agent rather than
@@ -223,6 +295,9 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
223
295
  const isClaim = operation === 'claim';
224
296
  const worktreeId = isClaim ? resolveWorktreeId(projectRoot, actorEnv, deps) : undefined;
225
297
  const leaseTtlMs = isClaim ? resolveLeaseTtlMs(actorEnv) : undefined;
298
+ const isTrustedAdoption = typeof deps.verifyIssueContractAdoption === 'function'
299
+ ? deps.verifyIssueContractAdoption
300
+ : undefined;
226
301
  try {
227
302
  const result = await service.run(operation, rawArgs, {
228
303
  projectRoot,
@@ -231,6 +306,8 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
231
306
  ...(sessionId ? { sessionId } : {}),
232
307
  ...(worktreeId ? { worktreeId } : {}),
233
308
  ...(leaseTtlMs ? { leaseTtlMs } : {}),
309
+ ...(contractPolicy ? { contractPolicy } : {}),
310
+ ...(isTrustedAdoption ? { isTrustedAdoption } : {}),
234
311
  // Kernel mutations enqueue a projection-outbox "dirty marker" that `forge export`
235
312
  // (the D16 git-JSONL portability projection) drains under target 'jsonl'. The
236
313
  // broker's legacy primitive default is still 'beads', so without steering the target
@@ -267,4 +344,5 @@ module.exports = {
267
344
  runIssueOperation,
268
345
  isHelpInvocation,
269
346
  resolveIssueActor,
347
+ resolveIssueContractPolicy,
270
348
  };