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,495 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// D19 — Filesystem-class detector + default-on gate helper.
|
|
4
|
+
//
|
|
5
|
+
// Two layers (the split is the testability crux):
|
|
6
|
+
// Layer A — classifyFromSignals(signals): PURE, zero I/O. 100% synthetic-tested.
|
|
7
|
+
// Layer B — gatherSignals(absPath, deps): thin I/O shell, ALL probes injectable.
|
|
8
|
+
// Public entry: classifyFilesystem(absPath, deps) = classifyFromSignals(gatherSignals(...)).
|
|
9
|
+
//
|
|
10
|
+
// Scope discipline: this module reports the filesystem CLASS only. Broad health
|
|
11
|
+
// stays in runtime-health.js. See docs/work/2026-06-06-kernel-backlog-memory-roadmap/
|
|
12
|
+
// D19-filesystem-doctor-design.md.
|
|
13
|
+
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const { execFileSync } = require('node:child_process');
|
|
16
|
+
|
|
17
|
+
// --- Remediation message table (remediationKey -> human text) ----------------
|
|
18
|
+
|
|
19
|
+
const OVERRIDE_LINE =
|
|
20
|
+
'Override (NOT recommended): set FORGE_KERNEL_ALLOW_UNSAFE_FS=1 to proceed\n' +
|
|
21
|
+
'at your own risk.';
|
|
22
|
+
|
|
23
|
+
const REMEDIATION = Object.freeze({
|
|
24
|
+
'local-ok': 'No action needed — the kernel database is on a local fixed disk.',
|
|
25
|
+
onedrive:
|
|
26
|
+
'Forge kernel cannot place its SQLite database on a cloud-synced folder\n' +
|
|
27
|
+
'(OneDrive). SQLite WAL mode corrupts when a sync client rewrites the\n' +
|
|
28
|
+
'database mid-write.\n\n' +
|
|
29
|
+
'Fix: move this repository outside the OneDrive folder, e.g. a non-synced\n' +
|
|
30
|
+
'local path such as C:\\dev\\<repo>, then re-run.\n\n' +
|
|
31
|
+
OVERRIDE_LINE,
|
|
32
|
+
dropbox:
|
|
33
|
+
'Forge kernel cannot place its SQLite database on a Dropbox-synced folder.\n' +
|
|
34
|
+
'A sync client rewriting the DB mid-write corrupts SQLite WAL mode.\n\n' +
|
|
35
|
+
'Fix: move this repository outside the Dropbox folder to a non-synced\n' +
|
|
36
|
+
'local path, then re-run.\n\n' +
|
|
37
|
+
OVERRIDE_LINE,
|
|
38
|
+
gdrive:
|
|
39
|
+
'Forge kernel cannot place its SQLite database on a Google Drive folder.\n' +
|
|
40
|
+
'A sync/streaming client rewriting the DB mid-write corrupts SQLite WAL mode.\n\n' +
|
|
41
|
+
'Fix: move this repository outside the Google Drive folder to a non-synced\n' +
|
|
42
|
+
'local path, then re-run.\n\n' +
|
|
43
|
+
OVERRIDE_LINE,
|
|
44
|
+
icloud:
|
|
45
|
+
'Forge kernel cannot place its SQLite database under iCloud Drive\n' +
|
|
46
|
+
'(Library/Mobile Documents). iCloud may evict/rewrite the file and corrupt\n' +
|
|
47
|
+
'SQLite WAL mode.\n\n' +
|
|
48
|
+
'Fix: move this repository outside iCloud Drive to a non-synced local path,\n' +
|
|
49
|
+
'then re-run.\n\n' +
|
|
50
|
+
OVERRIDE_LINE,
|
|
51
|
+
'network-unc':
|
|
52
|
+
'Forge kernel cannot place its SQLite database on a network share\n' +
|
|
53
|
+
'(UNC / SMB / NFS). SQLite file locking is unreliable over network\n' +
|
|
54
|
+
'filesystems and can corrupt the database.\n\n' +
|
|
55
|
+
'Fix: move this repository to a local fixed disk, then re-run.\n\n' +
|
|
56
|
+
OVERRIDE_LINE,
|
|
57
|
+
'mapped-network-drive':
|
|
58
|
+
'Forge kernel cannot place its SQLite database on a mapped network drive.\n' +
|
|
59
|
+
'SQLite file locking is unreliable over SMB/NFS and can corrupt the database.\n\n' +
|
|
60
|
+
'Fix: move this repository to a local fixed disk, then re-run.\n\n' +
|
|
61
|
+
OVERRIDE_LINE,
|
|
62
|
+
'wsl-cross':
|
|
63
|
+
'Warning: the kernel database lives on the Windows volume across the WSL\n' +
|
|
64
|
+
'filesystem boundary (/mnt/<letter>). This works but is slow and has lock\n' +
|
|
65
|
+
'edge cases. For best results, keep the repository on the Linux-native\n' +
|
|
66
|
+
'filesystem (e.g. under your home directory).',
|
|
67
|
+
unknown:
|
|
68
|
+
'Note: Forge could not identify this filesystem type; proceeding normally.\n' +
|
|
69
|
+
'For best reliability, keep the repository on a local fixed disk rather than\n' +
|
|
70
|
+
'a cloud-synced or network folder.',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// --- Risk-tier mapping (class -> tier) ---------------------------------------
|
|
74
|
+
|
|
75
|
+
const REFUSE_CLASSES = new Set([
|
|
76
|
+
'onedrive',
|
|
77
|
+
'dropbox',
|
|
78
|
+
'gdrive',
|
|
79
|
+
'icloud',
|
|
80
|
+
'network-unc',
|
|
81
|
+
'mapped-network-drive',
|
|
82
|
+
]);
|
|
83
|
+
const WARN_CLASSES = new Set(['wsl-cross', 'unknown']);
|
|
84
|
+
|
|
85
|
+
function riskTierFor(klass) {
|
|
86
|
+
if (REFUSE_CLASSES.has(klass)) return 'refuse';
|
|
87
|
+
if (WARN_CLASSES.has(klass)) return 'warn';
|
|
88
|
+
return 'safe';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makeClassification(klass, signal) {
|
|
92
|
+
return {
|
|
93
|
+
class: klass,
|
|
94
|
+
riskTier: riskTierFor(klass),
|
|
95
|
+
signal,
|
|
96
|
+
remediationKey: klass,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Path helpers (pure) -----------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// SQLite "paths" that never touch a real directory: in-memory and file: URIs.
|
|
103
|
+
function isNonFsPath(absPath) {
|
|
104
|
+
if (!absPath) return true;
|
|
105
|
+
if (absPath === ':memory:') return true;
|
|
106
|
+
if (/^file:/i.test(absPath)) return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Split a path into lowercase segments on both separators (for segment matching).
|
|
111
|
+
function pathSegments(absPath) {
|
|
112
|
+
return String(absPath)
|
|
113
|
+
.split(/[\\/]+/)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map(seg => seg.toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Boundary-aware, case/separator-normalized prefix test. Returns true only when
|
|
119
|
+
// `root` covers `absPath` at a path boundary (avoids OneDrive vs OneDriveBackup).
|
|
120
|
+
function pathStartsWith(absPath, root, { caseInsensitive }) {
|
|
121
|
+
if (!root) return false;
|
|
122
|
+
const norm = value => {
|
|
123
|
+
let v = String(value).replace(/[\\/]+/g, '/').replace(/\/+$/, '');
|
|
124
|
+
if (caseInsensitive) v = v.toLowerCase();
|
|
125
|
+
return v;
|
|
126
|
+
};
|
|
127
|
+
const a = norm(absPath);
|
|
128
|
+
const r = norm(root);
|
|
129
|
+
if (a === r) return true;
|
|
130
|
+
return a.startsWith(`${r}/`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hasSegmentMatching(absPath, regex) {
|
|
134
|
+
return pathSegments(absPath).some(seg => regex.test(seg));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasExactSegment(absPath, lowerName) {
|
|
138
|
+
return pathSegments(absPath).includes(lowerName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Layer A: pure classifier ------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function classifyWin32(signals) {
|
|
144
|
+
const { absPath, env = {} } = signals;
|
|
145
|
+
|
|
146
|
+
if (signals.isUNC) {
|
|
147
|
+
return makeClassification('network-unc', String.raw`UNC path (\\server\share)`);
|
|
148
|
+
}
|
|
149
|
+
if (signals.driveType === 'network') {
|
|
150
|
+
return makeClassification('mapped-network-drive', 'driveType=network');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Cloud-sync detection is path/env-based and INDEPENDENT of the drive probe.
|
|
154
|
+
// It must run BEFORE the driveType==='unknown' fail-open below, so a positively
|
|
155
|
+
// identified OneDrive/Dropbox/gdrive path still REFUSES even when the mapped-
|
|
156
|
+
// drive probe failed (G1 fallback). Otherwise the highest-risk class would be
|
|
157
|
+
// silently downgraded to warn and the kernel would init on a synced folder.
|
|
158
|
+
|
|
159
|
+
// OneDrive: env-declared sync roots (boundary-aware) OR a path segment.
|
|
160
|
+
const oneDriveRoots = [env.OneDrive, env.OneDriveConsumer, env.OneDriveCommercial]
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
for (const root of oneDriveRoots) {
|
|
163
|
+
if (pathStartsWith(absPath, root, { caseInsensitive: true })) {
|
|
164
|
+
return makeClassification('onedrive', 'env OneDrive root prefix');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (hasSegmentMatching(absPath, /^onedrive([ -].+)?$/i)) {
|
|
168
|
+
return makeClassification('onedrive', 'OneDrive path segment');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (env.Dropbox && pathStartsWith(absPath, env.Dropbox, { caseInsensitive: true })) {
|
|
172
|
+
return makeClassification('dropbox', 'env Dropbox root prefix');
|
|
173
|
+
}
|
|
174
|
+
if (hasExactSegment(absPath, 'dropbox')) {
|
|
175
|
+
return makeClassification('dropbox', 'Dropbox path segment');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (hasSegmentMatching(absPath, /^(google ?drive|googledrive|drivefs)$/i)) {
|
|
179
|
+
return makeClassification('gdrive', 'Google Drive path segment');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Drive probe miss with no cloud signal: fail-open per G1 (warn, never block).
|
|
183
|
+
if (signals.driveType === 'unknown') {
|
|
184
|
+
return makeClassification('unknown', 'driveType probe returned unknown');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return makeClassification('local-ok', 'no cloud/network signal matched');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function classifyDarwin(signals) {
|
|
191
|
+
const { absPath, homedir } = signals;
|
|
192
|
+
const home = homedir || '';
|
|
193
|
+
|
|
194
|
+
if (pathStartsWith(absPath, `${home}/Library/Mobile Documents`, { caseInsensitive: false })) {
|
|
195
|
+
return makeClassification('icloud', 'iCloud Mobile Documents');
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
pathStartsWith(absPath, `${home}/Dropbox`, { caseInsensitive: false }) ||
|
|
199
|
+
hasSegmentMatching(absPath, /^dropbox.*$/i) && absPath.includes('/Library/CloudStorage/')
|
|
200
|
+
) {
|
|
201
|
+
return makeClassification('dropbox', 'Dropbox folder');
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
pathStartsWith(absPath, `${home}/Google Drive`, { caseInsensitive: false }) ||
|
|
205
|
+
hasSegmentMatching(absPath, /^googledrive.*$/i) && absPath.includes('/Library/CloudStorage/')
|
|
206
|
+
) {
|
|
207
|
+
return makeClassification('gdrive', 'Google Drive folder');
|
|
208
|
+
}
|
|
209
|
+
if (hasSegmentMatching(absPath, /^onedrive.*$/i) && absPath.includes('/Library/CloudStorage/')) {
|
|
210
|
+
return makeClassification('onedrive', 'OneDrive CloudStorage folder');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return makeClassification('local-ok', 'no cloud signal matched');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const NETWORK_FS_TYPES = new Set(['cifs', 'smb3', 'smb', 'nfs', 'nfs4', 'fuse.sshfs']);
|
|
217
|
+
const FUSE_CLOUD_MAP = new Map([
|
|
218
|
+
['fuse.dropbox', 'dropbox'],
|
|
219
|
+
['fuse.rclone', 'gdrive'],
|
|
220
|
+
['fuse.google-drive-ocamlfuse', 'gdrive'],
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
function classifyLinux(signals) {
|
|
224
|
+
const { absPath, mountFsType, isWslInterop } = signals;
|
|
225
|
+
|
|
226
|
+
if (isWslInterop && /^\/mnt\/[a-z]\//i.test(absPath)) {
|
|
227
|
+
return makeClassification('wsl-cross', 'WSL interop + /mnt/<letter>');
|
|
228
|
+
}
|
|
229
|
+
if (mountFsType && NETWORK_FS_TYPES.has(mountFsType)) {
|
|
230
|
+
return makeClassification('network-unc', `network mount fs=${mountFsType}`);
|
|
231
|
+
}
|
|
232
|
+
if (mountFsType && FUSE_CLOUD_MAP.has(mountFsType)) {
|
|
233
|
+
return makeClassification(FUSE_CLOUD_MAP.get(mountFsType), `fuse mount fs=${mountFsType}`);
|
|
234
|
+
}
|
|
235
|
+
// Secondary segment signal for cloud sync folders on native mounts. These are
|
|
236
|
+
// PATH-based and remain valid even when the mount probe threw, so they run
|
|
237
|
+
// BEFORE the unknown-on-throw fallback below.
|
|
238
|
+
if (hasSegmentMatching(absPath, /^onedrive.*$/i)) {
|
|
239
|
+
return makeClassification('onedrive', 'OneDrive path segment');
|
|
240
|
+
}
|
|
241
|
+
if (hasExactSegment(absPath, 'dropbox')) {
|
|
242
|
+
return makeClassification('dropbox', 'Dropbox path segment');
|
|
243
|
+
}
|
|
244
|
+
if (hasSegmentMatching(absPath, /^(google ?drive)$/i)) {
|
|
245
|
+
return makeClassification('gdrive', 'Google Drive path segment');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// M3: the mount probe THREW (could not read /proc/mounts) and no path/wsl signal
|
|
249
|
+
// matched. Treat as unknown/warn — symmetric with win32's G1 fail-safe — instead
|
|
250
|
+
// of silently returning local-ok, which would suppress the warning entirely.
|
|
251
|
+
if (signals.mountProbeThrew) {
|
|
252
|
+
return makeClassification('unknown', 'mount probe failed (could not classify)');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return makeClassification('local-ok', 'no cloud/network/wsl signal matched');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function classifyFromSignals(signals = {}) {
|
|
259
|
+
if (isNonFsPath(signals.absPath)) {
|
|
260
|
+
return makeClassification('local-ok', 'non-filesystem path (memory/uri)');
|
|
261
|
+
}
|
|
262
|
+
switch (signals.platform) {
|
|
263
|
+
case 'win32':
|
|
264
|
+
return classifyWin32(signals);
|
|
265
|
+
case 'darwin':
|
|
266
|
+
return classifyDarwin(signals);
|
|
267
|
+
case 'linux':
|
|
268
|
+
return classifyLinux(signals);
|
|
269
|
+
default:
|
|
270
|
+
return makeClassification('unknown', `unrecognized platform ${signals.platform}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Layer B: thin signal gatherer (all probes injectable) -------------------
|
|
275
|
+
|
|
276
|
+
function isUNC(absPath) {
|
|
277
|
+
return /^[\\/]{2}[^\\/]/.test(String(absPath || ''));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function driveLetterOf(absPath) {
|
|
281
|
+
const match = /^([a-z]):[\\/]/i.exec(String(absPath || ''));
|
|
282
|
+
return match ? `${match[1].toUpperCase()}:` : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Bounded exec options for the Windows drive probes. The timeout is the BLOCKER-1
|
|
286
|
+
// fix: execFileSync's try/catch only catches throws, never hangs, so an
|
|
287
|
+
// unresponsive `net use` / PowerShell (e.g. a wedged network redirector) would
|
|
288
|
+
// hang the whole process indefinitely. With a timeout, a hang becomes a throw
|
|
289
|
+
// that the gatherSignals catch turns into driveType='unknown' → warn (never
|
|
290
|
+
// refuse), symmetric with the G1 fail-safe. killSignal forces the child dead.
|
|
291
|
+
const WIN_PROBE_EXEC_OPTIONS = Object.freeze({
|
|
292
|
+
encoding: 'utf8',
|
|
293
|
+
timeout: 1500,
|
|
294
|
+
killSignal: 'SIGKILL',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// PURE (M5): does `net use` stdout list this drive letter as a mapped (network)
|
|
298
|
+
// connection? Returns 'network' on a boundary-matched letter, else null. The
|
|
299
|
+
// regex is anchored to a line-leading "<letter>:" preceded by start/whitespace
|
|
300
|
+
// so a stray mention of the letter elsewhere cannot false-positive.
|
|
301
|
+
function parseNetUseDriveType(stdout, driveLetter) {
|
|
302
|
+
const letter = String(driveLetter || '').replace(':', '');
|
|
303
|
+
if (!letter) return null;
|
|
304
|
+
const letterRe = new RegExp(String.raw`(^|\s)${letter}:\s`, 'i');
|
|
305
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
306
|
+
if (letterRe.test(line)) {
|
|
307
|
+
return 'network';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// PURE (M5): a non-empty PowerShell DisplayRoot means the PSDrive is backed by a
|
|
314
|
+
// network/remote root. Returns 'network' when non-blank, else null.
|
|
315
|
+
function parseDisplayRoot(stdout) {
|
|
316
|
+
return String(stdout || '').trim() ? 'network' : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Windows: detect whether a drive letter is a mapped network drive.
|
|
320
|
+
// FAIL-SAFE FALLBACK (locked decision G1, option a): try `net use`, then a
|
|
321
|
+
// PowerShell DisplayRoot fallback; on ANY probe failure (including a timeout-
|
|
322
|
+
// induced throw, per BLOCKER 1) the caller treats the result as 'unknown'.
|
|
323
|
+
// Probes use arg ARRAYS (no shell) so they remain injection-free.
|
|
324
|
+
function defaultProbeDriveType(driveLetter, deps = {}) {
|
|
325
|
+
const exec = deps.execFileSync || execFileSync;
|
|
326
|
+
|
|
327
|
+
// Primary: `net use` lists mapped drive letters (and their remote paths).
|
|
328
|
+
const netOut = exec('net', ['use'], WIN_PROBE_EXEC_OPTIONS);
|
|
329
|
+
if (parseNetUseDriveType(netOut, driveLetter) === 'network') {
|
|
330
|
+
return 'network';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Secondary: PowerShell DisplayRoot — non-empty for network-backed PSDrives.
|
|
334
|
+
const psLetter = String(driveLetter).replace(':', '');
|
|
335
|
+
const psCmd =
|
|
336
|
+
`(Get-PSDrive -Name '${psLetter}' -ErrorAction SilentlyContinue).DisplayRoot`;
|
|
337
|
+
const psOut = exec('powershell', ['-NoProfile', '-Command', psCmd], WIN_PROBE_EXEC_OPTIONS);
|
|
338
|
+
if (parseDisplayRoot(psOut) === 'network') {
|
|
339
|
+
return 'network';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Letter exists, no network backing detected → fixed.
|
|
343
|
+
return 'fixed';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Linux: read /proc/mounts, return fs type of the longest mountpoint prefixing absPath.
|
|
347
|
+
function defaultProbeMounts(absPath, deps = {}) {
|
|
348
|
+
const readFileSync = deps.readFileSync || require('node:fs').readFileSync;
|
|
349
|
+
const raw = String(readFileSync('/proc/mounts', 'utf8') || '');
|
|
350
|
+
let best = null;
|
|
351
|
+
let bestLen = -1;
|
|
352
|
+
for (const line of raw.split('\n')) {
|
|
353
|
+
const parts = line.split(/\s+/);
|
|
354
|
+
if (parts.length < 3) continue;
|
|
355
|
+
const mountPoint = parts[1];
|
|
356
|
+
const fsType = parts[2];
|
|
357
|
+
if (
|
|
358
|
+
(absPath === mountPoint || absPath.startsWith(`${mountPoint.replace(/\/$/, '')}/`)) &&
|
|
359
|
+
mountPoint.length > bestLen
|
|
360
|
+
) {
|
|
361
|
+
best = fsType;
|
|
362
|
+
bestLen = mountPoint.length;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return best;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function defaultReadWslInterop(deps = {}) {
|
|
369
|
+
const env = deps.env || process.env;
|
|
370
|
+
if (env.WSL_DISTRO_NAME) return true;
|
|
371
|
+
const readFileSync = deps.readFileSync || require('node:fs').readFileSync;
|
|
372
|
+
const version = String(readFileSync('/proc/version', 'utf8') || '');
|
|
373
|
+
return /microsoft/i.test(version);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function gatherSignals(absPath, deps = {}) {
|
|
377
|
+
const platform = deps.platform || process.platform;
|
|
378
|
+
const env = deps.env || process.env;
|
|
379
|
+
const homedirFn = deps.homedir || os.homedir;
|
|
380
|
+
const homedir = typeof homedirFn === 'function' ? homedirFn() : homedirFn;
|
|
381
|
+
|
|
382
|
+
const signals = {
|
|
383
|
+
platform,
|
|
384
|
+
absPath,
|
|
385
|
+
env,
|
|
386
|
+
homedir,
|
|
387
|
+
isUNC: isUNC(absPath),
|
|
388
|
+
driveType: null,
|
|
389
|
+
mountFsType: null,
|
|
390
|
+
mountProbeThrew: false,
|
|
391
|
+
isWslInterop: false,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (platform === 'win32' && !signals.isUNC) {
|
|
395
|
+
const driveLetter = driveLetterOf(absPath);
|
|
396
|
+
if (driveLetter) {
|
|
397
|
+
const probe = deps.probeDriveType || (letter => defaultProbeDriveType(letter, deps));
|
|
398
|
+
try {
|
|
399
|
+
signals.driveType = probe(driveLetter);
|
|
400
|
+
} catch {
|
|
401
|
+
// G1 fail-safe: any probe failure → unknown → warn (never refuse).
|
|
402
|
+
signals.driveType = 'unknown';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (platform === 'linux') {
|
|
408
|
+
const probeMounts = deps.probeMounts || (p => defaultProbeMounts(p, deps));
|
|
409
|
+
try {
|
|
410
|
+
signals.mountFsType = probeMounts(absPath);
|
|
411
|
+
} catch {
|
|
412
|
+
// M3: a THROWN probe is distinct from a clean no-match. Record the throw so
|
|
413
|
+
// classifyLinux maps it to unknown/warn (symmetric with win32) instead of
|
|
414
|
+
// silently fail-opening to local-ok.
|
|
415
|
+
signals.mountFsType = null;
|
|
416
|
+
signals.mountProbeThrew = true;
|
|
417
|
+
}
|
|
418
|
+
const readWsl = deps.readWslInterop || (() => defaultReadWslInterop(deps));
|
|
419
|
+
try {
|
|
420
|
+
signals.isWslInterop = Boolean(readWsl());
|
|
421
|
+
} catch {
|
|
422
|
+
signals.isWslInterop = false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return signals;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function classifyFilesystem(absPath, deps = {}) {
|
|
430
|
+
return classifyFromSignals(gatherSignals(absPath, deps));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// --- Default-on gate helper --------------------------------------------------
|
|
434
|
+
|
|
435
|
+
// The unsafe-FS override is a deliberate, explicit opt-in: only the strings "1"
|
|
436
|
+
// and "true" (case-insensitive) enable it. A truthy-but-disabling value such as
|
|
437
|
+
// "0" or "false" — which Boolean() would wrongly treat as enabled — leaves the
|
|
438
|
+
// refuse gate ACTIVE. Anything else (unset, "", "no", "off") is also not active.
|
|
439
|
+
function isUnsafeFsOverrideActive(env = {}) {
|
|
440
|
+
const raw = env.FORGE_KERNEL_ALLOW_UNSAFE_FS;
|
|
441
|
+
if (typeof raw !== 'string') return false;
|
|
442
|
+
const value = raw.trim().toLowerCase();
|
|
443
|
+
return value === '1' || value === 'true';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function assertFilesystemSafeForKernel(dbPath, deps = {}) {
|
|
447
|
+
const env = deps.env || process.env;
|
|
448
|
+
const warn = deps.warn || (msg => console.warn(msg));
|
|
449
|
+
const classify = deps.classifyFilesystem || classifyFilesystem;
|
|
450
|
+
|
|
451
|
+
const classification = classify(dbPath, deps);
|
|
452
|
+
const remediation = REMEDIATION[classification.remediationKey] || REMEDIATION.unknown;
|
|
453
|
+
|
|
454
|
+
if (classification.riskTier === 'safe') {
|
|
455
|
+
return classification;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (classification.riskTier === 'warn') {
|
|
459
|
+
warn(`forge kernel filesystem warning (${classification.class}):\n${remediation}`);
|
|
460
|
+
return classification;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// refuse
|
|
464
|
+
const overrideActive = isUnsafeFsOverrideActive(env);
|
|
465
|
+
if (overrideActive) {
|
|
466
|
+
warn(
|
|
467
|
+
`forge kernel filesystem REFUSE downgraded by FORGE_KERNEL_ALLOW_UNSAFE_FS ` +
|
|
468
|
+
`(${classification.class}) — override active:\n${remediation}`,
|
|
469
|
+
);
|
|
470
|
+
return classification;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const error = new Error(
|
|
474
|
+
`Forge kernel refuses to initialize on a ${classification.class} filesystem.\n\n` +
|
|
475
|
+
` Detected: ${classification.signal}\n` +
|
|
476
|
+
` Path: ${dbPath}\n` +
|
|
477
|
+
` Class: ${classification.class}\n\n` +
|
|
478
|
+
`${remediation}`,
|
|
479
|
+
);
|
|
480
|
+
error.code = 'FORGE_KERNEL_UNSAFE_FS';
|
|
481
|
+
error.classification = classification;
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
module.exports = {
|
|
486
|
+
classifyFromSignals,
|
|
487
|
+
gatherSignals,
|
|
488
|
+
classifyFilesystem,
|
|
489
|
+
assertFilesystemSafeForKernel,
|
|
490
|
+
isUnsafeFsOverrideActive,
|
|
491
|
+
defaultProbeDriveType,
|
|
492
|
+
parseNetUseDriveType,
|
|
493
|
+
parseDisplayRoot,
|
|
494
|
+
REMEDIATION,
|
|
495
|
+
};
|