@geminix/gxpm 0.1.0

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 (299) hide show
  1. package/AGENTS.md +148 -0
  2. package/CANON.md +53 -0
  3. package/CLAUDE.md +60 -0
  4. package/CONTEXT.md +49 -0
  5. package/DEBUG.md +59 -0
  6. package/ISSUE_CONTEXT.md +25 -0
  7. package/README.md +143 -0
  8. package/VERSION +1 -0
  9. package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
  10. package/agents/grill-master.md +26 -0
  11. package/agents/implementer.md +32 -0
  12. package/agents/review-army/accessibility-reviewer.md +54 -0
  13. package/agents/review-army/code-quality-reviewer.md +54 -0
  14. package/agents/review-army/security-reviewer.md +56 -0
  15. package/agents/review-army/spec-compliance-reviewer.md +51 -0
  16. package/agents/review-army/test-reviewer.md +55 -0
  17. package/agents/reviewer.md +59 -0
  18. package/agents/ship-audit-army/docs-auditor.md +53 -0
  19. package/agents/ship-audit-army/performance-auditor.md +52 -0
  20. package/agents/ship-audit-army/security-auditor.md +52 -0
  21. package/agents/specifier.md +55 -0
  22. package/agents/triage-officer.md +27 -0
  23. package/bin/gxpm +17 -0
  24. package/bin/gxpm-browser +17 -0
  25. package/bin/gxpm-config +15 -0
  26. package/bin/gxpm-eval +13 -0
  27. package/bin/gxpm-global-discover +15 -0
  28. package/bin/gxpm-init +38 -0
  29. package/bin/gxpm-investigate +194 -0
  30. package/bin/gxpm-uninstall +15 -0
  31. package/bin/gxpm-update-check +165 -0
  32. package/commands/build.md +40 -0
  33. package/commands/help.md +53 -0
  34. package/commands/plan.md +34 -0
  35. package/commands/refine.md +46 -0
  36. package/commands/review.md +34 -0
  37. package/commands/ship.md +37 -0
  38. package/core/ac-check.ts +20 -0
  39. package/core/agent-runtime.ts +363 -0
  40. package/core/artifact-validator.ts +151 -0
  41. package/core/artifacts.ts +313 -0
  42. package/core/autopilot.ts +250 -0
  43. package/core/capabilities.ts +779 -0
  44. package/core/checkpoint.ts +370 -0
  45. package/core/cleanup.ts +32 -0
  46. package/core/command-probe.ts +82 -0
  47. package/core/config.ts +533 -0
  48. package/core/contracts/behavior-spec.schema.ts +38 -0
  49. package/core/contracts/converter.ts +61 -0
  50. package/core/contracts/host.ts +43 -0
  51. package/core/converters/converter.ts +93 -0
  52. package/core/converters/index.ts +8 -0
  53. package/core/converters/managed-artifact.ts +119 -0
  54. package/core/converters/parser.ts +159 -0
  55. package/core/converters/template-renderer.ts +35 -0
  56. package/core/converters/writer.ts +61 -0
  57. package/core/dag-executor.ts +426 -0
  58. package/core/dag-loader.ts +292 -0
  59. package/core/dag-schemas.ts +150 -0
  60. package/core/dispatch.ts +125 -0
  61. package/core/evidence.ts +148 -0
  62. package/core/gate.ts +269 -0
  63. package/core/hook-engine.ts +566 -0
  64. package/core/host-probe.ts +64 -0
  65. package/core/implement.ts +16 -0
  66. package/core/isolation-errors.ts +174 -0
  67. package/core/isolation-resolver.ts +921 -0
  68. package/core/issue-context.ts +381 -0
  69. package/core/issue-readiness.ts +457 -0
  70. package/core/issue-sync.ts +427 -0
  71. package/core/issues.ts +132 -0
  72. package/core/land.ts +108 -0
  73. package/core/orchestrator.ts +54 -0
  74. package/core/phase-artifact.ts +32 -0
  75. package/core/phase-gates.ts +130 -0
  76. package/core/phase-rewind.ts +94 -0
  77. package/core/plan-lint.ts +61 -0
  78. package/core/plan.ts +77 -0
  79. package/core/port-allocation.ts +50 -0
  80. package/core/pr-check.ts +15 -0
  81. package/core/preset-system/preset-resolver.ts +221 -0
  82. package/core/project-init-status.ts +127 -0
  83. package/core/qa.ts +15 -0
  84. package/core/resilience.ts +165 -0
  85. package/core/runs.ts +288 -0
  86. package/core/safe-path.test.ts +80 -0
  87. package/core/safe-path.ts +60 -0
  88. package/core/sdd-gate.test.ts +98 -0
  89. package/core/sdd-gate.ts +134 -0
  90. package/core/self-review.ts +62 -0
  91. package/core/session.ts +70 -0
  92. package/core/ship.ts +86 -0
  93. package/core/specify.ts +173 -0
  94. package/core/state.ts +1002 -0
  95. package/core/template-engine.ts +152 -0
  96. package/core/template-resolver.test.ts +70 -0
  97. package/core/template-resolver.ts +156 -0
  98. package/core/triage.ts +26 -0
  99. package/core/verify.ts +15 -0
  100. package/core/wiki-native.ts +2423 -0
  101. package/core/wiki.ts +27 -0
  102. package/core/workflow-event-emitter.ts +163 -0
  103. package/core/workflows/engine.ts +273 -0
  104. package/core/workflows/expressions.ts +76 -0
  105. package/core/workflows/index.ts +38 -0
  106. package/core/workflows/steps/command.ts +43 -0
  107. package/core/workflows/steps/gate.ts +47 -0
  108. package/core/workflows/steps/gxpm.ts +44 -0
  109. package/core/workflows/steps/linear.ts +31 -0
  110. package/core/workflows/steps/shell.ts +65 -0
  111. package/core/workflows/types.ts +62 -0
  112. package/core/workspace-runtime.ts +227 -0
  113. package/core/worktree-init-steps.ts +647 -0
  114. package/core/worktree-init.ts +330 -0
  115. package/core/worktree-owner.ts +143 -0
  116. package/docs/GXPM_VERIFY.md +98 -0
  117. package/docs/INSTALL_FOR_AGENTS.md +113 -0
  118. package/docs/README.md +57 -0
  119. package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
  120. package/docs/agents/domain.md +30 -0
  121. package/docs/agents/issue-tracker.md +30 -0
  122. package/docs/agents/triage-labels.md +32 -0
  123. package/docs/architecture/gxpm-architecture-diagram.md +265 -0
  124. package/docs/architecture/gxpm-current-architecture.md +175 -0
  125. package/docs/architecture/gxpm-current-flow.md +278 -0
  126. package/docs/architecture/gxpm-replacement-architecture.md +211 -0
  127. package/docs/architecture/gxpm-target-architecture.md +449 -0
  128. package/docs/architecture/gxpm-v0-contract.md +311 -0
  129. package/docs/architecture/layered-workflow-boundaries.md +193 -0
  130. package/docs/architecture/preset-system.md +126 -0
  131. package/docs/architecture/scaffold-northstar.md +23 -0
  132. package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
  133. package/docs/brainstorms/README.md +22 -0
  134. package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
  135. package/docs/governance/beta-skill-promotion.md +39 -0
  136. package/docs/governance/development-contract.md +144 -0
  137. package/docs/governance/gherkin-style.md +90 -0
  138. package/docs/governance/host-adapter.md +56 -0
  139. package/docs/governance/skill-authoring.md +87 -0
  140. package/docs/governance/skill-testing.md +356 -0
  141. package/docs/governance/template-authoring.md +53 -0
  142. package/docs/migrations/v0.2.md +51 -0
  143. package/docs/plans/README.md +23 -0
  144. package/docs/plans/bdd-then-tdd-plan.md +1767 -0
  145. package/docs/plans/docs-knowledge-system-plan.md +31 -0
  146. package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
  147. package/docs/research/agents-md-best-practices.md +207 -0
  148. package/docs/research/archon-study.md +351 -0
  149. package/docs/research/claude-hooks-study.md +440 -0
  150. package/docs/research/codex-hooks-study.md +624 -0
  151. package/docs/research/everything-claude-code-study.md +252 -0
  152. package/docs/research/from-skills-to-layered-workflow.md +322 -0
  153. package/docs/research/gsd-study.md +69 -0
  154. package/docs/research/kimi-hooks-study.md +274 -0
  155. package/docs/research/mattpocock-skills-comparison.md +429 -0
  156. package/docs/research/mattpocock-skills-study.md +275 -0
  157. package/docs/research/oh-my-codex-study.md +279 -0
  158. package/docs/research/perplexity-agent-skills-design.md +168 -0
  159. package/docs/research/pmc-gstack-skill-study.md +122 -0
  160. package/docs/research/spec-kit-study.md +224 -0
  161. package/docs/research/superpowers-study.md +209 -0
  162. package/docs/roadmap/initial-roadmap.md +53 -0
  163. package/docs/solutions/README.md +45 -0
  164. package/docs/solutions/artifact-nesting-recovery.md +58 -0
  165. package/docs/solutions/session-context-restore-practice.md +67 -0
  166. package/docs/solutions/workflow/version-drift-recovery.md +49 -0
  167. package/docs/solutions/worktree-gate-recovery.md +62 -0
  168. package/docs/specs/README.md +28 -0
  169. package/docs/specs/claude.md +45 -0
  170. package/docs/specs/codex.md +44 -0
  171. package/docs/specs/cursor.md +44 -0
  172. package/hosts/adapters/claude.ts +29 -0
  173. package/hosts/adapters/codex.ts +27 -0
  174. package/hosts/adapters/cursor.ts +27 -0
  175. package/hosts/adapters/kimi.ts +27 -0
  176. package/hosts/claude.ts +23 -0
  177. package/hosts/codex.ts +26 -0
  178. package/hosts/cursor.ts +19 -0
  179. package/hosts/index.ts +33 -0
  180. package/hosts/registry.test.ts +52 -0
  181. package/hosts/registry.ts +57 -0
  182. package/hosts/schema.ts +58 -0
  183. package/package.json +52 -0
  184. package/scripts/browser.ts +185 -0
  185. package/scripts/cleanup.ts +142 -0
  186. package/scripts/commands/artifact.ts +115 -0
  187. package/scripts/commands/autopilot.ts +143 -0
  188. package/scripts/commands/capability.ts +57 -0
  189. package/scripts/commands/config.ts +69 -0
  190. package/scripts/commands/dag.ts +126 -0
  191. package/scripts/commands/feedback.ts +123 -0
  192. package/scripts/commands/gate.ts +291 -0
  193. package/scripts/commands/helpers.ts +126 -0
  194. package/scripts/commands/hook.ts +66 -0
  195. package/scripts/commands/init.ts +515 -0
  196. package/scripts/commands/issue.ts +825 -0
  197. package/scripts/commands/phase.ts +61 -0
  198. package/scripts/commands/preset.ts +159 -0
  199. package/scripts/commands/runtime.ts +199 -0
  200. package/scripts/commands/specify.ts +71 -0
  201. package/scripts/commands/upgrade.ts +243 -0
  202. package/scripts/commands/verify.ts +183 -0
  203. package/scripts/commands/wiki.ts +242 -0
  204. package/scripts/commands/workflow.ts +131 -0
  205. package/scripts/dev-skill.ts +55 -0
  206. package/scripts/discover-skills.ts +116 -0
  207. package/scripts/doctor.ts +410 -0
  208. package/scripts/dogfood-check.ts +125 -0
  209. package/scripts/eval-functional.ts +218 -0
  210. package/scripts/eval.ts +246 -0
  211. package/scripts/gen-skill-docs.ts +201 -0
  212. package/scripts/global-discover.ts +217 -0
  213. package/scripts/governance-check.ts +75 -0
  214. package/scripts/gxpm-check.ts +12 -0
  215. package/scripts/gxpm.ts +216 -0
  216. package/scripts/host-config.ts +62 -0
  217. package/scripts/install-claude-hooks.ts +138 -0
  218. package/scripts/install-codex-hooks.ts +271 -0
  219. package/scripts/install-hooks.ts +128 -0
  220. package/scripts/install-kimi-hooks.ts +92 -0
  221. package/scripts/install-skill.ts +184 -0
  222. package/scripts/phase-artifact-commands.ts +100 -0
  223. package/scripts/post-land-sync.ts +46 -0
  224. package/scripts/scaffold-check.ts +85 -0
  225. package/scripts/skill-naming-check.ts +78 -0
  226. package/scripts/skill-structure-check.ts +157 -0
  227. package/scripts/skills-lock-check.ts +60 -0
  228. package/scripts/sync-markdown-artifacts.ts +172 -0
  229. package/scripts/uninstall.ts +162 -0
  230. package/scripts/version.ts +47 -0
  231. package/scripts/wait-pr-ready.ts +407 -0
  232. package/skills/gxpm/SKILL.md +485 -0
  233. package/skills/gxpm/SKILL.md.tmpl +422 -0
  234. package/skills/gxpm/references/CANON.md +53 -0
  235. package/skills/gxpm/references/key-rules.md +130 -0
  236. package/skills/gxpm-architecture/SKILL.md +106 -0
  237. package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
  238. package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
  239. package/skills/gxpm-autopilot/SKILL.md +116 -0
  240. package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
  241. package/skills/gxpm-browser/SKILL.md +105 -0
  242. package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
  243. package/skills/gxpm-browser/references/commands.md +43 -0
  244. package/skills/gxpm-browser/references/evidence-path.md +20 -0
  245. package/skills/gxpm-build/SKILL.md +78 -0
  246. package/skills/gxpm-cleanup/SKILL.md +76 -0
  247. package/skills/gxpm-debug-issue/SKILL.md +39 -0
  248. package/skills/gxpm-diagnose/SKILL.md +220 -0
  249. package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
  250. package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
  251. package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
  252. package/skills/gxpm-diagnose/references/phases.md +60 -0
  253. package/skills/gxpm-eval/SKILL.md +78 -0
  254. package/skills/gxpm-explore-codebase/SKILL.md +36 -0
  255. package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
  256. package/skills/gxpm-feedback/SKILL.md +122 -0
  257. package/skills/gxpm-grill/SKILL.md +159 -0
  258. package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
  259. package/skills/gxpm-grill/references/documentation-templates.md +56 -0
  260. package/skills/gxpm-grill/references/process.md +25 -0
  261. package/skills/gxpm-handoff/SKILL.md +112 -0
  262. package/skills/gxpm-hygiene/SKILL.md +69 -0
  263. package/skills/gxpm-implementer/SKILL.md +142 -0
  264. package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
  265. package/skills/gxpm-linear/SKILL.md +282 -0
  266. package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
  267. package/skills/gxpm-linear/references/commands.md +75 -0
  268. package/skills/gxpm-linear/references/workflows.md +120 -0
  269. package/skills/gxpm-planning/SKILL.md +134 -0
  270. package/skills/gxpm-prototype/SKILL.md +64 -0
  271. package/skills/gxpm-refactor-safely/SKILL.md +62 -0
  272. package/skills/gxpm-review-army/SKILL.md +117 -0
  273. package/skills/gxpm-review-changes/SKILL.md +36 -0
  274. package/skills/gxpm-setup/SKILL.md +101 -0
  275. package/skills/gxpm-specifier/SKILL.md +135 -0
  276. package/skills/gxpm-tdd/SKILL.md +187 -0
  277. package/skills/gxpm-tdd/references/interface-design.md +23 -0
  278. package/skills/gxpm-tdd/references/mocking.md +27 -0
  279. package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
  280. package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
  281. package/skills/gxpm-tdd/references/workflow.md +50 -0
  282. package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
  283. package/skills/gxpm-triage/SKILL.md +160 -0
  284. package/skills/gxpm-verify/SKILL.md +107 -0
  285. package/skills/gxpm-write-skill/SKILL.md +131 -0
  286. package/skills/gxpm-zoom-out/SKILL.md +69 -0
  287. package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
  288. package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
  289. package/templates/constitution-template.md +63 -0
  290. package/templates/hooks/gxpm-commit-msg +16 -0
  291. package/templates/hooks/gxpm-post-checkout +19 -0
  292. package/templates/hooks/gxpm-post-commit +7 -0
  293. package/templates/hooks/gxpm-post-merge +29 -0
  294. package/templates/hooks/gxpm-pre-commit +39 -0
  295. package/templates/hooks/gxpm-pre-push +33 -0
  296. package/templates/plan-template.md.tmpl +46 -0
  297. package/templates/spec-template.md.tmpl +63 -0
  298. package/templates/specify-stub.tmpl +22 -0
  299. package/templates/tasks-template.md.tmpl +32 -0
