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

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 (268) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +120 -9
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +78 -22
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/validation-receipt.js +190 -0
  107. package/lib/workflow/enforce-stage.js +44 -0
  108. package/lib/workflow/plan-authority.js +225 -0
  109. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  110. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  111. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  112. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  113. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  114. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  115. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  116. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  117. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  118. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  119. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  120. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  121. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  122. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  123. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  124. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  125. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  126. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  127. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  128. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  129. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  130. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  131. package/node_modules/@forge/contracts/index.js +32 -0
  132. package/node_modules/@forge/contracts/package.json +35 -0
  133. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  134. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  135. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  136. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  137. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  138. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  139. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  140. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  141. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  142. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  143. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  144. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  145. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  146. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  147. package/node_modules/@forge/contracts/src/identity.js +42 -0
  148. package/node_modules/@forge/contracts/src/schema.js +72 -0
  149. package/node_modules/@forge/contracts/src/validate.js +305 -0
  150. package/node_modules/@forge/flow/index.js +114 -0
  151. package/node_modules/@forge/flow/package.json +34 -0
  152. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  153. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  154. package/node_modules/@forge/flow/src/executor.js +279 -0
  155. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  156. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  157. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  158. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  159. package/node_modules/@forge/memory/index.js +331 -0
  160. package/node_modules/@forge/memory/package.json +34 -0
  161. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  162. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  163. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  164. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  165. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  166. package/package.json +16 -4
  167. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  168. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  169. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  170. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  171. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  172. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  173. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  174. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  175. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  176. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  177. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  178. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  179. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  180. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  181. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  182. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  183. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  184. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  185. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  186. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  187. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  188. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  189. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  190. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  191. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  192. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  193. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  194. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  195. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  196. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  197. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  198. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  199. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  200. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  201. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  202. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  203. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  204. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  205. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  206. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  207. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  208. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  209. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  210. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  211. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  212. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  213. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  214. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  215. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  216. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  217. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  218. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  219. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  220. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  221. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  222. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  223. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  224. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  225. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  226. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  227. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  228. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  229. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  230. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  231. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  232. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  233. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  234. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  235. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  236. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  237. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  238. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  239. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  240. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  241. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  242. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  243. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  244. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  245. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  246. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  247. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  248. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  249. package/scripts/commitlint.js +13 -15
  250. package/scripts/generate-risk-manifest.js +91 -0
  251. package/scripts/github-context-bridge.sh +10 -0
  252. package/scripts/legacy-claim-repair.js +145 -0
  253. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  254. package/scripts/process-tree.js +14 -2
  255. package/scripts/protected-state-check.js +440 -17
  256. package/scripts/sync-agent-skills.js +333 -34
  257. package/scripts/test-full-suite.js +704 -18
  258. package/scripts/test-profile.js +13 -3
  259. package/scripts/test.js +96 -15
  260. package/skills/coverage.json +1 -0
  261. package/skills/review/SKILL.md +2 -0
  262. package/skills/review/evals/scorecard.json +2 -2
  263. package/skills/setup/SKILL.md +18 -0
  264. package/skills/setup/evals/scorecard.json +3 -3
  265. package/skills/shepherd/SKILL.md +19 -2
  266. package/skills/shepherd/evals/scorecard.json +3 -3
  267. package/skills/validate/SKILL.md +3 -0
  268. package/skills/validate/evals/scorecard.json +1 -1
