@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG Execution Schemas
|
|
3
|
+
*
|
|
4
|
+
* Types and validation for the gxpm DAG executor.
|
|
5
|
+
* Inspired by Archon's dag-executor.ts and loader.ts,
|
|
6
|
+
* adapted for gxpm's phase-runtime and capability model.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TRIGGER_RULES = [
|
|
10
|
+
"all_success",
|
|
11
|
+
"one_success",
|
|
12
|
+
"none_failed_min_one_success",
|
|
13
|
+
"all_done",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type TriggerRule = (typeof TRIGGER_RULES)[number];
|
|
17
|
+
|
|
18
|
+
export const NODE_STATES = [
|
|
19
|
+
"pending",
|
|
20
|
+
"running",
|
|
21
|
+
"completed",
|
|
22
|
+
"skipped",
|
|
23
|
+
"failed",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type NodeState = (typeof NODE_STATES)[number];
|
|
27
|
+
|
|
28
|
+
/** Base fields every DAG node shares */
|
|
29
|
+
export interface DagNodeBase {
|
|
30
|
+
id: string;
|
|
31
|
+
depends_on?: string[];
|
|
32
|
+
trigger_rule?: TriggerRule;
|
|
33
|
+
when?: string;
|
|
34
|
+
retry?: {
|
|
35
|
+
max_attempts: number;
|
|
36
|
+
delay_ms?: number;
|
|
37
|
+
on_error?: "transient" | "all";
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A node that runs an agent prompt */
|
|
42
|
+
export interface PromptNode extends DagNodeBase {
|
|
43
|
+
type: "prompt";
|
|
44
|
+
prompt: string;
|
|
45
|
+
provider?: string;
|
|
46
|
+
model?: string;
|
|
47
|
+
systemPrompt?: string;
|
|
48
|
+
output_format?: Record<string, unknown>;
|
|
49
|
+
maxBudgetUsd?: number;
|
|
50
|
+
allowed_tools?: string[];
|
|
51
|
+
denied_tools?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A node that runs a shell/bash command */
|
|
55
|
+
export interface BashNode extends DagNodeBase {
|
|
56
|
+
type: "bash";
|
|
57
|
+
bash: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A node that runs a local script file */
|
|
61
|
+
export interface ScriptNode extends DagNodeBase {
|
|
62
|
+
type: "script";
|
|
63
|
+
script: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A node that represents a gxpm phase gate */
|
|
67
|
+
export interface PhaseNode extends DagNodeBase {
|
|
68
|
+
type: "phase";
|
|
69
|
+
phase: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A no-op node used for gating or grouping */
|
|
73
|
+
export interface ApprovalNode extends DagNodeBase {
|
|
74
|
+
type: "approval";
|
|
75
|
+
message?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A node that cancels the workflow on failure */
|
|
79
|
+
export interface CancelNode extends DagNodeBase {
|
|
80
|
+
type: "cancel";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type DagNode =
|
|
84
|
+
| PromptNode
|
|
85
|
+
| BashNode
|
|
86
|
+
| ScriptNode
|
|
87
|
+
| PhaseNode
|
|
88
|
+
| ApprovalNode
|
|
89
|
+
| CancelNode;
|
|
90
|
+
|
|
91
|
+
export function isPromptNode(node: DagNode): node is PromptNode {
|
|
92
|
+
return node.type === "prompt";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isBashNode(node: DagNode): node is BashNode {
|
|
96
|
+
return node.type === "bash";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isScriptNode(node: DagNode): node is ScriptNode {
|
|
100
|
+
return node.type === "script";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isPhaseNode(node: DagNode): node is PhaseNode {
|
|
104
|
+
return node.type === "phase";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isApprovalNode(node: DagNode): node is ApprovalNode {
|
|
108
|
+
return node.type === "approval";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isCancelNode(node: DagNode): node is CancelNode {
|
|
112
|
+
return node.type === "cancel";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Result of executing a single node */
|
|
116
|
+
export interface NodeOutput {
|
|
117
|
+
state: NodeState;
|
|
118
|
+
output: string;
|
|
119
|
+
error?: string;
|
|
120
|
+
durationMs?: number;
|
|
121
|
+
retries?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Overall DAG execution result */
|
|
125
|
+
export interface DagExecutionResult {
|
|
126
|
+
success: boolean;
|
|
127
|
+
nodeOutputs: Map<string, NodeOutput>;
|
|
128
|
+
durationMs: number;
|
|
129
|
+
error?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Raw workflow definition (before validation) */
|
|
133
|
+
export interface WorkflowDefinition {
|
|
134
|
+
name: string;
|
|
135
|
+
description: string;
|
|
136
|
+
provider?: string;
|
|
137
|
+
model?: string;
|
|
138
|
+
nodes: DagNode[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Validation / load error */
|
|
142
|
+
export interface WorkflowLoadError {
|
|
143
|
+
filename: string;
|
|
144
|
+
error: string;
|
|
145
|
+
errorType: "parse_error" | "validation_error" | "runtime_error";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type ParseResult =
|
|
149
|
+
| { workflow: WorkflowDefinition; error: null }
|
|
150
|
+
| { workflow: null; error: WorkflowLoadError };
|
package/core/dispatch.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readArtifact, writeArtifact } from "./artifacts";
|
|
2
|
+
import { readIssueState } from "./state";
|
|
3
|
+
import type { PhaseArtifactInput } from "./phase-artifact";
|
|
4
|
+
|
|
5
|
+
interface DispatchHandoffPayload {
|
|
6
|
+
inputArtifacts: string[];
|
|
7
|
+
status: "draft" | "finalized";
|
|
8
|
+
stopRule: string;
|
|
9
|
+
targetBranch: string;
|
|
10
|
+
validation: string[];
|
|
11
|
+
worktreePath: string;
|
|
12
|
+
worktreeDecision: "pending" | "created" | "reused" | "existing";
|
|
13
|
+
workflowType: string;
|
|
14
|
+
workflowId: string;
|
|
15
|
+
workerTasks: Array<{ id: string; description: string; status: "pending" | "in_progress" | "done" }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function generateTaskId(index: number): string {
|
|
19
|
+
return `task-${String(index + 1).padStart(3, "0")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeReadArtifactPayload(
|
|
23
|
+
root: string | undefined,
|
|
24
|
+
issueId: string,
|
|
25
|
+
type: string,
|
|
26
|
+
): Record<string, unknown> | undefined {
|
|
27
|
+
try {
|
|
28
|
+
const artifact = readArtifact({ root, issueId, type });
|
|
29
|
+
return (artifact.payload ?? {}) as Record<string, unknown>;
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildStopRule(
|
|
36
|
+
planPayload: Record<string, unknown> | undefined,
|
|
37
|
+
triagePayload: Record<string, unknown> | undefined,
|
|
38
|
+
): string {
|
|
39
|
+
const parts: string[] = [];
|
|
40
|
+
|
|
41
|
+
const planRisks = planPayload?.risks;
|
|
42
|
+
if (Array.isArray(planRisks)) {
|
|
43
|
+
for (const item of planRisks) {
|
|
44
|
+
if (typeof item === "string" && item.trim()) {
|
|
45
|
+
parts.push(item.trim());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const triageNonGoals = triagePayload?.nonGoals;
|
|
51
|
+
if (Array.isArray(triageNonGoals)) {
|
|
52
|
+
for (const item of triageNonGoals) {
|
|
53
|
+
if (typeof item === "string" && item.trim()) {
|
|
54
|
+
parts.push(item.trim());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parts.join("; ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildWorkerTasks(
|
|
63
|
+
planPayload: Record<string, unknown> | undefined,
|
|
64
|
+
): DispatchHandoffPayload["workerTasks"] {
|
|
65
|
+
const steps = planPayload?.steps;
|
|
66
|
+
if (!Array.isArray(steps)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tasks: DispatchHandoffPayload["workerTasks"] = [];
|
|
71
|
+
for (const step of steps) {
|
|
72
|
+
if (typeof step === "string" && step.trim()) {
|
|
73
|
+
tasks.push({
|
|
74
|
+
id: generateTaskId(tasks.length),
|
|
75
|
+
description: step.trim(),
|
|
76
|
+
status: "pending",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return tasks;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildValidation(
|
|
84
|
+
planPayload: Record<string, unknown> | undefined,
|
|
85
|
+
): string[] {
|
|
86
|
+
const raw = planPayload?.validation;
|
|
87
|
+
if (!Array.isArray(raw)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return raw.filter((item): item is string => typeof item === "string" && item.trim() !== "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function initializeDispatch(input: PhaseArtifactInput) {
|
|
94
|
+
const root = input.root ?? process.cwd();
|
|
95
|
+
const state = readIssueState({ root, issueId: input.issueId });
|
|
96
|
+
|
|
97
|
+
if (state.currentPhase !== "dispatch") {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Dispatch can only be initialized from dispatch phase: current phase is ${state.currentPhase}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const planPayload = safeReadArtifactPayload(root, input.issueId, "implementation-plan");
|
|
104
|
+
const triagePayload = safeReadArtifactPayload(root, input.issueId, "triage-report");
|
|
105
|
+
|
|
106
|
+
const payload: DispatchHandoffPayload = {
|
|
107
|
+
inputArtifacts: ["acceptance-contract", "implementation-plan"],
|
|
108
|
+
status: "draft",
|
|
109
|
+
stopRule: buildStopRule(planPayload, triagePayload),
|
|
110
|
+
targetBranch: "",
|
|
111
|
+
validation: buildValidation(planPayload),
|
|
112
|
+
worktreePath: "",
|
|
113
|
+
worktreeDecision: "pending",
|
|
114
|
+
workflowType: "issue",
|
|
115
|
+
workflowId: input.issueId,
|
|
116
|
+
workerTasks: buildWorkerTasks(planPayload),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return writeArtifact({
|
|
120
|
+
root,
|
|
121
|
+
issueId: input.issueId,
|
|
122
|
+
type: "dispatch-handoff",
|
|
123
|
+
payload,
|
|
124
|
+
});
|
|
125
|
+
}
|
package/core/evidence.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { getIssuePaths, readIssueState } from "./state";
|
|
4
|
+
|
|
5
|
+
export const EVIDENCE_KINDS = [
|
|
6
|
+
"command-logs",
|
|
7
|
+
"browser-snapshots",
|
|
8
|
+
"browser-screenshots",
|
|
9
|
+
"browser-console",
|
|
10
|
+
"browser-errors",
|
|
11
|
+
"screenshots",
|
|
12
|
+
"investigations",
|
|
13
|
+
"review",
|
|
14
|
+
"release",
|
|
15
|
+
"test-runs",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
|
|
19
|
+
|
|
20
|
+
export interface IssueEvidencePath {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
issueId: string;
|
|
23
|
+
kind: EvidenceKind;
|
|
24
|
+
path: string;
|
|
25
|
+
absolutePath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IssueEvidenceRecord extends IssueEvidencePath {
|
|
29
|
+
writtenAt: string;
|
|
30
|
+
mediaType: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface EvidencePathInput {
|
|
34
|
+
root?: string;
|
|
35
|
+
issueId: string;
|
|
36
|
+
kind: EvidenceKind | string;
|
|
37
|
+
filename: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface WriteEvidenceInput extends EvidencePathInput {
|
|
41
|
+
mediaType?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface WriteJsonEvidenceInput extends WriteEvidenceInput {
|
|
45
|
+
payload: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface WriteTextEvidenceInput extends WriteEvidenceInput {
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface WriteBytesEvidenceInput extends WriteEvidenceInput {
|
|
53
|
+
bytes: Uint8Array;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SAFE_FILENAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
57
|
+
const SAFE_EXTENSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
58
|
+
|
|
59
|
+
export function evidenceFilename(prefix: string, extension: string, timestamp = new Date()) {
|
|
60
|
+
assertSafeFilenamePart(prefix, "evidence filename prefix");
|
|
61
|
+
const normalizedExtension = extension.startsWith(".") ? extension.slice(1) : extension;
|
|
62
|
+
if (!SAFE_EXTENSION_PATTERN.test(normalizedExtension)) {
|
|
63
|
+
throw new Error(`Invalid evidence filename extension: ${extension}`);
|
|
64
|
+
}
|
|
65
|
+
const stamp = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
66
|
+
return `${prefix}-${stamp}.${normalizedExtension}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function prepareIssueEvidencePath(input: EvidencePathInput): IssueEvidencePath {
|
|
70
|
+
const root = input.root ?? process.cwd();
|
|
71
|
+
const kind = assertValidEvidenceKind(input.kind);
|
|
72
|
+
assertSafeEvidenceFilename(input.filename);
|
|
73
|
+
|
|
74
|
+
const paths = getIssuePaths(root, input.issueId);
|
|
75
|
+
readIssueState({ root, issueId: input.issueId });
|
|
76
|
+
|
|
77
|
+
const relativePath = toPosixPath(join("evidence", kind, input.filename));
|
|
78
|
+
const absolutePath = join(paths.issueDir, relativePath);
|
|
79
|
+
assertInsideIssueDir(paths.issueDir, absolutePath);
|
|
80
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
issueId: input.issueId,
|
|
85
|
+
kind,
|
|
86
|
+
path: relativePath,
|
|
87
|
+
absolutePath,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function writeIssueEvidenceJson(input: WriteJsonEvidenceInput): IssueEvidenceRecord {
|
|
92
|
+
const path = prepareIssueEvidencePath(input);
|
|
93
|
+
return writeIssueEvidence(path, `${JSON.stringify(input.payload, null, 2)}\n`, input.mediaType ?? "application/json");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function writeIssueEvidenceText(input: WriteTextEvidenceInput): IssueEvidenceRecord {
|
|
97
|
+
const path = prepareIssueEvidencePath(input);
|
|
98
|
+
return writeIssueEvidence(path, input.text, input.mediaType ?? "text/plain");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function writeIssueEvidenceBytes(input: WriteBytesEvidenceInput): IssueEvidenceRecord {
|
|
102
|
+
const path = prepareIssueEvidencePath(input);
|
|
103
|
+
return writeIssueEvidence(path, input.bytes, input.mediaType ?? "application/octet-stream");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeIssueEvidence(
|
|
107
|
+
path: IssueEvidencePath,
|
|
108
|
+
content: string | Uint8Array,
|
|
109
|
+
mediaType: string,
|
|
110
|
+
): IssueEvidenceRecord {
|
|
111
|
+
writeFileSync(path.absolutePath, content);
|
|
112
|
+
return {
|
|
113
|
+
...path,
|
|
114
|
+
writtenAt: new Date().toISOString(),
|
|
115
|
+
mediaType,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function assertValidEvidenceKind(value: string): EvidenceKind {
|
|
120
|
+
if (!EVIDENCE_KINDS.includes(value as EvidenceKind)) {
|
|
121
|
+
throw new Error(`Invalid evidence kind: ${value}`);
|
|
122
|
+
}
|
|
123
|
+
return value as EvidenceKind;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertSafeEvidenceFilename(filename: string) {
|
|
127
|
+
if (!SAFE_FILENAME_PATTERN.test(filename)) {
|
|
128
|
+
throw new Error(`Invalid evidence filename: ${filename}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function assertSafeFilenamePart(value: string, label: string) {
|
|
133
|
+
if (!SAFE_FILENAME_PATTERN.test(value)) {
|
|
134
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assertInsideIssueDir(issueDir: string, absolutePath: string) {
|
|
139
|
+
const issueRoot = resolve(issueDir);
|
|
140
|
+
const target = resolve(absolutePath);
|
|
141
|
+
if (target !== issueRoot && !target.startsWith(`${issueRoot}${sep}`)) {
|
|
142
|
+
throw new Error(`Evidence path escapes issue directory: ${absolutePath}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toPosixPath(path: string) {
|
|
147
|
+
return path.split(sep).join("/");
|
|
148
|
+
}
|
package/core/gate.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
CODE_COMMIT_PHASES,
|
|
5
|
+
PHASE_GATE_RULES,
|
|
6
|
+
PROTECTED_PATH_PATTERNS,
|
|
7
|
+
} from "./phase-gates";
|
|
8
|
+
import { PHASE_SKIP_MAP, type GxpmPhase, type IssueState } from "./state";
|
|
9
|
+
|
|
10
|
+
export type GateType = "pre-commit" | "commit-msg" | "pre-push" | "post-merge";
|
|
11
|
+
|
|
12
|
+
export type GateCode =
|
|
13
|
+
| "no-state"
|
|
14
|
+
| "no-issue-id"
|
|
15
|
+
| "no-protected-paths"
|
|
16
|
+
| "main-worktree-non-main"
|
|
17
|
+
| "feature-branch-outside-worktree-root"
|
|
18
|
+
| "wrong-phase"
|
|
19
|
+
| "missing-artifact"
|
|
20
|
+
| "missing-issue-ref"
|
|
21
|
+
| "phase-ok"
|
|
22
|
+
| "land-pending"
|
|
23
|
+
| "already-landed"
|
|
24
|
+
| "disabled"
|
|
25
|
+
| "contamination-detected"
|
|
26
|
+
| "missing-changed-files";
|
|
27
|
+
|
|
28
|
+
export interface GateVerdict {
|
|
29
|
+
allowed: boolean;
|
|
30
|
+
code: GateCode;
|
|
31
|
+
reason: string;
|
|
32
|
+
details?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PostMergeOutcome {
|
|
36
|
+
transitionTo: GxpmPhase | null;
|
|
37
|
+
reason: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Env = Record<string, string | undefined>;
|
|
41
|
+
|
|
42
|
+
export type HasArtifactFn = (issueId: string, artifactType: string) => boolean;
|
|
43
|
+
|
|
44
|
+
export interface BranchPolicyInput {
|
|
45
|
+
currentRoot: string;
|
|
46
|
+
currentBranch?: string;
|
|
47
|
+
canonicalMainRoot: string;
|
|
48
|
+
allowedWorktreeRoot?: string;
|
|
49
|
+
baseBranch?: string;
|
|
50
|
+
env: Env;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ISSUE_REF_PATTERN = /\b(GXG|GXPM)-\d+\b/i;
|
|
54
|
+
|
|
55
|
+
function isDisabled(env: Env): boolean {
|
|
56
|
+
return env.GXPM_GATE_DISABLE === "1";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isProtected(path: string): boolean {
|
|
60
|
+
return PROTECTED_PATH_PATTERNS.some((re) => re.test(path));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizePath(path: string): string {
|
|
64
|
+
const resolved = resolve(path).replace(/\/+$/, "");
|
|
65
|
+
try {
|
|
66
|
+
return realpathSync.native(resolved);
|
|
67
|
+
} catch {
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isInsidePath(root: string, candidate: string): boolean {
|
|
73
|
+
const normalizedRoot = normalizePath(root);
|
|
74
|
+
const normalizedCandidate = normalizePath(candidate);
|
|
75
|
+
const rel = relative(normalizedRoot, normalizedCandidate);
|
|
76
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function evaluateBranchPolicy(input: BranchPolicyInput): GateVerdict {
|
|
80
|
+
if (isDisabled(input.env)) {
|
|
81
|
+
return { allowed: true, code: "disabled", reason: "GXPM_GATE_DISABLE=1" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const currentRoot = normalizePath(input.currentRoot);
|
|
85
|
+
const canonicalMainRoot = normalizePath(input.canonicalMainRoot);
|
|
86
|
+
const currentBranch = input.currentBranch ?? "HEAD";
|
|
87
|
+
const baseBranch = input.baseBranch ?? "main";
|
|
88
|
+
|
|
89
|
+
if (currentBranch === baseBranch) {
|
|
90
|
+
return { allowed: true, code: "phase-ok", reason: `current branch is ${baseBranch}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (currentRoot === canonicalMainRoot) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
code: "main-worktree-non-main",
|
|
97
|
+
reason: `canonical main checkout must stay on ${baseBranch}; currentBranch=${currentBranch}`,
|
|
98
|
+
details: { currentRoot, canonicalMainRoot, currentBranch, baseBranch },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (input.allowedWorktreeRoot && !isInsidePath(input.allowedWorktreeRoot, currentRoot)) {
|
|
103
|
+
return {
|
|
104
|
+
allowed: false,
|
|
105
|
+
code: "feature-branch-outside-worktree-root",
|
|
106
|
+
reason: `feature branches must run under worktree root: ${normalizePath(input.allowedWorktreeRoot)}`,
|
|
107
|
+
details: {
|
|
108
|
+
currentRoot,
|
|
109
|
+
canonicalMainRoot,
|
|
110
|
+
currentBranch,
|
|
111
|
+
allowedWorktreeRoot: normalizePath(input.allowedWorktreeRoot),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
allowed: true,
|
|
118
|
+
code: "phase-ok",
|
|
119
|
+
reason: `currentBranch=${currentBranch} is outside canonical main checkout`,
|
|
120
|
+
details: { currentRoot, canonicalMainRoot, currentBranch },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function evaluatePreCommit(
|
|
125
|
+
state: IssueState,
|
|
126
|
+
stagedFiles: string[],
|
|
127
|
+
env: Env,
|
|
128
|
+
): GateVerdict {
|
|
129
|
+
if (isDisabled(env)) {
|
|
130
|
+
return { allowed: true, code: "disabled", reason: "GXPM_GATE_DISABLE=1" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const protectedHits = stagedFiles.filter(isProtected);
|
|
134
|
+
if (protectedHits.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
allowed: true,
|
|
137
|
+
code: "no-protected-paths",
|
|
138
|
+
reason: "no staged files in protected paths",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (CODE_COMMIT_PHASES.has(state.currentPhase)) {
|
|
143
|
+
return {
|
|
144
|
+
allowed: true,
|
|
145
|
+
code: "phase-ok",
|
|
146
|
+
reason: `currentPhase=${state.currentPhase} permits code edits`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
allowed: false,
|
|
152
|
+
code: "wrong-phase",
|
|
153
|
+
reason: `currentPhase=${state.currentPhase} forbids commits to protected paths`,
|
|
154
|
+
details: { protectedHits, currentPhase: state.currentPhase },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function evaluateCommitMsg(
|
|
159
|
+
message: string,
|
|
160
|
+
_state: IssueState,
|
|
161
|
+
env: Env,
|
|
162
|
+
): GateVerdict {
|
|
163
|
+
if (isDisabled(env)) {
|
|
164
|
+
return { allowed: true, code: "disabled", reason: "GXPM_GATE_DISABLE=1" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!ISSUE_REF_PATTERN.test(message)) {
|
|
168
|
+
return {
|
|
169
|
+
allowed: false,
|
|
170
|
+
code: "missing-issue-ref",
|
|
171
|
+
reason: "commit message must reference GXG-NNN or GXPM-NNN",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { allowed: true, code: "phase-ok", reason: "issue ref present" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function evaluatePrePush(
|
|
179
|
+
state: IssueState,
|
|
180
|
+
hasArtifactFn: HasArtifactFn,
|
|
181
|
+
env: Env,
|
|
182
|
+
root?: string,
|
|
183
|
+
): GateVerdict {
|
|
184
|
+
if (isDisabled(env)) {
|
|
185
|
+
return { allowed: true, code: "disabled", reason: "GXPM_GATE_DISABLE=1" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rule = PHASE_GATE_RULES.find((r) => r.fromPhase === state.currentPhase);
|
|
189
|
+
if (!rule) {
|
|
190
|
+
return {
|
|
191
|
+
allowed: true,
|
|
192
|
+
code: "phase-ok",
|
|
193
|
+
reason: `phase=${state.currentPhase} has no outbound artifact gate`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Under compressed rigor levels, skip artifact gates for phases that are
|
|
198
|
+
// collapsed (e.g. standard mode skips local-verify / ac-check / cleanup gates).
|
|
199
|
+
const skipSet = state.rigorLevel ? PHASE_SKIP_MAP[state.rigorLevel] : new Set<GxpmPhase>();
|
|
200
|
+
if (skipSet.has(rule.nextPhase)) {
|
|
201
|
+
return {
|
|
202
|
+
allowed: true,
|
|
203
|
+
code: "phase-ok",
|
|
204
|
+
reason: `phase=${state.currentPhase} outbound gate skipped under rigor=${state.rigorLevel}`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!hasArtifactFn(state.issueId, rule.requiredArtifact)) {
|
|
209
|
+
return {
|
|
210
|
+
allowed: false,
|
|
211
|
+
code: "missing-artifact",
|
|
212
|
+
reason: `pre-push: ${rule.requiredArtifact} required for next transition`,
|
|
213
|
+
details: { requiredArtifact: rule.requiredArtifact, command: rule.command },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (root) {
|
|
218
|
+
const artifactDir = resolve(root, ".gxpm", "issues", state.issueId, "artifacts");
|
|
219
|
+
|
|
220
|
+
// Check for .contaminated files
|
|
221
|
+
if (existsSync(artifactDir)) {
|
|
222
|
+
const contaminated = readdirSync(artifactDir).filter((f) => f.startsWith(".contaminated"));
|
|
223
|
+
if (contaminated.length > 0) {
|
|
224
|
+
return {
|
|
225
|
+
allowed: false,
|
|
226
|
+
code: "contamination-detected",
|
|
227
|
+
reason: `pre-push: contamination detected (${contaminated.join(", ")}). Run gxpm phase rewind ${state.issueId} --to implement --reason "contamination" and re-verify.`,
|
|
228
|
+
details: { contaminatedFiles: contaminated },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check local-verify changedFiles is non-empty
|
|
234
|
+
if (rule.requiredArtifact === "local-verify") {
|
|
235
|
+
try {
|
|
236
|
+
const artifactPath = resolve(artifactDir, "local-verify.json");
|
|
237
|
+
const raw = JSON.parse(readFileSync(artifactPath, "utf8"));
|
|
238
|
+
const payload = raw.payload ?? raw;
|
|
239
|
+
const changedFiles = payload.changedFiles ?? [];
|
|
240
|
+
if (!Array.isArray(changedFiles) || changedFiles.length === 0) {
|
|
241
|
+
return {
|
|
242
|
+
allowed: false,
|
|
243
|
+
code: "missing-changed-files",
|
|
244
|
+
reason: `pre-push: local-verify changedFiles is empty. Record what was changed before transitioning.`,
|
|
245
|
+
details: { command: `gxpm artifact edit ${state.issueId} local-verify` },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// ignore malformed artifact — missing-artifact check already passed,
|
|
250
|
+
// so this is an edge case we let through to avoid false blocks
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { allowed: true, code: "phase-ok", reason: "required artifact present" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function evaluatePostMerge(state: IssueState): PostMergeOutcome {
|
|
259
|
+
if (state.currentPhase === "qa") {
|
|
260
|
+
return { transitionTo: "land", reason: "merged from qa → auto-transition to land" };
|
|
261
|
+
}
|
|
262
|
+
if (state.currentPhase === "land") {
|
|
263
|
+
return { transitionTo: null, reason: "already landed" };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
transitionTo: null,
|
|
267
|
+
reason: `phase=${state.currentPhase} is not a merge-trigger phase`,
|
|
268
|
+
};
|
|
269
|
+
}
|