@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,131 @@
1
+ /**
2
+ * Workflow CLI — gxpm workflow subcommand.
3
+ *
4
+ * Lists, runs, checks status, and resumes YAML-defined workflows.
5
+ */
6
+
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
8
+ import { join, resolve } from "node:path";
9
+ import { WorkflowEngine } from "../../core/workflows/engine";
10
+
11
+ const WORKFLOWS_DIR = "workflows";
12
+ const RUNS_DIR = ".gxpm/workflow-runs";
13
+
14
+ export function runWorkflowCommand(argv: string[]) {
15
+ const subcommand = argv[1];
16
+
17
+ if (subcommand === "list") {
18
+ return listWorkflows();
19
+ }
20
+
21
+ if (subcommand === "run") {
22
+ const workflowId = argv[2];
23
+ const issueId = argv.find((a) => a.startsWith("--issue="))?.slice(8);
24
+ if (!workflowId) {
25
+ throw new Error("Usage: gxpm workflow run <workflow-id> --issue=<id>");
26
+ }
27
+ return runWorkflow(workflowId, issueId);
28
+ }
29
+
30
+ if (subcommand === "status") {
31
+ const runId = argv[2];
32
+ if (!runId) {
33
+ throw new Error("Usage: gxpm workflow status <run-id>");
34
+ }
35
+ return workflowStatus(runId);
36
+ }
37
+
38
+ if (subcommand === "resume") {
39
+ const runId = argv[2];
40
+ if (!runId) {
41
+ throw new Error("Usage: gxpm workflow resume <run-id>");
42
+ }
43
+ return resumeWorkflow(runId);
44
+ }
45
+
46
+ console.log("Usage: gxpm workflow <list|run|status|resume>");
47
+ }
48
+
49
+ function listWorkflows() {
50
+ const dir = resolve(WORKFLOWS_DIR);
51
+ if (!existsSync(dir)) {
52
+ console.log("No workflows directory found.");
53
+ return;
54
+ }
55
+
56
+ try {
57
+ const entries = readdirSync(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
58
+ if (entries.length === 0) {
59
+ console.log("No workflow definitions found.");
60
+ return;
61
+ }
62
+ console.log("Workflows:");
63
+ for (const entry of entries) {
64
+ console.log(` - ${entry}`);
65
+ }
66
+ } catch {
67
+ console.log("No workflow definitions found.");
68
+ }
69
+ }
70
+
71
+ function runWorkflow(workflowId: string, issueId?: string) {
72
+ const yamlPath = join(WORKFLOWS_DIR, `${workflowId}.yml`);
73
+ if (!existsSync(yamlPath)) {
74
+ throw new Error(`Workflow not found: ${workflowId}`);
75
+ }
76
+
77
+ const engine = new WorkflowEngine(process.cwd());
78
+ const def = engine.loadYaml(yamlPath);
79
+
80
+ const inputs: Record<string, unknown> = {};
81
+ if (issueId) inputs.issue_id = issueId;
82
+
83
+ const state = engine.createRun(def.id, inputs);
84
+
85
+ // Persist run state
86
+ const runDir = join(RUNS_DIR, state.runId);
87
+ mkdirSync(runDir, { recursive: true });
88
+ writeFileSync(join(runDir, "state.json"), JSON.stringify(state, null, 2));
89
+
90
+ console.log(`Workflow run started: ${state.runId}`);
91
+ console.log(`Workflow: ${def.name ?? def.id}`);
92
+ console.log(`Status: ${state.status}`);
93
+
94
+ // Execute
95
+ engine.run(state).then((result) => {
96
+ writeFileSync(join(runDir, "state.json"), JSON.stringify(result, null, 2));
97
+ console.log(`Workflow ${result.status}: ${result.runId}`);
98
+ });
99
+ }
100
+
101
+ function workflowStatus(runId: string) {
102
+ const statePath = join(RUNS_DIR, runId, "state.json");
103
+ if (!existsSync(statePath)) {
104
+ throw new Error(`Run not found: ${runId}`);
105
+ }
106
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
107
+ console.log(`Run: ${state.runId}`);
108
+ console.log(`Workflow: ${state.workflowId}`);
109
+ console.log(`Status: ${state.status}`);
110
+ console.log(`Current step: ${state.currentStepIndex}`);
111
+ }
112
+
113
+ function resumeWorkflow(runId: string) {
114
+ const statePath = join(RUNS_DIR, runId, "state.json");
115
+ if (!existsSync(statePath)) {
116
+ throw new Error(`Run not found: ${runId}`);
117
+ }
118
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
119
+
120
+ const engine = new WorkflowEngine(process.cwd());
121
+ // Reload workflow definition
122
+ const yamlPath = join(WORKFLOWS_DIR, `${state.workflowId}.yml`);
123
+ if (existsSync(yamlPath)) {
124
+ engine.loadYaml(yamlPath);
125
+ }
126
+
127
+ engine.resume(state).then((result) => {
128
+ writeFileSync(statePath, JSON.stringify(result, null, 2));
129
+ console.log(`Workflow resumed and ${result.status}: ${result.runId}`);
130
+ });
131
+ }
@@ -0,0 +1,55 @@
1
+ import { existsSync, watch } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { discoverTemplates } from "./discover-skills";
4
+ import { generateSkillDocs } from "./gen-skill-docs";
5
+
6
+ const ROOT = resolve(import.meta.dir, "..");
7
+ const templates = discoverTemplates(ROOT).filter(
8
+ (template) => template.tmpl.startsWith("skills/") && template.tmpl.endsWith(".tmpl")
9
+ );
10
+
11
+ if (templates.length === 0) {
12
+ console.error("[dev:skill] no gxpm skill template found");
13
+ process.exit(1);
14
+ }
15
+
16
+ let running = false;
17
+ let queued = false;
18
+
19
+ function regenerateAndValidate() {
20
+ if (running) {
21
+ queued = true;
22
+ return;
23
+ }
24
+
25
+ running = true;
26
+ try {
27
+ const generated = generateSkillDocs({ root: ROOT });
28
+ console.log(`[gen] ${generated.length > 0 ? generated.join(", ") : "skill docs are current"}`);
29
+ generateSkillDocs({ root: ROOT, dryRun: true });
30
+ console.log("[check] generated skill docs are current");
31
+ } catch (error) {
32
+ console.log(`[error] ${error instanceof Error ? error.message : String(error)}`);
33
+ } finally {
34
+ running = false;
35
+ if (queued) {
36
+ queued = false;
37
+ regenerateAndValidate();
38
+ }
39
+ }
40
+ }
41
+
42
+ console.log("[watch] watching skills/gxpm/SKILL.md.tmpl");
43
+ regenerateAndValidate();
44
+
45
+ for (const template of templates) {
46
+ const path = join(ROOT, template.tmpl);
47
+ if (!existsSync(path)) continue;
48
+ watch(path, { persistent: true }, () => {
49
+ console.log(`[watch] ${relative(ROOT, path)} changed`);
50
+ regenerateAndValidate();
51
+ });
52
+ }
53
+
54
+ console.log("[watch] press Ctrl+C to stop");
55
+ setInterval(() => {}, 60_000);
@@ -0,0 +1,116 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+
4
+ export interface SkillTemplate {
5
+ tmpl: string;
6
+ output: string;
7
+ /** Skill name derived from directory path, e.g. "gxpm" or "gxpm-debug-issue" */
8
+ name: string;
9
+ /** Relative paths to reference markdown files under references/ */
10
+ references?: string[];
11
+ /** Relative paths to script files under scripts/ */
12
+ scripts?: string[];
13
+ }
14
+
15
+ const SKIP_DIRS = new Set([
16
+ ".agents",
17
+ ".claude",
18
+ ".codex",
19
+ ".generated",
20
+ ".git",
21
+ ".gstack",
22
+ ".omc",
23
+ "build",
24
+ "coverage",
25
+ "dist",
26
+ "node_modules",
27
+ ]);
28
+
29
+ export function discoverTemplates(root = process.cwd()): SkillTemplate[] {
30
+ const templates: SkillTemplate[] = [];
31
+ const skillsDir = join(root, "skills");
32
+
33
+ function walk(dir: string) {
34
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
35
+ const fullPath = join(dir, entry.name);
36
+
37
+ if (entry.isDirectory()) {
38
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) {
39
+ continue;
40
+ }
41
+ walk(fullPath);
42
+ continue;
43
+ }
44
+
45
+ if (entry.isFile() && entry.name === "SKILL.md.tmpl") {
46
+ const tmpl = normalizePath(relative(root, fullPath));
47
+ const skillPath = tmpl.replace(/\.tmpl$/, "");
48
+ const name = skillPath.replace(/^skills\//, "").replace(/\/SKILL\.md$/, "");
49
+ const skillDir = dirname(fullPath);
50
+ templates.push({
51
+ tmpl,
52
+ output: skillPath,
53
+ name,
54
+ references: discoverReferences(root, skillDir),
55
+ scripts: discoverScripts(root, skillDir),
56
+ });
57
+ }
58
+
59
+ if (entry.isFile() && entry.name === "SKILL.md") {
60
+ // Skip generated artifacts that have a corresponding .tmpl source
61
+ const tmplPath = fullPath + ".tmpl";
62
+ if (existsSync(tmplPath)) {
63
+ continue;
64
+ }
65
+ const tmpl = normalizePath(relative(root, fullPath));
66
+ const name = tmpl.replace(/^skills\//, "").replace(/\/SKILL\.md$/, "");
67
+ const skillDir = dirname(fullPath);
68
+ templates.push({
69
+ tmpl,
70
+ output: tmpl,
71
+ name,
72
+ references: discoverReferences(root, skillDir),
73
+ scripts: discoverScripts(root, skillDir),
74
+ });
75
+ }
76
+ }
77
+ }
78
+
79
+ if (existsSync(skillsDir) && statSync(skillsDir).isDirectory()) {
80
+ walk(skillsDir);
81
+ }
82
+
83
+ return templates.sort((a, b) => a.tmpl.localeCompare(b.tmpl));
84
+ }
85
+
86
+ function discoverReferences(root: string, skillDir: string): string[] | undefined {
87
+ const refsDir = join(skillDir, "references");
88
+ if (!existsSync(refsDir) || !statSync(refsDir).isDirectory()) {
89
+ return undefined;
90
+ }
91
+ const refs: string[] = [];
92
+ for (const entry of readdirSync(refsDir, { withFileTypes: true })) {
93
+ if (entry.isFile() && entry.name.endsWith(".md")) {
94
+ refs.push(normalizePath(relative(root, join(refsDir, entry.name))));
95
+ }
96
+ }
97
+ return refs.length > 0 ? refs.sort() : undefined;
98
+ }
99
+
100
+ function discoverScripts(root: string, skillDir: string): string[] | undefined {
101
+ const scriptsDir = join(skillDir, "scripts");
102
+ if (!existsSync(scriptsDir) || !statSync(scriptsDir).isDirectory()) {
103
+ return undefined;
104
+ }
105
+ const scripts: string[] = [];
106
+ for (const entry of readdirSync(scriptsDir, { withFileTypes: true })) {
107
+ if (entry.isFile()) {
108
+ scripts.push(normalizePath(relative(root, join(scriptsDir, entry.name))));
109
+ }
110
+ }
111
+ return scripts.length > 0 ? scripts.sort() : undefined;
112
+ }
113
+
114
+ function normalizePath(path: string) {
115
+ return path.split("\\").join("/");
116
+ }
@@ -0,0 +1,410 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ALL_HOST_CONFIGS } from "../hosts";
6
+ import { getConfigValue, getResolvedConfigValue, type KnownConfigKey, KNOWN_CONFIG_KEYS } from "../core/config";
7
+ import { REQUIRED_GXPM_GIT_HOOKS } from "../core/project-init-status";
8
+ import { readGxpmVersion } from "./version";
9
+
10
+ export interface SkillCheck {
11
+ host: string;
12
+ installPath: string;
13
+ installed: boolean;
14
+ bytes?: number;
15
+ }
16
+
17
+ export interface RepoCheck {
18
+ cwd: string;
19
+ isGitRepo: boolean;
20
+ coreHooksPath: string | null;
21
+ gxpmHooksInstalled: boolean;
22
+ installedHooks: string[];
23
+ gxpmDirExists: boolean;
24
+ issueCount: number;
25
+ contextMdExists: boolean;
26
+ }
27
+
28
+ export interface RuntimeCheck {
29
+ bunAvailable: boolean;
30
+ gxpmVersion: string | null;
31
+ gxpmRepoRoot: string;
32
+ }
33
+
34
+ export interface DoctorCheck {
35
+ name: string;
36
+ status: "ok" | "warn" | "fail";
37
+ message: string;
38
+ }
39
+
40
+ export interface DoctorReport {
41
+ schema_version: number;
42
+ status: "healthy" | "warnings" | "error";
43
+ health_score: number;
44
+ checks: DoctorCheck[];
45
+ // Legacy fields preserved for backward compatibility
46
+ runtime?: RuntimeCheck;
47
+ skill?: SkillCheck[];
48
+ repo?: RepoCheck;
49
+ }
50
+
51
+ interface RunDoctorInput {
52
+ home?: string;
53
+ cwd?: string;
54
+ gxpmRoot?: string;
55
+ fix?: boolean;
56
+ }
57
+
58
+ const GXPM_HOOK_FILES = REQUIRED_GXPM_GIT_HOOKS;
59
+
60
+ const DEFAULT_GXPM_ROOT = resolve(import.meta.dir, "..");
61
+
62
+ export function runDoctor(input: RunDoctorInput = {}): DoctorReport {
63
+ const home = input.home ?? homedir();
64
+ const cwd = input.cwd ?? process.cwd();
65
+ const gxpmRoot = input.gxpmRoot ?? DEFAULT_GXPM_ROOT;
66
+ const fix = input.fix ?? false;
67
+
68
+ const runtime = checkRuntime(gxpmRoot);
69
+ const skillChecks = checkSkill(home);
70
+ const repo = checkRepo(cwd);
71
+
72
+ const checks: DoctorCheck[] = [];
73
+ const fixLog: string[] = [];
74
+
75
+ // --- Runtime checks ---
76
+ checks.push({
77
+ name: "bun_runtime",
78
+ status: runtime.bunAvailable ? "ok" : "fail",
79
+ message: runtime.bunAvailable ? "bun is available" : "bun not found on PATH",
80
+ });
81
+
82
+ checks.push({
83
+ name: "gxpm_version",
84
+ status: runtime.gxpmVersion ? "ok" : "warn",
85
+ message: runtime.gxpmVersion ? `gxpm ${runtime.gxpmVersion} at ${runtime.gxpmRepoRoot}` : "could not read VERSION",
86
+ });
87
+
88
+ // --- Skill checks ---
89
+ const missingSkills = skillChecks.filter((s) => !s.installed);
90
+ if (missingSkills.length === 0) {
91
+ checks.push({ name: "skill_installation", status: "ok", message: `${skillChecks.length} hosts have gxpm skill` });
92
+ } else {
93
+ checks.push({
94
+ name: "skill_installation",
95
+ status: "warn",
96
+ message: `${missingSkills.length} missing skill installations: ${missingSkills.map((s) => s.host).join(", ")}`,
97
+ });
98
+ if (fix) {
99
+ try {
100
+ execSync(`bun run "${join(gxpmRoot, "scripts", "install-skill.ts")}" --host all`, { stdio: "ignore" });
101
+ fixLog.push("installed missing skills to all hosts");
102
+ } catch {
103
+ fixLog.push("failed to install missing skills");
104
+ }
105
+ }
106
+ }
107
+
108
+ // --- Skill freshness ---
109
+ const stale = checkSkillFreshness(gxpmRoot);
110
+ if (stale.length === 0) {
111
+ checks.push({ name: "skill_freshness", status: "ok", message: "all SKILL.md files are up to date with templates" });
112
+ } else {
113
+ checks.push({
114
+ name: "skill_freshness",
115
+ status: "warn",
116
+ message: `${stale.length} stale SKILL.md files: ${stale.join(", ")}`,
117
+ });
118
+ if (fix) {
119
+ try {
120
+ execSync(`bun run "${join(gxpmRoot, "scripts", "gen-skill-docs.ts")}"`, { stdio: "ignore" });
121
+ fixLog.push("regenerated skill docs");
122
+ } catch {
123
+ fixLog.push("failed to regenerate skill docs");
124
+ }
125
+ }
126
+ }
127
+
128
+ // --- Repo checks ---
129
+ if (!repo.isGitRepo) {
130
+ checks.push({ name: "git_repo", status: "fail", message: "not a git repository" });
131
+ } else {
132
+ checks.push({ name: "git_repo", status: "ok", message: "git repository detected" });
133
+ }
134
+
135
+ if (repo.gxpmHooksInstalled) {
136
+ checks.push({ name: "gxpm_hooks", status: "ok", message: `all ${GXPM_HOOK_FILES.length} hooks installed` });
137
+ } else {
138
+ const missing = GXPM_HOOK_FILES.filter((h) => !repo.installedHooks.includes(h));
139
+ checks.push({
140
+ name: "gxpm_hooks",
141
+ status: repo.isGitRepo ? "warn" : "fail",
142
+ message: `missing hooks: ${missing.join(", ")}`,
143
+ });
144
+ if (fix && repo.isGitRepo) {
145
+ try {
146
+ execSync(`bun run "${join(gxpmRoot, "scripts", "install-hooks.ts")}" --target "${cwd}"`, { stdio: "ignore" });
147
+ fixLog.push("installed missing git hooks");
148
+ } catch {
149
+ fixLog.push("failed to install git hooks");
150
+ }
151
+ }
152
+ }
153
+
154
+ if (repo.gxpmDirExists) {
155
+ checks.push({ name: "gxpm_dir", status: "ok", message: `.gxpm/issues/ exists (${repo.issueCount} issues)` });
156
+ } else {
157
+ checks.push({ name: "gxpm_dir", status: "warn", message: ".gxpm/ not initialized" });
158
+ }
159
+
160
+ // --- Worktree availability ---
161
+ if (repo.isGitRepo) {
162
+ try {
163
+ execSync("git worktree list", { cwd, stdio: "ignore" });
164
+ checks.push({ name: "worktree_available", status: "ok", message: "git worktree is available" });
165
+ } catch {
166
+ checks.push({ name: "worktree_available", status: "warn", message: "git worktree command failed" });
167
+ }
168
+ }
169
+
170
+ // --- Config validity ---
171
+ const configIssues = checkConfigValidity(cwd, home);
172
+ if (configIssues.length === 0) {
173
+ checks.push({ name: "config_validity", status: "ok", message: "all config values are valid" });
174
+ } else {
175
+ checks.push({ name: "config_validity", status: "warn", message: configIssues.join("; ") });
176
+ }
177
+
178
+ // --- Linear CLI availability (if configured) ---
179
+ const provider = getConfigValue({ root: cwd, home, key: "sync.provider" });
180
+ if (provider.value === "linear") {
181
+ const linearOk = checkLinearQuick();
182
+ checks.push({
183
+ name: "linear_cli",
184
+ status: linearOk ? "ok" : "warn",
185
+ message: linearOk ? "Linear CLI available" : "Linear CLI not found (install with `npm install -g @linear/cli` or equivalent)",
186
+ });
187
+ }
188
+
189
+ // --- Upgrade error trail ---
190
+ const upgradeErrors = loadUpgradeErrors();
191
+ if (upgradeErrors.length > 0) {
192
+ const latest = upgradeErrors[upgradeErrors.length - 1];
193
+ checks.push({
194
+ name: "upgrade_errors",
195
+ status: "warn",
196
+ message: `Post-upgrade failure on ${latest.ts.slice(0, 10)} (${latest.from_version} -> ${latest.to_version}, phase: ${latest.phase}). Recovery: ${latest.hint}`,
197
+ });
198
+ }
199
+
200
+ // Compute health score
201
+ let score = 100;
202
+ for (const c of checks) {
203
+ if (c.status === "fail") score -= 20;
204
+ else if (c.status === "warn") score -= 5;
205
+ }
206
+ score = Math.max(0, score);
207
+
208
+ const hasFail = checks.some((c) => c.status === "fail");
209
+ const hasWarn = checks.some((c) => c.status === "warn");
210
+ const status: DoctorReport["status"] = hasFail ? "error" : hasWarn ? "warnings" : "healthy";
211
+
212
+ // Write fix log if any fixes were applied
213
+ if (fix && fixLog.length > 0) {
214
+ const auditDir = join(home, ".gxpm", "audit");
215
+ mkdirSync(auditDir, { recursive: true });
216
+ const line = JSON.stringify({ ts: new Date().toISOString(), fixes: fixLog }) + "\n";
217
+ writeFileSync(join(auditDir, "doctor-fixes.jsonl"), line, { flag: "a" });
218
+ }
219
+
220
+ return {
221
+ schema_version: 1,
222
+ status,
223
+ health_score: score,
224
+ checks,
225
+ runtime,
226
+ skill: skillChecks,
227
+ repo,
228
+ };
229
+ }
230
+
231
+ function checkRuntime(gxpmRoot: string): RuntimeCheck {
232
+ let bunAvailable = false;
233
+ try {
234
+ execSync("bun --version", { stdio: "ignore" });
235
+ bunAvailable = true;
236
+ } catch {}
237
+
238
+ let version: string | null = null;
239
+ try {
240
+ version = readGxpmVersion({ root: gxpmRoot });
241
+ } catch {}
242
+
243
+ return { bunAvailable, gxpmVersion: version, gxpmRepoRoot: gxpmRoot };
244
+ }
245
+
246
+ function checkSkill(home: string): SkillCheck[] {
247
+ return ALL_HOST_CONFIGS.map((host) => {
248
+ const installPath = join(home, host.globalRoot, "SKILL.md");
249
+ if (!existsSync(installPath)) {
250
+ return { host: host.name, installPath, installed: false };
251
+ }
252
+ let bytes: number | undefined;
253
+ try {
254
+ bytes = statSync(installPath).size;
255
+ } catch {}
256
+ return { host: host.name, installPath, installed: true, bytes };
257
+ });
258
+ }
259
+
260
+ function checkRepo(cwd: string): RepoCheck {
261
+ const repo: RepoCheck = {
262
+ cwd,
263
+ isGitRepo: false,
264
+ coreHooksPath: null,
265
+ gxpmHooksInstalled: false,
266
+ installedHooks: [],
267
+ gxpmDirExists: false,
268
+ issueCount: 0,
269
+ contextMdExists: existsSync(join(cwd, "CONTEXT.md")),
270
+ };
271
+
272
+ const gitDir = join(cwd, ".git");
273
+ try {
274
+ repo.isGitRepo = existsSync(gitDir) && statSync(gitDir).isDirectory();
275
+ } catch {}
276
+
277
+ if (repo.isGitRepo) {
278
+ try {
279
+ repo.coreHooksPath = execSync("git config core.hooksPath", {
280
+ cwd,
281
+ stdio: ["ignore", "pipe", "ignore"],
282
+ })
283
+ .toString()
284
+ .trim();
285
+ } catch {
286
+ repo.coreHooksPath = null;
287
+ }
288
+ }
289
+
290
+ const hooksDir = join(cwd, ".githooks");
291
+ if (existsSync(hooksDir)) {
292
+ for (const hook of GXPM_HOOK_FILES) {
293
+ if (existsSync(join(hooksDir, hook))) {
294
+ repo.installedHooks.push(hook);
295
+ }
296
+ }
297
+ repo.gxpmHooksInstalled = repo.installedHooks.length === GXPM_HOOK_FILES.length;
298
+ }
299
+
300
+ const gxpmDir = join(cwd, ".gxpm", "issues");
301
+ if (existsSync(gxpmDir)) {
302
+ repo.gxpmDirExists = true;
303
+ try {
304
+ repo.issueCount = readdirSync(gxpmDir).filter((name) => {
305
+ const p = join(gxpmDir, name);
306
+ try {
307
+ return statSync(p).isDirectory() && existsSync(join(p, "state.json"));
308
+ } catch {
309
+ return false;
310
+ }
311
+ }).length;
312
+ } catch {}
313
+ }
314
+
315
+ return repo;
316
+ }
317
+
318
+ function checkSkillFreshness(gxpmRoot: string): string[] {
319
+ const stale: string[] = [];
320
+ const skillsDir = join(gxpmRoot, "skills");
321
+ if (!existsSync(skillsDir)) return stale;
322
+
323
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
324
+ if (!entry.isDirectory()) continue;
325
+ const skillDir = join(skillsDir, entry.name);
326
+ const tmplPath = join(skillDir, "SKILL.md.tmpl");
327
+ const skillPath = join(skillDir, "SKILL.md");
328
+ if (!existsSync(tmplPath) || !existsSync(skillPath)) continue;
329
+ try {
330
+ const tmplStat = statSync(tmplPath);
331
+ const skillStat = statSync(skillPath);
332
+ if (tmplStat.mtimeMs > skillStat.mtimeMs) {
333
+ stale.push(entry.name);
334
+ }
335
+ } catch {
336
+ // skip
337
+ }
338
+ }
339
+ return stale;
340
+ }
341
+
342
+ function checkConfigValidity(cwd: string, home: string): string[] {
343
+ const issues: string[] = [];
344
+ for (const key of KNOWN_CONFIG_KEYS) {
345
+ try {
346
+ const resolved = getResolvedConfigValue({ root: cwd, home, key });
347
+ // Basic type checks based on registry knowledge would go here;
348
+ // for now we just ensure the value can be resolved without throwing.
349
+ } catch (e) {
350
+ issues.push(`${key}: ${e instanceof Error ? e.message : String(e)}`);
351
+ }
352
+ }
353
+ return issues;
354
+ }
355
+
356
+ function checkLinearQuick(): boolean {
357
+ try {
358
+ execSync("linear --version", {
359
+ encoding: "utf8",
360
+ stdio: ["ignore", "pipe", "ignore"],
361
+ env: { ...process.env, PATH: process.env.PATH },
362
+ });
363
+ return true;
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+
369
+ function loadUpgradeErrors(): Array<{ ts: string; phase: string; from_version: string; to_version: string; hint: string }> {
370
+ try {
371
+ const path = join(homedir(), ".gxpm", "audit", "upgrade-errors.jsonl");
372
+ if (!existsSync(path)) return [];
373
+ const lines = readFileSync(path, "utf-8").split("\n").filter((l) => l.trim());
374
+ return lines.map((l) => JSON.parse(l));
375
+ } catch {
376
+ return [];
377
+ }
378
+ }
379
+
380
+ export function formatDoctorReport(report: DoctorReport): string {
381
+ const lines: string[] = [];
382
+ lines.push("gxpm doctor");
383
+ lines.push("===========");
384
+ lines.push("");
385
+ lines.push(`Status: ${report.status} (score: ${report.health_score}/100)`);
386
+ lines.push("");
387
+
388
+ for (const c of report.checks) {
389
+ const icon = c.status === "ok" ? "✓" : c.status === "warn" ? "⚠" : "✗";
390
+ lines.push(` ${icon} ${c.name}: ${c.message}`);
391
+ }
392
+
393
+ lines.push("");
394
+ if (report.status === "healthy") {
395
+ lines.push("All checks passed.");
396
+ } else if (report.status === "warnings") {
397
+ lines.push("Some warnings found. Run `gxpm doctor --fix` to auto-repair where possible.");
398
+ } else {
399
+ lines.push("Failing checks found. Run `gxpm doctor --fix` to auto-repair where possible.");
400
+ }
401
+
402
+ return lines.join("\n");
403
+ }
404
+
405
+ if (import.meta.main) {
406
+ const fix = Bun.argv.includes("--fix");
407
+ const report = runDoctor({ fix });
408
+ console.log(formatDoctorReport(report));
409
+ process.exit(report.status === "error" ? 1 : 0);
410
+ }