@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,313 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
appendIssueEvent,
|
|
5
|
+
buildOwnershipChangedEvent,
|
|
6
|
+
getIssuePaths,
|
|
7
|
+
readIssueState,
|
|
8
|
+
touchIssueOwnership,
|
|
9
|
+
type StateEvent,
|
|
10
|
+
} from "./state";
|
|
11
|
+
import { resolveSessionId } from "./session";
|
|
12
|
+
import { getWorkflowEventEmitter } from "./workflow-event-emitter";
|
|
13
|
+
|
|
14
|
+
export const ARTIFACT_TYPES = [
|
|
15
|
+
"issue-intake",
|
|
16
|
+
"triage-report",
|
|
17
|
+
"autopilot-grant",
|
|
18
|
+
"acceptance-contract",
|
|
19
|
+
"implementation-plan",
|
|
20
|
+
"dispatch-handoff",
|
|
21
|
+
"behavior-spec",
|
|
22
|
+
"wiki-context",
|
|
23
|
+
"local-verify",
|
|
24
|
+
"acceptance-check",
|
|
25
|
+
"self-review",
|
|
26
|
+
"review-report",
|
|
27
|
+
"cleanup-report",
|
|
28
|
+
"ship-readiness",
|
|
29
|
+
"ship-audit-report",
|
|
30
|
+
"pr-check",
|
|
31
|
+
"verify-findings",
|
|
32
|
+
"qa-findings",
|
|
33
|
+
"land-findings",
|
|
34
|
+
"feedback-description",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export type ArtifactType = (typeof ARTIFACT_TYPES)[number];
|
|
38
|
+
|
|
39
|
+
export interface ArtifactRecord {
|
|
40
|
+
schemaVersion: 1;
|
|
41
|
+
type: ArtifactType;
|
|
42
|
+
path: string;
|
|
43
|
+
writtenAt: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StoredArtifact {
|
|
47
|
+
schemaVersion: 1;
|
|
48
|
+
issueId: string;
|
|
49
|
+
type: ArtifactType;
|
|
50
|
+
writtenAt: string;
|
|
51
|
+
payload: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ArtifactInput {
|
|
55
|
+
root?: string;
|
|
56
|
+
issueId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface WriteArtifactInput extends ArtifactInput {
|
|
60
|
+
type: ArtifactType | string;
|
|
61
|
+
payload: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface RewriteArtifactInput extends WriteArtifactInput {
|
|
65
|
+
timestamp?: string;
|
|
66
|
+
event?: StateEvent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ReadArtifactInput extends ArtifactInput {
|
|
70
|
+
type: ArtifactType | string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getGitDiffFiles(worktreeRoot: string): string[] {
|
|
74
|
+
try {
|
|
75
|
+
const result = Bun.spawnSync({
|
|
76
|
+
cmd: ["git", "diff", "--name-only", "HEAD"],
|
|
77
|
+
cwd: worktreeRoot,
|
|
78
|
+
stdout: "pipe",
|
|
79
|
+
stderr: "pipe",
|
|
80
|
+
});
|
|
81
|
+
if (result.exitCode !== 0) return [];
|
|
82
|
+
return result.stdout
|
|
83
|
+
.toString()
|
|
84
|
+
.split("\n")
|
|
85
|
+
.map((line) => line.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateLocalVerifyPayload(
|
|
93
|
+
payload: unknown,
|
|
94
|
+
worktreeRoot: string,
|
|
95
|
+
now: string,
|
|
96
|
+
): unknown {
|
|
97
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
98
|
+
const p = payload as Record<string, unknown>;
|
|
99
|
+
const changedFiles = Array.isArray(p.changedFiles) ? (p.changedFiles as string[]) : [];
|
|
100
|
+
if (changedFiles.length === 0) return payload;
|
|
101
|
+
|
|
102
|
+
const gitDiffFiles = getGitDiffFiles(worktreeRoot);
|
|
103
|
+
if (gitDiffFiles.length === 0) return payload;
|
|
104
|
+
|
|
105
|
+
const gitSet = new Set(gitDiffFiles);
|
|
106
|
+
const extraneous = changedFiles.filter((f) => !gitSet.has(f));
|
|
107
|
+
if (extraneous.length === 0) return payload;
|
|
108
|
+
|
|
109
|
+
const log = Array.isArray(p.verificationLog) ? [...p.verificationLog] : [];
|
|
110
|
+
log.push(`[${now}] SCOPE_DRIFT_WARNING: changedFiles contains ${extraneous.length} file(s) not in git diff: ${extraneous.join(", ")}`);
|
|
111
|
+
return { ...p, verificationLog: log };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function writeArtifact(input: WriteArtifactInput): ArtifactRecord {
|
|
115
|
+
const root = input.root ?? process.cwd();
|
|
116
|
+
const type = assertValidArtifactType(input.type);
|
|
117
|
+
const paths = getIssuePaths(root, input.issueId);
|
|
118
|
+
const state = readIssueState({ root, issueId: input.issueId });
|
|
119
|
+
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
const sessionId = resolveSessionId();
|
|
122
|
+
const relativePath = `artifacts/${type}.json`;
|
|
123
|
+
|
|
124
|
+
let payload = input.payload;
|
|
125
|
+
if (type === "local-verify") {
|
|
126
|
+
const worktreeRoot = resolve(root, ".gxpm", "worktrees", `gxpm-${input.issueId}`);
|
|
127
|
+
payload = validateLocalVerifyPayload(payload, worktreeRoot, now);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const artifact: StoredArtifact = {
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
issueId: input.issueId,
|
|
133
|
+
type,
|
|
134
|
+
writtenAt: now,
|
|
135
|
+
payload,
|
|
136
|
+
};
|
|
137
|
+
writeFileSync(join(paths.issueDir, relativePath), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
138
|
+
|
|
139
|
+
const record: ArtifactRecord = {
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
type,
|
|
142
|
+
path: relativePath,
|
|
143
|
+
writtenAt: now,
|
|
144
|
+
};
|
|
145
|
+
writeArtifactIndex(
|
|
146
|
+
paths.artifactIndexPath,
|
|
147
|
+
input.issueId,
|
|
148
|
+
upsertRecord(listArtifacts({ root, issueId: input.issueId }), record),
|
|
149
|
+
);
|
|
150
|
+
const nextState = touchIssueOwnership({
|
|
151
|
+
state: { ...state, updatedAt: now },
|
|
152
|
+
sessionId,
|
|
153
|
+
});
|
|
154
|
+
if (JSON.stringify(nextState) !== JSON.stringify(state)) {
|
|
155
|
+
writeFileSync(paths.statePath, `${JSON.stringify(nextState, null, 2)}\n`);
|
|
156
|
+
}
|
|
157
|
+
const ownershipEvent = buildOwnershipChangedEvent({
|
|
158
|
+
issueId: input.issueId,
|
|
159
|
+
timestamp: now,
|
|
160
|
+
previousState: state,
|
|
161
|
+
nextState,
|
|
162
|
+
sessionId,
|
|
163
|
+
});
|
|
164
|
+
if (ownershipEvent) {
|
|
165
|
+
appendIssueEvent({ issueDir: paths.issueDir, event: ownershipEvent });
|
|
166
|
+
}
|
|
167
|
+
appendIssueEvent({
|
|
168
|
+
issueDir: paths.issueDir,
|
|
169
|
+
event: artifactWrittenEvent(input.issueId, type, now, relativePath, sessionId),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
getWorkflowEventEmitter().emit({
|
|
173
|
+
type: "artifact_written",
|
|
174
|
+
issueId: input.issueId,
|
|
175
|
+
artifactType: type,
|
|
176
|
+
timestamp: now,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Fire-and-forget sync to external issue tracker
|
|
180
|
+
import("./issue-sync")
|
|
181
|
+
.then(({ maybeSyncIssue }) =>
|
|
182
|
+
maybeSyncIssue({
|
|
183
|
+
root,
|
|
184
|
+
issueId: input.issueId,
|
|
185
|
+
action: "artifact-written",
|
|
186
|
+
meta: { artifactType: type },
|
|
187
|
+
}),
|
|
188
|
+
)
|
|
189
|
+
.catch(() => {
|
|
190
|
+
// Silently fail — local state is truth
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return record;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function rewriteArtifact(input: RewriteArtifactInput): ArtifactRecord {
|
|
197
|
+
const root = input.root ?? process.cwd();
|
|
198
|
+
const type = assertValidArtifactType(input.type);
|
|
199
|
+
const paths = getIssuePaths(root, input.issueId);
|
|
200
|
+
readIssueState({ root, issueId: input.issueId });
|
|
201
|
+
|
|
202
|
+
const artifactPath = join(paths.issueDir, "artifacts", `${type}.json`);
|
|
203
|
+
if (!existsSync(artifactPath)) {
|
|
204
|
+
throw new Error(`Artifact not found: ${type}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const now = input.timestamp ?? new Date().toISOString();
|
|
208
|
+
const relativePath = `artifacts/${type}.json`;
|
|
209
|
+
const artifact: StoredArtifact = {
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
issueId: input.issueId,
|
|
212
|
+
type,
|
|
213
|
+
writtenAt: now,
|
|
214
|
+
payload: input.payload,
|
|
215
|
+
};
|
|
216
|
+
writeFileSync(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
217
|
+
|
|
218
|
+
const record: ArtifactRecord = {
|
|
219
|
+
schemaVersion: 1,
|
|
220
|
+
type,
|
|
221
|
+
path: relativePath,
|
|
222
|
+
writtenAt: now,
|
|
223
|
+
};
|
|
224
|
+
writeArtifactIndex(
|
|
225
|
+
paths.artifactIndexPath,
|
|
226
|
+
input.issueId,
|
|
227
|
+
upsertRecord(listArtifacts({ root, issueId: input.issueId }), record),
|
|
228
|
+
);
|
|
229
|
+
if (input.event) {
|
|
230
|
+
appendIssueEvent({ issueDir: paths.issueDir, event: input.event });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return record;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function readArtifact(input: ReadArtifactInput): StoredArtifact {
|
|
237
|
+
const root = input.root ?? process.cwd();
|
|
238
|
+
const type = assertValidArtifactType(input.type);
|
|
239
|
+
const paths = getIssuePaths(root, input.issueId);
|
|
240
|
+
const artifactPath = join(paths.issueDir, "artifacts", `${type}.json`);
|
|
241
|
+
|
|
242
|
+
if (!existsSync(artifactPath)) {
|
|
243
|
+
throw new Error(`Artifact not found: ${type}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return JSON.parse(readFileSync(artifactPath, "utf8")) as StoredArtifact;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function listArtifacts(input: ArtifactInput): ArtifactRecord[] {
|
|
250
|
+
const root = input.root ?? process.cwd();
|
|
251
|
+
const paths = getIssuePaths(root, input.issueId);
|
|
252
|
+
|
|
253
|
+
if (!existsSync(paths.artifactIndexPath)) {
|
|
254
|
+
throw new Error(`Artifact index not found: ${input.issueId}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const index = JSON.parse(readFileSync(paths.artifactIndexPath, "utf8")) as {
|
|
258
|
+
artifacts?: ArtifactRecord[];
|
|
259
|
+
};
|
|
260
|
+
return Array.isArray(index.artifacts) ? index.artifacts : [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function hasArtifact(input: ReadArtifactInput): boolean {
|
|
264
|
+
const root = input.root ?? process.cwd();
|
|
265
|
+
const type = assertValidArtifactType(input.type);
|
|
266
|
+
const paths = getIssuePaths(root, input.issueId);
|
|
267
|
+
return existsSync(join(paths.issueDir, "artifacts", `${type}.json`));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function writeArtifactIndex(path: string, issueId: string, artifacts: ArtifactRecord[]) {
|
|
271
|
+
writeFileSync(
|
|
272
|
+
path,
|
|
273
|
+
`${JSON.stringify(
|
|
274
|
+
{
|
|
275
|
+
schemaVersion: 1,
|
|
276
|
+
issueId,
|
|
277
|
+
artifacts,
|
|
278
|
+
},
|
|
279
|
+
null,
|
|
280
|
+
2,
|
|
281
|
+
)}\n`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function upsertRecord(records: ArtifactRecord[], record: ArtifactRecord) {
|
|
286
|
+
return [...records.filter((item) => item.type !== record.type), record].sort((a, b) =>
|
|
287
|
+
a.type.localeCompare(b.type),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function artifactWrittenEvent(
|
|
292
|
+
issueId: string,
|
|
293
|
+
artifactType: ArtifactType,
|
|
294
|
+
timestamp: string,
|
|
295
|
+
path: string,
|
|
296
|
+
sessionId: string,
|
|
297
|
+
): StateEvent {
|
|
298
|
+
return {
|
|
299
|
+
schemaVersion: 1,
|
|
300
|
+
type: "artifact.written",
|
|
301
|
+
issueId,
|
|
302
|
+
timestamp,
|
|
303
|
+
sessionId,
|
|
304
|
+
payload: { artifactType, path },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function assertValidArtifactType(value: string): ArtifactType {
|
|
309
|
+
if (!ARTIFACT_TYPES.includes(value as ArtifactType)) {
|
|
310
|
+
throw new Error(`Invalid artifact type: ${value}`);
|
|
311
|
+
}
|
|
312
|
+
return value as ArtifactType;
|
|
313
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readArtifact, writeArtifact } from "./artifacts";
|
|
3
|
+
import { listIssues } from "./issues";
|
|
4
|
+
import { readIssueState, type GxpmPhase } from "./state";
|
|
5
|
+
import { resolveSessionId } from "./session";
|
|
6
|
+
|
|
7
|
+
export const AUTOPILOT_PROFILES = ["full-delivery"] as const;
|
|
8
|
+
|
|
9
|
+
export type AutopilotProfile = (typeof AUTOPILOT_PROFILES)[number];
|
|
10
|
+
export type AutopilotGrantStatus = "active" | "stopped" | "completed" | "blocked";
|
|
11
|
+
|
|
12
|
+
export interface AutopilotGrant {
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
mode: "autopilot";
|
|
15
|
+
issueId: string;
|
|
16
|
+
runId: string;
|
|
17
|
+
profile: AutopilotProfile;
|
|
18
|
+
status: AutopilotGrantStatus;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
startedBySession: string;
|
|
22
|
+
prompt?: string;
|
|
23
|
+
expiresAt?: string;
|
|
24
|
+
stoppedAt?: string;
|
|
25
|
+
stopReason?: string;
|
|
26
|
+
allowedActions: string[];
|
|
27
|
+
hardStops: string[];
|
|
28
|
+
terminalPhases: GxpmPhase[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ActiveAutopilotGrant {
|
|
32
|
+
issueId: string;
|
|
33
|
+
currentPhase: GxpmPhase;
|
|
34
|
+
grant: AutopilotGrant;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface StartAutopilotGrantInput {
|
|
38
|
+
root?: string;
|
|
39
|
+
issueId: string;
|
|
40
|
+
profile?: AutopilotProfile;
|
|
41
|
+
prompt?: string;
|
|
42
|
+
ttlMinutes?: number;
|
|
43
|
+
now?: Date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface StopAutopilotGrantInput {
|
|
47
|
+
root?: string;
|
|
48
|
+
issueId: string;
|
|
49
|
+
reason?: string;
|
|
50
|
+
now?: Date;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_PROFILE: AutopilotProfile = "full-delivery";
|
|
54
|
+
|
|
55
|
+
const FULL_DELIVERY_ALLOWED_ACTIONS = [
|
|
56
|
+
"issue.triage",
|
|
57
|
+
"issue.plan",
|
|
58
|
+
"issue.dispatch",
|
|
59
|
+
"workspace.ensure",
|
|
60
|
+
"code.modify",
|
|
61
|
+
"artifact.write",
|
|
62
|
+
"verification.run",
|
|
63
|
+
"review.self",
|
|
64
|
+
"git.commit",
|
|
65
|
+
"git.push",
|
|
66
|
+
"pull-request.create",
|
|
67
|
+
"pull-request.update",
|
|
68
|
+
"pull-request.merge",
|
|
69
|
+
"issue.land",
|
|
70
|
+
"cleanup.land",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const DEFAULT_HARD_STOPS = [
|
|
74
|
+
"user.explicit_stop",
|
|
75
|
+
"secrets_or_credentials_required",
|
|
76
|
+
"paid_external_api_required",
|
|
77
|
+
"production_data_or_destructive_data_migration",
|
|
78
|
+
"irrecoverable_conflict_or_failed_verification",
|
|
79
|
+
"policy_or_permission_boundary",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const TERMINAL_PHASES: GxpmPhase[] = ["land"];
|
|
83
|
+
|
|
84
|
+
export function startAutopilotGrant(input: StartAutopilotGrantInput): AutopilotGrant {
|
|
85
|
+
const root = input.root ?? process.cwd();
|
|
86
|
+
const profile = input.profile ?? DEFAULT_PROFILE;
|
|
87
|
+
assertAutopilotProfile(profile);
|
|
88
|
+
readIssueState({ root, issueId: input.issueId });
|
|
89
|
+
|
|
90
|
+
const existing = readAutopilotGrant({ root, issueId: input.issueId });
|
|
91
|
+
if (existing && isAutopilotGrantActive(existing, input.now)) {
|
|
92
|
+
throw new Error(`Autopilot grant already active for ${input.issueId}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const now = input.now ?? new Date();
|
|
96
|
+
const nowIso = now.toISOString();
|
|
97
|
+
const grant: AutopilotGrant = {
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
mode: "autopilot",
|
|
100
|
+
issueId: input.issueId,
|
|
101
|
+
runId: randomUUID(),
|
|
102
|
+
profile,
|
|
103
|
+
status: "active",
|
|
104
|
+
startedAt: nowIso,
|
|
105
|
+
updatedAt: nowIso,
|
|
106
|
+
startedBySession: resolveSessionId(),
|
|
107
|
+
...(input.prompt ? { prompt: input.prompt } : {}),
|
|
108
|
+
...(input.ttlMinutes ? { expiresAt: new Date(now.getTime() + input.ttlMinutes * 60_000).toISOString() } : {}),
|
|
109
|
+
allowedActions: [...FULL_DELIVERY_ALLOWED_ACTIONS],
|
|
110
|
+
hardStops: [...DEFAULT_HARD_STOPS],
|
|
111
|
+
terminalPhases: [...TERMINAL_PHASES],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
writeArtifact({ root, issueId: input.issueId, type: "autopilot-grant", payload: grant });
|
|
115
|
+
return grant;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function stopAutopilotGrant(input: StopAutopilotGrantInput): AutopilotGrant {
|
|
119
|
+
const root = input.root ?? process.cwd();
|
|
120
|
+
readIssueState({ root, issueId: input.issueId });
|
|
121
|
+
const existing = readAutopilotGrant({ root, issueId: input.issueId });
|
|
122
|
+
if (!existing) {
|
|
123
|
+
throw new Error(`Autopilot grant not found for ${input.issueId}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const now = input.now ?? new Date();
|
|
127
|
+
const stopped: AutopilotGrant = {
|
|
128
|
+
...existing,
|
|
129
|
+
status: "stopped",
|
|
130
|
+
updatedAt: now.toISOString(),
|
|
131
|
+
stoppedAt: now.toISOString(),
|
|
132
|
+
stopReason: input.reason ?? "manual_stop",
|
|
133
|
+
};
|
|
134
|
+
writeArtifact({ root, issueId: input.issueId, type: "autopilot-grant", payload: stopped });
|
|
135
|
+
return stopped;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readAutopilotGrant(input: { root?: string; issueId: string }): AutopilotGrant | null {
|
|
139
|
+
const root = input.root ?? process.cwd();
|
|
140
|
+
try {
|
|
141
|
+
const artifact = readArtifact({ root, issueId: input.issueId, type: "autopilot-grant" });
|
|
142
|
+
return normalizeAutopilotGrant(artifact.payload);
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function listActiveAutopilotGrants(input: {
|
|
149
|
+
root?: string;
|
|
150
|
+
issueId?: string;
|
|
151
|
+
limit?: number;
|
|
152
|
+
now?: Date;
|
|
153
|
+
} = {}): ActiveAutopilotGrant[] {
|
|
154
|
+
const root = input.root ?? process.cwd();
|
|
155
|
+
const issueIds = input.issueId
|
|
156
|
+
? [input.issueId]
|
|
157
|
+
: listIssues({ root, includeAll: true }).map((entry) => entry.issueId);
|
|
158
|
+
const active: ActiveAutopilotGrant[] = [];
|
|
159
|
+
|
|
160
|
+
for (const issueId of issueIds) {
|
|
161
|
+
const grant = readAutopilotGrant({ root, issueId });
|
|
162
|
+
if (!grant || !isAutopilotGrantActive(grant, input.now)) continue;
|
|
163
|
+
let state;
|
|
164
|
+
try {
|
|
165
|
+
state = readIssueState({ root, issueId });
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (grant.terminalPhases.includes(state.currentPhase)) continue;
|
|
170
|
+
active.push({ issueId, currentPhase: state.currentPhase, grant });
|
|
171
|
+
if (input.limit && active.length >= input.limit) break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return active;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function isAutopilotGrantActive(grant: AutopilotGrant, now: Date = new Date()): boolean {
|
|
178
|
+
if (grant.status !== "active") return false;
|
|
179
|
+
if (!grant.expiresAt) return true;
|
|
180
|
+
return Date.parse(grant.expiresAt) > now.getTime();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isAutopilotProfile(value: string): value is AutopilotProfile {
|
|
184
|
+
return AUTOPILOT_PROFILES.includes(value as AutopilotProfile);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function assertAutopilotProfile(value: string): asserts value is AutopilotProfile {
|
|
188
|
+
if (!isAutopilotProfile(value)) {
|
|
189
|
+
throw new Error(`Unsupported autopilot profile: ${value}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function formatAutopilotGrantContext(active: ActiveAutopilotGrant[]): string {
|
|
194
|
+
if (active.length === 0) return "";
|
|
195
|
+
const lines = ["gxpm autopilot grant active:"];
|
|
196
|
+
for (const item of active) {
|
|
197
|
+
lines.push(
|
|
198
|
+
`- ${item.issueId}: profile=${item.grant.profile}, phase=${item.currentPhase}, runId=${item.grant.runId}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push("Autopilot Grant means the user pre-authorized gxpm workflow steps inside the profile.");
|
|
203
|
+
lines.push("Do not ask for confirmation or clarification for allowed actions; plan, implement, verify, PR, merge/land, and cleanup autonomously.");
|
|
204
|
+
lines.push("Stop only on hard stops, and persist the blocker/evidence in the current gxpm artifact before reporting.");
|
|
205
|
+
lines.push("Useful commands: gxpm autopilot status <issue-id>; gxpm issue next <issue-id>.");
|
|
206
|
+
return lines.join("\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildAutopilotStopContinuation(active: ActiveAutopilotGrant[]): string {
|
|
210
|
+
const context = formatAutopilotGrantContext(active);
|
|
211
|
+
if (!context) return "";
|
|
212
|
+
return `${context}\n\nContinue the gxpm workflow now. Do not produce a final answer while an active grant still has non-terminal work.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeAutopilotGrant(payload: unknown): AutopilotGrant | null {
|
|
216
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
217
|
+
const value = payload as Partial<AutopilotGrant>;
|
|
218
|
+
if (value.schemaVersion !== 1) return null;
|
|
219
|
+
if (value.mode !== "autopilot") return null;
|
|
220
|
+
if (typeof value.issueId !== "string") return null;
|
|
221
|
+
if (typeof value.runId !== "string") return null;
|
|
222
|
+
if (typeof value.profile !== "string" || !isAutopilotProfile(value.profile)) return null;
|
|
223
|
+
if (!["active", "stopped", "completed", "blocked"].includes(value.status ?? "")) return null;
|
|
224
|
+
if (!Array.isArray(value.allowedActions)) return null;
|
|
225
|
+
if (!Array.isArray(value.hardStops)) return null;
|
|
226
|
+
return {
|
|
227
|
+
schemaVersion: 1,
|
|
228
|
+
mode: "autopilot",
|
|
229
|
+
issueId: value.issueId,
|
|
230
|
+
runId: value.runId,
|
|
231
|
+
profile: value.profile,
|
|
232
|
+
status: value.status as AutopilotGrantStatus,
|
|
233
|
+
startedAt: stringOrNow(value.startedAt),
|
|
234
|
+
updatedAt: stringOrNow(value.updatedAt),
|
|
235
|
+
startedBySession: typeof value.startedBySession === "string" ? value.startedBySession : "",
|
|
236
|
+
...(typeof value.prompt === "string" ? { prompt: value.prompt } : {}),
|
|
237
|
+
...(typeof value.expiresAt === "string" ? { expiresAt: value.expiresAt } : {}),
|
|
238
|
+
...(typeof value.stoppedAt === "string" ? { stoppedAt: value.stoppedAt } : {}),
|
|
239
|
+
...(typeof value.stopReason === "string" ? { stopReason: value.stopReason } : {}),
|
|
240
|
+
allowedActions: value.allowedActions.filter((item): item is string => typeof item === "string"),
|
|
241
|
+
hardStops: value.hardStops.filter((item): item is string => typeof item === "string"),
|
|
242
|
+
terminalPhases: Array.isArray(value.terminalPhases)
|
|
243
|
+
? value.terminalPhases.filter((item): item is GxpmPhase => item === "land")
|
|
244
|
+
: [...TERMINAL_PHASES],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function stringOrNow(value: unknown): string {
|
|
249
|
+
return typeof value === "string" ? value : new Date().toISOString();
|
|
250
|
+
}
|