@@ -0,0 +1,457 @@
1
+ import { closeSync, openSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { hasArtifact } from "./artifacts";
3
+ import { listIssues } from "./issues";
4
+ import {
5
+ appendIssueEvent,
6
+ buildOwnershipChangedEvent,
7
+ getIssuePaths,
8
+ readIssueState,
9
+ touchIssueOwnership,
10
+ type GxpmPhase,
11
+ type IssueClaim,
12
+ type IssueType,
13
+ type StateEvent,
14
+ } from "./state";
15
+ import { resolveAgentIdentity, resolveSessionId } from "./session";
16
+ import { isTerminalRunStatus, readRun } from "./runs";
17
+
18
+ export type IssueReadinessDecision = "ready" | "blocked" | "ignored";
19
+
20
+ export const DEFAULT_CLAIM_STALE_AFTER_MS = 24 * 60 * 60 * 1000;
21
+
22
+ export interface IssueReadiness {
23
+ issueId: string;
24
+ issueType: IssueType;
25
+ currentPhase: GxpmPhase;
26
+ decision: IssueReadinessDecision;
27
+ reason: string;
28
+ claim?: IssueClaim;
29
+ }
30
+
31
+ export interface ClaimIssueResult {
32
+ issueId: string;
33
+ claimed: boolean;
34
+ claim: IssueClaim;
35
+ }
36
+
37
+ export interface ReleaseIssueClaimResult {
38
+ issueId: string;
39
+ released: boolean;
40
+ claim: IssueClaim;
41
+ }
42
+
43
+ export interface ReconcileIssueClaimResult {
44
+ issueId: string;
45
+ reconciled: boolean;
46
+ action: "none" | "released" | "marked_stale";
47
+ reason: string;
48
+ claim?: IssueClaim;
49
+ }
50
+
51
+ export function listReadyIssues(input: { root?: string } = {}): IssueReadiness[] {
52
+ return listIssueReadiness({ root: input.root, includeAll: true }).filter((issue) => issue.decision === "ready");
53
+ }
54
+
55
+ export function listIssueReadiness(input: { root?: string; includeAll?: boolean } = {}): IssueReadiness[] {
56
+ const root = input.root ?? process.cwd();
57
+ const issues = listIssues({ root, includeAll: true }).map((entry) =>
58
+ classifyIssueReadiness({ root, issueId: entry.issueId }),
59
+ );
60
+ return input.includeAll ? issues : issues.filter((issue) => issue.decision === "ready");
61
+ }
62
+
63
+ export function classifyIssueReadiness(input: {
64
+ root?: string;
65
+ issueId: string;
66
+ now?: Date | string;
67
+ staleAfterMs?: number;
68
+ }): IssueReadiness {
69
+ const root = input.root ?? process.cwd();
70
+ const state = readIssueState({ root, issueId: input.issueId });
71
+ const base = {
72
+ issueId: state.issueId,
73
+ issueType: state.issueType ?? "feature",
74
+ currentPhase: state.currentPhase,
75
+ };
76
+
77
+ if (state.archived) {
78
+ return { ...base, decision: "ignored", reason: "archived" };
79
+ }
80
+ if (state.issueType && state.issueType !== "feature") {
81
+ return { ...base, decision: "ignored", reason: `issue_type_${state.issueType}` };
82
+ }
83
+ if (state.currentPhase === "land") {
84
+ return { ...base, decision: "ignored", reason: "landed" };
85
+ }
86
+ if (state.currentPhase !== "implement") {
87
+ return { ...base, decision: "blocked", reason: `phase_${state.currentPhase}_not_implement` };
88
+ }
89
+ if (!hasArtifact({ root, issueId: state.issueId, type: "dispatch-handoff" })) {
90
+ return { ...base, decision: "blocked", reason: "missing_dispatch_handoff" };
91
+ }
92
+ if (state.claim) {
93
+ const claimDecision = classifyClaim({
94
+ root,
95
+ issueId: state.issueId,
96
+ claim: state.claim,
97
+ now: input.now,
98
+ staleAfterMs: input.staleAfterMs,
99
+ });
100
+ return { ...base, ...claimDecision };
101
+ }
102
+
103
+ return { ...base, decision: "ready", reason: "ready_for_run" };
104
+ }
105
+
106
+ export function claimIssue(input: {
107
+ root?: string;
108
+ issueId: string;
109
+ actor?: string;
110
+ sessionId?: string;
111
+ runId?: string;
112
+ }): ClaimIssueResult {
113
+ const root = input.root ?? process.cwd();
114
+ const sessionId = input.sessionId ?? resolveSessionId();
115
+ const actor = input.actor ?? resolveAgentIdentity(process.env, root).actor;
116
+ return withClaimLock(root, input.issueId, (paths) => {
117
+ const state = readIssueState({ root, issueId: input.issueId });
118
+
119
+ if (state.claim?.status === "claimed") {
120
+ if (state.claim.claimedBySession === sessionId && state.claim.actor === actor) {
121
+ return { issueId: state.issueId, claimed: false, claim: state.claim };
122
+ }
123
+ throw new Error(`Issue already claimed: ${state.issueId} by ${state.claim.claimedBySession}`);
124
+ }
125
+
126
+ const readiness = classifyIssueReadiness({ root, issueId: input.issueId });
127
+ if (readiness.decision !== "ready") {
128
+ throw new Error(`Issue not claimable: ${state.issueId} (${readiness.reason})`);
129
+ }
130
+
131
+ const now = new Date().toISOString();
132
+ const claim: IssueClaim = {
133
+ status: "claimed",
134
+ actor,
135
+ claimedBySession: sessionId,
136
+ claimedAt: now,
137
+ runId: input.runId,
138
+ };
139
+ const updated = touchIssueOwnership({
140
+ state: { ...state, claim, updatedAt: now },
141
+ sessionId,
142
+ });
143
+
144
+ writeFileSync(paths.statePath, `${JSON.stringify(updated, null, 2)}\n`);
145
+ const ownershipEvent = buildOwnershipChangedEvent({
146
+ issueId: state.issueId,
147
+ timestamp: now,
148
+ previousState: state,
149
+ nextState: updated,
150
+ sessionId,
151
+ });
152
+ if (ownershipEvent) {
153
+ appendIssueEvent({ issueDir: paths.issueDir, event: ownershipEvent });
154
+ }
155
+ appendIssueEvent({
156
+ issueDir: paths.issueDir,
157
+ event: issueClaimedEvent(state.issueId, now, claim),
158
+ });
159
+
160
+ return { issueId: state.issueId, claimed: true, claim };
161
+ });
162
+ }
163
+
164
+ export function releaseIssueClaim(input: {
165
+ root?: string;
166
+ issueId: string;
167
+ reason?: string;
168
+ sessionId?: string;
169
+ now?: Date | string;
170
+ }): ReleaseIssueClaimResult {
171
+ const root = input.root ?? process.cwd();
172
+ const sessionId = input.sessionId ?? resolveSessionId();
173
+ return withClaimLock(root, input.issueId, (paths) => {
174
+ const state = readIssueState({ root, issueId: input.issueId });
175
+ if (!state.claim) {
176
+ throw new Error(`Issue has no claim to release: ${state.issueId}`);
177
+ }
178
+ if (state.claim.status === "released") {
179
+ return { issueId: state.issueId, released: false, claim: state.claim };
180
+ }
181
+
182
+ const now = normalizeNow(input.now);
183
+ const claim: IssueClaim = {
184
+ status: "released",
185
+ actor: state.claim.actor,
186
+ claimedBySession: state.claim.claimedBySession,
187
+ claimedAt: state.claim.claimedAt,
188
+ runId: state.claim.runId,
189
+ releasedAt: now,
190
+ releasedBySession: sessionId,
191
+ releaseReason: input.reason ?? "manual_release",
192
+ };
193
+ writeClaimUpdate({
194
+ paths,
195
+ state,
196
+ claim,
197
+ sessionId,
198
+ event: issueClaimReleasedEvent(state.issueId, now, claim),
199
+ });
200
+
201
+ return { issueId: state.issueId, released: true, claim };
202
+ });
203
+ }
204
+
205
+ export function reconcileIssueClaim(input: {
206
+ root?: string;
207
+ issueId: string;
208
+ sessionId?: string;
209
+ now?: Date | string;
210
+ staleAfterMs?: number;
211
+ }): ReconcileIssueClaimResult {
212
+ const root = input.root ?? process.cwd();
213
+ const sessionId = input.sessionId ?? resolveSessionId();
214
+ return withClaimLock(root, input.issueId, (paths) => {
215
+ const state = readIssueState({ root, issueId: input.issueId });
216
+ if (!state.claim) {
217
+ return { issueId: state.issueId, reconciled: false, action: "none", reason: "no_claim" };
218
+ }
219
+ if (state.claim.status !== "claimed") {
220
+ return {
221
+ issueId: state.issueId,
222
+ reconciled: false,
223
+ action: "none",
224
+ reason: `claim_${state.claim.status}`,
225
+ claim: state.claim,
226
+ };
227
+ }
228
+
229
+ const terminalRun = terminalRunForClaim(root, state.issueId, state.claim);
230
+ if (terminalRun) {
231
+ const now = normalizeNow(input.now);
232
+ const claim: IssueClaim = {
233
+ status: "released",
234
+ actor: state.claim.actor,
235
+ claimedBySession: state.claim.claimedBySession,
236
+ claimedAt: state.claim.claimedAt,
237
+ runId: state.claim.runId,
238
+ releasedAt: now,
239
+ releasedBySession: sessionId,
240
+ releaseReason: `run_${terminalRun.status}`,
241
+ };
242
+ writeClaimUpdate({
243
+ paths,
244
+ state,
245
+ claim,
246
+ sessionId,
247
+ event: issueClaimReleasedEvent(state.issueId, now, claim),
248
+ });
249
+ return {
250
+ issueId: state.issueId,
251
+ reconciled: true,
252
+ action: "released",
253
+ reason: `run_${terminalRun.status}`,
254
+ claim,
255
+ };
256
+ }
257
+
258
+ if (isStaleClaim(state.claim, input.now, input.staleAfterMs)) {
259
+ const now = normalizeNow(input.now);
260
+ const claim: IssueClaim = {
261
+ status: "stale",
262
+ actor: state.claim.actor,
263
+ claimedBySession: state.claim.claimedBySession,
264
+ claimedAt: state.claim.claimedAt,
265
+ runId: state.claim.runId,
266
+ staleAt: now,
267
+ staleReason: "claim_age_exceeded",
268
+ };
269
+ writeClaimUpdate({
270
+ paths,
271
+ state,
272
+ claim,
273
+ sessionId,
274
+ event: issueClaimStaleEvent(state.issueId, now, claim, sessionId),
275
+ });
276
+ return {
277
+ issueId: state.issueId,
278
+ reconciled: true,
279
+ action: "marked_stale",
280
+ reason: "claim_age_exceeded",
281
+ claim,
282
+ };
283
+ }
284
+
285
+ return { issueId: state.issueId, reconciled: false, action: "none", reason: "claim_active", claim: state.claim };
286
+ });
287
+ }
288
+
289
+ function withClaimLock<T>(root: string, issueId: string, action: (paths: ReturnType<typeof getIssuePaths>) => T): T {
290
+ const paths = getIssuePaths(root, issueId);
291
+ const lockPath = `${paths.issueDir}/.claim.lock`;
292
+ let lockFd: number | null = null;
293
+ try {
294
+ lockFd = openSync(lockPath, "wx");
295
+ return action(paths);
296
+ } catch (error) {
297
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
298
+ throw new Error(`Issue claim locked: ${issueId}`);
299
+ }
300
+ throw error;
301
+ } finally {
302
+ if (lockFd !== null) {
303
+ closeSync(lockFd);
304
+ try {
305
+ unlinkSync(lockPath);
306
+ } catch {}
307
+ }
308
+ }
309
+ }
310
+
311
+ function classifyClaim(input: {
312
+ root: string;
313
+ issueId: string;
314
+ claim: IssueClaim;
315
+ now?: Date | string;
316
+ staleAfterMs?: number;
317
+ }): Pick<IssueReadiness, "decision" | "reason" | "claim"> {
318
+ if (input.claim.status === "released") {
319
+ return { decision: "ready", reason: "claim_released", claim: input.claim };
320
+ }
321
+ if (input.claim.status === "stale") {
322
+ return { decision: "blocked", reason: "stale_claim", claim: input.claim };
323
+ }
324
+
325
+ const terminalRun = terminalRunForClaim(input.root, input.issueId, input.claim);
326
+ if (terminalRun) {
327
+ return {
328
+ decision: "blocked",
329
+ reason: `claim_run_${terminalRun.status}_needs_reconcile`,
330
+ claim: input.claim,
331
+ };
332
+ }
333
+ if (isStaleClaim(input.claim, input.now, input.staleAfterMs)) {
334
+ return { decision: "blocked", reason: "stale_claim", claim: input.claim };
335
+ }
336
+ return { decision: "blocked", reason: "claimed_by_session", claim: input.claim };
337
+ }
338
+
339
+ function writeClaimUpdate(input: {
340
+ paths: ReturnType<typeof getIssuePaths>;
341
+ state: ReturnType<typeof readIssueState>;
342
+ claim: IssueClaim;
343
+ sessionId: string;
344
+ event: StateEvent;
345
+ }) {
346
+ const now = input.event.timestamp;
347
+ const updated = touchIssueOwnership({
348
+ state: { ...input.state, claim: input.claim, updatedAt: now },
349
+ sessionId: input.sessionId,
350
+ });
351
+
352
+ writeFileSync(input.paths.statePath, `${JSON.stringify(updated, null, 2)}\n`);
353
+ const ownershipEvent = buildOwnershipChangedEvent({
354
+ issueId: input.state.issueId,
355
+ timestamp: now,
356
+ previousState: input.state,
357
+ nextState: updated,
358
+ sessionId: input.sessionId,
359
+ });
360
+ if (ownershipEvent) {
361
+ appendIssueEvent({ issueDir: input.paths.issueDir, event: ownershipEvent });
362
+ }
363
+ appendIssueEvent({ issueDir: input.paths.issueDir, event: input.event });
364
+ }
365
+
366
+ function terminalRunForClaim(root: string, issueId: string, claim: IssueClaim) {
367
+ if (claim.status !== "claimed" || !claim.runId) {
368
+ return null;
369
+ }
370
+ try {
371
+ const run = readRun({ root, issueId, runId: claim.runId });
372
+ return isTerminalRunStatus(run.status) ? run : null;
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ function isStaleClaim(claim: IssueClaim, now: Date | string | undefined, staleAfterMs: number | undefined) {
379
+ if (claim.status !== "claimed") {
380
+ return false;
381
+ }
382
+ const claimedAtMs = Date.parse(claim.claimedAt);
383
+ if (!Number.isFinite(claimedAtMs)) {
384
+ return false;
385
+ }
386
+ const threshold = staleAfterMs ?? DEFAULT_CLAIM_STALE_AFTER_MS;
387
+ if (!Number.isFinite(threshold) || threshold < 0) {
388
+ return false;
389
+ }
390
+ return Date.parse(normalizeNow(now)) - claimedAtMs >= threshold;
391
+ }
392
+
393
+ function normalizeNow(value: Date | string | undefined) {
394
+ if (value instanceof Date) {
395
+ return value.toISOString();
396
+ }
397
+ return value ?? new Date().toISOString();
398
+ }
399
+
400
+ function issueClaimedEvent(issueId: string, timestamp: string, claim: IssueClaim): StateEvent {
401
+ return {
402
+ schemaVersion: 1,
403
+ type: "issue.claimed",
404
+ issueId,
405
+ timestamp,
406
+ sessionId: claim.claimedBySession,
407
+ payload: {
408
+ actor: claim.actor,
409
+ claimedBySession: claim.claimedBySession,
410
+ claimedAt: claim.claimedAt,
411
+ runId: claim.runId,
412
+ },
413
+ };
414
+ }
415
+
416
+ function issueClaimReleasedEvent(issueId: string, timestamp: string, claim: IssueClaim): StateEvent {
417
+ return {
418
+ schemaVersion: 1,
419
+ type: "issue.claim.released",
420
+ issueId,
421
+ timestamp,
422
+ sessionId: claim.status === "released" ? claim.releasedBySession : undefined,
423
+ payload: {
424
+ actor: claim.actor,
425
+ claimedBySession: claim.claimedBySession,
426
+ claimedAt: claim.claimedAt,
427
+ runId: claim.runId,
428
+ releasedAt: claim.status === "released" ? claim.releasedAt : undefined,
429
+ releasedBySession: claim.status === "released" ? claim.releasedBySession : undefined,
430
+ releaseReason: claim.status === "released" ? claim.releaseReason : undefined,
431
+ },
432
+ };
433
+ }
434
+
435
+ function issueClaimStaleEvent(
436
+ issueId: string,
437
+ timestamp: string,
438
+ claim: IssueClaim,
439
+ reconciledBySession: string,
440
+ ): StateEvent {
441
+ return {
442
+ schemaVersion: 1,
443
+ type: "issue.claim.stale",
444
+ issueId,
445
+ timestamp,
446
+ sessionId: reconciledBySession,
447
+ payload: {
448
+ actor: claim.actor,
449
+ claimedBySession: claim.claimedBySession,
450
+ claimedAt: claim.claimedAt,
451
+ runId: claim.runId,
452
+ reconciledBySession,
453
+ staleAt: claim.status === "stale" ? claim.staleAt : undefined,
454
+ staleReason: claim.status === "stale" ? claim.staleReason : undefined,
455
+ },
456
+ };
457
+ }