@@ -0,0 +1,347 @@
1
+ 'use strict';
2
+
3
+ const { createProbeResult } = require('./model');
4
+
5
+ const PROBE_REVISION = 'forge.harness-capability-probe.v1';
6
+ const DEFAULT_TIMEOUT_MS = 1500;
7
+ const DEFAULT_MAX_OUTPUT_BYTES = 16 * 1024;
8
+ const MAX_TIMEOUT_MS = 30 * 1000;
9
+ const MAX_OUTPUT_BYTES = 64 * 1024;
10
+ const EXECUTABLE_IDENTITY_PATTERN = /^[0-9a-f]{64}$/;
11
+ const SECRET_PATTERNS = Object.freeze([
12
+ /gh[pousr]_[a-z0-9]{20,}/i,
13
+ /sk-(?:live-|test-)?[a-z0-9]{16,}/i,
14
+ /(?:api[_ -]?key|authorization|credential|password|private[_ -]?key|secret|token)\s*[:=]\s*\S{8,}/i,
15
+ ]);
16
+ const PRIVATE_PATH_PATTERN = /(?:[a-z]:\\Users\\[^\\\s]+|\/(?:Users|home)\/[^/\s]+\/)/i;
17
+
18
+ const HARNESS_IDS = Object.freeze(['claude', 'codex', 'cursor', 'hermes']);
19
+ const CAPABILITY_IDS = Object.freeze([
20
+ 'native_streaming_monitor',
21
+ 'bounded_background_tasks',
22
+ 'wake_resume',
23
+ 'cancellation',
24
+ 'session_scoped_cleanup',
25
+ 'initiating_agent_delivery',
26
+ ]);
27
+
28
+ function capability(id, probeId, args, requiredTokens) {
29
+ if (args.length === 0 || args.at(-1) !== '--help') {
30
+ throw new TypeError(`Capability probe ${probeId} must use a side-effect-free help command`);
31
+ }
32
+ return Object.freeze({
33
+ id,
34
+ probeId,
35
+ args: Object.freeze(args),
36
+ requiredTokens: Object.freeze(requiredTokens),
37
+ });
38
+ }
39
+
40
+ function harnessSpec(command, versionPattern, capabilities) {
41
+ return Object.freeze({
42
+ command,
43
+ versionArgs: Object.freeze(['--version']),
44
+ versionPattern,
45
+ capabilities: Object.freeze(capabilities),
46
+ });
47
+ }
48
+
49
+ const HARNESS_PROBE_SPECS = Object.freeze({
50
+ claude: harnessSpec('claude', /^(?:claude(?:\s+code)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)?)$/i, [
51
+ capability('native_streaming_monitor', 'claude.stream-json-help.v1', ['--help'], ['--output-format', 'stream-json']),
52
+ capability('bounded_background_tasks', 'claude.remote-help.v1', ['--help'], ['--remote']),
53
+ capability('wake_resume', 'claude.resume-help.v1', ['--help'], ['--resume']),
54
+ capability('cancellation', 'claude.cancel-help.v1', ['--help'], ['--cancel']),
55
+ capability('session_scoped_cleanup', 'claude.session-cleanup-help.v1', ['--help'], ['session', 'delete']),
56
+ capability('initiating_agent_delivery', 'claude.stream-delivery-help.v1', ['--help'], ['--output-format', 'stream-json']),
57
+ ]),
58
+ codex: harnessSpec('codex', /^(?:codex(?:-cli)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)?)$/i, [
59
+ capability('native_streaming_monitor', 'codex.json-help.v1', ['--help'], ['--json', 'output-schema']),
60
+ capability('bounded_background_tasks', 'codex.cloud-help.v1', ['cloud', '--help'], ['task']),
61
+ capability('wake_resume', 'codex.resume-help.v1', ['resume', '--help'], ['resume']),
62
+ capability('cancellation', 'codex.cloud-cancel-help.v1', ['cloud', '--help'], ['cancel']),
63
+ capability('session_scoped_cleanup', 'codex.cloud-cleanup-help.v1', ['cloud', '--help'], ['delete']),
64
+ capability('initiating_agent_delivery', 'codex.delivery-help.v1', ['--help'], ['event', 'initiating', 'agent']),
65
+ ]),
66
+ cursor: harnessSpec('cursor-agent', /^(?:cursor(?:-agent)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)?)$/i, [
67
+ capability('native_streaming_monitor', 'cursor.json-help.v1', ['--help'], ['--json']),
68
+ capability('bounded_background_tasks', 'cursor.background-help.v1', ['--help'], ['background']),
69
+ capability('wake_resume', 'cursor.resume-help.v1', ['resume', '--help'], ['resume']),
70
+ capability('cancellation', 'cursor.cancel-help.v1', ['cancel', '--help'], ['cancel']),
71
+ capability('session_scoped_cleanup', 'cursor.session-cleanup-help.v1', ['sessions', '--help'], ['sessions', 'delete']),
72
+ capability('initiating_agent_delivery', 'cursor.follow-up-help.v1', ['--help'], ['follow-up', 'initiating', 'agent']),
73
+ ]),
74
+ hermes: harnessSpec('hermes', /^(?:hermes(?:-agent)?\s+)?v?(\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)?)$/i, [
75
+ capability('native_streaming_monitor', 'hermes.monitor-help.v1', ['monitor', '--help'], ['monitor', 'stream']),
76
+ capability('bounded_background_tasks', 'hermes.cron-help.v1', ['cron', '--help'], ['cron', 'task']),
77
+ capability('wake_resume', 'hermes.resume-help.v1', ['resume', '--help'], ['resume']),
78
+ capability('cancellation', 'hermes.cron-cancel-help.v1', ['cron', '--help'], ['pause', 'remove']),
79
+ capability('session_scoped_cleanup', 'hermes.session-cleanup-help.v1', ['sessions', '--help'], ['sessions', 'delete']),
80
+ capability('initiating_agent_delivery', 'hermes.origin-delivery-help.v1', ['cron', '--help'], ['origin', 'deliver']),
81
+ ]),
82
+ });
83
+
84
+ function getHarnessProbeSpec(harness) {
85
+ const spec = HARNESS_PROBE_SPECS[harness];
86
+ if (!spec) throw new TypeError(`Unsupported harness: ${harness}`);
87
+ return spec;
88
+ }
89
+
90
+ function assertBoundedPositiveInteger(value, name, maximum) {
91
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
92
+ throw new TypeError(`${name} must be an integer between 1 and ${maximum}`);
93
+ }
94
+ }
95
+
96
+ function isHostileOutput(output) {
97
+ return SECRET_PATTERNS.some((pattern) => pattern.test(output))
98
+ || PRIVATE_PATH_PATTERN.test(output)
99
+ || [...output].some((character) => {
100
+ const code = character.codePointAt(0);
101
+ return (code >= 0 && code <= 8)
102
+ || code === 11
103
+ || code === 12
104
+ || (code >= 14 && code <= 31)
105
+ || code === 127;
106
+ });
107
+ }
108
+
109
+ function normalizeExecution(response, maxOutputBytes) {
110
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
111
+ return { complete: false, reason: 'MALFORMED_RESULT' };
112
+ }
113
+ if (response.timedOut === true) return { complete: false, reason: 'PROBE_TIMEOUT' };
114
+ if (response.errorCode === 'ENOENT') return { complete: false, reason: 'EXECUTABLE_UNAVAILABLE' };
115
+ if (typeof response.stdout !== 'string' || typeof response.stderr !== 'string') {
116
+ return { complete: false, reason: 'MALFORMED_RESULT' };
117
+ }
118
+ const outputBytes = Buffer.byteLength(response.stdout, 'utf8') + Buffer.byteLength(response.stderr, 'utf8');
119
+ if (outputBytes > maxOutputBytes) return { complete: false, reason: 'OUTPUT_LIMIT' };
120
+ const output = `${response.stdout}\n${response.stderr}`;
121
+ if (isHostileOutput(output)) return { complete: false, reason: 'HOSTILE_OUTPUT' };
122
+ if (response.exitCode !== 0) return { complete: false, reason: 'PROBE_FAILED' };
123
+ return {
124
+ complete: true,
125
+ stdout: response.stdout,
126
+ stderr: response.stderr,
127
+ executableIdentity: response.executableIdentity,
128
+ };
129
+ }
130
+
131
+ async function executeBounded(execute, requestBase, timeoutMs) {
132
+ const controller = new AbortController();
133
+ let timer;
134
+ const timeout = new Promise((resolve) => {
135
+ timer = setTimeout(() => {
136
+ controller.abort();
137
+ resolve({ timedOut: true, stdout: '', stderr: '' });
138
+ }, timeoutMs);
139
+ });
140
+ const request = Object.freeze({ ...requestBase, signal: controller.signal });
141
+ try {
142
+ return await Promise.race([
143
+ Promise.resolve().then(() => execute(request)),
144
+ timeout,
145
+ ]);
146
+ } catch (_error) {
147
+ return { exitCode: null, stdout: '', stderr: '', errorCode: 'EXECUTION_ERROR' };
148
+ } finally {
149
+ clearTimeout(timer);
150
+ }
151
+ }
152
+
153
+ function requestFor({ harness, kind, probeId, command, args, timeoutMs, maxOutputBytes }) {
154
+ return {
155
+ harness,
156
+ kind,
157
+ probeId,
158
+ command,
159
+ args: Object.freeze([...args]),
160
+ timeoutMs,
161
+ maxOutputBytes,
162
+ sideEffectFree: true,
163
+ };
164
+ }
165
+
166
+ function incompleteCapabilities(spec, reason) {
167
+ return spec.capabilities.map((item) => ({
168
+ id: item.id,
169
+ available: false,
170
+ status: 'INCOMPLETE',
171
+ reason,
172
+ probe_id: item.probeId,
173
+ }));
174
+ }
175
+
176
+ function buildResult({ harness, spec, observedAt, status, availability, identity, version, capabilities }) {
177
+ return createProbeResult({
178
+ probeRevision: PROBE_REVISION,
179
+ harnessId: harness,
180
+ status,
181
+ availability,
182
+ observedAt,
183
+ executable: { command: spec.command, identity, version },
184
+ capabilities,
185
+ });
186
+ }
187
+
188
+ function parseVersion(stdout, pattern) {
189
+ const normalized = stdout.trim();
190
+ if (normalized.includes('\n') || normalized.includes('\r')) return null;
191
+ const match = pattern.exec(normalized);
192
+ return match ? match[1] : null;
193
+ }
194
+
195
+ function capabilityFromExecution(item, execution, identity) {
196
+ let status = 'INCOMPLETE';
197
+ let reason = execution.reason;
198
+ let available = false;
199
+ if (execution.complete && execution.executableIdentity !== identity) {
200
+ reason = 'EXECUTABLE_IDENTITY_CHANGED';
201
+ } else if (execution.complete) {
202
+ const output = execution.stdout.toLowerCase();
203
+ available = item.requiredTokens.every((token) => output.includes(token.toLowerCase()));
204
+ status = available ? 'AVAILABLE' : 'UNAVAILABLE';
205
+ reason = available ? 'PROBED_SUPPORTED' : 'PROBED_UNSUPPORTED';
206
+ }
207
+ return {
208
+ id: item.id,
209
+ available,
210
+ status,
211
+ reason,
212
+ probe_id: item.probeId,
213
+ };
214
+ }
215
+
216
+ async function probeCapability(item, context, cache) {
217
+ const { execute, harness, spec, timeoutMs, maxOutputBytes, identity } = context;
218
+ const cacheKey = JSON.stringify(item.args);
219
+ let response = cache.get(cacheKey);
220
+ if (!response) {
221
+ response = executeBounded(execute, requestFor({
222
+ harness,
223
+ kind: 'behavior',
224
+ probeId: item.probeId,
225
+ command: spec.command,
226
+ args: item.args,
227
+ timeoutMs,
228
+ maxOutputBytes,
229
+ }), timeoutMs);
230
+ cache.set(cacheKey, response);
231
+ }
232
+ const execution = normalizeExecution(await response, maxOutputBytes);
233
+ return capabilityFromExecution(item, execution, identity);
234
+ }
235
+
236
+ async function probeCapabilities(context) {
237
+ const cache = new Map();
238
+ const capabilities = [];
239
+ for (const item of context.spec.capabilities) {
240
+ capabilities.push(await probeCapability(item, context, cache));
241
+ }
242
+ return capabilities;
243
+ }
244
+
245
+ async function probeHarness(options) {
246
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
247
+ throw new TypeError('probe options must be an object');
248
+ }
249
+ const { harness, execute } = options;
250
+ const spec = getHarnessProbeSpec(harness);
251
+ if (typeof execute !== 'function') throw new TypeError('execute must be a function');
252
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
253
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
254
+ assertBoundedPositiveInteger(timeoutMs, 'timeoutMs', MAX_TIMEOUT_MS);
255
+ assertBoundedPositiveInteger(maxOutputBytes, 'maxOutputBytes', MAX_OUTPUT_BYTES);
256
+ const observedAt = options.observedAt ?? new Date().toISOString();
257
+
258
+ const versionResponse = await executeBounded(execute, requestFor({
259
+ harness,
260
+ kind: 'version',
261
+ probeId: `${harness}.version.v1`,
262
+ command: spec.command,
263
+ args: spec.versionArgs,
264
+ timeoutMs,
265
+ maxOutputBytes,
266
+ }), timeoutMs);
267
+ const versionExecution = normalizeExecution(versionResponse, maxOutputBytes);
268
+ if (!versionExecution.complete) {
269
+ return buildResult({
270
+ harness,
271
+ spec,
272
+ observedAt,
273
+ status: 'INCOMPLETE',
274
+ availability: 'UNAVAILABLE',
275
+ identity: null,
276
+ version: null,
277
+ capabilities: incompleteCapabilities(spec, versionExecution.reason),
278
+ });
279
+ }
280
+ const identity = versionExecution.executableIdentity;
281
+ const version = parseVersion(versionExecution.stdout || versionExecution.stderr, spec.versionPattern);
282
+ if (!EXECUTABLE_IDENTITY_PATTERN.test(identity || '')) {
283
+ return buildResult({
284
+ harness,
285
+ spec,
286
+ observedAt,
287
+ status: 'INCOMPLETE',
288
+ availability: 'UNAVAILABLE',
289
+ identity: null,
290
+ version,
291
+ capabilities: incompleteCapabilities(spec, 'EXECUTABLE_IDENTITY_UNAVAILABLE'),
292
+ });
293
+ }
294
+ if (!version) {
295
+ return buildResult({
296
+ harness,
297
+ spec,
298
+ observedAt,
299
+ status: 'INCOMPLETE',
300
+ availability: 'UNAVAILABLE',
301
+ identity,
302
+ version: null,
303
+ capabilities: incompleteCapabilities(spec, 'MALFORMED_VERSION'),
304
+ });
305
+ }
306
+
307
+ const capabilities = await probeCapabilities({
308
+ execute,
309
+ harness,
310
+ spec,
311
+ timeoutMs,
312
+ maxOutputBytes,
313
+ identity,
314
+ });
315
+
316
+ return buildResult({
317
+ harness,
318
+ spec,
319
+ observedAt,
320
+ status: capabilities.some((item) => item.status === 'INCOMPLETE') ? 'INCOMPLETE' : 'PASS',
321
+ availability: 'AVAILABLE',
322
+ identity,
323
+ version,
324
+ capabilities,
325
+ });
326
+ }
327
+
328
+ async function probeAllHarnesses(options) {
329
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
330
+ throw new TypeError('probe options must be an object');
331
+ }
332
+ return Promise.all(HARNESS_IDS.map((harness) => probeHarness({ ...options, harness })));
333
+ }
334
+
335
+ module.exports = {
336
+ CAPABILITY_IDS,
337
+ DEFAULT_MAX_OUTPUT_BYTES,
338
+ DEFAULT_TIMEOUT_MS,
339
+ MAX_OUTPUT_BYTES,
340
+ MAX_TIMEOUT_MS,
341
+ HARNESS_IDS,
342
+ HARNESS_PROBE_SPECS,
343
+ PROBE_REVISION,
344
+ getHarnessProbeSpec,
345
+ probeAllHarnesses,
346
+ probeHarness,
347
+ };
@@ -135,9 +135,9 @@ function resolveCodexRepoSkillsDir(projectRoot) {
135
135
  * @param {boolean} [params.clean=false] - Remove stale canonical-managed dirs first.
136
136
  * @returns {{targetSkillsDir: string, written: string[]}}
137
137
  */
138
- function populateCodexRepoSkills({ sourceRoot, projectRoot, clean = false }) {
138
+ function populateCodexRepoSkills({ sourceRoot, projectRoot, clean = false, excludeRelativePaths }) {
139
139
  const targetSkillsDir = resolveCodexRepoSkillsDir(projectRoot);
140
- const { written } = populateAgentSkills({ sourceRoot, targetSkillsDir, clean });
140
+ const { written } = populateAgentSkills({ sourceRoot, targetSkillsDir, clean, excludeRelativePaths });
141
141
  return { targetSkillsDir, written };
142
142
  }
143
143
 
@@ -39,6 +39,7 @@ const commands = [
39
39
  { file: "explain.js", module: require("./explain") },
40
40
  { file: "export.js", module: require("./export") },
41
41
  { file: "gate.js", module: require("./gate") },
42
+ { file: "github.js", module: require("./github") },
42
43
  { file: "hooks.js", module: require("./hooks") },
43
44
  { file: "inbox.js", module: require("./inbox") },
44
45
  { file: "init.js", module: require("./init") },
@@ -13,6 +13,7 @@ const { existsSync, readdirSync } = require('node:fs');
13
13
  const path = require('node:path');
14
14
  const { normalizeStageId } = require('../workflow/stages');
15
15
  const { ensureForgeHome, isMutatingVerb } = require('../activation/ensure-forge-home');
16
+ const { createGithubContext } = require('../github-context');
16
17
 
17
18
  // Static command manifest (bundleable fast path). This is a static require so
18
19
  // `bun build --compile` can bundle the command graph; the file is generated by
@@ -36,6 +37,7 @@ try {
36
37
  * @property {function(Array, Object, string, Object=): Promise<*>} handler - Async command handler (4th arg: resolved command opts)
37
38
  * @property {string} [usage] - Usage string (optional)
38
39
  * @property {Object<string, string>} [flags] - Flag descriptions (optional)
40
+ * @property {boolean|function(Array, Object, Object): boolean} [githubAuth] - Whether this invocation needs repository GitHub context
39
41
  */
40
42
 
41
43
  /**
@@ -57,6 +59,9 @@ function validateCommand(mod) {
57
59
  if (typeof mod.handler !== 'function') {
58
60
  return { valid: false, reason: 'missing or invalid "handler" export' };
59
61
  }
62
+ if (mod.githubAuth !== undefined && !['boolean', 'function'].includes(typeof mod.githubAuth)) {
63
+ return { valid: false, reason: 'invalid "githubAuth" export: expected boolean or function' };
64
+ }
60
65
  return { valid: true };
61
66
  }
62
67
 
@@ -174,6 +179,31 @@ function isStageCommand(commandName) {
174
179
  return normalizeStageId(commandName) !== null;
175
180
  }
176
181
 
182
+ function isHelpInvocation(args, flags) {
183
+ if (flags.help || flags['--help'] || flags['-h']) return true;
184
+ // Child arguments belong to the launcher, including its help flags.
185
+ const delimiter = args.indexOf('--');
186
+ const ownArgs = delimiter === -1 ? args : args.slice(0, delimiter);
187
+ return ownArgs.includes('--help') || ownArgs.includes('-h');
188
+ }
189
+
190
+ async function enforceCommandStage(options, commandName, args, flags, projectRoot, command) {
191
+ if (typeof options.enforceStage !== 'function' || !isStageCommand(commandName)) return null;
192
+ return options.enforceStage({ commandName, args, flags, projectRoot, command });
193
+ }
194
+
195
+ async function resolveCommandOptions(command, commandName, args, flags, projectRoot, options) {
196
+ const commandOpts = options.commandOpts ?? {};
197
+ if (!command.githubAuth || isHelpInvocation(args, flags)) return commandOpts;
198
+ const required = typeof command.githubAuth === 'function'
199
+ ? await command.githubAuth(args, flags, { commandName, projectRoot, command })
200
+ : command.githubAuth;
201
+ if (typeof required !== 'boolean') throw new TypeError('Invalid GitHub auth predicate result');
202
+ if (!required) return commandOpts;
203
+ const githubContext = await (options.prepareGithubContext || createGithubContext)(projectRoot);
204
+ return { ...commandOpts, githubContext };
205
+ }
206
+
177
207
  async function executeCommand(commands, commandName, args, flags, projectRoot, options = {}) {
178
208
  const command = commands.get(commandName);
179
209
  if (!command) {
@@ -181,23 +211,13 @@ async function executeCommand(commands, commandName, args, flags, projectRoot, o
181
211
  }
182
212
 
183
213
  try {
184
- let enforcement = null;
185
- if (typeof options.enforceStage === 'function' && isStageCommand(commandName)) {
186
- enforcement = await options.enforceStage({
187
- commandName,
188
- args,
189
- flags,
190
- projectRoot,
191
- command,
192
- });
193
-
194
- if (enforcement?.allowed === false) {
195
- return {
196
- success: false,
197
- error: enforcement.error ?? `Stage ${commandName} is blocked.`,
198
- enforcement,
199
- };
200
- }
214
+ const enforcement = await enforceCommandStage(options, commandName, args, flags, projectRoot, command);
215
+ if (enforcement?.allowed === false) {
216
+ return {
217
+ success: false,
218
+ error: enforcement.error ?? `Stage ${commandName} is blocked.`,
219
+ enforcement,
220
+ };
201
221
  }
202
222
 
203
223
  // Lazy `.forge/` home (activation foundation): the FIRST mutating verb in a
@@ -217,7 +237,17 @@ async function executeCommand(commands, commandName, args, flags, projectRoot, o
217
237
  // alias handlers read it (shouldUseKernelBroker(opts)); other handlers ignore
218
238
  // it. Backward-compatible: defaults to {} so existing 3-arg handlers are
219
239
  // unaffected.
220
- const result = await command.handler(args, flags, projectRoot, options.commandOpts ?? {});
240
+ let commandOpts;
241
+ try {
242
+ commandOpts = await resolveCommandOptions(command, commandName, args, flags, projectRoot, options);
243
+ } catch {
244
+ // Even injected errors can contain subprocess output or credentials.
245
+ return {
246
+ success: false,
247
+ error: 'Unable to verify this clone\'s GitHub account. Run forge github status for recovery.',
248
+ };
249
+ }
250
+ const result = await command.handler(args, flags, projectRoot, commandOpts);
221
251
 
222
252
  // Record stage COMPLETION into the kernel only when the command SUCCEEDED, so
223
253
  // a stage counts as 'done' (which gates ship/review) only when its command
@@ -617,6 +617,52 @@ function createIssueCloser(projectRoot, runFile, opts) {
617
617
  };
618
618
  }
619
619
 
620
+ async function reconcileDeadClaims(projectRoot, runFile, opts = {}) {
621
+ try {
622
+ if (typeof opts._reconcileClaims === 'function') {
623
+ return await opts._reconcileClaims({ projectRoot });
624
+ }
625
+
626
+ let driver = opts._kernelDriver;
627
+ let owned = false;
628
+ if (!driver) {
629
+ const commonDirOutput = tryRun(runFile, 'git', ['-C', projectRoot, 'rev-parse', '--git-common-dir']);
630
+ if (!commonDirOutput) {
631
+ return { examined: 0, released: [] };
632
+ }
633
+ const gitCommonDir = path.isAbsolute(commonDirOutput)
634
+ ? commonDirOutput
635
+ : path.resolve(projectRoot, commonDirOutput);
636
+ const {
637
+ buildMigratedKernelIssueDeps,
638
+ resolveKernelDatabasePath,
639
+ } = require('../kernel/cli-broker-factory');
640
+ const databasePath = resolveKernelDatabasePath({ gitCommonDir });
641
+ const fsApi = opts._fs || fs;
642
+ if (!fsApi.existsSync(databasePath)) return { examined: 0, released: [] };
643
+ const deps = await buildMigratedKernelIssueDeps({ projectRoot, gitCommonDir, databasePath });
644
+ driver = deps.kernelDriver;
645
+ owned = true;
646
+ }
647
+ if (!driver) return { examined: 0, released: [] };
648
+
649
+ try {
650
+ const { reconcileKernelClaims } = require('../kernel/claim-reconciler');
651
+ return await reconcileKernelClaims({
652
+ driver,
653
+ fsApi: opts._fs || fs,
654
+ manifestDir: opts._manifestDir,
655
+ isProcessAlive: opts._isProcessAlive,
656
+ getProcessIdentity: opts._getProcessIdentity,
657
+ });
658
+ } finally {
659
+ if (owned && typeof driver.close === 'function') driver.close();
660
+ }
661
+ } catch (_e) { /* best-effort: unverifiable authority never aborts clean */ // NOSONAR S2486
662
+ return { examined: 0, released: [] };
663
+ }
664
+ }
665
+
620
666
  /**
621
667
  * Main handler for the clean command.
622
668
  * @param {string[]} _args - Positional arguments (unused)
@@ -654,7 +700,10 @@ async function cleanWorktrees(worktreesDir, runFile, fsApi, dryRun, opts, closer
654
700
  // ancestry fast path instead of falling through to the slower squash tier.
655
701
  mergedBranches = mergedOut.split('\n').map(b => b.trim().replace(/^[*+]\s*/, '')).filter(Boolean);
