@enterpriseai/cli 3.19.0 → 3.19.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/package.json +3 -3
- package/resources/deploy/eai-app-template/producer-pin.json +5 -5
- package/resources/deploy/eai-app-template/tests/source-unknown-deployment-evidence.test.mjs +2141 -1
- package/resources/gofer/.gofer-version +4 -357
- package/resources/gofer/agents-skills/eai/SKILL.md +365 -0
- package/resources/gofer/agents-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/agents-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/bash-scripts/create-new-feature.sh +3 -1
- package/resources/gofer/bash-scripts/sync-implementation-status.sh +4 -3
- package/resources/gofer/bash-scripts/update-agent-context.sh +7 -8
- package/resources/gofer/bash-scripts/validate-artifact.sh +10 -6
- package/resources/gofer/bash-scripts/verify-object-type-routing-workspace.sh +184 -0
- package/resources/gofer/claude-commands/eai-update.md +46 -0
- package/resources/gofer/claude-commands/eai.md +355 -0
- package/resources/gofer/claude-skills/eai/SKILL.md +365 -0
- package/resources/gofer/claude-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/claude-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/commands/0_gofer_start.md +440 -99
- package/resources/gofer/commands/0a_problem_validation.md +187 -27
- package/resources/gofer/commands/10_gofer_cloud.md +183 -23
- package/resources/gofer/commands/1_gofer_research.md +239 -30
- package/resources/gofer/commands/2_gofer_specify.md +239 -35
- package/resources/gofer/commands/3_gofer_plan.md +259 -43
- package/resources/gofer/commands/4_gofer_tasks.md +401 -81
- package/resources/gofer/commands/5_gofer_implement.md +375 -84
- package/resources/gofer/commands/6_gofer_validate.md +284 -53
- package/resources/gofer/commands/7_gofer_save.md +187 -26
- package/resources/gofer/commands/7a_stakeholder_comms.md +183 -23
- package/resources/gofer/commands/8_gofer_branding.md +183 -23
- package/resources/gofer/commands/9_gofer_tests.md +212 -23
- package/resources/gofer/commands/gofer_bootstrap_workspace.md +121 -8
- package/resources/gofer/commands/gofer_check_workspace.md +123 -8
- package/resources/gofer/commands/gofer_constitution.md +183 -23
- package/resources/gofer/commands/gofer_diagnose.md +183 -23
- package/resources/gofer/commands/gofer_eai_first_run.md +254 -25
- package/resources/gofer/commands/gofer_hydrate.md +183 -23
- package/resources/gofer/commands/gofer_personality.md +117 -6
- package/resources/gofer/commands/gofer_plan.md +117 -6
- package/resources/gofer/commands/gofer_side.md +117 -6
- package/resources/gofer/commands/gofer_spec_summary.md +183 -23
- package/resources/gofer/commands/gofer_tdd.md +183 -23
- package/resources/gofer/commands/gofer_vocabulary.md +185 -25
- package/resources/gofer/commands/gofer_zoom_out.md +183 -23
- package/resources/gofer/config/typesafe-learning-review.json +16 -0
- package/resources/gofer/config/typesafe-semantic-review.json +9 -0
- package/resources/gofer/copilot-instructions/gofer-cost.instructions.md +15 -6
- package/resources/gofer/copilot-instructions/typescript.instructions.md +1 -1
- package/resources/gofer/copilot-prompts/eai-update.prompt.md +58 -0
- package/resources/gofer/copilot-prompts/eai.prompt.md +367 -0
- package/resources/gofer/gemini/commands/gofer/eai-update.md +22 -0
- package/resources/gofer/gemini/commands/gofer/eai-update.toml +2 -0
- package/resources/gofer/gemini/commands/gofer/eai.md +357 -0
- package/resources/gofer/gemini/commands/gofer/eai.toml +2 -0
- package/resources/gofer/gemini/commands/gofer/manifest.json +3 -27
- package/resources/gofer/gemini/extension.json +4 -4
- package/resources/gofer/github-agents/eai.agent.md +83 -0
- package/resources/gofer/github-agents/gofer-business.agent.md +73 -4
- package/resources/gofer/github-agents/gofer-implement.agent.md +71 -2
- package/resources/gofer/github-agents/gofer-plan.agent.md +72 -2
- package/resources/gofer/github-agents/gofer-research.agent.md +72 -2
- package/resources/gofer/github-agents/gofer-validate.agent.md +72 -2
- package/resources/gofer/github-skills/eai/SKILL.md +365 -0
- package/resources/gofer/github-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/github-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/grok-skills/eai/SKILL.md +267 -14
- package/resources/gofer/grok-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/hook-scripts/user-prompt-submit.mjs +58 -13
- package/resources/gofer/instruction-templates/base/agents-base.md +8 -4
- package/resources/gofer/instruction-templates/gofer/gofer-claude.md +7 -5
- package/resources/gofer/instruction-templates/gofer/gofer-copilot.md +10 -8
- package/resources/gofer/memory/constitution.md +9 -7
- package/resources/gofer/memory/gofer-model-policy.yaml +23 -1
- package/resources/gofer/memory/lessons.md +0 -31
- package/resources/gofer/node-scripts/canonical-descriptions.mjs +1 -1
- package/resources/gofer/node-scripts/check-version-alignment.mjs +4 -7
- package/resources/gofer/node-scripts/delivery-lineage-corpus.mjs +156 -0
- package/resources/gofer/node-scripts/generate-commands.mjs +1073 -265
- package/resources/gofer/node-scripts/gofer-acceptance-check.mjs +143 -0
- package/resources/gofer/node-scripts/gofer-agent-catalog.mjs +271 -0
- package/resources/gofer/node-scripts/gofer-benchmark-executor.mjs +123 -0
- package/resources/gofer/node-scripts/gofer-benchmark-sign.mjs +62 -0
- package/resources/gofer/node-scripts/gofer-benchmark-signer.mjs +89 -0
- package/resources/gofer/node-scripts/gofer-benchmark.mjs +76 -0
- package/resources/gofer/node-scripts/gofer-blocker-control.mjs +274 -0
- package/resources/gofer/node-scripts/gofer-claude-adapter.mjs +154 -0
- package/resources/gofer/node-scripts/gofer-closed-loop-audit.mjs +19 -2
- package/resources/gofer/node-scripts/gofer-delivery-check.mjs +121 -0
- package/resources/gofer/node-scripts/gofer-encrypted-key.mjs +58 -0
- package/resources/gofer/node-scripts/gofer-execution-metrics.mjs +477 -0
- package/resources/gofer/node-scripts/gofer-execution-recovery.mjs +334 -0
- package/resources/gofer/node-scripts/gofer-grok-adapter.mjs +184 -0
- package/resources/gofer/node-scripts/gofer-heldout-corpus.mjs +73 -0
- package/resources/gofer/node-scripts/gofer-heldout-snapshot.mjs +286 -0
- package/resources/gofer/node-scripts/gofer-heldout-verifier.mjs +211 -0
- package/resources/gofer/node-scripts/gofer-host-capability.mjs +335 -0
- package/resources/gofer/node-scripts/gofer-independent-reviewer.mjs +130 -0
- package/resources/gofer/node-scripts/gofer-live-routing.mjs +109 -0
- package/resources/gofer/node-scripts/gofer-local-capability-issuer.mjs +71 -0
- package/resources/gofer/node-scripts/gofer-local-isolation.mjs +175 -0
- package/resources/gofer/node-scripts/gofer-local-settings-cleanup.mjs +388 -0
- package/resources/gofer/node-scripts/gofer-loop-audit.mjs +54 -3
- package/resources/gofer/node-scripts/gofer-native-adapter.mjs +567 -0
- package/resources/gofer/node-scripts/gofer-native-benchmark-dispatch.mjs +145 -0
- package/resources/gofer/node-scripts/gofer-native-runtime.mjs +169 -0
- package/resources/gofer/node-scripts/gofer-pre-edit-check.mjs +204 -0
- package/resources/gofer/node-scripts/gofer-priority-check.mjs +214 -0
- package/resources/gofer/node-scripts/gofer-response-check.mjs +56 -0
- package/resources/gofer/node-scripts/gofer-reviewed-learning.mjs +1232 -0
- package/resources/gofer/node-scripts/gofer-run-verified-task.mjs +330 -0
- package/resources/gofer/node-scripts/gofer-runtime-ledger.mjs +289 -0
- package/resources/gofer/node-scripts/gofer-semantic-drift.mjs +584 -0
- package/resources/gofer/node-scripts/gofer-surface-update.mjs +1178 -0
- package/resources/gofer/node-scripts/gofer-trust-bootstrap.mjs +77 -0
- package/resources/gofer/node-scripts/gofer-trusted-benchmark.mjs +102 -0
- package/resources/gofer/node-scripts/gofer-trusted-evaluator.mjs +274 -0
- package/resources/gofer/node-scripts/gofer-tty-prompt.mjs +34 -0
- package/resources/gofer/node-scripts/gofer-typesafe-credentials.mjs +435 -0
- package/resources/gofer/node-scripts/gofer-ui-preview.mjs +1212 -0
- package/resources/gofer/node-scripts/gofer-verified-execution.mjs +484 -0
- package/resources/gofer/node-scripts/gofer-verifier-key-ceremony.mjs +101 -0
- package/resources/gofer/node-scripts/gofer-worktree-layout.mjs +32 -0
- package/resources/gofer/node-scripts/object-type-routing-phase-bundle.mjs +360 -0
- package/resources/gofer/node-scripts/package-agent-plugin.mjs +574 -82
- package/resources/gofer/node-scripts/parse-stage-command.mjs +1 -1
- package/resources/gofer/node-scripts/render-delivery-lineage.mjs +178 -0
- package/resources/gofer/node-scripts/sync-extension-resources.mjs +78 -2
- package/resources/gofer/node-scripts/validate-object-type-identifiers.mjs +767 -0
- package/resources/gofer/node-scripts/validate-object-type-routing-workspace.mjs +1325 -0
- package/resources/gofer/node-scripts/validate-v4-resource-contract.d.mts +44 -0
- package/resources/gofer/node-scripts/validate-v4-resource-contract.mjs +957 -0
- package/resources/gofer/node-scripts/workspace-bootstrap-lib.mjs +685 -116
- package/resources/gofer/references/agent-catalog.json +126 -0
- package/resources/gofer/references/blocker-mediation.md +156 -0
- package/resources/gofer/references/business-updates-and-goal-checks.md +109 -0
- package/resources/gofer/references/delivery-lineage.md +48 -0
- package/resources/gofer/references/mvp-capability-validation.md +102 -0
- package/resources/gofer/references/platform/README.md +1 -1
- package/resources/gofer/references/platform/eai-app-template.md +15 -13
- package/resources/gofer/references/platform/eai-auth-access.md +104 -0
- package/resources/gofer/references/platform/eai-config-driven-ui.md +5 -5
- package/resources/gofer/references/platform/eai-error-catalog.yaml +77 -19
- package/resources/gofer/references/platform/eai-repo-contract.md +61 -23
- package/resources/gofer/references/platform/eai-service-patterns.md +102 -105
- package/resources/gofer/references/platform/eai.md +44 -8
- package/resources/gofer/references/platform/vertical-template.md +10 -9
- package/resources/gofer/references/priority-outcome-protection.md +152 -0
- package/resources/gofer/references/verified-agent-execution.md +179 -0
- package/resources/gofer/schemas/gofer-learning-trace-v1.json +70 -0
- package/resources/gofer/specify-commands/0_gofer_start.md +440 -99
- package/resources/gofer/specify-commands/0a_problem_validation.md +187 -27
- package/resources/gofer/specify-commands/10_gofer_cloud.md +183 -23
- package/resources/gofer/specify-commands/1_gofer_research.md +239 -30
- package/resources/gofer/specify-commands/2_gofer_specify.md +239 -35
- package/resources/gofer/specify-commands/3_gofer_plan.md +259 -43
- package/resources/gofer/specify-commands/4_gofer_tasks.md +401 -81
- package/resources/gofer/specify-commands/5_gofer_implement.md +375 -84
- package/resources/gofer/specify-commands/6_gofer_validate.md +284 -53
- package/resources/gofer/specify-commands/7_gofer_save.md +187 -26
- package/resources/gofer/specify-commands/7a_stakeholder_comms.md +183 -23
- package/resources/gofer/specify-commands/8_gofer_branding.md +183 -23
- package/resources/gofer/specify-commands/9_gofer_tests.md +212 -23
- package/resources/gofer/specify-commands/gofer_bootstrap_workspace.md +121 -8
- package/resources/gofer/specify-commands/gofer_check_workspace.md +123 -8
- package/resources/gofer/specify-commands/gofer_constitution.md +183 -23
- package/resources/gofer/specify-commands/gofer_diagnose.md +183 -23
- package/resources/gofer/specify-commands/gofer_eai_first_run.md +254 -25
- package/resources/gofer/specify-commands/gofer_hydrate.md +183 -23
- package/resources/gofer/specify-commands/gofer_personality.md +117 -6
- package/resources/gofer/specify-commands/gofer_plan.md +117 -6
- package/resources/gofer/specify-commands/gofer_side.md +117 -6
- package/resources/gofer/specify-commands/gofer_spec_summary.md +183 -23
- package/resources/gofer/specify-commands/gofer_tdd.md +183 -23
- package/resources/gofer/specify-commands/gofer_vocabulary.md +185 -25
- package/resources/gofer/specify-commands/gofer_zoom_out.md +183 -23
- package/resources/gofer/specify-config/object-type-routing.json +24 -0
- package/resources/gofer/specify-config/typesafe-learning-review.json +16 -0
- package/resources/gofer/specify-config/typesafe-semantic-review.json +9 -0
- package/resources/gofer/system-skills/eai/SKILL.md +365 -0
- package/resources/gofer/system-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/system-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/templates/build-map-template.md +64 -0
- package/resources/gofer/templates/business-scenarios-template.json +14 -0
- package/resources/gofer/templates/ciso-security-summary-template.md +3 -3
- package/resources/gofer/templates/context-bundle-template.md +4 -3
- package/resources/gofer/templates/contract-pack-template.md +8 -8
- package/resources/gofer/templates/cto-architecture-summary-template.md +3 -3
- package/resources/gofer/templates/delivery-lineage-template.json +8 -0
- package/resources/gofer/templates/eai-preflight-template.md +109 -68
- package/resources/gofer/templates/goal-ledger-template.json +1 -1
- package/resources/gofer/templates/gofer-model-policy.yaml +28 -6
- package/resources/gofer/templates/loop-contract-template.json +3 -1
- package/resources/gofer/templates/plan-template.md +8 -7
- package/resources/gofer/templates/priority-plan-template.json +29 -0
- package/resources/gofer/templates/proposal-review-template.md +6 -3
- package/resources/gofer/templates/release-capability-ledger-template.md +24 -0
- package/resources/gofer/templates/research-template.md +7 -6
- package/resources/gofer/templates/spec-summary-template.md +1 -1
- package/resources/gofer/templates/spec-template.md +45 -6
- package/resources/gofer/templates/tasks-template.md +12 -3
- package/resources/gofer/templates/ui-preview-brief-template.md +55 -5
- package/resources/gofer/templates/ui-review-log-template.md +21 -3
- package/resources/gofer/templates/ui-show-and-tell-template.md +46 -0
- package/resources/gofer/templates/visuals/capability-heatmap-template.md +2 -2
- package/resources/gofer/templates/working-backwards-prfaq-template.md +9 -8
- package/resources/linked-sources.json +4 -4
- package/resources/gofer/agents-skills/0_gofer_start/SKILL.md +0 -1287
- package/resources/gofer/agents-skills/0a_problem_validation/SKILL.md +0 -429
- package/resources/gofer/agents-skills/10_gofer_cloud/SKILL.md +0 -635
- package/resources/gofer/agents-skills/1_gofer_research/SKILL.md +0 -1159
- package/resources/gofer/agents-skills/2_gofer_specify/SKILL.md +0 -860
- package/resources/gofer/agents-skills/3_gofer_plan/SKILL.md +0 -899
- package/resources/gofer/agents-skills/4_gofer_tasks/SKILL.md +0 -760
- package/resources/gofer/agents-skills/5_gofer_implement/SKILL.md +0 -944
- package/resources/gofer/agents-skills/6_gofer_validate/SKILL.md +0 -2302
- package/resources/gofer/agents-skills/7_gofer_save/SKILL.md +0 -423
- package/resources/gofer/agents-skills/7a_stakeholder_comms/SKILL.md +0 -508
- package/resources/gofer/agents-skills/8_gofer_branding/SKILL.md +0 -315
- package/resources/gofer/agents-skills/9_gofer_tests/SKILL.md +0 -558
- package/resources/gofer/agents-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
- package/resources/gofer/agents-skills/gofer_check_workspace/SKILL.md +0 -119
- package/resources/gofer/agents-skills/gofer_constitution/SKILL.md +0 -612
- package/resources/gofer/agents-skills/gofer_diagnose/SKILL.md +0 -114
- package/resources/gofer/agents-skills/gofer_eai_first_run/SKILL.md +0 -397
- package/resources/gofer/agents-skills/gofer_hydrate/SKILL.md +0 -561
- package/resources/gofer/agents-skills/gofer_personality/SKILL.md +0 -44
- package/resources/gofer/agents-skills/gofer_plan/SKILL.md +0 -40
- package/resources/gofer/agents-skills/gofer_side/SKILL.md +0 -45
- package/resources/gofer/agents-skills/gofer_spec_summary/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_tdd/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_vocabulary/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_zoom_out/SKILL.md +0 -112
- package/resources/gofer/claude-commands/0_gofer_start.md +0 -1282
- package/resources/gofer/claude-commands/0a_problem_validation.md +0 -424
- package/resources/gofer/claude-commands/10_gofer_cloud.md +0 -630
- package/resources/gofer/claude-commands/1_gofer_research.md +0 -1154
- package/resources/gofer/claude-commands/2_gofer_specify.md +0 -855
- package/resources/gofer/claude-commands/3_gofer_plan.md +0 -894
- package/resources/gofer/claude-commands/4_gofer_tasks.md +0 -755
- package/resources/gofer/claude-commands/5_gofer_implement.md +0 -939
- package/resources/gofer/claude-commands/6_gofer_validate.md +0 -2297
- package/resources/gofer/claude-commands/7_gofer_save.md +0 -418
- package/resources/gofer/claude-commands/7a_stakeholder_comms.md +0 -503
- package/resources/gofer/claude-commands/8_gofer_branding.md +0 -310
- package/resources/gofer/claude-commands/9_gofer_tests.md +0 -553
- package/resources/gofer/claude-commands/gofer_bootstrap_workspace.md +0 -143
- package/resources/gofer/claude-commands/gofer_check_workspace.md +0 -114
- package/resources/gofer/claude-commands/gofer_constitution.md +0 -607
- package/resources/gofer/claude-commands/gofer_diagnose.md +0 -109
- package/resources/gofer/claude-commands/gofer_eai_first_run.md +0 -392
- package/resources/gofer/claude-commands/gofer_hydrate.md +0 -556
- package/resources/gofer/claude-commands/gofer_personality.md +0 -39
- package/resources/gofer/claude-commands/gofer_plan.md +0 -35
- package/resources/gofer/claude-commands/gofer_side.md +0 -40
- package/resources/gofer/claude-commands/gofer_spec_summary.md +0 -107
- package/resources/gofer/claude-commands/gofer_tdd.md +0 -107
- package/resources/gofer/claude-commands/gofer_vocabulary.md +0 -107
- package/resources/gofer/claude-commands/gofer_zoom_out.md +0 -107
- package/resources/gofer/claude-skills/eai-gofer/SKILL.md +0 -56
- package/resources/gofer/copilot-prompts/0_gofer_start.prompt.md +0 -1308
- package/resources/gofer/copilot-prompts/0a_problem_validation.prompt.md +0 -429
- package/resources/gofer/copilot-prompts/10_gofer_cloud.prompt.md +0 -644
- package/resources/gofer/copilot-prompts/1_gofer_research.prompt.md +0 -1177
- package/resources/gofer/copilot-prompts/2_gofer_specify.prompt.md +0 -878
- package/resources/gofer/copilot-prompts/3_gofer_plan.prompt.md +0 -917
- package/resources/gofer/copilot-prompts/4_gofer_tasks.prompt.md +0 -778
- package/resources/gofer/copilot-prompts/5_gofer_implement.prompt.md +0 -962
- package/resources/gofer/copilot-prompts/6_gofer_validate.prompt.md +0 -2310
- package/resources/gofer/copilot-prompts/7_gofer_save.prompt.md +0 -433
- package/resources/gofer/copilot-prompts/7a_stakeholder_comms.prompt.md +0 -516
- package/resources/gofer/copilot-prompts/8_gofer_branding.prompt.md +0 -323
- package/resources/gofer/copilot-prompts/9_gofer_tests.prompt.md +0 -566
- package/resources/gofer/copilot-prompts/gofer_bootstrap_workspace.prompt.md +0 -161
- package/resources/gofer/copilot-prompts/gofer_check_workspace.prompt.md +0 -132
- package/resources/gofer/copilot-prompts/gofer_constitution.prompt.md +0 -621
- package/resources/gofer/copilot-prompts/gofer_diagnose.prompt.md +0 -127
- package/resources/gofer/copilot-prompts/gofer_eai_first_run.prompt.md +0 -410
- package/resources/gofer/copilot-prompts/gofer_hydrate.prompt.md +0 -571
- package/resources/gofer/copilot-prompts/gofer_personality.prompt.md +0 -57
- package/resources/gofer/copilot-prompts/gofer_plan.prompt.md +0 -53
- package/resources/gofer/copilot-prompts/gofer_side.prompt.md +0 -58
- package/resources/gofer/copilot-prompts/gofer_spec_summary.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_tdd.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_vocabulary.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_zoom_out.prompt.md +0 -125
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.md +0 -1282
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/0a_problem_validation.md +0 -424
- package/resources/gofer/gemini/commands/gofer/0a_problem_validation.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.md +0 -630
- package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.md +0 -1154
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/2_gofer_specify.md +0 -855
- package/resources/gofer/gemini/commands/gofer/2_gofer_specify.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/3_gofer_plan.md +0 -894
- package/resources/gofer/gemini/commands/gofer/3_gofer_plan.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.md +0 -755
- package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/5_gofer_implement.md +0 -939
- package/resources/gofer/gemini/commands/gofer/5_gofer_implement.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/6_gofer_validate.md +0 -2297
- package/resources/gofer/gemini/commands/gofer/6_gofer_validate.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/7_gofer_save.md +0 -418
- package/resources/gofer/gemini/commands/gofer/7_gofer_save.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.md +0 -503
- package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/8_gofer_branding.md +0 -310
- package/resources/gofer/gemini/commands/gofer/8_gofer_branding.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/9_gofer_tests.md +0 -553
- package/resources/gofer/gemini/commands/gofer/9_gofer_tests.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.md +0 -143
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.md +0 -114
- package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_constitution.md +0 -607
- package/resources/gofer/gemini/commands/gofer/gofer_constitution.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_diagnose.md +0 -109
- package/resources/gofer/gemini/commands/gofer/gofer_diagnose.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.md +0 -392
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_hydrate.md +0 -556
- package/resources/gofer/gemini/commands/gofer/gofer_hydrate.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_personality.md +0 -39
- package/resources/gofer/gemini/commands/gofer/gofer_personality.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_plan.md +0 -35
- package/resources/gofer/gemini/commands/gofer/gofer_plan.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_side.md +0 -40
- package/resources/gofer/gemini/commands/gofer/gofer_side.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_tdd.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_tdd.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.toml +0 -2
- package/resources/gofer/github-skills/eai-gofer/SKILL.md +0 -56
- package/resources/gofer/system-skills/0_gofer_start/SKILL.md +0 -1287
- package/resources/gofer/system-skills/0a_problem_validation/SKILL.md +0 -429
- package/resources/gofer/system-skills/10_gofer_cloud/SKILL.md +0 -635
- package/resources/gofer/system-skills/1_gofer_research/SKILL.md +0 -1159
- package/resources/gofer/system-skills/2_gofer_specify/SKILL.md +0 -860
- package/resources/gofer/system-skills/3_gofer_plan/SKILL.md +0 -899
- package/resources/gofer/system-skills/4_gofer_tasks/SKILL.md +0 -760
- package/resources/gofer/system-skills/5_gofer_implement/SKILL.md +0 -944
- package/resources/gofer/system-skills/6_gofer_validate/SKILL.md +0 -2302
- package/resources/gofer/system-skills/7_gofer_save/SKILL.md +0 -423
- package/resources/gofer/system-skills/7a_stakeholder_comms/SKILL.md +0 -508
- package/resources/gofer/system-skills/8_gofer_branding/SKILL.md +0 -315
- package/resources/gofer/system-skills/9_gofer_tests/SKILL.md +0 -558
- package/resources/gofer/system-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
- package/resources/gofer/system-skills/gofer_check_workspace/SKILL.md +0 -119
- package/resources/gofer/system-skills/gofer_constitution/SKILL.md +0 -612
- package/resources/gofer/system-skills/gofer_diagnose/SKILL.md +0 -114
- package/resources/gofer/system-skills/gofer_eai_first_run/SKILL.md +0 -397
- package/resources/gofer/system-skills/gofer_hydrate/SKILL.md +0 -561
- package/resources/gofer/system-skills/gofer_personality/SKILL.md +0 -44
- package/resources/gofer/system-skills/gofer_plan/SKILL.md +0 -40
- package/resources/gofer/system-skills/gofer_side/SKILL.md +0 -45
- package/resources/gofer/system-skills/gofer_spec_summary/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_tdd/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_vocabulary/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_zoom_out/SKILL.md +0 -112
- package/resources/gofer/templates/ui-approval-template.md +0 -32
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign a held-out benchmark attestation only after this process has re-run
|
|
3
|
+
* every case itself. The executor's saved verdicts are never trusted: the
|
|
4
|
+
* signer re-executes each protected check in the offline sandbox against the
|
|
5
|
+
* captured bytes, and binds the signature to the exact report it rechecked.
|
|
6
|
+
* It never activates a key. An inactive or unregistered key fails closed.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash, createPublicKey, sign } from 'node:crypto';
|
|
9
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { capabilityReceiptHash } from './gofer-host-capability.mjs';
|
|
12
|
+
import { recheckHeldOutSnapshot } from './gofer-heldout-verifier.mjs';
|
|
13
|
+
import { materializeHeldOutResultSnapshot } from './gofer-heldout-snapshot.mjs';
|
|
14
|
+
import { loadActiveBenchmarkVerifierKey, loadTrustedHeldOutCorpus,
|
|
15
|
+
resolveTrustedEvaluatorPublicKey } from './gofer-trusted-evaluator.mjs';
|
|
16
|
+
import { verifyTrustedBenchmarkEvidence } from './gofer-trusted-benchmark.mjs';
|
|
17
|
+
|
|
18
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
19
|
+
const sha = value => createHash('sha256').update(value).digest('hex');
|
|
20
|
+
const denied = () => new Error('BENCHMARK_SIGNER_REQUIRED');
|
|
21
|
+
const EVALUATOR = 'gofer-heldout-benchmark-verifier';
|
|
22
|
+
const VERIFIER_VERSION = '1';
|
|
23
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
24
|
+
const MAX_TTL_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `trustRoot` exists for isolated tests. The production composition root never
|
|
28
|
+
* passes it and always resolves the fixed account trust root.
|
|
29
|
+
*/
|
|
30
|
+
export async function signHeldOutBenchmarkAttestation({ workspaceRoot, trustRoot,
|
|
31
|
+
capabilityReceipt, capabilityPublicKey, snapshotId, getPassphrase, protectedRegistry,
|
|
32
|
+
ttlMs = DEFAULT_TTL_MS, now = Date.now() } = {}) {
|
|
33
|
+
let materialized;
|
|
34
|
+
try {
|
|
35
|
+
if (!text(workspaceRoot) || !path.isAbsolute(workspaceRoot) ||
|
|
36
|
+
capabilityReceipt?.host !== 'codex' || !text(capabilityReceipt?.provenance?.keyId) ||
|
|
37
|
+
!/^[a-f0-9]{64}$/.test(snapshotId ?? '') || !Number.isFinite(now) ||
|
|
38
|
+
!Number.isInteger(ttlMs) || ttlMs < 1 || ttlMs > MAX_TTL_MS) throw denied();
|
|
39
|
+
const receiptHash = capabilityReceiptHash(capabilityReceipt);
|
|
40
|
+
const corpus = await loadTrustedHeldOutCorpus({ workspaceRoot, trustRoot });
|
|
41
|
+
const corpusTrustRoot = path.dirname(path.dirname(corpus.corpusRoot));
|
|
42
|
+
|
|
43
|
+
// The signing identity must exist and be active before any work is done.
|
|
44
|
+
const signer = await loadActiveBenchmarkVerifierKey({ workspaceRoot, trustRoot, getPassphrase,
|
|
45
|
+
protectedRegistry });
|
|
46
|
+
if (signer.keyId === capabilityReceipt.provenance.keyId) throw denied();
|
|
47
|
+
const signerPublicKey = await resolveTrustedEvaluatorPublicKey({ host: 'codex',
|
|
48
|
+
provenance: { evaluator: EVALUATOR, keyId: signer.keyId } }, { workspaceRoot, trustRoot, protectedRegistry });
|
|
49
|
+
const capabilityKey = capabilityPublicKey?.type === 'public'
|
|
50
|
+
? capabilityPublicKey : createPublicKey(capabilityPublicKey);
|
|
51
|
+
if (signerPublicKey.export({ type: 'spki', format: 'der' })
|
|
52
|
+
.equals(capabilityKey.export({ type: 'spki', format: 'der' }))) throw denied();
|
|
53
|
+
|
|
54
|
+
// Independent re-execution. Any disagreement with the saved verdicts throws.
|
|
55
|
+
const recheck = await recheckHeldOutSnapshot({ corpusRoot: corpus.corpusRoot,
|
|
56
|
+
workspaceRoot, trustRoot: corpusTrustRoot, snapshotId });
|
|
57
|
+
if (recheck.corpusHash !== corpus.corpusHash ||
|
|
58
|
+
recheck.functionalPasses !== recheck.functionalRuns) throw denied();
|
|
59
|
+
|
|
60
|
+
materialized = await materializeHeldOutResultSnapshot({ trustRoot: corpusTrustRoot,
|
|
61
|
+
workspaceRoot, snapshotId });
|
|
62
|
+
const saved = JSON.parse(await readFile(path.join(materialized.root, 'receipts',
|
|
63
|
+
'benchmark-report.json'), 'utf8'));
|
|
64
|
+
const evidence = saved?.report;
|
|
65
|
+
// The signature binds the exact report the sandbox just rechecked.
|
|
66
|
+
if (!evidence || sha(JSON.stringify(evidence)) !== recheck.reportHash ||
|
|
67
|
+
evidence.provenance?.capabilityReceiptHash !== receiptHash ||
|
|
68
|
+
evidence.provenance?.corpusHash !== corpus.corpusHash) throw denied();
|
|
69
|
+
|
|
70
|
+
const payload = { schemaVersion: 1, host: 'codex', capabilityReceiptHash: receiptHash,
|
|
71
|
+
evidenceHash: sha(JSON.stringify(evidence)),
|
|
72
|
+
verification: 'functional-tests+independent-review', verifierVersion: VERIFIER_VERSION,
|
|
73
|
+
verifiedAt: new Date(now).toISOString(), expiresAt: new Date(now + ttlMs).toISOString(),
|
|
74
|
+
provenance: { evaluator: EVALUATOR, keyId: signer.keyId,
|
|
75
|
+
source: `heldout-snapshot:${snapshotId}` } };
|
|
76
|
+
const attestation = { ...payload, signature: { algorithm: 'ed25519', keyId: signer.keyId,
|
|
77
|
+
value: sign(null, Buffer.from(JSON.stringify(payload)), signer.privateKey)
|
|
78
|
+
.toString('base64url') } };
|
|
79
|
+
|
|
80
|
+
// The consumer must accept exactly what was signed, or nothing is returned.
|
|
81
|
+
const verified = await verifyTrustedBenchmarkEvidence({ host: 'codex', receiptHash,
|
|
82
|
+
evidence, attestation, capabilityKeyId: capabilityReceipt.provenance.keyId,
|
|
83
|
+
capabilityPublicKey: capabilityKey, workspaceRoot, now });
|
|
84
|
+
if (verified?.valid !== true) throw denied();
|
|
85
|
+
return Object.freeze({ attestation, evidence, verification: verified,
|
|
86
|
+
snapshotId, checks: recheck.functionalRuns });
|
|
87
|
+
} catch { throw denied(); }
|
|
88
|
+
finally { if (materialized) await rm(materialized.root, { recursive: true, force: true }); }
|
|
89
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** A benchmark execution result is not a functional pass. An independent
|
|
2
|
+
* verifier binds every verdict to the held-out input and execution receipt. */
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const positive = value => Number.isFinite(value) && value >= 0;
|
|
6
|
+
const reliability = value => Number.isFinite(value) && value >= 0 && value <= 1;
|
|
7
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
9
|
+
const validCase = item => item && text(item.id) && item.heldOut === true;
|
|
10
|
+
const validExecution = result => result && text(result.modelId) && positive(result.costUsd) && positive(result.durationMs) && text(result.receipt);
|
|
11
|
+
const validVerifiedRun = result => validExecution(result) && result.functionalVerified === true && text(result.verifierReceipt) && text(result.verifierId) && text(result.inputHash);
|
|
12
|
+
const wilson95 = (successes, total) => {
|
|
13
|
+
const z = 1.96;
|
|
14
|
+
const p = successes / total;
|
|
15
|
+
const denominator = 1 + z ** 2 / total;
|
|
16
|
+
const centre = (p + z ** 2 / (2 * total)) / denominator;
|
|
17
|
+
const spread = z * Math.sqrt((p * (1 - p) + z ** 2 / (4 * total)) / total) / denominator;
|
|
18
|
+
return Object.freeze({ level: 0.95, lower: Math.max(0, centre - spread), upper: Math.min(1, centre + spread) });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function runBenchmark({ cases, execute, verify, repetitions = 3, provenance } = {}) {
|
|
22
|
+
if (!Array.isArray(cases) || !cases.length || cases.length > 1000 || !cases.every(validCase) ||
|
|
23
|
+
new Set(cases.map(item => item.id)).size !== cases.length || typeof execute !== 'function' ||
|
|
24
|
+
typeof verify !== 'function' || execute === verify || repetitions !== 3 || !text(provenance?.harnessId) || !text(provenance?.modelId)) {
|
|
25
|
+
throw new Error('INVALID_BENCHMARK_CONTRACT');
|
|
26
|
+
}
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const benchmarkCase of cases) for (let run = 1; run <= repetitions; run++) {
|
|
29
|
+
const input = structuredClone(benchmarkCase.input);
|
|
30
|
+
const inputHash = hash({ caseId: benchmarkCase.id, input });
|
|
31
|
+
const execution = await execute(Object.freeze({ id: benchmarkCase.id, run, input }));
|
|
32
|
+
if (!validExecution(execution) || execution.modelId !== provenance.modelId) throw new Error('INVALID_BENCHMARK_RESULT');
|
|
33
|
+
const verdict = await verify(Object.freeze({ caseId: benchmarkCase.id, run, inputHash,
|
|
34
|
+
execution: { receipt: execution.receipt, modelId: execution.modelId ?? null, output: structuredClone(execution.output ?? null) } }));
|
|
35
|
+
if (!verdict || verdict.caseId !== benchmarkCase.id || verdict.run !== run || verdict.inputHash !== inputHash || verdict.executionReceipt !== execution.receipt ||
|
|
36
|
+
typeof verdict.passed !== 'boolean' || !text(verdict.receipt) || !text(verdict.verifierId) ||
|
|
37
|
+
!text(verdict.failureClassification) || !text(verdict.reviewReceipt)) throw new Error('INVALID_BENCHMARK_VERDICT');
|
|
38
|
+
results.push(Object.freeze({ caseId: benchmarkCase.id, run, modelId: execution.modelId ?? null,
|
|
39
|
+
costUsd: execution.costUsd, durationMs: execution.durationMs, receipt: execution.receipt, inputHash,
|
|
40
|
+
functionalVerified: verdict.passed, verifierReceipt: verdict.receipt, verifierId: verdict.verifierId,
|
|
41
|
+
failureClassification: verdict.failureClassification, reviewReceipt: verdict.reviewReceipt }));
|
|
42
|
+
}
|
|
43
|
+
if (new Set(results.map(result => result.receipt)).size !== results.length ||
|
|
44
|
+
new Set(results.map(result => result.verifierReceipt)).size !== results.length) {
|
|
45
|
+
throw new Error('NON_INDEPENDENT_BENCHMARK_RUNS');
|
|
46
|
+
}
|
|
47
|
+
const functionalPasses = results.filter(validVerifiedRun).length;
|
|
48
|
+
const totals = results.reduce((sum, result) => ({ costUsd: sum.costUsd + result.costUsd, durationMs: sum.durationMs + result.durationMs }), { costUsd: 0, durationMs: 0 });
|
|
49
|
+
return Object.freeze({ schemaVersion: 2, repetitions, caseCount: cases.length, runs: results, provenance: { ...provenance },
|
|
50
|
+
reliability: functionalPasses / results.length, confidenceInterval: wilson95(functionalPasses, results.length), functionalPasses, functionalRuns: results.length,
|
|
51
|
+
costUsd: totals.costUsd, durationMs: totals.durationMs, status: functionalPasses === results.length ? 'pass' : 'fail' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ablateBenchmark(reports) {
|
|
55
|
+
if (!Array.isArray(reports) || reports.length < 2 || reports.some(report => !report || !text(report.provenance?.harnessId) ||
|
|
56
|
+
!text(report.provenance?.modelId) || !reliability(report.reliability) || !positive(report.costUsd) || !positive(report.durationMs) ||
|
|
57
|
+
!['pass', 'fail'].includes(report.status))) throw new Error('INVALID_ABLATION_REPORT');
|
|
58
|
+
return Object.freeze(reports.map(report => Object.freeze({ harnessId: report.provenance.harnessId, modelId: report.provenance.modelId,
|
|
59
|
+
reliability: report.reliability, costUsd: report.costUsd, durationMs: report.durationMs, status: report.status })));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function gateBenchmark(candidate, baseline, { minReliability = 1, maxCostIncreasePct = 0.1, maxDurationIncreasePct = 0.1 } = {}) {
|
|
63
|
+
if (!candidate || !baseline || !positive(candidate.costUsd) || !positive(candidate.durationMs) || !positive(baseline.costUsd) ||
|
|
64
|
+
!positive(baseline.durationMs) || !reliability(candidate.reliability) || !reliability(baseline.reliability) ||
|
|
65
|
+
![minReliability, maxCostIncreasePct, maxDurationIncreasePct].every(positive)) throw new Error('INVALID_BENCHMARK_GATE');
|
|
66
|
+
const findings = [];
|
|
67
|
+
if (candidate.status !== 'pass' || candidate.reliability < minReliability) findings.push('FUNCTIONAL_RELIABILITY_REGRESSION');
|
|
68
|
+
if (candidate.reliability < baseline.reliability) findings.push('BASELINE_RELIABILITY_REGRESSION');
|
|
69
|
+
if (baseline.costUsd === 0 ? candidate.costUsd > 0 :
|
|
70
|
+
(candidate.costUsd - baseline.costUsd) / baseline.costUsd > maxCostIncreasePct) findings.push('COST_REGRESSION');
|
|
71
|
+
if (baseline.durationMs === 0 ? candidate.durationMs > 0 :
|
|
72
|
+
(candidate.durationMs - baseline.durationMs) / baseline.durationMs > maxDurationIncreasePct) findings.push('DURATION_REGRESSION');
|
|
73
|
+
return Object.freeze({ status: findings.length ? 'fail' : 'pass', findings,
|
|
74
|
+
candidate: { reliability: candidate.reliability, costUsd: candidate.costUsd, durationMs: candidate.durationMs },
|
|
75
|
+
baseline: { reliability: baseline.reliability, costUsd: baseline.costUsd, durationMs: baseline.durationMs } });
|
|
76
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises as fs, constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const FILE = 'blocker-register.json';
|
|
8
|
+
const MAX_EVIDENCE_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const CATEGORIES = ['decision', 'access', 'external', 'capability', 'solvable', 'unknown'];
|
|
10
|
+
const STATES = ['ready', 'waiting', 'blocked', 'resolved'];
|
|
11
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
12
|
+
const material = value => typeof value === 'string' && value.trim().length > 0;
|
|
13
|
+
const key = value => {
|
|
14
|
+
if (!material(value) || !/^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159}$/.test(value)) throw new Error('A stable machine key is required');
|
|
15
|
+
return value.toLowerCase();
|
|
16
|
+
};
|
|
17
|
+
const waits = category => ['decision', 'access', 'external', 'capability'].includes(category);
|
|
18
|
+
|
|
19
|
+
export function blockerIdentity(value) {
|
|
20
|
+
return digest(JSON.stringify([key(value.goalKey), key(value.subjectKey), key(value.conditionKey)]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readBounded(file) {
|
|
24
|
+
const check = stat => {
|
|
25
|
+
if (!stat.isFile()) throw new Error('Evidence must be a regular file');
|
|
26
|
+
if (stat.size > MAX_EVIDENCE_BYTES) throw new Error('Evidence exceeds the 16 MiB limit');
|
|
27
|
+
};
|
|
28
|
+
// Inspect the opened descriptor; a pathname precheck can race with replacement.
|
|
29
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
30
|
+
try {
|
|
31
|
+
check(await handle.stat());
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let total = 0;
|
|
34
|
+
while (true) {
|
|
35
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, MAX_EVIDENCE_BYTES - total + 1));
|
|
36
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
37
|
+
if (!bytesRead) return Buffer.concat(chunks, total);
|
|
38
|
+
total += bytesRead;
|
|
39
|
+
// Bound the read itself, not only the initial size of a file that can grow.
|
|
40
|
+
if (total > MAX_EVIDENCE_BYTES) throw new Error('Evidence exceeds the 16 MiB limit');
|
|
41
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
42
|
+
}
|
|
43
|
+
} finally {
|
|
44
|
+
await handle.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readInside(root, relative) {
|
|
49
|
+
if (!material(relative) || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')) throw new Error('Invalid evidence path');
|
|
50
|
+
const resolved = await fs.realpath(path.resolve(root, relative));
|
|
51
|
+
if (!resolved.startsWith(root + path.sep)) throw new Error('Evidence escapes the state directory');
|
|
52
|
+
return readBounded(resolved);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateRegister(register) {
|
|
56
|
+
if (register?.schemaVersion !== 1 || !register.blockers || typeof register.blockers !== 'object' || Array.isArray(register.blockers)) throw new Error('Invalid blocker register');
|
|
57
|
+
for (const [id, b] of Object.entries(register.blockers)) {
|
|
58
|
+
if (!b || blockerIdentity(b) !== id || !CATEGORIES.includes(b.category) || !STATES.includes(b.state) ||
|
|
59
|
+
typeof b.asked !== 'boolean' || !Array.isArray(b.tasks) || b.tasks.some(t => !/^T\d+$/.test(t)) ||
|
|
60
|
+
!Array.isArray(b.attempts) || !Array.isArray(b.history) || !Array.isArray(b.proofs) ||
|
|
61
|
+
!Number.isInteger(b.epoch) || b.epoch < 0 || b.epoch > 1) throw new Error('Invalid blocker entry');
|
|
62
|
+
const attempts = new Set();
|
|
63
|
+
const approaches = new Set();
|
|
64
|
+
for (const a of b.attempts) {
|
|
65
|
+
if (!material(a.id) || attempts.has(a.id) || !material(a.approach) ||
|
|
66
|
+
!Number.isInteger(a.epoch) || a.epoch < 0 || a.epoch > b.epoch ||
|
|
67
|
+
!['pending', 'progress', 'no_progress', 'blocked'].includes(a.result)) throw new Error('Invalid attempt history');
|
|
68
|
+
if (approaches.has(`${a.epoch}:${a.approach}`)) throw new Error('Repeated approach');
|
|
69
|
+
attempts.add(a.id);
|
|
70
|
+
approaches.add(`${a.epoch}:${a.approach}`);
|
|
71
|
+
}
|
|
72
|
+
if (b.attempts.length > 4 || [0, 1].some(epoch => b.attempts.filter(a => a.epoch === epoch).length > 2)) throw new Error('Invalid attempt budget');
|
|
73
|
+
if (b.attempts.filter(a => a.result === 'pending').length > 1) throw new Error('Multiple pending attempts');
|
|
74
|
+
if (b.state === 'resolved' && !b.proofs.some(p => p.kind === 'resolution')) throw new Error('Missing resolution evidence');
|
|
75
|
+
}
|
|
76
|
+
return register;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function load(root) {
|
|
80
|
+
try { return validateRegister(JSON.parse(await readInside(root, FILE))); }
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code === 'ENOENT') {
|
|
83
|
+
// A dangling register link is invalid, not an empty register.
|
|
84
|
+
try { await fs.lstat(path.join(root, FILE)); } catch (statError) { if (statError.code === 'ENOENT') return { schemaVersion: 1, blockers: {} }; }
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function checkProofs(root, blocker) {
|
|
91
|
+
for (const proof of blocker.proofs) {
|
|
92
|
+
if (!proof || !['change', 'resolution', 'diagnosis'].includes(proof.kind) || !material(proof.hash)) throw new Error('Invalid proof record');
|
|
93
|
+
const raw = await readInside(root, proof.file);
|
|
94
|
+
if (digest(raw) !== proof.hash) throw new Error('Blocker evidence changed or is missing');
|
|
95
|
+
if (proof.kind === 'diagnosis') {
|
|
96
|
+
const diagnosis = JSON.parse(raw);
|
|
97
|
+
if (digest(await readInside(root, diagnosis.evidence)) !== diagnosis.sha256) throw new Error('Diagnosis output changed or is missing');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function acceptProof(root, blocker, id, file, kind) {
|
|
103
|
+
const raw = await readInside(root, file);
|
|
104
|
+
const proof = JSON.parse(raw);
|
|
105
|
+
const hash = digest(raw);
|
|
106
|
+
if (proof.blockerId !== id || proof.kind !== kind || proof.result !== (kind === 'change' ? 'changed' : 'pass') ||
|
|
107
|
+
!material(proof.source) || !material(proof.summary) || blocker.proofs.some(p => p.hash === hash || p.source === proof.source)) {
|
|
108
|
+
throw new Error('New evidence for this blocker and action is required');
|
|
109
|
+
}
|
|
110
|
+
return { file, kind, hash, source: proof.source };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function acceptDiagnosis(root, id, file) {
|
|
114
|
+
const raw = await readInside(root, file);
|
|
115
|
+
const proof = JSON.parse(raw);
|
|
116
|
+
const age = Date.now() - Date.parse(proof.checkedAt);
|
|
117
|
+
if (proof.blockerId !== id || proof.kind !== 'diagnosis' ||
|
|
118
|
+
!['product', 'tooling', 'credential', 'environment', 'self-caused'].includes(proof.classification) ||
|
|
119
|
+
!material(proof.command) || !material(proof.observed) || !material(proof.source) ||
|
|
120
|
+
!material(proof.selfCauseCheck) || proof.selfCauseChecked !== true ||
|
|
121
|
+
proof.authorizedRepairAvailable !== false || !material(proof.authorityCheck) ||
|
|
122
|
+
!(material(proof.alternativeCheck) || material(proof.noSafeAlternativeReason)) ||
|
|
123
|
+
!Number.isFinite(age) || age < -300000 || age > 86400000 ||
|
|
124
|
+
digest(await readInside(root, proof.evidence)) !== proof.sha256) {
|
|
125
|
+
throw new Error('Fresh diagnosis and authority evidence are required before technical escalation');
|
|
126
|
+
}
|
|
127
|
+
return { file, kind: 'diagnosis', hash: digest(raw), source: proof.source };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function inspectBlockers(stateDir, { taskId } = {}) {
|
|
131
|
+
if (taskId !== undefined && !/^T\d+$/.test(taskId)) throw new Error('Invalid task ID');
|
|
132
|
+
const root = await fs.realpath(stateDir);
|
|
133
|
+
const register = await load(root);
|
|
134
|
+
const active = [];
|
|
135
|
+
for (const [id, blocker] of Object.entries(register.blockers)) {
|
|
136
|
+
// Empty task scope means the blocker affects the whole feature.
|
|
137
|
+
if (taskId && blocker.tasks.length && !blocker.tasks.includes(taskId)) continue;
|
|
138
|
+
try { await checkProofs(root, blocker); }
|
|
139
|
+
catch { active.push({ id, state: 'blocked', reason: 'evidence_invalid' }); continue; }
|
|
140
|
+
if (blocker.state !== 'resolved') active.push({ id, state: blocker.state, category: blocker.category });
|
|
141
|
+
}
|
|
142
|
+
return { status: active.length ? 'blocked' : 'clear', blockers: active,
|
|
143
|
+
registered: Object.keys(register.blockers).length,
|
|
144
|
+
coverage: 'Recorded blockers only; does not discover unreported blockers or verify user consent.' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function applyBlockerEvent(stateDir, event) {
|
|
148
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) throw new Error('An event object is required');
|
|
149
|
+
const root = await fs.realpath(stateDir);
|
|
150
|
+
const lockPath = path.join(root, '.gofer-blocker.lock');
|
|
151
|
+
const lock = await fs.open(lockPath, 'wx', 0o600);
|
|
152
|
+
let temporary;
|
|
153
|
+
try {
|
|
154
|
+
const register = await load(root);
|
|
155
|
+
const id = event.action === 'open' ? blockerIdentity(event) : event.blockerId;
|
|
156
|
+
if (typeof id !== 'string' || !/^[a-f0-9]{64}$/.test(id)) throw new Error('Blocker ID is required');
|
|
157
|
+
let b = register.blockers[id];
|
|
158
|
+
const response = { allowed: false, blockerId: id, decision: 'wait' };
|
|
159
|
+
if (event.action === 'open') {
|
|
160
|
+
if (!CATEGORIES.includes(event.category) || !material(event.owner) || !material(event.question) || !material(event.requiredChange) ||
|
|
161
|
+
!Array.isArray(event.tasks) || event.tasks.some(t => !/^T\d+$/.test(t))) throw new Error('Blocker classification and scope are required');
|
|
162
|
+
if (!b) {
|
|
163
|
+
b = { goalKey: key(event.goalKey), subjectKey: key(event.subjectKey), conditionKey: key(event.conditionKey),
|
|
164
|
+
category: event.category, needsDiagnosis: event.category !== 'decision', owner: event.owner, question: event.question, requiredChange: event.requiredChange,
|
|
165
|
+
tasks: [...new Set(event.tasks)], state: waits(event.category) ? 'waiting' : 'ready', asked: false,
|
|
166
|
+
attempts: [], proofs: [], history: [], epoch: 0 };
|
|
167
|
+
register.blockers[id] = b;
|
|
168
|
+
response.allowed = true;
|
|
169
|
+
response.decision = b.state;
|
|
170
|
+
} else {
|
|
171
|
+
// Rewording or another surface cannot clear or narrow a known blocker.
|
|
172
|
+
await checkProofs(root, b);
|
|
173
|
+
b.tasks = b.tasks.length && event.tasks.length ? [...new Set([...b.tasks, ...event.tasks])] : [];
|
|
174
|
+
response.decision = b.state === 'resolved' ? 'already_resolved' : 'already_registered';
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
if (!b) throw new Error('Unknown blocker');
|
|
178
|
+
await checkProofs(root, b);
|
|
179
|
+
const pending = b.attempts.find(a => a.result === 'pending');
|
|
180
|
+
if (event.action === 'ask') {
|
|
181
|
+
if (!b.asked && !pending && b.state !== 'resolved') {
|
|
182
|
+
// A business choice needs an answer, not a contrived failing command.
|
|
183
|
+
if (b.category !== 'decision' || b.needsDiagnosis === true) b.proofs.push(await acceptDiagnosis(root, id, event.verification));
|
|
184
|
+
b.asked = true;
|
|
185
|
+
b.state = 'waiting';
|
|
186
|
+
response.allowed = true;
|
|
187
|
+
response.decision = 'ask_once';
|
|
188
|
+
response.question = b.question;
|
|
189
|
+
response.requiredChange = b.requiredChange;
|
|
190
|
+
response.owner = b.owner;
|
|
191
|
+
}
|
|
192
|
+
} else if (event.action === 'attempt') {
|
|
193
|
+
const approach = key(event.approachKey);
|
|
194
|
+
const current = b.attempts.filter(a => a.epoch === b.epoch);
|
|
195
|
+
if (b.state === 'ready' && !pending && !waits(b.category) && current.length < 2 && !current.some(a => a.approach === approach)) {
|
|
196
|
+
const attempt = { id: randomUUID(), epoch: b.epoch, approach, result: 'pending' };
|
|
197
|
+
b.attempts.push(attempt);
|
|
198
|
+
response.allowed = true;
|
|
199
|
+
response.decision = current.length ? 'recover_once' : 'investigate_once';
|
|
200
|
+
response.attemptId = attempt.id;
|
|
201
|
+
} else if (b.state === 'ready' && !pending) {
|
|
202
|
+
b.state = 'blocked';
|
|
203
|
+
response.decision = 'stop_no_new_approach_or_budget';
|
|
204
|
+
}
|
|
205
|
+
} else if (event.action === 'result') {
|
|
206
|
+
if (!pending || event.attemptId !== pending.id || !['progress', 'no_progress', 'blocked'].includes(event.result) || !material(event.summary)) throw new Error('A result for the reserved attempt is required');
|
|
207
|
+
pending.result = event.result;
|
|
208
|
+
pending.summary = event.summary;
|
|
209
|
+
b.state = event.result === 'blocked' || b.attempts.filter(a => a.epoch === b.epoch).length >= 2 ? 'blocked' : 'ready';
|
|
210
|
+
response.allowed = true;
|
|
211
|
+
response.decision = b.state;
|
|
212
|
+
} else if (event.action === 'classify') {
|
|
213
|
+
if (pending || b.state === 'resolved' || !CATEGORIES.includes(event.category)) throw new Error('Cannot change this classification');
|
|
214
|
+
const previousCategory = b.category;
|
|
215
|
+
b.category = event.category;
|
|
216
|
+
b.needsDiagnosis = b.needsDiagnosis === true || previousCategory !== 'decision' || event.category !== 'decision';
|
|
217
|
+
if (waits(b.category)) b.state = 'waiting';
|
|
218
|
+
// Reclassification never resets attempts or clears a waiting decision.
|
|
219
|
+
response.allowed = true;
|
|
220
|
+
response.decision = b.state;
|
|
221
|
+
} else if (event.action === 'resume' || event.action === 'resolve') {
|
|
222
|
+
if (pending) throw new Error('Record the pending attempt result first');
|
|
223
|
+
const isResume = event.action === 'resume';
|
|
224
|
+
if (isResume && b.state === 'ready') throw new Error('Ready work does not need a fresh attempt budget');
|
|
225
|
+
if (isResume && b.epoch >= 1) {
|
|
226
|
+
response.decision = 'stop_resume_budget_exhausted';
|
|
227
|
+
} else if (!isResume && b.state === 'resolved') {
|
|
228
|
+
response.decision = 'already_resolved';
|
|
229
|
+
} else {
|
|
230
|
+
if (isResume && !['solvable', 'unknown'].includes(event.category)) throw new Error('Resume must identify work the AI can now attempt');
|
|
231
|
+
const proof = await acceptProof(root, b, id, event.evidence, isResume ? 'change' : 'resolution');
|
|
232
|
+
b.proofs.push(proof);
|
|
233
|
+
b.state = isResume ? 'ready' : 'resolved';
|
|
234
|
+
if (isResume) { b.epoch++; b.category = event.category; b.needsDiagnosis = true; }
|
|
235
|
+
response.allowed = true;
|
|
236
|
+
response.decision = b.state;
|
|
237
|
+
}
|
|
238
|
+
} else throw new Error('Unknown blocker action');
|
|
239
|
+
}
|
|
240
|
+
if (b.history.length >= 1000) throw new Error('Blocker history limit reached; review is required');
|
|
241
|
+
b.history.push({ at: new Date().toISOString(), action: event.action, allowed: response.allowed, decision: response.decision });
|
|
242
|
+
validateRegister(register);
|
|
243
|
+
temporary = path.join(root, `.blocker-register-${randomUUID()}.tmp`);
|
|
244
|
+
await fs.writeFile(temporary, JSON.stringify(register, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
245
|
+
await fs.rename(temporary, path.join(root, FILE));
|
|
246
|
+
temporary = undefined;
|
|
247
|
+
return { ...response, state: b.state };
|
|
248
|
+
} finally {
|
|
249
|
+
if (temporary) await fs.unlink(temporary).catch(() => {});
|
|
250
|
+
await lock.close();
|
|
251
|
+
await fs.unlink(lockPath);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function main() {
|
|
256
|
+
const args = process.argv.slice(2);
|
|
257
|
+
if (args.includes('--help')) {
|
|
258
|
+
console.log('Usage: gofer-blocker-control.mjs --state-dir <directory> [--event <private-json-file> | --task <T001>]'); return;
|
|
259
|
+
}
|
|
260
|
+
const values = {};
|
|
261
|
+
for (let i = 0; i < args.length; i++) {
|
|
262
|
+
if (!['--state-dir', '--event', '--task'].includes(args[i]) || !args[i + 1] || args[i + 1].startsWith('--')) throw new Error('Invalid arguments');
|
|
263
|
+
values[args[i].slice(2)] = args[++i];
|
|
264
|
+
}
|
|
265
|
+
if (!values['state-dir'] || (values.event && values.task)) throw new Error('State directory and one action are required');
|
|
266
|
+
const result = values.event
|
|
267
|
+
? await applyBlockerEvent(values['state-dir'], JSON.parse(await readBounded(await fs.realpath(values.event))))
|
|
268
|
+
: await inspectBlockers(values['state-dir'], { taskId: values.task });
|
|
269
|
+
console.log(JSON.stringify(result, null, 2));
|
|
270
|
+
if (result.allowed === false || result.status === 'blocked') process.exitCode = 1;
|
|
271
|
+
}
|
|
272
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
273
|
+
main().catch(() => { console.error('Blocker check stopped. Check local inputs or an active writer; do not repeat work or bypass the check.'); process.exitCode = 1; });
|
|
274
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code host profile for the shared native launcher. It reuses the same
|
|
3
|
+
* process-group evidence, Git baseline and scope checks as the Codex launcher.
|
|
4
|
+
* Only three things are Claude-specific: how the sandbox policy is described,
|
|
5
|
+
* how the command line is built, and how the result and usage are read.
|
|
6
|
+
*
|
|
7
|
+
* Isolation comes from Claude Code's own OS sandbox, configured per task. A
|
|
8
|
+
* live probe found the default sandbox lets a worker write the shared Git
|
|
9
|
+
* store of a linked worktree, so this policy denies that folder explicitly.
|
|
10
|
+
* Claude has no model catalogue command and no no-model boundary test, so this
|
|
11
|
+
* adapter does not issue capability receipts and does not, by itself, satisfy
|
|
12
|
+
* the EAI CLI isolation gate. See docs/verified-autonomous-runtime.md.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
15
|
+
import { lstatSync, mkdtempSync, existsSync, readdirSync, realpathSync, rmSync, rmdirSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { startLocalHostInvocation } from './gofer-native-adapter.mjs';
|
|
18
|
+
import { sharedGitDirectory } from './gofer-worktree-layout.mjs';
|
|
19
|
+
|
|
20
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
21
|
+
/**
|
|
22
|
+
* The per-task sandbox settings. `protectedReadPaths` (for example the account
|
|
23
|
+
* trust folder) are denied for both the shell sandbox and the file tools.
|
|
24
|
+
*/
|
|
25
|
+
export function claudeSandboxPolicy(workspaceRoot, { protectedReadPaths = [] } = {}) {
|
|
26
|
+
const common = sharedGitDirectory(workspaceRoot);
|
|
27
|
+
if (!common || !Array.isArray(protectedReadPaths) ||
|
|
28
|
+
protectedReadPaths.some(item => !path.isAbsolute(item ?? ''))) return null;
|
|
29
|
+
const denied = [...protectedReadPaths];
|
|
30
|
+
const settings = { sandbox: { enabled: true, allowUnsandboxedCommands: false,
|
|
31
|
+
filesystem: { denyWrite: [common], ...(denied.length ? { denyRead: denied } : {}) } },
|
|
32
|
+
...(denied.length ? { permissions: { deny: denied.flatMap(item =>
|
|
33
|
+
['Read', 'Edit', 'Write'].map(tool => `${tool}(/${item}/**)`)) } } : {}) };
|
|
34
|
+
return Object.freeze({ common, settings: JSON.stringify(settings) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseResult(stdoutText) {
|
|
38
|
+
const trimmed = String(stdoutText ?? '').trim();
|
|
39
|
+
for (const candidate of [trimmed, trimmed.split('\n').at(-1)]) {
|
|
40
|
+
try { const value = JSON.parse(candidate); if (value && typeof value === 'object') return value; }
|
|
41
|
+
catch { /* try the next candidate */ }
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Token counts in the shape the pricing code expects, plus Claude's own cost. */
|
|
47
|
+
export function extractClaudeUsage(stdoutText) {
|
|
48
|
+
const usage = parseResult(stdoutText)?.usage;
|
|
49
|
+
const output = usage?.output_tokens;
|
|
50
|
+
const fresh = usage?.input_tokens;
|
|
51
|
+
const cachedRead = usage?.cache_read_input_tokens ?? 0;
|
|
52
|
+
const cachedWrite = usage?.cache_creation_input_tokens ?? 0;
|
|
53
|
+
if (![output, fresh, cachedRead, cachedWrite].every(value => Number.isInteger(value) && value >= 0)) return null;
|
|
54
|
+
const reported = parseResult(stdoutText)?.total_cost_usd;
|
|
55
|
+
return Object.freeze({ inputTokens: fresh + cachedRead + cachedWrite, cachedInputTokens: cachedRead,
|
|
56
|
+
outputTokens: output, reportedCostUsd: Number.isFinite(reported) && reported >= 0 ? reported : null });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const editRule = (tool, scope) => {
|
|
60
|
+
const clean = scope.replace(/\/$/, '');
|
|
61
|
+
return `${tool}(${scope.endsWith('/') ? `${clean}/**` : clean})`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const CLAUDE_HOST = Object.freeze({
|
|
65
|
+
name: 'claude',
|
|
66
|
+
sandboxPolicy: (workspace, hostOptions) => claudeSandboxPolicy(workspace, hostOptions),
|
|
67
|
+
buildArgs: ({ sandboxPolicy, modelId, prompt, allowedWriteScope, hostOptions }) => {
|
|
68
|
+
const budget = hostOptions?.maxBudgetUsd;
|
|
69
|
+
if (!(budget > 0) || budget > 100) throw new Error('INVALID_NATIVE_REQUEST');
|
|
70
|
+
const tools = ['Bash', 'Read', 'Glob', 'Grep',
|
|
71
|
+
...allowedWriteScope.flatMap(scope => [editRule('Edit', scope), editRule('Write', scope)])];
|
|
72
|
+
return ['-p', prompt, '--output-format', 'json', '--model', modelId, '--permission-mode', 'dontAsk',
|
|
73
|
+
'--no-session-persistence', '--disable-slash-commands', '--strict-mcp-config',
|
|
74
|
+
'--max-budget-usd', String(budget), '--settings', sandboxPolicy.settings,
|
|
75
|
+
'--allowedTools', tools.join(',')];
|
|
76
|
+
},
|
|
77
|
+
// A nested Claude session must not inherit its parent's session variables.
|
|
78
|
+
env: base => Object.fromEntries(Object.entries(base).filter(([key]) =>
|
|
79
|
+
key !== 'CLAUDECODE' && !key.startsWith('CLAUDE_CODE') && !key.startsWith('GIT_'))),
|
|
80
|
+
// Claude's sandbox leaves an empty `.claude/.cc-writes` folder in the worktree.
|
|
81
|
+
// It is removed only when it is empty and alone, so a file a worker places
|
|
82
|
+
// there is still reported as a scope violation.
|
|
83
|
+
afterExit: ({ workspace }) => {
|
|
84
|
+
const directory = path.join(workspace, '.claude');
|
|
85
|
+
const bookkeeping = path.join(directory, '.cc-writes');
|
|
86
|
+
try {
|
|
87
|
+
if (lstatSync(directory).isDirectory() && readdirSync(directory).join() === '.cc-writes' &&
|
|
88
|
+
lstatSync(bookkeeping).isDirectory() && readdirSync(bookkeeping).length === 0) {
|
|
89
|
+
rmdirSync(bookkeeping);
|
|
90
|
+
rmdirSync(directory);
|
|
91
|
+
}
|
|
92
|
+
} catch { /* nothing to clean */ }
|
|
93
|
+
},
|
|
94
|
+
finalMessage: ({ stdout }) => String(parseResult(stdout)?.result ?? ''),
|
|
95
|
+
extractUsage: stdoutText => extractClaudeUsage(stdoutText),
|
|
96
|
+
assertSuccess: stdoutText => {
|
|
97
|
+
const result = parseResult(stdoutText);
|
|
98
|
+
if (!result) throw new Error('NATIVE_HOST_RESULT_UNPARSEABLE');
|
|
99
|
+
if (result.is_error === true || result.subtype !== 'success') throw new Error('NATIVE_HOST_REPORTED_ERROR');
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** `command` must be the pinned absolute path of the Claude executable, and
|
|
104
|
+
* `hostOptions.maxBudgetUsd` bounds this one run inside Claude itself. */
|
|
105
|
+
export function startLocalClaudeInvocation(options = {}) {
|
|
106
|
+
if (!path.isAbsolute(options.command ?? '')) throw new Error('NATIVE_HOST_COMMAND_MUST_BE_PINNED');
|
|
107
|
+
return startLocalHostInvocation({ ...options, host: CLAUDE_HOST });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A live boundary probe. Unlike Codex, Claude has no no-model sandbox command,
|
|
112
|
+
* so this asks a real model (about US$0.05 on Haiku) to try one allowed write
|
|
113
|
+
* and four forbidden ones (shell and file tool, sibling folder and shared Git
|
|
114
|
+
* store), then checks the filesystem. It spends money, so nothing calls it
|
|
115
|
+
* implicitly. Returns the executable when the boundary held, otherwise null.
|
|
116
|
+
*/
|
|
117
|
+
export function probeClaudeSandboxBoundary({ workspaceRoot, executable, model = 'haiku',
|
|
118
|
+
maxBudgetUsd = 0.25, run = spawnSync } = {}) {
|
|
119
|
+
if (!text(workspaceRoot) || !path.isAbsolute(executable ?? '')) return null;
|
|
120
|
+
const policy = claudeSandboxPolicy(workspaceRoot);
|
|
121
|
+
if (!policy) return null;
|
|
122
|
+
const workspace = realpathSync(workspaceRoot);
|
|
123
|
+
let sibling;
|
|
124
|
+
try {
|
|
125
|
+
sibling = mkdtempSync(path.join(path.dirname(workspace), '.gofer-claude-probe-'));
|
|
126
|
+
const inside_ = path.join(workspace, `.gofer-claude-probe-${process.pid}`);
|
|
127
|
+
const outside = path.join(sibling, 'outside.txt');
|
|
128
|
+
const outsideTool = path.join(sibling, 'outside-tool.txt');
|
|
129
|
+
const store = path.join(policy.common, `.gofer-claude-probe-${process.pid}`);
|
|
130
|
+
const prompt = 'This is a file-permission test. Do these five things, each separately, even if an ' +
|
|
131
|
+
`earlier one fails, and never retry or work around a failure. Shell: (1) touch ${inside_} (2) touch ${outside} ` +
|
|
132
|
+
`(3) touch ${store}. File tool: (4) use the Write tool to create ${outsideTool} with the text x. Then report each result and stop.`;
|
|
133
|
+
const args = ['-p', prompt, '--output-format', 'json', '--model', model, '--permission-mode', 'dontAsk',
|
|
134
|
+
'--no-session-persistence', '--max-budget-usd', String(maxBudgetUsd), '--settings', policy.settings,
|
|
135
|
+
// Mirrors production: Bash, and file tools scoped to one in-scope file only.
|
|
136
|
+
'--allowedTools', 'Bash,Write(gofer-claude-probe-scope.txt)'];
|
|
137
|
+
const result = run(executable, args, { cwd: workspace, encoding: 'utf8', timeout: 180000,
|
|
138
|
+
maxBuffer: 4 * 1024 * 1024, env: CLAUDE_HOST.env(process.env) });
|
|
139
|
+
const parsed = parseResult(result.stdout);
|
|
140
|
+
// A model that skipped the forbidden attempts would pass by doing nothing.
|
|
141
|
+
// Require its own report to describe at least three refusals as well.
|
|
142
|
+
const refusals = (String(parsed?.result ?? '').match(/not permitted|permission denied|denied|blocked|refused|not allowed/gi) ?? []).length;
|
|
143
|
+
const held = result.status === 0 && parsed?.is_error !== true && refusals >= 3 && existsSync(inside_) &&
|
|
144
|
+
!existsSync(outside) && !existsSync(store) && !existsSync(outsideTool);
|
|
145
|
+
rmSync(inside_, { force: true }); rmSync(outside, { force: true }); rmSync(store, { force: true });
|
|
146
|
+
return held ? executable : null;
|
|
147
|
+
} catch { return null; }
|
|
148
|
+
finally { if (sibling) rmSync(sibling, { recursive: true, force: true }); }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function claudeExecutableVersion(executable) {
|
|
152
|
+
try { return execFileSync(executable, ['--version'], { encoding: 'utf8', timeout: 5000 }).trim(); }
|
|
153
|
+
catch { return null; }
|
|
154
|
+
}
|