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.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +108 -6
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +8 -4
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +95 -14
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
package/lib/commands/worktree.js
CHANGED
|
@@ -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
|
|
114
|
-
* they may legitimately be absent.
|
|
115
|
-
*
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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((
|
|
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
|
-
//
|
|
242
|
-
|
|
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 @@
|
|
|
1
|
+
{"issue_id":"synthetic-issue-1","blocks_issue_id":"synthetic-issue-2"}
|
|
@@ -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');
|
package/lib/forge-issues.js
CHANGED
|
@@ -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
|
};
|