656
702
  }
657
- const ghMergedPrs = getGhMergedPrs(runFile);
703
+ const ghRunner = opts.githubContext?.bound
704
+ ? (_cmd, args, options) => opts.githubContext.runGh(args, options)
705
+ : runFile;
706
+ const ghMergedPrs = getGhMergedPrs(ghRunner);
658
707
  const ctx = { defaultBranch, mergedBranches, ghMergedPrs, runFile };
659
708
  const isMergedFn = opts._isMerged || (branch => detectMerged(branch, ctx));
660
709
 
@@ -703,6 +752,11 @@ async function handler(_args, flags, projectRoot, opts = {}) {
703
752
  }
704
753
  const { cleaned, active, survivors, dirty, closedIssues } = scan;
705
754
 
755
+ // Claim repair is an explicit clean-lifecycle maintenance action. Run after
756
+ // worktree removal so a newly-missing linked checkout can be observed, while
757
+ // dry-run remains mutation-free. Evidence gaps and Kernel failures fail closed.
758
+ if (!dryRun) await reconcileDeadClaims(projectRoot, runFile, opts);
759
+
706
760
  // Post-merge master auto-update (default-on; skipped in dry-run).
707
761
  let masterSync = null;
708
762
  if (masterSyncEnabled && !dryRun) {
@@ -723,6 +777,7 @@ function finalize(summary, masterSync) {
723
777
 
724
778
  module.exports = {
725
779
  name: 'clean',
780
+ githubAuth: true,
726
781
  description: 'Remove worktrees for merged branches (squash-aware) and fast-forward the default branch',
727
782
  usage: 'forge clean [--dry-run] [--no-master-sync]',
728
783
  flags: {
@@ -739,6 +794,7 @@ module.exports = {
739
794
  getGhMergedPrs,
740
795
  prEvidenceFor,
741
796
  createIssueCloser,
797
+ reconcileDeadClaims,
742
798
  isSquashMerged,
743
799
  hasMergedPrEvidence,
744
800
  isUnstartedBranch,
@@ -38,10 +38,39 @@ function buildMemoryCheck(projectRoot, env) {
38
38
  };
39
39
  }
40
40
  const serverPath = graphiti.mcpServerPath;
41
- const serverPathExists = fs.existsSync(serverPath);
42
- const reach = serverPathExists
43
- ? `mcp_server present at ${serverPath}`
44
- : `mcp_server path not found locally (${serverPath}); run it before agents can use graph memory`;
41
+ const path = require('node:path');
42
+ const resolvedServerPath = path.isAbsolute(serverPath)
43
+ ? serverPath
44
+ : path.resolve(projectRoot, serverPath);
45
+ const serverPathExists = fs.existsSync(resolvedServerPath);
46
+ let serverPathIsDirectory = false;
47
+ if (serverPathExists) {
48
+ try {
49
+ serverPathIsDirectory = fs.statSync(resolvedServerPath).isDirectory();
50
+ } catch {
51
+ serverPathIsDirectory = false;
52
+ }
53
+ }
54
+ const entrypointPath = path.join(resolvedServerPath, 'main.py');
55
+ let entrypointExists = false;
56
+ if (serverPathIsDirectory) {
57
+ try {
58
+ entrypointExists = fs.statSync(entrypointPath).isFile();
59
+ } catch {
60
+ entrypointExists = false;
61
+ }
62
+ }
63
+ const serverReady = serverPathExists && serverPathIsDirectory && entrypointExists;
64
+ let reach;
65
+ if (!serverPathExists) {
66
+ reach = `mcp_server path not found locally (${serverPath}); run it before agents can use graph memory`;
67
+ } else if (!serverPathIsDirectory) {
68
+ reach = `mcp_server path is not a directory (${serverPath})`;
69
+ } else if (!entrypointExists) {
70
+ reach = `mcp_server entrypoint not found (${path.join(serverPath, 'main.py')})`;
71
+ } else {
72
+ reach = `mcp_server present at ${serverPath}`;
73
+ }
45
74
  return {
46
75
  // Reflect reality in the line symbol: a graphiti backend whose MCP server
47
76
  // isn't present yet renders as `!` (warn), not a misleading `✓`. This does
@@ -49,13 +78,15 @@ function buildMemoryCheck(projectRoot, env) {
49
78
  // filesystem-class check (see buildDoctorReport), so an uninstalled opt-in
50
79
  // memory server warns without failing `forge doctor`.
51
80
  id: 'memory-backend',
52
- ok: serverPathExists,
81
+ ok: serverReady,
53
82
  backend,
54
83
  serverPathExists,
84
+ serverPathIsDirectory,
85
+ entrypointExists,
55
86
  detail: `memory backend: graphiti — served to agents via the graphiti-memory MCP server; ${reach}`,
56
87
  };
57
88
  } catch (err) {
58
- const backend = memoryRouter.resolveMemoryBackend({ projectRoot, env, warn: () => {} });
89
+ const backend = err.backend || memoryRouter.resolveMemoryBackend({ projectRoot, env, warn: () => {} });
59
90
  return { id: 'memory-backend', ok: false, backend, detail: err.message };
60
91
  }
61
92
  }