@garygentry/feature-forge 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/adapters/GENERATION-REPORT.md +17 -13
- package/adapters/claude/.feature-forge-bundle.json +6 -0
- package/adapters/claude/references/forge-config-schema.json +36 -10
- package/adapters/claude/references/pipeline-state-schema.json +4 -0
- package/adapters/claude/references/portable-root.md +8 -5
- package/adapters/claude/references/process-overview.md +15 -5
- package/adapters/claude/references/shared-conventions.md +69 -4
- package/adapters/claude/references/stack-resolution.md +4 -1
- package/adapters/claude/references/stacks/go.md +1 -1
- package/adapters/claude/references/stacks/python.md +1 -1
- package/adapters/claude/references/stacks/rust.md +1 -1
- package/adapters/claude/references/stacks/typescript.md +1 -1
- package/adapters/claude/references/templates/specs-hygiene/AGENTS.md +23 -0
- package/adapters/claude/references/templates/specs-hygiene/CLAUDE.md +22 -0
- package/adapters/claude/scripts/epic-manifest.py +1379 -0
- package/adapters/claude/scripts/forge-bootstrap.py +991 -0
- package/adapters/claude/scripts/forge-init.sh +44 -0
- package/adapters/claude/scripts/forge-root.sh +30 -8
- package/adapters/claude/scripts/validate-traceability.py +150 -0
- package/adapters/claude/skills/forge/SKILL.md +5 -5
- package/adapters/claude/skills/forge-0-epic/SKILL.md +13 -15
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/claude/skills/forge-1-prd/SKILL.md +6 -4
- package/adapters/claude/skills/forge-2-tech/SKILL.md +8 -7
- package/adapters/claude/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/claude/skills/forge-3-specs/SKILL.md +1 -1
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +2 -2
- package/adapters/claude/skills/forge-5-loop/SKILL.md +20 -18
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +13 -0
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +40 -0
- package/adapters/claude/skills/forge-6-docs/SKILL.md +11 -1
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +240 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/go/main.go +12 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
- package/adapters/claude/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
- package/adapters/claude/skills/forge-fix/SKILL.md +1 -1
- package/adapters/claude/skills/forge-init/SKILL.md +1 -1
- package/adapters/claude/skills/forge-verify/SKILL.md +7 -2
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/codex/.feature-forge-bundle.json +6 -0
- package/adapters/codex/agents/{forge-researcher.md → forge-researcher.toml} +4 -4
- package/adapters/codex/agents/{forge-spec-writer.md → forge-spec-writer.toml} +4 -4
- package/adapters/codex/agents/{forge-verifier.md → forge-verifier.toml} +4 -4
- package/adapters/codex/references/forge-config-schema.json +36 -10
- package/adapters/codex/references/pipeline-state-schema.json +4 -0
- package/adapters/codex/references/portable-root.md +8 -5
- package/adapters/codex/references/process-overview.md +15 -5
- package/adapters/codex/references/shared-conventions.md +69 -4
- package/adapters/codex/references/stack-resolution.md +4 -1
- package/adapters/codex/references/stacks/go.md +1 -1
- package/adapters/codex/references/stacks/python.md +1 -1
- package/adapters/codex/references/stacks/rust.md +1 -1
- package/adapters/codex/references/stacks/typescript.md +1 -1
- package/adapters/codex/references/templates/specs-hygiene/AGENTS.md +23 -0
- package/adapters/codex/references/templates/specs-hygiene/CLAUDE.md +22 -0
- package/adapters/codex/scripts/epic-manifest.py +1379 -0
- package/adapters/codex/scripts/forge-bootstrap.py +991 -0
- package/adapters/codex/scripts/forge-init.sh +44 -0
- package/adapters/codex/scripts/forge-root.sh +30 -8
- package/adapters/codex/scripts/validate-traceability.py +150 -0
- package/adapters/codex/skills/forge/{forge.md → SKILL.md} +16 -6
- package/adapters/codex/skills/forge-0-epic/{forge-0-epic.md → SKILL.md} +33 -25
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/codex/skills/forge-1-prd/{forge-1-prd.md → SKILL.md} +22 -10
- package/adapters/codex/skills/forge-2-tech/{forge-2-tech.md → SKILL.md} +26 -15
- package/adapters/codex/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/codex/skills/forge-3-specs/{forge-3-specs.md → SKILL.md} +16 -6
- package/adapters/codex/skills/forge-4-backlog/{forge-4-backlog.md → SKILL.md} +15 -5
- package/adapters/codex/skills/forge-5-loop/{forge-5-loop.md → SKILL.md} +40 -28
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +13 -0
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +40 -0
- package/adapters/codex/skills/forge-6-docs/{forge-6-docs.md → SKILL.md} +26 -6
- package/adapters/codex/skills/forge-bootstrap/SKILL.md +249 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/go/main.go +12 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
- package/adapters/codex/skills/forge-fix/{forge-fix.md → SKILL.md} +12 -2
- package/adapters/codex/skills/forge-init/{forge-init.md → SKILL.md} +11 -1
- package/adapters/codex/skills/forge-verify/{forge-verify.md → SKILL.md} +24 -9
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/copilot/.feature-forge-bundle.json +6 -0
- package/adapters/copilot/references/forge-config-schema.json +36 -10
- package/adapters/copilot/references/pipeline-state-schema.json +4 -0
- package/adapters/copilot/references/portable-root.md +8 -5
- package/adapters/copilot/references/process-overview.md +15 -5
- package/adapters/copilot/references/shared-conventions.md +69 -4
- package/adapters/copilot/references/stack-resolution.md +4 -1
- package/adapters/copilot/references/stacks/go.md +1 -1
- package/adapters/copilot/references/stacks/python.md +1 -1
- package/adapters/copilot/references/stacks/rust.md +1 -1
- package/adapters/copilot/references/stacks/typescript.md +1 -1
- package/adapters/copilot/references/templates/specs-hygiene/AGENTS.md +23 -0
- package/adapters/copilot/references/templates/specs-hygiene/CLAUDE.md +22 -0
- package/adapters/copilot/scripts/epic-manifest.py +1379 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +991 -0
- package/adapters/copilot/scripts/forge-init.sh +44 -0
- package/adapters/copilot/scripts/forge-root.sh +30 -8
- package/adapters/copilot/scripts/validate-traceability.py +150 -0
- package/adapters/copilot/skills/forge/forge.md +16 -6
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +33 -25
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +22 -10
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +26 -15
- package/adapters/copilot/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +16 -6
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +15 -5
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +40 -28
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +13 -0
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +40 -0
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +26 -6
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +249 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/go/main.go +12 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
- package/adapters/copilot/skills/forge-fix/forge-fix.md +12 -2
- package/adapters/copilot/skills/forge-init/forge-init.md +11 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +24 -9
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/cursor/.feature-forge-bundle.json +6 -0
- package/adapters/cursor/references/forge-config-schema.json +36 -10
- package/adapters/cursor/references/pipeline-state-schema.json +4 -0
- package/adapters/cursor/references/portable-root.md +8 -5
- package/adapters/cursor/references/process-overview.md +15 -5
- package/adapters/cursor/references/shared-conventions.md +69 -4
- package/adapters/cursor/references/stack-resolution.md +4 -1
- package/adapters/cursor/references/stacks/go.md +1 -1
- package/adapters/cursor/references/stacks/python.md +1 -1
- package/adapters/cursor/references/stacks/rust.md +1 -1
- package/adapters/cursor/references/stacks/typescript.md +1 -1
- package/adapters/cursor/references/templates/specs-hygiene/AGENTS.md +23 -0
- package/adapters/cursor/references/templates/specs-hygiene/CLAUDE.md +22 -0
- package/adapters/cursor/scripts/epic-manifest.py +1379 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +991 -0
- package/adapters/cursor/scripts/forge-init.sh +44 -0
- package/adapters/cursor/scripts/forge-root.sh +30 -8
- package/adapters/cursor/scripts/validate-traceability.py +150 -0
- package/adapters/cursor/skills/forge/forge.mdc +16 -6
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +33 -25
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +22 -10
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +26 -15
- package/adapters/cursor/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +16 -6
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +15 -5
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +40 -28
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +13 -0
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +40 -0
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +26 -6
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +250 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/go/main.go +12 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +12 -2
- package/adapters/cursor/skills/forge-init/forge-init.mdc +11 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +24 -9
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +1 -1
- package/adapters/gemini/.feature-forge-bundle.json +6 -0
- package/adapters/gemini/gemini-extension.json +5 -1
- package/adapters/gemini/references/forge-config-schema.json +36 -10
- package/adapters/gemini/references/pipeline-state-schema.json +4 -0
- package/adapters/gemini/references/portable-root.md +8 -5
- package/adapters/gemini/references/process-overview.md +15 -5
- package/adapters/gemini/references/shared-conventions.md +69 -4
- package/adapters/gemini/references/stack-resolution.md +4 -1
- package/adapters/gemini/references/stacks/go.md +1 -1
- package/adapters/gemini/references/stacks/python.md +1 -1
- package/adapters/gemini/references/stacks/rust.md +1 -1
- package/adapters/gemini/references/stacks/typescript.md +1 -1
- package/adapters/gemini/references/templates/specs-hygiene/AGENTS.md +23 -0
- package/adapters/gemini/references/templates/specs-hygiene/CLAUDE.md +22 -0
- package/adapters/gemini/scripts/epic-manifest.py +1379 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +991 -0
- package/adapters/gemini/scripts/forge-init.sh +44 -0
- package/adapters/gemini/scripts/forge-root.sh +30 -8
- package/adapters/gemini/scripts/validate-traceability.py +150 -0
- package/adapters/gemini/skills/forge/forge.md +16 -6
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +33 -25
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +2 -2
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +1 -1
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +22 -10
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +26 -15
- package/adapters/gemini/skills/forge-2-tech/references/stack-discovery-checklist.md +4 -4
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +16 -6
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +15 -5
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +40 -28
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +13 -0
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +40 -0
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +26 -6
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +249 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/go/main.go +12 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +24 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +25 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
- package/adapters/gemini/skills/forge-fix/forge-fix.md +12 -2
- package/adapters/gemini/skills/forge-init/forge-init.md +11 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +24 -9
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +1 -1
- package/dist/agent-targets.d.ts +20 -4
- package/dist/agent-targets.js +29 -4
- package/dist/apply.js +245 -18
- package/dist/cli.js +12 -6
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +7 -0
- package/dist/manifest.d.ts +4 -2
- package/dist/manifest.js +58 -2
- package/dist/placements.d.ts +69 -0
- package/dist/placements.js +116 -0
- package/dist/plan.d.ts +7 -0
- package/dist/plan.js +87 -1
- package/dist/rauf.d.ts +4 -4
- package/dist/rauf.js +3 -3
- package/dist/report.js +21 -0
- package/dist/source.d.ts +4 -3
- package/dist/source.js +4 -3
- package/dist/types.d.ts +163 -19
- package/dist/types.js +42 -11
- package/package.json +1 -1
- package/adapters/codex/agents/openai.yaml +0 -10
package/dist/apply.js
CHANGED
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
* Zero runtime dependencies; only `node:` built-ins.
|
|
13
13
|
*/
|
|
14
14
|
import * as fsp from "node:fs/promises";
|
|
15
|
+
import * as fs from "node:fs";
|
|
15
16
|
import * as path from "node:path";
|
|
16
17
|
import { ok, err } from "./types.js";
|
|
17
|
-
import { sha256File } from "./hash.js";
|
|
18
|
+
import { sha256File, sha256String } from "./hash.js";
|
|
18
19
|
import { resolveWithin, symlinkDir, removePath, removeEmptyDirsWithin, } from "./fsutil.js";
|
|
19
20
|
import { buildManifest, writeManifest } from "./manifest.js";
|
|
21
|
+
import { wrapBlock, upsertBlock, removeBlock } from "./placements.js";
|
|
20
22
|
/**
|
|
21
23
|
* Execute one agent's `PlannedAction` against the filesystem, then write/delete the manifest.
|
|
22
24
|
* Returns an `AgentReport` instead of throwing (REQ-OBS-03). See spec 04 §5.
|
|
@@ -84,8 +86,13 @@ async function applyCopyInstall(planned, ctx) {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
+
// Secondary placements (A4b) execute regardless of the primary mode; collect their inventory.
|
|
90
|
+
const placementResult = await applyPlacements(planned.placements ?? [], ctx, source);
|
|
91
|
+
if (!placementResult.ok)
|
|
92
|
+
return fail(ctx, planned, placementResult.error);
|
|
93
|
+
// No-op short-circuit (REQ-IDEM-01): every action — primary AND placement — unchanged ⇒ zero
|
|
94
|
+
// writes, manifest untouched.
|
|
95
|
+
if (allUnchanged(planned)) {
|
|
89
96
|
return success(ctx, planned);
|
|
90
97
|
}
|
|
91
98
|
const manifest = buildManifest({
|
|
@@ -97,6 +104,7 @@ async function applyCopyInstall(planned, ctx) {
|
|
|
97
104
|
skills: source.skills,
|
|
98
105
|
sourceHash: source.sourceHash,
|
|
99
106
|
raufPin: ctx.raufPin,
|
|
107
|
+
placements: placementResult.value,
|
|
100
108
|
previous: ctx.priorManifest,
|
|
101
109
|
now: () => new Date(ctx.now),
|
|
102
110
|
});
|
|
@@ -105,6 +113,11 @@ async function applyCopyInstall(planned, ctx) {
|
|
|
105
113
|
return fail(ctx, planned, wrote.error);
|
|
106
114
|
return success(ctx, planned);
|
|
107
115
|
}
|
|
116
|
+
/** True iff every planned action — primary files and all placement files — is "unchanged". */
|
|
117
|
+
function allUnchanged(planned) {
|
|
118
|
+
return (planned.files.every((f) => f.action === "unchanged") &&
|
|
119
|
+
(planned.placements ?? []).every((p) => p.files.every((f) => f.action === "unchanged")));
|
|
120
|
+
}
|
|
108
121
|
/** §5.3 copy-mode uninstall: remove recorded files, prune empty dirs, delete the manifest LAST. */
|
|
109
122
|
async function applyCopyUninstall(planned, ctx) {
|
|
110
123
|
for (const fa of planned.files) {
|
|
@@ -118,6 +131,9 @@ async function applyCopyUninstall(planned, ctx) {
|
|
|
118
131
|
const pruned = await removeEmptyDirsWithin(ctx.destination, ctx.agentRoot);
|
|
119
132
|
if (!pruned.ok)
|
|
120
133
|
return fail(ctx, planned, pruned.error);
|
|
134
|
+
const placementsRemoved = await removePlacements(planned.placements ?? []);
|
|
135
|
+
if (!placementsRemoved.ok)
|
|
136
|
+
return fail(ctx, planned, placementsRemoved.error);
|
|
121
137
|
const deleted = await deleteManifest(ctx);
|
|
122
138
|
if (!deleted.ok)
|
|
123
139
|
return fail(ctx, planned, deleted.error);
|
|
@@ -140,23 +156,35 @@ async function applySymlinkInstall(planned, ctx) {
|
|
|
140
156
|
if (!resolved.ok)
|
|
141
157
|
return fail(ctx, planned, resolved.error);
|
|
142
158
|
const linkPath = resolved.value;
|
|
143
|
-
//
|
|
144
|
-
|
|
159
|
+
// Secondary placements (A4b) apply even in symlink mode — they live under a different root than the
|
|
160
|
+
// symlinked namespace dir. Run them first so a placement-only change still rewrites the manifest.
|
|
161
|
+
const placementResult = await applyPlacements(planned.placements ?? [], ctx, source);
|
|
162
|
+
if (!placementResult.ok)
|
|
163
|
+
return fail(ctx, planned, placementResult.error);
|
|
164
|
+
const primary = planned.files;
|
|
165
|
+
const primaryUntouched = primary.every((f) => f.action === "unchanged") ||
|
|
166
|
+
primary.every((f) => f.action === "skip-modified");
|
|
167
|
+
// Nothing changed anywhere ⇒ zero writes, manifest untouched.
|
|
168
|
+
if (allUnchanged(planned)) {
|
|
145
169
|
return success(ctx, planned);
|
|
146
170
|
}
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
// The recorded mode/link reflect the primary namespace dir: only (re)link when it actually changed
|
|
172
|
+
// (a placement-only change leaves a live link and its prior manifest mode/link intact).
|
|
173
|
+
let effectiveMode = ctx.priorManifest?.mode ?? "symlink";
|
|
174
|
+
let linkTarget = ctx.priorManifest?.link?.target ?? (effectiveMode === "symlink" ? source.root : undefined);
|
|
175
|
+
let files = ctx.priorManifest?.files ?? source.files.map((f) => ({ path: f.relpath }));
|
|
176
|
+
if (!primaryUntouched) {
|
|
177
|
+
const removed = await removePath(linkPath);
|
|
178
|
+
if (!removed.ok)
|
|
179
|
+
return fail(ctx, planned, removed.error);
|
|
180
|
+
const linked = await symlinkDir(source.root, linkPath);
|
|
181
|
+
if (!linked.ok)
|
|
182
|
+
return fail(ctx, planned, linked.error);
|
|
183
|
+
effectiveMode = linked.value.mode;
|
|
184
|
+
linkTarget = effectiveMode === "symlink" ? source.root : undefined;
|
|
185
|
+
// files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
|
|
186
|
+
files = source.files.map((f) => ({ path: f.relpath }));
|
|
150
187
|
}
|
|
151
|
-
const removed = await removePath(linkPath);
|
|
152
|
-
if (!removed.ok)
|
|
153
|
-
return fail(ctx, planned, removed.error);
|
|
154
|
-
const linked = await symlinkDir(source.root, linkPath);
|
|
155
|
-
if (!linked.ok)
|
|
156
|
-
return fail(ctx, planned, linked.error);
|
|
157
|
-
const effectiveMode = linked.value.mode;
|
|
158
|
-
// files[] lists the bundle-relative paths with sha256 OMITTED (no per-file copy exists, 00 §3).
|
|
159
|
-
const files = source.files.map((f) => ({ path: f.relpath }));
|
|
160
188
|
const manifest = buildManifest({
|
|
161
189
|
agent: ctx.agent,
|
|
162
190
|
scope: ctx.scope,
|
|
@@ -166,8 +194,9 @@ async function applySymlinkInstall(planned, ctx) {
|
|
|
166
194
|
skills: source.skills,
|
|
167
195
|
sourceHash: source.sourceHash,
|
|
168
196
|
raufPin: ctx.raufPin,
|
|
197
|
+
placements: placementResult.value,
|
|
169
198
|
// Truthful record: a copy fallback must NOT carry link (copy-mode manifest invariant, 05).
|
|
170
|
-
...(
|
|
199
|
+
...(linkTarget !== undefined ? { link: { target: linkTarget } } : {}),
|
|
171
200
|
previous: ctx.priorManifest,
|
|
172
201
|
now: () => new Date(ctx.now),
|
|
173
202
|
});
|
|
@@ -184,12 +213,210 @@ async function applySymlinkUninstall(planned, ctx) {
|
|
|
184
213
|
const removed = await removePath(resolved.value);
|
|
185
214
|
if (!removed.ok)
|
|
186
215
|
return fail(ctx, planned, removed.error);
|
|
216
|
+
const placementsRemoved = await removePlacements(planned.placements ?? []);
|
|
217
|
+
if (!placementsRemoved.ok)
|
|
218
|
+
return fail(ctx, planned, placementsRemoved.error);
|
|
187
219
|
const deleted = await deleteManifest(ctx);
|
|
188
220
|
if (!deleted.ok)
|
|
189
221
|
return fail(ctx, planned, deleted.error);
|
|
190
222
|
return success(ctx, planned);
|
|
191
223
|
}
|
|
192
224
|
// ---------------------------------------------------------------------------
|
|
225
|
+
// Secondary placements (A4b)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
/**
|
|
228
|
+
* Execute every secondary placement for an install/update and return the inventory to record in the
|
|
229
|
+
* manifest. Each placement is contained to ITS OWN root (`resolveWithin(placement.root, …)`), so a
|
|
230
|
+
* mirror under `.codex` and a managed block under `.github` never escape their boundary (REQ-SEC-02).
|
|
231
|
+
* Unchanged/skip-modified entries carry their prior recorded hash forward so the manifest stays
|
|
232
|
+
* faithful. Never throws for expected errors.
|
|
233
|
+
*/
|
|
234
|
+
async function applyPlacements(placements, ctx, source) {
|
|
235
|
+
const priorByDest = new Map();
|
|
236
|
+
for (const p of ctx.priorManifest?.placements ?? [])
|
|
237
|
+
priorByDest.set(p.destination, p);
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const pl of placements) {
|
|
240
|
+
const prior = priorByDest.get(pl.destination) ?? null;
|
|
241
|
+
const res = pl.kind === "mirror"
|
|
242
|
+
? await applyMirror(pl, ctx, source, prior)
|
|
243
|
+
: await applyManagedBlock(pl, prior);
|
|
244
|
+
if (!res.ok)
|
|
245
|
+
return res;
|
|
246
|
+
out.push(res.value);
|
|
247
|
+
}
|
|
248
|
+
return ok(out);
|
|
249
|
+
}
|
|
250
|
+
/** §A4b mirror: copy/refresh/remove flat files under the second root; record per-file sha256. */
|
|
251
|
+
async function applyMirror(pl, ctx, source, prior) {
|
|
252
|
+
const priorByPath = new Map();
|
|
253
|
+
for (const f of prior?.files ?? [])
|
|
254
|
+
priorByPath.set(f.path, f);
|
|
255
|
+
const writeFile = ctx.writeFileSeam ?? defaultCopyFile;
|
|
256
|
+
const inventory = [];
|
|
257
|
+
for (const fa of pl.files) {
|
|
258
|
+
const resolved = resolveWithin(pl.root, pl.destination, fa.relpath);
|
|
259
|
+
if (!resolved.ok)
|
|
260
|
+
return resolved;
|
|
261
|
+
const destAbs = resolved.value;
|
|
262
|
+
switch (fa.action) {
|
|
263
|
+
case "create":
|
|
264
|
+
case "overwrite": {
|
|
265
|
+
if (fa.srcRelpath === undefined) {
|
|
266
|
+
return err({
|
|
267
|
+
code: "UNEXPECTED",
|
|
268
|
+
agent: ctx.agent,
|
|
269
|
+
message: `mirror action for "${fa.relpath}" is missing its source path`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const wrote = await writeFile(path.join(source.root, fa.srcRelpath), destAbs);
|
|
273
|
+
if (!wrote.ok)
|
|
274
|
+
return wrote;
|
|
275
|
+
inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "remove": {
|
|
279
|
+
const removed = await removePath(destAbs);
|
|
280
|
+
if (!removed.ok)
|
|
281
|
+
return removed;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "unchanged":
|
|
285
|
+
case "skip-modified": {
|
|
286
|
+
// Carry the prior record forward; if none exists (e.g. a v1→v2 manifest migration where the
|
|
287
|
+
// file is already on disk), reconstruct it by hashing the destination so the inventory stays
|
|
288
|
+
// faithful rather than silently dropping an unrecorded-but-present file.
|
|
289
|
+
const p = priorByPath.get(fa.relpath);
|
|
290
|
+
if (p !== undefined)
|
|
291
|
+
inventory.push(p);
|
|
292
|
+
else
|
|
293
|
+
inventory.push({ path: fa.relpath, sha256: sha256File(destAbs) });
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return ok({ kind: "mirror", root: pl.root, destination: pl.destination, files: inventory });
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* §A4b managed-block: merge/refresh the sentinel block into the (possibly user-owned) target file,
|
|
302
|
+
* preserving everything outside the sentinels. Records a single inventory entry whose sha256 is the
|
|
303
|
+
* written region's hash. skip-modified/unchanged carry the prior record forward (no write).
|
|
304
|
+
*/
|
|
305
|
+
async function applyManagedBlock(pl, prior) {
|
|
306
|
+
const fa = pl.files[0];
|
|
307
|
+
const basename = fa?.relpath ?? path.basename(pl.destination);
|
|
308
|
+
const resolved = resolveWithin(pl.root, pl.destination);
|
|
309
|
+
if (!resolved.ok)
|
|
310
|
+
return resolved;
|
|
311
|
+
const fileAbs = resolved.value;
|
|
312
|
+
const carry = () => {
|
|
313
|
+
const p = prior?.files.find((f) => f.path === basename);
|
|
314
|
+
return {
|
|
315
|
+
kind: "managed-block",
|
|
316
|
+
root: pl.root,
|
|
317
|
+
destination: pl.destination,
|
|
318
|
+
files: p !== undefined ? [p] : [],
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
if (fa === undefined || fa.action === "unchanged" || fa.action === "skip-modified") {
|
|
322
|
+
return ok(carry());
|
|
323
|
+
}
|
|
324
|
+
if (fa.action === "remove") {
|
|
325
|
+
// Uninstall is handled by removePlacements; an install-plan never yields "remove" here.
|
|
326
|
+
return ok(carry());
|
|
327
|
+
}
|
|
328
|
+
// create | overwrite — read existing (or treat as empty), upsert the block, write back.
|
|
329
|
+
const body = pl.blockContent ?? "";
|
|
330
|
+
let existing = "";
|
|
331
|
+
try {
|
|
332
|
+
existing = fs.readFileSync(fileAbs, "utf8");
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
existing = "";
|
|
336
|
+
}
|
|
337
|
+
const next = upsertBlock(existing, body);
|
|
338
|
+
const wrote = await writeText(fileAbs, next);
|
|
339
|
+
if (!wrote.ok)
|
|
340
|
+
return wrote;
|
|
341
|
+
return ok({
|
|
342
|
+
kind: "managed-block",
|
|
343
|
+
root: pl.root,
|
|
344
|
+
destination: pl.destination,
|
|
345
|
+
files: [{ path: basename, sha256: sha256String(wrapBlock(body)) }],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Remove every secondary placement during uninstall (A4b): a "mirror" deletes each recorded file
|
|
350
|
+
* (and prunes its now-empty dir); a "managed-block" strips ONLY the sentinel region, deleting the
|
|
351
|
+
* file only if nothing else remains. User content outside the block is always preserved.
|
|
352
|
+
*/
|
|
353
|
+
async function removePlacements(placements) {
|
|
354
|
+
for (const pl of placements) {
|
|
355
|
+
if (pl.kind === "managed-block") {
|
|
356
|
+
const resolved = resolveWithin(pl.root, pl.destination);
|
|
357
|
+
if (!resolved.ok)
|
|
358
|
+
return resolved;
|
|
359
|
+
const fileAbs = resolved.value;
|
|
360
|
+
let existing;
|
|
361
|
+
try {
|
|
362
|
+
existing = fs.readFileSync(fileAbs, "utf8");
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
continue; // already gone — nothing to strip
|
|
366
|
+
}
|
|
367
|
+
const stripped = removeBlock(existing);
|
|
368
|
+
if (stripped === "") {
|
|
369
|
+
const removed = await removePath(fileAbs);
|
|
370
|
+
if (!removed.ok)
|
|
371
|
+
return removed;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const wrote = await writeText(fileAbs, stripped);
|
|
375
|
+
if (!wrote.ok)
|
|
376
|
+
return wrote;
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// mirror: remove each recorded file, then prune the now-empty destination dir.
|
|
381
|
+
for (const fa of pl.files) {
|
|
382
|
+
const resolved = resolveWithin(pl.root, pl.destination, fa.relpath);
|
|
383
|
+
if (!resolved.ok)
|
|
384
|
+
return resolved;
|
|
385
|
+
const removed = await removePath(resolved.value);
|
|
386
|
+
if (!removed.ok)
|
|
387
|
+
return removed;
|
|
388
|
+
}
|
|
389
|
+
const pruned = await removeEmptyDirsWithin(pl.destination, pl.root);
|
|
390
|
+
if (!pruned.ok)
|
|
391
|
+
return pruned;
|
|
392
|
+
}
|
|
393
|
+
return ok(undefined);
|
|
394
|
+
}
|
|
395
|
+
/** Write text content to `destAbs`, creating the parent dir; maps EACCES/EPERM to WRITE_DENIED. */
|
|
396
|
+
async function writeText(destAbs, content) {
|
|
397
|
+
try {
|
|
398
|
+
await fsp.mkdir(path.dirname(destAbs), { recursive: true });
|
|
399
|
+
await fsp.writeFile(destAbs, content, "utf8");
|
|
400
|
+
return ok(undefined);
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
const code = e?.code;
|
|
404
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
405
|
+
return err({
|
|
406
|
+
code: "WRITE_DENIED",
|
|
407
|
+
message: `no write permission to ${destAbs}`,
|
|
408
|
+
path: destAbs,
|
|
409
|
+
remedy: "check directory permissions, or choose a different scope (--global vs project)",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return err({
|
|
413
|
+
code: "UNEXPECTED",
|
|
414
|
+
message: `filesystem error at ${destAbs}: ${e?.message ?? String(e)}`,
|
|
415
|
+
path: destAbs,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
193
420
|
// Internal helpers
|
|
194
421
|
// ---------------------------------------------------------------------------
|
|
195
422
|
/** Default per-file write seam: ensure the parent dir, then copy the source bytes. */
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,8 @@ import { readFileSync, realpathSync } from "node:fs";
|
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import { AGENT_IDS, AGENT_TARGETS, EXIT, err, ok, } from "./types.js";
|
|
19
|
-
import { detectAgent, detectAgents,
|
|
19
|
+
import { detectAgent, detectAgents, agentRootFor } from "./agent-targets.js"; // 02
|
|
20
|
+
import { resolvePlacements } from "./placements.js"; // 02 (A4b second-root placements)
|
|
20
21
|
import { locateSource } from "./source.js"; // 03
|
|
21
22
|
import { plan, resolveMode } from "./plan.js"; // 04
|
|
22
23
|
import { apply } from "./apply.js"; // 04
|
|
@@ -254,7 +255,8 @@ async function runMutation(subcommand, flags, env) {
|
|
|
254
255
|
for (const agent of targets) {
|
|
255
256
|
const detection = detectAgent(agent, ropts);
|
|
256
257
|
const r = await runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env);
|
|
257
|
-
|
|
258
|
+
// Carry the scope-effective confidence + docs URL onto the report for honest labeling (A4).
|
|
259
|
+
agentReports.push({ ...r, confidence: detection.confidence, docsUrl: detection.docsUrl });
|
|
258
260
|
}
|
|
259
261
|
const anyAgentFailed = agentReports.some((r) => !r.ok);
|
|
260
262
|
const exitCode = anyAgentFailed || raufError !== undefined ? EXIT.FAILURE : EXIT.SUCCESS;
|
|
@@ -274,9 +276,9 @@ async function runMutation(subcommand, flags, env) {
|
|
|
274
276
|
/** Run the pipeline for a single agent, returning its AgentReport (catches every expected error). */
|
|
275
277
|
async function runOneAgent(subcommand, agent, detection, flags, scope, mode, raufPin, env) {
|
|
276
278
|
const mpath = manifestPath(agent, scope, { home: env.home, cwd: env.cwd });
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const agentRoot =
|
|
279
|
+
// Containment boundary = the agent's install base dir (A4: decoupled from the detection dir,
|
|
280
|
+
// so codex contains under `.agents` and copilot under `.github`).
|
|
281
|
+
const agentRoot = agentRootFor(AGENT_TARGETS[agent], scope, { home: env.home, cwd: env.cwd });
|
|
280
282
|
// uninstall path: manifest → planUninstall → apply.
|
|
281
283
|
if (subcommand === "uninstall") {
|
|
282
284
|
const m = readManifest(mpath);
|
|
@@ -319,6 +321,9 @@ async function runOneAgent(subcommand, agent, detection, flags, scope, mode, rau
|
|
|
319
321
|
priorManifest: prior.value,
|
|
320
322
|
force: flags.force,
|
|
321
323
|
raufPin,
|
|
324
|
+
// A4b: resolve any second-root placements for this agent under the active scope (codex
|
|
325
|
+
// `.codex/agents`, copilot `.github/copilot-instructions.md`); empty for the rest.
|
|
326
|
+
placements: resolvePlacements(AGENT_TARGETS[agent], scope, { home: env.home, cwd: env.cwd }),
|
|
322
327
|
};
|
|
323
328
|
const planned = plan(subcommand, planCtx);
|
|
324
329
|
if (!planned.ok)
|
|
@@ -367,7 +372,8 @@ async function runList(flags, env) {
|
|
|
367
372
|
const agentReports = [];
|
|
368
373
|
for (const agent of targets) {
|
|
369
374
|
const detection = detectAgent(agent, ropts);
|
|
370
|
-
|
|
375
|
+
const base = listOneAgent(agent, detection, flags, scope, env);
|
|
376
|
+
agentReports.push({ ...base, confidence: detection.confidence, docsUrl: detection.docsUrl });
|
|
371
377
|
}
|
|
372
378
|
const anyFailed = agentReports.some((r) => !r.ok);
|
|
373
379
|
return {
|
package/dist/hash.d.ts
CHANGED
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* for an already-located, integrity-checked bundle, caught at the operation boundary.
|
|
16
16
|
*/
|
|
17
17
|
export declare function sha256File(filePath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* SHA-256 of a UTF-8 string, hex-encoded. Used to fingerprint a managed-block region (A4b) so the
|
|
20
|
+
* planner can tell a clean prior block (recorded hash matches) from a user-edited one. Pure.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sha256String(s: string): string;
|
|
18
23
|
/**
|
|
19
24
|
* Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
|
|
20
25
|
* set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
|
package/dist/hash.js
CHANGED
|
@@ -21,6 +21,13 @@ export function sha256File(filePath) {
|
|
|
21
21
|
const buf = fs.readFileSync(filePath);
|
|
22
22
|
return createHash("sha256").update(buf).digest("hex");
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 of a UTF-8 string, hex-encoded. Used to fingerprint a managed-block region (A4b) so the
|
|
26
|
+
* planner can tell a clean prior block (recorded hash matches) from a user-edited one. Pure.
|
|
27
|
+
*/
|
|
28
|
+
export function sha256String(s) {
|
|
29
|
+
return createHash("sha256").update(Buffer.from(s, "utf8")).digest("hex");
|
|
30
|
+
}
|
|
24
31
|
/**
|
|
25
32
|
* Deterministic SHA-256 over a directory tree's file set (OQ-4). The digest is a function of the
|
|
26
33
|
* set of `{ relativePosixPath, fileContentHash }` pairs ONLY — never of mtime, inode, or
|
package/dist/manifest.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Zero runtime dependencies; only `node:` built-ins. Named exports only. Core functions return
|
|
9
9
|
* `Result<T, E>` and never throw for expected errors; `JSON.parse` is wrapped in `try/catch`.
|
|
10
10
|
*/
|
|
11
|
-
import { type AgentId, type InstallManifest, type ManifestFile, type Mode, type PlannedAction, type ResolveOpts, type Result, type Scope } from "./types.js";
|
|
11
|
+
import { type AgentId, type InstallManifest, type ManifestFile, type Mode, type Placement, type PlannedAction, type ResolveOpts, type Result, type Scope } from "./types.js";
|
|
12
12
|
/**
|
|
13
13
|
* Inputs to {@link buildManifest}. The caller (apply.ts, spec 04) assembles this from the resolved
|
|
14
14
|
* detection target, the chosen scope/mode, and the apply result's per-file inventory.
|
|
@@ -28,12 +28,14 @@ export interface BuildManifestArgs {
|
|
|
28
28
|
readonly skills: readonly string[];
|
|
29
29
|
/** SHA-256 over the source bundle's canonical (sorted-path) file set — drift anchor (spec 03). */
|
|
30
30
|
readonly sourceHash: string;
|
|
31
|
-
/** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.
|
|
31
|
+
/** Recorded pinned rauf coordinate (e.g. "@garygentry/rauf@0.8.0"); `null` when `--skip-rauf` (spec 06). */
|
|
32
32
|
readonly raufPin: string | null;
|
|
33
33
|
/** Symlink mode only: the source bundle the namespace dir links to (REQ-SAFE-02). */
|
|
34
34
|
readonly link?: {
|
|
35
35
|
readonly target: string;
|
|
36
36
|
};
|
|
37
|
+
/** Secondary placement inventory written this run (A4b); omit/empty when the agent has none. */
|
|
38
|
+
readonly placements?: readonly Placement[];
|
|
37
39
|
/** Prior manifest, if any. When present, its `installedAt` is preserved (this is an update). */
|
|
38
40
|
readonly previous?: InstallManifest | null;
|
|
39
41
|
/** Injectable clock for deterministic tests. Default: `() => new Date()`. */
|
package/dist/manifest.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import { AGENT_TARGETS, MANIFEST_PREFIX, SCHEMA_VERSION, ok, err, } from "./types.js";
|
|
13
|
+
import { AGENT_TARGETS, MANIFEST_PREFIX, SCHEMA_VERSION, ok, err, READABLE_SCHEMA_VERSIONS, } from "./types.js";
|
|
14
14
|
import { destinationFor } from "./agent-targets.js";
|
|
15
15
|
/**
|
|
16
16
|
* Assemble an {@link InstallManifest} from an apply result (REQ-SAFE-01/03). Pure — no I/O.
|
|
@@ -40,8 +40,18 @@ export function buildManifest(args) {
|
|
|
40
40
|
skills,
|
|
41
41
|
files,
|
|
42
42
|
...(args.link !== undefined ? { link: args.link } : {}),
|
|
43
|
+
...(args.placements && args.placements.length > 0
|
|
44
|
+
? { placements: args.placements.map(normalizePlacement) }
|
|
45
|
+
: {}),
|
|
43
46
|
};
|
|
44
47
|
}
|
|
48
|
+
/** Canonicalize a placement for persistence: sort its file inventory by path (byte-wise). */
|
|
49
|
+
function normalizePlacement(p) {
|
|
50
|
+
const files = [...p.files]
|
|
51
|
+
.map((f) => ({ path: f.path, ...(f.sha256 !== undefined ? { sha256: f.sha256 } : {}) }))
|
|
52
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
53
|
+
return { kind: p.kind, root: p.root, destination: p.destination, files };
|
|
54
|
+
}
|
|
45
55
|
// ---------------------------------------------------------------------------
|
|
46
56
|
// manifestPath
|
|
47
57
|
// ---------------------------------------------------------------------------
|
|
@@ -142,7 +152,7 @@ function validateManifest(x) {
|
|
|
142
152
|
if (typeof x !== "object" || x === null)
|
|
143
153
|
return { ok: false, reason: "not an object" };
|
|
144
154
|
const o = x;
|
|
145
|
-
if (o.schemaVersion
|
|
155
|
+
if (!READABLE_SCHEMA_VERSIONS.includes(o.schemaVersion)) {
|
|
146
156
|
return { ok: false, reason: `unsupported schemaVersion ${String(o.schemaVersion)}` };
|
|
147
157
|
}
|
|
148
158
|
if (typeof o.agent !== "string" || !AGENT_IDS_SET.has(o.agent)) {
|
|
@@ -196,8 +206,45 @@ function validateManifest(x) {
|
|
|
196
206
|
if (o.mode === "copy" && o.link !== undefined) {
|
|
197
207
|
return { ok: false, reason: "copy mode manifest must not carry link" };
|
|
198
208
|
}
|
|
209
|
+
// Optional secondary placements (manifest v2, A4b). Absent on v1 and on agents without a rule.
|
|
210
|
+
if (o.placements !== undefined) {
|
|
211
|
+
if (!Array.isArray(o.placements))
|
|
212
|
+
return { ok: false, reason: "invalid placements[]" };
|
|
213
|
+
for (const p of o.placements) {
|
|
214
|
+
const reason = validatePlacement(p);
|
|
215
|
+
if (reason !== null)
|
|
216
|
+
return { ok: false, reason };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
199
219
|
return { ok: true, value: x };
|
|
200
220
|
}
|
|
221
|
+
/** Structural validation of one placement record; returns a reason string, or null if valid. */
|
|
222
|
+
function validatePlacement(p) {
|
|
223
|
+
if (typeof p !== "object" || p === null)
|
|
224
|
+
return "placements[] entry not an object";
|
|
225
|
+
const o = p;
|
|
226
|
+
if (o.kind !== "mirror" && o.kind !== "managed-block") {
|
|
227
|
+
return `invalid placement kind ${String(o.kind)}`;
|
|
228
|
+
}
|
|
229
|
+
if (typeof o.root !== "string" || o.root.length === 0)
|
|
230
|
+
return "placement missing root";
|
|
231
|
+
if (typeof o.destination !== "string" || o.destination.length === 0) {
|
|
232
|
+
return "placement missing destination";
|
|
233
|
+
}
|
|
234
|
+
if (!Array.isArray(o.files))
|
|
235
|
+
return "invalid placement files[]";
|
|
236
|
+
for (const f of o.files) {
|
|
237
|
+
if (typeof f !== "object" || f === null)
|
|
238
|
+
return "invalid placement files[] entry";
|
|
239
|
+
const ff = f;
|
|
240
|
+
if (typeof ff.path !== "string")
|
|
241
|
+
return "placement files[].path not a string";
|
|
242
|
+
if (ff.sha256 !== undefined && typeof ff.sha256 !== "string") {
|
|
243
|
+
return "placement files[].sha256 not a string";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
201
248
|
// ---------------------------------------------------------------------------
|
|
202
249
|
// planUninstall — the uninstall removal POLICY
|
|
203
250
|
// ---------------------------------------------------------------------------
|
|
@@ -212,11 +259,20 @@ export function planUninstall(manifest) {
|
|
|
212
259
|
const files = isSymlink
|
|
213
260
|
? [{ relpath: ".", action: "remove" }]
|
|
214
261
|
: manifest.files.map((f) => ({ relpath: f.path, action: "remove" }));
|
|
262
|
+
// Secondary placements (A4b) are removed too: a "mirror" deletes each recorded file; a
|
|
263
|
+
// "managed-block" strips only the sentinel region (apply interprets a "remove" action per kind).
|
|
264
|
+
const placements = (manifest.placements ?? []).map((p) => ({
|
|
265
|
+
kind: p.kind,
|
|
266
|
+
root: p.root,
|
|
267
|
+
destination: p.destination,
|
|
268
|
+
files: p.files.map((f) => ({ relpath: f.path, action: "remove" })),
|
|
269
|
+
}));
|
|
215
270
|
return ok({
|
|
216
271
|
agent: manifest.agent,
|
|
217
272
|
scope: manifest.scope,
|
|
218
273
|
mode: manifest.mode,
|
|
219
274
|
destination: manifest.destination,
|
|
220
275
|
files,
|
|
276
|
+
...(placements.length > 0 ? { placements } : {}),
|
|
221
277
|
});
|
|
222
278
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secondary install placements (A4b) — the second-root generalization the single-`destination`
|
|
3
|
+
* install model can't express. Two kinds (see {@link PlacementKind}):
|
|
4
|
+
* - "mirror" — codex copies the bundle's `agents/*.toml` FLAT into `.codex/agents/`, where
|
|
5
|
+
* Codex loads custom agents (it does NOT read them from `.agents/skills`).
|
|
6
|
+
* - "managed-block" — copilot writes a sentinel-delimited pointer block into the (possibly
|
|
7
|
+
* user-owned) `.github/copilot-instructions.md`, preserving the rest of it.
|
|
8
|
+
*
|
|
9
|
+
* This module is PURE: it resolves declarative {@link PlacementSpec}s to absolute roots, selects the
|
|
10
|
+
* mirror source files, and provides the managed-block string transforms (render/upsert/remove/read).
|
|
11
|
+
* The planner (plan.ts) decides actions and the apply engine (apply.ts) executes them; neither knows
|
|
12
|
+
* the per-kind string mechanics — those live here. Zero runtime dependencies; only `node:` built-ins.
|
|
13
|
+
*/
|
|
14
|
+
import { type AgentTarget, type PlacementKind, type PlacementSpec, type ResolveOpts, type Scope } from "./types.js";
|
|
15
|
+
import type { LocatedSource } from "./source.js";
|
|
16
|
+
/** A {@link PlacementSpec} resolved to absolute paths under a scope. Pure derivation; nothing stored. */
|
|
17
|
+
export interface ResolvedPlacement {
|
|
18
|
+
readonly kind: PlacementKind;
|
|
19
|
+
/** Absolute containment boundary (REQ-SEC-02): `<scopeRoot>/<spec.baseDir>`. */
|
|
20
|
+
readonly root: string;
|
|
21
|
+
/** Absolute destination: a DIR ("mirror") or a FILE ("managed-block"): `<root>/<spec.subpath>`. */
|
|
22
|
+
readonly destination: string;
|
|
23
|
+
readonly spec: PlacementSpec;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve every secondary placement declared on `target` to absolute roots under `scope` (A4b).
|
|
27
|
+
* Returns `[]` for agents with no placements (claude/cursor/gemini). Pure; the single derivation
|
|
28
|
+
* point so a new rule stays one `AGENT_TARGETS` edit (REQ-SCALE-01).
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolvePlacements(target: AgentTarget, scope: Scope, opts?: ResolveOpts): ResolvedPlacement[];
|
|
31
|
+
/** One selected mirror source: the bundle-relative source and its FLAT destination basename. */
|
|
32
|
+
export interface MirrorFile {
|
|
33
|
+
readonly srcRelpath: string;
|
|
34
|
+
readonly destRelpath: string;
|
|
35
|
+
readonly srcHash: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Select the bundle files a "mirror" placement copies (A4b): every `source.files` entry whose
|
|
39
|
+
* POSIX relpath starts with `spec.sourcePrefix`, copied FLAT (basename only) into the destination.
|
|
40
|
+
* Sorted by destination basename for deterministic plans. Pure.
|
|
41
|
+
*/
|
|
42
|
+
export declare function selectMirrorFiles(source: LocatedSource, spec: PlacementSpec): MirrorFile[];
|
|
43
|
+
/**
|
|
44
|
+
* Render the managed-block BODY (without sentinels) for copilot (A4b). Points Copilot — which has no
|
|
45
|
+
* skills loader — at the staged bundle under `.github/feature-forge/` and lists the available skills.
|
|
46
|
+
* Deterministic given the bundle's skill ids. Pure.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderCopilotBlock(skills: readonly string[]): string;
|
|
49
|
+
/** Wrap a rendered block body in the managed sentinels — the exact region written on disk. */
|
|
50
|
+
export declare function wrapBlock(body: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Extract the full managed region (sentinels INCLUDED) currently present in `content`, or `null` if
|
|
53
|
+
* no well-formed `start…end` region exists. The region is what `wrapBlock` produces, so its hash is
|
|
54
|
+
* directly comparable to a freshly rendered block. Pure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function extractManagedRegion(content: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Insert or replace the managed block in `existing`, preserving all user content outside the
|
|
59
|
+
* sentinels (A4b). If a region exists it is replaced in place; otherwise the block is appended after
|
|
60
|
+
* the existing content (separated by a blank line). `existing` is `""` for a not-yet-created file.
|
|
61
|
+
* The result always ends with a single trailing newline. Pure.
|
|
62
|
+
*/
|
|
63
|
+
export declare function upsertBlock(existing: string, body: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Remove the managed block from `existing`, preserving the rest (A4b uninstall). Returns the
|
|
66
|
+
* remaining content (trailing whitespace trimmed to a single newline), or `""` if nothing but the
|
|
67
|
+
* block (and whitespace) remains — the caller deletes the file in that case. Pure.
|
|
68
|
+
*/
|
|
69
|
+
export declare function removeBlock(existing: string): string;
|