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
package/lib/project-memory.js
CHANGED
|
@@ -1,320 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const crypto = require('node:crypto');
|
|
1
|
+
'use strict';
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const DEFAULT_LOCK_RETRY_MS = 10;
|
|
8
|
-
const DEFAULT_DEAD_LOCK_GRACE_MS = 1000;
|
|
9
|
-
const INVALID_LOCK_INITIALIZATION_BUFFER_MS = 1000;
|
|
3
|
+
const { resolveKernelDatabasePath } = require('./kernel/cli-broker-factory');
|
|
4
|
+
const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function nearestExistingPath(targetPath) {
|
|
19
|
-
let current = targetPath;
|
|
20
|
-
while (!fs.existsSync(current)) {
|
|
21
|
-
const next = path.dirname(current);
|
|
22
|
-
if (next === current) break;
|
|
23
|
-
current = next;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return current;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function assertRealPathInsideProjectRoot(root, targetPath) {
|
|
30
|
-
const realRoot = fs.realpathSync.native(root);
|
|
31
|
-
const existingPath = fs.existsSync(targetPath)
|
|
32
|
-
? targetPath
|
|
33
|
-
: nearestExistingPath(path.dirname(targetPath));
|
|
34
|
-
const realExistingPath = fs.realpathSync.native(existingPath);
|
|
35
|
-
|
|
36
|
-
if (realExistingPath === realRoot) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
assertInsideProjectRoot(realRoot, realExistingPath);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function memoryPath(projectRoot, options = {}) {
|
|
44
|
-
const root = path.resolve(projectRoot);
|
|
45
|
-
if (!options.filePath) {
|
|
46
|
-
const defaultPath = path.join(root, DEFAULT_MEMORY_FILE);
|
|
47
|
-
assertRealPathInsideProjectRoot(root, defaultPath);
|
|
48
|
-
return defaultPath;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const candidate = path.isAbsolute(options.filePath)
|
|
52
|
-
? options.filePath
|
|
53
|
-
: path.join(root, options.filePath);
|
|
54
|
-
const resolved = path.resolve(candidate);
|
|
55
|
-
assertInsideProjectRoot(root, resolved);
|
|
56
|
-
assertRealPathInsideProjectRoot(root, resolved);
|
|
57
|
-
|
|
58
|
-
return resolved;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function sleep(ms) {
|
|
62
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isProcessAlive(pid) {
|
|
66
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
process.kill(pid, 0);
|
|
70
|
-
return true;
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return err.code === 'EPERM';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function readLock(lockPath) {
|
|
77
|
-
try {
|
|
78
|
-
const lockStat = fs.statSync(lockPath);
|
|
79
|
-
const metadataPath = lockStat.isDirectory()
|
|
80
|
-
? path.join(lockPath, 'owner.json')
|
|
81
|
-
: lockPath;
|
|
82
|
-
return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
83
|
-
} catch (err) {
|
|
84
|
-
return {
|
|
85
|
-
pid: null,
|
|
86
|
-
createdAt: null,
|
|
87
|
-
invalid: true,
|
|
88
|
-
readError: err.message,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isLockDirectory(lockPath) {
|
|
94
|
-
try {
|
|
95
|
-
return fs.statSync(lockPath).isDirectory();
|
|
96
|
-
} catch (err) {
|
|
97
|
-
if (err.code !== 'ENOENT') {
|
|
98
|
-
throw err;
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function lockPathAgeMs(lockPath) {
|
|
105
|
-
try {
|
|
106
|
-
return Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
107
|
-
} catch (err) {
|
|
108
|
-
if (err.code !== 'ENOENT') {
|
|
109
|
-
throw err;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function lockAgeMs(lock) {
|
|
116
|
-
const createdAt = Date.parse(lock?.createdAt);
|
|
117
|
-
if (Number.isNaN(createdAt)) return null;
|
|
118
|
-
return Date.now() - createdAt;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isWithinGraceWindow(ageMs, graceMs) {
|
|
122
|
-
return ageMs !== null && Math.abs(ageMs) < graceMs;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function debugSuppressedLockError(message, err) {
|
|
126
|
-
if (process.env.FORGE_DEBUG_LOCKS) {
|
|
127
|
-
console.debug(`${message}: ${err.message}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function shouldKeepInvalidLock(lockPath, lock, options) {
|
|
132
|
-
if (!lock.invalid) {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
137
|
-
const lockRetryMs = options.lockRetryMs ?? DEFAULT_LOCK_RETRY_MS;
|
|
138
|
-
const invalidLockGraceMs = lockTimeoutMs + Math.max(lockRetryMs, INVALID_LOCK_INITIALIZATION_BUFFER_MS);
|
|
139
|
-
const lockPathAge = lockPathAgeMs(lockPath);
|
|
140
|
-
return isWithinGraceWindow(lockPathAge, invalidLockGraceMs);
|
|
141
|
-
}
|
|
6
|
+
// Project memory is a Forge read model persisted in the kernel store (kernel_memories),
|
|
7
|
+
// written DIRECTLY rather than through the issue CAS/guarded-event path. The store seam
|
|
8
|
+
// (options.store) is a driver-like object — { recordMemory, loadMemory, searchMemories,
|
|
9
|
+
// listMemories } — so hermetic tests stay in-memory. The default seam resolves the
|
|
10
|
+
// per-repo kernel database path and reuses one driver per database path (the CLI process
|
|
11
|
+
// is short-lived, so the connection closing on exit is sufficient cleanup).
|
|
142
12
|
|
|
143
|
-
|
|
144
|
-
if (lock.invalid) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const configuredGraceMs = options.deadLockGraceMs ?? DEFAULT_DEAD_LOCK_GRACE_MS;
|
|
149
|
-
const lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
150
|
-
const deadLockGraceMs = lockTimeoutMs <= configuredGraceMs ? 0 : configuredGraceMs;
|
|
151
|
-
const age = lockAgeMs(lock);
|
|
152
|
-
const tokenizedLock = typeof lock?.token === 'string' && lock.token !== '';
|
|
153
|
-
const tokenRecoveryGraceMs = Math.max(deadLockGraceMs, Math.floor(lockTimeoutMs * 0.9));
|
|
154
|
-
|
|
155
|
-
if (tokenizedLock && age !== null && age >= 0 && age < tokenRecoveryGraceMs) {
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return age !== null && age >= 0 && age < deadLockGraceMs;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function removeLockPath(lockPath) {
|
|
163
|
-
try {
|
|
164
|
-
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
165
|
-
return true;
|
|
166
|
-
} catch (err) {
|
|
167
|
-
debugSuppressedLockError(`project memory lock cleanup skipped for ${lockPath}`, err);
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function removeStaleLock(lockPath, options = {}) {
|
|
173
|
-
const lock = readLock(lockPath);
|
|
174
|
-
if (shouldKeepInvalidLock(lockPath, lock, options)) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (isProcessAlive(lock?.pid)) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (shouldKeepDeadOwnerLock(lock, options)) {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return removeLockPath(lockPath);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function isExistingLockError(err, lockPath) {
|
|
190
|
-
if (err.code === 'EEXIST' || err.code === 'EISDIR') {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return err.code === 'EPERM' && isLockDirectory(lockPath);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function cleanupCreatedLockFile(lockPath) {
|
|
198
|
-
try {
|
|
199
|
-
fs.unlinkSync(lockPath);
|
|
200
|
-
} catch (err) {
|
|
201
|
-
if (err.code !== 'ENOENT') {
|
|
202
|
-
debugSuppressedLockError(`project memory lock init cleanup skipped for ${lockPath}`, err);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function closeLockFd(lockFd, lockPath) {
|
|
208
|
-
try {
|
|
209
|
-
fs.closeSync(lockFd);
|
|
210
|
-
} catch (err) {
|
|
211
|
-
debugSuppressedLockError(`project memory lock fd close failed for ${lockPath}`, err);
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function closeLockFdAfterFailure(lockFd, lockPath) {
|
|
217
|
-
try {
|
|
218
|
-
closeLockFd(lockFd, lockPath);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
debugSuppressedLockError(`project memory lock fd close skipped after failure for ${lockPath}`, err);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function writeLockMetadata(lockFd, token) {
|
|
225
|
-
fs.writeFileSync(lockFd, JSON.stringify({
|
|
226
|
-
pid: process.pid,
|
|
227
|
-
createdAt: new Date().toISOString(),
|
|
228
|
-
token,
|
|
229
|
-
}), 'utf8');
|
|
230
|
-
fs.fsyncSync(lockFd);
|
|
231
|
-
}
|
|
13
|
+
const storeCache = new Map();
|
|
232
14
|
|
|
233
|
-
function
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function releaseLockFile(lockPath, token) {
|
|
246
|
-
const lock = readLock(lockPath);
|
|
247
|
-
if (!lock.invalid && lock.pid === process.pid && lock.token === token) {
|
|
248
|
-
fs.rmSync(lockPath, { force: true });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function acquireLock(filePath, options = {}) {
|
|
253
|
-
const lockPath = `${filePath}.lock`;
|
|
254
|
-
const timeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
255
|
-
const retryMs = options.lockRetryMs ?? DEFAULT_LOCK_RETRY_MS;
|
|
256
|
-
const startedAt = Date.now();
|
|
257
|
-
const token = `${process.pid}-${Date.now()}-${crypto.randomUUID()}`;
|
|
258
|
-
|
|
259
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
260
|
-
|
|
261
|
-
while (true) {
|
|
262
|
-
try {
|
|
263
|
-
initializeLockFile(lockPath, token);
|
|
264
|
-
return () => releaseLockFile(lockPath, token);
|
|
265
|
-
} catch (err) {
|
|
266
|
-
if (!isExistingLockError(err, lockPath)) {
|
|
267
|
-
throw err;
|
|
268
|
-
}
|
|
269
|
-
if (removeStaleLock(lockPath, options)) {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (Date.now() - startedAt >= timeoutMs) {
|
|
273
|
-
throw err;
|
|
274
|
-
}
|
|
275
|
-
sleep(retryMs);
|
|
276
|
-
}
|
|
15
|
+
function defaultStore(projectRoot, options = {}) {
|
|
16
|
+
const databasePath = resolveKernelDatabasePath({
|
|
17
|
+
projectRoot,
|
|
18
|
+
gitCommonDir: options.gitCommonDir,
|
|
19
|
+
databasePath: options.databasePath,
|
|
20
|
+
});
|
|
21
|
+
let store = storeCache.get(databasePath);
|
|
22
|
+
if (!store) {
|
|
23
|
+
store = createBuiltinSQLiteDriver({ databasePath });
|
|
24
|
+
storeCache.set(databasePath, store);
|
|
277
25
|
}
|
|
26
|
+
return store;
|
|
278
27
|
}
|
|
279
28
|
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
key: entry.key,
|
|
283
|
-
value: entry.value,
|
|
284
|
-
sourceAgent: entry['source-agent'],
|
|
285
|
-
timestamp: entry.timestamp,
|
|
286
|
-
tags: Array.isArray(entry.tags) ? [...entry.tags] : [],
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
if (entry.scope !== undefined) apiEntry.scope = entry.scope;
|
|
290
|
-
if (entry.confidence !== undefined) apiEntry.confidence = entry.confidence;
|
|
291
|
-
if (entry.supersedes !== undefined) apiEntry.supersedes = [...entry.supersedes];
|
|
292
|
-
if (entry['beads-refs'] !== undefined) apiEntry.beadsRefs = [...entry['beads-refs']];
|
|
293
|
-
|
|
294
|
-
return apiEntry;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function toDiskEntry(entry) {
|
|
298
|
-
const diskEntry = {
|
|
299
|
-
key: entry.key,
|
|
300
|
-
value: entry.value,
|
|
301
|
-
'source-agent': entry.sourceAgent || entry['source-agent'],
|
|
302
|
-
timestamp: entry.timestamp || new Date().toISOString(),
|
|
303
|
-
tags: Array.isArray(entry.tags) ? [...entry.tags] : [],
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
if (entry.scope !== undefined) diskEntry.scope = entry.scope;
|
|
307
|
-
if (entry.confidence !== undefined) diskEntry.confidence = entry.confidence;
|
|
308
|
-
if (entry.supersedes !== undefined) diskEntry.supersedes = [...entry.supersedes];
|
|
309
|
-
if (entry.beadsRefs !== undefined || entry['beads-refs'] !== undefined) {
|
|
310
|
-
diskEntry['beads-refs'] = [...(entry.beadsRefs || entry['beads-refs'])];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return diskEntry;
|
|
29
|
+
function resolveStore(projectRoot, options = {}) {
|
|
30
|
+
return options.store ?? defaultStore(projectRoot, options);
|
|
314
31
|
}
|
|
315
32
|
|
|
316
33
|
function assertEntryObject(entry) {
|
|
317
|
-
if (!entry || typeof entry !== 'object') {
|
|
34
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
318
35
|
throw new TypeError('project memory entry must be an object');
|
|
319
36
|
}
|
|
320
37
|
}
|
|
@@ -326,234 +43,115 @@ function assertRequiredString(value, fieldName) {
|
|
|
326
43
|
}
|
|
327
44
|
|
|
328
45
|
function assertStringArray(value, fieldName) {
|
|
329
|
-
if (!Array.isArray(value) || value.some(
|
|
46
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
330
47
|
throw new TypeError(`project memory entry ${fieldName} must be an array of strings`);
|
|
331
48
|
}
|
|
332
49
|
}
|
|
333
50
|
|
|
334
|
-
function
|
|
335
|
-
if (
|
|
336
|
-
throw new TypeError('project memory entry
|
|
51
|
+
function assertOptionalConfidence(value) {
|
|
52
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0 || value > 1)) {
|
|
53
|
+
throw new TypeError('project memory entry confidence must be a number from 0 to 1');
|
|
337
54
|
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateEntry(entry) {
|
|
58
|
+
assertEntryObject(entry);
|
|
59
|
+
assertRequiredString(entry.key, 'key');
|
|
338
60
|
if (!Object.hasOwn(entry, 'value') || entry.value === undefined) {
|
|
339
61
|
throw new TypeError('project memory entry value is required');
|
|
340
62
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
function validateTimestamp(entry) {
|
|
347
|
-
if (entry.timestamp !== undefined) {
|
|
348
|
-
const parsed = Date.parse(entry.timestamp);
|
|
349
|
-
if (typeof entry.timestamp !== 'string' || Number.isNaN(parsed)) {
|
|
350
|
-
throw new TypeError('project memory entry timestamp must be an ISO-compatible string');
|
|
351
|
-
}
|
|
63
|
+
assertRequiredString(entry.sourceAgent || entry['source-agent'], 'sourceAgent');
|
|
64
|
+
if (entry.timestamp !== undefined
|
|
65
|
+
&& (typeof entry.timestamp !== 'string' || Number.isNaN(Date.parse(entry.timestamp)))) {
|
|
66
|
+
throw new TypeError('project memory entry timestamp must be an ISO timestamp string');
|
|
352
67
|
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function validateScope(entry) {
|
|
356
68
|
if (entry.scope !== undefined && (typeof entry.scope !== 'string' || entry.scope.trim() === '')) {
|
|
357
69
|
throw new TypeError('project memory entry scope must be a non-empty string');
|
|
358
70
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
function validateConfidence(entry) {
|
|
362
|
-
if (entry.confidence !== undefined) {
|
|
363
|
-
if (typeof entry.confidence !== 'number' || entry.confidence < 0 || entry.confidence > 1) {
|
|
364
|
-
throw new TypeError('project memory entry confidence must be a number from 0 to 1');
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function validateOptionalArrays(entry) {
|
|
71
|
+
assertOptionalConfidence(entry.confidence);
|
|
370
72
|
if (entry.tags !== undefined) assertStringArray(entry.tags, 'tags');
|
|
371
73
|
if (entry.supersedes !== undefined) assertStringArray(entry.supersedes, 'supersedes');
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function validateEntry(entry) {
|
|
377
|
-
assertEntryObject(entry);
|
|
378
|
-
validateRequiredFields(entry);
|
|
379
|
-
validateTimestamp(entry);
|
|
380
|
-
validateScope(entry);
|
|
381
|
-
validateConfidence(entry);
|
|
382
|
-
validateOptionalArrays(entry);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function validateStoredEntry(entry) {
|
|
386
|
-
validateEntry(entry);
|
|
387
|
-
assertRequiredString(entry['source-agent'], 'source-agent');
|
|
388
|
-
assertStringArray(entry.tags, 'tags');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function readEntries(projectRoot, options = {}) {
|
|
392
|
-
const filePath = memoryPath(projectRoot, options);
|
|
393
|
-
if (!fs.existsSync(filePath)) {
|
|
394
|
-
return [];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
398
|
-
if (content.trim() === '') return [];
|
|
399
|
-
|
|
400
|
-
const hasTerminatingNewline = /\r?\n$/.test(content);
|
|
401
|
-
const lines = content.split(/\r?\n/);
|
|
402
|
-
if (hasTerminatingNewline && lines.at(-1) === '') {
|
|
403
|
-
lines.pop();
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const entries = [];
|
|
407
|
-
lines.forEach((line, index) => {
|
|
408
|
-
const trailingIncompleteLine = !hasTerminatingNewline && index === lines.length - 1;
|
|
409
|
-
if (trailingIncompleteLine) return;
|
|
410
|
-
|
|
411
|
-
let parsed;
|
|
412
|
-
try {
|
|
413
|
-
parsed = JSON.parse(line);
|
|
414
|
-
} catch (err) {
|
|
415
|
-
if (trailingIncompleteLine) return;
|
|
416
|
-
throw new Error(`invalid project memory JSONL at line ${index + 1}: ${err.message}`);
|
|
417
|
-
}
|
|
418
|
-
try {
|
|
419
|
-
validateStoredEntry(parsed);
|
|
420
|
-
} catch (err) {
|
|
421
|
-
if (trailingIncompleteLine) return;
|
|
422
|
-
throw new Error(`invalid project memory entry at line ${index + 1}: ${err.message}`);
|
|
423
|
-
}
|
|
424
|
-
entries.push(parsed);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
return dedupeEntries(entries);
|
|
74
|
+
if (entry.beadsRefs !== undefined) assertStringArray(entry.beadsRefs, 'beadsRefs');
|
|
75
|
+
if (entry['beads-refs'] !== undefined) assertStringArray(entry['beads-refs'], 'beads-refs');
|
|
428
76
|
}
|
|
429
77
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
78
|
+
// Canonicalize an entry for storage: resolve the snake-case input aliases, default the
|
|
79
|
+
// timestamp and tags, and carry the optional fields only when present (so the persisted
|
|
80
|
+
// shape matches the legacy entry exactly).
|
|
81
|
+
function normalizeEntry(key, entry) {
|
|
82
|
+
const normalized = {
|
|
83
|
+
key,
|
|
84
|
+
value: entry.value,
|
|
85
|
+
sourceAgent: entry.sourceAgent || entry['source-agent'],
|
|
86
|
+
tags: Array.isArray(entry.tags) ? [...entry.tags] : [],
|
|
87
|
+
timestamp: entry.timestamp ?? new Date().toISOString(),
|
|
88
|
+
};
|
|
433
89
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
} else {
|
|
440
|
-
deduped[existingIndex] = entry;
|
|
441
|
-
}
|
|
90
|
+
if (entry.scope !== undefined) normalized.scope = entry.scope;
|
|
91
|
+
if (entry.confidence !== undefined) normalized.confidence = entry.confidence;
|
|
92
|
+
if (entry.supersedes !== undefined) normalized.supersedes = [...entry.supersedes];
|
|
93
|
+
if (entry.beadsRefs !== undefined || entry['beads-refs'] !== undefined) {
|
|
94
|
+
normalized.beadsRefs = [...(entry.beadsRefs || entry['beads-refs'])];
|
|
442
95
|
}
|
|
443
96
|
|
|
444
|
-
return
|
|
97
|
+
return normalized;
|
|
445
98
|
}
|
|
446
99
|
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
100
|
+
function write(projectRoot, entry, options = {}) {
|
|
101
|
+
validateEntry(entry);
|
|
102
|
+
const normalized = normalizeEntry(entry.key.trim(), entry);
|
|
103
|
+
resolveStore(projectRoot, options).recordMemory(normalized);
|
|
104
|
+
return normalized;
|
|
452
105
|
}
|
|
453
106
|
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
460
|
-
if (content === '' || /\r?\n$/.test(content)) {
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const lastLineStart = Math.max(content.lastIndexOf('\n'), content.lastIndexOf('\r')) + 1;
|
|
465
|
-
fs.truncateSync(filePath, Buffer.byteLength(content.slice(0, lastLineStart), 'utf8'));
|
|
466
|
-
return false;
|
|
107
|
+
function read(projectRoot, key, options = {}) {
|
|
108
|
+
assertRequiredString(key, 'read key');
|
|
109
|
+
return resolveStore(projectRoot, options).loadMemory(key.trim());
|
|
467
110
|
}
|
|
468
111
|
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const needsLineBreak = normalizeJsonlTail(filePath);
|
|
474
|
-
const record = Buffer.from(`${needsLineBreak ? '\n' : ''}${JSON.stringify(entry)}\n`, 'utf8');
|
|
475
|
-
const rollbackSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
|
|
476
|
-
const fd = fs.openSync(filePath, 'a');
|
|
477
|
-
let fdClosed = false;
|
|
478
|
-
try {
|
|
479
|
-
writeAllSync(fd, record);
|
|
480
|
-
fs.fsyncSync(fd);
|
|
481
|
-
} catch (err) {
|
|
482
|
-
try {
|
|
483
|
-
fs.closeSync(fd);
|
|
484
|
-
fdClosed = true;
|
|
485
|
-
} catch (_rollbackCloseErr) {
|
|
486
|
-
// Preserve the original append/fsync error.
|
|
487
|
-
}
|
|
488
|
-
try {
|
|
489
|
-
fs.truncateSync(filePath, rollbackSize);
|
|
490
|
-
} catch (_rollbackErr) {
|
|
491
|
-
// Preserve the original append/fsync error.
|
|
492
|
-
}
|
|
493
|
-
throw err;
|
|
494
|
-
} finally {
|
|
495
|
-
if (!fdClosed) {
|
|
496
|
-
fs.closeSync(fd);
|
|
497
|
-
}
|
|
112
|
+
function search(projectRoot, query, options = {}) {
|
|
113
|
+
if (typeof query !== 'string' || query.trim() === '') {
|
|
114
|
+
return [];
|
|
498
115
|
}
|
|
116
|
+
return resolveStore(projectRoot, options).searchMemories(query.trim());
|
|
499
117
|
}
|
|
500
118
|
|
|
501
119
|
function list(projectRoot, options = {}) {
|
|
502
|
-
return
|
|
120
|
+
return resolveStore(projectRoot, options).listMemories();
|
|
503
121
|
}
|
|
504
122
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const entry = readEntries(projectRoot, options).find((candidate) => candidate.key === normalizedKey);
|
|
512
|
-
return entry ? toApiEntry(entry) : null;
|
|
123
|
+
// The newest `limit` entries (default recall with no query). Delegates to the FTS-backed
|
|
124
|
+
// driver so recall never loads and re-sorts the whole table. `options.agents` (a
|
|
125
|
+
// source_agent allow-list) scopes the read, e.g. to human `remember` notes only.
|
|
126
|
+
function recent(projectRoot, limit, options = {}) {
|
|
127
|
+
return resolveStore(projectRoot, options).recentMemories(limit, { agents: options.agents });
|
|
513
128
|
}
|
|
514
129
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
const normalized = toDiskEntry({
|
|
522
|
-
...entry,
|
|
523
|
-
key: entry.key.trim(),
|
|
524
|
-
sourceAgent: (entry.sourceAgent || entry['source-agent']).trim(),
|
|
525
|
-
tags: entry.tags || [],
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
appendEntry(projectRoot, normalized, options);
|
|
529
|
-
return toApiEntry(normalized);
|
|
530
|
-
} finally {
|
|
531
|
-
releaseLock();
|
|
532
|
-
}
|
|
130
|
+
// Total stored memories (optionally scoped by `options.agents`) — paired with `recent` so
|
|
131
|
+
// recall can report "showing N of TOTAL".
|
|
132
|
+
function count(projectRoot, options = {}) {
|
|
133
|
+
return resolveStore(projectRoot, options).countMemories({ agents: options.agents });
|
|
533
134
|
}
|
|
534
135
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
entry.scope,
|
|
541
|
-
String(entry.confidence ?? ''),
|
|
542
|
-
...(Array.isArray(entry.tags) ? entry.tags : []),
|
|
543
|
-
...(Array.isArray(entry.supersedes) ? entry.supersedes : []),
|
|
544
|
-
...(Array.isArray(entry['beads-refs']) ? entry['beads-refs'] : []),
|
|
545
|
-
].join('\n').toLowerCase();
|
|
136
|
+
// BM25 top-N recall over the FTS5 index (token-AND). Unlike `search` (the legacy LIKE
|
|
137
|
+
// helper) this does not short-circuit an empty query — the driver falls back to recent so
|
|
138
|
+
// recall stays capped either way.
|
|
139
|
+
function searchRanked(projectRoot, query, limit, options = {}) {
|
|
140
|
+
return resolveStore(projectRoot, options).searchMemoriesRanked(query, limit);
|
|
546
141
|
}
|
|
547
142
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
143
|
+
// Close and forget every cached default store. The CLI process is short-lived (the OS
|
|
144
|
+
// closes the handle on exit), so this is mainly a lifecycle helper for long-lived hosts and
|
|
145
|
+
// tests — it releases the SQLite/WAL handle before a temp dir is removed.
|
|
146
|
+
function closeAll() {
|
|
147
|
+
for (const store of storeCache.values()) {
|
|
148
|
+
try {
|
|
149
|
+
if (store && typeof store.close === 'function') store.close();
|
|
150
|
+
} catch {
|
|
151
|
+
// best-effort close
|
|
152
|
+
}
|
|
551
153
|
}
|
|
552
|
-
|
|
553
|
-
const needle = query.trim().toLowerCase();
|
|
554
|
-
return readEntries(projectRoot, options)
|
|
555
|
-
.filter((entry) => searchableText(entry).includes(needle))
|
|
556
|
-
.map(toApiEntry);
|
|
154
|
+
storeCache.clear();
|
|
557
155
|
}
|
|
558
156
|
|
|
559
157
|
module.exports = {
|
|
@@ -561,4 +159,8 @@ module.exports = {
|
|
|
561
159
|
write,
|
|
562
160
|
search,
|
|
563
161
|
list,
|
|
162
|
+
recent,
|
|
163
|
+
count,
|
|
164
|
+
searchRanked,
|
|
165
|
+
closeAll,
|
|
564
166
|
};
|