forge-workflow 0.0.10 → 0.1.0-beta.2
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/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +461 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +838 -972
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +307 -100
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doc-gate OKF (Open Knowledge Format) bundle generator.
|
|
5
|
+
*
|
|
6
|
+
* Deterministic, script-first "markdown -> OKF bundle" core. A "bundle" is a
|
|
7
|
+
* directory tree of concept docs (the unit of distribution). Each source `.md`
|
|
8
|
+
* becomes a concept doc whose Concept ID is its path minus `.md`.
|
|
9
|
+
*
|
|
10
|
+
* Spec facts implemented (OKF v0.1 DRAFT — "not an official Google product"):
|
|
11
|
+
* - Concept front matter: the ONLY required key is `type`. `title` is the
|
|
12
|
+
* optional display name (there is NO `name` key). We inject `type` + `title`
|
|
13
|
+
* and preserve the body verbatim.
|
|
14
|
+
* - Per-folder `index.md` lists that folder's contents and carries NO front
|
|
15
|
+
* matter. The BUNDLE-ROOT `index.md` is the ONLY index that may carry front
|
|
16
|
+
* matter, for exactly one key: `okf_version: "0.1"`.
|
|
17
|
+
*
|
|
18
|
+
* OKF is a SERIALIZATION / interop format only — it has no enforcement or query
|
|
19
|
+
* semantics. Generation NEVER touches detect()/gate() verdicts; resolution
|
|
20
|
+
* correctness stays doc-gate's own concern.
|
|
21
|
+
*
|
|
22
|
+
* Conventions shared with the rest of doc-gate:
|
|
23
|
+
* - Tracked-files-only reads: source markdown comes from `git ls-files`, never a
|
|
24
|
+
* raw filesystem walk, so untracked clutter can't leak into a bundle.
|
|
25
|
+
* - Fail-closed git wrapper that THROWS.
|
|
26
|
+
* - Symlink-safe writes: every write target is `lstat`-checked and refused if it
|
|
27
|
+
* (or a parent component) is a symlink, and nothing is ever written outside the
|
|
28
|
+
* out dir.
|
|
29
|
+
*
|
|
30
|
+
* @module doc-gate/okf
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require('node:fs');
|
|
34
|
+
const path = require('node:path');
|
|
35
|
+
const cp = require('node:child_process');
|
|
36
|
+
|
|
37
|
+
const OKF_VERSION = '0.1';
|
|
38
|
+
const ROOT_INDEX = 'index.md';
|
|
39
|
+
const NAV_BEGIN = '<!-- BEGIN FORGE OKF NAV -->';
|
|
40
|
+
const NAV_END = '<!-- END FORGE OKF NAV -->';
|
|
41
|
+
|
|
42
|
+
// Top-level markdown that is boilerplate, not knowledge-base content. Only used
|
|
43
|
+
// by the auto-source fallback (when there is no docs/ dir and no --source).
|
|
44
|
+
const AGENT_OR_META_MD = new Set([
|
|
45
|
+
'agents.md', 'claude.md', 'gemini.md', 'readme.md', 'contributing.md',
|
|
46
|
+
'license.md', 'changelog.md', 'code_of_conduct.md', 'security.md',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// --- git (fail-closed) --------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Strict git wrapper — THROWS on any failure (fail-closed), like declaration.js. */
|
|
52
|
+
function gitStrict(root, args) {
|
|
53
|
+
// NOSONAR S4036 - hardcoded CLI command, no user input; developer-tool context.
|
|
54
|
+
const res = cp.spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); // NOSONAR S4036
|
|
55
|
+
if (res.error) throw new Error(`git ${args.join(' ')}: ${res.error.message}`);
|
|
56
|
+
if (res.status !== 0) {
|
|
57
|
+
throw new Error(`git ${args.join(' ')} exited ${res.status}: ${String(res.stderr || '').trim()}`);
|
|
58
|
+
}
|
|
59
|
+
return res.stdout;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Tracked files (repo-relative, forward-slashed) at `root`. */
|
|
63
|
+
function trackedFiles(root) {
|
|
64
|
+
return gitStrict(root, ['ls-files'])
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map(s => s.trim())
|
|
67
|
+
.filter(line => line !== '');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- path hygiene -------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalize + validate a user-supplied relative path. Rejects blank/whitespace
|
|
74
|
+
* input and any `..` segment; returns a forward-slashed, trimmed path. A lone
|
|
75
|
+
* `.` normalizes to '' (meaning "repo root").
|
|
76
|
+
*/
|
|
77
|
+
function cleanRel(value, label) {
|
|
78
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
79
|
+
throw new Error(`${label} must be a non-empty path`);
|
|
80
|
+
}
|
|
81
|
+
const norm = value.replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
82
|
+
const stripped = norm === '.' ? '' : norm;
|
|
83
|
+
if (stripped.split('/').some(seg => seg === '..')) {
|
|
84
|
+
throw new Error(`${label} must not contain '..': ${value}`);
|
|
85
|
+
}
|
|
86
|
+
return stripped;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- source resolution --------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
const isMarkdown = file => file.toLowerCase().endsWith('.md');
|
|
92
|
+
|
|
93
|
+
/** True when a fallback markdown file should be skipped (out dir / boilerplate). */
|
|
94
|
+
function isExcludedFallback(file, outRel) {
|
|
95
|
+
if (outRel !== '' && (file === outRel || file.startsWith(`${outRel}/`))) return true;
|
|
96
|
+
if (!file.includes('/') && AGENT_OR_META_MD.has(file.toLowerCase())) return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the source markdown set + the base dir concept paths are relative to.
|
|
102
|
+
* With `--source`: tracked markdown UNDER that dir. Auto: prefer `docs/`; else
|
|
103
|
+
* fall back to all tracked markdown minus the out dir and top-level boilerplate.
|
|
104
|
+
*/
|
|
105
|
+
function resolveSources(root, sourceRel, outRel) {
|
|
106
|
+
const all = trackedFiles(root);
|
|
107
|
+
if (sourceRel !== null) {
|
|
108
|
+
const base = sourceRel;
|
|
109
|
+
const prefix = base === '' ? '' : `${base}/`;
|
|
110
|
+
// Exclude the output dir even with an explicit --source, so re-generating
|
|
111
|
+
// never folds a previous bundle back into itself.
|
|
112
|
+
const inOut = f => outRel !== '' && (f === outRel || f.startsWith(`${outRel}/`));
|
|
113
|
+
const files = all.filter(f => isMarkdown(f) && f.startsWith(prefix) && f !== base && !inOut(f));
|
|
114
|
+
return { base, files };
|
|
115
|
+
}
|
|
116
|
+
const docs = all.filter(f => isMarkdown(f) && f.startsWith('docs/'));
|
|
117
|
+
if (docs.length > 0) return { base: 'docs', files: docs };
|
|
118
|
+
const files = all.filter(f => isMarkdown(f) && !isExcludedFallback(f, outRel));
|
|
119
|
+
return { base: '', files };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Concept path relative to the source base dir. */
|
|
123
|
+
function conceptRel(file, base) {
|
|
124
|
+
return base === '' ? file : file.slice(base.length + 1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- concept construction -----------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** Infer a sensible OKF `type` from the concept path (default `document`). */
|
|
130
|
+
function inferType(rel) {
|
|
131
|
+
const p = rel.toLowerCase();
|
|
132
|
+
if (p.includes('guide') || p.includes('tutorial') || p.includes('how-to') || p.includes('howto')) {
|
|
133
|
+
return 'guide';
|
|
134
|
+
}
|
|
135
|
+
if (p.includes('reference') || p.includes('/api/') || p.startsWith('api/')) {
|
|
136
|
+
return 'reference';
|
|
137
|
+
}
|
|
138
|
+
return 'document';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** First ATX H1 (`# Title`) in `body`, or null. */
|
|
142
|
+
function firstHeading(body) {
|
|
143
|
+
const m = body.match(/^#\s+(.+?)\s*$/m);
|
|
144
|
+
return m ? m[1].trim() : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Strip surrounding single/double quotes from a YAML scalar. */
|
|
148
|
+
function stripYamlScalar(value) {
|
|
149
|
+
const t = value.trim();
|
|
150
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
151
|
+
return t.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
return t;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Split a leading YAML front-matter block. Returns `{ inner, body }` where
|
|
158
|
+
* `inner` is the raw block content (no `---` fences) or null when absent.
|
|
159
|
+
*/
|
|
160
|
+
function splitFrontMatter(content) {
|
|
161
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
162
|
+
if (!m) return { inner: null, body: content };
|
|
163
|
+
return { inner: m[1], body: content.slice(m[0].length) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Set of top-level keys present in a raw front-matter block (lower-cased). */
|
|
167
|
+
function frontMatterKeys(inner) {
|
|
168
|
+
const keys = new Set();
|
|
169
|
+
for (const line of inner.split('\n')) {
|
|
170
|
+
const m = line.match(/^([A-Za-z0-9_-]+)\s*:/);
|
|
171
|
+
if (m) keys.add(m[1].toLowerCase());
|
|
172
|
+
}
|
|
173
|
+
return keys;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Display title for a concept: H1, else existing front-matter title, else filename. */
|
|
177
|
+
function conceptTitle(content, rel) {
|
|
178
|
+
const { inner, body } = splitFrontMatter(content);
|
|
179
|
+
const heading = firstHeading(body);
|
|
180
|
+
if (heading) return heading;
|
|
181
|
+
if (inner) {
|
|
182
|
+
const m = inner.match(/^title\s*:\s*(.+?)\s*$/m);
|
|
183
|
+
if (m) return stripYamlScalar(m[1]);
|
|
184
|
+
}
|
|
185
|
+
return path.posix.basename(rel).replace(/\.md$/i, '');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build the concept doc content: ensure a single front-matter block that carries
|
|
190
|
+
* at least `type` (+ `title`), preserving the original body and any pre-existing
|
|
191
|
+
* front matter (only MISSING required keys are appended — never duplicated).
|
|
192
|
+
*/
|
|
193
|
+
function buildConcept(content, rel, title) {
|
|
194
|
+
const { inner, body } = splitFrontMatter(content);
|
|
195
|
+
if (inner === null) {
|
|
196
|
+
const fm = `---\ntype: ${JSON.stringify(inferType(rel))}\ntitle: ${JSON.stringify(title)}\n---\n`;
|
|
197
|
+
// No injected blank line — preserve the body verbatim, matching the
|
|
198
|
+
// existing-front-matter branch below.
|
|
199
|
+
return `${fm}${content}`;
|
|
200
|
+
}
|
|
201
|
+
const keys = frontMatterKeys(inner);
|
|
202
|
+
const additions = [];
|
|
203
|
+
if (!keys.has('type')) additions.push(`type: ${JSON.stringify(inferType(rel))}`);
|
|
204
|
+
if (!keys.has('title')) additions.push(`title: ${JSON.stringify(title)}`);
|
|
205
|
+
const mergedInner = additions.length > 0 ? `${inner}\n${additions.join('\n')}` : inner;
|
|
206
|
+
return `---\n${mergedInner}\n---\n${body}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- index construction -------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/** Build a folder tree: `Map<dir, { files: string[], subdirs: Set<string> }>`. */
|
|
212
|
+
function buildTree(conceptRels) {
|
|
213
|
+
const folders = new Map();
|
|
214
|
+
const ensure = dir => {
|
|
215
|
+
if (!folders.has(dir)) folders.set(dir, { files: [], subdirs: new Set() });
|
|
216
|
+
return folders.get(dir);
|
|
217
|
+
};
|
|
218
|
+
ensure('');
|
|
219
|
+
for (const rel of conceptRels) {
|
|
220
|
+
const dirName = path.posix.dirname(rel);
|
|
221
|
+
const dir = dirName === '.' ? '' : dirName;
|
|
222
|
+
ensure(dir).files.push(rel);
|
|
223
|
+
let child = dir;
|
|
224
|
+
while (child !== '') {
|
|
225
|
+
const parentName = path.posix.dirname(child);
|
|
226
|
+
const parent = parentName === '.' ? '' : parentName;
|
|
227
|
+
ensure(parent).subdirs.add(child);
|
|
228
|
+
child = parent;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return folders;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Deterministic code-point order — localeCompare() varies by runtime/locale and
|
|
235
|
+
// would make the bundle ordering (indexes + concept list) non-reproducible.
|
|
236
|
+
const byCodePoint = (a, b) => (a < b ? -1 : Number(a > b));
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Render a folder's `index.md`. The bundle-root index (dir === '') is the ONLY
|
|
240
|
+
* one that carries front matter, for the single key `okf_version`.
|
|
241
|
+
*/
|
|
242
|
+
function renderIndex(dir, node, titles) {
|
|
243
|
+
const isRoot = dir === '';
|
|
244
|
+
const lines = [];
|
|
245
|
+
if (isRoot) lines.push('---', `okf_version: ${JSON.stringify(OKF_VERSION)}`, '---', '');
|
|
246
|
+
lines.push(`# ${isRoot ? 'Knowledge Base' : path.posix.basename(dir)}`, '');
|
|
247
|
+
for (const sub of [...node.subdirs].sort(byCodePoint)) {
|
|
248
|
+
const rel = path.posix.relative(dir, sub);
|
|
249
|
+
lines.push(`- [${path.posix.basename(sub)}/](${rel}/index.md)`);
|
|
250
|
+
}
|
|
251
|
+
for (const file of [...node.files].sort(byCodePoint)) {
|
|
252
|
+
const rel = path.posix.relative(dir, file);
|
|
253
|
+
lines.push(`- [${titles.get(file)}](${rel})`);
|
|
254
|
+
}
|
|
255
|
+
return `${lines.join('\n')}\n`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- symlink-safe writing -----------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/** Refuse if ANY existing path segment from `rootAbs` down to `targetAbs` is a
|
|
261
|
+
* symlink — otherwise a symlinked ANCESTOR could redirect writes outside the repo
|
|
262
|
+
* even though the final component isn't itself a link. */
|
|
263
|
+
function assertNoSymlinkedAncestor(targetAbs, rootAbs) {
|
|
264
|
+
const rel = path.relative(rootAbs, targetAbs);
|
|
265
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
266
|
+
throw new Error(`path escapes the repo root: ${targetAbs}`);
|
|
267
|
+
}
|
|
268
|
+
let cur = rootAbs;
|
|
269
|
+
for (const part of rel.split(path.sep).filter(seg => seg !== '')) {
|
|
270
|
+
cur = path.join(cur, part);
|
|
271
|
+
let seg = null;
|
|
272
|
+
try { seg = fs.lstatSync(cur); } catch { /* absent yet */ }
|
|
273
|
+
if (seg?.isSymbolicLink()) throw new Error(`refusing to write through a symlinked path segment: ${cur}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Prepare the out dir: refuse a symlinked ancestor OR target, create if missing, require a dir. */
|
|
278
|
+
function prepareOutRoot(outAbs, rootAbs) {
|
|
279
|
+
assertNoSymlinkedAncestor(outAbs, rootAbs);
|
|
280
|
+
let stat = null;
|
|
281
|
+
try { stat = fs.lstatSync(outAbs); } catch { /* absent: stat stays null */ }
|
|
282
|
+
if (stat === null) { fs.mkdirSync(outAbs, { recursive: true }); return; }
|
|
283
|
+
if (!stat.isDirectory()) throw new Error(`out path exists and is not a directory: ${outAbs}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Create the dir chain under `outAbs`, refusing to traverse a symlinked component. */
|
|
287
|
+
function ensureDirSafe(outAbs, dirAbs) {
|
|
288
|
+
const relParts = path.relative(outAbs, dirAbs).split(path.sep).filter(seg => seg !== '');
|
|
289
|
+
let cur = outAbs;
|
|
290
|
+
for (const part of relParts) {
|
|
291
|
+
cur = path.join(cur, part);
|
|
292
|
+
let stat = null;
|
|
293
|
+
try { stat = fs.lstatSync(cur); } catch { /* absent: stat stays null */ }
|
|
294
|
+
if (stat?.isSymbolicLink()) {
|
|
295
|
+
throw new Error(`refusing to write through a symlinked directory: ${cur}`);
|
|
296
|
+
}
|
|
297
|
+
if (stat === null) fs.mkdirSync(cur);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Write `content` to `outAbs/relPath`, symlink-safe and contained within `outAbs`. */
|
|
302
|
+
function writeFileSafe(outAbs, relPath, content) {
|
|
303
|
+
const targetAbs = path.resolve(outAbs, relPath);
|
|
304
|
+
const within = path.relative(outAbs, targetAbs);
|
|
305
|
+
if (within.startsWith('..') || path.isAbsolute(within)) {
|
|
306
|
+
throw new Error(`refusing to write outside the out dir: ${relPath}`);
|
|
307
|
+
}
|
|
308
|
+
ensureDirSafe(outAbs, path.dirname(targetAbs));
|
|
309
|
+
let stat = null;
|
|
310
|
+
try { stat = fs.lstatSync(targetAbs); } catch { /* absent: stat stays null */ }
|
|
311
|
+
if (stat?.isSymbolicLink()) {
|
|
312
|
+
throw new Error(`${relPath} is a symlink; refusing to write through it.`);
|
|
313
|
+
}
|
|
314
|
+
fs.writeFileSync(targetAbs, content);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- public API ---------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Generate an OKF bundle from a repo's tracked markdown.
|
|
321
|
+
*
|
|
322
|
+
* @param {object} opts
|
|
323
|
+
* @param {string} opts.root - Repository root (a git working tree).
|
|
324
|
+
* @param {string} [opts.source] - Source docs dir; auto-detected when omitted.
|
|
325
|
+
* @param {string} [opts.out='.okf'] - Bundle output dir (must not be the repo root).
|
|
326
|
+
* @returns {{ ok: boolean, error?: string, out?: string, source?: string,
|
|
327
|
+
* concepts?: string[], count?: number, okfVersion?: string }}
|
|
328
|
+
*/
|
|
329
|
+
function generateBundle({ root, source, out } = {}) {
|
|
330
|
+
if (typeof root !== 'string' || root.trim() === '') throw new Error('root must be a non-empty path');
|
|
331
|
+
const outRel = cleanRel(out ?? '.okf', 'out');
|
|
332
|
+
if (outRel === '') throw new Error('out dir must not be the repo root');
|
|
333
|
+
const sourceRel = source === undefined || source === null ? null : cleanRel(source, 'source');
|
|
334
|
+
|
|
335
|
+
const { base, files } = resolveSources(root, sourceRel, outRel);
|
|
336
|
+
if (files.length === 0) {
|
|
337
|
+
return { ok: false, error: 'No tracked markdown found to generate an OKF bundle from.' };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const outAbs = path.resolve(root, outRel);
|
|
341
|
+
prepareOutRoot(outAbs, path.resolve(root));
|
|
342
|
+
|
|
343
|
+
const concepts = [];
|
|
344
|
+
const titles = new Map();
|
|
345
|
+
for (const file of files) {
|
|
346
|
+
const rel = conceptRel(file, base);
|
|
347
|
+
const abs = path.join(root, file);
|
|
348
|
+
// `git ls-files` can list tracked .md SYMLINKS; readFileSync would follow them
|
|
349
|
+
// outside the repo. Refuse a symlinked source before reading (fail-closed).
|
|
350
|
+
let srcStat = null;
|
|
351
|
+
try { srcStat = fs.lstatSync(abs); } catch { /* a real read error surfaces below */ }
|
|
352
|
+
if (srcStat?.isSymbolicLink()) {
|
|
353
|
+
throw new Error(`refusing to read a symlinked source file (could escape the repo): ${file}`);
|
|
354
|
+
}
|
|
355
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
356
|
+
const title = conceptTitle(content, rel);
|
|
357
|
+
titles.set(rel, title);
|
|
358
|
+
writeFileSafe(outAbs, rel, buildConcept(content, rel, title));
|
|
359
|
+
concepts.push(rel);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const [dir, node] of buildTree(concepts)) {
|
|
363
|
+
const indexRel = dir === '' ? ROOT_INDEX : `${dir}/${ROOT_INDEX}`;
|
|
364
|
+
writeFileSafe(outAbs, indexRel, renderIndex(dir, node, titles));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
out: outRel,
|
|
370
|
+
source: base === '' ? '(tracked markdown)' : base,
|
|
371
|
+
concepts: concepts.sort(byCodePoint),
|
|
372
|
+
count: concepts.length,
|
|
373
|
+
okfVersion: OKF_VERSION,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Build the delimited AGENTS.md managed nav block pointing at the bundle root. */
|
|
378
|
+
function buildNavBlock(outRel) {
|
|
379
|
+
const indexRel = `${outRel}/${ROOT_INDEX}`;
|
|
380
|
+
return [
|
|
381
|
+
NAV_BEGIN,
|
|
382
|
+
'## Knowledge Base (OKF)',
|
|
383
|
+
'',
|
|
384
|
+
'This repository maintains an Open Knowledge Format (OKF v0.1 draft) bundle.',
|
|
385
|
+
`Start at the bundle index: [\`${indexRel}\`](${indexRel}).`,
|
|
386
|
+
'',
|
|
387
|
+
'This is a thin navigation overlay; the bundle itself is the source of truth.',
|
|
388
|
+
NAV_END,
|
|
389
|
+
].join('\n');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Insert or replace the managed nav block, preserving surrounding content. */
|
|
393
|
+
function upsertNavBlock(existing, block) {
|
|
394
|
+
const start = existing.indexOf(NAV_BEGIN);
|
|
395
|
+
const end = existing.indexOf(NAV_END);
|
|
396
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
397
|
+
return `${existing.slice(0, start)}${block}${existing.slice(end + NAV_END.length)}`;
|
|
398
|
+
}
|
|
399
|
+
if (existing === '') return `${block}\n`;
|
|
400
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
401
|
+
return `${existing}${sep}${block}\n`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Write/update a thin OKF navigation section in AGENTS.md (NOT CLAUDE.md) that
|
|
406
|
+
* POINTS AT the bundle root index. Idempotent via a delimited managed block.
|
|
407
|
+
*
|
|
408
|
+
* @param {object} opts
|
|
409
|
+
* @param {string} opts.root - Repository root.
|
|
410
|
+
* @param {string} [opts.out='.okf'] - Bundle dir the nav should point at.
|
|
411
|
+
* @returns {{ ok: boolean, path: string, target: string, created: boolean }}
|
|
412
|
+
*/
|
|
413
|
+
function linkAgentsMd({ root, out } = {}) {
|
|
414
|
+
if (typeof root !== 'string' || root.trim() === '') throw new Error('root must be a non-empty path');
|
|
415
|
+
const outRel = cleanRel(out ?? '.okf', 'out');
|
|
416
|
+
if (outRel === '') throw new Error('out dir must not be the repo root');
|
|
417
|
+
|
|
418
|
+
const agentsAbs = path.join(root, 'AGENTS.md');
|
|
419
|
+
let stat = null;
|
|
420
|
+
try { stat = fs.lstatSync(agentsAbs); } catch { /* absent: stat stays null */ }
|
|
421
|
+
if (stat?.isSymbolicLink()) {
|
|
422
|
+
throw new Error('AGENTS.md is a symlink; refusing to write through it.');
|
|
423
|
+
}
|
|
424
|
+
const existing = stat === null ? '' : fs.readFileSync(agentsAbs, 'utf8');
|
|
425
|
+
fs.writeFileSync(agentsAbs, upsertNavBlock(existing, buildNavBlock(outRel)));
|
|
426
|
+
return { ok: true, path: 'AGENTS.md', target: `${outRel}/${ROOT_INDEX}`, created: stat === null };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = { generateBundle, linkAgentsMd, OKF_VERSION };
|