@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,221 @@
|
|
|
1
|
+
// PRESET RESOLVER — Override > Preset > Core three-layer resolution.
|
|
2
|
+
// Supports replace / prepend / append / wrap composition strategies.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
|
|
7
|
+
export type CompositionStrategy = "replace" | "prepend" | "append" | "wrap";
|
|
8
|
+
|
|
9
|
+
export interface CompositionRule {
|
|
10
|
+
/** Target file path, relative to project root (e.g. "skills/gxpm/SKILL.md"). */
|
|
11
|
+
target: string;
|
|
12
|
+
/** How to compose the source into the target. */
|
|
13
|
+
strategy: CompositionStrategy;
|
|
14
|
+
/** Source file path, relative to the preset directory. */
|
|
15
|
+
source: string;
|
|
16
|
+
/** Optional anchor marker for prepend/append/wrap positioning. */
|
|
17
|
+
anchor?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PresetManifest {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
version: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
/** IDs of presets this one extends (lower priority). */
|
|
26
|
+
extends?: string[];
|
|
27
|
+
/** Composition rules for this preset. */
|
|
28
|
+
rules: CompositionRule[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PresetRegistry {
|
|
32
|
+
/** Ordered list of active preset IDs (highest priority first). */
|
|
33
|
+
active: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ResolveResult {
|
|
37
|
+
content: string;
|
|
38
|
+
/** Which layer provided the final content. */
|
|
39
|
+
source: "override" | "preset" | "core";
|
|
40
|
+
/** Preset IDs that contributed (empty for override/core). */
|
|
41
|
+
appliedPresets: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PRESET_REGISTRY_FILE = ".registry";
|
|
45
|
+
const MANIFEST_FILE = "manifest.json";
|
|
46
|
+
|
|
47
|
+
export class PresetResolver {
|
|
48
|
+
private presets = new Map<string, PresetManifest>();
|
|
49
|
+
private registry: PresetRegistry = { active: [] };
|
|
50
|
+
|
|
51
|
+
constructor(private projectRoot: string) {}
|
|
52
|
+
|
|
53
|
+
/** Load the preset registry and all active preset manifests. */
|
|
54
|
+
load(): void {
|
|
55
|
+
this.presets.clear();
|
|
56
|
+
this.loadRegistry();
|
|
57
|
+
for (const presetId of this.registry.active) {
|
|
58
|
+
this.loadPreset(presetId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the final content for a target path.
|
|
64
|
+
*
|
|
65
|
+
* Priority: Override > Preset > Core
|
|
66
|
+
*
|
|
67
|
+
* @param targetPath — file path relative to project root
|
|
68
|
+
* @param baseContent — core-layer content (e.g. template-generated output)
|
|
69
|
+
* @returns resolved content and metadata, or null if no content found
|
|
70
|
+
*/
|
|
71
|
+
resolve(targetPath: string, baseContent?: string): ResolveResult | null {
|
|
72
|
+
const normalized = targetPath.replace(/^\//, "");
|
|
73
|
+
|
|
74
|
+
// 1. Override layer (highest priority)
|
|
75
|
+
const overridePath = join(this.projectRoot, ".gxpm", "overrides", normalized);
|
|
76
|
+
if (existsSync(overridePath)) {
|
|
77
|
+
return {
|
|
78
|
+
content: readFileSync(overridePath, "utf8"),
|
|
79
|
+
source: "override",
|
|
80
|
+
appliedPresets: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. Preset layer (middle priority)
|
|
85
|
+
let content = baseContent ?? "";
|
|
86
|
+
let presetApplied = false;
|
|
87
|
+
const appliedPresets: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const presetId of this.registry.active) {
|
|
90
|
+
const preset = this.presets.get(presetId);
|
|
91
|
+
if (!preset) continue;
|
|
92
|
+
|
|
93
|
+
const rules = preset.rules.filter((r) => this.matchTarget(r.target, normalized));
|
|
94
|
+
if (rules.length === 0) continue;
|
|
95
|
+
|
|
96
|
+
for (const rule of rules) {
|
|
97
|
+
const sourcePath = join(this.presetDir, presetId, rule.source);
|
|
98
|
+
if (!existsSync(sourcePath)) continue;
|
|
99
|
+
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
100
|
+
content = this.applyStrategy(content, sourceContent, rule.strategy, rule.anchor);
|
|
101
|
+
presetApplied = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
appliedPresets.push(presetId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (presetApplied) {
|
|
108
|
+
return { content, source: "preset", appliedPresets };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Core layer (lowest priority)
|
|
112
|
+
if (baseContent !== undefined) {
|
|
113
|
+
return { content: baseContent, source: "core", appliedPresets: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// No content available at any layer
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** List all loaded preset IDs. */
|
|
121
|
+
listPresets(): string[] {
|
|
122
|
+
return Array.from(this.presets.keys());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** List active preset IDs in resolution order. */
|
|
126
|
+
listActive(): string[] {
|
|
127
|
+
return [...this.registry.active];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Get a loaded preset manifest by ID. */
|
|
131
|
+
getPreset(id: string): PresetManifest | undefined {
|
|
132
|
+
return this.presets.get(id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private get presetDir(): string {
|
|
136
|
+
return join(this.projectRoot, ".gxpm", "presets");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private loadRegistry(): void {
|
|
140
|
+
const registryPath = join(this.presetDir, PRESET_REGISTRY_FILE);
|
|
141
|
+
if (!existsSync(registryPath)) {
|
|
142
|
+
this.registry = { active: [] };
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const raw = readFileSync(registryPath, "utf8");
|
|
147
|
+
const doc = JSON.parse(raw) as PresetRegistry;
|
|
148
|
+
this.registry = { active: doc.active ?? [] };
|
|
149
|
+
} catch {
|
|
150
|
+
this.registry = { active: [] };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private loadPreset(id: string): void {
|
|
155
|
+
const manifestPath = join(this.presetDir, id, MANIFEST_FILE);
|
|
156
|
+
if (!existsSync(manifestPath)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(manifestPath, "utf8");
|
|
161
|
+
const manifest = JSON.parse(raw) as PresetManifest;
|
|
162
|
+
this.presets.set(id, manifest);
|
|
163
|
+
} catch {
|
|
164
|
+
// Silently skip malformed presets
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private matchTarget(ruleTarget: string, filePath: string): boolean {
|
|
169
|
+
// Simple glob-like matching: exact match or wildcard suffix
|
|
170
|
+
if (ruleTarget === filePath) return true;
|
|
171
|
+
if (ruleTarget.endsWith("/*")) {
|
|
172
|
+
const prefix = ruleTarget.slice(0, -1);
|
|
173
|
+
return filePath.startsWith(prefix);
|
|
174
|
+
}
|
|
175
|
+
if (ruleTarget.includes("*")) {
|
|
176
|
+
const regex = new RegExp("^" + ruleTarget.replace(/\*/g, ".*") + "$");
|
|
177
|
+
return regex.test(filePath);
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private applyStrategy(
|
|
183
|
+
base: string,
|
|
184
|
+
source: string,
|
|
185
|
+
strategy: CompositionStrategy,
|
|
186
|
+
anchor?: string,
|
|
187
|
+
): string {
|
|
188
|
+
switch (strategy) {
|
|
189
|
+
case "replace":
|
|
190
|
+
return source;
|
|
191
|
+
case "prepend":
|
|
192
|
+
return anchor ? this.insertBefore(base, source, anchor) : source + "\n" + base;
|
|
193
|
+
case "append":
|
|
194
|
+
return anchor ? this.insertAfter(base, source, anchor) : base + "\n" + source;
|
|
195
|
+
case "wrap": {
|
|
196
|
+
if (anchor) {
|
|
197
|
+
const parts = base.split(anchor);
|
|
198
|
+
if (parts.length >= 2) {
|
|
199
|
+
return parts[0] + source + parts.slice(1).join(anchor);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return source + "\n" + base + "\n" + source;
|
|
203
|
+
}
|
|
204
|
+
default:
|
|
205
|
+
return base;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private insertBefore(base: string, source: string, anchor: string): string {
|
|
210
|
+
const idx = base.indexOf(anchor);
|
|
211
|
+
if (idx === -1) return source + "\n" + base;
|
|
212
|
+
return base.slice(0, idx) + source + "\n" + base.slice(idx);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private insertAfter(base: string, source: string, anchor: string): string {
|
|
216
|
+
const idx = base.lastIndexOf(anchor);
|
|
217
|
+
if (idx === -1) return base + "\n" + source;
|
|
218
|
+
const end = idx + anchor.length;
|
|
219
|
+
return base.slice(0, end) + "\n" + source + base.slice(end);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type ProjectInitializationKind = "uninitialized" | "partial" | "initialized";
|
|
5
|
+
|
|
6
|
+
export interface ProjectInitializationStatus {
|
|
7
|
+
kind: ProjectInitializationKind;
|
|
8
|
+
cwd: string | null;
|
|
9
|
+
missingMarkers: string[];
|
|
10
|
+
presentMarkers: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const REQUIRED_GXPM_DIRS = [
|
|
14
|
+
".gxpm/issues",
|
|
15
|
+
".gxpm/local",
|
|
16
|
+
".gxpm/out-of-scope",
|
|
17
|
+
".gxpm/wiki",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const REQUIRED_GXPM_FILES = [
|
|
21
|
+
".gxpm/config.json",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const REQUIRED_GXPM_GIT_HOOKS = [
|
|
25
|
+
"gxpm-pre-commit",
|
|
26
|
+
"gxpm-commit-msg",
|
|
27
|
+
"gxpm-pre-push",
|
|
28
|
+
"gxpm-post-merge",
|
|
29
|
+
"gxpm-post-checkout",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function getProjectInitializationStatus(cwd: string | null | undefined): ProjectInitializationStatus {
|
|
33
|
+
if (!cwd) {
|
|
34
|
+
return {
|
|
35
|
+
kind: "uninitialized",
|
|
36
|
+
cwd: null,
|
|
37
|
+
missingMarkers: ["cwd"],
|
|
38
|
+
presentMarkers: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const presentMarkers: string[] = [];
|
|
43
|
+
const missingMarkers: string[] = [];
|
|
44
|
+
const gxpmRootExists = isDirectory(join(cwd, ".gxpm"));
|
|
45
|
+
|
|
46
|
+
if (!gxpmRootExists) {
|
|
47
|
+
return {
|
|
48
|
+
kind: "uninitialized",
|
|
49
|
+
cwd,
|
|
50
|
+
missingMarkers: [".gxpm", ...REQUIRED_GXPM_DIRS, ...REQUIRED_GXPM_FILES],
|
|
51
|
+
presentMarkers,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
presentMarkers.push(".gxpm");
|
|
55
|
+
|
|
56
|
+
for (const dir of REQUIRED_GXPM_DIRS) {
|
|
57
|
+
if (isDirectory(join(cwd, dir))) {
|
|
58
|
+
presentMarkers.push(dir);
|
|
59
|
+
} else {
|
|
60
|
+
missingMarkers.push(dir);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const file of REQUIRED_GXPM_FILES) {
|
|
65
|
+
if (isFile(join(cwd, file))) {
|
|
66
|
+
presentMarkers.push(file);
|
|
67
|
+
} else {
|
|
68
|
+
missingMarkers.push(file);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (hasGitMetadata(cwd)) {
|
|
73
|
+
for (const hook of REQUIRED_GXPM_GIT_HOOKS) {
|
|
74
|
+
const marker = `.githooks/${hook}`;
|
|
75
|
+
if (isFile(join(cwd, marker))) {
|
|
76
|
+
presentMarkers.push(marker);
|
|
77
|
+
} else {
|
|
78
|
+
missingMarkers.push(marker);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
kind: missingMarkers.length === 0 ? "initialized" : "partial",
|
|
85
|
+
cwd,
|
|
86
|
+
missingMarkers,
|
|
87
|
+
presentMarkers,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatProjectInitializationContext(status: ProjectInitializationStatus): string | null {
|
|
92
|
+
if (status.kind === "initialized") return null;
|
|
93
|
+
|
|
94
|
+
const missing = status.missingMarkers.slice(0, 8).join(", ");
|
|
95
|
+
const suffix = status.missingMarkers.length > 8 ? ", ..." : "";
|
|
96
|
+
const state =
|
|
97
|
+
status.kind === "uninitialized"
|
|
98
|
+
? "gxpm hooks are active, but this repository has not been initialized"
|
|
99
|
+
: "gxpm initialization is incomplete";
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
`${state}.`,
|
|
103
|
+
`Missing: ${missing}${suffix}.`,
|
|
104
|
+
"Run `gxpm init --target <repo>` or `gxpm doctor --fix` after reviewing existing repo hooks.",
|
|
105
|
+
"Until initialization is complete, gxpm hooks will not write plan state.",
|
|
106
|
+
].join(" ");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasGitMetadata(cwd: string): boolean {
|
|
110
|
+
return existsSync(join(cwd, ".git"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isDirectory(path: string): boolean {
|
|
114
|
+
try {
|
|
115
|
+
return statSync(path).isDirectory();
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isFile(path: string): boolean {
|
|
122
|
+
try {
|
|
123
|
+
return statSync(path).isFile();
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/core/qa.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createPhaseArtifactInitializer } from "./phase-artifact";
|
|
2
|
+
|
|
3
|
+
export const initializeQaFindings = createPhaseArtifactInitializer({
|
|
4
|
+
artifactType: "qa-findings",
|
|
5
|
+
label: "QA findings",
|
|
6
|
+
payload: {
|
|
7
|
+
browserEvidence: [],
|
|
8
|
+
findings: [],
|
|
9
|
+
risks: [],
|
|
10
|
+
status: "draft",
|
|
11
|
+
summary: "",
|
|
12
|
+
verifyFindingsArtifact: "verify-findings",
|
|
13
|
+
},
|
|
14
|
+
requiredPhase: "verify",
|
|
15
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// ─── Error Classification ────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type ErrorType = "FATAL" | "TRANSIENT" | "UNKNOWN";
|
|
4
|
+
|
|
5
|
+
/** Fatal error patterns — authentication/authorization/quota issues that won't resolve with retry */
|
|
6
|
+
export const FATAL_PATTERNS = [
|
|
7
|
+
"unauthorized",
|
|
8
|
+
"forbidden",
|
|
9
|
+
"invalid token",
|
|
10
|
+
"authentication failed",
|
|
11
|
+
"permission denied",
|
|
12
|
+
"401",
|
|
13
|
+
"403",
|
|
14
|
+
"credit balance",
|
|
15
|
+
"auth error",
|
|
16
|
+
"insufficient credit",
|
|
17
|
+
"quota exceeded",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Transient error patterns — temporary issues that may resolve with retry */
|
|
21
|
+
export const TRANSIENT_PATTERNS = [
|
|
22
|
+
"timeout",
|
|
23
|
+
"econnrefused",
|
|
24
|
+
"econnreset",
|
|
25
|
+
"etimedout",
|
|
26
|
+
"rate limit",
|
|
27
|
+
"too many requests",
|
|
28
|
+
"429",
|
|
29
|
+
"503",
|
|
30
|
+
"502",
|
|
31
|
+
"network error",
|
|
32
|
+
"socket hang up",
|
|
33
|
+
"exited with code",
|
|
34
|
+
"temporarily unavailable",
|
|
35
|
+
"connection reset",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if error message matches any pattern in the list.
|
|
40
|
+
*/
|
|
41
|
+
export function matchesPattern(message: string, patterns: readonly string[]): boolean {
|
|
42
|
+
return patterns.some((pattern) => message.includes(pattern));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Classify an error to determine if it's transient (can retry) or fatal (should fail).
|
|
47
|
+
* FATAL patterns take priority over TRANSIENT patterns to prevent an error message
|
|
48
|
+
* containing both from being retried.
|
|
49
|
+
*/
|
|
50
|
+
export function classifyError(error: Error): ErrorType {
|
|
51
|
+
const message = error.message.toLowerCase();
|
|
52
|
+
|
|
53
|
+
if (matchesPattern(message, FATAL_PATTERNS)) {
|
|
54
|
+
return "FATAL";
|
|
55
|
+
}
|
|
56
|
+
if (matchesPattern(message, TRANSIENT_PATTERNS)) {
|
|
57
|
+
return "TRANSIENT";
|
|
58
|
+
}
|
|
59
|
+
return "UNKNOWN";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Safe Send Message ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface SafeSendMessageOptions {
|
|
65
|
+
/** Threshold for consecutive UNKNOWN errors before throwing. Default: 3 */
|
|
66
|
+
unknownThreshold?: number;
|
|
67
|
+
/** Mutable counter shared across calls to track consecutive UNKNOWN errors. */
|
|
68
|
+
unknownCounter?: { count: number };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SafeSendMessageResult<T> {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
value?: T;
|
|
74
|
+
error?: Error;
|
|
75
|
+
errorType?: ErrorType;
|
|
76
|
+
unknownCount?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Execute a function with error classification and differentiated handling:
|
|
81
|
+
* - TRANSIENT errors: swallow and return { ok: false }
|
|
82
|
+
* - FATAL errors: re-throw immediately
|
|
83
|
+
* - UNKNOWN errors: track consecutive occurrences; throw only when threshold is exceeded
|
|
84
|
+
*/
|
|
85
|
+
export async function safeSendMessage<T>(
|
|
86
|
+
fn: () => T | Promise<T>,
|
|
87
|
+
options: SafeSendMessageOptions = {},
|
|
88
|
+
): Promise<SafeSendMessageResult<T>> {
|
|
89
|
+
const threshold = options.unknownThreshold ?? 3;
|
|
90
|
+
const counter = options.unknownCounter ?? { count: 0 };
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const value = await Promise.resolve(fn());
|
|
94
|
+
counter.count = 0;
|
|
95
|
+
return { ok: true, value, unknownCount: counter.count };
|
|
96
|
+
} catch (rawError) {
|
|
97
|
+
const error = rawError instanceof Error ? rawError : new Error(String(rawError));
|
|
98
|
+
const errorType = classifyError(error);
|
|
99
|
+
|
|
100
|
+
if (errorType === "FATAL") {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (errorType === "TRANSIENT") {
|
|
105
|
+
return { ok: false, error, errorType, unknownCount: counter.count };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// UNKNOWN
|
|
109
|
+
counter.count += 1;
|
|
110
|
+
if (counter.count >= threshold) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
return { ok: false, error, errorType, unknownCount: counter.count };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Retry Wrapper ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export interface RetryOptions {
|
|
120
|
+
/** Maximum number of retry attempts for TRANSIENT errors. Default: 2 */
|
|
121
|
+
maxRetries?: number;
|
|
122
|
+
/** Base delay in milliseconds for exponential backoff. Default: 1000 */
|
|
123
|
+
baseDelayMs?: number;
|
|
124
|
+
/** Optional callback invoked before each retry. */
|
|
125
|
+
onRetry?: (attempt: number, error: Error, delayMs: number) => void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sleep(ms: number): Promise<void> {
|
|
129
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Execute a function with automatic retry for TRANSIENT errors only.
|
|
134
|
+
* FATAL and UNKNOWN errors fail immediately.
|
|
135
|
+
* Backoff delays: baseDelayMs * attempt (linear by default; caller can customize via onRetry).
|
|
136
|
+
*/
|
|
137
|
+
export async function withRetry<T>(fn: () => T | Promise<T>, options: RetryOptions = {}): Promise<T> {
|
|
138
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
139
|
+
const baseDelayMs = options.baseDelayMs ?? 1000;
|
|
140
|
+
|
|
141
|
+
let lastError: Error | undefined;
|
|
142
|
+
|
|
143
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
144
|
+
try {
|
|
145
|
+
return await Promise.resolve(fn());
|
|
146
|
+
} catch (rawError) {
|
|
147
|
+
const error = rawError instanceof Error ? rawError : new Error(String(rawError));
|
|
148
|
+
lastError = error;
|
|
149
|
+
const errorType = classifyError(error);
|
|
150
|
+
|
|
151
|
+
if (errorType === "FATAL" || errorType === "UNKNOWN") {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// TRANSIENT — retry if attempts remain
|
|
156
|
+
if (attempt < maxRetries) {
|
|
157
|
+
const delayMs = baseDelayMs * (attempt + 1);
|
|
158
|
+
options.onRetry?.(attempt + 1, error, delayMs);
|
|
159
|
+
await sleep(delayMs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw lastError ?? new Error("Retry exhausted");
|
|
165
|
+
}
|