@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/config.ts ADDED
@@ -0,0 +1,533 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export type WorktreeEnforcement = "required" | "forbidden" | "optional" | "unset";
6
+ export type WorktreeDefault = "use" | "skip" | "ask";
7
+ export type ConfigValueSource = "config-repo" | "config-global" | "env" | "default" | "unset";
8
+
9
+ export interface ConfigEntry {
10
+ key: KnownConfigKey;
11
+ value: unknown;
12
+ source: ConfigValueSource;
13
+ description: string;
14
+ }
15
+
16
+ export interface WorktreePolicy {
17
+ enforcement: WorktreeEnforcement;
18
+ default: WorktreeDefault;
19
+ source: PolicySource;
20
+ }
21
+
22
+ export type PolicySource =
23
+ | "config-repo"
24
+ | "config-global"
25
+ | "user-message"
26
+ | "agents-md"
27
+ | "default";
28
+
29
+ export interface ResolveWorktreePolicyInput {
30
+ /** Repo root (defaults to cwd). */
31
+ root?: string;
32
+ /** Override $HOME. */
33
+ home?: string;
34
+ /**
35
+ * Whether the user explicitly stated worktree preference in this conversation.
36
+ * The CLI itself can't know this — the LLM must pass it in.
37
+ */
38
+ userMessage?: { enforcement?: WorktreeEnforcement; default?: WorktreeDefault };
39
+ /** Explicit AGENTS.md content (testing). Otherwise read from repo. */
40
+ agentsMdContent?: string;
41
+ }
42
+
43
+ export type SyncProvider = "linear" | "github" | "none";
44
+
45
+ interface ConfigDoc {
46
+ worktree?: {
47
+ enforcement?: WorktreeEnforcement;
48
+ default?: WorktreeDefault;
49
+ baseBranch?: string;
50
+ };
51
+ workspace?: {
52
+ root?: string;
53
+ basePort?: number;
54
+ };
55
+ update_check?: boolean;
56
+ sync?: {
57
+ provider?: SyncProvider;
58
+ linearTeamId?: string;
59
+ linearTeamKey?: string;
60
+ autoSync?: boolean;
61
+ syncArtifacts?: boolean;
62
+ };
63
+ [k: string]: unknown;
64
+ }
65
+
66
+ const CONFIG_FILENAME = "config.json";
67
+ const WORKTREE_ENFORCEMENT_VALUES = ["required", "forbidden", "optional", "unset"] as const;
68
+ const WORKTREE_DEFAULT_VALUES = ["use", "skip", "ask"] as const;
69
+ const SYNC_PROVIDER_VALUES = ["linear", "github", "none"] as const;
70
+
71
+ function getGxpmHome(): string {
72
+ const envHome = process.env.GXPM_HOME;
73
+ if (envHome && envHome.trim()) return envHome.trim();
74
+ return homedir();
75
+ }
76
+
77
+ function getRepoConfigPath(root: string): string {
78
+ const envPath = process.env.GXPM_CONFIG_PATH;
79
+ if (envPath && envPath.trim()) return envPath.trim();
80
+ return join(root, ".gxpm", CONFIG_FILENAME);
81
+ }
82
+
83
+ const CONFIG_REGISTRY = {
84
+ "worktree.enforcement": {
85
+ defaultValue: "optional" satisfies WorktreeEnforcement,
86
+ description: "Worktree policy enforcement: required, forbidden, optional, or unset.",
87
+ normalize: (value: unknown) => normalizeEnum("worktree.enforcement", value, WORKTREE_ENFORCEMENT_VALUES),
88
+ },
89
+ "worktree.default": {
90
+ defaultValue: "ask" satisfies WorktreeDefault,
91
+ description: "Default worktree choice when enforcement is optional: use, skip, or ask.",
92
+ normalize: (value: unknown) => normalizeEnum("worktree.default", value, WORKTREE_DEFAULT_VALUES),
93
+ },
94
+ "worktree.baseBranch": {
95
+ defaultValue: "main",
96
+ description: "Default branch name used as the base for new worktrees (e.g., main, master, develop).",
97
+ normalize: (value: unknown) => normalizeNonEmptyString("worktree.baseBranch", value),
98
+ },
99
+ "workspace.root": {
100
+ defaultValue: ".gxpm/local/workspaces",
101
+ description: "Default root for gxpm-managed per-issue execution workspaces.",
102
+ normalize: (value: unknown) => normalizeNonEmptyString("workspace.root", value),
103
+ },
104
+ "workspace.basePort": {
105
+ defaultValue: 3090,
106
+ description: "Base port for deterministic dev server port allocation in workspaces.",
107
+ normalize: (value: unknown) => normalizePositiveInteger("workspace.basePort", value),
108
+ },
109
+ update_check: {
110
+ defaultValue: true,
111
+ description: "Whether gxpm-update-check should check the remote VERSION.",
112
+ normalize: normalizeBoolean,
113
+ },
114
+ "sync.provider": {
115
+ defaultValue: "none" satisfies SyncProvider,
116
+ description: "Issue tracker sync provider: linear, github, or none.",
117
+ normalize: (value: unknown) => normalizeEnum("sync.provider", value, SYNC_PROVIDER_VALUES),
118
+ },
119
+ "sync.linearTeamId": {
120
+ defaultValue: "",
121
+ description: "Linear team ID (UUID) for issue creation.",
122
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
123
+ },
124
+ "sync.linearTeamKey": {
125
+ defaultValue: "",
126
+ description: "Linear team key (e.g., ENG) used in issue identifiers.",
127
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
128
+ },
129
+ "sync.autoSync": {
130
+ defaultValue: true,
131
+ description: "Automatically sync local state to the configured issue tracker.",
132
+ normalize: normalizeBoolean,
133
+ },
134
+ "sync.syncArtifacts": {
135
+ defaultValue: true,
136
+ description: "Sync artifact summaries to the issue tracker description.",
137
+ normalize: normalizeBoolean,
138
+ },
139
+ "sync.linearAssigneeId": {
140
+ defaultValue: "",
141
+ description: "Linear user ID to assign as default assignee for synced issues.",
142
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
143
+ },
144
+ "agent.name": {
145
+ defaultValue: "",
146
+ description: "Human-readable agent identity used as issue creator/assignee name. Falls back to host name if empty.",
147
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
148
+ },
149
+ "feedback.gxpmSourceRoot": {
150
+ defaultValue: "",
151
+ description: "Absolute path to the gxpm source repository where feedback issues are created. When empty, feedback creation is disabled.",
152
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
153
+ },
154
+ "worktree.initSteps": {
155
+ defaultValue: ["owner-marker", "issue-context"] as string[],
156
+ description: "Ordered list of worktree initialization step names run after worktree creation.",
157
+ normalize: (value: unknown) => normalizeStringArray("worktree.initSteps", value),
158
+ },
159
+ "worktree.nodeModulesMode": {
160
+ defaultValue: "symlink" as "symlink" | "overlay" | "none",
161
+ description: "How to set up node_modules in worktrees: symlink (direct), overlay (root overlay + local packages), or none.",
162
+ normalize: (value: unknown) => normalizeEnum("worktree.nodeModulesMode", value, ["symlink", "overlay", "none"]),
163
+ },
164
+ "worktree.envFiles": {
165
+ defaultValue: [] as string[],
166
+ description: "Relative paths of .env files to symlink from the main repo into worktrees.",
167
+ normalize: (value: unknown) => normalizeStringArray("worktree.envFiles", value),
168
+ },
169
+ "worktree.envLocalFiles": {
170
+ defaultValue: [] as string[],
171
+ description: "Relative paths of .env.local files to generate in worktrees with isolated ports.",
172
+ normalize: (value: unknown) => normalizeStringArray("worktree.envLocalFiles", value),
173
+ },
174
+ "worktree.sharedDirs": {
175
+ defaultValue: [] as string[],
176
+ description: "Relative paths of directories to symlink from main repo into worktrees (e.g. .gstack, .qoder).",
177
+ normalize: (value: unknown) => normalizeStringArray("worktree.sharedDirs", value),
178
+ },
179
+ "worktree.portSlots": {
180
+ defaultValue: 10,
181
+ description: "Number of port slots for worktree isolation. Each slot gets a unique port offset.",
182
+ normalize: (value: unknown) => normalizePositiveInteger("worktree.portSlots", value),
183
+ },
184
+ "worktree.portBaseWeb": {
185
+ defaultValue: 5173,
186
+ description: "Base port for the web dev server in worktrees.",
187
+ normalize: (value: unknown) => normalizePositiveInteger("worktree.portBaseWeb", value),
188
+ },
189
+ "worktree.portBaseServer": {
190
+ defaultValue: 3000,
191
+ description: "Base port for the API server in worktrees.",
192
+ normalize: (value: unknown) => normalizePositiveInteger("worktree.portBaseServer", value),
193
+ },
194
+ "worktree.portBaseStudio": {
195
+ defaultValue: 4111,
196
+ description: "Base port for the Mastra Studio / debug server in worktrees.",
197
+ normalize: (value: unknown) => normalizePositiveInteger("worktree.portBaseStudio", value),
198
+ },
199
+ "worktree.warpMdPath": {
200
+ defaultValue: "warp.md",
201
+ description: "Relative path inside the worktree where warp.md should be written.",
202
+ normalize: (value: unknown) => normalizeNonEmptyString("worktree.warpMdPath", value),
203
+ },
204
+ "worktree.launchJsonPath": {
205
+ defaultValue: ".claude/launch.json",
206
+ description: "Relative path inside the worktree where .claude/launch.json should be written.",
207
+ normalize: (value: unknown) => normalizeNonEmptyString("worktree.launchJsonPath", value),
208
+ },
209
+ "worktree.hooksScript": {
210
+ defaultValue: "",
211
+ description: "Relative path to a script that installs git hooks in the worktree (e.g. scripts/setup/git-hooks/install-hooks.mjs). Empty = skip.",
212
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
213
+ },
214
+ "worktree.baselineFreshnessScript": {
215
+ defaultValue: "",
216
+ description: "Relative path to a script that checks baseline freshness (e.g. scripts/test/guards/assert-worktree-baseline-freshness.mjs). Empty = use built-in check.",
217
+ normalize: (value: unknown) => (typeof value === "string" ? value : ""),
218
+ },
219
+ } as const;
220
+
221
+ export type KnownConfigKey = keyof typeof CONFIG_REGISTRY;
222
+ export const KNOWN_CONFIG_KEYS = Object.keys(CONFIG_REGISTRY) as KnownConfigKey[];
223
+
224
+ function repoConfigPath(root: string) {
225
+ return getRepoConfigPath(root);
226
+ }
227
+ function globalConfigPath(home: string) {
228
+ return join(home, ".gxpm", CONFIG_FILENAME);
229
+ }
230
+
231
+ function readConfig(path: string): ConfigDoc {
232
+ if (!existsSync(path)) return {};
233
+ try {
234
+ return JSON.parse(readFileSync(path, "utf8")) as ConfigDoc;
235
+ } catch {
236
+ return {};
237
+ }
238
+ }
239
+
240
+ function writeConfig(path: string, doc: ConfigDoc) {
241
+ mkdirSync(dirname(path), { recursive: true });
242
+ writeFileSync(path, JSON.stringify(doc, null, 2) + "\n");
243
+ }
244
+
245
+ function getEnvConfigValue(key: string): unknown | undefined {
246
+ const envMap: Record<string, string> = {
247
+ "sync.provider": "GXPM_SYNC_PROVIDER",
248
+ "sync.linearTeamId": "GXPM_LINEAR_TEAM_ID",
249
+ "sync.linearTeamKey": "GXPM_LINEAR_TEAM_KEY",
250
+ "sync.linearAssigneeId": "GXPM_LINEAR_ASSIGNEE_ID",
251
+ "sync.autoSync": "GXPM_AUTO_SYNC",
252
+ "sync.syncArtifacts": "GXPM_SYNC_ARTIFACTS",
253
+ "worktree.enforcement": "GXPM_WORKTREE_ENFORCEMENT",
254
+ "worktree.default": "GXPM_WORKTREE_DEFAULT",
255
+ "worktree.baseBranch": "GXPM_WORKTREE_BASE_BRANCH",
256
+ "workspace.root": "GXPM_WORKSPACE_ROOT",
257
+ "workspace.basePort": "GXPM_WORKSPACE_BASE_PORT",
258
+ update_check: "GXPM_UPDATE_CHECK",
259
+ "agent.name": "GXPM_AGENT_NAME",
260
+ "feedback.gxpmSourceRoot": "GXPM_FEEDBACK_SOURCE_ROOT",
261
+ "worktree.initSteps": "GXPM_WORKTREE_INIT_STEPS",
262
+ "worktree.nodeModulesMode": "GXPM_WORKTREE_NODE_MODULES_MODE",
263
+ "worktree.envFiles": "GXPM_WORKTREE_ENV_FILES",
264
+ "worktree.envLocalFiles": "GXPM_WORKTREE_ENV_LOCAL_FILES",
265
+ "worktree.sharedDirs": "GXPM_WORKTREE_SHARED_DIRS",
266
+ "worktree.portSlots": "GXPM_WORKTREE_PORT_SLOTS",
267
+ "worktree.portBaseWeb": "GXPM_WORKTREE_PORT_BASE_WEB",
268
+ "worktree.portBaseServer": "GXPM_WORKTREE_PORT_BASE_SERVER",
269
+ "worktree.portBaseStudio": "GXPM_WORKTREE_PORT_BASE_STUDIO",
270
+ "worktree.warpMdPath": "GXPM_WORKTREE_WARP_MD_PATH",
271
+ "worktree.launchJsonPath": "GXPM_WORKTREE_LAUNCH_JSON_PATH",
272
+ "worktree.hooksScript": "GXPM_WORKTREE_HOOKS_SCRIPT",
273
+ "worktree.baselineFreshnessScript": "GXPM_WORKTREE_BASELINE_SCRIPT",
274
+ };
275
+ const envKey = envMap[key];
276
+ if (!envKey) return undefined;
277
+ const raw = process.env[envKey];
278
+ if (raw === undefined) return undefined;
279
+ if (raw === "true") return true;
280
+ if (raw === "false") return false;
281
+ if (/^-?\d+$/.test(raw)) return Number(raw);
282
+ return raw;
283
+ }
284
+
285
+ export function getConfigValue(input: { root?: string; home?: string; key: string }): {
286
+ value: unknown;
287
+ source: "config-repo" | "config-global" | "env" | "unset";
288
+ } {
289
+ const root = input.root ?? process.cwd();
290
+ // Check env var override first for select keys
291
+ const envVal = getEnvConfigValue(input.key);
292
+ if (envVal !== undefined) return { value: envVal, source: "env" };
293
+
294
+ const repo = readConfig(repoConfigPath(root));
295
+ const repoVal = lookup(repo, input.key);
296
+ if (repoVal !== undefined) return { value: repoVal, source: "config-repo" };
297
+ const global = readConfig(globalConfigPath(input.home ?? getGxpmHome()));
298
+ const globalVal = lookup(global, input.key);
299
+ if (globalVal !== undefined) return { value: globalVal, source: "config-global" };
300
+ return { value: undefined, source: "unset" };
301
+ }
302
+
303
+ export function getResolvedConfigValue(input: { root?: string; home?: string; key: string }): {
304
+ value: unknown;
305
+ source: ConfigValueSource;
306
+ } {
307
+ assertKnownConfigKey(input.key);
308
+ const explicit = getConfigValue(input);
309
+ if (explicit.value !== undefined) {
310
+ return { value: explicit.value, source: explicit.source };
311
+ }
312
+ return { value: CONFIG_REGISTRY[input.key].defaultValue, source: "default" };
313
+ }
314
+
315
+ export function setConfigValue(input: {
316
+ root?: string;
317
+ home?: string;
318
+ scope: "repo" | "global";
319
+ key: string;
320
+ value: unknown;
321
+ }) {
322
+ assertKnownConfigKey(input.key);
323
+ const path =
324
+ input.scope === "repo"
325
+ ? repoConfigPath(input.root ?? process.cwd())
326
+ : globalConfigPath(input.home ?? homedir());
327
+ const doc = readConfig(path);
328
+ assign(doc, input.key, normalizeConfigValue(input.key, input.value));
329
+ writeConfig(path, doc);
330
+ return path;
331
+ }
332
+
333
+ export function listConfig(input: { root?: string; home?: string } = {}): {
334
+ repo: ConfigDoc;
335
+ global: ConfigDoc;
336
+ } {
337
+ return {
338
+ repo: readConfig(repoConfigPath(input.root ?? process.cwd())),
339
+ global: readConfig(globalConfigPath(input.home ?? homedir())),
340
+ };
341
+ }
342
+
343
+ export function listConfigEntries(input: { root?: string; home?: string } = {}): ConfigEntry[] {
344
+ return KNOWN_CONFIG_KEYS.map((key) => {
345
+ const resolved = getResolvedConfigValue({ ...input, key });
346
+ return {
347
+ key,
348
+ value: resolved.value,
349
+ source: resolved.source,
350
+ description: CONFIG_REGISTRY[key].description,
351
+ };
352
+ });
353
+ }
354
+
355
+ /**
356
+ * Parse a `## gxpm Config` block from AGENTS.md content. Recognizes lines like
357
+ * - worktree.enforcement: required
358
+ * - worktree.default: use
359
+ * Returns the partial config (or {} when no block found / no recognized keys).
360
+ */
361
+ export function parseAgentsMdConfig(content: string): ConfigDoc {
362
+ const lines = content.split("\n");
363
+ let inSection = false;
364
+ const sectionLines: string[] = [];
365
+ const headingPattern = /^##\s+gxpm\s+config\s*$/i;
366
+ const nextHeadingPattern = /^##\s/;
367
+
368
+ for (const line of lines) {
369
+ if (!inSection) {
370
+ if (headingPattern.test(line)) {
371
+ inSection = true;
372
+ }
373
+ continue;
374
+ }
375
+ if (nextHeadingPattern.test(line)) break;
376
+ sectionLines.push(line);
377
+ }
378
+
379
+ if (!inSection || sectionLines.length === 0) return {};
380
+
381
+ const doc: ConfigDoc = {};
382
+ for (const line of sectionLines) {
383
+ const m = line.match(/^\s*[-*]?\s*([a-zA-Z][a-zA-Z0-9_.]*)\s*:\s*([^\n#]+?)\s*$/);
384
+ if (!m) continue;
385
+ const key = m[1];
386
+ const valueRaw = m[2].trim().replace(/^["']|["']$/g, "");
387
+ if (!isKnownConfigKey(key)) continue;
388
+ assign(doc, key, normalizeConfigValue(key, normalizeValue(valueRaw)));
389
+ }
390
+ return doc;
391
+ }
392
+
393
+ /**
394
+ * Resolve worktree policy through the precedence chain:
395
+ * 1. config.json (repo > global) — hard-coded by maintainer, wins everything
396
+ * 2. user message (passed in) — current conversation override
397
+ * 3. AGENTS.md `## gxpm Config` block — repo convention
398
+ * 4. default: enforcement=optional, default=ask
399
+ */
400
+ export function resolveWorktreePolicy(
401
+ input: ResolveWorktreePolicyInput = {},
402
+ ): WorktreePolicy {
403
+ const root = input.root ?? process.cwd();
404
+ const home = input.home ?? homedir();
405
+
406
+ // 1. config.json (repo wins over global)
407
+ const cfgEnforce = getConfigValue({ root, home, key: "worktree.enforcement" });
408
+ const cfgDefault = getConfigValue({ root, home, key: "worktree.default" });
409
+ if (cfgEnforce.value !== undefined) {
410
+ return {
411
+ enforcement: cfgEnforce.value as WorktreeEnforcement,
412
+ default: ((cfgDefault.value as WorktreeDefault) ?? "ask") as WorktreeDefault,
413
+ source: cfgEnforce.source,
414
+ };
415
+ }
416
+
417
+ // 2. user message
418
+ if (input.userMessage?.enforcement !== undefined) {
419
+ return {
420
+ enforcement: input.userMessage.enforcement,
421
+ default: input.userMessage.default ?? "ask",
422
+ source: "user-message",
423
+ };
424
+ }
425
+
426
+ // 3. AGENTS.md
427
+ const agents = input.agentsMdContent ?? readAgentsMd(root);
428
+ if (agents) {
429
+ const parsed = parseAgentsMdConfig(agents);
430
+ const wt = (parsed.worktree ?? {}) as { enforcement?: WorktreeEnforcement; default?: WorktreeDefault };
431
+ if (wt.enforcement !== undefined) {
432
+ return {
433
+ enforcement: wt.enforcement,
434
+ default: wt.default ?? "ask",
435
+ source: "agents-md",
436
+ };
437
+ }
438
+ }
439
+
440
+ // 4. default
441
+ return { enforcement: "optional", default: "ask", source: "default" };
442
+ }
443
+
444
+ function readAgentsMd(root: string): string | null {
445
+ const path = join(root, "AGENTS.md");
446
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
447
+ }
448
+
449
+ function lookup(doc: Record<string, unknown>, key: string): unknown {
450
+ const parts = key.split(".");
451
+ let cur: any = doc;
452
+ for (const p of parts) {
453
+ if (cur && typeof cur === "object" && p in cur) cur = cur[p];
454
+ else return undefined;
455
+ }
456
+ return cur;
457
+ }
458
+
459
+ function assign(doc: Record<string, unknown>, key: string, value: unknown) {
460
+ const parts = key.split(".");
461
+ let cur: any = doc;
462
+ for (let i = 0; i < parts.length - 1; i += 1) {
463
+ const p = parts[i];
464
+ if (typeof cur[p] !== "object" || cur[p] === null) cur[p] = {};
465
+ cur = cur[p];
466
+ }
467
+ cur[parts[parts.length - 1]] = value;
468
+ }
469
+
470
+ function isKnownConfigKey(key: string): key is KnownConfigKey {
471
+ return key in CONFIG_REGISTRY;
472
+ }
473
+
474
+ function assertKnownConfigKey(key: string): asserts key is KnownConfigKey {
475
+ if (!isKnownConfigKey(key)) {
476
+ throw new Error(`Unknown config key: ${key}`);
477
+ }
478
+ }
479
+
480
+ function normalizeConfigValue(key: KnownConfigKey, value: unknown) {
481
+ return CONFIG_REGISTRY[key].normalize(value as never);
482
+ }
483
+
484
+ function normalizeBoolean(value: unknown) {
485
+ if (typeof value === "boolean") return value;
486
+ if (value === "true") return true;
487
+ if (value === "false") return false;
488
+ throw new Error("update_check must be true or false");
489
+ }
490
+
491
+ function normalizeNonEmptyString(key: string, value: unknown) {
492
+ if (typeof value !== "string" || value.trim() === "") {
493
+ throw new Error(`${key} must be a non-empty string`);
494
+ }
495
+ return value;
496
+ }
497
+
498
+ function normalizePositiveInteger(key: string, value: unknown) {
499
+ if (typeof value === "string") {
500
+ const parsed = Number(value);
501
+ if (Number.isInteger(parsed) && parsed >= 1) return parsed;
502
+ }
503
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
504
+ return value;
505
+ }
506
+ throw new Error(`${key} must be a positive integer`);
507
+ }
508
+
509
+ function normalizeEnum<T extends readonly string[]>(key: string, value: unknown, allowed: T): T[number] {
510
+ if (typeof value !== "string" || !allowed.includes(value)) {
511
+ throw new Error(`${key} must be one of: ${allowed.join(", ")}`);
512
+ }
513
+ return value as T[number];
514
+ }
515
+
516
+ function normalizeStringArray(key: string, value: unknown): string[] {
517
+ if (value === undefined || value === null) return [];
518
+ if (Array.isArray(value)) {
519
+ if (value.every((v) => typeof v === "string")) return value as string[];
520
+ throw new Error(`${key} must be an array of strings`);
521
+ }
522
+ if (typeof value === "string") {
523
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
524
+ }
525
+ throw new Error(`${key} must be an array of strings or a comma-separated string`);
526
+ }
527
+
528
+ function normalizeValue(raw: string): unknown {
529
+ if (raw === "true") return true;
530
+ if (raw === "false") return false;
531
+ if (/^-?\d+$/.test(raw)) return Number(raw);
532
+ return raw;
533
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+
3
+ const ScenarioSchema = z.object({
4
+ id: z.string().min(1),
5
+ name: z.string().min(1),
6
+ given: z.array(z.string().min(1)).min(1),
7
+ when: z.string().min(1),
8
+ then: z.array(z.string().min(1)).min(1),
9
+ examples: z.array(z.record(z.string(), z.unknown())).default([]),
10
+ stubPath: z.string().min(1),
11
+ });
12
+
13
+ const FeatureSchema = z.object({
14
+ title: z.string().min(1),
15
+ asA: z.string().min(1),
16
+ iWant: z.string().min(1),
17
+ soThat: z.string().min(1),
18
+ });
19
+
20
+ export const BehaviorSpecSchema = z.object({
21
+ $schema: z.literal("behavior-spec.v1"),
22
+ issueId: z.string().min(1),
23
+ createdAt: z.string().datetime(),
24
+ createdBy: z.string().min(1),
25
+ confirmedAt: z.string().datetime().nullable(),
26
+ confirmedBy: z.string().nullable(),
27
+ feature: FeatureSchema,
28
+ scenarios: z.array(ScenarioSchema).min(1),
29
+ guidelinesRef: z.string().min(1),
30
+ }).refine(
31
+ (spec) =>
32
+ (spec.confirmedAt === null && spec.confirmedBy === null) ||
33
+ (spec.confirmedAt !== null && spec.confirmedBy !== null),
34
+ { message: "confirmedAt and confirmedBy must both be null or both be set" },
35
+ );
36
+
37
+ export type BehaviorSpec = z.infer<typeof BehaviorSpecSchema>;
38
+ export type BehaviorSpecScenario = z.infer<typeof ScenarioSchema>;
@@ -0,0 +1,61 @@
1
+ // CONTRACT LAYER — zero external dependencies.
2
+ // Converter interfaces for multi-platform skill transformation.
3
+
4
+ export interface SkillFrontmatter {
5
+ [key: string]: string | number | boolean | undefined;
6
+ }
7
+
8
+ export interface SkillSection {
9
+ type: "heading" | "paragraph" | "code" | "blockquote" | "list" | "html" | "raw";
10
+ content: string;
11
+ level?: number; // for headings
12
+ lang?: string; // for code blocks
13
+ }
14
+
15
+ export interface ManagedBlock {
16
+ id: string;
17
+ content: string;
18
+ }
19
+
20
+ export interface SkillDocument {
21
+ frontmatter: SkillFrontmatter;
22
+ body: SkillSection[];
23
+ managedBlocks: ManagedBlock[];
24
+ raw: string; // original raw source for passthrough cases
25
+ }
26
+
27
+ export interface ParseOptions {
28
+ /** Whether to extract managed artifact blocks during parsing. */
29
+ extractManagedBlocks?: boolean;
30
+ }
31
+
32
+ export interface Parser {
33
+ parse(source: string, options?: ParseOptions): SkillDocument;
34
+ }
35
+
36
+ export interface ConvertOptions {
37
+ hostName: string;
38
+ /** Host-specific variables for template rendering. */
39
+ templateVars?: Record<string, string>;
40
+ }
41
+
42
+ export interface Converter {
43
+ convert(doc: SkillDocument, options: ConvertOptions): SkillDocument;
44
+ }
45
+
46
+ export interface WriteOptions {
47
+ /** If provided, merge generated managed blocks with existing user content. */
48
+ existingContent?: string;
49
+ /** The generated mark to prepend (e.g. AUTO-GENERATED comment). */
50
+ generatedMark?: string;
51
+ }
52
+
53
+ export interface TargetWriter {
54
+ write(doc: SkillDocument, options?: WriteOptions): string;
55
+ }
56
+
57
+ export interface TransformPipeline {
58
+ parser: Parser;
59
+ converter: Converter;
60
+ writer: TargetWriter;
61
+ }
@@ -0,0 +1,43 @@
1
+ // CONTRACT LAYER — zero external dependencies.
2
+ // Host adapters and skill tooling import types from this subpath.
3
+ // HARD RULE: This file must never import SDK packages or other runtime modules.
4
+
5
+ export type FrontmatterConfig =
6
+ | {
7
+ mode: "preserve";
8
+ keys?: never;
9
+ }
10
+ | {
11
+ mode: "allowlist";
12
+ keys: string[];
13
+ };
14
+
15
+ export interface HostHooksConfig {
16
+ /** Name of the configuration file that holds hooks (e.g. "settings.json", "hooks.json") */
17
+ configFileName: string;
18
+ /** Serialization format of the config file */
19
+ configFormat: "json" | "toml";
20
+ /** Path segments relative to the host root (e.g. [".claude", "settings.json"]) */
21
+ configPathSegments: string[];
22
+ /** Whether a feature flag must be enabled for hooks to fire */
23
+ featureFlagRequired?: boolean;
24
+ /** Key name of the feature flag inside the config file */
25
+ featureFlagKey?: string;
26
+ /** TOML section that contains the feature flag (e.g. "[features]") */
27
+ featureFlagSection?: string;
28
+ }
29
+
30
+ export interface HostConfig {
31
+ name: string;
32
+ displayName: string;
33
+ cliCommand: string;
34
+ hostSubdir: string;
35
+ globalRoot: string;
36
+ localSkillRoot: string;
37
+ usesEnvVars: boolean;
38
+ frontmatter: FrontmatterConfig;
39
+ install: {
40
+ strategy: "copy" | "symlink";
41
+ };
42
+ hooks?: HostHooksConfig;
43
+ }