@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,123 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
createIssueState,
|
|
5
|
+
getIssuePaths,
|
|
6
|
+
isIssueType,
|
|
7
|
+
ISSUE_TYPES,
|
|
8
|
+
type IssueType,
|
|
9
|
+
} from "../../core/state";
|
|
10
|
+
import { writeArtifact } from "../../core/artifacts";
|
|
11
|
+
import { getResolvedConfigValue } from "../../core/config";
|
|
12
|
+
import { getNextAvailableIssueId } from "../../core/issues";
|
|
13
|
+
import { optionRequiredValue, optionValue } from "./helpers";
|
|
14
|
+
|
|
15
|
+
const ISSUE_TYPE_USAGE = ISSUE_TYPES.join("|");
|
|
16
|
+
const FEEDBACK_CREATE_USAGE = `Usage: gxpm feedback create <issue-id>|--auto-id [--title <title>] [--description <text>] [--type ${ISSUE_TYPE_USAGE}]`;
|
|
17
|
+
|
|
18
|
+
export async function runFeedbackCommand(argv: string[], subcommand: string | undefined) {
|
|
19
|
+
if (subcommand === "create") {
|
|
20
|
+
await runFeedbackCreate(argv);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`Usage: gxpm feedback <subcommand>
|
|
25
|
+
|
|
26
|
+
Subcommands:
|
|
27
|
+
create <issue-id>|--auto-id [--title <title>] [--description <text>] [--type meta|feature|spike]`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runFeedbackCreate(argv: string[]) {
|
|
31
|
+
const sourceRoot = resolveFeedbackSourceRoot();
|
|
32
|
+
const args = argv.slice(2);
|
|
33
|
+
|
|
34
|
+
const hasAutoId = args.includes("--auto-id");
|
|
35
|
+
const positional: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
38
|
+
const arg = args[index];
|
|
39
|
+
if (arg === "--auto-id") continue;
|
|
40
|
+
if (arg === "--title" || arg === "--description" || arg === "--type") {
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg.startsWith("--")) {
|
|
45
|
+
throw new Error(`Unknown option for gxpm feedback create: ${arg}`);
|
|
46
|
+
}
|
|
47
|
+
positional.push(arg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (hasAutoId && positional.length > 0) {
|
|
51
|
+
throw new Error(FEEDBACK_CREATE_USAGE);
|
|
52
|
+
}
|
|
53
|
+
if (positional.length > 1) {
|
|
54
|
+
throw new Error(FEEDBACK_CREATE_USAGE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const issueId = positional[0] ?? (hasAutoId ? getNextAvailableIssueId({ root: sourceRoot }) : undefined);
|
|
58
|
+
if (!issueId) {
|
|
59
|
+
throw new Error(FEEDBACK_CREATE_USAGE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const title = optionValue(argv, "--title") ?? undefined;
|
|
63
|
+
const description = optionValue(argv, "--description") ?? undefined;
|
|
64
|
+
const issueType = parseFeedbackIssueType(argv);
|
|
65
|
+
|
|
66
|
+
// Ensure .gxpm directory exists in source root
|
|
67
|
+
const gxpmDir = join(sourceRoot, ".gxpm");
|
|
68
|
+
if (!existsSync(gxpmDir)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Feedback source root does not appear to be a gxpm repository: ${sourceRoot}\n` +
|
|
71
|
+
`Missing .gxpm directory. Set feedback.gxpmSourceRoot to a valid gxpm repo path.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const state = createIssueState({ root: sourceRoot, issueId, issueType });
|
|
76
|
+
|
|
77
|
+
// Write feedback-description artifact when title or description is provided
|
|
78
|
+
if (title || description) {
|
|
79
|
+
writeArtifact({
|
|
80
|
+
root: sourceRoot,
|
|
81
|
+
issueId,
|
|
82
|
+
type: "feedback-description",
|
|
83
|
+
payload: {
|
|
84
|
+
title: title ?? "",
|
|
85
|
+
description: description ?? "",
|
|
86
|
+
reportedFrom: process.cwd(),
|
|
87
|
+
reportedAt: new Date().toISOString(),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`created feedback issue ${state.issueId} at ${state.currentPhase}`);
|
|
93
|
+
console.log(`type: ${state.issueType}`);
|
|
94
|
+
console.log(`sourceRoot: ${sourceRoot}`);
|
|
95
|
+
console.log(`statePath: ${getIssuePaths(sourceRoot, issueId).statePath}`);
|
|
96
|
+
if (title) {
|
|
97
|
+
console.log(`title: ${title}`);
|
|
98
|
+
}
|
|
99
|
+
if (description) {
|
|
100
|
+
console.log(`description: ${description}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveFeedbackSourceRoot(): string {
|
|
105
|
+
const resolved = getResolvedConfigValue({ key: "feedback.gxpmSourceRoot" });
|
|
106
|
+
const raw = String(resolved.value ?? "").trim();
|
|
107
|
+
if (!raw) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`feedback.gxpmSourceRoot is not configured.\n` +
|
|
110
|
+
`Set it with: gxpm config set feedback.gxpmSourceRoot </absolute/path/to/gxpm/repo>`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return resolve(raw);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseFeedbackIssueType(argv: string[]): IssueType {
|
|
117
|
+
const raw = optionValue(argv, "--type");
|
|
118
|
+
if (!raw) return "meta";
|
|
119
|
+
if (!isIssueType(raw)) {
|
|
120
|
+
throw new Error(`Invalid issue type: ${raw}; expected ${ISSUE_TYPE_USAGE}`);
|
|
121
|
+
}
|
|
122
|
+
return raw;
|
|
123
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { appendIssueEvent, getIssuePaths, readIssueState, transitionIssuePhase, type StateEvent } from "../../core/state";
|
|
3
|
+
import { hasArtifact, readArtifact } from "../../core/artifacts";
|
|
4
|
+
import { evaluateBranchPolicy, evaluateCommitMsg, evaluatePostMerge, evaluatePreCommit, evaluatePrePush } from "../../core/gate";
|
|
5
|
+
import { initializeLandFindings, reconcileLandFindings } from "../../core/land";
|
|
6
|
+
import { runPostLandSkillSync } from "../post-land-sync";
|
|
7
|
+
import { asRecord, currentGitBranch, currentGitRoot, detectCanonicalMainRoot, optionValue } from "./helpers";
|
|
8
|
+
import { getResolvedConfigValue } from "../../core/config";
|
|
9
|
+
|
|
10
|
+
export function runGateCommand(argv: string[], subcommand: string | undefined, issueId: string | undefined) {
|
|
11
|
+
if (subcommand === "pre-commit") {
|
|
12
|
+
runPreCommitGate(argv, issueId);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (subcommand === "branch-policy") {
|
|
16
|
+
runBranchPolicyGate(argv);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (subcommand === "commit-msg") {
|
|
20
|
+
runCommitMsgGate(argv, issueId);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (subcommand === "pre-push") {
|
|
24
|
+
runPrePushGate(issueId);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (subcommand === "post-merge") {
|
|
28
|
+
runPostMergeGate(issueId);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (subcommand === "post-merge-reconcile") {
|
|
32
|
+
runPostMergeReconcileGate(argv, issueId);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (subcommand === "brainstorm-skip") {
|
|
36
|
+
runBrainstormSkipGate(argv, issueId);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Unknown command: ${["gate", subcommand].filter(Boolean).join(" ")}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runBrainstormSkipGate(argv: string[], issueId: string | undefined) {
|
|
43
|
+
if (!issueId) {
|
|
44
|
+
throw new Error("Usage: gxpm gate brainstorm-skip <issue-id> --reason <text>");
|
|
45
|
+
}
|
|
46
|
+
const reason = optionValue(argv, "--reason");
|
|
47
|
+
if (!reason) {
|
|
48
|
+
throw new Error("Usage: gxpm gate brainstorm-skip <issue-id> --reason <text>");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stateRoot = process.cwd();
|
|
52
|
+
const paths = getIssuePaths(stateRoot, issueId);
|
|
53
|
+
const intake = readArtifact({ root: stateRoot, issueId, type: "issue-intake" });
|
|
54
|
+
const payload = asRecord(intake.payload);
|
|
55
|
+
const acceptance = Array.isArray(payload.acceptance) ? payload.acceptance : [];
|
|
56
|
+
const antiPatterns = Array.isArray(payload.verified_pitfalls_to_avoid)
|
|
57
|
+
? payload.verified_pitfalls_to_avoid
|
|
58
|
+
: [];
|
|
59
|
+
|
|
60
|
+
const event: StateEvent = {
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
type: "gate.brainstorm.skipped",
|
|
63
|
+
issueId,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
payload: {
|
|
66
|
+
reason,
|
|
67
|
+
intake_completeness_score: acceptance.length > 0 ? 1 : 0,
|
|
68
|
+
ac_count: acceptance.length,
|
|
69
|
+
anti_pattern_count: antiPatterns.length,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
appendIssueEvent({ issueDir: paths.issueDir, event });
|
|
74
|
+
console.log(`[gxpm gate brainstorm-skip] gate.brainstorm.skipped: ${reason}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function gateEvent(verdict: { allowed: boolean; code: string; reason: string; details?: Record<string, unknown> }, gate: string, issueId: string): StateEvent {
|
|
78
|
+
return {
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
type: verdict.allowed ? "gate.passed" : "gate.blocked",
|
|
81
|
+
issueId,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
payload: { gate, code: verdict.code, reason: verdict.reason, ...(verdict.details ?? {}) },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runBranchPolicyGate(argv: string[]) {
|
|
88
|
+
const currentRoot = currentGitRoot() ?? process.cwd();
|
|
89
|
+
const currentBranch = optionValue(argv, "--branch") ?? currentGitBranch();
|
|
90
|
+
const canonicalMainRoot =
|
|
91
|
+
optionValue(argv, "--canonical-main") ??
|
|
92
|
+
process.env.GXPM_CANONICAL_MAIN ??
|
|
93
|
+
detectCanonicalMainRoot() ??
|
|
94
|
+
currentRoot;
|
|
95
|
+
const allowedWorktreeRoot = optionValue(argv, "--worktree-root") ?? process.env.GXPM_WORKTREE_ROOT ?? undefined;
|
|
96
|
+
const baseBranch = getResolvedConfigValue({ key: "worktree.baseBranch" }).value as string;
|
|
97
|
+
const verdict = evaluateBranchPolicy({
|
|
98
|
+
currentRoot,
|
|
99
|
+
currentBranch,
|
|
100
|
+
canonicalMainRoot,
|
|
101
|
+
allowedWorktreeRoot,
|
|
102
|
+
baseBranch,
|
|
103
|
+
env: process.env,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!verdict.allowed) {
|
|
107
|
+
console.error(`[gxpm gate branch-policy] ${verdict.code}: ${verdict.reason}`);
|
|
108
|
+
console.error(`Use a dedicated git worktree for feature branches; keep the canonical checkout on ${baseBranch}.`);
|
|
109
|
+
console.error("Escape: GXPM_GATE_DISABLE=1 git ...");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`[gxpm gate branch-policy] ${verdict.code}: ${verdict.reason}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runPreCommitGate(argv: string[], issueId: string | undefined) {
|
|
117
|
+
if (!issueId) {
|
|
118
|
+
throw new Error("Usage: gxpm gate pre-commit <issue-id> --staged <files...>");
|
|
119
|
+
}
|
|
120
|
+
const dashStaged = argv.indexOf("--staged");
|
|
121
|
+
const stagedFiles = dashStaged >= 0 ? argv.slice(dashStaged + 1) : [];
|
|
122
|
+
|
|
123
|
+
const stateRoot = process.cwd();
|
|
124
|
+
const paths = getIssuePaths(stateRoot, issueId);
|
|
125
|
+
if (!existsSync(paths.statePath)) {
|
|
126
|
+
console.log(`no-state: ${issueId} not tracked by gxpm`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const state = readIssueState({ root: stateRoot, issueId });
|
|
131
|
+
const verdict = evaluatePreCommit(state, stagedFiles, process.env);
|
|
132
|
+
appendIssueEvent({ issueDir: paths.issueDir, event: gateEvent(verdict, "pre-commit", issueId) });
|
|
133
|
+
|
|
134
|
+
if (!verdict.allowed) {
|
|
135
|
+
console.error(`[gxpm gate pre-commit] ${verdict.code}: ${verdict.reason}`);
|
|
136
|
+
const hits = verdict.details?.protectedHits;
|
|
137
|
+
if (Array.isArray(hits)) {
|
|
138
|
+
for (const f of hits) console.error(` - ${f}`);
|
|
139
|
+
}
|
|
140
|
+
console.error("Escape: GXPM_GATE_DISABLE=1 git commit ...");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(`[gxpm gate pre-commit] ${verdict.code}: ${verdict.reason}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runCommitMsgGate(argv: string[], firstArg: string | undefined) {
|
|
147
|
+
const msgFile = firstArg;
|
|
148
|
+
if (!msgFile) {
|
|
149
|
+
throw new Error("Usage: gxpm gate commit-msg <msg-file> [--issue <id>]");
|
|
150
|
+
}
|
|
151
|
+
const dashIssue = argv.indexOf("--issue");
|
|
152
|
+
const explicitIssue = dashIssue >= 0 ? argv[dashIssue + 1] : null;
|
|
153
|
+
const message = readFileSync(msgFile, "utf8");
|
|
154
|
+
|
|
155
|
+
const inferred =
|
|
156
|
+
explicitIssue ??
|
|
157
|
+
(message.match(/\b(GXG|GXPM)-\d+\b/i)?.[0]?.toUpperCase() ?? null);
|
|
158
|
+
if (!inferred) {
|
|
159
|
+
console.error(
|
|
160
|
+
"[gxpm gate commit-msg] missing-issue-ref: commit message must reference GXG-NNN or GXPM-NNN",
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const stateRoot = process.cwd();
|
|
166
|
+
const paths = getIssuePaths(stateRoot, inferred);
|
|
167
|
+
if (!existsSync(paths.statePath)) {
|
|
168
|
+
console.log(`no-state: ${inferred} not tracked by gxpm`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const state = readIssueState({ root: stateRoot, issueId: inferred });
|
|
173
|
+
const verdict = evaluateCommitMsg(message, state, process.env);
|
|
174
|
+
if (!verdict.allowed) {
|
|
175
|
+
console.error(`[gxpm gate commit-msg] ${verdict.code}: ${verdict.reason}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
console.log(`[gxpm gate commit-msg] ${verdict.code}: ${verdict.reason}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function runPrePushGate(issueId: string | undefined) {
|
|
182
|
+
if (!issueId) {
|
|
183
|
+
throw new Error("Usage: gxpm gate pre-push <issue-id>");
|
|
184
|
+
}
|
|
185
|
+
const stateRoot = process.cwd();
|
|
186
|
+
const paths = getIssuePaths(stateRoot, issueId);
|
|
187
|
+
if (!existsSync(paths.statePath)) {
|
|
188
|
+
console.log(`no-state: ${issueId} not tracked by gxpm`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const state = readIssueState({ root: stateRoot, issueId });
|
|
193
|
+
const verdict = evaluatePrePush(
|
|
194
|
+
state,
|
|
195
|
+
(id, t) => hasArtifact({ root: stateRoot, issueId: id, type: t }),
|
|
196
|
+
process.env,
|
|
197
|
+
stateRoot,
|
|
198
|
+
);
|
|
199
|
+
if (!verdict.allowed) {
|
|
200
|
+
console.error(`[gxpm gate pre-push] ${verdict.code}: ${verdict.reason}`);
|
|
201
|
+
if (verdict.details && typeof verdict.details.command === "string") {
|
|
202
|
+
console.error(
|
|
203
|
+
`Hint: ${verdict.details.command.replace("<issue-id>", issueId)}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
console.log(`[gxpm gate pre-push] ${verdict.code}: ${verdict.reason}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function runPostMergeGate(issueId: string | undefined) {
|
|
212
|
+
if (!issueId) {
|
|
213
|
+
throw new Error("Usage: gxpm gate post-merge <issue-id>");
|
|
214
|
+
}
|
|
215
|
+
const stateRoot = process.cwd();
|
|
216
|
+
const paths = getIssuePaths(stateRoot, issueId);
|
|
217
|
+
if (!existsSync(paths.statePath)) {
|
|
218
|
+
console.log(`no-state: ${issueId} not tracked by gxpm`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const state = readIssueState({ root: stateRoot, issueId });
|
|
223
|
+
const outcome = evaluatePostMerge(state);
|
|
224
|
+
if (!outcome.transitionTo) {
|
|
225
|
+
console.log(`[gxpm gate post-merge] ${outcome.reason}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Auto-initialize the gate artifact before transitioning so the merge always
|
|
230
|
+
// produces a complete trail. Idempotent: writeArtifact overwrites existing files.
|
|
231
|
+
if (outcome.transitionTo === "land" && !hasArtifact({ root: stateRoot, issueId, type: "land-findings" })) {
|
|
232
|
+
initializeLandFindings({ root: stateRoot, issueId });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const updated = transitionIssuePhase({ root: stateRoot, issueId, nextPhase: outcome.transitionTo });
|
|
236
|
+
console.log(`transitioned ${issueId}: ${state.currentPhase} -> ${updated.currentPhase}`);
|
|
237
|
+
if (updated.currentPhase === "land") {
|
|
238
|
+
console.log(`Hint: gxpm cleanup land ${issueId} --execute # 清理 worktree + local branch`);
|
|
239
|
+
const sync = runPostLandSkillSync({ env: process.env });
|
|
240
|
+
if (!sync.ok) {
|
|
241
|
+
console.error(`[gxpm land sync] ${sync.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function runPostMergeReconcileGate(argv: string[], issueId: string | undefined) {
|
|
247
|
+
if (!issueId) {
|
|
248
|
+
throw new Error("Usage: gxpm gate post-merge-reconcile <issue-id> --sha <sha>");
|
|
249
|
+
}
|
|
250
|
+
const sha = optionValue(argv, "--sha");
|
|
251
|
+
if (!sha) {
|
|
252
|
+
throw new Error("Usage: gxpm gate post-merge-reconcile <issue-id> --sha <sha>");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const stateRoot = process.cwd();
|
|
256
|
+
const paths = getIssuePaths(stateRoot, issueId);
|
|
257
|
+
if (!existsSync(paths.statePath)) {
|
|
258
|
+
console.log(`no-state: ${issueId} not tracked by gxpm`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const state = readIssueState({ root: stateRoot, issueId });
|
|
263
|
+
if (state.currentPhase !== "land") {
|
|
264
|
+
console.log(`[gxpm gate post-merge-reconcile] phase=${state.currentPhase} is not land`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!hasArtifact({ root: stateRoot, issueId, type: "land-findings" })) {
|
|
268
|
+
console.log("[gxpm gate post-merge-reconcile] land-findings artifact missing");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (!gitCommitExists(stateRoot, sha)) {
|
|
272
|
+
throw new Error(`Git commit not found for post-merge reconcile: ${sha}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const result = reconcileLandFindings({ root: stateRoot, issueId, sha });
|
|
276
|
+
if (result.reconciled) {
|
|
277
|
+
console.log(`[gxpm gate post-merge-reconcile] reconciled land-findings for ${issueId}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(`[gxpm gate post-merge-reconcile] ${result.reason}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function gitCommitExists(cwd: string, sha: string) {
|
|
284
|
+
const result = Bun.spawnSync({
|
|
285
|
+
cmd: ["git", "cat-file", "-e", `${sha}^{commit}`],
|
|
286
|
+
cwd,
|
|
287
|
+
stdout: "pipe",
|
|
288
|
+
stderr: "pipe",
|
|
289
|
+
});
|
|
290
|
+
return result.exitCode === 0;
|
|
291
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export function readJsonPayloadFromArgs(argv: string[], usagePrefix: string) {
|
|
4
|
+
const dashJson = argv.indexOf("--json");
|
|
5
|
+
const dashFrom = argv.indexOf("--from");
|
|
6
|
+
const useStdin = argv.includes("--stdin");
|
|
7
|
+
|
|
8
|
+
const inputs = [dashJson >= 0, dashFrom >= 0, useStdin].filter(Boolean).length;
|
|
9
|
+
if (inputs === 0) {
|
|
10
|
+
throw new Error(`${usagePrefix} requires one of: --json <json> | --from <file> | --stdin`);
|
|
11
|
+
}
|
|
12
|
+
if (inputs > 1) {
|
|
13
|
+
throw new Error(`${usagePrefix}: pick exactly one of --json / --from / --stdin`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let raw: string;
|
|
17
|
+
if (dashJson >= 0) {
|
|
18
|
+
raw = argv[dashJson + 1] ?? "";
|
|
19
|
+
} else if (dashFrom >= 0) {
|
|
20
|
+
const file = argv[dashFrom + 1];
|
|
21
|
+
if (!file) throw new Error("--from requires a file path");
|
|
22
|
+
raw = readFileSync(file, "utf8");
|
|
23
|
+
} else {
|
|
24
|
+
raw = readFileSync(0, "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${usagePrefix}: invalid JSON payload — ${error instanceof Error ? error.message : String(error)}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parsePositiveIntegerOption(argv: string[], option: string) {
|
|
37
|
+
if (!argv.includes(option)) return undefined;
|
|
38
|
+
const raw = optionRequiredValue(argv, option);
|
|
39
|
+
const value = Number.parseInt(raw, 10);
|
|
40
|
+
if (!Number.isInteger(value) || value < 1 || String(value) !== raw) {
|
|
41
|
+
throw new Error(`${option} requires a positive integer`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function optionRequiredValue(argv: string[], option: string) {
|
|
47
|
+
const value = optionValue(argv, option);
|
|
48
|
+
if (!value) {
|
|
49
|
+
throw new Error(`${option} requires a value`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function optionValue(argv: string[], option: string) {
|
|
55
|
+
const index = argv.indexOf(option);
|
|
56
|
+
if (index < 0) return null;
|
|
57
|
+
const value = argv[index + 1];
|
|
58
|
+
return value && !value.startsWith("--") ? value : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function payloadTitle(payload: unknown) {
|
|
62
|
+
if (!payload || typeof payload !== "object") return null;
|
|
63
|
+
const title = (payload as Record<string, unknown>).title;
|
|
64
|
+
return typeof title === "string" && title.trim() ? title : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function asRecord(value: unknown): Record<string, unknown> {
|
|
68
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
69
|
+
? (value as Record<string, unknown>)
|
|
70
|
+
: {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function currentGitBranch() {
|
|
74
|
+
const result = Bun.spawnSync({
|
|
75
|
+
cmd: ["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
stdout: "pipe",
|
|
78
|
+
stderr: "pipe",
|
|
79
|
+
});
|
|
80
|
+
if (result.exitCode !== 0) return undefined;
|
|
81
|
+
const branch = result.stdout.toString().trim();
|
|
82
|
+
return branch || undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function currentGitRoot() {
|
|
86
|
+
const result = Bun.spawnSync({
|
|
87
|
+
cmd: ["git", "rev-parse", "--show-toplevel"],
|
|
88
|
+
cwd: process.cwd(),
|
|
89
|
+
stdout: "pipe",
|
|
90
|
+
stderr: "pipe",
|
|
91
|
+
});
|
|
92
|
+
if (result.exitCode !== 0) return undefined;
|
|
93
|
+
const root = result.stdout.toString().trim();
|
|
94
|
+
return root || undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function detectCanonicalMainRoot() {
|
|
98
|
+
const result = Bun.spawnSync({
|
|
99
|
+
cmd: ["git", "worktree", "list", "--porcelain"],
|
|
100
|
+
cwd: process.cwd(),
|
|
101
|
+
stdout: "pipe",
|
|
102
|
+
stderr: "pipe",
|
|
103
|
+
});
|
|
104
|
+
if (result.exitCode !== 0) return undefined;
|
|
105
|
+
return result.stdout
|
|
106
|
+
.toString()
|
|
107
|
+
.split("\n")
|
|
108
|
+
.find((line) => line.startsWith("worktree "))
|
|
109
|
+
?.slice("worktree ".length);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect whether the current cwd is inside a linked git worktree (not the canonical main checkout).
|
|
114
|
+
*/
|
|
115
|
+
export function isInsideLinkedWorktree(): boolean {
|
|
116
|
+
const result = Bun.spawnSync({
|
|
117
|
+
cmd: ["git", "rev-parse", "--git-path", "HEAD"],
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
stdout: "pipe",
|
|
120
|
+
stderr: "pipe",
|
|
121
|
+
});
|
|
122
|
+
if (result.exitCode !== 0) return false;
|
|
123
|
+
const gitPath = result.stdout.toString().trim();
|
|
124
|
+
// In a linked worktree, git-path resolves to .git/worktrees/<name>/HEAD
|
|
125
|
+
return gitPath.includes("/worktrees/");
|
|
126
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gxpm hook <event> --host <host>`
|
|
3
|
+
*
|
|
4
|
+
* Unified hook entry point called by all host-specific hook configurations.
|
|
5
|
+
* Reads JSON from stdin, dispatches to the hook engine, and prints host-formatted output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { processHook, formatHookOutput, isValidHookHost, type HookInput } from "../../core/hook-engine";
|
|
9
|
+
|
|
10
|
+
export async function runHookCommand(argv: string[]): Promise<void> {
|
|
11
|
+
// argv: ["hook", "SessionStart", "--host", "codex", ...]
|
|
12
|
+
const eventName = argv[1];
|
|
13
|
+
if (!eventName) {
|
|
14
|
+
console.error("Usage: gxpm hook <event> --host <claude|codex|cursor|kimi>");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let host: string | undefined;
|
|
19
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
20
|
+
if (argv[i] === "--host" && i + 1 < argv.length) {
|
|
21
|
+
host = argv[i + 1];
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!host || !isValidHookHost(host)) {
|
|
27
|
+
console.error("Usage: gxpm hook <event> --host <claude|codex|cursor|kimi>");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let input: HookInput;
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readStdin();
|
|
34
|
+
input = raw ? JSON.parse(raw) : ({} as HookInput);
|
|
35
|
+
} catch {
|
|
36
|
+
// fail-open: if stdin is not valid JSON, allow the action to proceed
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await processHook(host, eventName, input);
|
|
41
|
+
|
|
42
|
+
const output = formatHookOutput(host, eventName, result);
|
|
43
|
+
if (output) {
|
|
44
|
+
console.log(output);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.exit(result.exitCode);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readStdin(): Promise<string> {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
if (process.stdin.isTTY) {
|
|
53
|
+
resolve("");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let data = "";
|
|
57
|
+
process.stdin.setEncoding("utf8");
|
|
58
|
+
process.stdin.on("data", (chunk) => {
|
|
59
|
+
data += chunk;
|
|
60
|
+
});
|
|
61
|
+
process.stdin.on("end", () => {
|
|
62
|
+
resolve(data);
|
|
63
|
+
});
|
|
64
|
+
process.stdin.resume();
|
|
65
|
+
});
|
|
66
|
+
}
|