@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,93 @@
|
|
|
1
|
+
import type { Converter, ConvertOptions, SkillDocument, SkillFrontmatter, SkillSection } from "../contracts/converter";
|
|
2
|
+
import type { HostConfig } from "../contracts/host";
|
|
3
|
+
import { getHostConfig } from "../../hosts";
|
|
4
|
+
|
|
5
|
+
function filterFrontmatter(fm: SkillFrontmatter, host: HostConfig): SkillFrontmatter {
|
|
6
|
+
if (host.frontmatter.mode === "preserve") {
|
|
7
|
+
return { ...fm };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const allowed = new Set(host.frontmatter.keys);
|
|
11
|
+
const filtered: SkillFrontmatter = {};
|
|
12
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
13
|
+
if (allowed.has(key)) {
|
|
14
|
+
filtered[key] = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return filtered;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildPreamble(host: HostConfig): string {
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
lines.push("## Host Preamble");
|
|
23
|
+
lines.push("");
|
|
24
|
+
lines.push(`Target host: ${host.displayName}.`);
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push("```bash");
|
|
27
|
+
|
|
28
|
+
if (host.usesEnvVars) {
|
|
29
|
+
lines.push('GXPM_ROOT="${GXPM_ROOT:-$PWD}"');
|
|
30
|
+
lines.push(`GXPM_STATE_DIR="\${GXPM_STATE_DIR:-$GXPM_ROOT/.gxpm}"`);
|
|
31
|
+
lines.push("export GXPM_ROOT GXPM_STATE_DIR");
|
|
32
|
+
} else {
|
|
33
|
+
lines.push('GXPM_ROOT="${GXPM_ROOT:-$PWD}"');
|
|
34
|
+
lines.push("export GXPM_ROOT");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
lines.push("```");
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function injectPreambleSections(sections: SkillSection[], host: HostConfig): SkillSection[] {
|
|
42
|
+
// Find the first heading after frontmatter (usually # Title)
|
|
43
|
+
// Insert preamble right after it, before the next section.
|
|
44
|
+
const preamble: SkillSection = {
|
|
45
|
+
type: "raw",
|
|
46
|
+
content: buildPreamble(host),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (sections.length === 0) {
|
|
50
|
+
return [preamble];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Insert after the first heading if present, otherwise at top
|
|
54
|
+
let insertIndex = 0;
|
|
55
|
+
for (let i = 0; i < sections.length; i++) {
|
|
56
|
+
if (sections[i].type === "heading") {
|
|
57
|
+
insertIndex = i + 1;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = [...sections];
|
|
63
|
+
result.splice(insertIndex, 0, preamble);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class HostConverter implements Converter {
|
|
68
|
+
convert(doc: SkillDocument, options: ConvertOptions): SkillDocument {
|
|
69
|
+
const host = getHostConfig(options.hostName);
|
|
70
|
+
|
|
71
|
+
// 1. Filter frontmatter
|
|
72
|
+
const frontmatter = filterFrontmatter(doc.frontmatter, host);
|
|
73
|
+
|
|
74
|
+
// 2. Inject host preamble into body sections
|
|
75
|
+
let body = injectPreambleSections(doc.body, host);
|
|
76
|
+
|
|
77
|
+
// 3. If templateVars include injected sections (like artifact commands),
|
|
78
|
+
// they are already resolved as raw blocks by the template renderer.
|
|
79
|
+
// We keep them as-is.
|
|
80
|
+
|
|
81
|
+
// 4. Preserve managed blocks (they travel with the document)
|
|
82
|
+
const managedBlocks = doc.managedBlocks.map((b) => ({ ...b }));
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
frontmatter,
|
|
86
|
+
body,
|
|
87
|
+
managedBlocks,
|
|
88
|
+
raw: doc.raw,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { filterFrontmatter, buildPreamble };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Multi-platform skill converter pipeline.
|
|
2
|
+
// Re-exports the parser, converter, writer, and template renderer.
|
|
3
|
+
|
|
4
|
+
export { SkillParser } from "./parser";
|
|
5
|
+
export { HostConverter, filterFrontmatter, buildPreamble } from "./converter";
|
|
6
|
+
export { SkillWriter, serializeFrontmatter, serializeBody, wrapManagedBlock } from "./writer";
|
|
7
|
+
export { renderTemplate, applyTemplateVars } from "./template-renderer";
|
|
8
|
+
export { parseManagedBlocks, mergeManagedBlocks } from "./managed-artifact";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ManagedBlock } from "../contracts/converter";
|
|
2
|
+
|
|
3
|
+
const MANAGED_BEGIN_RE = /<!--\s*BEGIN\s+MANAGED:(\S+)\s*-->/g;
|
|
4
|
+
const MANAGED_END_RE = /<!--\s*END\s+MANAGED:(\S+)\s*-->/g;
|
|
5
|
+
|
|
6
|
+
interface BlockRange {
|
|
7
|
+
id: string;
|
|
8
|
+
start: number; // index of BEGIN marker start
|
|
9
|
+
end: number; // index of END marker end
|
|
10
|
+
content: string; // content between markers
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function findManagedRanges(text: string): BlockRange[] {
|
|
14
|
+
const ranges: BlockRange[] = [];
|
|
15
|
+
const beginMatches = Array.from(text.matchAll(MANAGED_BEGIN_RE));
|
|
16
|
+
|
|
17
|
+
for (const beginMatch of beginMatches) {
|
|
18
|
+
const id = beginMatch[1];
|
|
19
|
+
const startIndex = beginMatch.index;
|
|
20
|
+
const afterBegin = beginMatch.index + beginMatch[0].length;
|
|
21
|
+
|
|
22
|
+
const endRe = new RegExp(`<!--\\s*END\\s+MANAGED:${id}\\s*-->`, "g");
|
|
23
|
+
endRe.lastIndex = afterBegin;
|
|
24
|
+
const endMatch = endRe.exec(text);
|
|
25
|
+
if (!endMatch) {
|
|
26
|
+
throw new Error(`Unclosed managed block: ${id} at index ${startIndex}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = text.slice(afterBegin, endMatch.index);
|
|
30
|
+
ranges.push({
|
|
31
|
+
id,
|
|
32
|
+
start: startIndex,
|
|
33
|
+
end: endMatch.index + endMatch[0].length,
|
|
34
|
+
content,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return ranges;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse managed blocks from existing content.
|
|
43
|
+
*/
|
|
44
|
+
export function parseManagedBlocks(text: string): ManagedBlock[] {
|
|
45
|
+
const ranges = findManagedRanges(text);
|
|
46
|
+
return ranges.map((r) => ({ id: r.id, content: r.content }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Merge user-managed blocks from existing content into newly generated content.
|
|
51
|
+
*
|
|
52
|
+
* Strategy:
|
|
53
|
+
* - New managed blocks replace old ones with the same ID.
|
|
54
|
+
* - Old managed blocks that no longer exist in the new content are removed.
|
|
55
|
+
* - Non-managed content in the existing file is preserved entirely.
|
|
56
|
+
*
|
|
57
|
+
* This is achieved by extracting all managed blocks from the new content,
|
|
58
|
+
* then overlaying them onto the existing content by ID.
|
|
59
|
+
*
|
|
60
|
+
* Actually, simpler: the generated content already has the new managed blocks.
|
|
61
|
+
* We want to preserve any non-managed parts of the existing content that are
|
|
62
|
+
* OUTSIDE managed blocks, but since the entire file is generated, this is only
|
|
63
|
+
* relevant for partial-generation scenarios.
|
|
64
|
+
*
|
|
65
|
+
* For SKILL.md generation, the simplest safe approach:
|
|
66
|
+
* - Extract old managed blocks from existing.
|
|
67
|
+
* - Extract new managed blocks from generated.
|
|
68
|
+
* - Build a merged document where:
|
|
69
|
+
* - The non-managed skeleton comes from the GENERATED content (source of truth)
|
|
70
|
+
* - Any managed block present in BOTH old and new keeps the NEW version
|
|
71
|
+
* - Any managed block ONLY in old is dropped (it was generated before)
|
|
72
|
+
* - Any managed block ONLY in new is added
|
|
73
|
+
*
|
|
74
|
+
* If the user wants to preserve arbitrary non-managed edits, they should not
|
|
75
|
+
* edit an AUTO-GENERATED file. The managed-block system is for allowing
|
|
76
|
+
* intentional extension points.
|
|
77
|
+
*/
|
|
78
|
+
export function mergeManagedBlocks(existingContent: string, generatedContent: string): string {
|
|
79
|
+
const existingRanges = findManagedRanges(existingContent);
|
|
80
|
+
const generatedRanges = findManagedRanges(generatedContent);
|
|
81
|
+
|
|
82
|
+
if (existingRanges.length === 0) {
|
|
83
|
+
// No existing managed blocks: return generated as-is
|
|
84
|
+
return generatedContent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (generatedRanges.length === 0) {
|
|
88
|
+
// Generated has no managed blocks: return generated as-is
|
|
89
|
+
return generatedContent;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build a map of generated blocks by ID
|
|
93
|
+
const generatedById = new Map<string, BlockRange>();
|
|
94
|
+
for (const r of generatedRanges) {
|
|
95
|
+
generatedById.set(r.id, r);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Start from the generated content, but for each managed block that also
|
|
99
|
+
// existed in the old content, we use the NEW content (source of truth).
|
|
100
|
+
// For any non-managed user edits between old blocks... we can't safely preserve
|
|
101
|
+
// them because the skeleton may have changed. So we just return generated.
|
|
102
|
+
|
|
103
|
+
// However, if the user added custom content OUTSIDE managed blocks in the old
|
|
104
|
+
// file, we might want to append it. That's too complex and error-prone.
|
|
105
|
+
// The contract is: edit .tmpl, not .md.
|
|
106
|
+
|
|
107
|
+
// One useful feature: if the generated block has a special sentinel
|
|
108
|
+
// "<!-- MANAGED-PRESERVE -->", we could keep old content. But let's keep it simple.
|
|
109
|
+
|
|
110
|
+
return generatedContent;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A stricter merge that preserves user content outside managed blocks
|
|
115
|
+
* only if the generated skeleton hasn't changed. This is a future enhancement.
|
|
116
|
+
*/
|
|
117
|
+
export function mergeManagedBlocksStrict(existingContent: string, generatedContent: string): string {
|
|
118
|
+
return mergeManagedBlocks(existingContent, generatedContent);
|
|
119
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ManagedBlock, ParseOptions, Parser, SkillDocument, SkillFrontmatter, SkillSection } from "../contracts/converter";
|
|
2
|
+
|
|
3
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
4
|
+
const MANAGED_BEGIN_RE = /<!--\s*BEGIN\s+MANAGED:(\S+)\s*-->/gi;
|
|
5
|
+
|
|
6
|
+
function parseFrontmatter(source: string): { frontmatter: SkillFrontmatter; bodyStart: number } {
|
|
7
|
+
const match = source.match(FRONTMATTER_RE);
|
|
8
|
+
if (!match) {
|
|
9
|
+
return { frontmatter: {}, bodyStart: 0 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fm: SkillFrontmatter = {};
|
|
13
|
+
const lines = match[1].split("\n");
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
17
|
+
const colonIndex = trimmed.indexOf(":");
|
|
18
|
+
if (colonIndex < 0) continue;
|
|
19
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
20
|
+
const rawValue = trimmed.slice(colonIndex + 1).trim();
|
|
21
|
+
if (rawValue === "true") {
|
|
22
|
+
fm[key] = true;
|
|
23
|
+
} else if (rawValue === "false") {
|
|
24
|
+
fm[key] = false;
|
|
25
|
+
} else if (/^-?\d+$/.test(rawValue)) {
|
|
26
|
+
fm[key] = Number(rawValue);
|
|
27
|
+
} else {
|
|
28
|
+
fm[key] = rawValue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { frontmatter: fm, bodyStart: match[0].length };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function splitSections(body: string): SkillSection[] {
|
|
36
|
+
const sections: SkillSection[] = [];
|
|
37
|
+
const lines = body.split("\n");
|
|
38
|
+
let buffer: string[] = [];
|
|
39
|
+
let inCodeBlock = false;
|
|
40
|
+
let codeLang = "";
|
|
41
|
+
|
|
42
|
+
function flush() {
|
|
43
|
+
if (buffer.length === 0) return;
|
|
44
|
+
const content = buffer.join("\n");
|
|
45
|
+
if (content.trim().length === 0) {
|
|
46
|
+
buffer = [];
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const firstLine = buffer[0];
|
|
51
|
+
if (firstLine.startsWith("#")) {
|
|
52
|
+
const level = firstLine.match(/^#+/)?.[0].length ?? 1;
|
|
53
|
+
sections.push({ type: "heading", content, level });
|
|
54
|
+
} else if (firstLine.startsWith("```")) {
|
|
55
|
+
sections.push({ type: "code", content, lang: codeLang });
|
|
56
|
+
} else if (firstLine.startsWith(">")) {
|
|
57
|
+
sections.push({ type: "blockquote", content });
|
|
58
|
+
} else if (/^\s*[-*+]|^\s*\d+\./.test(firstLine)) {
|
|
59
|
+
sections.push({ type: "list", content });
|
|
60
|
+
} else if (firstLine.startsWith("<") || firstLine.startsWith("<!--")) {
|
|
61
|
+
sections.push({ type: "html", content });
|
|
62
|
+
} else {
|
|
63
|
+
sections.push({ type: "paragraph", content });
|
|
64
|
+
}
|
|
65
|
+
buffer = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
if (line.startsWith("```")) {
|
|
70
|
+
if (inCodeBlock) {
|
|
71
|
+
buffer.push(line);
|
|
72
|
+
flush();
|
|
73
|
+
inCodeBlock = false;
|
|
74
|
+
codeLang = "";
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
flush();
|
|
78
|
+
inCodeBlock = true;
|
|
79
|
+
codeLang = line.slice(3).trim();
|
|
80
|
+
buffer.push(line);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (inCodeBlock) {
|
|
85
|
+
buffer.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (line.startsWith("#")) {
|
|
90
|
+
flush();
|
|
91
|
+
buffer.push(line);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (line.trim() === "") {
|
|
96
|
+
flush();
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
buffer.push(line);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
flush();
|
|
104
|
+
return sections;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function extractManagedBlocks(body: string): { blocks: ManagedBlock[]; cleanedBody: string } {
|
|
108
|
+
const blocks: ManagedBlock[] = [];
|
|
109
|
+
let cleaned = body;
|
|
110
|
+
|
|
111
|
+
let offset = 0;
|
|
112
|
+
while (true) {
|
|
113
|
+
const beginMatch = MANAGED_BEGIN_RE.exec(body);
|
|
114
|
+
if (!beginMatch) break;
|
|
115
|
+
|
|
116
|
+
const blockId = beginMatch[1];
|
|
117
|
+
const startIndex = beginMatch.index;
|
|
118
|
+
const endRe = new RegExp(`<!--\\s*END\\s+MANAGED:${blockId}\\s*-->`, "i");
|
|
119
|
+
const afterBegin = body.slice(startIndex + beginMatch[0].length);
|
|
120
|
+
const endMatch = endRe.exec(afterBegin);
|
|
121
|
+
if (!endMatch) {
|
|
122
|
+
throw new Error(`Unclosed managed block: ${blockId} at index ${startIndex}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const contentStart = startIndex + beginMatch[0].length;
|
|
126
|
+
const contentEnd = contentStart + endMatch.index;
|
|
127
|
+
const content = body.slice(contentStart, contentEnd);
|
|
128
|
+
blocks.push({ id: blockId, content });
|
|
129
|
+
|
|
130
|
+
const blockEnd = contentEnd + endMatch[0].length;
|
|
131
|
+
cleaned = cleaned.slice(0, startIndex - offset) + cleaned.slice(blockEnd - offset);
|
|
132
|
+
offset += blockEnd - startIndex;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { blocks, cleanedBody: cleaned };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class SkillParser implements Parser {
|
|
139
|
+
parse(source: string, options?: ParseOptions): SkillDocument {
|
|
140
|
+
const { frontmatter, bodyStart } = parseFrontmatter(source);
|
|
141
|
+
let body = source.slice(bodyStart);
|
|
142
|
+
|
|
143
|
+
let managedBlocks: ManagedBlock[] = [];
|
|
144
|
+
if (options?.extractManagedBlocks) {
|
|
145
|
+
const extracted = extractManagedBlocks(body);
|
|
146
|
+
managedBlocks = extracted.blocks;
|
|
147
|
+
body = extracted.cleanedBody;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sections = splitSections(body);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
frontmatter,
|
|
154
|
+
body: sections,
|
|
155
|
+
managedBlocks,
|
|
156
|
+
raw: source,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SkillDocument } from "../contracts/converter";
|
|
2
|
+
|
|
3
|
+
export interface TemplateVars {
|
|
4
|
+
[key: string]: string | Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render {{VAR}} and {{REFERENCE:name}} placeholders in a raw skill source.
|
|
9
|
+
* This operates on raw strings BEFORE parsing, preserving the existing
|
|
10
|
+
* gen-skill-docs behavior but extracted into a dedicated module.
|
|
11
|
+
*/
|
|
12
|
+
export function renderTemplate(source: string, vars: TemplateVars): string {
|
|
13
|
+
let result = source;
|
|
14
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
17
|
+
} else if (key === "references" && typeof value === "object" && value !== null) {
|
|
18
|
+
for (const [refName, refContent] of Object.entries(value)) {
|
|
19
|
+
result = result.replaceAll(`{{REFERENCE:${refName}}}`, refContent as string);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Apply template variables to a SkillDocument by re-rendering its raw source.
|
|
28
|
+
* This is a convenience for the existing pipeline where template vars are
|
|
29
|
+
* resolved before parsing.
|
|
30
|
+
*/
|
|
31
|
+
export function applyTemplateVars(doc: SkillDocument, vars: TemplateVars): SkillDocument {
|
|
32
|
+
const rendered = renderTemplate(doc.raw, vars);
|
|
33
|
+
// Return a document with updated raw; caller can re-parse if needed.
|
|
34
|
+
return { ...doc, raw: rendered };
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ManagedBlock, SkillDocument, SkillFrontmatter, SkillSection, TargetWriter, WriteOptions } from "../contracts/converter";
|
|
2
|
+
import { mergeManagedBlocks } from "./managed-artifact";
|
|
3
|
+
|
|
4
|
+
function serializeFrontmatter(fm: SkillFrontmatter): string {
|
|
5
|
+
const entries = Object.entries(fm).filter(([, v]) => v !== undefined);
|
|
6
|
+
if (entries.length === 0) return "";
|
|
7
|
+
const lines = entries.map(([k, v]) => {
|
|
8
|
+
if (typeof v === "boolean") return `${k}: ${v}`;
|
|
9
|
+
if (typeof v === "number") return `${k}: ${v}`;
|
|
10
|
+
return `${k}: ${v}`;
|
|
11
|
+
});
|
|
12
|
+
return `---\n${lines.join("\n")}\n---\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function serializeSection(section: SkillSection): string {
|
|
16
|
+
return section.content;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function serializeBody(sections: SkillSection[]): string {
|
|
20
|
+
return sections.map(serializeSection).join("\n\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function wrapManagedBlock(block: ManagedBlock): string {
|
|
24
|
+
return `<!-- BEGIN MANAGED:${block.id} -->\n${block.content}\n<!-- END MANAGED:${block.id} -->`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SkillWriter implements TargetWriter {
|
|
28
|
+
write(doc: SkillDocument, options?: WriteOptions): string {
|
|
29
|
+
// 1. Build generated content
|
|
30
|
+
const fm = serializeFrontmatter(doc.frontmatter);
|
|
31
|
+
const body = serializeBody(doc.body);
|
|
32
|
+
|
|
33
|
+
let managedBody = body;
|
|
34
|
+
if (doc.managedBlocks.length > 0) {
|
|
35
|
+
const blocksText = doc.managedBlocks.map(wrapManagedBlock).join("\n\n");
|
|
36
|
+
managedBody = `${body}\n\n${blocksText}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let generated = fm ? `${fm}\n${managedBody}` : managedBody;
|
|
40
|
+
|
|
41
|
+
if (options?.generatedMark) {
|
|
42
|
+
const fmMatch = generated.match(/^---\n[\s\S]*?\n---\n?/);
|
|
43
|
+
if (fmMatch) {
|
|
44
|
+
const head = fmMatch[0].trimEnd();
|
|
45
|
+
const rest = generated.slice(fmMatch[0].length).replace(/^\n+/, "");
|
|
46
|
+
generated = `${head}\n${options.generatedMark}\n\n${rest}`;
|
|
47
|
+
} else {
|
|
48
|
+
generated = `${options.generatedMark}\n\n${generated}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. If existing content provided, merge managed blocks
|
|
53
|
+
if (options?.existingContent) {
|
|
54
|
+
generated = mergeManagedBlocks(options.existingContent, generated);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return generated;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { serializeFrontmatter, serializeBody, wrapManagedBlock };
|