forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4
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/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
|
@@ -68,6 +68,27 @@ function findLatestStageRun(driver, issueId, stage) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// (Re-)entering an earlier stage invalidates the work that followed it: any
|
|
72
|
+
// DOWNSTREAM stage previously recorded 'done' is reopened to 'active' so a gated
|
|
73
|
+
// stage (ship/review) re-requires a fresh completion (R2 — the dev<->validate
|
|
74
|
+
// rework loop must not let a stale validate=done pass ship). Reuses the idempotent
|
|
75
|
+
// 'start' write, which keeps the row's id + original started_at but clears its
|
|
76
|
+
// done status. Stages the entered stage does not precede (and non-'done' rows) are
|
|
77
|
+
// left untouched, so forward progression records nothing extra.
|
|
78
|
+
function invalidateDownstreamStages(driver, issueId, stageId, warn) {
|
|
79
|
+
const index = STAGE_PATH.indexOf(stageId);
|
|
80
|
+
if (index < 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (let next = index + 1; next < STAGE_PATH.length; next += 1) {
|
|
84
|
+
const downstream = STAGE_PATH[next];
|
|
85
|
+
const run = findLatestStageRun(driver, issueId, downstream);
|
|
86
|
+
if (run?.status === 'done') {
|
|
87
|
+
recordStageRunSafe(driver, issueId, downstream, 'start', warn);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
// Decide whether entering stageId is allowed given ONLY the kernel's recorded
|
|
72
93
|
// stage history (used when no inline/file workflow state exists):
|
|
73
94
|
// - Stateless stages (plan/dev/validate/verify) are always re-entrant, so the
|
|
@@ -123,7 +144,12 @@ async function resolveActiveIssueId(driver, branch) {
|
|
|
123
144
|
try {
|
|
124
145
|
if (typeof driver.listWorktrees === 'function') {
|
|
125
146
|
const rows = driver.listWorktrees() || [];
|
|
126
|
-
|
|
147
|
+
// Match only ACTIVE (live) linkage rows, newest first (listWorktrees orders
|
|
148
|
+
// registered_at DESC). A stale/superseded registration for a reused branch
|
|
149
|
+
// name must not rebind stage state to the OLD issue (be18881c). Tolerate a
|
|
150
|
+
// null state for rows written before the state column was populated.
|
|
151
|
+
const match = rows.find(row => row && row.branch === branch && row.issue_id
|
|
152
|
+
&& (row.state === 'active' || row.state == null));
|
|
127
153
|
if (match) {
|
|
128
154
|
return match.issue_id;
|
|
129
155
|
}
|
|
@@ -141,7 +167,7 @@ async function resolveActiveIssueId(driver, branch) {
|
|
|
141
167
|
|
|
142
168
|
// Lazily build a kernel driver from the project root (the real CLI path).
|
|
143
169
|
// Best-effort: returns null when the kernel is unavailable so the caller
|
|
144
|
-
// degrades to legacy file
|
|
170
|
+
// degrades to the legacy file state instead of crashing a stage command.
|
|
145
171
|
async function buildKernelDriver(projectRoot) {
|
|
146
172
|
if (!projectRoot) {
|
|
147
173
|
return null;
|
|
@@ -344,7 +370,7 @@ async function enforceStageEntry({
|
|
|
344
370
|
// B1 — kernel stage-state authority. Injectable for tests; the real CLI sets
|
|
345
371
|
// autoResolveKernel:true so the driver + active issue are resolved from the
|
|
346
372
|
// worktree. When neither a driver nor autoResolveKernel is provided, the
|
|
347
|
-
// kernel path is inert and behavior matches the legacy file
|
|
373
|
+
// kernel path is inert and behavior matches the legacy file state.
|
|
348
374
|
kernelDriver,
|
|
349
375
|
activeIssueId,
|
|
350
376
|
branch,
|
|
@@ -360,9 +386,9 @@ async function enforceStageEntry({
|
|
|
360
386
|
repairWorkflowRuntimeAssets(projectRoot);
|
|
361
387
|
}
|
|
362
388
|
|
|
363
|
-
// Resolve the active issue backend (env > .forge/config.yaml > default 'kernel')
|
|
364
|
-
//
|
|
365
|
-
//
|
|
389
|
+
// Resolve the active issue backend (env > .forge/config.yaml > default 'kernel').
|
|
390
|
+
// The kernel is the only backend and needs no bd, so bd is never a hard
|
|
391
|
+
// prerequisite and stages must run without it.
|
|
366
392
|
const issueBackend = resolveIssueBackend({ deps: {}, env: process.env, projectRoot, warn: () => {} });
|
|
367
393
|
const runtimeHealth = await resolveStageRuntimeHealth({
|
|
368
394
|
health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime,
|
|
@@ -391,6 +417,7 @@ async function enforceStageEntry({
|
|
|
391
417
|
const finish = (result) => {
|
|
392
418
|
if (kernelActive) {
|
|
393
419
|
recordStageRunSafe(driver, issueId, stageId, 'start', warn);
|
|
420
|
+
invalidateDownstreamStages(driver, issueId, stageId, warn);
|
|
394
421
|
result.recordCompletion = () => recordStageRunSafe(driver, issueId, stageId, 'complete', warn);
|
|
395
422
|
}
|
|
396
423
|
return result;
|
|
@@ -401,6 +428,14 @@ async function enforceStageEntry({
|
|
|
401
428
|
return enforceWithFileState(currentState, stageId, flags, args, finish);
|
|
402
429
|
}
|
|
403
430
|
|
|
431
|
+
// B1 — strict mode is the explicit opt-in to the legacy fail-closed behavior:
|
|
432
|
+
// require prior stages / authoritative state. The default (unset) degrades to a
|
|
433
|
+
// loud warning and seeds the kernel so future gating gets real data. A recorded
|
|
434
|
+
// history that CONTRADICTS (e.g. validate started-but-not-done) still throws in
|
|
435
|
+
// both modes — that rework protection lives in enforceWithKernelState below and
|
|
436
|
+
// is never weakened by this flag.
|
|
437
|
+
const strict = process.env.FORGE_STAGE_GATE === 'strict';
|
|
438
|
+
|
|
404
439
|
// No inline/file state: the kernel is authoritative. Gate on recorded stage
|
|
405
440
|
// completions (tolerant read) so `ship` is reachable from a pure-CLI
|
|
406
441
|
// plan->dev->validate progression with no .forge-state.json.
|
|
@@ -409,15 +444,37 @@ async function enforceStageEntry({
|
|
|
409
444
|
if (decided) {
|
|
410
445
|
return decided;
|
|
411
446
|
}
|
|
447
|
+
// enforceWithKernelState returned null → the kernel has an issue linked but
|
|
448
|
+
// NO recorded predecessor (a contradiction would have thrown above). Degrade
|
|
449
|
+
// to warn + seed unless strict: allow the stage and let finish() record it so
|
|
450
|
+
// future gating has real history.
|
|
451
|
+
if (!strict) {
|
|
452
|
+
warn(
|
|
453
|
+
`[forge] no recorded workflow history for issue ${issueId} — allowing '${stageId}' ` +
|
|
454
|
+
`and recording it in the kernel. Set FORGE_STAGE_GATE=strict to require prior stages.`
|
|
455
|
+
);
|
|
456
|
+
return finish({ allowed: true, stage: stageId, workflowState: null, degradedGate: 'kernel-empty' });
|
|
457
|
+
}
|
|
412
458
|
}
|
|
413
459
|
|
|
414
460
|
if (STATELESS_ENTRY_STAGES.has(stageId)) {
|
|
415
461
|
return finish({ allowed: true, stage: stageId, workflowState: null });
|
|
416
462
|
}
|
|
417
463
|
|
|
464
|
+
// No workflow state AND no kernel-linked issue for this branch. Degrade to warn
|
|
465
|
+
// unless strict so incremental adoption (fresh setup, in-flight branch, manual
|
|
466
|
+
// commit) is not blocked.
|
|
467
|
+
if (!strict) {
|
|
468
|
+
warn(
|
|
469
|
+
`[forge] no workflow state and no kernel-linked issue for this branch — stage gate skipped for '${stageId}'. ` +
|
|
470
|
+
`Link an issue with 'forge worktree create <slug>' to enable stage tracking.`
|
|
471
|
+
);
|
|
472
|
+
return finish({ allowed: true, stage: stageId, workflowState: null, degradedGate: 'no-kernel-context' });
|
|
473
|
+
}
|
|
474
|
+
|
|
418
475
|
throw new Error(
|
|
419
476
|
`Stage ${stageId} requires authoritative workflow state. ` +
|
|
420
|
-
`Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing.`
|
|
477
|
+
`Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing (or unset FORGE_STAGE_GATE).`
|
|
421
478
|
);
|
|
422
479
|
}
|
|
423
480
|
|
|
@@ -98,7 +98,7 @@ function extractWorkflowStateFromIssue(issue = {}) {
|
|
|
98
98
|
return extractWorkflowStateFromComments(mergedComments);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function
|
|
101
|
+
function readIssueWorkflowState(issueId, options = {}) {
|
|
102
102
|
if (!issueId) {
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
@@ -127,7 +127,7 @@ function readBeadsIssue(issueId, options = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function
|
|
130
|
+
function readWorkflowStateFromIssue(issueId, options = {}) {
|
|
131
131
|
if (!issueId) {
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
@@ -150,33 +150,33 @@ function readWorkflowStateFromBeads(issueId, options = {}) {
|
|
|
150
150
|
// history, so a single read covers both the structured issue and the raw
|
|
151
151
|
// comment text carrying the serialized WorkflowState marker. No separate
|
|
152
152
|
// comment-list read is required.
|
|
153
|
-
const issue =
|
|
153
|
+
const issue = readIssueWorkflowState(issueId, { projectRoot: options.projectRoot });
|
|
154
154
|
return extractWorkflowStateFromIssue(issue);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function
|
|
157
|
+
function loadStateFromIssue(options, projectRoot) {
|
|
158
158
|
if (options.comments) {
|
|
159
159
|
const state = extractWorkflowStateFromComments(options.comments);
|
|
160
160
|
if (state) {
|
|
161
|
-
return { state, source: '
|
|
161
|
+
return { state, source: 'issue' };
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
if (options.issue) {
|
|
166
166
|
const state = extractWorkflowStateFromIssue(options.issue);
|
|
167
167
|
if (state) {
|
|
168
|
-
return { state, source: '
|
|
168
|
+
return { state, source: 'issue' };
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (options.issueId) {
|
|
173
|
-
const state =
|
|
173
|
+
const state = readWorkflowStateFromIssue(options.issueId, {
|
|
174
174
|
comments: options.comments,
|
|
175
175
|
issue: options.issue,
|
|
176
176
|
projectRoot,
|
|
177
177
|
});
|
|
178
178
|
if (state) {
|
|
179
|
-
return { state, source: '
|
|
179
|
+
return { state, source: 'issue' };
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -185,21 +185,21 @@ function loadStateFromBeads(options, projectRoot) {
|
|
|
185
185
|
|
|
186
186
|
function loadState(projectRoot, options = {}) {
|
|
187
187
|
if (!projectRoot) {
|
|
188
|
-
const
|
|
189
|
-
return
|
|
188
|
+
const issueResult = loadStateFromIssue(options, null);
|
|
189
|
+
return issueResult || { state: null, source: null };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
if (options.
|
|
192
|
+
if (options.preferIssueLookup && (options.comments || options.issue || options.issueId)) {
|
|
193
193
|
try {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
return
|
|
194
|
+
const issueResult = loadStateFromIssue(options, projectRoot);
|
|
195
|
+
if (issueResult) {
|
|
196
|
+
return issueResult;
|
|
197
197
|
}
|
|
198
198
|
} catch (error) {
|
|
199
|
-
if (typeof options.
|
|
200
|
-
options.
|
|
199
|
+
if (typeof options.onIssueLookupError === 'function') {
|
|
200
|
+
options.onIssueLookupError(error);
|
|
201
201
|
}
|
|
202
|
-
// Fall through to the on-disk state file when
|
|
202
|
+
// Fall through to the on-disk state file when the issue cannot be read.
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -209,13 +209,13 @@ function loadState(projectRoot, options = {}) {
|
|
|
209
209
|
const raw = fs.readFileSync(statePath, 'utf8');
|
|
210
210
|
return { state: readWorkflowState(raw), source: 'file' };
|
|
211
211
|
} catch (_parseError) {
|
|
212
|
-
// File is malformed — fall through to
|
|
212
|
+
// File is malformed — fall through to the issue-lookup fallback
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
216
|
+
const issueResult = loadStateFromIssue(options, projectRoot);
|
|
217
|
+
if (issueResult) {
|
|
218
|
+
return issueResult;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
return { state: null, source: null };
|
|
@@ -331,8 +331,8 @@ module.exports = {
|
|
|
331
331
|
extractWorkflowStateFromIssue,
|
|
332
332
|
initializeState,
|
|
333
333
|
loadState,
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
readIssueWorkflowState,
|
|
335
|
+
readWorkflowStateFromIssue,
|
|
336
336
|
saveState,
|
|
337
337
|
transitionStage,
|
|
338
338
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-workflow",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "Local runtime control plane for AI-assisted engineering workflows, gates, evidence, and all AI agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forge": "bin/forge.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"globals": "^17.3.0",
|
|
60
60
|
"js-yaml": "^5.1.0",
|
|
61
61
|
"lefthook": "^2.1.4",
|
|
62
|
-
"typescript": "
|
|
62
|
+
"typescript": "7.0.2"
|
|
63
63
|
},
|
|
64
64
|
"keywords": [
|
|
65
65
|
"ai",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Always: before any response, if a Forge skill could apply, invoke it (or run forge skill for)"
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
globs: []
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Using Forge (skill dispatch — auto-trigger)
|
|
8
|
+
|
|
9
|
+
**Before ANY response or action** — including clarifying questions, exploring the
|
|
10
|
+
codebase, or checking files — if there is even a **1% chance** a Forge skill
|
|
11
|
+
applies to what you are doing, invoke that skill first. Then announce
|
|
12
|
+
`Using [skill] to [purpose]` and follow it.
|
|
13
|
+
|
|
14
|
+
- Not sure which skill fits? Run `forge skill for "<what you are about to do>"`
|
|
15
|
+
for the deterministic best-fit, or read the routing table in the dispatch skill.
|
|
16
|
+
- Common routes: build/scope a feature -> plan; implement a task -> dev; fix a
|
|
17
|
+
failing test -> dev (debug first); run checks/lint/tests -> validate; open a PR
|
|
18
|
+
-> ship; address review feedback -> review; where am I -> status.
|
|
19
|
+
|
|
20
|
+
This rule is a **thin pointer**. The full 1%-rule, red-flags table, subagent
|
|
21
|
+
escape hatch, and routing table live in the **`using-forge` dispatch skill**,
|
|
22
|
+
installed into your agent's own skill surface by `forge setup` (for Cursor:
|
|
23
|
+
`.cursor/skills/using-forge/SKILL.md`) — invoke it by name, or run
|
|
24
|
+
`forge skill for "<situation>"`. Do not duplicate that policy here.
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
# Subcommands:
|
|
5
5
|
# workload Show team workload by developer
|
|
6
6
|
# epic Epic progress rollup
|
|
7
|
-
# dashboard Team health dashboard
|
|
8
7
|
# add Add developer to team map
|
|
9
8
|
# verify Check 1:1 Beads<>GitHub enforcement
|
|
10
9
|
# sync Manual GitHub<>Beads sync
|
|
@@ -22,7 +21,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
22
21
|
[[ -f "$SCRIPT_DIR/lib/identity.sh" ]] && source "$SCRIPT_DIR/lib/identity.sh"
|
|
23
22
|
[[ -f "$SCRIPT_DIR/lib/workload.sh" ]] && source "$SCRIPT_DIR/lib/workload.sh"
|
|
24
23
|
[[ -f "$SCRIPT_DIR/lib/epic.sh" ]] && source "$SCRIPT_DIR/lib/epic.sh"
|
|
25
|
-
[[ -f "$SCRIPT_DIR/lib/dashboard.sh" ]] && source "$SCRIPT_DIR/lib/dashboard.sh"
|
|
26
24
|
[[ -f "$SCRIPT_DIR/lib/hooks.sh" ]] && source "$SCRIPT_DIR/lib/hooks.sh"
|
|
27
25
|
[[ -f "$SCRIPT_DIR/lib/verify.sh" ]] && source "$SCRIPT_DIR/lib/verify.sh"
|
|
28
26
|
[[ -f "$SCRIPT_DIR/lib/claim.sh" ]] && source "$SCRIPT_DIR/lib/claim.sh"
|
|
@@ -34,7 +32,6 @@ FORGE_SCRIPTS="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
34
32
|
# ── Stub implementations ──
|
|
35
33
|
# cmd_workload provided by lib/workload.sh
|
|
36
34
|
# cmd_epic provided by lib/epic.sh
|
|
37
|
-
# cmd_dashboard provided by lib/dashboard.sh
|
|
38
35
|
cmd_add() { auto_detect_identity "$@"; }
|
|
39
36
|
# cmd_verify provided by lib/verify.sh
|
|
40
37
|
cmd_sync() { forge_team_sync "$@"; }
|
|
@@ -47,7 +44,6 @@ Usage: forge team <subcommand> [args...]
|
|
|
47
44
|
Subcommands:
|
|
48
45
|
workload [--developer=<user>] [--me] Show team workload
|
|
49
46
|
epic <issue-id> Epic progress rollup
|
|
50
|
-
dashboard Team health dashboard
|
|
51
47
|
add [--github=<user>] Add developer to team map
|
|
52
48
|
verify Check 1:1 Beads<>GitHub sync
|
|
53
49
|
sync Manual GitHub<>Beads sync
|
|
@@ -69,7 +65,6 @@ main() {
|
|
|
69
65
|
case "$subcommand" in
|
|
70
66
|
workload) cmd_workload "$@" ;;
|
|
71
67
|
epic) cmd_epic "$@" ;;
|
|
72
|
-
dashboard) cmd_dashboard "$@" ;;
|
|
73
68
|
add) cmd_add "$@" ;;
|
|
74
69
|
verify) cmd_verify "$@" ;;
|
|
75
70
|
sync) cmd_sync "$@" ;;
|
|
@@ -60,7 +60,7 @@ assert_output_contains "unknown subcommand shows error" "unknown subcommand" bas
|
|
|
60
60
|
# Test 4: Each stub subcommand is reachable (exits 0)
|
|
61
61
|
echo ""
|
|
62
62
|
echo "-- stub subcommands reachable --"
|
|
63
|
-
for cmd in workload epic
|
|
63
|
+
for cmd in workload epic add verify sync claim; do
|
|
64
64
|
assert_exit "$cmd exits 0" 0 bash "$DISPATCHER" "$cmd"
|
|
65
65
|
assert_output_contains "$cmd outputs stub message" "not implemented" bash "$DISPATCHER" "$cmd"
|
|
66
66
|
done
|
|
@@ -17,7 +17,6 @@ assert_contains_file() {
|
|
|
17
17
|
echo "── /status integration ──"
|
|
18
18
|
STATUS="$SCRIPT_DIR/.claude/commands/status.md"
|
|
19
19
|
assert_contains_file "status calls workload --me" "forge-team.*workload.*--me" "$STATUS"
|
|
20
|
-
assert_contains_file "status calls dashboard" "forge-team.*dashboard" "$STATUS"
|
|
21
20
|
|
|
22
21
|
echo ""
|
|
23
22
|
echo "── /plan integration ──"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* pr-auto-actions — the pr-monitor workflow's bridge from the `--pull --json`
|
|
6
|
+
* verdict payload to Tier-2 auto-action DECISIONS. Reads the payload file the
|
|
7
|
+
* monitor already wrote (default `pull.json`), asks lib/pr-monitor/auto-actions
|
|
8
|
+
* (the single, unit-tested decision core) what is safe to do, and emits the
|
|
9
|
+
* flags to `$GITHUB_OUTPUT` (or stdout locally) for the workflow's action steps.
|
|
10
|
+
*
|
|
11
|
+
* This script performs NO GitHub writes and takes NO action — it only decides.
|
|
12
|
+
* The workflow owns the visible `gh` calls and the per-head-SHA idempotency
|
|
13
|
+
* markers. Fails CLOSED: an unreadable/malformed payload or any thrown error
|
|
14
|
+
* yields `update_branch=false` / `rerun=false`.
|
|
15
|
+
*
|
|
16
|
+
* Usage: node scripts/pr-auto-actions.js [pull.json] [isFork]
|
|
17
|
+
* isFork: "true" when the PR head is a cross-repository fork (skips update).
|
|
18
|
+
*
|
|
19
|
+
* @module scripts/pr-auto-actions
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
|
|
24
|
+
const { decideAutoActions } = require('../lib/pr-monitor/auto-actions');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Append `key=value` lines to `$GITHUB_OUTPUT` when set, else print them. Values
|
|
28
|
+
* are single-line (booleans / csv / short reasons) so no multiline escaping is
|
|
29
|
+
* needed; newlines are stripped defensively.
|
|
30
|
+
*
|
|
31
|
+
* @param {Record<string,string>} outputs
|
|
32
|
+
*/
|
|
33
|
+
function emitOutputs(outputs) {
|
|
34
|
+
const target = process.env.GITHUB_OUTPUT;
|
|
35
|
+
const text = Object.entries(outputs)
|
|
36
|
+
.map(([k, val]) => `${k}=${String(val).replace(/[\r\n]+/g, ' ')}`)
|
|
37
|
+
.join('\n') + '\n';
|
|
38
|
+
if (target) fs.appendFileSync(target, text);
|
|
39
|
+
else process.stdout.write(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read + parse the payload file, returning null (fail-closed) on any error. */
|
|
43
|
+
function readPayload(path) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function main(argv) {
|
|
52
|
+
const path = argv[2] || 'pull.json';
|
|
53
|
+
const isFork = String(argv[3] || '').toLowerCase() === 'true';
|
|
54
|
+
|
|
55
|
+
const payload = readPayload(path);
|
|
56
|
+
if (!payload) {
|
|
57
|
+
// Fail closed: no readable verdict payload → take no action.
|
|
58
|
+
emitOutputs({
|
|
59
|
+
update_branch: 'false', update_reason: `no readable payload at ${path}`,
|
|
60
|
+
rerun: 'false', rerun_run_ids: '', rerun_reason: `no readable payload at ${path}`,
|
|
61
|
+
});
|
|
62
|
+
console.log(`auto-actions: no readable payload at ${path} — no action (fail closed)`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { updateBranch, rerunFlaky } = decideAutoActions(payload, { isFork });
|
|
67
|
+
emitOutputs({
|
|
68
|
+
update_branch: updateBranch.should ? 'true' : 'false',
|
|
69
|
+
update_reason: updateBranch.reason,
|
|
70
|
+
rerun: rerunFlaky.should ? 'true' : 'false',
|
|
71
|
+
rerun_run_ids: (rerunFlaky.runIds || []).join(','),
|
|
72
|
+
rerun_reason: rerunFlaky.reason,
|
|
73
|
+
});
|
|
74
|
+
console.log(`auto-actions: update_branch=${updateBranch.should} (${updateBranch.reason})`);
|
|
75
|
+
console.log(`auto-actions: rerun=${rerunFlaky.should} (${rerunFlaky.reason})`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (require.main === module) {
|
|
80
|
+
try {
|
|
81
|
+
process.exit(main(process.argv));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// Absolute fail-closed backstop: never let this script red-X the monitor.
|
|
84
|
+
emitOutputs({
|
|
85
|
+
update_branch: 'false', update_reason: `error: ${(err && err.message) || err}`,
|
|
86
|
+
rerun: 'false', rerun_run_ids: '', rerun_reason: `error: ${(err && err.message) || err}`,
|
|
87
|
+
});
|
|
88
|
+
console.log(`auto-actions: error — no action (fail closed): ${(err && err.message) || err}`);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { emitOutputs, readPayload, main };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* pr-verdict label emitter — maps a canonical merge verdict (from
|
|
6
|
+
* `forge shepherd <pr> --pull --json`, lib/pr-pull.js) to its single
|
|
7
|
+
* `pr-verdict:*` label and emits `label` + `all_labels` to `$GITHUB_OUTPUT`
|
|
8
|
+
* (or stdout locally) for the pr-monitor workflow's reconcile step.
|
|
9
|
+
*
|
|
10
|
+
* SINGLE SOURCE: the verdict value and the label vocabulary both come from
|
|
11
|
+
* lib/pr-pull.js — this script computes NO verdict of its own, so the label can
|
|
12
|
+
* never disagree with `--pull`. Fails closed to `unknown` on empty input.
|
|
13
|
+
* Performs NO GitHub writes (the workflow owns the visible `gh` calls).
|
|
14
|
+
*
|
|
15
|
+
* Usage: node scripts/pr-verdict-label.js <VERDICT>
|
|
16
|
+
*
|
|
17
|
+
* @module scripts/pr-verdict-label
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
|
|
22
|
+
const { verdictLabel, VERDICT_LABELS } = require('../lib/pr-pull');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Append `key=value` lines to `$GITHUB_OUTPUT` when set, else print them.
|
|
26
|
+
*
|
|
27
|
+
* @param {Record<string,string>} outputs
|
|
28
|
+
*/
|
|
29
|
+
function emitOutputs(outputs) {
|
|
30
|
+
const target = process.env.GITHUB_OUTPUT;
|
|
31
|
+
const text = Object.entries(outputs).map(([k, val]) => `${k}=${val}`).join('\n') + '\n';
|
|
32
|
+
if (target) fs.appendFileSync(target, text);
|
|
33
|
+
else process.stdout.write(text);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function main(argv) {
|
|
37
|
+
// Fail closed: a missing/empty verdict arg maps to the `unknown` label rather
|
|
38
|
+
// than erroring — the workflow must still land a label on every pass.
|
|
39
|
+
const verdict = argv[2] || 'UNKNOWN';
|
|
40
|
+
const label = verdictLabel(verdict);
|
|
41
|
+
emitOutputs({ label, all_labels: VERDICT_LABELS.join(',') });
|
|
42
|
+
console.log(`pr-verdict label: ${label} (verdict ${verdict})`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (require.main === module) {
|
|
47
|
+
process.exit(main(process.argv));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { emitOutputs, main };
|
package/scripts/test.js
CHANGED
|
@@ -156,6 +156,8 @@ function isKnownTargetablePath(file) {
|
|
|
156
156
|
|
|
157
157
|
if (file === 'README.md'
|
|
158
158
|
|| file === 'bin/forge.js'
|
|
159
|
+
|| file === 'bin/forge-cmd.js'
|
|
160
|
+
|| file === 'bin/forge-preflight.js'
|
|
159
161
|
|| file === 'QUICKSTART.md'
|
|
160
162
|
|| file === 'CHANGELOG.md'
|
|
161
163
|
|| file === 'AGENTS.md'
|
|
@@ -166,7 +168,12 @@ function isKnownTargetablePath(file) {
|
|
|
166
168
|
|| file.startsWith('docs/guides/')
|
|
167
169
|
|| file.startsWith('docs/plans/')
|
|
168
170
|
|| file.startsWith('docs/reference/')
|
|
169
|
-
|| file.startsWith('docs/work/')
|
|
171
|
+
|| file.startsWith('docs/work/')
|
|
172
|
+
// Skill sources + their committed mirror map to the skill suite (see
|
|
173
|
+
// SKILL_TEST_TARGETS in lib/commands/test.js); a skills-only PR stays on the
|
|
174
|
+
// targeted lane instead of the full suite.
|
|
175
|
+
|| file.startsWith('skills/')
|
|
176
|
+
|| file.startsWith('.agents/skills/')) {
|
|
170
177
|
return true;
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -14,10 +14,14 @@ description: >
|
|
|
14
14
|
and NOT for read-only selecting or ranking the next ready issue without claiming (that is
|
|
15
15
|
triage-ready).
|
|
16
16
|
allowed-tools: Read, Bash(forge:*)
|
|
17
|
+
next: dev
|
|
18
|
+
terminal: false
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
# Claim safety — claim, then prove you own the lease
|
|
20
22
|
|
|
23
|
+
> **Chain:** once you have PROVEN the lease (`forge issue owns <id>` exits 0), continue to `dev` to work the issue (`smith` proceeds into plan/dev after this proof). Usable standalone whenever ownership is in question.
|
|
24
|
+
|
|
21
25
|
Claiming is not owning. The Forge kernel keys a claim's idempotency on
|
|
22
26
|
`claim.create:<issue_id>:<actor>`, so a **same-key duplicate replay returns
|
|
23
27
|
`ok:true`** echoing the *current* call's `claim_id`. A genuine cross-actor
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "claim-safety",
|
|
3
|
+
"fixtures": "present",
|
|
4
|
+
"static": {
|
|
5
|
+
"token_cost": {
|
|
6
|
+
"desc_chars": 995,
|
|
7
|
+
"body_lines": 88,
|
|
8
|
+
"score": 43
|
|
9
|
+
},
|
|
10
|
+
"caps": {
|
|
11
|
+
"desc_within": true,
|
|
12
|
+
"body_within": true,
|
|
13
|
+
"score": 100
|
|
14
|
+
},
|
|
15
|
+
"description_quality": {
|
|
16
|
+
"has_trigger_cues": true,
|
|
17
|
+
"has_disambiguation_cues": true,
|
|
18
|
+
"adequate_length": true,
|
|
19
|
+
"score": 100
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"router_reachability": {
|
|
23
|
+
"has_curated_rule": true,
|
|
24
|
+
"router_exempt": false,
|
|
25
|
+
"fixtures": "present",
|
|
26
|
+
"fixtures_total": 6,
|
|
27
|
+
"fixtures_best_hit": 2,
|
|
28
|
+
"reachable": true,
|
|
29
|
+
"keyword_alignment": 0.33
|
|
30
|
+
},
|
|
31
|
+
"behavioral": {
|
|
32
|
+
"trigger_recall": null,
|
|
33
|
+
"trigger_precision": null,
|
|
34
|
+
"disambiguation": null,
|
|
35
|
+
"chain_correctness": null,
|
|
36
|
+
"outcome_quality": null,
|
|
37
|
+
"variance": null,
|
|
38
|
+
"note": "behavioral — W5 (LLM judge): semantic recall/precision/chain/outcome/variance, not deterministic"
|
|
39
|
+
},
|
|
40
|
+
"composite": 83
|
|
41
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"description": "Command -> owning-skill coverage map (plan 2026-07-20-next-phase-plan.md §3.3). Every registered user-facing command MUST map to an existing skill OR carry an explicit { exempt: <reason> }. Enforced by the coverage gate (lib/skill-eval.js evaluateCoverage, surfaced by `forge skill coverage`/`forge skill scores`, gated in test/skill-eval.test.js). Exempts reference the pending skill from plan §3.2 and are tightened as those skills land.",
|
|
4
|
+
"commands": {
|
|
5
|
+
"plan": "plan",
|
|
6
|
+
"dev": "dev",
|
|
7
|
+
"validate": "validate",
|
|
8
|
+
"ship": "ship",
|
|
9
|
+
"shepherd": "shepherd",
|
|
10
|
+
"status": "status",
|
|
11
|
+
"prime": "status",
|
|
12
|
+
"orient": "hermes-forge",
|
|
13
|
+
"recap": "hermes-forge",
|
|
14
|
+
"remember": "memory",
|
|
15
|
+
"recall": "memory",
|
|
16
|
+
"memory": "memory",
|
|
17
|
+
"ready": "triage-ready",
|
|
18
|
+
"blocked": "triage-ready",
|
|
19
|
+
"claim": "claim-safety",
|
|
20
|
+
"claims": "claim-safety",
|
|
21
|
+
"issue": "issue-basics",
|
|
22
|
+
"issues": "issue-basics",
|
|
23
|
+
"create": "issue-basics",
|
|
24
|
+
"close": "issue-basics",
|
|
25
|
+
"comment": "issue-basics",
|
|
26
|
+
"show": "issue-basics",
|
|
27
|
+
"update": "issue-basics",
|
|
28
|
+
"list": "issue-basics",
|
|
29
|
+
"skill": "using-forge",
|
|
30
|
+
|
|
31
|
+
"worktree": "worktree",
|
|
32
|
+
"clean": "worktree",
|
|
33
|
+
"orphans": { "exempt": "full ownership (command-name routing + `forge issue orphans` guidance in the issue-basics body) pending — issue e1e33cf8; issue-basics does not yet document it, so mapping now would be a hollow claim" },
|
|
34
|
+
"rollback": "rollback",
|
|
35
|
+
"review": "review",
|
|
36
|
+
"verify": "verify",
|
|
37
|
+
"check": { "exempt": "legacy alias for `validate` (renamed in PR #50) — bin/forge-cmd.js VALID_COMMANDS only; owned by the `validate` skill" },
|
|
38
|
+
"docs": { "exempt": "bin/forge.js-direct; AGENTS.md/docs management — owning skill pending (coverage pass, plan §3.2)" },
|
|
39
|
+
"reset": "setup",
|
|
40
|
+
"reinstall": "setup",
|
|
41
|
+
|
|
42
|
+
"gate": "gates",
|
|
43
|
+
"control": "gates",
|
|
44
|
+
"doc-gate": "gates",
|
|
45
|
+
|
|
46
|
+
"adapter": { "exempt": "extensions skill pending (plan §3.2 B: new `extensions` skill)" },
|
|
47
|
+
"add": { "exempt": "extensions skill pending (plan §3.2 B: new `extensions` skill)" },
|
|
48
|
+
"new": { "exempt": "extensions skill pending (plan §3.2 B: new `extensions` skill)" },
|
|
49
|
+
"audit": { "exempt": "extensions skill pending (plan §3.2 B: new `extensions` skill)" },
|
|
50
|
+
"role": { "exempt": "extensions skill pending — role/skill binding (plan §3.2 B)" },
|
|
51
|
+
|
|
52
|
+
"export": "portability",
|
|
53
|
+
"migrate": "portability",
|
|
54
|
+
"sync": { "exempt": "cloud-native external-platform sync (ADR-0004); a local no-op with no value until a server backend is configured — skill deferred (issue 666f4b4c)" },
|
|
55
|
+
|
|
56
|
+
"inbox": { "exempt": "team skill pending (plan §3.2 B: new `team` skill)" },
|
|
57
|
+
"serve": { "exempt": "team skill pending (plan §3.2 B: new `team` skill)" },
|
|
58
|
+
"team": { "exempt": "team skill pending (plan §3.2 B: new `team` skill)" },
|
|
59
|
+
|
|
60
|
+
"setup": "setup",
|
|
61
|
+
"init": "setup",
|
|
62
|
+
"upgrade": "setup",
|
|
63
|
+
"doctor": "setup",
|
|
64
|
+
"hooks": "setup",
|
|
65
|
+
"recommend": "setup",
|
|
66
|
+
|
|
67
|
+
"push": { "exempt": "ship skill push/preflight extension pending (plan §3.2 B)" },
|
|
68
|
+
"preflight": { "exempt": "ship skill push/preflight extension pending (plan §3.2 B)" },
|
|
69
|
+
"pr": { "exempt": "ship/shepherd PR-lifecycle extension pending (plan §3.2)" },
|
|
70
|
+
"merge": { "exempt": "ship/shepherd PR-lifecycle extension pending (plan §3.2)" },
|
|
71
|
+
|
|
72
|
+
"patch": { "exempt": "kernel skill fold pending (plan §3.2 C: fold into `kernel`)" },
|
|
73
|
+
"stage": { "exempt": "kernel skill fold pending (plan §3.2 C: fold into `kernel`)" },
|
|
74
|
+
"explain": { "exempt": "kernel skill fold pending (plan §3.2 C: fold into `kernel`)" },
|
|
75
|
+
"options": { "exempt": "kernel skill fold pending (plan §3.2 C: fold into `kernel`)" },
|
|
76
|
+
"insights": { "exempt": "kernel skill fold pending — kernel evidence inspection (plan §3.2 C)" },
|
|
77
|
+
|
|
78
|
+
"lint": { "exempt": "internal quality plumbing; skill assignment pending (coverage pass)" },
|
|
79
|
+
"release": { "exempt": "internal release plumbing; skill assignment pending (coverage pass)" },
|
|
80
|
+
"test": { "exempt": "internal test-runner plumbing; skill assignment pending (coverage pass)" },
|
|
81
|
+
"stale": { "exempt": "internal stale-detection plumbing; skill assignment pending (coverage pass)" }
|
|
82
|
+
}
|
|
83
|
+
}
|