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.
Files changed (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. 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
- const match = rows.find(row => row && row.branch === branch && row.issue_id);
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/beads state instead of crashing a stage command.
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/beads state.
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') so
364
- // the runtime gate only treats bd as a hard prerequisite for the beads backend. The
365
- // kernel default needs no bd, so stages must run without it.
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 readBeadsIssue(issueId, options = {}) {
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 readWorkflowStateFromBeads(issueId, options = {}) {
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 = readBeadsIssue(issueId, { projectRoot: options.projectRoot });
153
+ const issue = readIssueWorkflowState(issueId, { projectRoot: options.projectRoot });
154
154
  return extractWorkflowStateFromIssue(issue);
155
155
  }
156
156
 
157
- function loadStateFromBeads(options, projectRoot) {
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: 'beads' };
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: 'beads' };
168
+ return { state, source: 'issue' };
169
169
  }
170
170
  }
171
171
 
172
172
  if (options.issueId) {
173
- const state = readWorkflowStateFromBeads(options.issueId, {
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: 'beads' };
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 beadsResult = loadStateFromBeads(options, null);
189
- return beadsResult || { state: null, source: null };
188
+ const issueResult = loadStateFromIssue(options, null);
189
+ return issueResult || { state: null, source: null };
190
190
  }
191
191
 
192
- if (options.preferBeads && (options.comments || options.issue || options.issueId)) {
192
+ if (options.preferIssueLookup && (options.comments || options.issue || options.issueId)) {
193
193
  try {
194
- const beadsResult = loadStateFromBeads(options, projectRoot);
195
- if (beadsResult) {
196
- return beadsResult;
194
+ const issueResult = loadStateFromIssue(options, projectRoot);
195
+ if (issueResult) {
196
+ return issueResult;
197
197
  }
198
198
  } catch (error) {
199
- if (typeof options.onBeadsError === 'function') {
200
- options.onBeadsError(error);
199
+ if (typeof options.onIssueLookupError === 'function') {
200
+ options.onIssueLookupError(error);
201
201
  }
202
- // Fall through to the on-disk state file when Beads cannot be read.
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 Beads fallback
212
+ // File is malformed — fall through to the issue-lookup fallback
213
213
  }
214
214
  }
215
215
 
216
- const beadsResult = loadStateFromBeads(options, projectRoot);
217
- if (beadsResult) {
218
- return beadsResult;
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
- readBeadsIssue,
335
- readWorkflowStateFromBeads,
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.2",
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": "5.4.5"
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 dashboard add verify sync claim; do
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
+ }