forge-workflow 0.0.9 → 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 +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- 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 +466 -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/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- 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/reference/TOOLCHAIN.md +658 -0
- 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 +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- 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 +66 -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 +22 -2
- 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 +851 -979
- 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 +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- 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 +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- 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 +697 -0
- 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/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -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 +8 -4
- 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 +166 -0
- 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 +332 -23
- 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/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- 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 +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- 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 +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- 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-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- 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 +144 -47
- 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 +183 -79
- 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/beads-migrate-to-dolt.mjs +503 -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 +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- 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-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- 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} +46 -52
- 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} +134 -159
- 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/skills/ship/SKILL.md +251 -0
- 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/skills/status/SKILL.md +102 -0
- 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/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- 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 -255
- 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-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- 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 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- 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,387 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module memory/router
|
|
5
|
+
*
|
|
6
|
+
* Single dispatch seam for `forge remember` / `forge recall`. The DEFAULT is
|
|
7
|
+
* `local`, which now means the kernel `kernel_memories` table indexed by FTS5
|
|
8
|
+
* (via `lib/project-memory.js`) — the same knowledge layer decisions and issues
|
|
9
|
+
* share. The router exists so the opt-in `graphiti` knowledge-graph tier can slot
|
|
10
|
+
* in behind the same CLI verbs WITHOUT changing the clean default path.
|
|
11
|
+
*
|
|
12
|
+
* Public config surface is deliberately `local | graphiti` only. `graphiti` is
|
|
13
|
+
* EXPERIMENTAL — its config/doctor scaffolding ships but the runtime emitter is a
|
|
14
|
+
* fast-follow, so today it always writes the local kernel floor and the emit is a
|
|
15
|
+
* best-effort no-op unless a caller injects an emitter.
|
|
16
|
+
*
|
|
17
|
+
* Design: docs/work/2026-07-09-decision-store/design.md §B.1 (memory consolidation
|
|
18
|
+
* onto the kernel + FTS5). The retired flat JSONL store (`lib/memory-store.js`) is
|
|
19
|
+
* imported once into kernel_memories on first use, then never written again.
|
|
20
|
+
*
|
|
21
|
+
* Hard rule: `remember`/`recall` must NEVER hang or fail. Under `graphiti`, the
|
|
22
|
+
* emit is FIRE-AND-FORGET with a HARD FALLBACK to the local kernel store on any
|
|
23
|
+
* error/timeout — a down sidecar can never strand a note. The local kernel write
|
|
24
|
+
* is the floor and always happens. Strict validation (`assertMemoryConfigValid`)
|
|
25
|
+
* is a separate, explicit gate for tooling (e.g. `forge doctor`).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const crypto = require('node:crypto');
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const projectMemory = require('../project-memory');
|
|
32
|
+
|
|
33
|
+
/** Supported PUBLIC backends, default first. */
|
|
34
|
+
const MEMORY_BACKENDS = ['local', 'graphiti'];
|
|
35
|
+
const DEFAULT_MEMORY_BACKEND = 'local';
|
|
36
|
+
const ENV_VAR = 'FORGE_MEMORY_BACKEND';
|
|
37
|
+
|
|
38
|
+
/** Default recall cap — newest-N, so `recall` with no query never dumps the whole store. */
|
|
39
|
+
const DEFAULT_RECALL_LIMIT = 20;
|
|
40
|
+
/** sourceAgent stamped on CLI `remember` notes (distinct from insights-written rows). */
|
|
41
|
+
const REMEMBER_SOURCE_AGENT = 'forge remember';
|
|
42
|
+
/** sourceAgent stamped on notes imported once from the retired JSONL store. */
|
|
43
|
+
const IMPORT_SOURCE_AGENT = 'forge remember (imported)';
|
|
44
|
+
/**
|
|
45
|
+
* The source_agents that are human `remember` notes — the scope of the DEFAULT (no-query)
|
|
46
|
+
* `recall` view, so machine/insights records never pollute or miscount the plain listing.
|
|
47
|
+
* A query, or `--all`, still reaches every stored memory.
|
|
48
|
+
*/
|
|
49
|
+
const HUMAN_MEMORY_AGENTS = [REMEMBER_SOURCE_AGENT, IMPORT_SOURCE_AGENT];
|
|
50
|
+
/** The retired flat JSONL store, imported once into kernel_memories, then renamed. */
|
|
51
|
+
const LEGACY_JSONL_RELATIVE = ['.forge', 'memory', 'notes.jsonl'];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Safely read the `memory` block from `<projectRoot>/.forge/config.yaml`.
|
|
55
|
+
* Never throws — a missing or malformed file resolves to `{}` so the default
|
|
56
|
+
* (local) path is byte-identical to shipping no config at all.
|
|
57
|
+
*
|
|
58
|
+
* @param {string|undefined} projectRoot
|
|
59
|
+
* @returns {object} The parsed `memory` object, or `{}`.
|
|
60
|
+
*/
|
|
61
|
+
function readMemoryConfig(projectRoot) {
|
|
62
|
+
if (!projectRoot) return {};
|
|
63
|
+
|
|
64
|
+
const fs = require('node:fs');
|
|
65
|
+
const path = require('node:path');
|
|
66
|
+
const configPath = path.join(projectRoot, '.forge', 'config.yaml');
|
|
67
|
+
if (!fs.existsSync(configPath)) return {};
|
|
68
|
+
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
// Lazy-require keeps the default no-config path free of the YAML parser.
|
|
72
|
+
const YAML = require('yaml');
|
|
73
|
+
parsed = YAML.parse(fs.readFileSync(configPath, 'utf8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
79
|
+
const memory = parsed.memory;
|
|
80
|
+
if (!memory || typeof memory !== 'object' || Array.isArray(memory)) return {};
|
|
81
|
+
return memory;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gather the raw backend signal by precedence (deps > env > config), WITHOUT
|
|
86
|
+
* validation. Returns `{ value, source }` or `{ value: null, source: null }`.
|
|
87
|
+
*/
|
|
88
|
+
function collectBackendSignal({ deps = {}, env = process.env, projectRoot, config } = {}) {
|
|
89
|
+
if (typeof deps.memoryBackend === 'string' && deps.memoryBackend.trim()) {
|
|
90
|
+
return { value: deps.memoryBackend.trim(), source: 'deps' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const envValue = env && env[ENV_VAR];
|
|
94
|
+
if (typeof envValue === 'string' && envValue.trim()) {
|
|
95
|
+
return { value: envValue.trim(), source: 'env' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const memory = config || readMemoryConfig(projectRoot);
|
|
99
|
+
const configValue = memory && memory.backend;
|
|
100
|
+
if (typeof configValue === 'string' && configValue.trim()) {
|
|
101
|
+
return { value: configValue.trim(), source: 'config' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { value: null, source: null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the active memory backend by precedence:
|
|
109
|
+
* deps.memoryBackend > FORGE_MEMORY_BACKEND env > .forge/config.yaml > 'local'.
|
|
110
|
+
*
|
|
111
|
+
* An UNKNOWN value (from any source) warns and falls back to `local` so a typo
|
|
112
|
+
* — or a legacy `kernel` value — can never break `remember`/`recall`. Use
|
|
113
|
+
* `assertMemoryConfigValid` when you need a hard error instead.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} [options]
|
|
116
|
+
* @returns {'local'|'graphiti'}
|
|
117
|
+
*/
|
|
118
|
+
function resolveMemoryBackend({
|
|
119
|
+
deps = {},
|
|
120
|
+
env = process.env,
|
|
121
|
+
projectRoot,
|
|
122
|
+
config,
|
|
123
|
+
warn = console.warn,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const { value, source } = collectBackendSignal({ deps, env, projectRoot, config });
|
|
126
|
+
if (!value) return DEFAULT_MEMORY_BACKEND;
|
|
127
|
+
|
|
128
|
+
const normalized = value.toLowerCase();
|
|
129
|
+
if (MEMORY_BACKENDS.includes(normalized)) return normalized;
|
|
130
|
+
|
|
131
|
+
warn(
|
|
132
|
+
`Unknown memory backend "${value}" from ${source}; `
|
|
133
|
+
+ `falling back to "${DEFAULT_MEMORY_BACKEND}". Valid backends: ${MEMORY_BACKENDS.join(', ')}.`,
|
|
134
|
+
);
|
|
135
|
+
return DEFAULT_MEMORY_BACKEND;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Strict validation for the resolved backend. Unlike `resolveMemoryBackend`
|
|
140
|
+
* (which soft-falls-back), this THROWS a clear, actionable error when the
|
|
141
|
+
* selection is inconsistent. Used by tooling (e.g. `forge doctor`).
|
|
142
|
+
*
|
|
143
|
+
* @param {object} [options]
|
|
144
|
+
* @returns {{ backend: string, graphiti: object|null }}
|
|
145
|
+
*/
|
|
146
|
+
function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, config } = {}) {
|
|
147
|
+
const memory = config || readMemoryConfig(projectRoot);
|
|
148
|
+
const backend = resolveMemoryBackend({ deps, env, projectRoot, config: memory, warn: () => {} });
|
|
149
|
+
|
|
150
|
+
if (backend !== 'graphiti') {
|
|
151
|
+
return { backend, graphiti: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const graphiti = memory && memory.graphiti;
|
|
155
|
+
const hasServerPath = graphiti
|
|
156
|
+
&& typeof graphiti.mcpServerPath === 'string'
|
|
157
|
+
&& graphiti.mcpServerPath.trim() !== '';
|
|
158
|
+
if (!hasServerPath) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'memory.backend is "graphiti" but memory.graphiti.mcpServerPath is not set. '
|
|
161
|
+
+ 'Configure the Graphiti MCP server path (the checkout\'s mcp_server directory) '
|
|
162
|
+
+ 'in .forge/config.yaml. See docs/guides/memory-backends.md. '
|
|
163
|
+
+ 'The local store stays the default and the safety floor when unset.',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return { backend, graphiti };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Best-effort, fire-and-forget emit of an episode to the graph backend. This is
|
|
171
|
+
* a SEAM: the actual Graphiti MCP client lands in a fast-follow PR. It NEVER
|
|
172
|
+
* throws and NEVER blocks the caller — any error/timeout is swallowed so the
|
|
173
|
+
* local floor write is the only thing that can affect `remember`'s result.
|
|
174
|
+
*
|
|
175
|
+
* CONTRACT for the fast-follow emitter (MUST hold — safe today only because no
|
|
176
|
+
* emitter is constructed): the emitter MUST NOT keep the Node event loop alive
|
|
177
|
+
* or delay CLI exit. Any spawned process/socket/timer it creates MUST be
|
|
178
|
+
* `child.unref()`'d (or otherwise detached / left with no lingering handle), and
|
|
179
|
+
* any network/RPC call MUST be bounded by its OWN timeout so a hung sidecar can
|
|
180
|
+
* never stall `forge remember`. This function does NOT await the emit, so a
|
|
181
|
+
* lingering handle inside `emit()` would be the ONLY way to break the
|
|
182
|
+
* never-hang guarantee — the emitter, not this seam, owns preventing that.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} [emitter] - optional `{ emit(entry) }` injected by callers.
|
|
185
|
+
* @param {object} entry - the persisted local entry.
|
|
186
|
+
*/
|
|
187
|
+
function fireAndForgetGraphitiEmit(emitter, entry) {
|
|
188
|
+
if (!emitter || typeof emitter.emit !== 'function') return;
|
|
189
|
+
try {
|
|
190
|
+
// Do not await: fire-and-forget. If it returns a promise, swallow rejection.
|
|
191
|
+
const maybePromise = emitter.emit(entry);
|
|
192
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
193
|
+
maybePromise.then(() => {}, () => {});
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Hard fallback: the local write already succeeded. Never surface emit errors.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Render a non-string memory value (e.g. an insights skill record) as a compact, READABLE
|
|
202
|
+
* one-liner rather than a raw JSON blob. Flattens the top level: primitive fields become
|
|
203
|
+
* `key: value`; nested fields fall back to compact JSON.
|
|
204
|
+
*
|
|
205
|
+
* @param {*} value
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function renderStructuredValue(value) {
|
|
209
|
+
if (value === null || value === undefined) return String(value);
|
|
210
|
+
if (typeof value !== 'object') return String(value);
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
return value.map(item => (item !== null && typeof item === 'object' ? JSON.stringify(item) : String(item))).join(', ');
|
|
213
|
+
}
|
|
214
|
+
return Object.entries(value)
|
|
215
|
+
.map(([key, val]) => (val !== null && typeof val === 'object' ? `${key}: ${JSON.stringify(val)}` : `${key}: ${val}`))
|
|
216
|
+
.join(' · ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Map a kernel_memories entry to the CLI note shape `remember`/`recall` render. A string
|
|
221
|
+
* value IS the note text; a structured value (an insights/typed record) is rendered readably
|
|
222
|
+
* and flagged `machine` with its `sourceAgent` so the CLI can LABEL it rather than mislabel a
|
|
223
|
+
* raw JSON blob as a plain note.
|
|
224
|
+
*
|
|
225
|
+
* @param {object} entry - a kernel_memories entry ({ key, value, sourceAgent, timestamp, tags }).
|
|
226
|
+
* @returns {{ id: string, note: string, sourceAgent: string, machine: boolean, timestamp: string, tags: string[] }}
|
|
227
|
+
*/
|
|
228
|
+
function toNote(entry) {
|
|
229
|
+
if (!entry) return null;
|
|
230
|
+
const isString = typeof entry.value === 'string';
|
|
231
|
+
return {
|
|
232
|
+
id: entry.key,
|
|
233
|
+
note: isString ? entry.value : renderStructuredValue(entry.value),
|
|
234
|
+
sourceAgent: typeof entry.sourceAgent === 'string' ? entry.sourceAgent : '',
|
|
235
|
+
machine: !isString,
|
|
236
|
+
timestamp: typeof entry.timestamp === 'string' ? entry.timestamp : '',
|
|
237
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function legacyJsonlPath(projectRoot) {
|
|
242
|
+
return path.join(projectRoot, ...LEGACY_JSONL_RELATIVE);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// A STABLE key for a legacy record that lacks an `id`, derived from its content — so a
|
|
246
|
+
// re-run (or a failed rename) upserts the same row instead of double-inserting under a
|
|
247
|
+
// fresh random UUID.
|
|
248
|
+
function legacyContentKey(parsed) {
|
|
249
|
+
const basis = JSON.stringify({
|
|
250
|
+
note: parsed.note,
|
|
251
|
+
timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : '',
|
|
252
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.filter(tag => typeof tag === 'string') : [],
|
|
253
|
+
});
|
|
254
|
+
return `import:${crypto.createHash('sha256').update(basis).digest('hex').slice(0, 32)}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* One-time import of the retired flat JSONL store into kernel_memories. Idempotent (keys
|
|
259
|
+
* are the original note ids, so a re-run upserts) and best-effort (a malformed record or a
|
|
260
|
+
* write error can never break `remember`/`recall`). After a pass the file is renamed so the
|
|
261
|
+
* import runs at most once and JSONL is never read or written again.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} projectRoot
|
|
264
|
+
* @param {object} [options] - forwarded to `projectMemory.write` (e.g. an injected store).
|
|
265
|
+
*/
|
|
266
|
+
function migrateJsonlNotesOnce(projectRoot, options = {}) {
|
|
267
|
+
if (!projectRoot) return;
|
|
268
|
+
const storePath = legacyJsonlPath(projectRoot);
|
|
269
|
+
let raw;
|
|
270
|
+
try {
|
|
271
|
+
if (!fs.existsSync(storePath)) return;
|
|
272
|
+
raw = fs.readFileSync(storePath, 'utf8');
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (!trimmed) continue;
|
|
280
|
+
let parsed;
|
|
281
|
+
try {
|
|
282
|
+
parsed = JSON.parse(trimmed);
|
|
283
|
+
} catch {
|
|
284
|
+
continue; // Skip an unparseable legacy line — one bad record can't block the import.
|
|
285
|
+
}
|
|
286
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.note !== 'string' || parsed.note.trim() === '') {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const entry = {
|
|
290
|
+
// A record with a stable id keys off it; one without keys off its content hash, so a
|
|
291
|
+
// re-import can never double-insert the same note.
|
|
292
|
+
key: typeof parsed.id === 'string' && parsed.id ? parsed.id : legacyContentKey(parsed),
|
|
293
|
+
value: parsed.note,
|
|
294
|
+
sourceAgent: IMPORT_SOURCE_AGENT,
|
|
295
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.filter(tag => typeof tag === 'string') : [],
|
|
296
|
+
};
|
|
297
|
+
if (typeof parsed.timestamp === 'string' && parsed.timestamp && !Number.isNaN(Date.parse(parsed.timestamp))) {
|
|
298
|
+
entry.timestamp = parsed.timestamp;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
projectMemory.write(projectRoot, entry, options);
|
|
302
|
+
} catch {
|
|
303
|
+
// Skip an unwritable legacy record; never break remember/recall on import.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
fs.renameSync(storePath, `${storePath}.migrated`);
|
|
309
|
+
} catch {
|
|
310
|
+
// Best-effort: the import is idempotent by key, so a failed rename re-imports safely.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Append a note through kernel_memories — the local floor for EVERY backend, always written
|
|
316
|
+
* first so a note is durably captured. `graphiti` additionally fires a best-effort,
|
|
317
|
+
* non-blocking emit toward the graph backend. Imports any retired JSONL store on first use.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} projectRoot
|
|
320
|
+
* @param {string} note
|
|
321
|
+
* @param {object} [options] - { tags, deps, env, config, graphitiEmitter, store }
|
|
322
|
+
* @returns {{ id: string, note: string, timestamp: string, tags: string[] }}
|
|
323
|
+
*/
|
|
324
|
+
function append(projectRoot, note, options = {}) {
|
|
325
|
+
migrateJsonlNotesOnce(projectRoot, options);
|
|
326
|
+
const backend = resolveMemoryBackend({ ...options, projectRoot });
|
|
327
|
+
const written = projectMemory.write(projectRoot, {
|
|
328
|
+
key: crypto.randomUUID(),
|
|
329
|
+
value: note,
|
|
330
|
+
sourceAgent: REMEMBER_SOURCE_AGENT,
|
|
331
|
+
tags: Array.isArray(options.tags) ? options.tags : [],
|
|
332
|
+
}, options);
|
|
333
|
+
const entry = toNote(written);
|
|
334
|
+
if (backend === 'graphiti') {
|
|
335
|
+
fireAndForgetGraphitiEmit(options.graphitiEmitter, entry);
|
|
336
|
+
}
|
|
337
|
+
return entry;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Read notes back through the FTS-backed kernel layer. WITH a query: BM25 token-AND top-N.
|
|
342
|
+
* WITHOUT a query: the newest `limit` entries plus the total count (never a bare full dump).
|
|
343
|
+
* Imports any retired JSONL store on first use.
|
|
344
|
+
*
|
|
345
|
+
* A query searches the WHOLE store (human notes + insights/typed records), so anything the
|
|
346
|
+
* kernel knows is recallable. The default no-query view is scoped to human `remember` notes
|
|
347
|
+
* so machine/insights records never pollute or miscount the plain listing; `selection.all`
|
|
348
|
+
* widens it to every stored memory.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} projectRoot
|
|
351
|
+
* @param {object} [selection] - { query, limit, all }
|
|
352
|
+
* @param {object} [options] - forwarded to the project-memory read paths (e.g. a store).
|
|
353
|
+
* @returns {{ notes: object[], total: number, capped: boolean, query: string, limit: number, scope: string }}
|
|
354
|
+
*/
|
|
355
|
+
function recall(projectRoot, selection = {}, options = {}) {
|
|
356
|
+
migrateJsonlNotesOnce(projectRoot, options);
|
|
357
|
+
const requested = selection.limit;
|
|
358
|
+
const limit = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_RECALL_LIMIT;
|
|
359
|
+
const query = String(selection.query || '').trim();
|
|
360
|
+
const includeAll = Boolean(selection.all);
|
|
361
|
+
|
|
362
|
+
if (query) {
|
|
363
|
+
const notes = projectMemory.searchRanked(projectRoot, query, limit, options).map(toNote);
|
|
364
|
+
// BM25 returns at most `limit`; a full result set signals there may be more.
|
|
365
|
+
return { notes, total: notes.length, capped: notes.length >= limit, query, limit, scope: 'all' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
|
|
369
|
+
const readOptions = { ...options, agents };
|
|
370
|
+
const notes = projectMemory.recent(projectRoot, limit, readOptions).map(toNote);
|
|
371
|
+
const total = projectMemory.count(projectRoot, readOptions);
|
|
372
|
+
return { notes, total, capped: total > notes.length, query: '', limit, scope: includeAll ? 'all' : 'remembered' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
MEMORY_BACKENDS,
|
|
377
|
+
DEFAULT_MEMORY_BACKEND,
|
|
378
|
+
DEFAULT_RECALL_LIMIT,
|
|
379
|
+
ENV_VAR,
|
|
380
|
+
readMemoryConfig,
|
|
381
|
+
resolveMemoryBackend,
|
|
382
|
+
assertMemoryConfigValid,
|
|
383
|
+
migrateJsonlNotesOnce,
|
|
384
|
+
toNote,
|
|
385
|
+
append,
|
|
386
|
+
recall,
|
|
387
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const projectMemory = require('../project-memory');
|
|
2
|
+
|
|
3
|
+
const CATEGORIES = new Set([
|
|
4
|
+
'decisions',
|
|
5
|
+
'episodes',
|
|
6
|
+
'skills',
|
|
7
|
+
'state',
|
|
8
|
+
'issues',
|
|
9
|
+
'audit',
|
|
10
|
+
'preferences',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function assertCategory(category) {
|
|
14
|
+
if (!CATEGORIES.has(category)) {
|
|
15
|
+
throw new Error(`Unknown memory category: ${category}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertProvenance(provenance) {
|
|
20
|
+
if (!provenance || typeof provenance !== 'object') {
|
|
21
|
+
throw new TypeError('typed memory provenance is required');
|
|
22
|
+
}
|
|
23
|
+
for (const field of ['actor', 'reason', 'source']) {
|
|
24
|
+
if (typeof provenance[field] !== 'string' || provenance[field].trim() === '') {
|
|
25
|
+
throw new TypeError(`typed memory provenance.${field} is required`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function keyFor(category, key) {
|
|
31
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
32
|
+
throw new TypeError('typed memory key is required');
|
|
33
|
+
}
|
|
34
|
+
return `${category}:${key.trim()}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function adapter(options = {}) {
|
|
38
|
+
return options.memory ?? projectMemory;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stringArrayOption(value, fieldName) {
|
|
42
|
+
if (value === undefined) return undefined;
|
|
43
|
+
const values = Array.isArray(value) ? value : [value];
|
|
44
|
+
if (values.some(item => typeof item !== 'string')) {
|
|
45
|
+
throw new TypeError(`typed memory ${fieldName} must contain only strings`);
|
|
46
|
+
}
|
|
47
|
+
return values.map(item => item.trim()).filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeTyped(projectRoot, category, key, data, options = {}) {
|
|
51
|
+
assertCategory(category);
|
|
52
|
+
assertProvenance(options.provenance);
|
|
53
|
+
|
|
54
|
+
const provenance = {
|
|
55
|
+
actor: options.provenance.actor.trim(),
|
|
56
|
+
reason: options.provenance.reason.trim(),
|
|
57
|
+
source: options.provenance.source.trim(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return adapter(options).write(projectRoot, {
|
|
61
|
+
key: keyFor(category, key),
|
|
62
|
+
value: {
|
|
63
|
+
category,
|
|
64
|
+
data,
|
|
65
|
+
provenance,
|
|
66
|
+
},
|
|
67
|
+
sourceAgent: provenance.actor,
|
|
68
|
+
tags: [category, ...(stringArrayOption(options.tags, 'tags') ?? [])],
|
|
69
|
+
beadsRefs: stringArrayOption(options.beadsRefs, 'beadsRefs'),
|
|
70
|
+
}, options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readTyped(projectRoot, category, key, options = {}) {
|
|
74
|
+
assertCategory(category);
|
|
75
|
+
return adapter(options).read(projectRoot, keyFor(category, key), options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function searchTyped(projectRoot, category, query, options = {}) {
|
|
79
|
+
assertCategory(category);
|
|
80
|
+
const prefix = `${category}:`;
|
|
81
|
+
const results = adapter(options).search(projectRoot, `${category} ${query ?? ''}`.trim(), options) ?? [];
|
|
82
|
+
if (!Array.isArray(results)) return [];
|
|
83
|
+
return results.filter(entry => typeof entry?.key === 'string' && entry.key.startsWith(prefix));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function categoryWriter(category) {
|
|
87
|
+
return (projectRoot, key, data, options = {}) => writeTyped(projectRoot, category, key, data, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
CATEGORIES: [...CATEGORIES],
|
|
92
|
+
writeTyped,
|
|
93
|
+
readTyped,
|
|
94
|
+
searchTyped,
|
|
95
|
+
writeDecision: categoryWriter('decisions'),
|
|
96
|
+
writeEpisode: categoryWriter('episodes'),
|
|
97
|
+
writeSkill: categoryWriter('skills'),
|
|
98
|
+
writeState: categoryWriter('state'),
|
|
99
|
+
writeIssue: categoryWriter('issues'),
|
|
100
|
+
writeAudit: categoryWriter('audit'),
|
|
101
|
+
writePreference: categoryWriter('preferences'),
|
|
102
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module memory-digest
|
|
5
|
+
*
|
|
6
|
+
* Builds the BOUNDED, token-capped memory digest that Forge PUSHES to an agent at
|
|
7
|
+
* session start (the `memory-inject` context intent in the hook contract). This is
|
|
8
|
+
* the missing "push" half of Forge memory: today an agent only sees remembered
|
|
9
|
+
* notes if it TYPES `forge recall`, so memory is effectively orphaned.
|
|
10
|
+
*
|
|
11
|
+
* Two layers, kept separate for testability:
|
|
12
|
+
* - collectDigestData(projectRoot, opts) — BEST-EFFORT fetch (each source wrapped;
|
|
13
|
+
* a failure yields [] for that source). Fetchers are injectable so tests never
|
|
14
|
+
* touch a real DB. Async (issue reads are async).
|
|
15
|
+
* - buildMemoryDigest(data, { budgetTokens }) — PURE formatting + token-capping via
|
|
16
|
+
* orientation's applyBudget. Empty data → empty digest (the caller then injects
|
|
17
|
+
* nothing). Never exceeds the budget.
|
|
18
|
+
*
|
|
19
|
+
* The digest is a small NUDGE, not a manual: the default budget is deliberately tiny.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { applyBudget, buildSection, estimateTokens } = require('./orientation');
|
|
23
|
+
const { fenceUntrusted } = require('./untrusted-content');
|
|
24
|
+
const { collectInbox, inboxSection } = require('./inbox');
|
|
25
|
+
|
|
26
|
+
const DEFAULT_DIGEST_BUDGET_TOKENS = 400;
|
|
27
|
+
const DEFAULT_NOTE_LIMIT = 5;
|
|
28
|
+
const DEFAULT_ISSUE_LIMIT = 5;
|
|
29
|
+
const DIGEST_HEADER = 'Forge memory (auto-injected at session start):';
|
|
30
|
+
|
|
31
|
+
/** Run an async producer, returning `fallback` on any throw/rejection (never propagates). */
|
|
32
|
+
async function safe(producer, fallback) {
|
|
33
|
+
try {
|
|
34
|
+
const value = await producer();
|
|
35
|
+
return value === undefined || value === null ? fallback : value;
|
|
36
|
+
} catch {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Default note fetch: newest remembered notes via the kernel-backed memory router. */
|
|
42
|
+
function defaultFetchNotes(projectRoot, opts = {}) {
|
|
43
|
+
const memoryRouter = require('./memory/router');
|
|
44
|
+
const result = memoryRouter.recall(projectRoot, { limit: opts.noteLimit || DEFAULT_NOTE_LIMIT });
|
|
45
|
+
return Array.isArray(result && result.notes) ? result.notes : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Pull an issues array out of a runIssueOperation result, defensively (shape varies). */
|
|
49
|
+
function extractIssues(result) {
|
|
50
|
+
let payload = result && result.data;
|
|
51
|
+
if (!payload && result && typeof result.output === 'string') {
|
|
52
|
+
try { payload = JSON.parse(result.output); } catch { return []; }
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(payload)) return payload;
|
|
55
|
+
if (payload && Array.isArray(payload.issues)) return payload.issues;
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default issue fetch for a status kind ('ready' | 'in_progress'). Best-effort.
|
|
61
|
+
* The CLI `--limit` is NOT trusted (`forge issue ready --json --limit 2` empirically
|
|
62
|
+
* returns the whole set), so the result is HARD-CAPPED with `.slice(0, limit)` — else
|
|
63
|
+
* the digest dumps every ready issue and applyBudget truncates the claimed tail away.
|
|
64
|
+
* `opts.runIssueOperation` is injectable so tests exercise the cap deterministically.
|
|
65
|
+
*/
|
|
66
|
+
async function defaultFetchIssues(projectRoot, kind, opts = {}) {
|
|
67
|
+
const runIssueOperation = opts.runIssueOperation || require('./forge-issues').runIssueOperation;
|
|
68
|
+
const limit = opts.issueLimit || DEFAULT_ISSUE_LIMIT;
|
|
69
|
+
const [operation, args] = kind === 'ready'
|
|
70
|
+
? ['ready', ['--json', '--limit', String(limit)]]
|
|
71
|
+
: ['list', ['--status', 'in_progress', '--json', '--limit', String(limit)]];
|
|
72
|
+
const result = await runIssueOperation(operation, args, projectRoot);
|
|
73
|
+
return extractIssues(result).slice(0, limit);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Default inbox fetch: pending targeted dashboard instruction comments (fail-open). */
|
|
77
|
+
function defaultFetchInbox(projectRoot, opts = {}) {
|
|
78
|
+
return collectInbox(projectRoot, opts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Best-effort gather of the digest inputs. Each source degrades to [] independently.
|
|
83
|
+
* @param {string} projectRoot
|
|
84
|
+
* @param {object} [opts] - { fetchNotes, fetchIssues, fetchInbox, noteLimit, issueLimit }
|
|
85
|
+
* @returns {Promise<{ notes: object[], ready: object[], claimed: object[], inbox: object[] }>}
|
|
86
|
+
*/
|
|
87
|
+
async function collectDigestData(projectRoot, opts = {}) {
|
|
88
|
+
const fetchNotes = opts.fetchNotes || defaultFetchNotes;
|
|
89
|
+
const fetchIssues = opts.fetchIssues || defaultFetchIssues;
|
|
90
|
+
const fetchInbox = opts.fetchInbox || defaultFetchInbox;
|
|
91
|
+
const notes = await safe(() => fetchNotes(projectRoot, opts), []);
|
|
92
|
+
const ready = await safe(() => fetchIssues(projectRoot, 'ready', opts), []);
|
|
93
|
+
const claimed = await safe(() => fetchIssues(projectRoot, 'in_progress', opts), []);
|
|
94
|
+
const inbox = await safe(() => fetchInbox(projectRoot, opts), []);
|
|
95
|
+
return {
|
|
96
|
+
notes: Array.isArray(notes) ? notes : [],
|
|
97
|
+
ready: Array.isArray(ready) ? ready : [],
|
|
98
|
+
claimed: Array.isArray(claimed) ? claimed : [],
|
|
99
|
+
inbox: Array.isArray(inbox) ? inbox : [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `- [date ]note` for a recall note. */
|
|
104
|
+
function formatNoteLine(note) {
|
|
105
|
+
const date = typeof note.timestamp === 'string' && note.timestamp ? `${note.timestamp.slice(0, 10)} ` : '';
|
|
106
|
+
return `- ${date}${note.note}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `- [label] title` for an issue row (title/id defensively resolved). */
|
|
110
|
+
function formatIssueLine(label, issue) {
|
|
111
|
+
const title = (issue && (issue.title || issue.id)) || 'untitled';
|
|
112
|
+
return `- [${label}] ${title}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Build the notes section, or null when there are no notes. */
|
|
116
|
+
function notesSection(notes) {
|
|
117
|
+
if (!notes.length) return null;
|
|
118
|
+
return buildSection({
|
|
119
|
+
id: 'digest_notes',
|
|
120
|
+
title: 'Remembered notes',
|
|
121
|
+
content: notes.map(formatNoteLine).join('\n'),
|
|
122
|
+
priority: 10,
|
|
123
|
+
preserve: false,
|
|
124
|
+
// Untrusted: a planted note is DATA, not instructions. Fenced after truncation.
|
|
125
|
+
untrustedSource: 'memory',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the open-issues section, or null when both are empty. CLAIMED lines come FIRST
|
|
131
|
+
* so that when applyBudget truncates the tail, it is the (less critical) ready list that
|
|
132
|
+
* is cut — the agent's own in-progress work must never be the vanished tail.
|
|
133
|
+
*/
|
|
134
|
+
function issuesSection(ready, claimed) {
|
|
135
|
+
const lines = [
|
|
136
|
+
...claimed.map(issue => formatIssueLine('claimed', issue)),
|
|
137
|
+
...ready.map(issue => formatIssueLine('ready', issue)),
|
|
138
|
+
];
|
|
139
|
+
if (!lines.length) return null;
|
|
140
|
+
return buildSection({
|
|
141
|
+
id: 'digest_issues',
|
|
142
|
+
title: 'Open issues',
|
|
143
|
+
content: lines.join('\n'),
|
|
144
|
+
priority: 20,
|
|
145
|
+
preserve: false,
|
|
146
|
+
// Untrusted: an issue title is attacker-influenceable. Fenced after truncation.
|
|
147
|
+
untrustedSource: 'issue-titles',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Assemble the bounded digest text. PURE. Never exceeds `budgetTokens` (delegated to
|
|
153
|
+
* applyBudget). Empty inputs → { text: '', empty: true } so the caller injects nothing.
|
|
154
|
+
*
|
|
155
|
+
* @param {{ notes?: object[], ready?: object[], claimed?: object[] }} [data]
|
|
156
|
+
* @param {object} [options] - { budgetTokens }
|
|
157
|
+
* @returns {{ text: string, empty: boolean, tokens: number }}
|
|
158
|
+
*/
|
|
159
|
+
function buildMemoryDigest(data = {}, options = {}) {
|
|
160
|
+
const notes = Array.isArray(data.notes) ? data.notes : [];
|
|
161
|
+
const ready = Array.isArray(data.ready) ? data.ready : [];
|
|
162
|
+
const claimed = Array.isArray(data.claimed) ? data.claimed : [];
|
|
163
|
+
const inbox = Array.isArray(data.inbox) ? data.inbox : [];
|
|
164
|
+
|
|
165
|
+
// Inbox (priority 5) is a THIRD section beside notes + issues; a fresh human directive
|
|
166
|
+
// outranks stale notes (10) and the agent's own issue list (20) under budget pressure.
|
|
167
|
+
const sections = [inboxSection(inbox), notesSection(notes), issuesSection(ready, claimed)].filter(Boolean);
|
|
168
|
+
if (!sections.length) return { text: '', empty: true, tokens: 0 };
|
|
169
|
+
|
|
170
|
+
const budgetTokens = options.budgetTokens || DEFAULT_DIGEST_BUDGET_TOKENS;
|
|
171
|
+
const budgeted = applyBudget(sections, budgetTokens);
|
|
172
|
+
const body = budgeted.sections
|
|
173
|
+
.filter(section => section.content)
|
|
174
|
+
// Fence AFTER applyBudget truncates, so the ⟦END UNTRUSTED⟧ close marker always
|
|
175
|
+
// survives (fencing before truncation would let the budget cut the terminator and
|
|
176
|
+
// leave an unclosed fence a payload could exploit). Provenance-labelled per section.
|
|
177
|
+
.map(section => `${section.title}:\n${fenceUntrusted(section.content, { source: section.untrustedSource })}`)
|
|
178
|
+
.join('\n\n');
|
|
179
|
+
if (!body) return { text: '', empty: true, tokens: 0 };
|
|
180
|
+
|
|
181
|
+
const text = `${DIGEST_HEADER}\n\n${body}`;
|
|
182
|
+
return { text, empty: false, tokens: estimateTokens(text) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
DEFAULT_DIGEST_BUDGET_TOKENS,
|
|
187
|
+
DIGEST_HEADER,
|
|
188
|
+
buildMemoryDigest,
|
|
189
|
+
collectDigestData,
|
|
190
|
+
extractIssues,
|
|
191
|
+
// exported for focused reuse / tests
|
|
192
|
+
defaultFetchNotes,
|
|
193
|
+
defaultFetchIssues,
|
|
194
|
+
defaultFetchInbox,
|
|
195
|
+
};
|