@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,825 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
createIssueState,
|
|
5
|
+
getIssuePaths,
|
|
6
|
+
isIssueType,
|
|
7
|
+
ISSUE_TYPES,
|
|
8
|
+
readIssueState,
|
|
9
|
+
setIssueArchived,
|
|
10
|
+
transitionIssuePhase,
|
|
11
|
+
type IssueType,
|
|
12
|
+
type StateEvent,
|
|
13
|
+
} from "../../core/state";
|
|
14
|
+
import { readSyncState } from "../../core/issue-sync";
|
|
15
|
+
import { hasArtifact } from "../../core/artifacts";
|
|
16
|
+
import { readResumePacket, writeIssueCheckpoint } from "../../core/checkpoint";
|
|
17
|
+
import { buildIssueContext } from "../../core/issue-context";
|
|
18
|
+
import { getNextAvailableIssueId, listIssues, recentLandedIssues } from "../../core/issues";
|
|
19
|
+
import { PHASE_GATE_RULES } from "../../core/phase-gates";
|
|
20
|
+
import {
|
|
21
|
+
claimIssue,
|
|
22
|
+
listIssueReadiness,
|
|
23
|
+
listReadyIssues,
|
|
24
|
+
reconcileIssueClaim,
|
|
25
|
+
releaseIssueClaim,
|
|
26
|
+
} from "../../core/issue-readiness";
|
|
27
|
+
import { runPostLandSkillSync } from "../post-land-sync";
|
|
28
|
+
import { ensureIssueWorkspaceWithResolver } from "../../core/workspace-runtime";
|
|
29
|
+
import { readArtifact, writeArtifact } from "../../core/artifacts";
|
|
30
|
+
import { currentGitBranch, detectCanonicalMainRoot, currentGitRoot, optionRequiredValue, optionValue, parsePositiveIntegerOption, payloadTitle, readJsonPayloadFromArgs } from "./helpers";
|
|
31
|
+
import { getResolvedConfigValue } from "../../core/config";
|
|
32
|
+
import { readWorktreeOwner, writeWorktreeOwnerMarker, writeIssueContextMd } from "../../core/worktree-owner";
|
|
33
|
+
|
|
34
|
+
const ISSUE_TYPE_USAGE = ISSUE_TYPES.join("|");
|
|
35
|
+
const ISSUE_TYPE_LIST = formatList(ISSUE_TYPES);
|
|
36
|
+
const ISSUE_CREATE_USAGE = `Usage: gxpm issue create <issue-id> (or --auto-id) [--type ${ISSUE_TYPE_USAGE}] [--parent <parent-issue-id>]`;
|
|
37
|
+
|
|
38
|
+
export async function runIssueCommand(argv: string[], subcommand: string | undefined, issueId: string | undefined, value: string | undefined) {
|
|
39
|
+
if (subcommand === "create") {
|
|
40
|
+
const resolvedId = resolveIssueCreateId(argv);
|
|
41
|
+
const issueType = parseIssueTypeOption(argv, "feature");
|
|
42
|
+
const parentId = parseParentOption(argv);
|
|
43
|
+
let state = createIssueState({ issueId: resolvedId, issueType });
|
|
44
|
+
if (parentId) {
|
|
45
|
+
state = addIssueRelation({ childId: resolvedId, parentId });
|
|
46
|
+
console.log(`parent: ${parentId}`);
|
|
47
|
+
}
|
|
48
|
+
console.log(`created ${state.issueId} at ${state.currentPhase}`);
|
|
49
|
+
console.log(`statePath: ${getIssuePaths(process.cwd(), resolvedId).statePath}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (subcommand === "status") {
|
|
54
|
+
if (!issueId) throw new Error("Usage: gxpm issue status <issue-id>");
|
|
55
|
+
const state = readIssueState({ issueId });
|
|
56
|
+
console.log(`issueId: ${state.issueId}`);
|
|
57
|
+
console.log(`currentPhase: ${state.currentPhase}`);
|
|
58
|
+
console.log(`updatedAt: ${state.updatedAt}`);
|
|
59
|
+
console.log(`statePath: ${getIssuePaths(process.cwd(), issueId).statePath}`);
|
|
60
|
+
if (state.creator) {
|
|
61
|
+
console.log(`creator: ${state.creator.actor} (${state.creator.host})`);
|
|
62
|
+
}
|
|
63
|
+
if (state.claim?.status === "claimed") {
|
|
64
|
+
console.log(`assignee: ${state.claim.actor} (${state.claim.claimedBySession.split(":")[0] ?? "unknown"})`);
|
|
65
|
+
}
|
|
66
|
+
const syncState = readSyncState({ issueId });
|
|
67
|
+
if (syncState.targets.length > 0) {
|
|
68
|
+
for (const target of syncState.targets) {
|
|
69
|
+
const syncStatus = target.lastError ? `error: ${target.lastError.message}` : `synced at ${target.syncedAt ?? "unknown"}`;
|
|
70
|
+
console.log(`external: ${target.provider} ${target.displayId} (${target.url}) — ${syncStatus}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (subcommand === "list") {
|
|
77
|
+
const json = argv.includes("--json");
|
|
78
|
+
const includeAll = argv.includes("--all");
|
|
79
|
+
const archivedOnly = argv.includes("--archived");
|
|
80
|
+
const types = parseIssueTypesOption(argv);
|
|
81
|
+
const limit = parsePositiveIntegerOption(argv, "--limit");
|
|
82
|
+
const recentIdx = argv.indexOf("--recent");
|
|
83
|
+
const recentN = recentIdx >= 0 ? parseInt(argv[recentIdx + 1] ?? "5", 10) || 5 : 0;
|
|
84
|
+
let entries: ReturnType<typeof listIssues>;
|
|
85
|
+
if (recentN > 0) {
|
|
86
|
+
if (types || limit !== undefined) {
|
|
87
|
+
throw new Error("gxpm issue list --recent cannot be combined with --type or --limit");
|
|
88
|
+
}
|
|
89
|
+
entries = recentLandedIssues({ limit: recentN });
|
|
90
|
+
} else {
|
|
91
|
+
entries = listIssues({ includeAll, archivedOnly, types, limit });
|
|
92
|
+
}
|
|
93
|
+
if (json) {
|
|
94
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (entries.length === 0) {
|
|
98
|
+
const hint = includeAll
|
|
99
|
+
? "no issues tracked under .gxpm/issues/"
|
|
100
|
+
: "no active issues (use 'gxpm issue list --all' to include landed/archived)";
|
|
101
|
+
console.log(hint);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const idWidth = Math.max(8, ...entries.map((e) => e.issueId.length));
|
|
105
|
+
const typeWidth = Math.max(7, ...entries.map((e) => e.issueType.length));
|
|
106
|
+
const phaseWidth = Math.max(13, ...entries.map((e) => e.currentPhase.length));
|
|
107
|
+
console.log(`${"ISSUE".padEnd(idWidth)} ${"TYPE".padEnd(typeWidth)} ${"PHASE".padEnd(phaseWidth)} UPDATED FLAGS`);
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
let flags = entry.archived ? "archived" : "";
|
|
110
|
+
try {
|
|
111
|
+
const st = readIssueState({ issueId: entry.issueId });
|
|
112
|
+
const hasParent = st.relations?.some((r) => r.relation === "parent");
|
|
113
|
+
const hasChild = st.relations?.some((r) => r.relation === "child");
|
|
114
|
+
const relFlags = [hasParent ? "has-parent" : "", hasChild ? "has-child" : ""].filter(Boolean).join(",");
|
|
115
|
+
if (relFlags) {
|
|
116
|
+
flags = flags ? `${flags},${relFlags}` : relFlags;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// ignore
|
|
120
|
+
}
|
|
121
|
+
console.log(
|
|
122
|
+
`${entry.issueId.padEnd(idWidth)} ${entry.issueType.padEnd(typeWidth)} ${entry.currentPhase.padEnd(phaseWidth)} ${entry.updatedAt} ${flags}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (subcommand === "ready") {
|
|
129
|
+
runIssueReady(argv);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (subcommand === "claim") {
|
|
134
|
+
runIssueClaim(argv, issueId);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (subcommand === "release") {
|
|
139
|
+
runIssueRelease(argv, issueId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (subcommand === "reconcile-claim") {
|
|
144
|
+
runIssueReconcileClaim(argv, issueId);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (subcommand === "archive" || subcommand === "unarchive") {
|
|
149
|
+
if (!issueId) throw new Error(`Usage: gxpm issue ${subcommand} <issue-id>`);
|
|
150
|
+
setIssueArchived({ issueId, archived: subcommand === "archive" });
|
|
151
|
+
console.log(`${subcommand === "archive" ? "archived" : "unarchived"} ${issueId}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (subcommand === "next") {
|
|
156
|
+
if (!issueId) throw new Error("Usage: gxpm issue next <issue-id>");
|
|
157
|
+
runIssueNext(issueId);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (subcommand === "checkpoint") {
|
|
162
|
+
if (!issueId) {
|
|
163
|
+
throw new Error("Usage: gxpm issue checkpoint <issue-id> --title <title> --json <json> | --from <file> | --stdin");
|
|
164
|
+
}
|
|
165
|
+
runIssueCheckpoint(argv, issueId);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (subcommand === "resume") {
|
|
170
|
+
if (!issueId) throw new Error("Usage: gxpm issue resume <issue-id>");
|
|
171
|
+
runIssueResume(issueId);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (subcommand === "batch") {
|
|
176
|
+
if (!issueId) throw new Error("Usage: gxpm issue batch <issue-id>");
|
|
177
|
+
runIssueBatch(issueId);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (subcommand === "history") {
|
|
182
|
+
if (!issueId) throw new Error("Usage: gxpm issue history <issue-id> [--json]");
|
|
183
|
+
runIssueHistory(issueId, argv.includes("--json"));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (subcommand === "ownership") {
|
|
188
|
+
if (!issueId) throw new Error("Usage: gxpm issue ownership <issue-id> [--field <name>] [--history-contains <session-id>]");
|
|
189
|
+
runIssueOwnership(argv, issueId);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (subcommand === "context") {
|
|
194
|
+
const asJson = argv.includes("--json");
|
|
195
|
+
if (argv.includes("--auto")) {
|
|
196
|
+
runIssueContext("--auto", asJson);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!issueId) throw new Error("Usage: gxpm issue context <issue-id> [--json]");
|
|
200
|
+
runIssueContext(issueId, asJson);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (subcommand === "transition") {
|
|
205
|
+
if (!issueId || !value) throw new Error("Usage: gxpm issue transition <issue-id> <phase>");
|
|
206
|
+
const before = readIssueState({ issueId });
|
|
207
|
+
const after = transitionIssuePhase({ issueId, nextPhase: value, skipCleanup: argv.includes("--skip-cleanup") });
|
|
208
|
+
|
|
209
|
+
// Auto-ensure worktree on dispatch -> specify transition
|
|
210
|
+
if (before.currentPhase === "dispatch" && after.currentPhase === "specify") {
|
|
211
|
+
try {
|
|
212
|
+
// Build linkedIssues from relations so child issues reuse parent worktree
|
|
213
|
+
const linkedIssues = (before.relations ?? []).map((r) => r.issueId);
|
|
214
|
+
const result = await ensureIssueWorkspaceWithResolver({ issueId, existingEnvId: issueId, hints: linkedIssues.length > 0 ? { linkedIssues } : undefined });
|
|
215
|
+
if (result.resolution?.status === "resolved" && result.resolution.env) {
|
|
216
|
+
const handoff = readArtifact({ issueId, type: "dispatch-handoff" });
|
|
217
|
+
writeArtifact({
|
|
218
|
+
issueId,
|
|
219
|
+
type: "dispatch-handoff",
|
|
220
|
+
payload: {
|
|
221
|
+
...(handoff.payload as Record<string, unknown>),
|
|
222
|
+
worktreePath: result.resolution.env.workspacePath,
|
|
223
|
+
worktreeDecision: result.method?.type === "created" ? "created" : "reused",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
// Write enhanced worktree ownership marker + passive context recovery file
|
|
227
|
+
const allLinked = [issueId, ...linkedIssues];
|
|
228
|
+
try {
|
|
229
|
+
writeWorktreeOwnerMarker(result.workspacePath, {
|
|
230
|
+
ownerIssueId: issueId,
|
|
231
|
+
linkedIssues: allLinked,
|
|
232
|
+
currentPhase: after.currentPhase,
|
|
233
|
+
branchName: result.resolution.env.branchName,
|
|
234
|
+
workspacePath: result.resolution.env.workspacePath,
|
|
235
|
+
});
|
|
236
|
+
writeIssueContextMd(result.workspacePath, {
|
|
237
|
+
issueId,
|
|
238
|
+
currentPhase: after.currentPhase,
|
|
239
|
+
branchName: result.resolution.env.branchName,
|
|
240
|
+
workspacePath: result.resolution.env.workspacePath,
|
|
241
|
+
updatedAt: new Date().toISOString(),
|
|
242
|
+
});
|
|
243
|
+
} catch {
|
|
244
|
+
// best-effort; marker is advisory
|
|
245
|
+
}
|
|
246
|
+
console.log(`worktree: ${result.workspacePath}`);
|
|
247
|
+
if (result.resolution.env.branchName) {
|
|
248
|
+
console.log(`branch: ${result.resolution.env.branchName}`);
|
|
249
|
+
}
|
|
250
|
+
if (result.warnings) {
|
|
251
|
+
for (const warning of result.warnings) {
|
|
252
|
+
console.log(`warning: ${warning}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (result.resolution?.status === "blocked" && result.userMessage) {
|
|
256
|
+
console.error(`worktree blocked: ${result.userMessage}`);
|
|
257
|
+
}
|
|
258
|
+
} catch (worktreeError) {
|
|
259
|
+
const message = worktreeError instanceof Error ? worktreeError.message : String(worktreeError);
|
|
260
|
+
console.error(`worktree ensure failed: ${message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Refresh passive context recovery files on any transition while in a worktree
|
|
265
|
+
refreshWorktreeContextFiles(after.issueId, after.currentPhase);
|
|
266
|
+
|
|
267
|
+
console.log(`transitioned ${after.issueId}: ${before.currentPhase} -> ${after.currentPhase}`);
|
|
268
|
+
if (after.currentPhase === "land") {
|
|
269
|
+
const sync = runPostLandSkillSync({ env: process.env });
|
|
270
|
+
if (!sync.ok) console.error(`[gxpm land sync] ${sync.message}`);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error(`Unknown command: ${["issue", subcommand].filter(Boolean).join(" ")}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function runIssueHistory(issueId: string, asJson: boolean) {
|
|
279
|
+
const paths = getIssuePaths(process.cwd(), issueId);
|
|
280
|
+
if (!existsSync(paths.eventsPath)) {
|
|
281
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const lines = readFileSync(paths.eventsPath, "utf8")
|
|
285
|
+
.split("\n")
|
|
286
|
+
.map((line) => line.trim())
|
|
287
|
+
.filter(Boolean);
|
|
288
|
+
const events = lines.map((line) => JSON.parse(line) as StateEvent);
|
|
289
|
+
|
|
290
|
+
if (asJson) {
|
|
291
|
+
console.log(JSON.stringify(events, null, 2));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(`${issueId} timeline (${events.length} event${events.length === 1 ? "" : "s"})`);
|
|
296
|
+
console.log("─".repeat(60));
|
|
297
|
+
|
|
298
|
+
for (const event of events) {
|
|
299
|
+
const ts = event.timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
300
|
+
const type = event.type.padEnd(20);
|
|
301
|
+
const detail = formatEventDetail(event);
|
|
302
|
+
console.log(`${ts} ${type} ${detail}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatEventDetail(event: StateEvent): string {
|
|
307
|
+
const p = event.payload as Record<string, unknown>;
|
|
308
|
+
switch (event.type) {
|
|
309
|
+
case "issue.created":
|
|
310
|
+
return `phase: ${p.initialPhase ?? "?"}`;
|
|
311
|
+
case "phase.transitioned":
|
|
312
|
+
return `${p.fromPhase ?? "?"} → ${p.toPhase ?? "?"}`;
|
|
313
|
+
case "artifact.written":
|
|
314
|
+
return `${p.artifactType ?? "?"} (${p.path ?? ""})`;
|
|
315
|
+
case "artifact.reconciled":
|
|
316
|
+
return `${p.artifactType ?? "?"} (${p.mergedSha ?? ""})`;
|
|
317
|
+
case "checkpoint.written":
|
|
318
|
+
return `${p.checkpointPath ?? "?"} (${p.resumePacketPath ?? ""})`;
|
|
319
|
+
case "gate.passed":
|
|
320
|
+
if (p.gate) return `${p.gate} (${p.code ?? ""})`;
|
|
321
|
+
return `${p.fromPhase ?? "?"} → ${p.toPhase ?? "?"} (${p.requiredArtifact ?? ""})`;
|
|
322
|
+
case "gate.blocked":
|
|
323
|
+
if (p.gate) return `${p.gate}: ${p.reason ?? ""}`;
|
|
324
|
+
return `${p.fromPhase ?? "?"} → ${p.toPhase ?? "?"} blocked: ${p.missingArtifact ?? ""}`;
|
|
325
|
+
case "issue.claimed":
|
|
326
|
+
return `${p.actor ?? "?"} by ${p.claimedBySession ?? "?"}`;
|
|
327
|
+
case "issue.claim.released":
|
|
328
|
+
return `${p.releaseReason ?? "released"} by ${p.releasedBySession ?? "?"}`;
|
|
329
|
+
case "issue.claim.stale":
|
|
330
|
+
return `${p.staleReason ?? "stale"} since ${p.staleAt ?? "?"}`;
|
|
331
|
+
case "ownership.changed":
|
|
332
|
+
return `${p.fromSession ?? "?"} → ${p.toSession ?? "?"}`;
|
|
333
|
+
default:
|
|
334
|
+
return JSON.stringify(p);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function runIssueReady(argv: string[]) {
|
|
339
|
+
const includeAll = argv.includes("--all");
|
|
340
|
+
const issues = includeAll ? listIssueReadiness({ includeAll: true }) : listReadyIssues();
|
|
341
|
+
if (argv.includes("--json")) {
|
|
342
|
+
console.log(JSON.stringify(issues, null, 2));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (issues.length === 0) {
|
|
346
|
+
console.log(includeAll ? "no issues tracked" : "no ready issues");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const issueWidth = Math.max("ISSUE".length, ...issues.map((issue) => issue.issueId.length));
|
|
350
|
+
const phaseWidth = Math.max("PHASE".length, ...issues.map((issue) => issue.currentPhase.length));
|
|
351
|
+
const decisionWidth = Math.max("DECISION".length, ...issues.map((issue) => issue.decision.length));
|
|
352
|
+
console.log(
|
|
353
|
+
`${"ISSUE".padEnd(issueWidth)} ${"PHASE".padEnd(phaseWidth)} ${"DECISION".padEnd(decisionWidth)} REASON`,
|
|
354
|
+
);
|
|
355
|
+
for (const issue of issues) {
|
|
356
|
+
console.log(
|
|
357
|
+
`${issue.issueId.padEnd(issueWidth)} ${issue.currentPhase.padEnd(phaseWidth)} ${issue.decision.padEnd(decisionWidth)} ${issue.reason}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function runIssueClaim(argv: string[], issueId: string | undefined) {
|
|
363
|
+
const useNext = argv.includes("--next");
|
|
364
|
+
if (useNext && issueId && !issueId.startsWith("--")) {
|
|
365
|
+
throw new Error("Usage: choose either `gxpm issue claim <issue-id>` or `gxpm issue claim --next`");
|
|
366
|
+
}
|
|
367
|
+
const actor = argv.includes("--actor") ? optionRequiredValue(argv, "--actor") : undefined;
|
|
368
|
+
const runId = argv.includes("--run") ? optionRequiredValue(argv, "--run") : undefined;
|
|
369
|
+
const targetIssueId = useNext ? listReadyIssues()[0]?.issueId : issueId;
|
|
370
|
+
if (useNext && !targetIssueId) {
|
|
371
|
+
throw new Error("No ready issues to claim");
|
|
372
|
+
}
|
|
373
|
+
if (!targetIssueId || targetIssueId.startsWith("--")) {
|
|
374
|
+
throw new Error("Usage: gxpm issue claim <issue-id> [--actor <name>] [--run <run-id>] [--json] | gxpm issue claim --next [--actor <name>] [--run <run-id>] [--json]");
|
|
375
|
+
}
|
|
376
|
+
const result = claimIssue({
|
|
377
|
+
issueId: targetIssueId,
|
|
378
|
+
actor,
|
|
379
|
+
runId,
|
|
380
|
+
});
|
|
381
|
+
if (argv.includes("--json")) {
|
|
382
|
+
console.log(JSON.stringify(result, null, 2));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
console.log(`${result.claimed ? "claimed" : "already claimed"} ${result.issueId}`);
|
|
386
|
+
console.log(`actor: ${result.claim.actor}`);
|
|
387
|
+
console.log(`session: ${result.claim.claimedBySession}`);
|
|
388
|
+
if (result.claim.runId) console.log(`runId: ${result.claim.runId}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function runIssueRelease(argv: string[], issueId: string | undefined) {
|
|
392
|
+
if (!issueId || issueId.startsWith("-")) {
|
|
393
|
+
throw new Error("Usage: gxpm issue release <issue-id> [--reason <text>] [--json]");
|
|
394
|
+
}
|
|
395
|
+
const reason = argv.includes("--reason") ? optionRequiredValue(argv, "--reason") : undefined;
|
|
396
|
+
const result = releaseIssueClaim({
|
|
397
|
+
issueId,
|
|
398
|
+
reason,
|
|
399
|
+
});
|
|
400
|
+
if (argv.includes("--json")) {
|
|
401
|
+
console.log(JSON.stringify(result, null, 2));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
console.log(`${result.released ? "released" : "already released"} ${result.issueId}`);
|
|
405
|
+
console.log(`reason: ${result.claim.status === "released" ? result.claim.releaseReason : "already_released"}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function runIssueReconcileClaim(argv: string[], issueId: string | undefined) {
|
|
409
|
+
if (!issueId || issueId.startsWith("-")) {
|
|
410
|
+
throw new Error("Usage: gxpm issue reconcile-claim <issue-id> [--stale-after-ms N] [--json]");
|
|
411
|
+
}
|
|
412
|
+
const result = reconcileIssueClaim({
|
|
413
|
+
issueId,
|
|
414
|
+
staleAfterMs: parsePositiveIntegerOption(argv, "--stale-after-ms"),
|
|
415
|
+
});
|
|
416
|
+
if (argv.includes("--json")) {
|
|
417
|
+
console.log(JSON.stringify(result, null, 2));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
console.log(`${result.reconciled ? "reconciled" : "unchanged"} ${result.issueId}`);
|
|
421
|
+
console.log(`action: ${result.action}`);
|
|
422
|
+
console.log(`reason: ${result.reason}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function runIssueOwnership(argv: string[], issueId: string) {
|
|
426
|
+
const state = readIssueState({ issueId });
|
|
427
|
+
const ownership = state.ownership;
|
|
428
|
+
const field = optionValue(argv, "--field");
|
|
429
|
+
const historyContains = optionValue(argv, "--history-contains");
|
|
430
|
+
|
|
431
|
+
if (historyContains) {
|
|
432
|
+
process.exit(ownership?.history.some((entry) => entry.sessionId === historyContains) ? 0 : 1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (field) {
|
|
436
|
+
if (field === "currentSession") {
|
|
437
|
+
console.log(ownership?.currentSession ?? "");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (field === "history") {
|
|
441
|
+
console.log(JSON.stringify(ownership?.history ?? []));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
throw new Error(`Unknown ownership field: ${field}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log(`issueId: ${state.issueId}`);
|
|
448
|
+
console.log(`currentSession: ${ownership?.currentSession ?? ""}`);
|
|
449
|
+
console.log("history:");
|
|
450
|
+
console.log("session\tfirstTouch\tlastTouch");
|
|
451
|
+
for (const entry of ownership?.history ?? []) {
|
|
452
|
+
console.log(`${entry.sessionId}\t${entry.firstTouch}\t${entry.lastTouch}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function runIssueNext(issueId: string) {
|
|
457
|
+
const state = readIssueState({ issueId });
|
|
458
|
+
console.log(`${issueId} currentPhase: ${state.currentPhase}`);
|
|
459
|
+
|
|
460
|
+
const rule = PHASE_GATE_RULES.find((r) => r.fromPhase === state.currentPhase);
|
|
461
|
+
if (!rule) {
|
|
462
|
+
console.log("");
|
|
463
|
+
console.log(`Phase ${state.currentPhase} is terminal — no further transition.`);
|
|
464
|
+
if (state.currentPhase === "land") {
|
|
465
|
+
console.log("This issue has landed. Mark as Done in upstream issue tracker.");
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const has = hasArtifact({ issueId, type: rule.requiredArtifact });
|
|
471
|
+
console.log("");
|
|
472
|
+
|
|
473
|
+
// Worktree advisory: when in dispatch on canonical main checkout with a feature branch, warn early
|
|
474
|
+
if (state.currentPhase === "dispatch") {
|
|
475
|
+
const branch = currentGitBranch();
|
|
476
|
+
const baseBranch = getResolvedConfigValue({ key: "worktree.baseBranch" }).value as string;
|
|
477
|
+
if (branch && branch !== baseBranch) {
|
|
478
|
+
const canonicalRoot = detectCanonicalMainRoot();
|
|
479
|
+
const currentRoot = currentGitRoot();
|
|
480
|
+
if (canonicalRoot && currentRoot && currentRoot === canonicalRoot) {
|
|
481
|
+
console.log(`WARNING: You are on a feature branch in the canonical ${baseBranch} checkout.`);
|
|
482
|
+
console.log(" gxpm requires feature branches to run in a dedicated git worktree.");
|
|
483
|
+
console.log(` Run: gxpm workspace ensure ${issueId}`);
|
|
484
|
+
console.log("");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Phase command reference for agent clarity
|
|
490
|
+
console.log(`Available commands for ${state.currentPhase}:`);
|
|
491
|
+
console.log(` init: ${rule.command.replace("<issue-id>", issueId)}`);
|
|
492
|
+
console.log(` write: gxpm artifact write ${issueId} ${rule.requiredArtifact} --json '...'`);
|
|
493
|
+
console.log(` edit: gxpm artifact edit ${issueId} ${rule.requiredArtifact}`);
|
|
494
|
+
console.log(` transition: gxpm issue transition ${issueId} ${rule.nextPhase}`);
|
|
495
|
+
console.log("");
|
|
496
|
+
|
|
497
|
+
if (!has) {
|
|
498
|
+
console.log(`Next: ${rule.command.replace("<issue-id>", issueId)}`);
|
|
499
|
+
console.log(` → creates draft of artifact: ${rule.requiredArtifact}`);
|
|
500
|
+
console.log("");
|
|
501
|
+
console.log(`Then: edit the artifact (or use 'gxpm artifact write ${issueId} ${rule.requiredArtifact} --json ...')`);
|
|
502
|
+
console.log(`Then: gxpm issue transition ${issueId} ${rule.nextPhase}`);
|
|
503
|
+
} else {
|
|
504
|
+
console.log(`Artifact ${rule.requiredArtifact} already exists.`);
|
|
505
|
+
// Worktree advisory: when dispatch-handoff exists but worktree is still pending
|
|
506
|
+
if (state.currentPhase === "dispatch") {
|
|
507
|
+
try {
|
|
508
|
+
const handoff = readArtifact({ issueId, type: "dispatch-handoff" });
|
|
509
|
+
const decision = (handoff.payload as Record<string, unknown>)?.worktreeDecision;
|
|
510
|
+
if (decision === "pending") {
|
|
511
|
+
console.log(`Next: gxpm workspace ensure ${issueId}`);
|
|
512
|
+
console.log(" → prepares the git worktree before implementation");
|
|
513
|
+
console.log("");
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
// ignore missing dispatch-handoff
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
console.log(`Next: gxpm issue transition ${issueId} ${rule.nextPhase}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function runIssueCheckpoint(argv: string[], issueId: string) {
|
|
524
|
+
const payload = readJsonPayloadFromArgs(argv, "gxpm issue checkpoint");
|
|
525
|
+
const title = optionValue(argv, "--title") ?? payloadTitle(payload) ?? "checkpoint";
|
|
526
|
+
const reason = optionValue(argv, "--reason");
|
|
527
|
+
if (reason && payload && typeof payload === "object") {
|
|
528
|
+
(payload as Record<string, unknown>).transitionReason = reason;
|
|
529
|
+
}
|
|
530
|
+
const record = writeIssueCheckpoint({
|
|
531
|
+
issueId,
|
|
532
|
+
title,
|
|
533
|
+
branch: currentGitBranch(),
|
|
534
|
+
payload,
|
|
535
|
+
});
|
|
536
|
+
// Refresh passive context recovery files so the agent can recover after context compaction
|
|
537
|
+
try {
|
|
538
|
+
const state = readIssueState({ issueId });
|
|
539
|
+
refreshWorktreeContextFiles(issueId, state.currentPhase, title);
|
|
540
|
+
} catch {
|
|
541
|
+
// best-effort
|
|
542
|
+
}
|
|
543
|
+
console.log(`checkpoint saved for ${issueId}`);
|
|
544
|
+
console.log(`file: ${record.path}`);
|
|
545
|
+
console.log(`resume: ${record.resumePacketPath}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function refreshWorktreeContextFiles(issueId: string, currentPhase: string, title?: string) {
|
|
549
|
+
// Try to locate the worktree path from multiple sources
|
|
550
|
+
let workspacePath: string | undefined;
|
|
551
|
+
|
|
552
|
+
// 1. Check current directory first
|
|
553
|
+
const cwd = process.cwd();
|
|
554
|
+
const owner = readWorktreeOwner(cwd);
|
|
555
|
+
if (owner && (owner.ownerIssueId === issueId || owner.linkedIssues.includes(issueId))) {
|
|
556
|
+
workspacePath = owner.workspacePath ?? cwd;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 2. Fall back to dispatch-handoff artifact worktreePath
|
|
560
|
+
if (!workspacePath) {
|
|
561
|
+
try {
|
|
562
|
+
const handoff = readArtifact({ issueId, type: "dispatch-handoff" });
|
|
563
|
+
const payload = (handoff.payload ?? {}) as Record<string, unknown>;
|
|
564
|
+
const candidate =
|
|
565
|
+
(payload.worktreePath as string) ??
|
|
566
|
+
(payload.workspace as string) ??
|
|
567
|
+
(payload.worktree as string);
|
|
568
|
+
if (candidate && typeof candidate === "string") {
|
|
569
|
+
workspacePath = candidate;
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
// no dispatch-handoff
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!workspacePath) return;
|
|
577
|
+
|
|
578
|
+
// Read existing owner from the worktree to preserve createdAt / linkedIssues
|
|
579
|
+
const existingOwner = readWorktreeOwner(workspacePath);
|
|
580
|
+
const linkedIssues = existingOwner?.linkedIssues ?? [issueId];
|
|
581
|
+
const createdAt = existingOwner?.createdAt ?? new Date().toISOString();
|
|
582
|
+
|
|
583
|
+
const phaseOrder = [
|
|
584
|
+
"triage", "plan", "dispatch", "specify", "implement",
|
|
585
|
+
"local-verify", "ac-check", "self-review", "ship",
|
|
586
|
+
"pr-check", "verify", "qa", "land",
|
|
587
|
+
] as const;
|
|
588
|
+
const idx = phaseOrder.indexOf(currentPhase as (typeof phaseOrder)[number]);
|
|
589
|
+
const nextPhase = idx >= 0 && idx < phaseOrder.length - 1 ? phaseOrder[idx + 1] : undefined;
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
writeWorktreeOwnerMarker(workspacePath, {
|
|
593
|
+
ownerIssueId: existingOwner?.ownerIssueId ?? issueId,
|
|
594
|
+
linkedIssues,
|
|
595
|
+
createdAt,
|
|
596
|
+
currentPhase,
|
|
597
|
+
title: title ?? existingOwner?.title,
|
|
598
|
+
updatedAt: new Date().toISOString(),
|
|
599
|
+
branchName: existingOwner?.branchName ?? currentGitBranch(),
|
|
600
|
+
workspacePath,
|
|
601
|
+
});
|
|
602
|
+
writeIssueContextMd(workspacePath, {
|
|
603
|
+
issueId: existingOwner?.ownerIssueId ?? issueId,
|
|
604
|
+
currentPhase,
|
|
605
|
+
title: title ?? existingOwner?.title,
|
|
606
|
+
nextPhase,
|
|
607
|
+
branchName: existingOwner?.branchName ?? currentGitBranch(),
|
|
608
|
+
workspacePath,
|
|
609
|
+
updatedAt: new Date().toISOString(),
|
|
610
|
+
});
|
|
611
|
+
} catch {
|
|
612
|
+
// best-effort
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function runIssueResume(issueId: string) {
|
|
617
|
+
const packet = readResumePacket({ issueId });
|
|
618
|
+
console.log(`${issueId} resume packet`);
|
|
619
|
+
console.log(`phase: ${packet.phase}`);
|
|
620
|
+
console.log(`status: ${packet.status}`);
|
|
621
|
+
console.log(`title: ${packet.title}`);
|
|
622
|
+
console.log(`branch: ${packet.branch}`);
|
|
623
|
+
console.log(`saved: ${packet.writtenAt}`);
|
|
624
|
+
console.log(`checkpoint: ${packet.checkpointPath}`);
|
|
625
|
+
if (packet.parentCheckpointId) {
|
|
626
|
+
console.log(`parent: ${packet.parentCheckpointId}`);
|
|
627
|
+
}
|
|
628
|
+
if (packet.transitionReason) {
|
|
629
|
+
console.log(`reason: ${packet.transitionReason}`);
|
|
630
|
+
}
|
|
631
|
+
console.log("");
|
|
632
|
+
console.log("Summary:");
|
|
633
|
+
console.log(packet.summary);
|
|
634
|
+
console.log("");
|
|
635
|
+
console.log("Remaining Work:");
|
|
636
|
+
if (packet.remainingWork.length === 0) {
|
|
637
|
+
console.log("1. none");
|
|
638
|
+
} else {
|
|
639
|
+
packet.remainingWork.forEach((item, index) => console.log(`${index + 1}. ${item}`));
|
|
640
|
+
}
|
|
641
|
+
console.log("");
|
|
642
|
+
console.log("Notes:");
|
|
643
|
+
if (packet.notes.length === 0) {
|
|
644
|
+
console.log("- none");
|
|
645
|
+
} else {
|
|
646
|
+
packet.notes.forEach((item) => console.log(`- ${item}`));
|
|
647
|
+
}
|
|
648
|
+
console.log("");
|
|
649
|
+
console.log(`Next: gxpm issue next ${issueId}`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function runIssueContext(issueId: string, asJson: boolean) {
|
|
653
|
+
let resolvedId = issueId;
|
|
654
|
+
if (issueId === "--auto") {
|
|
655
|
+
const owner = readWorktreeOwner(process.cwd());
|
|
656
|
+
if (!owner) {
|
|
657
|
+
const msg = "No .gxpm-worktree-owner.json found in current directory. Are you in a worktree?";
|
|
658
|
+
if (asJson) {
|
|
659
|
+
console.log(JSON.stringify({ error: msg }, null, 2));
|
|
660
|
+
} else {
|
|
661
|
+
console.error(msg);
|
|
662
|
+
}
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
resolvedId = owner.ownerIssueId;
|
|
666
|
+
if (!asJson) {
|
|
667
|
+
console.log(`(auto-resolved from worktree owner: ${resolvedId})\n`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const context = buildIssueContext({ issueId: resolvedId });
|
|
671
|
+
|
|
672
|
+
if (asJson) {
|
|
673
|
+
console.log(JSON.stringify(context, null, 2));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
console.log(`issueId: ${context.issueId}`);
|
|
678
|
+
console.log(`currentPhase: ${context.currentPhase}`);
|
|
679
|
+
if (context.title) {
|
|
680
|
+
console.log(`title: ${context.title}`);
|
|
681
|
+
}
|
|
682
|
+
console.log(`confidence: ${context.confidence}`);
|
|
683
|
+
console.log("");
|
|
684
|
+
console.log("Confidence reasons:");
|
|
685
|
+
for (const reason of context.confidenceReasons) {
|
|
686
|
+
console.log(`- ${reason}`);
|
|
687
|
+
}
|
|
688
|
+
if (context.resumePhase) {
|
|
689
|
+
console.log("");
|
|
690
|
+
console.log(`resumePhase: ${context.resumePhase}`);
|
|
691
|
+
console.log(`resumeWrittenAt: ${context.resumeWrittenAt ?? "n/a"}`);
|
|
692
|
+
console.log(`checkpointExists: ${context.checkpointExists}`);
|
|
693
|
+
}
|
|
694
|
+
console.log("");
|
|
695
|
+
console.log("Required reads:");
|
|
696
|
+
for (const path of context.requiredReads) {
|
|
697
|
+
console.log(`- ${path}`);
|
|
698
|
+
}
|
|
699
|
+
console.log("");
|
|
700
|
+
console.log("Agent instructions:");
|
|
701
|
+
for (const instruction of context.agentInstructions) {
|
|
702
|
+
console.log(`- ${instruction}`);
|
|
703
|
+
}
|
|
704
|
+
console.log("");
|
|
705
|
+
console.log(`Next: ${context.next}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function resolveIssueCreateId(argv: string[]) {
|
|
709
|
+
const args = argv.slice(2);
|
|
710
|
+
const hasAutoId = args.includes("--auto-id");
|
|
711
|
+
const positional: string[] = [];
|
|
712
|
+
|
|
713
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
714
|
+
const arg = args[index];
|
|
715
|
+
if (arg === "--auto-id") continue;
|
|
716
|
+
if (arg === "--type") {
|
|
717
|
+
index += 1;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (arg === "--parent") {
|
|
721
|
+
index += 1;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (arg.startsWith("--")) {
|
|
725
|
+
throw new Error(`Unknown option for gxpm issue create: ${arg}`);
|
|
726
|
+
}
|
|
727
|
+
positional.push(arg);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (hasAutoId && positional.length > 0) {
|
|
731
|
+
throw new Error(ISSUE_CREATE_USAGE);
|
|
732
|
+
}
|
|
733
|
+
if (positional.length > 1) {
|
|
734
|
+
throw new Error(ISSUE_CREATE_USAGE);
|
|
735
|
+
}
|
|
736
|
+
if (positional[0]) return positional[0];
|
|
737
|
+
if (hasAutoId) return getNextAvailableIssueId();
|
|
738
|
+
throw new Error(ISSUE_CREATE_USAGE);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function parseParentOption(argv: string[]): string | undefined {
|
|
742
|
+
if (!argv.includes("--parent")) return undefined;
|
|
743
|
+
return optionRequiredValue(argv, "--parent");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function addIssueRelation(input: { childId: string; parentId: string }) {
|
|
747
|
+
const now = new Date().toISOString();
|
|
748
|
+
// Update child: add parent relation
|
|
749
|
+
const childState = readIssueState({ issueId: input.childId });
|
|
750
|
+
const childRelations: Array<{ relation: string; issueId: string; createdAt: string }> = [
|
|
751
|
+
...(childState.relations ?? []),
|
|
752
|
+
{ relation: "parent", issueId: input.parentId, createdAt: now },
|
|
753
|
+
];
|
|
754
|
+
const childPaths = getIssuePaths(process.cwd(), input.childId);
|
|
755
|
+
const childRaw = JSON.parse(readFileSync(childPaths.statePath, "utf8")) as Record<string, unknown>;
|
|
756
|
+
childRaw.relations = childRelations;
|
|
757
|
+
writeFileSync(childPaths.statePath, `${JSON.stringify(childRaw, null, 2)}\n`);
|
|
758
|
+
|
|
759
|
+
// Update parent: add child relation
|
|
760
|
+
const parentState = readIssueState({ issueId: input.parentId });
|
|
761
|
+
const parentRelations: Array<{ relation: string; issueId: string; createdAt: string }> = [
|
|
762
|
+
...(parentState.relations ?? []),
|
|
763
|
+
{ relation: "child", issueId: input.childId, createdAt: now },
|
|
764
|
+
];
|
|
765
|
+
const parentPaths = getIssuePaths(process.cwd(), input.parentId);
|
|
766
|
+
const parentRaw = JSON.parse(readFileSync(parentPaths.statePath, "utf8")) as Record<string, unknown>;
|
|
767
|
+
parentRaw.relations = parentRelations;
|
|
768
|
+
writeFileSync(parentPaths.statePath, `${JSON.stringify(parentRaw, null, 2)}\n`);
|
|
769
|
+
|
|
770
|
+
return { ...childState, relations: childRelations.map((r) => ({ ...r, relation: r.relation as "parent" | "child" | "related", createdAt: r.createdAt })) };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function runIssueBatch(issueId: string) {
|
|
774
|
+
const state = readIssueState({ issueId });
|
|
775
|
+
const relations = state.relations ?? [];
|
|
776
|
+
const batchIssues = [issueId, ...relations.map((r) => r.issueId)];
|
|
777
|
+
|
|
778
|
+
console.log(`batch for ${issueId}`);
|
|
779
|
+
console.log("─".repeat(60));
|
|
780
|
+
console.log(`${issueId} ${state.currentPhase} (self)`);
|
|
781
|
+
|
|
782
|
+
for (const rel of relations) {
|
|
783
|
+
try {
|
|
784
|
+
const relState = readIssueState({ issueId: rel.issueId });
|
|
785
|
+
const marker = rel.relation === "parent" ? "↑ parent" : rel.relation === "child" ? "↓ child" : "→ related";
|
|
786
|
+
console.log(`${rel.issueId} ${relState.currentPhase} (${marker})`);
|
|
787
|
+
} catch {
|
|
788
|
+
console.log(`${rel.issueId} (unknown) (${rel.relation})`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (relations.length === 0) {
|
|
793
|
+
console.log("(no related issues)");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function parseIssueTypeOption(argv: string[], fallback: IssueType): IssueType {
|
|
798
|
+
if (!argv.includes("--type")) return fallback;
|
|
799
|
+
return parseIssueType(optionRequiredValue(argv, "--type"));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function parseIssueTypesOption(argv: string[]): IssueType[] | undefined {
|
|
803
|
+
if (!argv.includes("--type")) return undefined;
|
|
804
|
+
const raw = optionRequiredValue(argv, "--type");
|
|
805
|
+
const values = raw
|
|
806
|
+
.split(",")
|
|
807
|
+
.map((item) => item.trim())
|
|
808
|
+
.filter(Boolean);
|
|
809
|
+
if (values.length === 0) {
|
|
810
|
+
throw new Error(`--type requires one or more of: ${ISSUE_TYPE_LIST}`);
|
|
811
|
+
}
|
|
812
|
+
return values.map(parseIssueType);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function parseIssueType(value: string): IssueType {
|
|
816
|
+
if (!isIssueType(value)) {
|
|
817
|
+
throw new Error(`Invalid issue type: ${value}; expected ${ISSUE_TYPE_LIST}`);
|
|
818
|
+
}
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function formatList(values: readonly string[]) {
|
|
823
|
+
if (values.length <= 1) return values.join("");
|
|
824
|
+
return `${values.slice(0, -1).join(", ")}, or ${values.at(-1)}`;
|
|
825
|
+
}
|