@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.
- package/AGENTS.md +148 -0
- package/CANON.md +53 -0
- package/CLAUDE.md +60 -0
- package/CONTEXT.md +49 -0
- package/DEBUG.md +59 -0
- package/ISSUE_CONTEXT.md +25 -0
- package/README.md +143 -0
- package/VERSION +1 -0
- package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
- package/agents/grill-master.md +26 -0
- package/agents/implementer.md +32 -0
- package/agents/review-army/accessibility-reviewer.md +54 -0
- package/agents/review-army/code-quality-reviewer.md +54 -0
- package/agents/review-army/security-reviewer.md +56 -0
- package/agents/review-army/spec-compliance-reviewer.md +51 -0
- package/agents/review-army/test-reviewer.md +55 -0
- package/agents/reviewer.md +59 -0
- package/agents/ship-audit-army/docs-auditor.md +53 -0
- package/agents/ship-audit-army/performance-auditor.md +52 -0
- package/agents/ship-audit-army/security-auditor.md +52 -0
- package/agents/specifier.md +55 -0
- package/agents/triage-officer.md +27 -0
- package/bin/gxpm +17 -0
- package/bin/gxpm-browser +17 -0
- package/bin/gxpm-config +15 -0
- package/bin/gxpm-eval +13 -0
- package/bin/gxpm-global-discover +15 -0
- package/bin/gxpm-init +38 -0
- package/bin/gxpm-investigate +194 -0
- package/bin/gxpm-uninstall +15 -0
- package/bin/gxpm-update-check +165 -0
- package/commands/build.md +40 -0
- package/commands/help.md +53 -0
- package/commands/plan.md +34 -0
- package/commands/refine.md +46 -0
- package/commands/review.md +34 -0
- package/commands/ship.md +37 -0
- package/core/ac-check.ts +20 -0
- package/core/agent-runtime.ts +363 -0
- package/core/artifact-validator.ts +151 -0
- package/core/artifacts.ts +313 -0
- package/core/autopilot.ts +250 -0
- package/core/capabilities.ts +779 -0
- package/core/checkpoint.ts +370 -0
- package/core/cleanup.ts +32 -0
- package/core/command-probe.ts +82 -0
- package/core/config.ts +533 -0
- package/core/contracts/behavior-spec.schema.ts +38 -0
- package/core/contracts/converter.ts +61 -0
- package/core/contracts/host.ts +43 -0
- package/core/converters/converter.ts +93 -0
- package/core/converters/index.ts +8 -0
- package/core/converters/managed-artifact.ts +119 -0
- package/core/converters/parser.ts +159 -0
- package/core/converters/template-renderer.ts +35 -0
- package/core/converters/writer.ts +61 -0
- package/core/dag-executor.ts +426 -0
- package/core/dag-loader.ts +292 -0
- package/core/dag-schemas.ts +150 -0
- package/core/dispatch.ts +125 -0
- package/core/evidence.ts +148 -0
- package/core/gate.ts +269 -0
- package/core/hook-engine.ts +566 -0
- package/core/host-probe.ts +64 -0
- package/core/implement.ts +16 -0
- package/core/isolation-errors.ts +174 -0
- package/core/isolation-resolver.ts +921 -0
- package/core/issue-context.ts +381 -0
- package/core/issue-readiness.ts +457 -0
- package/core/issue-sync.ts +427 -0
- package/core/issues.ts +132 -0
- package/core/land.ts +108 -0
- package/core/orchestrator.ts +54 -0
- package/core/phase-artifact.ts +32 -0
- package/core/phase-gates.ts +130 -0
- package/core/phase-rewind.ts +94 -0
- package/core/plan-lint.ts +61 -0
- package/core/plan.ts +77 -0
- package/core/port-allocation.ts +50 -0
- package/core/pr-check.ts +15 -0
- package/core/preset-system/preset-resolver.ts +221 -0
- package/core/project-init-status.ts +127 -0
- package/core/qa.ts +15 -0
- package/core/resilience.ts +165 -0
- package/core/runs.ts +288 -0
- package/core/safe-path.test.ts +80 -0
- package/core/safe-path.ts +60 -0
- package/core/sdd-gate.test.ts +98 -0
- package/core/sdd-gate.ts +134 -0
- package/core/self-review.ts +62 -0
- package/core/session.ts +70 -0
- package/core/ship.ts +86 -0
- package/core/specify.ts +173 -0
- package/core/state.ts +1002 -0
- package/core/template-engine.ts +152 -0
- package/core/template-resolver.test.ts +70 -0
- package/core/template-resolver.ts +156 -0
- package/core/triage.ts +26 -0
- package/core/verify.ts +15 -0
- package/core/wiki-native.ts +2423 -0
- package/core/wiki.ts +27 -0
- package/core/workflow-event-emitter.ts +163 -0
- package/core/workflows/engine.ts +273 -0
- package/core/workflows/expressions.ts +76 -0
- package/core/workflows/index.ts +38 -0
- package/core/workflows/steps/command.ts +43 -0
- package/core/workflows/steps/gate.ts +47 -0
- package/core/workflows/steps/gxpm.ts +44 -0
- package/core/workflows/steps/linear.ts +31 -0
- package/core/workflows/steps/shell.ts +65 -0
- package/core/workflows/types.ts +62 -0
- package/core/workspace-runtime.ts +227 -0
- package/core/worktree-init-steps.ts +647 -0
- package/core/worktree-init.ts +330 -0
- package/core/worktree-owner.ts +143 -0
- package/docs/GXPM_VERIFY.md +98 -0
- package/docs/INSTALL_FOR_AGENTS.md +113 -0
- package/docs/README.md +57 -0
- package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
- package/docs/agents/domain.md +30 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +32 -0
- package/docs/architecture/gxpm-architecture-diagram.md +265 -0
- package/docs/architecture/gxpm-current-architecture.md +175 -0
- package/docs/architecture/gxpm-current-flow.md +278 -0
- package/docs/architecture/gxpm-replacement-architecture.md +211 -0
- package/docs/architecture/gxpm-target-architecture.md +449 -0
- package/docs/architecture/gxpm-v0-contract.md +311 -0
- package/docs/architecture/layered-workflow-boundaries.md +193 -0
- package/docs/architecture/preset-system.md +126 -0
- package/docs/architecture/scaffold-northstar.md +23 -0
- package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
- package/docs/brainstorms/README.md +22 -0
- package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
- package/docs/governance/beta-skill-promotion.md +39 -0
- package/docs/governance/development-contract.md +144 -0
- package/docs/governance/gherkin-style.md +90 -0
- package/docs/governance/host-adapter.md +56 -0
- package/docs/governance/skill-authoring.md +87 -0
- package/docs/governance/skill-testing.md +356 -0
- package/docs/governance/template-authoring.md +53 -0
- package/docs/migrations/v0.2.md +51 -0
- package/docs/plans/README.md +23 -0
- package/docs/plans/bdd-then-tdd-plan.md +1767 -0
- package/docs/plans/docs-knowledge-system-plan.md +31 -0
- package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
- package/docs/research/agents-md-best-practices.md +207 -0
- package/docs/research/archon-study.md +351 -0
- package/docs/research/claude-hooks-study.md +440 -0
- package/docs/research/codex-hooks-study.md +624 -0
- package/docs/research/everything-claude-code-study.md +252 -0
- package/docs/research/from-skills-to-layered-workflow.md +322 -0
- package/docs/research/gsd-study.md +69 -0
- package/docs/research/kimi-hooks-study.md +274 -0
- package/docs/research/mattpocock-skills-comparison.md +429 -0
- package/docs/research/mattpocock-skills-study.md +275 -0
- package/docs/research/oh-my-codex-study.md +279 -0
- package/docs/research/perplexity-agent-skills-design.md +168 -0
- package/docs/research/pmc-gstack-skill-study.md +122 -0
- package/docs/research/spec-kit-study.md +224 -0
- package/docs/research/superpowers-study.md +209 -0
- package/docs/roadmap/initial-roadmap.md +53 -0
- package/docs/solutions/README.md +45 -0
- package/docs/solutions/artifact-nesting-recovery.md +58 -0
- package/docs/solutions/session-context-restore-practice.md +67 -0
- package/docs/solutions/workflow/version-drift-recovery.md +49 -0
- package/docs/solutions/worktree-gate-recovery.md +62 -0
- package/docs/specs/README.md +28 -0
- package/docs/specs/claude.md +45 -0
- package/docs/specs/codex.md +44 -0
- package/docs/specs/cursor.md +44 -0
- package/hosts/adapters/claude.ts +29 -0
- package/hosts/adapters/codex.ts +27 -0
- package/hosts/adapters/cursor.ts +27 -0
- package/hosts/adapters/kimi.ts +27 -0
- package/hosts/claude.ts +23 -0
- package/hosts/codex.ts +26 -0
- package/hosts/cursor.ts +19 -0
- package/hosts/index.ts +33 -0
- package/hosts/registry.test.ts +52 -0
- package/hosts/registry.ts +57 -0
- package/hosts/schema.ts +58 -0
- package/package.json +52 -0
- package/scripts/browser.ts +185 -0
- package/scripts/cleanup.ts +142 -0
- package/scripts/commands/artifact.ts +115 -0
- package/scripts/commands/autopilot.ts +143 -0
- package/scripts/commands/capability.ts +57 -0
- package/scripts/commands/config.ts +69 -0
- package/scripts/commands/dag.ts +126 -0
- package/scripts/commands/feedback.ts +123 -0
- package/scripts/commands/gate.ts +291 -0
- package/scripts/commands/helpers.ts +126 -0
- package/scripts/commands/hook.ts +66 -0
- package/scripts/commands/init.ts +515 -0
- package/scripts/commands/issue.ts +825 -0
- package/scripts/commands/phase.ts +61 -0
- package/scripts/commands/preset.ts +159 -0
- package/scripts/commands/runtime.ts +199 -0
- package/scripts/commands/specify.ts +71 -0
- package/scripts/commands/upgrade.ts +243 -0
- package/scripts/commands/verify.ts +183 -0
- package/scripts/commands/wiki.ts +242 -0
- package/scripts/commands/workflow.ts +131 -0
- package/scripts/dev-skill.ts +55 -0
- package/scripts/discover-skills.ts +116 -0
- package/scripts/doctor.ts +410 -0
- package/scripts/dogfood-check.ts +125 -0
- package/scripts/eval-functional.ts +218 -0
- package/scripts/eval.ts +246 -0
- package/scripts/gen-skill-docs.ts +201 -0
- package/scripts/global-discover.ts +217 -0
- package/scripts/governance-check.ts +75 -0
- package/scripts/gxpm-check.ts +12 -0
- package/scripts/gxpm.ts +216 -0
- package/scripts/host-config.ts +62 -0
- package/scripts/install-claude-hooks.ts +138 -0
- package/scripts/install-codex-hooks.ts +271 -0
- package/scripts/install-hooks.ts +128 -0
- package/scripts/install-kimi-hooks.ts +92 -0
- package/scripts/install-skill.ts +184 -0
- package/scripts/phase-artifact-commands.ts +100 -0
- package/scripts/post-land-sync.ts +46 -0
- package/scripts/scaffold-check.ts +85 -0
- package/scripts/skill-naming-check.ts +78 -0
- package/scripts/skill-structure-check.ts +157 -0
- package/scripts/skills-lock-check.ts +60 -0
- package/scripts/sync-markdown-artifacts.ts +172 -0
- package/scripts/uninstall.ts +162 -0
- package/scripts/version.ts +47 -0
- package/scripts/wait-pr-ready.ts +407 -0
- package/skills/gxpm/SKILL.md +485 -0
- package/skills/gxpm/SKILL.md.tmpl +422 -0
- package/skills/gxpm/references/CANON.md +53 -0
- package/skills/gxpm/references/key-rules.md +130 -0
- package/skills/gxpm-architecture/SKILL.md +106 -0
- package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
- package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
- package/skills/gxpm-autopilot/SKILL.md +116 -0
- package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
- package/skills/gxpm-browser/SKILL.md +105 -0
- package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
- package/skills/gxpm-browser/references/commands.md +43 -0
- package/skills/gxpm-browser/references/evidence-path.md +20 -0
- package/skills/gxpm-build/SKILL.md +78 -0
- package/skills/gxpm-cleanup/SKILL.md +76 -0
- package/skills/gxpm-debug-issue/SKILL.md +39 -0
- package/skills/gxpm-diagnose/SKILL.md +220 -0
- package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
- package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
- package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
- package/skills/gxpm-diagnose/references/phases.md +60 -0
- package/skills/gxpm-eval/SKILL.md +78 -0
- package/skills/gxpm-explore-codebase/SKILL.md +36 -0
- package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
- package/skills/gxpm-feedback/SKILL.md +122 -0
- package/skills/gxpm-grill/SKILL.md +159 -0
- package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
- package/skills/gxpm-grill/references/documentation-templates.md +56 -0
- package/skills/gxpm-grill/references/process.md +25 -0
- package/skills/gxpm-handoff/SKILL.md +112 -0
- package/skills/gxpm-hygiene/SKILL.md +69 -0
- package/skills/gxpm-implementer/SKILL.md +142 -0
- package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
- package/skills/gxpm-linear/SKILL.md +282 -0
- package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
- package/skills/gxpm-linear/references/commands.md +75 -0
- package/skills/gxpm-linear/references/workflows.md +120 -0
- package/skills/gxpm-planning/SKILL.md +134 -0
- package/skills/gxpm-prototype/SKILL.md +64 -0
- package/skills/gxpm-refactor-safely/SKILL.md +62 -0
- package/skills/gxpm-review-army/SKILL.md +117 -0
- package/skills/gxpm-review-changes/SKILL.md +36 -0
- package/skills/gxpm-setup/SKILL.md +101 -0
- package/skills/gxpm-specifier/SKILL.md +135 -0
- package/skills/gxpm-tdd/SKILL.md +187 -0
- package/skills/gxpm-tdd/references/interface-design.md +23 -0
- package/skills/gxpm-tdd/references/mocking.md +27 -0
- package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
- package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
- package/skills/gxpm-tdd/references/workflow.md +50 -0
- package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
- package/skills/gxpm-triage/SKILL.md +160 -0
- package/skills/gxpm-verify/SKILL.md +107 -0
- package/skills/gxpm-write-skill/SKILL.md +131 -0
- package/skills/gxpm-zoom-out/SKILL.md +69 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
- package/templates/constitution-template.md +63 -0
- package/templates/hooks/gxpm-commit-msg +16 -0
- package/templates/hooks/gxpm-post-checkout +19 -0
- package/templates/hooks/gxpm-post-commit +7 -0
- package/templates/hooks/gxpm-post-merge +29 -0
- package/templates/hooks/gxpm-pre-commit +39 -0
- package/templates/hooks/gxpm-pre-push +33 -0
- package/templates/plan-template.md.tmpl +46 -0
- package/templates/spec-template.md.tmpl +63 -0
- package/templates/specify-stub.tmpl +22 -0
- package/templates/tasks-template.md.tmpl +32 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
interface HookSpec {
|
|
6
|
+
gxpmFile: string;
|
|
7
|
+
topLevelFile: string;
|
|
8
|
+
argsForwarding: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HOOK_SPECS: HookSpec[] = [
|
|
12
|
+
{ gxpmFile: "gxpm-pre-commit", topLevelFile: "pre-commit", argsForwarding: "" },
|
|
13
|
+
{ gxpmFile: "gxpm-commit-msg", topLevelFile: "commit-msg", argsForwarding: ' "$1"' },
|
|
14
|
+
{ gxpmFile: "gxpm-pre-push", topLevelFile: "pre-push", argsForwarding: ' "$@"' },
|
|
15
|
+
{ gxpmFile: "gxpm-post-merge", topLevelFile: "post-merge", argsForwarding: ' "$@"' },
|
|
16
|
+
{ gxpmFile: "gxpm-post-checkout", topLevelFile: "post-checkout", argsForwarding: ' "$@"' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function dispatcherScript(gxpmFile: string, argsForwarding: string): string {
|
|
20
|
+
return `#!/bin/bash
|
|
21
|
+
# gxpm dispatcher (installed by gxpm-init --install-hooks)
|
|
22
|
+
# Calls the gxpm-specific hook if present; harmless otherwise.
|
|
23
|
+
set -e
|
|
24
|
+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
if [ -x "$HOOK_DIR/${gxpmFile}" ]; then
|
|
26
|
+
"$HOOK_DIR/${gxpmFile}"${argsForwarding}
|
|
27
|
+
fi
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface InstallOptions {
|
|
32
|
+
target: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseArgs(argv: string[]): InstallOptions {
|
|
36
|
+
const dashTarget = argv.indexOf("--target");
|
|
37
|
+
const target = dashTarget >= 0 ? argv[dashTarget + 1] : process.cwd();
|
|
38
|
+
return { target: resolve(target) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isGitRepo(dir: string): boolean {
|
|
42
|
+
const gitDir = join(dir, ".git");
|
|
43
|
+
if (!existsSync(gitDir)) return false;
|
|
44
|
+
try {
|
|
45
|
+
return statSync(gitDir).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function main(argv: string[]) {
|
|
52
|
+
const { target } = parseArgs(argv);
|
|
53
|
+
|
|
54
|
+
if (!isGitRepo(target)) {
|
|
55
|
+
console.error(`Not a git repository: ${target}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const templatesDir = resolve(import.meta.dir, "..", "templates", "hooks");
|
|
60
|
+
const hooksDir = join(target, ".githooks");
|
|
61
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const skipped: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (const spec of HOOK_SPECS) {
|
|
66
|
+
const src = join(templatesDir, spec.gxpmFile);
|
|
67
|
+
const dst = join(hooksDir, spec.gxpmFile);
|
|
68
|
+
copyFileSync(src, dst);
|
|
69
|
+
execSync(`chmod +x "${dst}"`);
|
|
70
|
+
console.log(`installed: .githooks/${spec.gxpmFile}`);
|
|
71
|
+
|
|
72
|
+
const topLevelPath = join(hooksDir, spec.topLevelFile);
|
|
73
|
+
if (!existsSync(topLevelPath)) {
|
|
74
|
+
writeFileSync(topLevelPath, dispatcherScript(spec.gxpmFile, spec.argsForwarding));
|
|
75
|
+
execSync(`chmod +x "${topLevelPath}"`);
|
|
76
|
+
console.log(`created dispatcher: .githooks/${spec.topLevelFile}`);
|
|
77
|
+
} else {
|
|
78
|
+
skipped.push(spec.topLevelFile);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const absoluteHooksPath = join(resolve(target), ".githooks");
|
|
83
|
+
let currentHooksPath = "";
|
|
84
|
+
try {
|
|
85
|
+
currentHooksPath = execSync("git config core.hooksPath", { cwd: target }).toString().trim();
|
|
86
|
+
} catch {
|
|
87
|
+
// unset
|
|
88
|
+
}
|
|
89
|
+
if (currentHooksPath !== absoluteHooksPath) {
|
|
90
|
+
execSync(`git config core.hooksPath "${absoluteHooksPath}"`, { cwd: target });
|
|
91
|
+
console.log(`set git config core.hooksPath = ${absoluteHooksPath}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`core.hooksPath already = ${absoluteHooksPath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (skipped.length > 0) {
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log("Existing top-level hooks were NOT overwritten:");
|
|
99
|
+
for (const name of skipped) {
|
|
100
|
+
console.log(` .githooks/${name}`);
|
|
101
|
+
}
|
|
102
|
+
console.log("To wire gxpm into them, append:");
|
|
103
|
+
console.log(' [ -x .githooks/gxpm-<hook-name> ] && .githooks/gxpm-<hook-name> "$@"');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const gxpmRoot = resolve(import.meta.dir, "..");
|
|
107
|
+
installAgentsFragment(target, gxpmRoot);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installAgentsFragment(target: string, _gxpmRoot: string): void {
|
|
111
|
+
const agentsPath = join(target, "AGENTS.md");
|
|
112
|
+
if (!existsSync(agentsPath)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const MARKER_START = "<!-- GXPM:CODE-REVIEW-GRAPH:START -->";
|
|
117
|
+
const MARKER_END = "<!-- GXPM:CODE-REVIEW-GRAPH:END -->";
|
|
118
|
+
|
|
119
|
+
let content = readFileSync(agentsPath, "utf8");
|
|
120
|
+
if (content.includes(MARKER_START) && content.includes(MARKER_END)) {
|
|
121
|
+
const regex = new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}\\n?`, "g");
|
|
122
|
+
content = content.replace(regex, "");
|
|
123
|
+
writeFileSync(agentsPath, content);
|
|
124
|
+
console.log(`cleaned legacy graph marker from: ${agentsPath}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main(Bun.argv.slice(2));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install Kimi CLI hooks that call the unified `gxpm hook` entry point.
|
|
3
|
+
*
|
|
4
|
+
* Appends/updates a `[[hooks]]` block in `~/.kimi/config.toml`.
|
|
5
|
+
* Kimi hooks are user-scoped only (no repo-level hooks.json equivalent).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
interface InstallKimiHooksOptions {
|
|
13
|
+
home?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface InstallResult {
|
|
17
|
+
configTomlPath: string;
|
|
18
|
+
rootDir: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const GXPM_HOOKS_START = "# gxpm hooks (managed by gxpm init) — do not edit manually";
|
|
22
|
+
const GXPM_HOOKS_END = "# end gxpm hooks";
|
|
23
|
+
|
|
24
|
+
const HOOKS_TOML = `[[hooks]]
|
|
25
|
+
event = "SessionStart"
|
|
26
|
+
command = "gxpm hook SessionStart --host kimi"
|
|
27
|
+
matcher = "startup|clear|compact"
|
|
28
|
+
timeout = 30
|
|
29
|
+
|
|
30
|
+
[[hooks]]
|
|
31
|
+
event = "UserPromptSubmit"
|
|
32
|
+
command = "gxpm hook UserPromptSubmit --host kimi"
|
|
33
|
+
matcher = ""
|
|
34
|
+
timeout = 30
|
|
35
|
+
|
|
36
|
+
[[hooks]]
|
|
37
|
+
event = "PreToolUse"
|
|
38
|
+
command = "gxpm hook PreToolUse --host kimi"
|
|
39
|
+
matcher = "edit_file|write_file"
|
|
40
|
+
timeout = 10`;
|
|
41
|
+
|
|
42
|
+
export function installKimiHooks(options: InstallKimiHooksOptions = {}): InstallResult {
|
|
43
|
+
const home = options.home ?? homedir();
|
|
44
|
+
const rootDir = join(home, ".kimi");
|
|
45
|
+
mkdirSync(rootDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const configTomlPath = join(rootDir, "config.toml");
|
|
48
|
+
|
|
49
|
+
let content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf8") : "";
|
|
50
|
+
content = replaceGxpmBlock(content, HOOKS_TOML);
|
|
51
|
+
writeFileSync(configTomlPath, content);
|
|
52
|
+
|
|
53
|
+
return { configTomlPath, rootDir };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function replaceGxpmBlock(content: string, block: string): string {
|
|
57
|
+
const startIdx = content.indexOf(GXPM_HOOKS_START);
|
|
58
|
+
const endIdx = content.indexOf(GXPM_HOOKS_END);
|
|
59
|
+
|
|
60
|
+
if (startIdx >= 0 && endIdx >= 0 && endIdx > startIdx) {
|
|
61
|
+
// Replace existing block
|
|
62
|
+
const before = content.slice(0, startIdx);
|
|
63
|
+
const after = content.slice(endIdx + GXPM_HOOKS_END.length);
|
|
64
|
+
return `${before.trimEnd()}\n\n${GXPM_HOOKS_START}\n${block}\n${GXPM_HOOKS_END}\n${after.trimStart()}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Append new block
|
|
68
|
+
const trimmed = content.trimEnd();
|
|
69
|
+
const separator = trimmed.length > 0 ? "\n\n" : "";
|
|
70
|
+
return `${trimmed}${separator}${GXPM_HOOKS_START}\n${block}\n${GXPM_HOOKS_END}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseArgs(argv: string[]): InstallKimiHooksOptions {
|
|
74
|
+
const opts: InstallKimiHooksOptions = {};
|
|
75
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
76
|
+
const a = argv[i];
|
|
77
|
+
if (a === "--home") opts.home = argv[++i];
|
|
78
|
+
else throw new Error(`Unknown argument: ${a}`);
|
|
79
|
+
}
|
|
80
|
+
return opts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (import.meta.main) {
|
|
84
|
+
try {
|
|
85
|
+
const result = installKimiHooks(parseArgs(Bun.argv.slice(2)));
|
|
86
|
+
console.log(`wrote: ${result.configTomlPath}`);
|
|
87
|
+
console.log("Restart Kimi CLI to activate hooks.");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { ALL_HOST_CONFIGS, getHostConfig } from "../hosts";
|
|
5
|
+
import { discoverTemplates } from "./discover-skills";
|
|
6
|
+
import { renderSkillContentForHost } from "./gen-skill-docs";
|
|
7
|
+
import { SkillParser, HostConverter, SkillWriter } from "../core/converters";
|
|
8
|
+
import type { HostConfig } from "../core/contracts/host";
|
|
9
|
+
|
|
10
|
+
interface InstallSkillOptions {
|
|
11
|
+
/** @deprecated use `hosts` instead */
|
|
12
|
+
hostName?: string; // "codex" | "claude" | "all"
|
|
13
|
+
/** Explicit list of host names to install to. Overrides `hostName`. */
|
|
14
|
+
hosts?: string[];
|
|
15
|
+
root?: string; // gxpm repo root (default: cwd)
|
|
16
|
+
home?: string; // override for testing (default: homedir())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Default root is the gxpm repo itself (the parent of scripts/), not cwd —
|
|
20
|
+
// install-skill must read templates from gxpm regardless of where it's invoked.
|
|
21
|
+
const DEFAULT_GXPM_ROOT = resolve(import.meta.dir, "..");
|
|
22
|
+
|
|
23
|
+
function resolveSkillInstallPath(skillName: string, host: HostConfig, home: string): string {
|
|
24
|
+
// For gxpm main skill, preserve backward-compatible path using host.globalRoot
|
|
25
|
+
if (skillName === "gxpm") {
|
|
26
|
+
return join(home, host.globalRoot, "SKILL.md");
|
|
27
|
+
}
|
|
28
|
+
// For other skills, install into host-specific skill directory
|
|
29
|
+
if (host.name === "codex") {
|
|
30
|
+
return join(home, ".codex", "skills", skillName, "SKILL.md");
|
|
31
|
+
}
|
|
32
|
+
if (host.name === "claude") {
|
|
33
|
+
return join(home, ".claude", "skills", skillName, "SKILL.md");
|
|
34
|
+
}
|
|
35
|
+
// Fallback to host.globalRoot parent + skill name
|
|
36
|
+
return join(home, dirname(host.globalRoot), skillName, "SKILL.md");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderOrReadSkill(root: string, host: HostConfig, template: ReturnType<typeof discoverTemplates>[number]): string {
|
|
40
|
+
if (template.tmpl.endsWith(".tmpl")) {
|
|
41
|
+
return renderSkillContentForHost(root, host, template.tmpl, template.references);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Static file: use the new AST pipeline for multi-platform conversion
|
|
45
|
+
const source = readFileSync(join(root, template.tmpl), "utf8");
|
|
46
|
+
const parser = new SkillParser();
|
|
47
|
+
const converter = new HostConverter();
|
|
48
|
+
const writer = new SkillWriter();
|
|
49
|
+
const doc = parser.parse(source);
|
|
50
|
+
const converted = converter.convert(doc, { hostName: host.name });
|
|
51
|
+
return writer.write(converted);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function installSkill(options: InstallSkillOptions = {}): string[] {
|
|
55
|
+
const root = options.root ?? DEFAULT_GXPM_ROOT;
|
|
56
|
+
const home = options.home ?? homedir();
|
|
57
|
+
const targets = resolveTargets(options.hostName, options.hosts);
|
|
58
|
+
|
|
59
|
+
const installed: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const template of discoverTemplates(root)) {
|
|
62
|
+
for (const host of targets) {
|
|
63
|
+
const content = renderOrReadSkill(root, host, template);
|
|
64
|
+
// For .tmpl files, frontmatter is not yet filtered; apply host-specific filtering.
|
|
65
|
+
// For static files, the new pipeline already filtered frontmatter.
|
|
66
|
+
const transformed = template.tmpl.endsWith(".tmpl") ? applyFrontmatter(content, host) : content;
|
|
67
|
+
const installPath = resolveSkillInstallPath(template.name, host, home);
|
|
68
|
+
mkdirSync(dirname(installPath), { recursive: true });
|
|
69
|
+
writeFileSync(installPath, transformed);
|
|
70
|
+
installed.push(installPath);
|
|
71
|
+
|
|
72
|
+
// Install references
|
|
73
|
+
if (template.references) {
|
|
74
|
+
const skillInstallDir = dirname(installPath);
|
|
75
|
+
const refsInstallDir = join(skillInstallDir, "references");
|
|
76
|
+
for (const refPath of template.references) {
|
|
77
|
+
const refContent = readFileSync(join(root, refPath), "utf8");
|
|
78
|
+
const refName = refPath.replace(/^.*\//, "");
|
|
79
|
+
const refInstallPath = join(refsInstallDir, refName);
|
|
80
|
+
mkdirSync(refsInstallDir, { recursive: true });
|
|
81
|
+
writeFileSync(refInstallPath, refContent);
|
|
82
|
+
installed.push(refInstallPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Install scripts
|
|
87
|
+
if (template.scripts) {
|
|
88
|
+
const skillInstallDir = dirname(installPath);
|
|
89
|
+
const scriptsInstallDir = join(skillInstallDir, "scripts");
|
|
90
|
+
for (const scriptPath of template.scripts) {
|
|
91
|
+
const scriptContent = readFileSync(join(root, scriptPath), "utf8");
|
|
92
|
+
const scriptName = scriptPath.replace(/^.*\//, "");
|
|
93
|
+
const scriptInstallPath = join(scriptsInstallDir, scriptName);
|
|
94
|
+
mkdirSync(scriptsInstallDir, { recursive: true });
|
|
95
|
+
writeFileSync(scriptInstallPath, scriptContent);
|
|
96
|
+
installed.push(scriptInstallPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return installed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveTargets(name: string | undefined, hosts: string[] | undefined): readonly HostConfig[] {
|
|
106
|
+
if (hosts && hosts.length > 0) {
|
|
107
|
+
return hosts.map((n) => getHostConfig(n));
|
|
108
|
+
}
|
|
109
|
+
if (!name || name === "all") {
|
|
110
|
+
return ALL_HOST_CONFIGS;
|
|
111
|
+
}
|
|
112
|
+
return [getHostConfig(name)];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function applyFrontmatter(content: string, host: HostConfig): string {
|
|
116
|
+
if (host.frontmatter.mode === "preserve") {
|
|
117
|
+
return content;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
121
|
+
if (!match) {
|
|
122
|
+
return content;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const body = content.slice(match[0].length);
|
|
126
|
+
const allowed = new Set(host.frontmatter.keys);
|
|
127
|
+
const filteredFrontmatter = match[1]
|
|
128
|
+
.split("\n")
|
|
129
|
+
.filter((line) => {
|
|
130
|
+
if (!line.trim() || line.startsWith("#")) return true;
|
|
131
|
+
const colonIndex = line.indexOf(":");
|
|
132
|
+
if (colonIndex < 0) return true;
|
|
133
|
+
const key = line.slice(0, colonIndex).trim();
|
|
134
|
+
return allowed.has(key);
|
|
135
|
+
})
|
|
136
|
+
.join("\n");
|
|
137
|
+
|
|
138
|
+
return `---\n${filteredFrontmatter}\n---\n${body}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface ParsedArgs {
|
|
142
|
+
hostName?: string;
|
|
143
|
+
hosts?: string[];
|
|
144
|
+
root?: string;
|
|
145
|
+
home?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
149
|
+
const options: ParsedArgs = {};
|
|
150
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
151
|
+
const arg = argv[index];
|
|
152
|
+
if (arg === "--host") {
|
|
153
|
+
options.hostName = argv[++index];
|
|
154
|
+
} else if (arg === "--hosts") {
|
|
155
|
+
const raw = argv[++index];
|
|
156
|
+
options.hosts = raw ? raw.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
157
|
+
} else if (arg === "--root") {
|
|
158
|
+
options.root = argv[++index];
|
|
159
|
+
} else if (arg === "--home") {
|
|
160
|
+
options.home = argv[++index];
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return options;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (import.meta.main) {
|
|
169
|
+
try {
|
|
170
|
+
const args = parseArgs(Bun.argv.slice(2));
|
|
171
|
+
const installed = installSkill({
|
|
172
|
+
hostName: args.hostName,
|
|
173
|
+
hosts: args.hosts,
|
|
174
|
+
root: args.root ? resolve(args.root) : undefined,
|
|
175
|
+
home: args.home ? resolve(args.home) : undefined,
|
|
176
|
+
});
|
|
177
|
+
for (const path of installed) {
|
|
178
|
+
console.log(`installed: ${path}`);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { initializeAcceptanceCheck } from "../core/ac-check";
|
|
2
|
+
import { type ArtifactType } from "../core/artifacts";
|
|
3
|
+
import { initializeDispatch } from "../core/dispatch";
|
|
4
|
+
import { initializeLocalVerify } from "../core/implement";
|
|
5
|
+
import { initializeLandFindings } from "../core/land";
|
|
6
|
+
import { PHASE_GATE_RULES } from "../core/phase-gates";
|
|
7
|
+
import { initializePlan } from "../core/plan";
|
|
8
|
+
import { initializePrCheck } from "../core/pr-check";
|
|
9
|
+
import { initializeQaFindings } from "../core/qa";
|
|
10
|
+
import { initializeSelfReview } from "../core/self-review";
|
|
11
|
+
import { initializeShipReadiness } from "../core/ship";
|
|
12
|
+
import { initializeCleanup } from "../core/cleanup";
|
|
13
|
+
import { initializeSpecify } from "../core/specify";
|
|
14
|
+
import { initializeTriage } from "../core/triage";
|
|
15
|
+
import { initializeVerifyFindings } from "../core/verify";
|
|
16
|
+
|
|
17
|
+
interface PhaseArtifactCommand {
|
|
18
|
+
artifactType: ArtifactType;
|
|
19
|
+
command: string;
|
|
20
|
+
initialize: (input: { issueId: string }) => unknown;
|
|
21
|
+
successMessage: (issueId: string) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PHASE_ARTIFACT_HANDLERS: Partial<Record<
|
|
25
|
+
ArtifactType,
|
|
26
|
+
Pick<PhaseArtifactCommand, "initialize" | "successMessage">
|
|
27
|
+
>> = {
|
|
28
|
+
"acceptance-contract": {
|
|
29
|
+
initialize: initializeTriage,
|
|
30
|
+
successMessage: (issueId) => `initialized triage artifacts for ${issueId}`,
|
|
31
|
+
},
|
|
32
|
+
"implementation-plan": {
|
|
33
|
+
initialize: initializePlan,
|
|
34
|
+
successMessage: (issueId) => `initialized plan artifact for ${issueId}`,
|
|
35
|
+
},
|
|
36
|
+
"dispatch-handoff": {
|
|
37
|
+
initialize: initializeDispatch,
|
|
38
|
+
successMessage: (issueId) => `initialized dispatch handoff for ${issueId}`,
|
|
39
|
+
},
|
|
40
|
+
"behavior-spec": {
|
|
41
|
+
initialize: initializeSpecify,
|
|
42
|
+
successMessage: (issueId) => `initialized behavior-spec artifact for ${issueId}`,
|
|
43
|
+
},
|
|
44
|
+
"local-verify": {
|
|
45
|
+
initialize: initializeLocalVerify,
|
|
46
|
+
successMessage: (issueId) => `initialized local verify artifact for ${issueId}`,
|
|
47
|
+
},
|
|
48
|
+
"acceptance-check": {
|
|
49
|
+
initialize: initializeAcceptanceCheck,
|
|
50
|
+
successMessage: (issueId) => `initialized acceptance check artifact for ${issueId}`,
|
|
51
|
+
},
|
|
52
|
+
"self-review": {
|
|
53
|
+
initialize: (input) => initializeSelfReview({ ...input, army: process.argv.includes("--army") }),
|
|
54
|
+
successMessage: (issueId) => `initialized self review artifact for ${issueId}`,
|
|
55
|
+
},
|
|
56
|
+
"cleanup-report": {
|
|
57
|
+
initialize: initializeCleanup,
|
|
58
|
+
successMessage: (issueId) => `initialized cleanup report for ${issueId}`,
|
|
59
|
+
},
|
|
60
|
+
"ship-readiness": {
|
|
61
|
+
initialize: (input) => initializeShipReadiness({ ...input, army: process.argv.includes("--army") }),
|
|
62
|
+
successMessage: (issueId) => `initialized ship readiness artifact for ${issueId}`,
|
|
63
|
+
},
|
|
64
|
+
"pr-check": {
|
|
65
|
+
initialize: initializePrCheck,
|
|
66
|
+
successMessage: (issueId) => `initialized pr check artifact for ${issueId}`,
|
|
67
|
+
},
|
|
68
|
+
"verify-findings": {
|
|
69
|
+
initialize: initializeVerifyFindings,
|
|
70
|
+
successMessage: (issueId) => `initialized verify findings artifact for ${issueId}`,
|
|
71
|
+
},
|
|
72
|
+
"qa-findings": {
|
|
73
|
+
initialize: initializeQaFindings,
|
|
74
|
+
successMessage: (issueId) => `initialized QA findings artifact for ${issueId}`,
|
|
75
|
+
},
|
|
76
|
+
"land-findings": {
|
|
77
|
+
initialize: initializeLandFindings,
|
|
78
|
+
successMessage: (issueId) => `initialized land findings artifact for ${issueId}`,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const PHASE_ARTIFACT_COMMANDS: PhaseArtifactCommand[] = PHASE_GATE_RULES.map((rule) => {
|
|
83
|
+
const handler = PHASE_ARTIFACT_HANDLERS[rule.requiredArtifact];
|
|
84
|
+
if (!handler) {
|
|
85
|
+
throw new Error(`Missing phase artifact handler for ${rule.requiredArtifact}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
artifactType: rule.requiredArtifact,
|
|
90
|
+
command: rule.command,
|
|
91
|
+
...handler,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export function findPhaseArtifactCommand(command: string, subcommand: string | undefined) {
|
|
96
|
+
return PHASE_ARTIFACT_COMMANDS.find((item) => {
|
|
97
|
+
const [, registeredCommand, registeredSubcommand] = item.command.split(" ");
|
|
98
|
+
return registeredCommand === command && registeredSubcommand === subcommand;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
interface PostLandSkillSyncOptions {
|
|
4
|
+
root?: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface PostLandSkillSyncResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_GXPM_ROOT = resolve(import.meta.dir, "..");
|
|
14
|
+
|
|
15
|
+
export function runPostLandSkillSync(options: PostLandSkillSyncOptions = {}): PostLandSkillSyncResult {
|
|
16
|
+
const root = options.root ?? DEFAULT_GXPM_ROOT;
|
|
17
|
+
const env = options.env ?? process.env;
|
|
18
|
+
if (env.GXPM_SKIP_POST_LAND_SYNC === "1") {
|
|
19
|
+
return { ok: true, message: "post-land skill install skipped" };
|
|
20
|
+
}
|
|
21
|
+
const gxpmInit = env.GXPM_INIT_BIN || join(root, "bin", "gxpm-init");
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = Bun.spawnSync({
|
|
25
|
+
cmd: [gxpmInit, "--install-skill", "--host", "all"],
|
|
26
|
+
cwd: root,
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
env,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (result.exitCode === 0) {
|
|
33
|
+
return { ok: true, message: "skill install sync completed" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const stderr = result.stderr.toString().trim();
|
|
37
|
+
const stdout = result.stdout.toString().trim();
|
|
38
|
+
const detail = stderr || stdout || `exit code ${result.exitCode}`;
|
|
39
|
+
return { ok: false, message: `post-land skill install failed: ${detail}` };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: `post-land skill install failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { ARTIFACT_TYPES, type ArtifactType } from "../core/artifacts";
|
|
3
|
+
import { CAPABILITY_REGISTRY } from "../core/capabilities";
|
|
4
|
+
import { listValidatedArtifactTypes } from "../core/artifact-validator";
|
|
5
|
+
import { PHASE_GATE_RULES } from "../core/phase-gates";
|
|
6
|
+
import { ALL_HOST_CONFIGS } from "../hosts";
|
|
7
|
+
import { generateSkillDocs } from "./gen-skill-docs";
|
|
8
|
+
import { validateGovernanceDocs } from "./governance-check";
|
|
9
|
+
import { validateAllConfigs } from "./host-config";
|
|
10
|
+
import { validateSkillNaming } from "./skill-naming-check";
|
|
11
|
+
import { validateVersionTruth } from "./version";
|
|
12
|
+
import { validateSkillsLock } from "./skills-lock-check";
|
|
13
|
+
import { validateSkillStructure, formatSkillStructureErrors } from "./skill-structure-check";
|
|
14
|
+
|
|
15
|
+
// Default to the gxpm repo itself (parent of scripts/) so the check is
|
|
16
|
+
// meaningful regardless of the cwd the CLI was invoked from.
|
|
17
|
+
const DEFAULT_GXPM_ROOT = resolve(import.meta.dir, "..");
|
|
18
|
+
|
|
19
|
+
export interface RunScaffoldCheckOptions {
|
|
20
|
+
root?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateLayeredWorkflowContracts(): string[] {
|
|
24
|
+
const errors: string[] = [];
|
|
25
|
+
const artifactTypes = new Set<ArtifactType>(ARTIFACT_TYPES);
|
|
26
|
+
const validatedArtifactTypes = new Set<ArtifactType>(listValidatedArtifactTypes());
|
|
27
|
+
const capabilityArtifacts = new Set<ArtifactType>(
|
|
28
|
+
CAPABILITY_REGISTRY.flatMap((capability) => capability.outputContract.artifacts),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
for (const rule of PHASE_GATE_RULES) {
|
|
32
|
+
if (!capabilityArtifacts.has(rule.requiredArtifact)) {
|
|
33
|
+
errors.push(
|
|
34
|
+
`phase gate ${rule.fromPhase}->${rule.nextPhase} requires ${rule.requiredArtifact}, but no capability outputs it`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const artifactType of ARTIFACT_TYPES) {
|
|
40
|
+
if (!validatedArtifactTypes.has(artifactType)) {
|
|
41
|
+
errors.push(`artifact ${artifactType} is not covered by artifact-validator`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const artifactType of capabilityArtifacts) {
|
|
46
|
+
if (!artifactTypes.has(artifactType)) {
|
|
47
|
+
errors.push(`capability outputs unknown artifact ${artifactType}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runScaffoldCheck(options: RunScaffoldCheckOptions = {}) {
|
|
55
|
+
const root = options.root ?? DEFAULT_GXPM_ROOT;
|
|
56
|
+
|
|
57
|
+
const hostErrors = validateAllConfigs(ALL_HOST_CONFIGS);
|
|
58
|
+
const governanceErrors = validateGovernanceDocs({ root });
|
|
59
|
+
const versionErrors = validateVersionTruth({ root });
|
|
60
|
+
const skillNamingErrors = validateSkillNaming({ root });
|
|
61
|
+
const skillsLockErrors = validateSkillsLock({ root });
|
|
62
|
+
const layeredWorkflowErrors = validateLayeredWorkflowContracts();
|
|
63
|
+
const skillStructureViolations = validateSkillStructure(root);
|
|
64
|
+
const { errors: skillStructureErrors, warnings: skillStructureWarnings } = formatSkillStructureErrors(skillStructureViolations);
|
|
65
|
+
const errors = [
|
|
66
|
+
...hostErrors.map((error) => `host config: ${error}`),
|
|
67
|
+
...governanceErrors.map((error) => `governance: ${error}`),
|
|
68
|
+
...versionErrors.map((error) => `version: ${error}`),
|
|
69
|
+
...skillNamingErrors.map((error) => `skill-naming: ${error}`),
|
|
70
|
+
...skillsLockErrors.map((error) => `skills-lock: ${error}`),
|
|
71
|
+
...layeredWorkflowErrors.map((error) => `layered-workflow: ${error}`),
|
|
72
|
+
...skillStructureErrors,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
if (skillStructureWarnings.length > 0) {
|
|
76
|
+
console.warn(skillStructureWarnings.join("\n"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (errors.length > 0) {
|
|
80
|
+
throw new Error(errors.join("\n"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
generateSkillDocs({ root, dryRun: true });
|
|
84
|
+
return `gxpm scaffold check passed (${ALL_HOST_CONFIGS.length} hosts)`;
|
|
85
|
+
}
|