@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,1232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash, sign, verify } from 'node:crypto';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { requestTypeSafeEvaluation, sharedCredentialConfigured } from './gofer-typesafe-credentials.mjs';
|
|
10
|
+
import { loadActiveBenchmarkVerifierKey, resolveTrustedEvaluatorPublicKey,
|
|
11
|
+
VERIFIER_EVALUATOR } from './gofer-trusted-evaluator.mjs';
|
|
12
|
+
import { promptHidden } from './gofer-tty-prompt.mjs';
|
|
13
|
+
|
|
14
|
+
const POLICY_PATH = path.join('.specify', 'config', 'typesafe-learning-review.json');
|
|
15
|
+
const STORE_PATH = path.join('.specify', 'memory', 'reviewed-learning');
|
|
16
|
+
const MAX_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
17
|
+
const MAX_RECORDS = 100_000;
|
|
18
|
+
const MAX_LESSON_BYTES = 8 * 1024;
|
|
19
|
+
const OFFICIAL_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
|
|
20
|
+
const noFollowFlag = process.platform === 'win32' ? 0 : constants.O_NOFOLLOW;
|
|
21
|
+
const sensitiveKey = /(authorization|api[-_]?key|secret|token|password|credential|cookie)/i;
|
|
22
|
+
const secretPatterns = [
|
|
23
|
+
/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
|
|
24
|
+
/\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{12,}\b/g,
|
|
25
|
+
/\bTYPESAFE_API_KEY\s*=\s*[^\s]+/gi,
|
|
26
|
+
/\b(?:api[-_]?key|access[-_]?token|password|secret|token)\s*=\s*[^\s&]+/gi,
|
|
27
|
+
];
|
|
28
|
+
const verifiedDirectories = new Map();
|
|
29
|
+
const REVIEW_LOCK_TTL_MS = 5 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
function sha256(value) {
|
|
32
|
+
// This is a content-addressing digest, never a password derivation function.
|
|
33
|
+
// lgtm[js/insufficient-password-hash]
|
|
34
|
+
return createHash('sha256').update(value).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stable(value) {
|
|
38
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function canonical(value) {
|
|
46
|
+
return JSON.stringify(stable(value));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function text(value) {
|
|
50
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function credentialSourceLabel(value) {
|
|
54
|
+
if (value === 'environment') return 'environment';
|
|
55
|
+
if (value === 'project_secret_file') return 'project_secret_file';
|
|
56
|
+
if (value === 'macos_keychain') return 'macos_keychain';
|
|
57
|
+
return 'none';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validTime(value) {
|
|
61
|
+
const normalized = text(value);
|
|
62
|
+
if (!normalized || Number.isNaN(Date.parse(normalized))) return '';
|
|
63
|
+
return new Date(normalized).toISOString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function truncateUtf8(value, maxBytes) {
|
|
67
|
+
const input = Buffer.from(String(value), 'utf8');
|
|
68
|
+
if (input.length <= maxBytes) return input.toString('utf8');
|
|
69
|
+
let end = maxBytes;
|
|
70
|
+
while (end > 0 && (input[end] & 0xc0) === 0x80) end -= 1;
|
|
71
|
+
return input.subarray(0, end).toString('utf8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function redactString(value, maxBytes) {
|
|
75
|
+
let output = String(value);
|
|
76
|
+
for (const pattern of secretPatterns) output = output.replace(pattern, '[REDACTED]');
|
|
77
|
+
return truncateUtf8(output, maxBytes);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function redactValue(value, maxBytes, key = '', depth = 0) {
|
|
81
|
+
if (depth > 8) return '[TRUNCATED_DEPTH]';
|
|
82
|
+
if (sensitiveKey.test(key)) return '[REDACTED]';
|
|
83
|
+
if (typeof value === 'string') return redactString(value, maxBytes);
|
|
84
|
+
if (typeof value === 'number' || typeof value === 'boolean' || value === null) return value;
|
|
85
|
+
if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactValue(item, maxBytes, '', depth + 1));
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Object.entries(value).slice(0, 100).map(([name, item]) => [
|
|
89
|
+
redactString(name, 120),
|
|
90
|
+
redactValue(item, maxBytes, name, depth + 1),
|
|
91
|
+
])
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return String(value ?? '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function safeWorkspace(workspace) {
|
|
98
|
+
const root = path.resolve(workspace);
|
|
99
|
+
const info = await fs.lstat(root).catch(() => null);
|
|
100
|
+
if (!info || !info.isDirectory() || info.isSymbolicLink()) {
|
|
101
|
+
throw new Error('LEARNING_WORKSPACE_INVALID');
|
|
102
|
+
}
|
|
103
|
+
return root;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function rejectSymlinkComponents(root, target) {
|
|
107
|
+
const relative = path.relative(root, target);
|
|
108
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
109
|
+
throw new Error('LEARNING_PATH_OUTSIDE_WORKSPACE');
|
|
110
|
+
}
|
|
111
|
+
let current = root;
|
|
112
|
+
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
113
|
+
current = path.join(current, component);
|
|
114
|
+
const info = await fs.lstat(current).catch((error) => {
|
|
115
|
+
if (error?.code === 'ENOENT') return null;
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
if (!info) return;
|
|
119
|
+
if (info.isSymbolicLink()) throw new Error('LEARNING_PATH_SYMLINK');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function confined(workspace, input, { exists = false, regular = false } = {}) {
|
|
124
|
+
const root = await safeWorkspace(workspace);
|
|
125
|
+
const target = path.resolve(root, input);
|
|
126
|
+
await rejectSymlinkComponents(root, target);
|
|
127
|
+
const info = await fs.lstat(target).catch((error) => {
|
|
128
|
+
if (error?.code === 'ENOENT') return null;
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
if (exists && !info) throw new Error('LEARNING_PATH_MISSING');
|
|
132
|
+
if (info?.isSymbolicLink()) throw new Error('LEARNING_PATH_SYMLINK');
|
|
133
|
+
if (regular && info && !info.isFile()) throw new Error('LEARNING_PATH_NOT_FILE');
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function holdDirectoryChain(root, target) {
|
|
138
|
+
const relativeParent = path.relative(root, path.dirname(target));
|
|
139
|
+
const directories = [root];
|
|
140
|
+
let current = root;
|
|
141
|
+
for (const component of relativeParent.split(path.sep).filter(Boolean)) {
|
|
142
|
+
current = path.join(current, component);
|
|
143
|
+
directories.push(current);
|
|
144
|
+
}
|
|
145
|
+
const held = [];
|
|
146
|
+
try {
|
|
147
|
+
for (const directory of directories) {
|
|
148
|
+
const handle = await fs.open(directory,
|
|
149
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | noFollowFlag);
|
|
150
|
+
const opened = await handle.stat();
|
|
151
|
+
const currentInfo = await fs.lstat(directory);
|
|
152
|
+
if (!opened.isDirectory() || currentInfo.isSymbolicLink() ||
|
|
153
|
+
!sameIdentity(opened, currentInfo)) {
|
|
154
|
+
await handle.close();
|
|
155
|
+
throw new Error('LEARNING_STORE_PARENT_CHANGED');
|
|
156
|
+
}
|
|
157
|
+
held.push({ directory, handle, identity: opened });
|
|
158
|
+
}
|
|
159
|
+
return held;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
await Promise.allSettled(held.map(({ handle }) => handle.close()));
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function readBoundedFile(workspace, input, maxBytes = MAX_SOURCE_BYTES) {
|
|
167
|
+
const root = await safeWorkspace(workspace);
|
|
168
|
+
const target = await confined(root, input, { exists: true, regular: true });
|
|
169
|
+
const held = await holdDirectoryChain(root, target);
|
|
170
|
+
let handle;
|
|
171
|
+
try {
|
|
172
|
+
handle = await fs.open(target, constants.O_RDONLY | noFollowFlag);
|
|
173
|
+
const info = await handle.stat();
|
|
174
|
+
if (!info.isFile()) throw new Error('LEARNING_STORE_INVALID');
|
|
175
|
+
if (info.size > maxBytes) throw new Error('LEARNING_INPUT_TOO_LARGE');
|
|
176
|
+
const chunks = [];
|
|
177
|
+
let total = 0;
|
|
178
|
+
while (true) {
|
|
179
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, maxBytes - total + 1));
|
|
180
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
181
|
+
if (!bytesRead) break;
|
|
182
|
+
total += bytesRead;
|
|
183
|
+
if (total > maxBytes) throw new Error('LEARNING_INPUT_TOO_LARGE');
|
|
184
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
185
|
+
}
|
|
186
|
+
const content = Buffer.concat(chunks, total).toString('utf8');
|
|
187
|
+
for (const entry of held) {
|
|
188
|
+
const current = await fs.lstat(entry.directory);
|
|
189
|
+
if (current.isSymbolicLink() || !sameIdentity(entry.identity, current)) {
|
|
190
|
+
throw new Error('LEARNING_STORE_PARENT_CHANGED');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { target, content };
|
|
194
|
+
} finally {
|
|
195
|
+
if (handle) await handle.close();
|
|
196
|
+
await Promise.allSettled(held.map(({ handle: directoryHandle }) => directoryHandle.close()));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function ensureStore(workspace) {
|
|
201
|
+
const root = await safeWorkspace(workspace);
|
|
202
|
+
let current = root;
|
|
203
|
+
for (const component of STORE_PATH.split(path.sep)) {
|
|
204
|
+
const parentHandle = await fs.open(current,
|
|
205
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | noFollowFlag);
|
|
206
|
+
try {
|
|
207
|
+
const openedParent = await parentHandle.stat();
|
|
208
|
+
const parentAfterOpen = await fs.lstat(current);
|
|
209
|
+
if (!openedParent.isDirectory() || parentAfterOpen.isSymbolicLink() ||
|
|
210
|
+
!sameIdentity(openedParent, parentAfterOpen)) {
|
|
211
|
+
throw new Error('LEARNING_STORE_PARENT_CHANGED');
|
|
212
|
+
}
|
|
213
|
+
const next = path.join(current, component);
|
|
214
|
+
await fs.mkdir(next, { mode: 0o700 }).catch((error) => {
|
|
215
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
216
|
+
});
|
|
217
|
+
const parentAfter = await fs.lstat(current);
|
|
218
|
+
if (!sameIdentity(openedParent, parentAfter)) {
|
|
219
|
+
throw new Error('LEARNING_STORE_PATH_INVALID');
|
|
220
|
+
}
|
|
221
|
+
// The parent remains open, and the child is opened no-follow then validated by identity.
|
|
222
|
+
// codeql[js/file-system-race]
|
|
223
|
+
const childHandle = await fs.open(next,
|
|
224
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | noFollowFlag).catch((error) => {
|
|
225
|
+
if (['ELOOP', 'ENOTDIR'].includes(error?.code)) throw new Error('LEARNING_PATH_SYMLINK');
|
|
226
|
+
throw error;
|
|
227
|
+
});
|
|
228
|
+
try {
|
|
229
|
+
const openedChild = await childHandle.stat();
|
|
230
|
+
const child = await fs.lstat(next);
|
|
231
|
+
if (!openedChild.isDirectory() || child.isSymbolicLink() ||
|
|
232
|
+
!sameIdentity(child, openedChild)) {
|
|
233
|
+
throw new Error('LEARNING_STORE_PATH_INVALID');
|
|
234
|
+
}
|
|
235
|
+
await childHandle.chmod(0o700);
|
|
236
|
+
verifiedDirectories.set(next, { dev: openedChild.dev, ino: openedChild.ino });
|
|
237
|
+
} finally {
|
|
238
|
+
await childHandle.close();
|
|
239
|
+
}
|
|
240
|
+
current = next;
|
|
241
|
+
} finally {
|
|
242
|
+
await parentHandle.close();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return current;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function withPrivateDirectory(parent, name, action) {
|
|
249
|
+
await verifyRememberedDirectory(parent);
|
|
250
|
+
const parentHandle = await fs.open(parent,
|
|
251
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | noFollowFlag);
|
|
252
|
+
const parentIdentity = await parentHandle.stat();
|
|
253
|
+
const target = path.join(parent, name);
|
|
254
|
+
try {
|
|
255
|
+
await fs.mkdir(target, { mode: 0o700 }).catch((error) => {
|
|
256
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
257
|
+
});
|
|
258
|
+
await verifyRememberedDirectory(parent);
|
|
259
|
+
const handle = await fs.open(target,
|
|
260
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | noFollowFlag).catch((error) => {
|
|
261
|
+
if (['ELOOP', 'ENOTDIR'].includes(error?.code)) throw new Error('LEARNING_STORE_PATH_INVALID');
|
|
262
|
+
throw error;
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
const opened = await handle.stat();
|
|
266
|
+
const current = await fs.lstat(target);
|
|
267
|
+
const currentParent = await fs.lstat(parent);
|
|
268
|
+
if (!opened.isDirectory() || current.isSymbolicLink() || !sameIdentity(opened, current) ||
|
|
269
|
+
currentParent.isSymbolicLink() || !sameIdentity(parentIdentity, currentParent)) {
|
|
270
|
+
throw new Error('LEARNING_STORE_PATH_INVALID');
|
|
271
|
+
}
|
|
272
|
+
await handle.chmod(0o700);
|
|
273
|
+
verifiedDirectories.set(target, { dev: opened.dev, ino: opened.ino });
|
|
274
|
+
const result = await action(target);
|
|
275
|
+
const [parentAfterAction, targetAfterAction] = await Promise.all([
|
|
276
|
+
fs.lstat(parent),
|
|
277
|
+
fs.lstat(target),
|
|
278
|
+
]);
|
|
279
|
+
if (parentAfterAction.isSymbolicLink() || targetAfterAction.isSymbolicLink() ||
|
|
280
|
+
!sameIdentity(parentIdentity, parentAfterAction) ||
|
|
281
|
+
!sameIdentity(opened, targetAfterAction)) {
|
|
282
|
+
throw new Error('LEARNING_STORE_PATH_INVALID');
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
} finally {
|
|
286
|
+
await handle.close();
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
await parentHandle.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sameIdentity(left, right) {
|
|
294
|
+
if (left.dev === undefined || left.ino === undefined ||
|
|
295
|
+
right.dev === undefined || right.ino === undefined) return true;
|
|
296
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function withVerifiedParent(target, action) {
|
|
300
|
+
const parent = path.dirname(target);
|
|
301
|
+
const handle = await fs.open(parent,
|
|
302
|
+
constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | noFollowFlag);
|
|
303
|
+
const opened = await handle.stat();
|
|
304
|
+
try {
|
|
305
|
+
const current = await fs.lstat(parent);
|
|
306
|
+
if (!opened.isDirectory() || current.isSymbolicLink() || !sameIdentity(opened, current)) {
|
|
307
|
+
throw new Error('LEARNING_STORE_PARENT_CHANGED');
|
|
308
|
+
}
|
|
309
|
+
const expected = verifiedDirectories.get(parent);
|
|
310
|
+
if (expected && !sameIdentity(expected, opened)) throw new Error('LEARNING_STORE_PARENT_CHANGED');
|
|
311
|
+
const result = await action();
|
|
312
|
+
const after = await fs.lstat(parent);
|
|
313
|
+
if (after.isSymbolicLink() || !sameIdentity(opened, after)) {
|
|
314
|
+
throw new Error('LEARNING_STORE_PARENT_CHANGED');
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
} finally {
|
|
318
|
+
await handle.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function verifyRememberedDirectory(target) {
|
|
323
|
+
await withVerifiedParent(path.join(target, '.guard'), async () => undefined);
|
|
324
|
+
const current = await fs.lstat(target);
|
|
325
|
+
const expected = verifiedDirectories.get(target);
|
|
326
|
+
if (current.isSymbolicLink() || !current.isDirectory() ||
|
|
327
|
+
(expected && !sameIdentity(expected, current))) {
|
|
328
|
+
throw new Error('LEARNING_STORE_PATH_INVALID');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function existingRegularFile(target) {
|
|
333
|
+
const info = await fs.lstat(target).catch((error) => {
|
|
334
|
+
if (error?.code === 'ENOENT') return null;
|
|
335
|
+
throw error;
|
|
336
|
+
});
|
|
337
|
+
if (info && (info.isSymbolicLink() || !info.isFile())) {
|
|
338
|
+
throw new Error('LEARNING_STORE_INVALID');
|
|
339
|
+
}
|
|
340
|
+
return info;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function appendPrivate(target, value) {
|
|
344
|
+
return withVerifiedParent(target, async () => {
|
|
345
|
+
const beforeOpen = await existingRegularFile(target);
|
|
346
|
+
const handle = await fs.open(
|
|
347
|
+
target,
|
|
348
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | noFollowFlag,
|
|
349
|
+
0o600
|
|
350
|
+
);
|
|
351
|
+
try {
|
|
352
|
+
const info = await handle.stat();
|
|
353
|
+
if (!info.isFile() || (beforeOpen && !sameIdentity(beforeOpen, info))) {
|
|
354
|
+
throw new Error('LEARNING_STORE_INVALID');
|
|
355
|
+
}
|
|
356
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
357
|
+
await handle.sync();
|
|
358
|
+
} finally {
|
|
359
|
+
await handle.close();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function writePrivateExclusive(target, value) {
|
|
365
|
+
return withVerifiedParent(target, async () => {
|
|
366
|
+
let handle;
|
|
367
|
+
try {
|
|
368
|
+
await existingRegularFile(target);
|
|
369
|
+
handle = await fs.open(
|
|
370
|
+
target,
|
|
371
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag,
|
|
372
|
+
0o600
|
|
373
|
+
);
|
|
374
|
+
await handle.writeFile(value);
|
|
375
|
+
await handle.sync();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
378
|
+
const beforeOpen = await existingRegularFile(target);
|
|
379
|
+
const existingHandle = await fs.open(target, constants.O_RDONLY | noFollowFlag);
|
|
380
|
+
try {
|
|
381
|
+
const info = await existingHandle.stat();
|
|
382
|
+
if (!info.isFile() || !beforeOpen || !sameIdentity(beforeOpen, info)) {
|
|
383
|
+
throw new Error('LEARNING_STORE_INVALID');
|
|
384
|
+
}
|
|
385
|
+
const existing = await existingHandle.readFile('utf8');
|
|
386
|
+
if (existing !== value) throw new Error('LEARNING_RECORD_COLLISION');
|
|
387
|
+
} finally {
|
|
388
|
+
await existingHandle.close();
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
if (handle) await handle.close();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function readJsonLines(target) {
|
|
397
|
+
return withVerifiedParent(target, async () => {
|
|
398
|
+
const beforeOpen = await existingRegularFile(target);
|
|
399
|
+
if (!beforeOpen) return [];
|
|
400
|
+
const handle = await fs.open(target, constants.O_RDONLY | noFollowFlag).catch((error) => {
|
|
401
|
+
if (error?.code === 'ENOENT') return null;
|
|
402
|
+
throw error;
|
|
403
|
+
});
|
|
404
|
+
if (!handle) return [];
|
|
405
|
+
try {
|
|
406
|
+
const info = await handle.stat();
|
|
407
|
+
const current = await fs.lstat(target);
|
|
408
|
+
if (!info.isFile() || current.isSymbolicLink() || !current.isFile() ||
|
|
409
|
+
!sameIdentity(beforeOpen, info) || !sameIdentity(info, current) ||
|
|
410
|
+
info.size > MAX_SOURCE_BYTES) throw new Error('LEARNING_STORE_INVALID');
|
|
411
|
+
const lines = (await handle.readFile('utf8')).split('\n').filter(Boolean);
|
|
412
|
+
if (lines.length > MAX_RECORDS) throw new Error('LEARNING_STORE_LIMIT');
|
|
413
|
+
return lines.map((line) => JSON.parse(line));
|
|
414
|
+
} finally {
|
|
415
|
+
await handle.close();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function eventType(name) {
|
|
421
|
+
if (['started', 'resumed', 'call_reserved', 'attempt_reserved', 'lease_granted'].includes(name)) return 'lifecycle';
|
|
422
|
+
if (['ledger_authorized', 'commit_authorized', 'approval'].includes(name)) return 'approval';
|
|
423
|
+
if (['check', 'verified'].includes(name)) return 'validation';
|
|
424
|
+
if (['blocked', 'cancelled', 'stale', 'repair_required', 'failed', 'error'].includes(name)) return 'error';
|
|
425
|
+
if (/command|shell/.test(name)) return 'command';
|
|
426
|
+
if (/file|patch|write|edit/.test(name)) return 'file_change';
|
|
427
|
+
if (/tool/.test(name)) return 'tool';
|
|
428
|
+
if (/message|prompt|response/.test(name)) return 'message';
|
|
429
|
+
return 'other';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function validateTrace(trace) {
|
|
433
|
+
if (trace?.schemaVersion !== 1 || !/^trace_[a-f0-9]{32}$/.test(trace.traceId ?? '') ||
|
|
434
|
+
!/^project_[a-f0-9]{24}$/.test(trace.projectId ?? '') || !text(trace.host) ||
|
|
435
|
+
!text(trace.sessionId) || !text(trace.objective) || !validTime(trace.startedAt) ||
|
|
436
|
+
!validTime(trace.endedAt) || !Array.isArray(trace.events) ||
|
|
437
|
+
trace.events.length < 1 || trace.events.length > 100 ||
|
|
438
|
+
!['host-event-export', 'verified-execution-journal'].includes(trace.source?.kind) ||
|
|
439
|
+
!/^[a-f0-9]{64}$/.test(trace.source?.sha256 ?? '') ||
|
|
440
|
+
Object.keys(trace.source ?? {}).sort().join(',') !== 'kind,sha256' ||
|
|
441
|
+
!/^[a-f0-9]{64}$/.test(trace.traceHash ?? '')) {
|
|
442
|
+
throw new Error('LEARNING_TRACE_INVALID');
|
|
443
|
+
}
|
|
444
|
+
for (const [index, event] of trace.events.entries()) {
|
|
445
|
+
if (event.sequence !== index + 1 || !/^event_[a-f0-9]{24}$/.test(event.id ?? '') ||
|
|
446
|
+
!validTime(event.time) || !text(event.name) || !text(event.type) ||
|
|
447
|
+
!event.data || typeof event.data !== 'object' || Array.isArray(event.data)) {
|
|
448
|
+
throw new Error('LEARNING_TRACE_EVENT_INVALID');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const { traceHash, ...withoutHash } = trace;
|
|
452
|
+
if (sha256(canonical(withoutHash)) !== traceHash) throw new Error('LEARNING_TRACE_HASH_INVALID');
|
|
453
|
+
return trace;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function normalizeTrace({
|
|
457
|
+
workspace,
|
|
458
|
+
host,
|
|
459
|
+
objective,
|
|
460
|
+
sessionId,
|
|
461
|
+
sourceKind = 'host-event-export',
|
|
462
|
+
sourceContent,
|
|
463
|
+
events,
|
|
464
|
+
maxEvents = 100,
|
|
465
|
+
maxTextBytes = 2000,
|
|
466
|
+
}) {
|
|
467
|
+
if (!text(workspace) || !text(host) || !text(objective) || !text(sourceContent) ||
|
|
468
|
+
!Array.isArray(events) || events.length < 1 || events.length > maxEvents ||
|
|
469
|
+
!Number.isSafeInteger(maxEvents) || maxEvents < 1 || maxEvents > 100 ||
|
|
470
|
+
!Number.isSafeInteger(maxTextBytes) || maxTextBytes < 128 || maxTextBytes > 8192) {
|
|
471
|
+
throw new Error('LEARNING_NORMALIZE_INVALID');
|
|
472
|
+
}
|
|
473
|
+
const sourceHash = sha256(sourceContent);
|
|
474
|
+
const times = events.map((event) => validTime(event.time ?? event.timestamp ?? event.at)).filter(Boolean);
|
|
475
|
+
if (times.length !== events.length) throw new Error('LEARNING_EVENT_TIME_REQUIRED');
|
|
476
|
+
const projectId = `project_${sha256(path.resolve(workspace)).slice(0, 24)}`;
|
|
477
|
+
const resolvedSession = truncateUtf8(text(sessionId) || sourceHash.slice(0, 32), 200);
|
|
478
|
+
const normalizedEvents = events.map((event, index) => {
|
|
479
|
+
const name = redactString(text(event.event ?? event.name ?? event.type) || 'event', 120);
|
|
480
|
+
const data = redactValue(
|
|
481
|
+
Object.fromEntries(Object.entries(event).filter(([key]) => !['time', 'timestamp', 'at', 'event', 'name', 'type'].includes(key))),
|
|
482
|
+
maxTextBytes
|
|
483
|
+
);
|
|
484
|
+
const base = { sequence: index + 1, time: times[index], type: eventType(name), name, data };
|
|
485
|
+
return { id: `event_${sha256(canonical({ sourceHash, ...base })).slice(0, 24)}`, ...base };
|
|
486
|
+
});
|
|
487
|
+
const core = {
|
|
488
|
+
schemaVersion: 1,
|
|
489
|
+
projectId,
|
|
490
|
+
host: truncateUtf8(text(host), 64),
|
|
491
|
+
sessionId: resolvedSession,
|
|
492
|
+
objective: redactString(objective, 4000),
|
|
493
|
+
startedAt: times[0],
|
|
494
|
+
endedAt: times[times.length - 1],
|
|
495
|
+
source: { kind: sourceKind, sha256: sourceHash },
|
|
496
|
+
events: normalizedEvents,
|
|
497
|
+
};
|
|
498
|
+
const traceId = `trace_${sha256(canonical(core)).slice(0, 32)}`;
|
|
499
|
+
const withoutHash = { schemaVersion: 1, traceId, ...core };
|
|
500
|
+
return validateTrace({ ...withoutHash, traceHash: sha256(canonical(withoutHash)) });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export async function normalizeJournal({
|
|
504
|
+
workspace = process.cwd(),
|
|
505
|
+
input,
|
|
506
|
+
host,
|
|
507
|
+
objective,
|
|
508
|
+
sessionId,
|
|
509
|
+
sourceKind = 'verified-execution-journal',
|
|
510
|
+
}) {
|
|
511
|
+
const root = await safeWorkspace(workspace);
|
|
512
|
+
const { content } = await readBoundedFile(root, input);
|
|
513
|
+
const lines = content.split('\n').filter(Boolean);
|
|
514
|
+
if (!lines.length || lines.length > 100) throw new Error('LEARNING_EVENT_LIMIT');
|
|
515
|
+
const events = lines.map((line) => JSON.parse(line));
|
|
516
|
+
const trace = normalizeTrace({
|
|
517
|
+
workspace: root,
|
|
518
|
+
host,
|
|
519
|
+
objective,
|
|
520
|
+
sessionId,
|
|
521
|
+
sourceKind,
|
|
522
|
+
sourceContent: content,
|
|
523
|
+
events,
|
|
524
|
+
});
|
|
525
|
+
const store = await ensureStore(root);
|
|
526
|
+
const tracePath = await withPrivateDirectory(store, 'traces', async (traceDir) => {
|
|
527
|
+
const target = path.join(traceDir, `${trace.traceId}.json`);
|
|
528
|
+
await writePrivateExclusive(target, `${JSON.stringify(trace, null, 2)}\n`);
|
|
529
|
+
return target;
|
|
530
|
+
});
|
|
531
|
+
return { trace, tracePath };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function loadPolicy(workspace) {
|
|
535
|
+
const { content } = await readBoundedFile(workspace, POLICY_PATH, 64 * 1024);
|
|
536
|
+
const policy = JSON.parse(content);
|
|
537
|
+
const thresholds = policy.thresholds;
|
|
538
|
+
if (policy.schemaVersion !== 1 || policy.provider !== 'typesafe' ||
|
|
539
|
+
policy.endpoint !== OFFICIAL_ENDPOINT || policy.model !== 'jev-latest' ||
|
|
540
|
+
typeof policy.enabled !== 'boolean' || !Number.isSafeInteger(policy.timeoutMs) ||
|
|
541
|
+
policy.timeoutMs < 1000 || policy.timeoutMs > 30_000 ||
|
|
542
|
+
!Number.isSafeInteger(policy.maxEvents) || policy.maxEvents < 1 || policy.maxEvents > 100 ||
|
|
543
|
+
!Number.isSafeInteger(policy.maxTextBytes) || policy.maxTextBytes < 128 || policy.maxTextBytes > 8192 ||
|
|
544
|
+
!Number.isSafeInteger(policy.maxProjectionBytes) || policy.maxProjectionBytes < 4096 ||
|
|
545
|
+
policy.maxProjectionBytes > 1024 * 1024 || !thresholds ||
|
|
546
|
+
['taskSuccess', 'reusableLesson', 'evidenceSupport'].some((key) =>
|
|
547
|
+
typeof thresholds[key] !== 'number' || thresholds[key] < 0 || thresholds[key] > 1)) {
|
|
548
|
+
throw new Error('LEARNING_POLICY_INVALID');
|
|
549
|
+
}
|
|
550
|
+
return policy;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function proposedLesson(value) {
|
|
554
|
+
const title = truncateUtf8(text(value?.title), 200);
|
|
555
|
+
const lesson = truncateUtf8(text(value?.lesson), MAX_LESSON_BYTES);
|
|
556
|
+
const kind = truncateUtf8(text(value?.kind) || 'engineering-pattern', 80);
|
|
557
|
+
if (!title || !lesson) throw new Error('LEARNING_LESSON_REQUIRED');
|
|
558
|
+
return { title, lesson, kind };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function buildEvaluationProjection(trace, proposal, policy) {
|
|
562
|
+
validateTrace(trace);
|
|
563
|
+
const boundedProposal = proposedLesson(proposal);
|
|
564
|
+
const safeProposal = redactValue(boundedProposal, policy.maxTextBytes);
|
|
565
|
+
const projection = {
|
|
566
|
+
schemaVersion: 1,
|
|
567
|
+
trace: {
|
|
568
|
+
traceId: trace.traceId,
|
|
569
|
+
projectId: trace.projectId,
|
|
570
|
+
host: redactString(trace.host, 200),
|
|
571
|
+
objective: redactString(trace.objective, policy.maxTextBytes),
|
|
572
|
+
startedAt: trace.startedAt,
|
|
573
|
+
endedAt: trace.endedAt,
|
|
574
|
+
source: { kind: trace.source.kind, sha256: trace.source.sha256 },
|
|
575
|
+
events: trace.events.slice(0, policy.maxEvents).map((event) => ({
|
|
576
|
+
id: event.id,
|
|
577
|
+
sequence: event.sequence,
|
|
578
|
+
time: event.time,
|
|
579
|
+
type: event.type,
|
|
580
|
+
name: redactString(event.name, 200),
|
|
581
|
+
data: redactValue(event.data, policy.maxTextBytes),
|
|
582
|
+
})),
|
|
583
|
+
},
|
|
584
|
+
proposedMemory: safeProposal,
|
|
585
|
+
};
|
|
586
|
+
const serialized = canonical(projection);
|
|
587
|
+
if (Buffer.byteLength(serialized, 'utf8') > policy.maxProjectionBytes) {
|
|
588
|
+
throw new Error('LEARNING_PROJECTION_TOO_LARGE');
|
|
589
|
+
}
|
|
590
|
+
return { projection, projectionHash: sha256(serialized), proposal: safeProposal };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function probability(answer) {
|
|
594
|
+
const direct = Number(answer?.noul ?? answer?.probability ?? answer?.score);
|
|
595
|
+
if (Number.isFinite(direct) && direct >= 0 && direct <= 1) return direct;
|
|
596
|
+
const choice = String(answer?.choice ?? '').toLowerCase();
|
|
597
|
+
const confidence = Number(answer?.confidence);
|
|
598
|
+
if (['yes', 'true'].includes(choice) && Number.isFinite(confidence) && confidence >= 0 && confidence <= 1) {
|
|
599
|
+
return confidence;
|
|
600
|
+
}
|
|
601
|
+
if (['no', 'false'].includes(choice) && Number.isFinite(confidence) && confidence >= 0 && confidence <= 1) {
|
|
602
|
+
return 1 - confidence;
|
|
603
|
+
}
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function answerMap(payload) {
|
|
608
|
+
if (payload?.answers && typeof payload.answers === 'object') return payload.answers;
|
|
609
|
+
if (payload?.questions && typeof payload.questions === 'object') return payload.questions;
|
|
610
|
+
if (payload?.results && typeof payload.results === 'object') return payload.results;
|
|
611
|
+
return {};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export async function evaluateTrace({
|
|
615
|
+
workspace = process.cwd(),
|
|
616
|
+
trace,
|
|
617
|
+
proposal,
|
|
618
|
+
dryRun = false,
|
|
619
|
+
fetchImpl = globalThis.fetch,
|
|
620
|
+
env = process.env,
|
|
621
|
+
keychain,
|
|
622
|
+
}) {
|
|
623
|
+
const root = await safeWorkspace(workspace);
|
|
624
|
+
const policy = await loadPolicy(root);
|
|
625
|
+
const expectedProjectId = `project_${sha256(root).slice(0, 24)}`;
|
|
626
|
+
if (trace?.projectId !== expectedProjectId) throw new Error('LEARNING_TRACE_PROJECT_MISMATCH');
|
|
627
|
+
const { projection, projectionHash, proposal: boundedProposal } =
|
|
628
|
+
buildEvaluationProjection(trace, proposal, policy);
|
|
629
|
+
const rubric = {
|
|
630
|
+
task_success: {
|
|
631
|
+
type: 'noul',
|
|
632
|
+
instructions: 'Did this trace complete the stated engineering objective successfully?',
|
|
633
|
+
},
|
|
634
|
+
reusable_lesson: {
|
|
635
|
+
type: 'noul',
|
|
636
|
+
instructions: 'Is the proposed lesson reusable for a future engineering task?',
|
|
637
|
+
},
|
|
638
|
+
evidence_support: {
|
|
639
|
+
type: 'noul',
|
|
640
|
+
instructions: 'Is the proposed lesson supported by concrete evidence in this trace?',
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
const rubricHash = sha256(canonical(rubric));
|
|
644
|
+
if (dryRun) {
|
|
645
|
+
return {
|
|
646
|
+
status: 'dry_run',
|
|
647
|
+
networkCalled: false,
|
|
648
|
+
projectionHash,
|
|
649
|
+
rubricHash,
|
|
650
|
+
eventCount: projection.trace.events.length,
|
|
651
|
+
projectionBytes: Buffer.byteLength(canonical(projection), 'utf8'),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
// Check Keychain metadata only. The secret stays outside this
|
|
655
|
+
// content-addressing module and cannot enter a stored digest.
|
|
656
|
+
const sharedConfigured =
|
|
657
|
+
policy.enabled || env.GOFER_DISABLE_SHARED_CREDENTIALS === '1'
|
|
658
|
+
? false
|
|
659
|
+
: await sharedCredentialConfigured({ keychain });
|
|
660
|
+
if (!policy.enabled && !sharedConfigured) {
|
|
661
|
+
return { status: 'disabled', networkCalled: false, projectionHash, rubricHash };
|
|
662
|
+
}
|
|
663
|
+
const providerResult = await requestTypeSafeEvaluation({
|
|
664
|
+
workspace: root,
|
|
665
|
+
env,
|
|
666
|
+
fetchImpl,
|
|
667
|
+
policy,
|
|
668
|
+
projection,
|
|
669
|
+
rubric,
|
|
670
|
+
keychain,
|
|
671
|
+
});
|
|
672
|
+
if (providerResult.status !== 'received') {
|
|
673
|
+
return { ...providerResult, projectionHash, rubricHash };
|
|
674
|
+
}
|
|
675
|
+
// Persist only a fixed label. Never carry provider credential data into
|
|
676
|
+
// content-addressed evaluation or candidate records.
|
|
677
|
+
const credentialSource = credentialSourceLabel(providerResult.credentialSource);
|
|
678
|
+
const { payload } = providerResult;
|
|
679
|
+
const answers = answerMap(payload);
|
|
680
|
+
const probabilities = {
|
|
681
|
+
taskSuccess: probability(answers.task_success),
|
|
682
|
+
reusableLesson: probability(answers.reusable_lesson),
|
|
683
|
+
evidenceSupport: probability(answers.evidence_support),
|
|
684
|
+
};
|
|
685
|
+
const passed =
|
|
686
|
+
probabilities.taskSuccess >= policy.thresholds.taskSuccess &&
|
|
687
|
+
probabilities.reusableLesson >= policy.thresholds.reusableLesson &&
|
|
688
|
+
probabilities.evidenceSupport >= policy.thresholds.evidenceSupport;
|
|
689
|
+
const evaluationSeed = {
|
|
690
|
+
schemaVersion: 1,
|
|
691
|
+
traceId: trace.traceId,
|
|
692
|
+
traceHash: trace.traceHash,
|
|
693
|
+
projectId: trace.projectId,
|
|
694
|
+
proposal: boundedProposal,
|
|
695
|
+
projectionHash,
|
|
696
|
+
evaluator: { provider: policy.provider, model: policy.model,
|
|
697
|
+
credentialSource },
|
|
698
|
+
rubricHash,
|
|
699
|
+
thresholds: policy.thresholds,
|
|
700
|
+
probabilities,
|
|
701
|
+
passed,
|
|
702
|
+
};
|
|
703
|
+
const evaluatedAt = new Date().toISOString();
|
|
704
|
+
const evaluationCore = {
|
|
705
|
+
...evaluationSeed,
|
|
706
|
+
evaluatedAt,
|
|
707
|
+
};
|
|
708
|
+
const evaluation = {
|
|
709
|
+
evaluationId: `evaluation_${sha256(canonical(evaluationSeed)).slice(0, 32)}`,
|
|
710
|
+
...evaluationCore,
|
|
711
|
+
};
|
|
712
|
+
const store = await ensureStore(root);
|
|
713
|
+
await appendPrivate(path.join(store, 'evaluations.jsonl'), evaluation);
|
|
714
|
+
if (!passed) return { status: 'not_candidate', networkCalled: true, evaluation };
|
|
715
|
+
const candidateSeed = {
|
|
716
|
+
schemaVersion: 1,
|
|
717
|
+
projectId: trace.projectId,
|
|
718
|
+
traceId: trace.traceId,
|
|
719
|
+
traceHash: trace.traceHash,
|
|
720
|
+
evaluationId: evaluation.evaluationId,
|
|
721
|
+
proposal: boundedProposal,
|
|
722
|
+
evaluator: evaluation.evaluator,
|
|
723
|
+
rubricHash,
|
|
724
|
+
probabilities,
|
|
725
|
+
};
|
|
726
|
+
const candidate = {
|
|
727
|
+
candidateId: `candidate_${sha256(canonical(candidateSeed)).slice(0, 32)}`,
|
|
728
|
+
...candidateSeed,
|
|
729
|
+
createdAt: evaluatedAt,
|
|
730
|
+
state: 'candidate',
|
|
731
|
+
};
|
|
732
|
+
const existing = await currentCandidates(store, root);
|
|
733
|
+
if (!existing.has(candidate.candidateId)) {
|
|
734
|
+
await appendPrivate(path.join(store, 'candidates.jsonl'), { action: 'created', ...candidate });
|
|
735
|
+
}
|
|
736
|
+
return { status: 'candidate', networkCalled: true, evaluation, candidate };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function reviewTransactionPayload(transaction) {
|
|
740
|
+
const { approval, ...payload } = transaction;
|
|
741
|
+
return payload;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function signReviewTransaction(transaction, privateKey, keyId) {
|
|
745
|
+
const payload = reviewTransactionPayload(transaction);
|
|
746
|
+
return {
|
|
747
|
+
...payload,
|
|
748
|
+
approval: {
|
|
749
|
+
algorithm: 'ed25519',
|
|
750
|
+
evaluator: VERIFIER_EVALUATOR,
|
|
751
|
+
keyId,
|
|
752
|
+
value: sign(null, Buffer.from(canonical(payload)), privateKey).toString('base64url'),
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function authenticReviewTransaction(transaction, workspace) {
|
|
758
|
+
const approval = transaction?.approval;
|
|
759
|
+
if (approval?.algorithm !== 'ed25519' || approval.evaluator !== VERIFIER_EVALUATOR ||
|
|
760
|
+
!text(approval.keyId) || !/^[A-Za-z0-9_-]+$/.test(approval.value ?? '')) return false;
|
|
761
|
+
try {
|
|
762
|
+
const publicKey = await resolveTrustedEvaluatorPublicKey({
|
|
763
|
+
host: 'codex',
|
|
764
|
+
provenance: { evaluator: approval.evaluator, keyId: approval.keyId },
|
|
765
|
+
}, { workspaceRoot: workspace });
|
|
766
|
+
return verify(null, Buffer.from(canonical(reviewTransactionPayload(transaction))), publicKey,
|
|
767
|
+
Buffer.from(approval.value, 'base64url'));
|
|
768
|
+
} catch {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function trustedReviewTransactions(store, workspace) {
|
|
774
|
+
const transactions = await readJsonLines(path.join(store, 'review-transactions.jsonl'));
|
|
775
|
+
const trusted = [];
|
|
776
|
+
for (const transaction of transactions) {
|
|
777
|
+
const grantsMemory = (transaction.memories ?? []).some((record) =>
|
|
778
|
+
['approved', 'superseded'].includes(record?.action));
|
|
779
|
+
if (!grantsMemory || await authenticReviewTransaction(transaction, workspace)) trusted.push(transaction);
|
|
780
|
+
}
|
|
781
|
+
return trusted;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function currentCandidates(store, workspace) {
|
|
785
|
+
const records = await readJsonLines(path.join(store, 'candidates.jsonl'));
|
|
786
|
+
const transactions = await trustedReviewTransactions(store, workspace);
|
|
787
|
+
const state = new Map();
|
|
788
|
+
for (const record of records) {
|
|
789
|
+
if (record.action === 'created') state.set(record.candidateId, { ...record });
|
|
790
|
+
else if (state.has(record.candidateId)) {
|
|
791
|
+
state.set(record.candidateId, { ...state.get(record.candidateId), ...record });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
for (const transaction of transactions) {
|
|
795
|
+
const record = transaction.candidate;
|
|
796
|
+
if (record && state.has(record.candidateId)) {
|
|
797
|
+
state.set(record.candidateId, { ...state.get(record.candidateId), ...record });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return state;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function currentMemories(store, workspace) {
|
|
804
|
+
const records = await readJsonLines(path.join(store, 'memories.jsonl'));
|
|
805
|
+
const transactions = await trustedReviewTransactions(store, workspace);
|
|
806
|
+
const state = new Map();
|
|
807
|
+
for (const record of records) {
|
|
808
|
+
if (record.action === 'approved') state.set(record.memoryId, { ...record });
|
|
809
|
+
else if (state.has(record.memoryId)) state.set(record.memoryId, { ...state.get(record.memoryId), ...record });
|
|
810
|
+
}
|
|
811
|
+
for (const transaction of transactions) {
|
|
812
|
+
for (const record of transaction.memories ?? []) {
|
|
813
|
+
if (record.action === 'approved') state.set(record.memoryId, { ...record });
|
|
814
|
+
else if (state.has(record.memoryId)) state.set(record.memoryId, { ...state.get(record.memoryId), ...record });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return state;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function recoverReviewLock(lockPath, expectedIdentity) {
|
|
821
|
+
const recoveryPath = `${lockPath}.recovery`;
|
|
822
|
+
let recovery;
|
|
823
|
+
try {
|
|
824
|
+
recovery = await fs.open(recoveryPath,
|
|
825
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag, 0o600);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (error?.code === 'EEXIST') throw new Error('LEARNING_REVIEW_IN_PROGRESS');
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
const recoveryIdentity = await recovery.stat();
|
|
831
|
+
try {
|
|
832
|
+
const current = await fs.lstat(lockPath).catch((error) => {
|
|
833
|
+
if (error?.code === 'ENOENT') return null;
|
|
834
|
+
throw error;
|
|
835
|
+
});
|
|
836
|
+
if (!current || current.isSymbolicLink() || !sameIdentity(expectedIdentity, current)) {
|
|
837
|
+
throw new Error('LEARNING_REVIEW_IN_PROGRESS');
|
|
838
|
+
}
|
|
839
|
+
await fs.unlink(lockPath);
|
|
840
|
+
} finally {
|
|
841
|
+
await recovery.close();
|
|
842
|
+
const currentRecovery = await fs.lstat(recoveryPath).catch((error) => {
|
|
843
|
+
if (error?.code === 'ENOENT') return null;
|
|
844
|
+
throw error;
|
|
845
|
+
});
|
|
846
|
+
if (currentRecovery && !currentRecovery.isSymbolicLink() &&
|
|
847
|
+
sameIdentity(recoveryIdentity, currentRecovery)) await fs.unlink(recoveryPath);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function withReviewLock(store, action) {
|
|
852
|
+
const lockPath = path.join(store, 'review.lock');
|
|
853
|
+
return withVerifiedParent(lockPath, async () => {
|
|
854
|
+
let lock;
|
|
855
|
+
try {
|
|
856
|
+
lock = await fs.open(lockPath,
|
|
857
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag, 0o600);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
860
|
+
const existing = await fs.open(lockPath, constants.O_RDONLY | noFollowFlag);
|
|
861
|
+
let lockIdentity;
|
|
862
|
+
let record;
|
|
863
|
+
try {
|
|
864
|
+
lockIdentity = await existing.stat();
|
|
865
|
+
record = JSON.parse(await existing.readFile('utf8'));
|
|
866
|
+
} catch {
|
|
867
|
+
const current = await fs.lstat(lockPath);
|
|
868
|
+
const incompleteExpired = lockIdentity &&
|
|
869
|
+
Date.now() - lockIdentity.mtimeMs >= REVIEW_LOCK_TTL_MS &&
|
|
870
|
+
!current.isSymbolicLink() && sameIdentity(lockIdentity, current);
|
|
871
|
+
if (!incompleteExpired) throw new Error('LEARNING_REVIEW_IN_PROGRESS');
|
|
872
|
+
await recoverReviewLock(lockPath, lockIdentity);
|
|
873
|
+
return withReviewLock(store, action);
|
|
874
|
+
} finally {
|
|
875
|
+
await existing.close();
|
|
876
|
+
}
|
|
877
|
+
const expired = validTime(record?.expiresAt) && Date.parse(record.expiresAt) <= Date.now();
|
|
878
|
+
let ownerAlive = true;
|
|
879
|
+
if (Number.isSafeInteger(record?.pid) && record.pid > 0) {
|
|
880
|
+
try { process.kill(record.pid, 0); } catch (ownerError) {
|
|
881
|
+
ownerAlive = ownerError?.code !== 'ESRCH';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const current = await fs.lstat(lockPath);
|
|
885
|
+
if (!expired || ownerAlive || current.isSymbolicLink() || !sameIdentity(lockIdentity, current)) {
|
|
886
|
+
throw new Error('LEARNING_REVIEW_IN_PROGRESS');
|
|
887
|
+
}
|
|
888
|
+
await recoverReviewLock(lockPath, lockIdentity);
|
|
889
|
+
return withReviewLock(store, action);
|
|
890
|
+
}
|
|
891
|
+
const lockIdentity = await lock.stat();
|
|
892
|
+
const createdAt = new Date();
|
|
893
|
+
await lock.writeFile(JSON.stringify({
|
|
894
|
+
schemaVersion: 1,
|
|
895
|
+
pid: process.pid,
|
|
896
|
+
createdAt: createdAt.toISOString(),
|
|
897
|
+
expiresAt: new Date(createdAt.getTime() + REVIEW_LOCK_TTL_MS).toISOString(),
|
|
898
|
+
}));
|
|
899
|
+
await lock.sync();
|
|
900
|
+
try {
|
|
901
|
+
return await action();
|
|
902
|
+
} finally {
|
|
903
|
+
await lock.close();
|
|
904
|
+
const current = await fs.lstat(lockPath).catch((error) => {
|
|
905
|
+
if (error?.code === 'ENOENT') return null;
|
|
906
|
+
throw error;
|
|
907
|
+
});
|
|
908
|
+
if (current && !current.isSymbolicLink() && sameIdentity(lockIdentity, current)) {
|
|
909
|
+
await fs.unlink(lockPath);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export async function listCandidates({ workspace = process.cwd(), state } = {}) {
|
|
916
|
+
const root = await safeWorkspace(workspace);
|
|
917
|
+
const store = await ensureStore(root);
|
|
918
|
+
return [...(await currentCandidates(store, root)).values()]
|
|
919
|
+
.filter((candidate) => !state || candidate.state === state)
|
|
920
|
+
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export async function reviewCandidate({
|
|
924
|
+
workspace = process.cwd(),
|
|
925
|
+
candidateId,
|
|
926
|
+
decision,
|
|
927
|
+
actor,
|
|
928
|
+
reason,
|
|
929
|
+
replacementMemoryId,
|
|
930
|
+
now = () => new Date(),
|
|
931
|
+
}) {
|
|
932
|
+
const root = await safeWorkspace(workspace);
|
|
933
|
+
if (!/^candidate_[a-f0-9]{32}$/.test(candidateId ?? '') ||
|
|
934
|
+
!['approve', 'reject', 'supersede'].includes(decision) || !text(actor) || !text(reason)) {
|
|
935
|
+
throw new Error('LEARNING_REVIEW_INVALID');
|
|
936
|
+
}
|
|
937
|
+
let approvalAuthority;
|
|
938
|
+
if (['approve', 'supersede'].includes(decision)) {
|
|
939
|
+
try {
|
|
940
|
+
approvalAuthority = await loadActiveBenchmarkVerifierKey({
|
|
941
|
+
workspaceRoot: root,
|
|
942
|
+
getPassphrase: () => promptHidden(
|
|
943
|
+
`${decision === 'approve' ? 'Approve' : 'Supersede'} ${candidateId} — reviewer passphrase: `
|
|
944
|
+
),
|
|
945
|
+
});
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error?.message === 'TTY_REQUIRED' || error?.message === 'PASSPHRASE_REQUIRED') {
|
|
948
|
+
throw new Error('LEARNING_HUMAN_APPROVAL_REQUIRED');
|
|
949
|
+
}
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const store = await ensureStore(root);
|
|
954
|
+
return withReviewLock(store, async () => {
|
|
955
|
+
const candidates = await currentCandidates(store, root);
|
|
956
|
+
const candidate = candidates.get(candidateId);
|
|
957
|
+
if (!candidate) throw new Error('LEARNING_CANDIDATE_NOT_FOUND');
|
|
958
|
+
const reviewedAt = now().toISOString();
|
|
959
|
+
if (decision === 'approve') {
|
|
960
|
+
if (candidate.state !== 'candidate') throw new Error('LEARNING_CANDIDATE_ALREADY_REVIEWED');
|
|
961
|
+
const memoryCore = {
|
|
962
|
+
schemaVersion: 1,
|
|
963
|
+
candidateId,
|
|
964
|
+
projectId: candidate.projectId,
|
|
965
|
+
title: candidate.proposal.title,
|
|
966
|
+
content: candidate.proposal.lesson,
|
|
967
|
+
kind: candidate.proposal.kind,
|
|
968
|
+
traceId: candidate.traceId,
|
|
969
|
+
traceHash: candidate.traceHash,
|
|
970
|
+
evaluationId: candidate.evaluationId,
|
|
971
|
+
evaluator: candidate.evaluator,
|
|
972
|
+
rubricHash: candidate.rubricHash,
|
|
973
|
+
probabilities: candidate.probabilities,
|
|
974
|
+
approvedAt: reviewedAt,
|
|
975
|
+
approvedBy: redactString(actor, 200),
|
|
976
|
+
approvalReason: redactString(reason, 1000),
|
|
977
|
+
};
|
|
978
|
+
const memory = {
|
|
979
|
+
action: 'approved',
|
|
980
|
+
memoryId: `memory_${sha256(canonical(memoryCore)).slice(0, 32)}`,
|
|
981
|
+
...memoryCore,
|
|
982
|
+
state: 'approved',
|
|
983
|
+
};
|
|
984
|
+
const candidateUpdate = {
|
|
985
|
+
action: 'reviewed',
|
|
986
|
+
candidateId,
|
|
987
|
+
state: 'approved',
|
|
988
|
+
memoryId: memory.memoryId,
|
|
989
|
+
reviewedAt,
|
|
990
|
+
reviewedBy: redactString(actor, 200),
|
|
991
|
+
reviewReason: redactString(reason, 1000),
|
|
992
|
+
};
|
|
993
|
+
const transaction = {
|
|
994
|
+
action: 'review_transaction',
|
|
995
|
+
transactionId: `review_${sha256(canonical({ candidateUpdate, memory })).slice(0, 32)}`,
|
|
996
|
+
candidate: candidateUpdate,
|
|
997
|
+
memories: [memory],
|
|
998
|
+
};
|
|
999
|
+
await appendPrivate(path.join(store, 'review-transactions.jsonl'),
|
|
1000
|
+
signReviewTransaction(transaction, approvalAuthority.privateKey, approvalAuthority.keyId));
|
|
1001
|
+
return { candidateId, state: 'approved', memory };
|
|
1002
|
+
}
|
|
1003
|
+
if (decision === 'reject') {
|
|
1004
|
+
if (candidate.state !== 'candidate') throw new Error('LEARNING_CANDIDATE_ALREADY_REVIEWED');
|
|
1005
|
+
const review = {
|
|
1006
|
+
action: 'reviewed',
|
|
1007
|
+
candidateId,
|
|
1008
|
+
state: 'rejected',
|
|
1009
|
+
reviewedAt,
|
|
1010
|
+
reviewedBy: redactString(actor, 200),
|
|
1011
|
+
reviewReason: redactString(reason, 1000),
|
|
1012
|
+
};
|
|
1013
|
+
await appendPrivate(path.join(store, 'review-transactions.jsonl'), {
|
|
1014
|
+
action: 'review_transaction',
|
|
1015
|
+
transactionId: `review_${sha256(canonical(review)).slice(0, 32)}`,
|
|
1016
|
+
candidate: review,
|
|
1017
|
+
memories: [],
|
|
1018
|
+
});
|
|
1019
|
+
return review;
|
|
1020
|
+
}
|
|
1021
|
+
if (candidate.state !== 'approved' || candidate.memoryId === replacementMemoryId ||
|
|
1022
|
+
!/^memory_[a-f0-9]{32}$/.test(replacementMemoryId ?? '')) {
|
|
1023
|
+
throw new Error('LEARNING_SUPERSEDE_INVALID');
|
|
1024
|
+
}
|
|
1025
|
+
const memories = await currentMemories(store, root);
|
|
1026
|
+
const current = memories.get(candidate.memoryId);
|
|
1027
|
+
const replacement = memories.get(replacementMemoryId);
|
|
1028
|
+
if (!current || !replacement || current.projectId !== replacement.projectId ||
|
|
1029
|
+
current.projectId !== candidate.projectId || replacement.state !== 'approved') {
|
|
1030
|
+
throw new Error('LEARNING_REPLACEMENT_INVALID');
|
|
1031
|
+
}
|
|
1032
|
+
const update = {
|
|
1033
|
+
action: 'superseded',
|
|
1034
|
+
memoryId: current.memoryId,
|
|
1035
|
+
state: 'superseded',
|
|
1036
|
+
supersededBy: replacementMemoryId,
|
|
1037
|
+
reviewedAt,
|
|
1038
|
+
reviewedBy: redactString(actor, 200),
|
|
1039
|
+
reviewReason: redactString(reason, 1000),
|
|
1040
|
+
};
|
|
1041
|
+
const candidateUpdate = {
|
|
1042
|
+
action: 'reviewed',
|
|
1043
|
+
candidateId,
|
|
1044
|
+
state: 'superseded',
|
|
1045
|
+
replacementMemoryId,
|
|
1046
|
+
reviewedAt,
|
|
1047
|
+
reviewedBy: update.reviewedBy,
|
|
1048
|
+
reviewReason: update.reviewReason,
|
|
1049
|
+
};
|
|
1050
|
+
const transaction = {
|
|
1051
|
+
action: 'review_transaction',
|
|
1052
|
+
transactionId: `review_${sha256(canonical({ candidateUpdate, update })).slice(0, 32)}`,
|
|
1053
|
+
candidate: candidateUpdate,
|
|
1054
|
+
memories: [update],
|
|
1055
|
+
};
|
|
1056
|
+
await appendPrivate(path.join(store, 'review-transactions.jsonl'),
|
|
1057
|
+
signReviewTransaction(transaction, approvalAuthority.privateKey, approvalAuthority.keyId));
|
|
1058
|
+
return update;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function queryTerms(query) {
|
|
1063
|
+
return [...new Set(String(query).toLowerCase().match(/[a-z0-9_-]{3,}/g) ?? [])].slice(0, 100);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export async function searchApprovedMemory({ workspace = process.cwd(), query, limit = 10 } = {}) {
|
|
1067
|
+
if (!text(query) || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
1068
|
+
throw new Error('LEARNING_SEARCH_INVALID');
|
|
1069
|
+
}
|
|
1070
|
+
const root = await safeWorkspace(workspace);
|
|
1071
|
+
const store = await ensureStore(root);
|
|
1072
|
+
const projectId = `project_${sha256(root).slice(0, 24)}`;
|
|
1073
|
+
const terms = queryTerms(query);
|
|
1074
|
+
return [...(await currentMemories(store, root)).values()]
|
|
1075
|
+
.filter((memory) => memory.projectId === projectId && memory.state === 'approved')
|
|
1076
|
+
.map((memory) => {
|
|
1077
|
+
const haystack = `${memory.title} ${memory.content} ${memory.kind}`.toLowerCase();
|
|
1078
|
+
const matches = terms.filter((term) => haystack.includes(term)).length;
|
|
1079
|
+
return { ...memory, relevance: terms.length ? matches / terms.length : 0 };
|
|
1080
|
+
})
|
|
1081
|
+
.filter((memory) => memory.relevance > 0)
|
|
1082
|
+
.sort((a, b) => b.relevance - a.relevance || String(b.approvedAt).localeCompare(String(a.approvedAt)))
|
|
1083
|
+
.slice(0, limit);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export async function recordMemoryOutcome({
|
|
1087
|
+
workspace = process.cwd(),
|
|
1088
|
+
memoryId,
|
|
1089
|
+
runId,
|
|
1090
|
+
outcome,
|
|
1091
|
+
note = '',
|
|
1092
|
+
now = () => new Date(),
|
|
1093
|
+
}) {
|
|
1094
|
+
if (!/^memory_[a-f0-9]{32}$/.test(memoryId ?? '') || !text(runId) ||
|
|
1095
|
+
!['helped', 'neutral', 'harmed'].includes(outcome)) {
|
|
1096
|
+
throw new Error('LEARNING_FEEDBACK_INVALID');
|
|
1097
|
+
}
|
|
1098
|
+
const root = await safeWorkspace(workspace);
|
|
1099
|
+
const store = await ensureStore(root);
|
|
1100
|
+
return withReviewLock(store, async () => {
|
|
1101
|
+
const memory = (await currentMemories(store, root)).get(memoryId);
|
|
1102
|
+
if (!memory || memory.state !== 'approved') throw new Error('LEARNING_MEMORY_NOT_APPROVED');
|
|
1103
|
+
const feedbackCore = {
|
|
1104
|
+
schemaVersion: 1,
|
|
1105
|
+
memoryId,
|
|
1106
|
+
projectId: memory.projectId,
|
|
1107
|
+
runId: truncateUtf8(runId, 200),
|
|
1108
|
+
outcome,
|
|
1109
|
+
note: redactString(note, 1000),
|
|
1110
|
+
};
|
|
1111
|
+
const feedback = {
|
|
1112
|
+
...feedbackCore,
|
|
1113
|
+
feedbackId: `feedback_${sha256(canonical(feedbackCore)).slice(0, 32)}`,
|
|
1114
|
+
recordedAt: now().toISOString(),
|
|
1115
|
+
};
|
|
1116
|
+
const existing = await readJsonLines(path.join(store, 'feedback.jsonl'));
|
|
1117
|
+
if (existing.some((item) => item.feedbackId === feedback.feedbackId)) return feedback;
|
|
1118
|
+
await appendPrivate(path.join(store, 'feedback.jsonl'), feedback);
|
|
1119
|
+
return feedback;
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
export async function learningReport({ workspace = process.cwd() } = {}) {
|
|
1124
|
+
const root = await safeWorkspace(workspace);
|
|
1125
|
+
const store = await ensureStore(root);
|
|
1126
|
+
const projectId = `project_${sha256(root).slice(0, 24)}`;
|
|
1127
|
+
const candidates = [...(await currentCandidates(store, root)).values()].filter((item) => item.projectId === projectId);
|
|
1128
|
+
const memories = [...(await currentMemories(store, root)).values()].filter((item) => item.projectId === projectId);
|
|
1129
|
+
const feedback = (await readJsonLines(path.join(store, 'feedback.jsonl'))).filter((item) => item.projectId === projectId);
|
|
1130
|
+
const outcomes = Object.fromEntries(['helped', 'neutral', 'harmed'].map((name) => [
|
|
1131
|
+
name,
|
|
1132
|
+
feedback.filter((item) => item.outcome === name).length,
|
|
1133
|
+
]));
|
|
1134
|
+
return {
|
|
1135
|
+
projectId,
|
|
1136
|
+
candidates: {
|
|
1137
|
+
total: candidates.length,
|
|
1138
|
+
pending: candidates.filter((item) => item.state === 'candidate').length,
|
|
1139
|
+
approved: candidates.filter((item) => item.state === 'approved').length,
|
|
1140
|
+
rejected: candidates.filter((item) => item.state === 'rejected').length,
|
|
1141
|
+
superseded: candidates.filter((item) => item.state === 'superseded').length,
|
|
1142
|
+
},
|
|
1143
|
+
memories: {
|
|
1144
|
+
approved: memories.filter((item) => item.state === 'approved').length,
|
|
1145
|
+
superseded: memories.filter((item) => item.state === 'superseded').length,
|
|
1146
|
+
},
|
|
1147
|
+
feedback: { total: feedback.length, ...outcomes },
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function parseArgs(argv) {
|
|
1152
|
+
const [action, ...rest] = argv;
|
|
1153
|
+
const flags = new Map();
|
|
1154
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1155
|
+
const name = rest[index];
|
|
1156
|
+
if (!name.startsWith('--')) throw new Error('LEARNING_CLI_INVALID');
|
|
1157
|
+
if (name === '--dry-run') flags.set(name, true);
|
|
1158
|
+
else {
|
|
1159
|
+
const value = rest[++index];
|
|
1160
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${name}`);
|
|
1161
|
+
flags.set(name, value);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return { action, flags };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async function main() {
|
|
1168
|
+
const { action, flags } = parseArgs(process.argv.slice(2));
|
|
1169
|
+
const workspace = flags.get('--workspace') ?? process.cwd();
|
|
1170
|
+
let result;
|
|
1171
|
+
if (action === 'normalize') {
|
|
1172
|
+
result = await normalizeJournal({
|
|
1173
|
+
workspace,
|
|
1174
|
+
input: flags.get('--input'),
|
|
1175
|
+
host: flags.get('--host'),
|
|
1176
|
+
objective: flags.get('--objective'),
|
|
1177
|
+
sessionId: flags.get('--session'),
|
|
1178
|
+
sourceKind: flags.get('--source-kind') ?? 'verified-execution-journal',
|
|
1179
|
+
});
|
|
1180
|
+
result = { traceId: result.trace.traceId, tracePath: result.tracePath };
|
|
1181
|
+
} else if (action === 'evaluate') {
|
|
1182
|
+
const traceFile = await readBoundedFile(workspace, flags.get('--trace'));
|
|
1183
|
+
const lessonFile = await readBoundedFile(workspace, flags.get('--lesson-file'), MAX_LESSON_BYTES);
|
|
1184
|
+
result = await evaluateTrace({
|
|
1185
|
+
workspace,
|
|
1186
|
+
trace: JSON.parse(traceFile.content),
|
|
1187
|
+
proposal: {
|
|
1188
|
+
title: flags.get('--title'),
|
|
1189
|
+
lesson: lessonFile.content,
|
|
1190
|
+
kind: flags.get('--kind'),
|
|
1191
|
+
},
|
|
1192
|
+
dryRun: flags.get('--dry-run') === true,
|
|
1193
|
+
});
|
|
1194
|
+
} else if (action === 'candidates') {
|
|
1195
|
+
result = await listCandidates({ workspace, state: flags.get('--state') });
|
|
1196
|
+
} else if (action === 'review') {
|
|
1197
|
+
const decision = flags.get('--decision');
|
|
1198
|
+
result = await reviewCandidate({
|
|
1199
|
+
workspace,
|
|
1200
|
+
candidateId: flags.get('--candidate'),
|
|
1201
|
+
decision,
|
|
1202
|
+
actor: flags.get('--actor'),
|
|
1203
|
+
reason: flags.get('--reason'),
|
|
1204
|
+
replacementMemoryId: flags.get('--replacement'),
|
|
1205
|
+
});
|
|
1206
|
+
} else if (action === 'search') {
|
|
1207
|
+
result = await searchApprovedMemory({
|
|
1208
|
+
workspace,
|
|
1209
|
+
query: flags.get('--query'),
|
|
1210
|
+
limit: flags.has('--limit') ? Number(flags.get('--limit')) : 10,
|
|
1211
|
+
});
|
|
1212
|
+
} else if (action === 'feedback') {
|
|
1213
|
+
result = await recordMemoryOutcome({
|
|
1214
|
+
workspace,
|
|
1215
|
+
memoryId: flags.get('--memory'),
|
|
1216
|
+
runId: flags.get('--run'),
|
|
1217
|
+
outcome: flags.get('--outcome'),
|
|
1218
|
+
note: flags.get('--note'),
|
|
1219
|
+
});
|
|
1220
|
+
} else if (action === 'report') {
|
|
1221
|
+
result = await learningReport({ workspace });
|
|
1222
|
+
} else {
|
|
1223
|
+
throw new Error('Usage: normalize | evaluate | candidates | review | search | feedback | report');
|
|
1224
|
+
}
|
|
1225
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const direct = process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
1229
|
+
if (direct) main().catch((error) => {
|
|
1230
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1231
|
+
process.exitCode = 1;
|
|
1232
|
+
});
|