@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
package/core/runs.ts ADDED
@@ -0,0 +1,288 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+ import { getIssuePaths, readIssueState } from "./state";
12
+ import { resolveSessionId } from "./session";
13
+ import { classifyError, type ErrorType } from "./resilience";
14
+ import { getWorkflowEventEmitter } from "./workflow-event-emitter";
15
+
16
+ export const RUN_STATUSES = [
17
+ "preparing-workspace",
18
+ "building-prompt",
19
+ "launching-agent",
20
+ "streaming-turn",
21
+ "succeeded",
22
+ "failed",
23
+ "timed-out",
24
+ "stalled",
25
+ "canceled-by-reconciliation",
26
+ ] as const;
27
+
28
+ export type RunStatus = (typeof RUN_STATUSES)[number];
29
+
30
+ export const TERMINAL_RUN_STATUSES = [
31
+ "succeeded",
32
+ "failed",
33
+ "timed-out",
34
+ "stalled",
35
+ "canceled-by-reconciliation",
36
+ ] as const satisfies readonly RunStatus[];
37
+
38
+ export type TerminalRunStatus = (typeof TERMINAL_RUN_STATUSES)[number];
39
+
40
+ export interface RunEvent {
41
+ schemaVersion: 1;
42
+ type: string;
43
+ timestamp: string;
44
+ status: RunStatus;
45
+ message?: string;
46
+ payload?: Record<string, unknown>;
47
+ }
48
+
49
+ export interface RunRecord {
50
+ schemaVersion: 1;
51
+ issueId: string;
52
+ runId: string;
53
+ attempt: number;
54
+ status: RunStatus;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ sessionId: string;
58
+ workspacePath?: string;
59
+ failureReason?: string;
60
+ errorType?: ErrorType;
61
+ events: RunEvent[];
62
+ }
63
+
64
+ export interface StartRunInput {
65
+ root?: string;
66
+ issueId: string;
67
+ runId?: string;
68
+ attempt?: number;
69
+ status?: RunStatus | string;
70
+ workspacePath?: string;
71
+ message?: string;
72
+ payload?: Record<string, unknown>;
73
+ }
74
+
75
+ export interface AppendRunEventInput {
76
+ root?: string;
77
+ issueId: string;
78
+ runId: string;
79
+ type: string;
80
+ status?: RunStatus | string;
81
+ message?: string;
82
+ failureReason?: string;
83
+ errorType?: ErrorType;
84
+ payload?: Record<string, unknown>;
85
+ }
86
+
87
+ export interface RunRefInput {
88
+ root?: string;
89
+ issueId: string;
90
+ runId: string;
91
+ }
92
+
93
+ export function startRun(input: StartRunInput): RunRecord {
94
+ const root = input.root ?? process.cwd();
95
+ readIssueState({ root, issueId: input.issueId });
96
+ const now = new Date().toISOString();
97
+ const status = assertRunStatus(input.status ?? "preparing-workspace");
98
+ const run: RunRecord = {
99
+ schemaVersion: 1,
100
+ issueId: input.issueId,
101
+ runId: input.runId ? assertRunId(input.runId) : createRunId(now),
102
+ attempt: normalizeAttempt(input.attempt),
103
+ status,
104
+ createdAt: now,
105
+ updatedAt: now,
106
+ sessionId: resolveSessionId(),
107
+ workspacePath: input.workspacePath,
108
+ events: [
109
+ {
110
+ schemaVersion: 1,
111
+ type: "run.started",
112
+ timestamp: now,
113
+ status,
114
+ message: input.message,
115
+ payload: input.payload,
116
+ },
117
+ ],
118
+ };
119
+
120
+ try {
121
+ mkdirSync(runsDir(root, input.issueId), { recursive: true });
122
+ writeRunRecord(root, run);
123
+ } catch (rawError) {
124
+ const error = rawError instanceof Error ? rawError : new Error(String(rawError));
125
+ run.errorType = classifyError(error);
126
+ // Write what we can before re-throwing so the error type is discoverable
127
+ try {
128
+ writeRunRecord(root, run);
129
+ } catch {
130
+ // ignore secondary write failure
131
+ }
132
+ throw error;
133
+ }
134
+
135
+ getWorkflowEventEmitter().emit({
136
+ type: "run_started",
137
+ issueId: input.issueId,
138
+ runId: run.runId,
139
+ status: run.status,
140
+ timestamp: now,
141
+ });
142
+
143
+ return run;
144
+ }
145
+
146
+ export function appendRunEvent(input: AppendRunEventInput): RunRecord {
147
+ const root = input.root ?? process.cwd();
148
+ const run = readRun({ root, issueId: input.issueId, runId: input.runId });
149
+ const now = new Date().toISOString();
150
+ const status = assertRunStatus(input.status ?? run.status);
151
+
152
+ let errorType = input.errorType;
153
+ if (!errorType && input.failureReason) {
154
+ errorType = classifyError(new Error(input.failureReason));
155
+ }
156
+
157
+ const updated: RunRecord = {
158
+ ...run,
159
+ status,
160
+ updatedAt: now,
161
+ failureReason: input.failureReason ?? run.failureReason,
162
+ errorType: errorType ?? run.errorType,
163
+ events: [
164
+ ...run.events,
165
+ {
166
+ schemaVersion: 1,
167
+ type: input.type,
168
+ timestamp: now,
169
+ status,
170
+ message: input.message,
171
+ payload: input.payload,
172
+ },
173
+ ],
174
+ };
175
+
176
+ try {
177
+ writeRunRecord(root, updated);
178
+ } catch (rawError) {
179
+ const error = rawError instanceof Error ? rawError : new Error(String(rawError));
180
+ updated.errorType = classifyError(error);
181
+ try {
182
+ writeRunRecord(root, updated);
183
+ } catch {
184
+ // ignore secondary write failure
185
+ }
186
+ throw error;
187
+ }
188
+
189
+ if (isTerminalRunStatus(status)) {
190
+ if (status === "failed" || status === "timed-out" || status === "stalled" || status === "canceled-by-reconciliation") {
191
+ getWorkflowEventEmitter().emit({
192
+ type: "run_failed",
193
+ issueId: input.issueId,
194
+ runId: input.runId,
195
+ failureReason: input.failureReason,
196
+ timestamp: now,
197
+ });
198
+ } else {
199
+ getWorkflowEventEmitter().emit({
200
+ type: "run_completed",
201
+ issueId: input.issueId,
202
+ runId: input.runId,
203
+ status,
204
+ timestamp: now,
205
+ });
206
+ }
207
+ }
208
+
209
+ return updated;
210
+ }
211
+
212
+ export function readRun(input: RunRefInput): RunRecord {
213
+ const root = input.root ?? process.cwd();
214
+ const file = runPath(root, input.issueId, input.runId);
215
+ if (!existsSync(file)) {
216
+ throw new Error(`Run not found: ${input.runId}`);
217
+ }
218
+ return JSON.parse(readFileSync(file, "utf8")) as RunRecord;
219
+ }
220
+
221
+ export function deleteRun(input: RunRefInput): boolean {
222
+ const root = input.root ?? process.cwd();
223
+ const file = runPath(root, input.issueId, input.runId);
224
+ if (!existsSync(file)) {
225
+ return false;
226
+ }
227
+ unlinkSync(file);
228
+ return true;
229
+ }
230
+
231
+ export function listRuns(input: { root?: string; issueId: string }): RunRecord[] {
232
+ const root = input.root ?? process.cwd();
233
+ readIssueState({ root, issueId: input.issueId });
234
+ const dir = runsDir(root, input.issueId);
235
+ if (!existsSync(dir)) {
236
+ return [];
237
+ }
238
+
239
+ return readdirSync(dir)
240
+ .filter((name) => name.endsWith(".json"))
241
+ .map((name) => JSON.parse(readFileSync(join(dir, name), "utf8")) as RunRecord)
242
+ .sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
243
+ }
244
+
245
+ export function isTerminalRunStatus(status: RunStatus): status is TerminalRunStatus {
246
+ return TERMINAL_RUN_STATUSES.includes(status as TerminalRunStatus);
247
+ }
248
+
249
+ function writeRunRecord(root: string, run: RunRecord) {
250
+ writeFileSync(runPath(root, run.issueId, run.runId), `${JSON.stringify(run, null, 2)}\n`);
251
+ }
252
+
253
+ function runsDir(root: string, issueId: string) {
254
+ return join(getIssuePaths(root, issueId).issueDir, "runs");
255
+ }
256
+
257
+ function runPath(root: string, issueId: string, runId: string) {
258
+ return join(runsDir(root, issueId), `${assertRunId(runId)}.json`);
259
+ }
260
+
261
+ export function createRunId(timestamp: string) {
262
+ const compact = timestamp.replace(/[-:.TZ]/g, "").slice(0, 14);
263
+ return `run-${compact}-${randomUUID().slice(0, 8)}`;
264
+ }
265
+
266
+ function assertRunId(value: string) {
267
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) {
268
+ throw new Error(`Invalid run id: ${value}`);
269
+ }
270
+ return value;
271
+ }
272
+
273
+ function assertRunStatus(value: string): RunStatus {
274
+ if (!RUN_STATUSES.includes(value as RunStatus)) {
275
+ throw new Error(`Invalid run status: ${value}`);
276
+ }
277
+ return value as RunStatus;
278
+ }
279
+
280
+ function normalizeAttempt(value: number | undefined) {
281
+ if (value === undefined) {
282
+ return 1;
283
+ }
284
+ if (!Number.isInteger(value) || value <= 0) {
285
+ throw new Error("run attempt must be a positive integer");
286
+ }
287
+ return value;
288
+ }
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { normalizePath, isPathInsideRoot, assertPathInsideRoot, ensureInside } from "./safe-path";
3
+ import { resolve, join } from "node:path";
4
+ import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+
7
+ let tmpDir: string;
8
+
9
+ function setupTmp() {
10
+ tmpDir = mkdtempSync(join(tmpdir(), "safe-path-test-"));
11
+ mkdirSync(join(tmpDir, "foo", "bar"), { recursive: true });
12
+ }
13
+
14
+ function cleanupTmp() {
15
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
16
+ }
17
+
18
+ describe("normalizePath", () => {
19
+ it("resolves relative paths", () => {
20
+ expect(normalizePath(".")).toBe(resolve("."));
21
+ });
22
+
23
+ it("normalizes trailing slashes", () => {
24
+ const p = normalizePath("/tmp/");
25
+ expect(p.endsWith("/")).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe("isPathInsideRoot", () => {
30
+ beforeEach(() => setupTmp());
31
+ afterEach(() => cleanupTmp());
32
+
33
+ it("returns true for paths inside root", () => {
34
+ expect(isPathInsideRoot(tmpDir, join(tmpDir, "foo"))).toBe(true);
35
+ expect(isPathInsideRoot(tmpDir, join(tmpDir, "foo", "bar"))).toBe(true);
36
+ });
37
+
38
+ it("returns true for root itself", () => {
39
+ expect(isPathInsideRoot(tmpDir, tmpDir)).toBe(true);
40
+ });
41
+
42
+ it("returns false for paths outside root", () => {
43
+ expect(isPathInsideRoot(tmpDir, "/usr")).toBe(false);
44
+ expect(isPathInsideRoot(join(tmpDir, "foo"), join(tmpDir, "bar"))).toBe(false);
45
+ });
46
+
47
+ it("returns false for traversal escapes", () => {
48
+ expect(isPathInsideRoot(tmpDir, join(tmpDir, "..", "etc"))).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe("assertPathInsideRoot", () => {
53
+ beforeEach(() => setupTmp());
54
+ afterEach(() => cleanupTmp());
55
+
56
+ it("does not throw for valid paths", () => {
57
+ expect(() => assertPathInsideRoot(tmpDir, join(tmpDir, "foo"))).not.toThrow();
58
+ });
59
+
60
+ it("throws for escaped paths", () => {
61
+ expect(() => assertPathInsideRoot(tmpDir, "/usr")).toThrow("Path escapes allowed root");
62
+ });
63
+ });
64
+
65
+ describe("ensureInside", () => {
66
+ beforeEach(() => setupTmp());
67
+ afterEach(() => cleanupTmp());
68
+
69
+ it("accepts single root", () => {
70
+ expect(() => ensureInside(join(tmpDir, "foo"), tmpDir)).not.toThrow();
71
+ });
72
+
73
+ it("accepts multiple roots if one matches", () => {
74
+ expect(() => ensureInside(join(tmpDir, "foo"), ["/usr", tmpDir])).not.toThrow();
75
+ });
76
+
77
+ it("throws if none match", () => {
78
+ expect(() => ensureInside("/usr", ["/tmp", "/var"])).toThrow("Path escape blocked");
79
+ });
80
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Safe Path — shared path escape guard.
3
+ *
4
+ * Extracted from core/gate.ts and core/workspace-runtime.ts.
5
+ * All file-system writes must validate target paths through these utilities.
6
+ */
7
+
8
+ import { realpathSync } from "node:fs";
9
+ import { isAbsolute, relative, resolve } from "node:path";
10
+
11
+ /**
12
+ * Normalize a path for comparison.
13
+ * Resolves symlinks via realpathSync if available; falls back to resolve.
14
+ */
15
+ export function normalizePath(path: string): string {
16
+ const resolved = resolve(path).replace(/\/+$/, "");
17
+ try {
18
+ return realpathSync.native(resolved);
19
+ } catch {
20
+ return resolved;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Check whether `candidate` is inside `root` (inclusive).
26
+ */
27
+ export function isPathInsideRoot(root: string, candidate: string): boolean {
28
+ const normalizedRoot = normalizePath(root);
29
+ const normalizedCandidate = normalizePath(candidate);
30
+ const rel = relative(normalizedRoot, normalizedCandidate);
31
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
32
+ }
33
+
34
+ /**
35
+ * Assert that `candidate` is inside `root`. Throws if not.
36
+ */
37
+ export function assertPathInsideRoot(root: string, candidate: string): void {
38
+ if (!isPathInsideRoot(root, candidate)) {
39
+ throw new Error(
40
+ `Path escapes allowed root: candidate=${resolve(candidate)} root=${normalizePath(root)}`
41
+ );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Ensure a write target is inside one of the allowed roots.
47
+ * Accepts multiple allowed roots (e.g. project root + tmp dir).
48
+ */
49
+ export function ensureInside(
50
+ target: string,
51
+ allowedRoot: string | readonly string[]
52
+ ): void {
53
+ const roots = Array.isArray(allowedRoot) ? allowedRoot : [allowedRoot];
54
+ const ok = roots.some((root) => isPathInsideRoot(root, target));
55
+ if (!ok) {
56
+ throw new Error(
57
+ `Path escape blocked: target=${resolve(target)} allowedRoots=${roots.map(normalizePath).join(", ")}`
58
+ );
59
+ }
60
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { runSddGate, extractConstitutionCheck, formatSddGateResult } from "./sdd-gate";
3
+
4
+ describe("runSddGate", () => {
5
+ it("passes when all automated checks are true", () => {
6
+ const result = runSddGate({
7
+ capabilityDeclared: true,
8
+ testStrategyDefined: true,
9
+ simplicityJustified: true,
10
+ integrationPathClear: true,
11
+ });
12
+ expect(result.passed).toBe(true);
13
+ expect(result.failedChecks).toHaveLength(0);
14
+ expect(result.passedChecks).toHaveLength(4);
15
+ });
16
+
17
+ it("fails when any automated check is false", () => {
18
+ const result = runSddGate({
19
+ capabilityDeclared: true,
20
+ testStrategyDefined: false,
21
+ simplicityJustified: true,
22
+ integrationPathClear: true,
23
+ });
24
+ expect(result.passed).toBe(false);
25
+ expect(result.failedChecks.length).toBeGreaterThan(0);
26
+ expect(result.failedChecks[0].article).toContain("Test-First");
27
+ });
28
+
29
+ it("fails when all automated checks are missing", () => {
30
+ const result = runSddGate({});
31
+ expect(result.passed).toBe(false);
32
+ expect(result.failedChecks).toHaveLength(4);
33
+ });
34
+
35
+ it("includes manual review articles", () => {
36
+ const result = runSddGate({
37
+ capabilityDeclared: true,
38
+ testStrategyDefined: true,
39
+ simplicityJustified: true,
40
+ integrationPathClear: true,
41
+ });
42
+ expect(result.manualReviewArticles.length).toBeGreaterThan(0);
43
+ expect(result.manualReviewArticles.some((a) => a.includes("Anti-Abstraction"))).toBe(true);
44
+ });
45
+ });
46
+
47
+ describe("extractConstitutionCheck", () => {
48
+ it("extracts valid constitution check", () => {
49
+ const payload = {
50
+ constitutionCheck: {
51
+ capabilityDeclared: true,
52
+ capabilitySlice: "execution.multi-agent",
53
+ testStrategyDefined: true,
54
+ testStrategy: "unit + integration",
55
+ simplicityJustified: true,
56
+ simplicityJustification: "3 modules",
57
+ integrationPathClear: true,
58
+ integrationPath: "real CLI in worktree",
59
+ },
60
+ };
61
+ const result = extractConstitutionCheck(payload);
62
+ expect(result).not.toBeNull();
63
+ expect(result!.capabilityDeclared).toBe(true);
64
+ expect(result!.capabilitySlice).toBe("execution.multi-agent");
65
+ });
66
+
67
+ it("returns null when constitutionCheck is missing", () => {
68
+ expect(extractConstitutionCheck({})).toBeNull();
69
+ });
70
+
71
+ it("handles partial data with defaults", () => {
72
+ const payload = { constitutionCheck: { capabilityDeclared: true } };
73
+ const result = extractConstitutionCheck(payload);
74
+ expect(result!.capabilityDeclared).toBe(true);
75
+ expect(result!.testStrategyDefined).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("formatSddGateResult", () => {
80
+ it("formats passed result", () => {
81
+ const result = runSddGate({
82
+ capabilityDeclared: true,
83
+ testStrategyDefined: true,
84
+ simplicityJustified: true,
85
+ integrationPathClear: true,
86
+ });
87
+ const text = formatSddGateResult(result);
88
+ expect(text).toContain("PASSED");
89
+ expect(text).toContain("✅");
90
+ });
91
+
92
+ it("formats failed result", () => {
93
+ const result = runSddGate({ capabilityDeclared: false });
94
+ const text = formatSddGateResult(result);
95
+ expect(text).toContain("FAILED");
96
+ expect(text).toContain("❌");
97
+ });
98
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * SDD Gate — automated Nine Articles checker.
3
+ *
4
+ * Validates that an implementation-plan artifact passes the Phase -1 Gates
5
+ * before allowing triage → plan advancement.
6
+ *
7
+ * Articles checked automatically:
8
+ * 1. Capability-First → constitutionCheck.capabilityDeclared
9
+ * 3. Test-First → constitutionCheck.testStrategyDefined
10
+ * 7. Simplicity Gate → constitutionCheck.simplicityJustified
11
+ * 9. Integration-First → constitutionCheck.integrationPathClear
12
+ *
13
+ * Articles requiring human judgment (not automated here):
14
+ * 2. CLI Interface Mandate, 4. Composition, 5. Explicit, 6. Fail Fast, 8. Anti-Abstraction
15
+ */
16
+
17
+ export interface ConstitutionCheck {
18
+ capabilityDeclared: boolean;
19
+ capabilitySlice?: string;
20
+ testStrategyDefined: boolean;
21
+ testStrategy?: string;
22
+ simplicityJustified: boolean;
23
+ simplicityJustification?: string;
24
+ integrationPathClear: boolean;
25
+ integrationPath?: string;
26
+ }
27
+
28
+ export interface SddGateResult {
29
+ passed: boolean;
30
+ /** Which articles passed */
31
+ passedChecks: string[];
32
+ /** Which articles failed with reasons */
33
+ failedChecks: { article: string; reason: string }[];
34
+ /** Human-judgment articles that require manual review */
35
+ manualReviewArticles: string[];
36
+ }
37
+
38
+ const AUTOMATED_ARTICLES = [
39
+ { key: "capabilityDeclared", name: "Capability-First (Article 1)" },
40
+ { key: "testStrategyDefined", name: "Test-First (Article 3)" },
41
+ { key: "simplicityJustified", name: "Simplicity Gate (Article 7)" },
42
+ { key: "integrationPathClear", name: "Integration-First (Article 9)" },
43
+ ] as const;
44
+
45
+ const MANUAL_ARTICLES = [
46
+ "CLI Interface Mandate (Article 2)",
47
+ "Composition over Inheritance (Article 4)",
48
+ "Explicit over Implicit (Article 5)",
49
+ "Fail Fast, Fail Loud (Article 6)",
50
+ "Anti-Abstraction (Article 8)",
51
+ ];
52
+
53
+ /**
54
+ * Run automated SDD constitution checks against a constitutionCheck payload.
55
+ */
56
+ export function runSddGate(check: Partial<ConstitutionCheck>): SddGateResult {
57
+ const passedChecks: string[] = [];
58
+ const failedChecks: { article: string; reason: string }[] = [];
59
+
60
+ for (const article of AUTOMATED_ARTICLES) {
61
+ const value = check[article.key as keyof ConstitutionCheck];
62
+ if (value === true) {
63
+ passedChecks.push(article.name);
64
+ } else {
65
+ failedChecks.push({
66
+ article: article.name,
67
+ reason: `${article.key} is not true`,
68
+ });
69
+ }
70
+ }
71
+
72
+ return {
73
+ passed: failedChecks.length === 0,
74
+ passedChecks,
75
+ failedChecks,
76
+ manualReviewArticles: MANUAL_ARTICLES,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Validate a raw implementation-plan payload and return a typed ConstitutionCheck.
82
+ * Returns null if the payload does not contain a constitutionCheck field.
83
+ */
84
+ export function extractConstitutionCheck(
85
+ planPayload: Record<string, unknown>
86
+ ): ConstitutionCheck | null {
87
+ const raw = planPayload.constitutionCheck;
88
+ if (!raw || typeof raw !== "object") return null;
89
+
90
+ const c = raw as Record<string, unknown>;
91
+ return {
92
+ capabilityDeclared: c.capabilityDeclared === true,
93
+ capabilitySlice: typeof c.capabilitySlice === "string" ? c.capabilitySlice : undefined,
94
+ testStrategyDefined: c.testStrategyDefined === true,
95
+ testStrategy: typeof c.testStrategy === "string" ? c.testStrategy : undefined,
96
+ simplicityJustified: c.simplicityJustified === true,
97
+ simplicityJustification: typeof c.simplicityJustification === "string" ? c.simplicityJustification : undefined,
98
+ integrationPathClear: c.integrationPathClear === true,
99
+ integrationPath: typeof c.integrationPath === "string" ? c.integrationPath : undefined,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Format SDD gate result for CLI output.
105
+ */
106
+ export function formatSddGateResult(result: SddGateResult): string {
107
+ const lines: string[] = [];
108
+
109
+ lines.push(`SDD Constitution Check: ${result.passed ? "✅ PASSED" : "❌ FAILED"}`);
110
+ lines.push("");
111
+
112
+ if (result.passedChecks.length > 0) {
113
+ lines.push("Automated checks passed:");
114
+ for (const name of result.passedChecks) {
115
+ lines.push(` ✅ ${name}`);
116
+ }
117
+ }
118
+
119
+ if (result.failedChecks.length > 0) {
120
+ lines.push("");
121
+ lines.push("Automated checks failed:");
122
+ for (const item of result.failedChecks) {
123
+ lines.push(` ❌ ${item.article}: ${item.reason}`);
124
+ }
125
+ }
126
+
127
+ lines.push("");
128
+ lines.push("Manual review required:");
129
+ for (const name of result.manualReviewArticles) {
130
+ lines.push(` ⚠️ ${name}`);
131
+ }
132
+
133
+ return lines.join("\n");
134
+ }