@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
package/core/wiki.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildNativeWikiIndex,
|
|
3
|
+
ensureNativeWikiCurrent,
|
|
4
|
+
evaluateNativeWiki,
|
|
5
|
+
extractCitedFiles,
|
|
6
|
+
getNativeWikiContextForIssue,
|
|
7
|
+
getNativeWikiStatus,
|
|
8
|
+
initializeNativeWiki,
|
|
9
|
+
queryNativeWiki,
|
|
10
|
+
updateNativeWiki,
|
|
11
|
+
} from "./wiki-native";
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
NativeWikiBuildResult,
|
|
15
|
+
NativeWikiDimensions,
|
|
16
|
+
NativeWikiEvalReport,
|
|
17
|
+
NativeWikiFileDimensions,
|
|
18
|
+
NativeWikiFileEntry,
|
|
19
|
+
NativeWikiGraph,
|
|
20
|
+
NativeWikiGraphEdge,
|
|
21
|
+
NativeWikiIndex,
|
|
22
|
+
NativeWikiIssueContext,
|
|
23
|
+
NativeWikiQueryResult,
|
|
24
|
+
NativeWikiState,
|
|
25
|
+
NativeWikiStatus,
|
|
26
|
+
WikiPageSummary,
|
|
27
|
+
} from "./wiki-native";
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowEventEmitter — typed event emitter for gxpm execution observability.
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
* - Singleton via getWorkflowEventEmitter()
|
|
6
|
+
* - Fire-and-forget: listener errors never propagate to the emitter
|
|
7
|
+
* - Issue-scoped subscriptions via subscribeForIssue()
|
|
8
|
+
* - Built on Node.js EventEmitter (zero external deps)
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from "node:events";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Event types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface PhaseTransitionEvent {
|
|
17
|
+
type: "phase_transition";
|
|
18
|
+
issueId: string;
|
|
19
|
+
fromPhase: string | null;
|
|
20
|
+
toPhase: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ArtifactWrittenEvent {
|
|
25
|
+
type: "artifact_written";
|
|
26
|
+
issueId: string;
|
|
27
|
+
artifactType: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RunStartedEvent {
|
|
32
|
+
type: "run_started";
|
|
33
|
+
issueId: string;
|
|
34
|
+
runId: string;
|
|
35
|
+
status: string;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RunCompletedEvent {
|
|
40
|
+
type: "run_completed";
|
|
41
|
+
issueId: string;
|
|
42
|
+
runId: string;
|
|
43
|
+
status: string;
|
|
44
|
+
timestamp: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RunFailedEvent {
|
|
48
|
+
type: "run_failed";
|
|
49
|
+
issueId: string;
|
|
50
|
+
runId: string;
|
|
51
|
+
failureReason?: string;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface IssueCreatedEvent {
|
|
56
|
+
type: "issue_created";
|
|
57
|
+
issueId: string;
|
|
58
|
+
issueType?: string;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface IssueTransitionedEvent {
|
|
63
|
+
type: "issue_transitioned";
|
|
64
|
+
issueId: string;
|
|
65
|
+
fromPhase: string | null;
|
|
66
|
+
toPhase: string;
|
|
67
|
+
timestamp: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type WorkflowEmitterEvent =
|
|
71
|
+
| PhaseTransitionEvent
|
|
72
|
+
| ArtifactWrittenEvent
|
|
73
|
+
| RunStartedEvent
|
|
74
|
+
| RunCompletedEvent
|
|
75
|
+
| RunFailedEvent
|
|
76
|
+
| IssueCreatedEvent
|
|
77
|
+
| IssueTransitionedEvent;
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Emitter class
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export type WorkflowEventListener = (event: WorkflowEmitterEvent) => void;
|
|
84
|
+
|
|
85
|
+
const WORKFLOW_EVENT = "workflow_event";
|
|
86
|
+
|
|
87
|
+
export class WorkflowEventEmitter {
|
|
88
|
+
private emitter = new EventEmitter();
|
|
89
|
+
|
|
90
|
+
constructor() {
|
|
91
|
+
this.emitter.setMaxListeners(50);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Emit a workflow event. Fire-and-forget: errors are caught and logged to stderr.
|
|
96
|
+
*/
|
|
97
|
+
emit(event: WorkflowEmitterEvent): void {
|
|
98
|
+
try {
|
|
99
|
+
this.emitter.emit(WORKFLOW_EVENT, event);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
102
|
+
console.error(`[workflow-event-emitter] emit error: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to all workflow events. Returns an unsubscribe function.
|
|
108
|
+
*/
|
|
109
|
+
subscribe(listener: WorkflowEventListener): () => void {
|
|
110
|
+
const safeListener = (event: WorkflowEmitterEvent): void => {
|
|
111
|
+
try {
|
|
112
|
+
listener(event);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
115
|
+
console.error(`[workflow-event-emitter] listener error: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.emitter.on(WORKFLOW_EVENT, safeListener);
|
|
120
|
+
return (): void => {
|
|
121
|
+
this.emitter.removeListener(WORKFLOW_EVENT, safeListener);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Subscribe to events for a specific issue only. Returns an unsubscribe function.
|
|
127
|
+
*/
|
|
128
|
+
subscribeForIssue(issueId: string, listener: WorkflowEventListener): () => void {
|
|
129
|
+
return this.subscribe((event: WorkflowEmitterEvent) => {
|
|
130
|
+
if ("issueId" in event && event.issueId === issueId) {
|
|
131
|
+
listener(event);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Remove all listeners. Useful for graceful shutdown.
|
|
138
|
+
*/
|
|
139
|
+
clear(): void {
|
|
140
|
+
this.emitter.removeAllListeners();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Singleton
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
let instance: WorkflowEventEmitter | null = null;
|
|
149
|
+
|
|
150
|
+
export function getWorkflowEventEmitter(): WorkflowEventEmitter {
|
|
151
|
+
if (!instance) {
|
|
152
|
+
instance = new WorkflowEventEmitter();
|
|
153
|
+
}
|
|
154
|
+
return instance;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reset singleton for testing.
|
|
159
|
+
*/
|
|
160
|
+
export function resetWorkflowEventEmitter(): void {
|
|
161
|
+
instance?.clear();
|
|
162
|
+
instance = null;
|
|
163
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// WORKFLOW ENGINE — declarative workflow runtime.
|
|
2
|
+
// Loads YAML definitions, executes step-by-step, supports resume.
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import YAML from "yaml";
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowDefinition,
|
|
10
|
+
StepConfig,
|
|
11
|
+
StepBase,
|
|
12
|
+
StepContext,
|
|
13
|
+
StepResult,
|
|
14
|
+
RunState,
|
|
15
|
+
RunStatus,
|
|
16
|
+
StepStatus,
|
|
17
|
+
} from "./types";
|
|
18
|
+
import { evaluateCondition, evaluateExpression } from "./expressions";
|
|
19
|
+
|
|
20
|
+
export class StepRegistry {
|
|
21
|
+
private steps = new Map<string, StepBase>();
|
|
22
|
+
|
|
23
|
+
register(step: StepBase): void {
|
|
24
|
+
if (this.steps.has(step.typeKey)) {
|
|
25
|
+
throw new Error(`Step type ${step.typeKey} already registered`);
|
|
26
|
+
}
|
|
27
|
+
this.steps.set(step.typeKey, step);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(typeKey: string): StepBase | undefined {
|
|
31
|
+
return this.steps.get(typeKey);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has(typeKey: string): boolean {
|
|
35
|
+
return this.steps.has(typeKey);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list(): string[] {
|
|
39
|
+
return Array.from(this.steps.keys());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class WorkflowEngine {
|
|
44
|
+
private registry = new StepRegistry();
|
|
45
|
+
private definitions = new Map<string, WorkflowDefinition>();
|
|
46
|
+
|
|
47
|
+
constructor(private projectRoot: string) {}
|
|
48
|
+
|
|
49
|
+
getRegistry(): StepRegistry {
|
|
50
|
+
return this.registry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerStep(step: StepBase): void {
|
|
54
|
+
this.registry.register(step);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadYaml(yamlPath: string): WorkflowDefinition {
|
|
58
|
+
const absPath = join(this.projectRoot, yamlPath);
|
|
59
|
+
const text = readFileSync(absPath, "utf8");
|
|
60
|
+
const def = parseWorkflowYaml(text);
|
|
61
|
+
this.definitions.set(def.id, def);
|
|
62
|
+
return def;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
createRun(workflowId: string, inputs: Record<string, unknown>): RunState {
|
|
66
|
+
const def = this.definitions.get(workflowId);
|
|
67
|
+
if (!def) throw new Error(`Workflow ${workflowId} not loaded`);
|
|
68
|
+
|
|
69
|
+
const runId = randomUUID();
|
|
70
|
+
return {
|
|
71
|
+
runId,
|
|
72
|
+
workflowId,
|
|
73
|
+
status: "created",
|
|
74
|
+
currentStepIndex: -1,
|
|
75
|
+
stepIndexStack: [],
|
|
76
|
+
stepResults: {},
|
|
77
|
+
inputs,
|
|
78
|
+
createdAt: new Date().toISOString(),
|
|
79
|
+
updatedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async run(state: RunState): Promise<RunState> {
|
|
84
|
+
const def = this.definitions.get(state.workflowId);
|
|
85
|
+
if (!def) throw new Error(`Workflow ${state.workflowId} not loaded`);
|
|
86
|
+
|
|
87
|
+
state.status = "running";
|
|
88
|
+
state.updatedAt = new Date().toISOString();
|
|
89
|
+
|
|
90
|
+
const ctx: StepContext = {
|
|
91
|
+
inputs: state.inputs,
|
|
92
|
+
steps: state.stepResults,
|
|
93
|
+
runId: state.runId,
|
|
94
|
+
projectRoot: this.projectRoot,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
while (state.currentStepIndex < def.steps.length - 1) {
|
|
98
|
+
state.currentStepIndex++;
|
|
99
|
+
const stepConfig = def.steps[state.currentStepIndex];
|
|
100
|
+
|
|
101
|
+
if (stepConfig.type === "if") {
|
|
102
|
+
const condition = stepConfig.config.condition as string;
|
|
103
|
+
const shouldRun = evaluateCondition(condition, ctx);
|
|
104
|
+
if (!shouldRun) {
|
|
105
|
+
state.stepResults[stepConfig.id] = { status: "skipped", output: {} };
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
state.stepIndexStack.push(state.currentStepIndex);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (stepConfig.type === "endif") {
|
|
113
|
+
state.stepIndexStack.pop();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (stepConfig.type === "foreach") {
|
|
118
|
+
state.stepIndexStack.push(state.currentStepIndex);
|
|
119
|
+
const collection = resolveValue(stepConfig.config.collection, ctx) as unknown[];
|
|
120
|
+
if (!Array.isArray(collection) || collection.length === 0) {
|
|
121
|
+
state.stepResults[stepConfig.id] = { status: "skipped", output: {} };
|
|
122
|
+
state.stepIndexStack.pop();
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const results: StepResult[] = [];
|
|
126
|
+
for (const item of collection) {
|
|
127
|
+
const subSteps = stepConfig.config.steps as StepConfig[];
|
|
128
|
+
for (const subStep of subSteps) {
|
|
129
|
+
const result = await this.executeStep(subStep, { ...ctx, item }, state);
|
|
130
|
+
results.push(result);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
state.stepResults[stepConfig.id] = {
|
|
134
|
+
status: "completed",
|
|
135
|
+
output: { results },
|
|
136
|
+
};
|
|
137
|
+
state.stepIndexStack.pop();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = await this.executeStep(stepConfig, ctx, state);
|
|
142
|
+
ctx.steps = state.stepResults;
|
|
143
|
+
|
|
144
|
+
if (result.status === "failed") {
|
|
145
|
+
state.status = "failed";
|
|
146
|
+
state.updatedAt = new Date().toISOString();
|
|
147
|
+
return state;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (result.status === "paused") {
|
|
151
|
+
state.status = "paused";
|
|
152
|
+
state.updatedAt = new Date().toISOString();
|
|
153
|
+
this.saveState(state);
|
|
154
|
+
return state;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
state.status = "completed";
|
|
159
|
+
state.updatedAt = new Date().toISOString();
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Persist run state to disk so it can survive process restarts.
|
|
165
|
+
*/
|
|
166
|
+
saveState(state: RunState): string {
|
|
167
|
+
const runsDir = join(this.projectRoot, ".gxpm", "workflows", "runs", state.runId);
|
|
168
|
+
mkdirSync(runsDir, { recursive: true });
|
|
169
|
+
const statePath = join(runsDir, "state.json");
|
|
170
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
171
|
+
return statePath;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Load a previously persisted run state from disk.
|
|
176
|
+
*/
|
|
177
|
+
loadState(runId: string): RunState {
|
|
178
|
+
const statePath = join(this.projectRoot, ".gxpm", "workflows", "runs", runId, "state.json");
|
|
179
|
+
if (!existsSync(statePath)) {
|
|
180
|
+
throw new Error(`Workflow run state not found: ${statePath}`);
|
|
181
|
+
}
|
|
182
|
+
const raw = JSON.parse(readFileSync(statePath, "utf8")) as RunState;
|
|
183
|
+
return raw;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async resume(state: RunState): Promise<RunState> {
|
|
187
|
+
if (state.status !== "paused") {
|
|
188
|
+
throw new Error(`Cannot resume run in status ${state.status}`);
|
|
189
|
+
}
|
|
190
|
+
const lastStep = this.getLastStep(state);
|
|
191
|
+
if (lastStep && lastStep.status === "paused") {
|
|
192
|
+
const handler = this.registry.get(lastStep.stepId);
|
|
193
|
+
if (handler?.canResume && !handler.canResume(lastStep)) {
|
|
194
|
+
throw new Error(`Step ${lastStep.stepId} cannot be resumed`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return this.run(state);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async resumeFromDisk(runId: string): Promise<RunState> {
|
|
201
|
+
const state = this.loadState(runId);
|
|
202
|
+
return this.resume(state);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async executeStep(
|
|
206
|
+
stepConfig: StepConfig,
|
|
207
|
+
ctx: StepContext,
|
|
208
|
+
state: RunState,
|
|
209
|
+
): Promise<StepResult> {
|
|
210
|
+
const handler = this.registry.get(stepConfig.type);
|
|
211
|
+
if (!handler) {
|
|
212
|
+
const err = new Error(`Unknown step type: ${stepConfig.type}`);
|
|
213
|
+
const result: StepResult = {
|
|
214
|
+
status: "failed",
|
|
215
|
+
output: {},
|
|
216
|
+
error: err.message,
|
|
217
|
+
};
|
|
218
|
+
state.stepResults[stepConfig.id] = result;
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Validate config
|
|
223
|
+
if (handler.validate) {
|
|
224
|
+
const errors = handler.validate(stepConfig.config);
|
|
225
|
+
if (errors.length > 0) {
|
|
226
|
+
const result: StepResult = {
|
|
227
|
+
status: "failed",
|
|
228
|
+
output: {},
|
|
229
|
+
error: `Validation: ${errors.join("; ")}`,
|
|
230
|
+
};
|
|
231
|
+
state.stepResults[stepConfig.id] = result;
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const result = await handler.execute(stepConfig.config, ctx);
|
|
238
|
+
state.stepResults[stepConfig.id] = result;
|
|
239
|
+
return result;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const result: StepResult = {
|
|
242
|
+
status: "failed",
|
|
243
|
+
output: {},
|
|
244
|
+
error: err instanceof Error ? err.message : String(err),
|
|
245
|
+
};
|
|
246
|
+
state.stepResults[stepConfig.id] = result;
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private getLastStep(state: RunState): { stepId: string; status: StepStatus } | null {
|
|
252
|
+
const entries = Object.entries(state.stepResults);
|
|
253
|
+
if (entries.length === 0) return null;
|
|
254
|
+
const [lastId, lastResult] = entries[entries.length - 1];
|
|
255
|
+
return { stepId: lastId, status: lastResult.status };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveValue(template: unknown, context: StepContext): unknown {
|
|
260
|
+
if (typeof template === "string") {
|
|
261
|
+
return evaluateExpression(template, context);
|
|
262
|
+
}
|
|
263
|
+
return template;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Parse workflow YAML using the 'yaml' library.
|
|
267
|
+
export function parseWorkflowYaml(text: string): WorkflowDefinition {
|
|
268
|
+
const doc = YAML.parse(text);
|
|
269
|
+
if (!doc || typeof doc !== "object") {
|
|
270
|
+
throw new Error("Invalid YAML: document is not an object");
|
|
271
|
+
}
|
|
272
|
+
return doc as WorkflowDefinition;
|
|
273
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// EXPRESSION ENGINE — minimal Jinja2-like evaluator.
|
|
2
|
+
// Supports variable interpolation ${var} and simple conditions.
|
|
3
|
+
// NO eval / new Function. Whitelist only.
|
|
4
|
+
|
|
5
|
+
import type { StepContext } from "./types";
|
|
6
|
+
|
|
7
|
+
const ALLOWED_NAMESPACES = new Set([
|
|
8
|
+
"inputs",
|
|
9
|
+
"steps",
|
|
10
|
+
"issue",
|
|
11
|
+
"artifacts",
|
|
12
|
+
"item",
|
|
13
|
+
"fan_in",
|
|
14
|
+
"env",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function evaluateExpression(template: string, context: StepContext): unknown {
|
|
18
|
+
// Simple variable interpolation: ${namespace.key} or ${namespace.key.subkey}
|
|
19
|
+
const interpolated = template.replace(/\$\{([^}]+)\}/g, (_match, path) => {
|
|
20
|
+
const value = resolvePath(path.trim(), context);
|
|
21
|
+
return value !== undefined ? String(value) : "";
|
|
22
|
+
});
|
|
23
|
+
return interpolated;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function evaluateCondition(condition: string, context: StepContext): boolean {
|
|
27
|
+
const evaluated = evaluateExpression(condition, context);
|
|
28
|
+
if (typeof evaluated === "boolean") return evaluated;
|
|
29
|
+
if (typeof evaluated === "string") {
|
|
30
|
+
const normalized = evaluated.trim().toLowerCase();
|
|
31
|
+
return normalized === "true" || normalized === "yes" || normalized === "1";
|
|
32
|
+
}
|
|
33
|
+
if (typeof evaluated === "number") return evaluated !== 0;
|
|
34
|
+
return Boolean(evaluated);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolvePath(path: string, context: StepContext): unknown {
|
|
38
|
+
const parts = path.split(".");
|
|
39
|
+
const namespace = parts[0];
|
|
40
|
+
|
|
41
|
+
if (!ALLOWED_NAMESPACES.has(namespace)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let value: unknown;
|
|
46
|
+
switch (namespace) {
|
|
47
|
+
case "inputs":
|
|
48
|
+
value = context.inputs;
|
|
49
|
+
break;
|
|
50
|
+
case "steps":
|
|
51
|
+
value = context.steps;
|
|
52
|
+
break;
|
|
53
|
+
case "item":
|
|
54
|
+
value = context.item;
|
|
55
|
+
break;
|
|
56
|
+
case "fan_in":
|
|
57
|
+
value = context.fanIn;
|
|
58
|
+
break;
|
|
59
|
+
case "env":
|
|
60
|
+
value = process.env;
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (let i = 1; i < parts.length; i++) {
|
|
67
|
+
if (value === null || value === undefined) return undefined;
|
|
68
|
+
if (typeof value === "object") {
|
|
69
|
+
value = (value as Record<string, unknown>)[parts[i]];
|
|
70
|
+
} else {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// WORKFLOW ENGINE — public API.
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
WorkflowEngine,
|
|
5
|
+
StepRegistry,
|
|
6
|
+
parseWorkflowYaml,
|
|
7
|
+
} from "./engine";
|
|
8
|
+
export {
|
|
9
|
+
evaluateExpression,
|
|
10
|
+
evaluateCondition,
|
|
11
|
+
} from "./expressions";
|
|
12
|
+
export type {
|
|
13
|
+
WorkflowDefinition,
|
|
14
|
+
WorkflowInputSchema,
|
|
15
|
+
StepConfig,
|
|
16
|
+
StepBase,
|
|
17
|
+
StepContext,
|
|
18
|
+
StepResult,
|
|
19
|
+
RunState,
|
|
20
|
+
RunStatus,
|
|
21
|
+
StepStatus,
|
|
22
|
+
} from "./types";
|
|
23
|
+
|
|
24
|
+
// Built-in step types
|
|
25
|
+
export { CommandStep } from "./steps/command";
|
|
26
|
+
export { LinearStep } from "./steps/linear";
|
|
27
|
+
export { GxpmStep } from "./steps/gxpm";
|
|
28
|
+
export { GateStep } from "./steps/gate";
|
|
29
|
+
export { ShellStep } from "./steps/shell";
|
|
30
|
+
|
|
31
|
+
// Convenience: register all built-in steps
|
|
32
|
+
export function registerBuiltinSteps(engine: WorkflowEngine): void {
|
|
33
|
+
engine.registerStep(CommandStep);
|
|
34
|
+
engine.registerStep(LinearStep);
|
|
35
|
+
engine.registerStep(GxpmStep);
|
|
36
|
+
engine.registerStep(GateStep);
|
|
37
|
+
engine.registerStep(ShellStep);
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// COMMAND STEP — execute a shell command.
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import type { StepBase, StepResult, StepContext } from "../types";
|
|
5
|
+
|
|
6
|
+
export const CommandStep: StepBase = {
|
|
7
|
+
typeKey: "command",
|
|
8
|
+
|
|
9
|
+
validate(config) {
|
|
10
|
+
const errors: string[] = [];
|
|
11
|
+
if (!config.command || typeof config.command !== "string") {
|
|
12
|
+
errors.push("'command' is required and must be a string");
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
execute(config, context) {
|
|
18
|
+
const command = config.command as string;
|
|
19
|
+
const cwd = config.cwd ? String(config.cwd).replace("${projectRoot}", context.projectRoot) : context.projectRoot;
|
|
20
|
+
const capture = config.captureOutput !== false;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const output = execSync(command, {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: capture ? ["pipe", "pipe", "pipe"] : "inherit",
|
|
27
|
+
timeout: (config.timeout as number) || 60000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
status: "completed",
|
|
32
|
+
output: capture ? { stdout: output, command } : { command },
|
|
33
|
+
};
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
36
|
+
return {
|
|
37
|
+
status: config.ignoreFailure ? "completed" : "failed",
|
|
38
|
+
output: { command },
|
|
39
|
+
error: errorMessage,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// GATE STEP — pause workflow execution for human review/approval.
|
|
2
|
+
// When the engine reaches a gate step, it persists state and returns
|
|
3
|
+
// status "paused". The workflow can be resumed via `gxpm workflow resume`.
|
|
4
|
+
|
|
5
|
+
import type { StepBase, StepResult, StepContext } from "../types";
|
|
6
|
+
|
|
7
|
+
export const GateStep: StepBase = {
|
|
8
|
+
typeKey: "gate",
|
|
9
|
+
|
|
10
|
+
validate(config) {
|
|
11
|
+
const errors: string[] = [];
|
|
12
|
+
if (!config.message || typeof config.message !== "string") {
|
|
13
|
+
errors.push("'message' is required and must be a string");
|
|
14
|
+
}
|
|
15
|
+
return errors;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
execute(config, context) {
|
|
19
|
+
const message = config.message as string;
|
|
20
|
+
const condition = config.condition as string | undefined;
|
|
21
|
+
|
|
22
|
+
// If a condition is provided and evaluates to false, skip the gate
|
|
23
|
+
if (condition) {
|
|
24
|
+
const { evaluateCondition } = require("../expressions");
|
|
25
|
+
const shouldGate = evaluateCondition(condition, context);
|
|
26
|
+
if (!shouldGate) {
|
|
27
|
+
return {
|
|
28
|
+
status: "skipped",
|
|
29
|
+
output: { message, reason: "condition evaluated to false" },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
status: "paused",
|
|
36
|
+
output: {
|
|
37
|
+
message,
|
|
38
|
+
runId: context.runId,
|
|
39
|
+
pausedAt: new Date().toISOString(),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
canResume() {
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
};
|