dsh-compound-engineering 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -2
- package/README.zh.md +161 -0
- package/cordis.patch.yml +6 -0
- package/lib/frontmatter.js +167 -0
- package/lib/index.js +46 -0
- package/lib/provider.js +201 -0
- package/package.json +62 -4
- package/skills/CODING_STANDARDS.md +68 -0
- package/skills/ce-babysit-pr/SKILL.md +59 -0
- package/skills/ce-babysit-pr/references/branch-currency.md +22 -0
- package/skills/ce-babysit-pr/references/envelope.md +44 -0
- package/skills/ce-babysit-pr/references/pipeline.md +15 -0
- package/skills/ce-babysit-pr/references/report.md +20 -0
- package/skills/ce-babysit-pr/references/settle.md +37 -0
- package/skills/ce-babysit-pr/references/setup.md +33 -0
- package/skills/ce-babysit-pr/references/stack-commands.md +39 -0
- package/skills/ce-babysit-pr/references/stack.md +43 -0
- package/skills/ce-babysit-pr/references/tick.md +102 -0
- package/skills/ce-babysit-pr/references/watch-loop.md +222 -0
- package/skills/ce-babysit-pr/scripts/pr-snapshot +3656 -0
- package/skills/ce-bakeoff/SKILL.md +48 -0
- package/skills/ce-bakeoff/references/candidates.md +37 -0
- package/skills/ce-bakeoff/references/judging.md +19 -0
- package/skills/ce-bakeoff/references/output.md +11 -0
- package/skills/ce-bakeoff/references/verification.md +15 -0
- package/skills/ce-brainstorm/SKILL.md +69 -0
- package/skills/ce-brainstorm/references/agents/slack-researcher.md +127 -0
- package/skills/ce-brainstorm/references/approaches.md +57 -0
- package/skills/ce-brainstorm/references/bakeoff.md +9 -0
- package/skills/ce-brainstorm/references/blindspot-pass.md +70 -0
- package/skills/ce-brainstorm/references/brainstorm-sections.md +458 -0
- package/skills/ce-brainstorm/references/dialogue.md +89 -0
- package/skills/ce-brainstorm/references/handoff.md +187 -0
- package/skills/ce-brainstorm/references/html-rendering.md +638 -0
- package/skills/ce-brainstorm/references/interaction-rules.md +33 -0
- package/skills/ce-brainstorm/references/markdown-rendering.md +242 -0
- package/skills/ce-brainstorm/references/model-tiers.md +13 -0
- package/skills/ce-brainstorm/references/output-mode.md +26 -0
- package/skills/ce-brainstorm/references/phase-0.md +104 -0
- package/skills/ce-brainstorm/references/plan-write.md +32 -0
- package/skills/ce-brainstorm/references/product-pressure-test.md +42 -0
- package/skills/ce-brainstorm/references/reasoning-elevation.md +124 -0
- package/skills/ce-brainstorm/references/settled-decisions.md +45 -0
- package/skills/ce-brainstorm/references/synthesis-summary.md +149 -0
- package/skills/ce-brainstorm/references/universal-brainstorming.md +72 -0
- package/skills/ce-brainstorm/references/verdict-routing.md +26 -0
- package/skills/ce-brainstorm/references/visual-probes.md +164 -0
- package/skills/ce-brainstorm/scripts/elevation-dispatch.sh +288 -0
- package/skills/ce-brainstorm/scripts/light-webserver.js +1275 -0
- package/skills/ce-brainstorm/scripts/packs-resolve.py +737 -0
- package/skills/ce-brainstorm/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-code-review/SKILL.md +40 -0
- package/skills/ce-code-review/references/action-class-rubric.md +67 -0
- package/skills/ce-code-review/references/cross-model-recovery.md +38 -0
- package/skills/ce-code-review/references/cross-model-review.md +191 -0
- package/skills/ce-code-review/references/depth-paths.md +32 -0
- package/skills/ce-code-review/references/diff-scope.md +59 -0
- package/skills/ce-code-review/references/dispatch-reviewers.md +135 -0
- package/skills/ce-code-review/references/findings-schema.json +125 -0
- package/skills/ce-code-review/references/finish-input.md +118 -0
- package/skills/ce-code-review/references/finish-review.md +202 -0
- package/skills/ce-code-review/references/intent-and-plan.md +73 -0
- package/skills/ce-code-review/references/modes-and-output.md +156 -0
- package/skills/ce-code-review/references/persona-catalog.md +66 -0
- package/skills/ce-code-review/references/personas/adversarial-reviewer.md +111 -0
- package/skills/ce-code-review/references/personas/agent-native-reviewer.md +173 -0
- package/skills/ce-code-review/references/personas/api-contract-reviewer.md +44 -0
- package/skills/ce-code-review/references/personas/correctness-reviewer.md +48 -0
- package/skills/ce-code-review/references/personas/data-migration-reviewer.md +111 -0
- package/skills/ce-code-review/references/personas/deployment-verification-agent.md +157 -0
- package/skills/ce-code-review/references/personas/julik-frontend-races-reviewer.md +45 -0
- package/skills/ce-code-review/references/personas/learnings-researcher.md +260 -0
- package/skills/ce-code-review/references/personas/maintainability-reviewer.md +85 -0
- package/skills/ce-code-review/references/personas/performance-reviewer.md +45 -0
- package/skills/ce-code-review/references/personas/previous-comments-reviewer.md +59 -0
- package/skills/ce-code-review/references/personas/project-standards-reviewer.md +77 -0
- package/skills/ce-code-review/references/personas/reliability-reviewer.md +49 -0
- package/skills/ce-code-review/references/personas/security-reviewer.md +50 -0
- package/skills/ce-code-review/references/personas/swift-ios-reviewer.md +99 -0
- package/skills/ce-code-review/references/personas/testing-reviewer.md +51 -0
- package/skills/ce-code-review/references/review-output-template.md +172 -0
- package/skills/ce-code-review/references/scope.md +176 -0
- package/skills/ce-code-review/references/select-and-route.md +93 -0
- package/skills/ce-code-review/references/subagent-template.md +206 -0
- package/skills/ce-code-review/references/validator-batch-template.md +67 -0
- package/skills/ce-code-review/scripts/cross-model-adversarial-review.sh +1275 -0
- package/skills/ce-code-review/scripts/findings-mechanics.py +274 -0
- package/skills/ce-code-review/scripts/packs-resolve.py +737 -0
- package/skills/ce-code-review/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-code-review/scripts/review-scope.py +419 -0
- package/skills/ce-code-review/scripts/run-log.py +280 -0
- package/skills/ce-commit/SKILL.md +60 -0
- package/skills/ce-commit-push-pr/SKILL.md +69 -0
- package/skills/ce-commit-push-pr/references/apply-and-handoff.md +79 -0
- package/skills/ce-commit-push-pr/references/branch-creation.md +47 -0
- package/skills/ce-commit-push-pr/references/commit-and-push.md +26 -0
- package/skills/ce-commit-push-pr/references/compose.md +29 -0
- package/skills/ce-commit-push-pr/references/context.md +42 -0
- package/skills/ce-commit-push-pr/references/gh-stack-cli.md +83 -0
- package/skills/ce-commit-push-pr/references/pr-description-writing.md +208 -0
- package/skills/ce-commit-push-pr/references/stack-submit.md +81 -0
- package/skills/ce-compound/SKILL.md +79 -0
- package/skills/ce-compound/assets/resolution-template.md +96 -0
- package/skills/ce-compound/references/agents/best-practices-researcher.md +115 -0
- package/skills/ce-compound/references/agents/data-integrity-guardian.md +68 -0
- package/skills/ce-compound/references/agents/framework-docs-researcher.md +93 -0
- package/skills/ce-compound/references/agents/pattern-recognition-specialist.md +55 -0
- package/skills/ce-compound/references/agents/performance-oracle.md +108 -0
- package/skills/ce-compound/references/agents/security-sentinel.md +91 -0
- package/skills/ce-compound/references/agents/session-historian.md +82 -0
- package/skills/ce-compound/references/assembly.md +127 -0
- package/skills/ce-compound/references/concepts-vocabulary.md +98 -0
- package/skills/ce-compound/references/enhancement.md +42 -0
- package/skills/ce-compound/references/grounding-validation.md +89 -0
- package/skills/ce-compound/references/lightweight.md +56 -0
- package/skills/ce-compound/references/modes.md +13 -0
- package/skills/ce-compound/references/refresh-and-discoverability.md +96 -0
- package/skills/ce-compound/references/report.md +115 -0
- package/skills/ce-compound/references/research.md +144 -0
- package/skills/ce-compound/references/schema.yaml +236 -0
- package/skills/ce-compound/references/session-history.md +73 -0
- package/skills/ce-compound/references/yaml-schema.md +130 -0
- package/skills/ce-compound/scripts/packs-resolve.py +737 -0
- package/skills/ce-compound/scripts/session-history/__pycache__/dsh_session.cpython-314.pyc +0 -0
- package/skills/ce-compound/scripts/session-history/discover-sessions.sh +290 -0
- package/skills/ce-compound/scripts/session-history/discover_dsh.py +123 -0
- package/skills/ce-compound/scripts/session-history/dsh_session.py +225 -0
- package/skills/ce-compound/scripts/session-history/extract-errors.py +178 -0
- package/skills/ce-compound/scripts/session-history/extract-metadata.py +301 -0
- package/skills/ce-compound/scripts/session-history/extract-skeleton.py +346 -0
- package/skills/ce-compound/scripts/validate-doc-claims.py +497 -0
- package/skills/ce-compound/scripts/validate-frontmatter.py +137 -0
- package/skills/ce-compound-refresh/SKILL.md +88 -0
- package/skills/ce-compound-refresh/assets/resolution-template.md +96 -0
- package/skills/ce-compound-refresh/references/classify.md +42 -0
- package/skills/ce-compound-refresh/references/commit.md +7 -0
- package/skills/ce-compound-refresh/references/concepts-vocabulary.md +116 -0
- package/skills/ce-compound-refresh/references/discoverability.md +15 -0
- package/skills/ce-compound-refresh/references/investigate.md +23 -0
- package/skills/ce-compound-refresh/references/modes.md +27 -0
- package/skills/ce-compound-refresh/references/per-action-flows.md +142 -0
- package/skills/ce-compound-refresh/references/report.md +23 -0
- package/skills/ce-compound-refresh/references/schema.yaml +236 -0
- package/skills/ce-compound-refresh/references/scope.md +18 -0
- package/skills/ce-compound-refresh/references/worth-audit.md +35 -0
- package/skills/ce-compound-refresh/references/yaml-schema.md +130 -0
- package/skills/ce-compound-refresh/scripts/validate-doc-claims.py +497 -0
- package/skills/ce-compound-refresh/scripts/validate-frontmatter.py +137 -0
- package/skills/ce-debug/SKILL.md +122 -0
- package/skills/ce-debug/references/anti-patterns.md +91 -0
- package/skills/ce-debug/references/defense-in-depth.md +39 -0
- package/skills/ce-debug/references/fix.md +21 -0
- package/skills/ce-debug/references/investigate.md +114 -0
- package/skills/ce-debug/references/investigation-techniques.md +384 -0
- package/skills/ce-debug/references/pipeline-mode.md +78 -0
- package/skills/ce-debug/references/post-fix-handoff.md +60 -0
- package/skills/ce-debug/references/return-to-caller.md +57 -0
- package/skills/ce-doc-review/SKILL.md +75 -0
- package/skills/ce-doc-review/references/bulk-preview.md +146 -0
- package/skills/ce-doc-review/references/cross-model-review.md +207 -0
- package/skills/ce-doc-review/references/decision-primer.md +47 -0
- package/skills/ce-doc-review/references/dispatch.md +56 -0
- package/skills/ce-doc-review/references/document-intake.md +44 -0
- package/skills/ce-doc-review/references/findings-schema.json +85 -0
- package/skills/ce-doc-review/references/modes.md +20 -0
- package/skills/ce-doc-review/references/open-questions-defer.md +158 -0
- package/skills/ce-doc-review/references/persona-selection.md +41 -0
- package/skills/ce-doc-review/references/personas/adversarial-document-reviewer.md +109 -0
- package/skills/ce-doc-review/references/personas/coherence-reviewer.md +69 -0
- package/skills/ce-doc-review/references/personas/design-lens-reviewer.md +48 -0
- package/skills/ce-doc-review/references/personas/feasibility-reviewer.md +50 -0
- package/skills/ce-doc-review/references/personas/product-lens-reviewer.md +85 -0
- package/skills/ce-doc-review/references/personas/scope-guardian-reviewer.md +83 -0
- package/skills/ce-doc-review/references/personas/security-lens-reviewer.md +41 -0
- package/skills/ce-doc-review/references/personas/whole-doc-reviewer.md +25 -0
- package/skills/ce-doc-review/references/rendering-floor.md +175 -0
- package/skills/ce-doc-review/references/review-output-template.md +123 -0
- package/skills/ce-doc-review/references/subagent-template.md +188 -0
- package/skills/ce-doc-review/references/synthesis-and-presentation.md +350 -0
- package/skills/ce-doc-review/references/walkthrough.md +347 -0
- package/skills/ce-doc-review/scripts/cross-model-doc-review.sh +1270 -0
- package/skills/ce-doc-review/scripts/packs-resolve.py +737 -0
- package/skills/ce-doc-review/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-dogfood/SKILL.md +66 -0
- package/skills/ce-dogfood/agents/openai.yaml +2 -0
- package/skills/ce-dogfood/references/dogfood-report-template.md +97 -0
- package/skills/ce-dogfood/references/phases.md +167 -0
- package/skills/ce-dogfood/references/test-matrix-taxonomy.md +62 -0
- package/skills/ce-dogfood/scripts/packs-resolve.py +737 -0
- package/skills/ce-explain/SKILL.md +81 -0
- package/skills/ce-explain/references/agents/behavior-trace-scout.md +19 -0
- package/skills/ce-explain/references/agents/work-recap-scout.md +23 -0
- package/skills/ce-explain/references/check-in.md +18 -0
- package/skills/ce-explain/references/destinations.md +35 -0
- package/skills/ce-explain/references/explainer-html.md +29 -0
- package/skills/ce-explain/references/explainer-markdown.md +24 -0
- package/skills/ce-explain/references/intake.md +50 -0
- package/skills/ce-explain/references/orchestration.md +46 -0
- package/skills/ce-handoff/SKILL.md +54 -0
- package/skills/ce-handoff/references/create.md +88 -0
- package/skills/ce-handoff/references/resume.md +46 -0
- package/skills/ce-ideate/SKILL.md +75 -0
- package/skills/ce-ideate/references/agents/issue-intelligence-analyst.md +166 -0
- package/skills/ce-ideate/references/agents/learnings-researcher.md +247 -0
- package/skills/ce-ideate/references/agents/slack-researcher.md +127 -0
- package/skills/ce-ideate/references/agents/web-researcher.md +121 -0
- package/skills/ce-ideate/references/decomposition.md +39 -0
- package/skills/ce-ideate/references/divergent-ideation.md +112 -0
- package/skills/ce-ideate/references/grounding.md +138 -0
- package/skills/ce-ideate/references/html-rendering.md +638 -0
- package/skills/ce-ideate/references/ideation-sections.md +191 -0
- package/skills/ce-ideate/references/issue-intelligence.md +44 -0
- package/skills/ce-ideate/references/markdown-rendering.md +242 -0
- package/skills/ce-ideate/references/output-mode.md +34 -0
- package/skills/ce-ideate/references/post-ideation-workflow.md +169 -0
- package/skills/ce-ideate/references/scope-gates.md +148 -0
- package/skills/ce-ideate/references/universal-ideation.md +119 -0
- package/skills/ce-ideate/references/user-research-artifacts.md +28 -0
- package/skills/ce-ideate/references/web-research-cache.md +56 -0
- package/skills/ce-noslop/SKILL.md +45 -0
- package/skills/ce-noslop/references/patterns.md +61 -0
- package/skills/ce-noslop/references/terminology.md +3 -0
- package/skills/ce-optimize/SKILL.md +64 -0
- package/skills/ce-optimize/references/agents/learnings-researcher.md +247 -0
- package/skills/ce-optimize/references/agents/repo-research-analyst.md +258 -0
- package/skills/ce-optimize/references/example-expensive-benchmark-spec.yaml +88 -0
- package/skills/ce-optimize/references/example-hard-spec.yaml +64 -0
- package/skills/ce-optimize/references/example-judge-spec.yaml +83 -0
- package/skills/ce-optimize/references/example-text-target-spec.yaml +80 -0
- package/skills/ce-optimize/references/experiment-log-schema.yaml +563 -0
- package/skills/ce-optimize/references/experiment-prompt-template.md +143 -0
- package/skills/ce-optimize/references/judge-prompt-template.md +117 -0
- package/skills/ce-optimize/references/loop.md +286 -0
- package/skills/ce-optimize/references/measurement.md +168 -0
- package/skills/ce-optimize/references/optimize-spec-schema.yaml +570 -0
- package/skills/ce-optimize/references/persistence.md +93 -0
- package/skills/ce-optimize/references/spec.md +107 -0
- package/skills/ce-optimize/references/text-targets.md +51 -0
- package/skills/ce-optimize/references/usage-guide.md +147 -0
- package/skills/ce-optimize/references/wrap-up.md +54 -0
- package/skills/ce-optimize/scripts/decide.mjs +676 -0
- package/skills/ce-optimize/scripts/experiment-worktree.sh +330 -0
- package/skills/ce-optimize/scripts/measure.sh +140 -0
- package/skills/ce-optimize/scripts/parallel-probe.sh +134 -0
- package/skills/ce-plan/SKILL.md +60 -0
- package/skills/ce-plan/references/agents/agent-native-planning-strategist.md +66 -0
- package/skills/ce-plan/references/agents/architecture-strategist.md +46 -0
- package/skills/ce-plan/references/agents/best-practices-researcher.md +118 -0
- package/skills/ce-plan/references/agents/data-integrity-guardian.md +68 -0
- package/skills/ce-plan/references/agents/framework-docs-researcher.md +97 -0
- package/skills/ce-plan/references/agents/git-history-analyzer.md +40 -0
- package/skills/ce-plan/references/agents/learnings-researcher.md +268 -0
- package/skills/ce-plan/references/agents/pattern-recognition-specialist.md +55 -0
- package/skills/ce-plan/references/agents/performance-oracle.md +108 -0
- package/skills/ce-plan/references/agents/repo-research-analyst.md +262 -0
- package/skills/ce-plan/references/agents/security-sentinel.md +91 -0
- package/skills/ce-plan/references/agents/slack-researcher.md +131 -0
- package/skills/ce-plan/references/agents/spec-flow-analyzer.md +84 -0
- package/skills/ce-plan/references/agents/web-researcher.md +125 -0
- package/skills/ce-plan/references/approach-altitude.md +55 -0
- package/skills/ce-plan/references/bakeoff.md +9 -0
- package/skills/ce-plan/references/deepening-workflow.md +263 -0
- package/skills/ce-plan/references/final-review.md +134 -0
- package/skills/ce-plan/references/html-rendering.md +638 -0
- package/skills/ce-plan/references/intake.md +158 -0
- package/skills/ce-plan/references/markdown-rendering.md +242 -0
- package/skills/ce-plan/references/output-contracts.md +29 -0
- package/skills/ce-plan/references/output-mode.md +45 -0
- package/skills/ce-plan/references/plan-handoff.md +116 -0
- package/skills/ce-plan/references/plan-sections.md +489 -0
- package/skills/ce-plan/references/reasoning-elevation.md +124 -0
- package/skills/ce-plan/references/research.md +211 -0
- package/skills/ce-plan/references/resume.md +61 -0
- package/skills/ce-plan/references/settled-decisions.md +45 -0
- package/skills/ce-plan/references/structure.md +207 -0
- package/skills/ce-plan/references/synthesis-summary.md +260 -0
- package/skills/ce-plan/references/universal-planning.md +170 -0
- package/skills/ce-plan/scripts/elevation-dispatch.sh +288 -0
- package/skills/ce-plan/scripts/packs-resolve.py +737 -0
- package/skills/ce-plan/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-polish/SKILL.md +22 -0
- package/skills/ce-polish/agents/openai.yaml +2 -0
- package/skills/ce-polish/references/dev-server-astro.md +58 -0
- package/skills/ce-polish/references/dev-server-detection.md +40 -0
- package/skills/ce-polish/references/dev-server-next.md +61 -0
- package/skills/ce-polish/references/dev-server-nuxt.md +58 -0
- package/skills/ce-polish/references/dev-server-procfile.md +59 -0
- package/skills/ce-polish/references/dev-server-rails.md +50 -0
- package/skills/ce-polish/references/dev-server-remix.md +58 -0
- package/skills/ce-polish/references/dev-server-sveltekit.md +58 -0
- package/skills/ce-polish/references/dev-server-vite.md +48 -0
- package/skills/ce-polish/references/install-riffrec.md +35 -0
- package/skills/ce-polish/references/launch-json-schema.md +177 -0
- package/skills/ce-polish/references/live-loop.md +98 -0
- package/skills/ce-polish/references/live-remote.md +35 -0
- package/skills/ce-polish/references/live-start.md +93 -0
- package/skills/ce-polish/references/live-stream-contract.md +253 -0
- package/skills/ce-polish/references/run.md +67 -0
- package/skills/ce-polish/scripts/detect-project-type.sh +277 -0
- package/skills/ce-polish/scripts/detect-riffrec.sh +225 -0
- package/skills/ce-polish/scripts/live-endpoint.js +2841 -0
- package/skills/ce-polish/scripts/read-launch-json.sh +117 -0
- package/skills/ce-polish/scripts/resolve-package-manager.sh +95 -0
- package/skills/ce-polish/scripts/resolve-port.sh +307 -0
- package/skills/ce-pov/SKILL.md +64 -0
- package/skills/ce-pov/references/agents/external-evidence-researcher.md +26 -0
- package/skills/ce-pov/references/agents/pov-peer.md +43 -0
- package/skills/ce-pov/references/agents/precedent-activity-scout.md +21 -0
- package/skills/ce-pov/references/agents/project-grounding-scout.md +32 -0
- package/skills/ce-pov/references/boundaries.md +36 -0
- package/skills/ce-pov/references/cross-model-panel.md +596 -0
- package/skills/ce-pov/references/followup.md +9 -0
- package/skills/ce-pov/references/grounding.md +47 -0
- package/skills/ce-pov/references/intake.md +61 -0
- package/skills/ce-pov/references/invocation.md +47 -0
- package/skills/ce-pov/references/method.md +68 -0
- package/skills/ce-pov/references/pov-schema.json +56 -0
- package/skills/ce-pov/references/report.md +7 -0
- package/skills/ce-pov/scripts/cross-model-pov.sh +1110 -0
- package/skills/ce-pov/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-product-pulse/SKILL.md +81 -0
- package/skills/ce-product-pulse/agents/openai.yaml +2 -0
- package/skills/ce-product-pulse/references/config.md +20 -0
- package/skills/ce-product-pulse/references/interview.md +260 -0
- package/skills/ce-product-pulse/references/report-template.md +89 -0
- package/skills/ce-product-pulse/references/run.md +46 -0
- package/skills/ce-product-pulse/references/setup.md +43 -0
- package/skills/ce-promote/SKILL.md +90 -0
- package/skills/ce-promote/agents/openai.yaml +2 -0
- package/skills/ce-promote/references/spiral-cli.md +149 -0
- package/skills/ce-proof/SKILL.md +65 -0
- package/skills/ce-proof/references/api.md +176 -0
- package/skills/ce-proof/references/workflows.md +124 -0
- package/skills/ce-prototype/SKILL.md +59 -0
- package/skills/ce-prototype/assets/annotate.css +206 -0
- package/skills/ce-prototype/assets/annotate.js +837 -0
- package/skills/ce-prototype/references/annotation-loop.md +32 -0
- package/skills/ce-prototype/references/build.md +39 -0
- package/skills/ce-prototype/references/craft-floor.md +43 -0
- package/skills/ce-prototype/references/preview.md +118 -0
- package/skills/ce-prototype/references/scoping.md +38 -0
- package/skills/ce-prototype/references/write-back.md +37 -0
- package/skills/ce-prototype/scripts/light-webserver.js +1275 -0
- package/skills/ce-resolve-pr-feedback/SKILL.md +60 -0
- package/skills/ce-resolve-pr-feedback/references/agents/pr-comment-resolver.md +59 -0
- package/skills/ce-resolve-pr-feedback/references/evaluation-rubric.md +141 -0
- package/skills/ce-resolve-pr-feedback/references/full-mode.md +336 -0
- package/skills/ce-resolve-pr-feedback/references/pipeline-mode.md +23 -0
- package/skills/ce-resolve-pr-feedback/references/resume.md +60 -0
- package/skills/ce-resolve-pr-feedback/references/return-to-caller.md +111 -0
- package/skills/ce-resolve-pr-feedback/references/targeted-mode.md +48 -0
- package/skills/ce-resolve-pr-feedback/scripts/get-pr-comments +176 -0
- package/skills/ce-resolve-pr-feedback/scripts/get-thread-for-comment +84 -0
- package/skills/ce-resolve-pr-feedback/scripts/pending-feedback.py +287 -0
- package/skills/ce-resolve-pr-feedback/scripts/reply-to-pr-thread +57 -0
- package/skills/ce-resolve-pr-feedback/scripts/resolve-pr-thread +23 -0
- package/skills/ce-retune/SKILL.md +42 -0
- package/skills/ce-retune/agents/openai.yaml +2 -0
- package/skills/ce-retune/references/baseline-mining.md +122 -0
- package/skills/ce-retune/references/corpus-audit.md +128 -0
- package/skills/ce-retune/references/cut-passes.md +129 -0
- package/skills/ce-retune/references/halt-taxonomy.md +123 -0
- package/skills/ce-retune/references/noise-floor.md +131 -0
- package/skills/ce-retune/references/workflow-shapes.md +73 -0
- package/skills/ce-riffrec-feedback-analysis/SKILL.md +33 -0
- package/skills/ce-riffrec-feedback-analysis/references/analyzer.md +18 -0
- package/skills/ce-riffrec-feedback-analysis/references/compound-engineering-feedback-format.md +116 -0
- package/skills/ce-riffrec-feedback-analysis/references/extensive-analysis.md +98 -0
- package/skills/ce-riffrec-feedback-analysis/references/install-riffrec.md +27 -0
- package/skills/ce-riffrec-feedback-analysis/references/quick-bug-report.md +38 -0
- package/skills/ce-riffrec-feedback-analysis/scripts/analyze_riffrec_zip.py +1306 -0
- package/skills/ce-setup/SKILL.md +103 -0
- package/skills/ce-setup/agents/openai.yaml +2 -0
- package/skills/ce-setup/assets/compounding-directive.md +11 -0
- package/skills/ce-setup/assets/noslop-directive.md +7 -0
- package/skills/ce-setup/assets/pack-rule-template.md +11 -0
- package/skills/ce-setup/references/acpx-cache-warm.md +14 -0
- package/skills/ce-setup/references/config-template.yaml +210 -0
- package/skills/ce-setup/references/legacy-codex-tool-map.md +15 -0
- package/skills/ce-setup/references/pack-scaffold.md +45 -0
- package/skills/ce-setup/references/repo-fixes.md +121 -0
- package/skills/ce-setup/scripts/check-health +929 -0
- package/skills/ce-setup/scripts/packs-resolve.py +737 -0
- package/skills/ce-simplify-code/SKILL.md +76 -0
- package/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
- package/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
- package/skills/ce-simplify-code/references/personas/comment-reviewer.md +24 -0
- package/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
- package/skills/ce-simplify-code/references/personas/test-value-reviewer.md +23 -0
- package/skills/ce-simplify-code/references/reviewer-dispatch.md +11 -0
- package/skills/ce-simplify-code/references/standalone-report.md +25 -0
- package/skills/ce-strategy/SKILL.md +61 -0
- package/skills/ce-strategy/references/grounding.md +22 -0
- package/skills/ce-strategy/references/interview.md +179 -0
- package/skills/ce-strategy/references/strategy-template.md +94 -0
- package/skills/ce-strategy/references/update-run.md +33 -0
- package/skills/ce-sweep/SKILL.md +93 -0
- package/skills/ce-sweep/agents/openai.yaml +2 -0
- package/skills/ce-sweep/references/agents/media-analyzer.md +54 -0
- package/skills/ce-sweep/references/interview.md +208 -0
- package/skills/ce-sweep/references/model-tiers.md +13 -0
- package/skills/ce-sweep/references/plan-template.md +62 -0
- package/skills/ce-sweep/references/run.md +126 -0
- package/skills/ce-sweep/references/sources/email.md +57 -0
- package/skills/ce-sweep/references/sources/github-issues.md +59 -0
- package/skills/ce-sweep/references/sources/slack.md +61 -0
- package/skills/ce-sweep/references/state-schema.md +157 -0
- package/skills/ce-sweep/references/subagent-template.md +60 -0
- package/skills/ce-sweep/scripts/analyze_riffrec_zip.py +1306 -0
- package/skills/ce-sweep/scripts/sweep-state.py +793 -0
- package/skills/ce-test-browser/SKILL.md +50 -0
- package/skills/ce-test-browser/references/agent-browser-driver.md +47 -0
- package/skills/ce-test-browser/references/pipeline-orchestration.md +45 -0
- package/skills/ce-test-browser/references/route-and-report.md +129 -0
- package/skills/ce-test-browser/scripts/resolve-port.sh +41 -0
- package/skills/ce-test-xcode/SKILL.md +26 -0
- package/skills/ce-test-xcode/agents/openai.yaml +2 -0
- package/skills/ce-test-xcode/references/setup-and-build.md +32 -0
- package/skills/ce-test-xcode/references/test-and-report.md +64 -0
- package/skills/ce-work/SKILL.md +62 -0
- package/skills/ce-work/references/agents/figma-design-sync.md +27 -0
- package/skills/ce-work/references/agents/implementation-worker.md +17 -0
- package/skills/ce-work/references/cross-model-execution.md +191 -0
- package/skills/ce-work/references/execution-engines.md +64 -0
- package/skills/ce-work/references/execution-strategy.md +73 -0
- package/skills/ce-work/references/implementation-loop.md +107 -0
- package/skills/ce-work/references/implementation-result-schema.json +52 -0
- package/skills/ce-work/references/input-triage.md +50 -0
- package/skills/ce-work/references/non-code-execution.md +23 -0
- package/skills/ce-work/references/return-to-caller.md +33 -0
- package/skills/ce-work/references/review-findings-followup.md +82 -0
- package/skills/ce-work/references/shipping-workflow.md +89 -0
- package/skills/ce-work/references/tracker-defer.md +150 -0
- package/skills/ce-work/references/work-intake.md +51 -0
- package/skills/ce-work/references/workspace-setup.md +27 -0
- package/skills/ce-work/scripts/__pycache__/unit_workspace_jobs.cpython-314.pyc +0 -0
- package/skills/ce-work/scripts/__pycache__/unit_workspace_state.cpython-314.pyc +0 -0
- package/skills/ce-work/scripts/cross-model-work.sh +1385 -0
- package/skills/ce-work/scripts/peer-job-runner.py +2594 -0
- package/skills/ce-work/scripts/unit-workspace.py +215 -0
- package/skills/ce-work/scripts/unit_workspace_ignored.py +93 -0
- package/skills/ce-work/scripts/unit_workspace_integration.py +677 -0
- package/skills/ce-work/scripts/unit_workspace_jobs.py +1216 -0
- package/skills/ce-work/scripts/unit_workspace_lifecycle.py +981 -0
- package/skills/ce-work/scripts/unit_workspace_state.py +1379 -0
- package/skills/ce-work/scripts/unit_workspace_transaction.py +612 -0
- package/skills/ce-worktree/SKILL.md +54 -0
- package/skills/lfg/SKILL.md +59 -0
- package/skills/lfg/references/debug-return.md +25 -0
- package/skills/lfg/references/intake.md +30 -0
- package/skills/lfg/references/next-work-handoff.md +96 -0
- package/skills/lfg/references/plan-brief.md +36 -0
- package/skills/lfg/references/review-followup.md +78 -0
- package/skills/lfg/references/shipping.md +65 -0
- package/skills/lfg/references/stage-routing.md +49 -0
- package/skills/lfg/references/task-visibility.md +11 -0
- package/skills/lfg/references/tracker-defer.md +150 -0
- package/skills/lfg/references/work-return.md +27 -0
- package/skills/wtf/SKILL.md +33 -0
- package/skills/wtf/agents/openai.yaml +2 -0
|
@@ -0,0 +1,1385 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run one pre-sanctioned, write-capable implementation route in a controller-
|
|
3
|
+
# supplied detached workspace. The adapter never creates worktrees, changes
|
|
4
|
+
# recipients, integrates output, or retries through another route.
|
|
5
|
+
#
|
|
6
|
+
# Every route reaches its agent through acpx (the Agent Client Protocol
|
|
7
|
+
# client), which prints the JSON-RPC stream; one parser reads every route.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# cross-model-work.sh <authorization-json> <workspace> <unit-packet> <expected-packet-sha256> <result-dir>
|
|
11
|
+
#
|
|
12
|
+
# Routes: codex | claude | grok-cli | cursor | composer | grok-cursor | opencode | antigravity
|
|
13
|
+
# Output: <result-dir>/implementation-result.json and redacted adapter.log
|
|
14
|
+
# Exit: 0 host-resolvable terminal result, 1 failed/schema-invalid, 2 unavailable
|
|
15
|
+
#
|
|
16
|
+
# Introspection (no model call):
|
|
17
|
+
# cross-model-work.sh --emit-adapter <route>
|
|
18
|
+
# cross-model-work.sh --check-model antigravity <model>
|
|
19
|
+
# opens an Antigravity session for <model> without sending a prompt; exit 0
|
|
20
|
+
# when the account offers it, exit 2 with the reason on stderr otherwise
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
umask 077
|
|
24
|
+
|
|
25
|
+
# Prefer the runner-exported interpreter (sys.executable via CE_PEER_PYTHON),
|
|
26
|
+
# else probe execution — Windows Store's python3 stub satisfies `command -v`
|
|
27
|
+
# then exits nonzero (see resolve-python convention / #1247).
|
|
28
|
+
PY="${CE_PEER_PYTHON:-}"
|
|
29
|
+
if [ -z "$PY" ]; then
|
|
30
|
+
PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"
|
|
31
|
+
fi
|
|
32
|
+
[ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; }
|
|
33
|
+
|
|
34
|
+
# Cursor's ACP server offers one preset per model and rejects effort variants,
|
|
35
|
+
# so grok-cursor runs at high/fast and composer at its fast tier.
|
|
36
|
+
M_GROK_CURSOR="grok-4.7[context=256k,reasoning_effort=high,fast=true]"
|
|
37
|
+
M_COMPOSER="composer-2.5[fast=true]"
|
|
38
|
+
M_ANTIGRAVITY="gemini-4-argon"
|
|
39
|
+
|
|
40
|
+
log() { printf '[cross-model-work] %s\n' "$*" >&2; }
|
|
41
|
+
|
|
42
|
+
route_target() {
|
|
43
|
+
case "$1" in
|
|
44
|
+
codex|claude|cursor|composer|antigravity) printf '%s' "$1" ;;
|
|
45
|
+
grok-cli|grok-cursor) printf 'grok' ;;
|
|
46
|
+
opencode) printf 'opencode' ;;
|
|
47
|
+
*) return 1 ;;
|
|
48
|
+
esac
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
route_harness() {
|
|
52
|
+
case "$1" in
|
|
53
|
+
codex) printf 'codex' ;;
|
|
54
|
+
claude) printf 'claude' ;;
|
|
55
|
+
grok-cli) printf 'grok' ;;
|
|
56
|
+
cursor|composer|grok-cursor) printf 'cursor-agent' ;;
|
|
57
|
+
opencode) printf 'opencode' ;;
|
|
58
|
+
antigravity) printf 'antigravity' ;;
|
|
59
|
+
*) return 1 ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
route_model() {
|
|
64
|
+
local route="$1" target override="${CE_WORK_MODEL_OVERRIDE:-}"
|
|
65
|
+
if [ -n "${MODEL_REQUESTED:-}" ]; then
|
|
66
|
+
printf '%s' "$MODEL_REQUESTED"
|
|
67
|
+
return
|
|
68
|
+
fi
|
|
69
|
+
target="$(route_target "$route")" || return 1
|
|
70
|
+
if [ -n "$override" ] && [ "${CE_WORK_MODEL_OVERRIDE_TARGET:-}" = "$target" ]; then
|
|
71
|
+
printf '%s' "$override"
|
|
72
|
+
return
|
|
73
|
+
fi
|
|
74
|
+
case "$route" in
|
|
75
|
+
codex|claude|grok-cli|cursor) printf 'auto' ;;
|
|
76
|
+
grok-cursor) printf '%s' "$M_GROK_CURSOR" ;;
|
|
77
|
+
composer) printf '%s' "$M_COMPOSER" ;;
|
|
78
|
+
opencode) printf 'auto' ;;
|
|
79
|
+
antigravity) printf '%s' "$M_ANTIGRAVITY" ;;
|
|
80
|
+
esac
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
route_available() {
|
|
84
|
+
case "$1" in
|
|
85
|
+
codex) command -v codex >/dev/null 2>&1 ;;
|
|
86
|
+
claude) command -v claude >/dev/null 2>&1 ;;
|
|
87
|
+
grok-cli) command -v grok >/dev/null 2>&1 ;;
|
|
88
|
+
cursor|composer|grok-cursor) command -v cursor-agent >/dev/null 2>&1 ;;
|
|
89
|
+
opencode) command -v opencode >/dev/null 2>&1 ;;
|
|
90
|
+
antigravity) command -v agy_acp_server.par >/dev/null 2>&1 ;;
|
|
91
|
+
*) return 1 ;;
|
|
92
|
+
esac
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# --- acpx transport (keep byte-identical across migrated peer workers) -----
|
|
96
|
+
# Every route reaches its agent through acpx, which speaks the Agent Client
|
|
97
|
+
# Protocol and prints the JSON-RPC stream, so one parser covers every CLI.
|
|
98
|
+
# The calling worker supplies route_available, route_model, and log.
|
|
99
|
+
ACPX_VERSION="0.21.0"
|
|
100
|
+
ACPX_UNAVAILABLE="" # pre-egress reason; ACPX_SCOPE says whether every route shares it
|
|
101
|
+
ACPX_SCOPE=""
|
|
102
|
+
ACPX_NPM_REFRESH=false
|
|
103
|
+
|
|
104
|
+
acpx_agent() { # <route> -> acpx built-in agent name
|
|
105
|
+
case "$1" in
|
|
106
|
+
codex) printf 'codex' ;;
|
|
107
|
+
claude) printf 'claude' ;;
|
|
108
|
+
grok-cli) printf 'grok-build' ;;
|
|
109
|
+
grok-cursor|cursor|composer) printf 'cursor' ;;
|
|
110
|
+
opencode) printf 'opencode' ;;
|
|
111
|
+
antigravity) printf 'antigravity' ;;
|
|
112
|
+
*) return 1 ;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# acpx launches Google's Antigravity ACP runtime by name. It is a separate
|
|
117
|
+
# download from the agy CLI and needs its localharness_external helper from the
|
|
118
|
+
# same release, beside it or named by ANTIGRAVITY_HARNESS_PATH.
|
|
119
|
+
acpx_antigravity_helper() {
|
|
120
|
+
local runtime
|
|
121
|
+
if [ -n "${ANTIGRAVITY_HARNESS_PATH:-}" ]; then printf '%s' "$ANTIGRAVITY_HARNESS_PATH"; return 0; fi
|
|
122
|
+
runtime="$(command -v agy_acp_server.par 2>/dev/null)" || return 1
|
|
123
|
+
printf '%s/localharness_external' "$(dirname "$runtime")"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# The runtime reads skills and settings from GEMINI_HOME, so every run gets a
|
|
127
|
+
# private one, deleted with the caller's scratch. A file-backed login (no macOS
|
|
128
|
+
# keychain) is read only from there, so it is copied in; refreshes are not
|
|
129
|
+
# written back, so concurrent peers cannot clobber the user's token.
|
|
130
|
+
acpx_antigravity_home() { # <private-dir> -> GEMINI_HOME path
|
|
131
|
+
local home="$1/gemini-home"
|
|
132
|
+
local token="${GEMINI_HOME:-${HOME:-}/.gemini}/antigravity-acp/acp_token.json"
|
|
133
|
+
(umask 077; mkdir -p "$home/antigravity-acp") || return 1
|
|
134
|
+
printf '{"auth":{"type":"oauth-personal"}}\n' > "$home/antigravity-acp/settings.json" || return 1
|
|
135
|
+
if [ -f "$token" ]; then
|
|
136
|
+
(umask 077; cp "$token" "$home/antigravity-acp/acp_token.json") || return 1
|
|
137
|
+
fi
|
|
138
|
+
printf '%s' "$home"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
acpx_unavailable() { # <shared|route> <reason>
|
|
142
|
+
ACPX_SCOPE="$1"; ACPX_UNAVAILABLE="$2"
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# acpx lets ~/.acpx/config.json and <cwd>/.acpxrc.json replace a built-in
|
|
147
|
+
# agent's launch command; a route that would run something else is skipped.
|
|
148
|
+
# opencode launches the installed CLI by path, which no config entry overrides.
|
|
149
|
+
acpx_config_guard() { # <route> <cwd>
|
|
150
|
+
local agent file
|
|
151
|
+
[ "$1" = opencode ] && return 0
|
|
152
|
+
agent="$(acpx_agent "$1")" || return 1
|
|
153
|
+
for file in "${HOME:-}/.acpx/config.json" "$2/.acpxrc.json"; do
|
|
154
|
+
[ -e "$file" ] || continue
|
|
155
|
+
if ! jq -e --arg a "$agent" '(.agents // {}) | keys | map(ascii_downcase) | index($a) == null' "$file" >/dev/null 2>&1; then
|
|
156
|
+
acpx_unavailable route "$file overrides or cannot be checked for the '$agent' agent launch"
|
|
157
|
+
return 1
|
|
158
|
+
fi
|
|
159
|
+
done
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
acpx_preflight() { # <route> <cwd>; nothing is sent to a provider here
|
|
163
|
+
local v major minor
|
|
164
|
+
ACPX_UNAVAILABLE=""; ACPX_SCOPE=""
|
|
165
|
+
command -v node >/dev/null 2>&1 || { acpx_unavailable shared "node not found; acpx needs Node 22.13 or newer"; return 1; }
|
|
166
|
+
v="$(node -p 'process.versions.node' 2>/dev/null)"
|
|
167
|
+
major="${v%%.*}"; minor="${v#*.}"; minor="${minor%%.*}"
|
|
168
|
+
case "$major$minor" in ''|*[!0-9]*) acpx_unavailable shared "cannot read the Node version; acpx needs Node 22.13 or newer"; return 1 ;; esac
|
|
169
|
+
if [ "$major" -lt 22 ] || { [ "$major" -eq 22 ] && [ "$minor" -lt 13 ]; }; then
|
|
170
|
+
acpx_unavailable shared "Node $v is too old; acpx needs Node 22.13 or newer"
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
command -v npx >/dev/null 2>&1 || { acpx_unavailable shared "npx not found; acpx runs through npx"; return 1; }
|
|
174
|
+
if [ "$1" = antigravity ]; then
|
|
175
|
+
case "$(uname -s 2>/dev/null)" in
|
|
176
|
+
MINGW*|MSYS*|CYGWIN*) acpx_unavailable route "the Antigravity route is not yet supported on native Windows"; return 1 ;;
|
|
177
|
+
esac
|
|
178
|
+
route_available "$1" || { acpx_unavailable route "Antigravity's ACP runtime (agy_acp_server.par) is not on PATH; it is a separate download from the agy CLI (run ce-setup for the link)"; return 1; }
|
|
179
|
+
[ -x "$(acpx_antigravity_helper)" ] || { acpx_unavailable route "Antigravity's localharness_external helper is not beside agy_acp_server.par or at ANTIGRAVITY_HARNESS_PATH; install both from the same release (run ce-setup for the link)"; return 1; }
|
|
180
|
+
fi
|
|
181
|
+
route_available "$1" || { acpx_unavailable route "the agent CLI for route '$1' is not installed"; return 1; }
|
|
182
|
+
if [ "$1" = opencode ]; then
|
|
183
|
+
case "$(uname -s 2>/dev/null)" in
|
|
184
|
+
MINGW*|MSYS*|CYGWIN*) acpx_unavailable route "opencode launches through acpx's raw --agent command, which acpx rejects on native Windows"; return 1 ;;
|
|
185
|
+
esac
|
|
186
|
+
fi
|
|
187
|
+
acpx_config_guard "$1" "$2"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Claude's ACP adapter always loads project settings, so the launch goes
|
|
191
|
+
# through a wrapper that adds --safe-mode.
|
|
192
|
+
acpx_claude_wrapper() { # <private-dir> -> wrapper path
|
|
193
|
+
local real wrapper="$1/claude-safe-mode"
|
|
194
|
+
real="$(command -v claude)" || return 1
|
|
195
|
+
printf '#!/bin/sh\nexec %s --safe-mode "$@"\n' "$(printf '%q' "$real")" > "$wrapper" && chmod 700 "$wrapper" || return 1
|
|
196
|
+
printf '%s' "$wrapper"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# npm resolves offline-first, so cached metadata older than the acpx pin or a
|
|
200
|
+
# version an adapter's dependencies need fails with ETARGET before anything is
|
|
201
|
+
# sent. One launch with fresh metadata heals the cache. npm ignores
|
|
202
|
+
# prefer_online while prefer_offline is set, so the refresh turns it off.
|
|
203
|
+
acpx_stale_npm_metadata() { # <stderr-log> <stream-log>
|
|
204
|
+
[ "$ACPX_NPM_REFRESH" = true ] && return 1
|
|
205
|
+
grep -qE 'ETARGET|notarget|No matching version found' "$1" "$2" 2>/dev/null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Prints the NUL-delimited argv prefix through `npx acpx@<pin>` and its global
|
|
209
|
+
# flags; the worker appends permission flags, `--model`, the agent, and exec options.
|
|
210
|
+
# The codex and claude adapters are pointed at the installed CLIs so the pin
|
|
211
|
+
# governs the transport, not which models the agent can serve.
|
|
212
|
+
acpx_base_argv() { # <route> <cwd> <mcp-config> <timeout-secs> <claude-wrapper> <gemini-home>
|
|
213
|
+
printf '%s\0' env
|
|
214
|
+
# An ambient Google API key or cloud project would bill it instead of the
|
|
215
|
+
# Antigravity subscription. env takes -u only before its first assignment.
|
|
216
|
+
case "$1" in
|
|
217
|
+
antigravity) printf '%s\0' -u GEMINI_API_KEY -u GOOGLE_API_KEY -u GOOGLE_APPLICATION_CREDENTIALS \
|
|
218
|
+
-u GOOGLE_CLOUD_PROJECT -u GOOGLE_CLOUD_LOCATION -u GOOGLE_GENAI_USE_VERTEXAI -u AGY_LLM_GATEWAY_URL ;;
|
|
219
|
+
esac
|
|
220
|
+
if [ "$ACPX_NPM_REFRESH" = true ]; then
|
|
221
|
+
printf '%s\0' npm_config_prefer_offline=false npm_config_fetch_retries=0 npm_config_prefer_online=true
|
|
222
|
+
else
|
|
223
|
+
printf '%s\0' npm_config_prefer_offline=true npm_config_fetch_retries=0
|
|
224
|
+
fi
|
|
225
|
+
case "$1" in
|
|
226
|
+
# codex-acp ignores app-server CLI flags.
|
|
227
|
+
codex) printf '%s\0' "CODEX_PATH=$(command -v codex)" 'CODEX_CONFIG={"features":{"plugins":false}}' ;;
|
|
228
|
+
claude) printf '%s\0' "CLAUDE_CODE_EXECUTABLE=$5" ;;
|
|
229
|
+
antigravity) printf '%s\0' "GEMINI_HOME=$6" "ANTIGRAVITY_HARNESS_PATH=$(acpx_antigravity_helper)" ;;
|
|
230
|
+
esac
|
|
231
|
+
printf '%s\0' npx -y "acpx@$ACPX_VERSION" --cwd "$2" --format json --mcp-config "$3" --timeout "$4"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
acpx_agent_argv() { # <route>
|
|
235
|
+
# The preflight refuses opencode when it is not installed; the bare name keeps
|
|
236
|
+
# --emit-adapter output readable on a machine without it.
|
|
237
|
+
if [ "$1" = opencode ]; then printf '%s\0' --agent "$(command -v opencode || printf opencode) acp" exec
|
|
238
|
+
else printf '%s\0' "$(acpx_agent "$1")" exec; fi
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# The run's outcome is the result of its own session/prompt request, matched by
|
|
242
|
+
# id because --model adds a session/set_model request before the prompt.
|
|
243
|
+
acpx_outcome() { # <stream-log> -> end_turn | <other stopReason> | error | incomplete | not-sent
|
|
244
|
+
local id outcome
|
|
245
|
+
id="$(jq -R 'fromjson? | select(.method == "session/prompt") | .id' "$1" 2>/dev/null | tail -1)"
|
|
246
|
+
[ -n "$id" ] || { printf 'not-sent'; return 0; }
|
|
247
|
+
outcome="$(jq -rR --argjson id "$id" 'fromjson? | select(.method == null and .id == $id) | if .error then "error" else (.result.stopReason // "error") end' "$1" 2>/dev/null | tail -1)"
|
|
248
|
+
printf '%s' "${outcome:-incomplete}"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Antigravity holds session/new open while it waits for a browser sign-in it
|
|
252
|
+
# will never get, so a run without a usable login would otherwise sit out the
|
|
253
|
+
# whole idle cap. Nothing has been sent while the session is pending.
|
|
254
|
+
ACPX_SESSION_START_SECS="${CROSS_MODEL_SESSION_START_SECS:-60}"
|
|
255
|
+
acpx_session_state() { # <stream-log> -> starting | pending | open
|
|
256
|
+
local id
|
|
257
|
+
jq -eR 'fromjson? | select(.result.protocolVersion? != null)' "$1" >/dev/null 2>&1 || { printf 'starting'; return 0; }
|
|
258
|
+
id="$(jq -R 'fromjson? | select(.method == "session/new") | .id' "$1" 2>/dev/null | tail -1)"
|
|
259
|
+
if [ -n "$id" ] && jq -eR --argjson id "$id" 'fromjson? | select(.method == null and .id == $id)' "$1" >/dev/null 2>&1; then
|
|
260
|
+
printf 'open'
|
|
261
|
+
else
|
|
262
|
+
printf 'pending'
|
|
263
|
+
fi
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
acpx_text() { # <stream-log> <outfile>: the agent's reply text
|
|
267
|
+
jq -jR 'fromjson? | select(.method == "session/update" and .params.update.sessionUpdate == "agent_message_chunk") | .params.update.content.text // empty' "$1" > "$2" 2>/dev/null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Model ids the adapter reports in the prompt result's _meta. acpx documents
|
|
271
|
+
# _meta as adapter-defined, so these are the adapter's assertion, not proof.
|
|
272
|
+
acpx_served_models() { # <stream-log> -> "<model> <tokens>" lines
|
|
273
|
+
jq -rR 'fromjson? | select(.result.stopReason?) | .result._meta // {} |
|
|
274
|
+
((.quota.model_usage // [])[] | "\(.model) \(.token_count.totalTokens // 0)"),
|
|
275
|
+
(.modelId // empty | "\(.) 0")' "$1" 2>/dev/null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Bounded failure evidence from the stream. The stream echoes the outbound
|
|
279
|
+
# prompt, so only error messages and the stop reason are read from it.
|
|
280
|
+
acpx_failure_evidence() { # <stream-log>
|
|
281
|
+
local evidence
|
|
282
|
+
evidence="$(jq -rR 'fromjson? | (.error.message? // empty), (.result.stopReason? // empty | select(. != "end_turn") | "stopReason=" + .)' "$1" 2>/dev/null | tr '\n' ' ')"
|
|
283
|
+
evidence="${evidence% }"
|
|
284
|
+
[ "${#evidence}" -gt 300 ] && evidence="${evidence:0:147} ... ${evidence: -147}"
|
|
285
|
+
printf '%s' "$evidence"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# codex-acp streams a provider rejection (such as a model the account may not
|
|
289
|
+
# use) as reply text and still ends the turn with end_turn, so only the reply
|
|
290
|
+
# carries it. Bounded inside jq so a long reply never enters a bash string.
|
|
291
|
+
acpx_reply_evidence() { # <stream-log>
|
|
292
|
+
jq -nrR '[inputs | fromjson? | select(.method == "session/update" and .params.update.sessionUpdate == "agent_message_chunk") | .params.update.content.text // empty]
|
|
293
|
+
| join("") | gsub("\\s+"; " ") | ltrimstr(" ") | rtrimstr(" ")
|
|
294
|
+
| if length > 300 then .[0:147] + " ... " + .[-147:] else . end' "$1" 2>/dev/null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
acpx_advertised_models() { # <stream-log>
|
|
298
|
+
jq -nrR '[inputs | fromjson? | .result.models.availableModels[]?.modelId | sub("\\[.*$"; "")]
|
|
299
|
+
| reduce .[] as $m ([]; if any(.[]; . == $m) then . else . + [$m] end)
|
|
300
|
+
| join(", ") | if length > 300 then .[0:296] + " ..." else . end' "$1" 2>/dev/null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Exit 5 is acpx's permission refusal. Paired with stopReason cancelled, the
|
|
304
|
+
# refusal ended the turn. Exit 5 with end_turn means the turn finished. A
|
|
305
|
+
# cancelled turn with any other exit is an interrupt.
|
|
306
|
+
acpx_permission_cancel_reason() { # <stream-log> <exit-code>
|
|
307
|
+
[ "$(acpx_outcome "$1")" = cancelled ] && [ "$2" = 5 ] || return 0
|
|
308
|
+
printf '%s' "permission request cancelled the turn"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
acpx_node_startup_line() { # <stderr-log>
|
|
312
|
+
local line=""
|
|
313
|
+
line="$(grep -m1 -E '^Error ' "$1" 2>/dev/null)"
|
|
314
|
+
[ -n "$line" ] || line="$(grep -m1 -E '^node:' "$1" 2>/dev/null)"
|
|
315
|
+
printf '%s' "$line"
|
|
316
|
+
}
|
|
317
|
+
# --- end acpx transport -----------------------------------------------------
|
|
318
|
+
|
|
319
|
+
# Cursor routes take Cursor's ACP model ids, which may carry a bracketed preset
|
|
320
|
+
# (grok-4.7[context=256k,reasoning_effort=high,fast=true]); the family rule
|
|
321
|
+
# applies to the id before the bracket.
|
|
322
|
+
CURSOR_MODEL_RE='^([A-Za-z0-9][A-Za-z0-9._:/-]*)(\[[A-Za-z0-9._=,:-]*\])?$'
|
|
323
|
+
|
|
324
|
+
validate_model_override() {
|
|
325
|
+
local route="$1" override="${CE_WORK_MODEL_OVERRIDE:-}" override_target="${CE_WORK_MODEL_OVERRIDE_TARGET:-}" target base base_lower
|
|
326
|
+
[ -n "$override" ] || { [ -z "$override_target" ]; return; }
|
|
327
|
+
case "$override_target" in
|
|
328
|
+
codex|claude|grok|cursor|composer|opencode|antigravity) ;;
|
|
329
|
+
*) return 1 ;;
|
|
330
|
+
esac
|
|
331
|
+
target="$(route_target "$route")" || return 1
|
|
332
|
+
[ "$override_target" = "$target" ] || return 0
|
|
333
|
+
base="$override"
|
|
334
|
+
case "$route" in
|
|
335
|
+
cursor|composer|grok-cursor)
|
|
336
|
+
[[ "$override" =~ $CURSOR_MODEL_RE ]] || return 1
|
|
337
|
+
base="${BASH_REMATCH[1]}"
|
|
338
|
+
;;
|
|
339
|
+
esac
|
|
340
|
+
if [ "$route" = cursor ]; then
|
|
341
|
+
base_lower="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')"
|
|
342
|
+
case "$base_lower" in composer|composer-*|grok|grok-*|cursor-grok-*) return 1 ;; esac
|
|
343
|
+
return 0
|
|
344
|
+
fi
|
|
345
|
+
case "$route:$base" in
|
|
346
|
+
codex:gpt-*|codex:o[0-9]*|claude:fable|claude:opus|claude:sonnet|claude:haiku|claude:claude-*|grok-cli:grok-*|grok-cursor:grok-*|grok-cursor:cursor-grok-*|composer:composer-*|opencode:*/*) ;;
|
|
347
|
+
antigravity:gemini-*) [[ "$base" =~ ^gemini-[A-Za-z0-9._-]+$ ]] || return 1 ;;
|
|
348
|
+
*) return 1 ;;
|
|
349
|
+
esac
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Accept an effort only where the route's ACP adapter exposes an effort option
|
|
353
|
+
# and lists the value (claude effort, codex and grok reasoning_effort, checked
|
|
354
|
+
# 2026-10-06 through acpx 0.19.4; antigravity thinking_level, checked
|
|
355
|
+
# 2026-10-14; opencode 2.x effort, checked 2026-10-23). Codex and OpenCode
|
|
356
|
+
# levels vary per model, and OpenCode 1.x has no effort option, so a listed
|
|
357
|
+
# level can still be refused before the prompt is sent. Cursor fixes effort in
|
|
358
|
+
# the model preset, so any effort there makes the route unavailable instead of
|
|
359
|
+
# being dropped.
|
|
360
|
+
validate_effort_override() {
|
|
361
|
+
local route="$1" effort="${EFFORT_REQUESTED:-}"
|
|
362
|
+
[ -n "$effort" ] || return 0
|
|
363
|
+
case "$route:$effort" in
|
|
364
|
+
claude:low|claude:medium|claude:high|claude:xhigh|claude:max) ;;
|
|
365
|
+
codex:low|codex:medium|codex:high|codex:xhigh|codex:max|codex:ultra) ;;
|
|
366
|
+
grok-cli:low|grok-cli:medium|grok-cli:high|grok-cli:xhigh) ;;
|
|
367
|
+
antigravity:low|antigravity:medium|antigravity:high) ;;
|
|
368
|
+
opencode:none|opencode:low|opencode:medium|opencode:high|opencode:xhigh|opencode:max) ;;
|
|
369
|
+
*) return 1 ;;
|
|
370
|
+
esac
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# The worker runs in the prepared workspace with every permission request
|
|
374
|
+
# approved. Each adapter runs in its write-capable mode (codex agent, claude
|
|
375
|
+
# default, cursor agent, opencode build, antigravity auto_edit, since its
|
|
376
|
+
# default mode waits on approvals that never come); none of them confines
|
|
377
|
+
# writes to the workspace, which references/cross-model-execution.md records
|
|
378
|
+
# per route. Codex, Claude, and Antigravity run at high effort and native Grok
|
|
379
|
+
# at xhigh unless the authorization requests another level.
|
|
380
|
+
adapter_argv() {
|
|
381
|
+
local route="$1" model
|
|
382
|
+
acpx_agent "$route" >/dev/null || return 1
|
|
383
|
+
model="$(route_model "$route")"
|
|
384
|
+
acpx_base_argv "$route" "$WORKSPACE" "$MCP_CONFIG" "$ACPX_TIMEOUT" "$CLAUDE_WRAPPER" "$GEMINI_HOME_DIR"
|
|
385
|
+
printf '%s\0' --approve-all
|
|
386
|
+
[ "$model" = auto ] || printf '%s\0' --model "$model"
|
|
387
|
+
acpx_agent_argv "$route"
|
|
388
|
+
case "$route" in
|
|
389
|
+
codex) printf '%s\0' --config-option mode=agent --config-option "reasoning_effort=${EFFORT_REQUESTED:-high}" ;;
|
|
390
|
+
# Claude otherwise starts in the user's default permission mode.
|
|
391
|
+
claude) printf '%s\0' --config-option mode=default --config-option "effort=${EFFORT_REQUESTED:-high}" ;;
|
|
392
|
+
grok-cli) printf '%s\0' --config-option "reasoning_effort=${EFFORT_REQUESTED:-xhigh}" ;;
|
|
393
|
+
cursor|composer|grok-cursor) printf '%s\0' --config-option mode=agent ;;
|
|
394
|
+
opencode) printf '%s\0' --config-option mode=build
|
|
395
|
+
[ -z "${EFFORT_REQUESTED:-}" ] || printf '%s\0' --config-option "effort=$EFFORT_REQUESTED" ;;
|
|
396
|
+
antigravity) printf '%s\0' --config-option mode=auto_edit --config-option "thinking_level=${EFFORT_REQUESTED:-high}" ;;
|
|
397
|
+
esac
|
|
398
|
+
printf '%s\0' --file "$PROMPT_FILE"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
ACPX_TIMEOUT="${CE_PEER_HARD_SECS:-7200}"
|
|
402
|
+
case "$ACPX_TIMEOUT" in ''|0|*[!0-9]*) ACPX_TIMEOUT=7200 ;; esac
|
|
403
|
+
|
|
404
|
+
if [ "${1:-}" = "--emit-adapter" ]; then
|
|
405
|
+
WORKSPACE="<workspace>"
|
|
406
|
+
PROMPT_FILE="<prompt-file>"
|
|
407
|
+
MCP_CONFIG="<mcp-config>"
|
|
408
|
+
CLAUDE_WRAPPER="<claude-safe-mode-wrapper>"
|
|
409
|
+
GEMINI_HOME_DIR="<gemini-home>"
|
|
410
|
+
ROUTE="${2:-}"
|
|
411
|
+
EFFORT_REQUESTED="${CROSS_MODEL_EFFORT_OVERRIDE:-}"
|
|
412
|
+
validate_model_override "$ROUTE" || {
|
|
413
|
+
printf "model override '%s' not compatible with route '%s'\n" "${CE_WORK_MODEL_OVERRIDE:-}" "$ROUTE" >&2
|
|
414
|
+
exit 2
|
|
415
|
+
}
|
|
416
|
+
validate_effort_override "$ROUTE" || {
|
|
417
|
+
printf "effort override '%s' not compatible with route '%s'\n" "$EFFORT_REQUESTED" "$ROUTE" >&2
|
|
418
|
+
exit 2
|
|
419
|
+
}
|
|
420
|
+
adapter_argv "$ROUTE" >/dev/null 2>&1 || { printf "unknown route '%s'\n" "$ROUTE" >&2; exit 2; }
|
|
421
|
+
adapter_argv "$ROUTE" | tr '\0' ' '
|
|
422
|
+
printf '\n'
|
|
423
|
+
exit 0
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
SIGN_IN_STALL_REASON="transport unavailable (pre-egress, route): Antigravity is waiting for a Google sign-in; the agy CLI's sign-in does not carry over, so sign the ACP runtime in once: put {\"auth\":{\"type\":\"oauth-personal\"}} in ${GEMINI_HOME:-~/.gemini}/antigravity-acp/settings.json, run npx acpx@$ACPX_VERSION antigravity exec ok in a terminal and finish the browser sign-in, then retry"
|
|
427
|
+
|
|
428
|
+
# --check-model antigravity <model>: acpx checks --model against the models the
|
|
429
|
+
# session advertises before any prompt, so opening a session answers whether the
|
|
430
|
+
# account offers the model. acpx records the session under $HOME/.acpx; the
|
|
431
|
+
# check deletes that record, and the directories when it created them. A scratch
|
|
432
|
+
# HOME is not an option: the runtime then never opens the session.
|
|
433
|
+
if [ "${1:-}" = "--check-model" ]; then
|
|
434
|
+
ROUTE="${2:-}"
|
|
435
|
+
CE_WORK_MODEL_OVERRIDE="${3:-}"
|
|
436
|
+
CE_WORK_MODEL_OVERRIDE_TARGET=antigravity
|
|
437
|
+
[ "$ROUTE" = antigravity ] || { printf "no model check for route '%s'\n" "$ROUTE" >&2; exit 2; }
|
|
438
|
+
validate_model_override antigravity && [ -n "$CE_WORK_MODEL_OVERRIDE" ] || {
|
|
439
|
+
printf "model '%s' not compatible with route 'antigravity'\n" "$CE_WORK_MODEL_OVERRIDE" >&2
|
|
440
|
+
exit 2
|
|
441
|
+
}
|
|
442
|
+
command -v jq >/dev/null 2>&1 || { echo "transport unavailable (pre-egress, shared): jq not found; the ACP stream is read with jq" >&2; exit 2; }
|
|
443
|
+
CHECK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ce-work-model-check-XXXXXX")" || exit 2
|
|
444
|
+
CHECK_PID=""
|
|
445
|
+
ACPX_STATE="${HOME:-}/.acpx"
|
|
446
|
+
ACPX_STATE_EXISTED=false; [ -d "$ACPX_STATE" ] && ACPX_STATE_EXISTED=true
|
|
447
|
+
# Every exit path stops the detached acpx group (TERM, then KILL) and removes
|
|
448
|
+
# what the check created.
|
|
449
|
+
check_cleanup() {
|
|
450
|
+
local i
|
|
451
|
+
# Probe the whole group, not the leader: acpx can exit and leave the
|
|
452
|
+
# runtime running in its group.
|
|
453
|
+
if [ -n "$CHECK_PID" ] && kill -0 -- -"$CHECK_PID" 2>/dev/null; then
|
|
454
|
+
kill -TERM -- -"$CHECK_PID" 2>/dev/null
|
|
455
|
+
for i in 1 2 3 4 5; do kill -0 -- -"$CHECK_PID" 2>/dev/null || break; sleep 1; done
|
|
456
|
+
kill -KILL -- -"$CHECK_PID" 2>/dev/null || true
|
|
457
|
+
fi
|
|
458
|
+
rm -rf "$CHECK_DIR"
|
|
459
|
+
[ "$ACPX_STATE_EXISTED" = true ] || { rmdir "$ACPX_STATE/sessions" "$ACPX_STATE" 2>/dev/null || true; }
|
|
460
|
+
}
|
|
461
|
+
trap check_cleanup EXIT
|
|
462
|
+
trap 'exit 130' INT TERM HUP QUIT
|
|
463
|
+
mkdir "$CHECK_DIR/cwd" || exit 2
|
|
464
|
+
acpx_preflight antigravity "$CHECK_DIR/cwd" || {
|
|
465
|
+
printf 'transport unavailable (pre-egress, %s): %s\n' "$ACPX_SCOPE" "$ACPX_UNAVAILABLE" >&2
|
|
466
|
+
exit 2
|
|
467
|
+
}
|
|
468
|
+
GEMINI_HOME_DIR="$(acpx_antigravity_home "$CHECK_DIR")" || { echo "cannot prepare a private Antigravity home" >&2; exit 2; }
|
|
469
|
+
printf '{"mcpServers":[]}\n' > "$CHECK_DIR/mcp.json"
|
|
470
|
+
open_check_session() {
|
|
471
|
+
ARGS=()
|
|
472
|
+
while IFS= read -r -d '' token; do ARGS+=("$token"); done < <(
|
|
473
|
+
acpx_base_argv antigravity "$CHECK_DIR/cwd" "$CHECK_DIR/mcp.json" "$ACPX_TIMEOUT" "" "$GEMINI_HOME_DIR")
|
|
474
|
+
ARGS+=(--model "$CE_WORK_MODEL_OVERRIDE" antigravity sessions new --name "ce-work-model-check-$$")
|
|
475
|
+
set -m
|
|
476
|
+
(cd "$CHECK_DIR" && exec "${ARGS[@]}" < /dev/null > "$CHECK_DIR/stdout" 2> "$CHECK_DIR/stderr") &
|
|
477
|
+
CHECK_PID=$!
|
|
478
|
+
set +m
|
|
479
|
+
CHECK_DEADLINE=$(( $(date +%s) + ACPX_SESSION_START_SECS ))
|
|
480
|
+
while kill -0 "$CHECK_PID" 2>/dev/null && [ "$(date +%s)" -lt "$CHECK_DEADLINE" ]; do sleep 1; done
|
|
481
|
+
if kill -0 "$CHECK_PID" 2>/dev/null; then
|
|
482
|
+
echo "$SIGN_IN_STALL_REASON" >&2
|
|
483
|
+
exit 2
|
|
484
|
+
fi
|
|
485
|
+
wait "$CHECK_PID"
|
|
486
|
+
CHECK_EXIT=$?
|
|
487
|
+
}
|
|
488
|
+
open_check_session
|
|
489
|
+
if [ "$CHECK_EXIT" -ne 0 ] && acpx_stale_npm_metadata "$CHECK_DIR/stderr" "$CHECK_DIR/stdout"; then
|
|
490
|
+
log "npm resolved against stale cached package metadata; retrying the model check once with fresh metadata"
|
|
491
|
+
ACPX_NPM_REFRESH=true
|
|
492
|
+
open_check_session
|
|
493
|
+
fi
|
|
494
|
+
RECORD_ID="$(jq -r 'select(.action? == "session_ensured") | .acpxRecordId // empty' "$CHECK_DIR/stdout" 2>/dev/null | tail -1)"
|
|
495
|
+
case "$RECORD_ID" in ''|*[!A-Za-z0-9-]*) ;; *) rm -f "$ACPX_STATE/sessions/$RECORD_ID.json" ;; esac
|
|
496
|
+
[ "$CHECK_EXIT" -eq 0 ] && exit 0
|
|
497
|
+
REASON="$(grep -m1 '^npm error' "$CHECK_DIR/stderr" 2>/dev/null)"
|
|
498
|
+
if [ -n "$REASON" ]; then
|
|
499
|
+
printf 'transport unavailable (pre-egress, shared): %s\n' "${REASON:0:200}" >&2
|
|
500
|
+
else
|
|
501
|
+
REASON="$(acpx_failure_evidence "$CHECK_DIR/stdout")"
|
|
502
|
+
[ -n "$REASON" ] || REASON="$(grep -m1 -e 'did not advertise' -e 'Error' "$CHECK_DIR/stderr" 2>/dev/null)"
|
|
503
|
+
printf 'transport unavailable (pre-egress, route): %s\n' "${REASON:-acpx exited $CHECK_EXIT while opening a session}" >&2
|
|
504
|
+
fi
|
|
505
|
+
exit 2
|
|
506
|
+
fi
|
|
507
|
+
|
|
508
|
+
AUTHORIZATION="${1:-}"
|
|
509
|
+
WORKSPACE="${2:-}"
|
|
510
|
+
PACKET="${3:-}"
|
|
511
|
+
EXPECTED_PACKET_DIGEST="${4:-}"
|
|
512
|
+
RESULT_DIR="${5:-}"
|
|
513
|
+
[[ "$EXPECTED_PACKET_DIGEST" =~ ^[0-9a-f]{64}$ ]] || { log "expected packet digest must be lowercase SHA-256"; exit 2; }
|
|
514
|
+
[ -n "$AUTHORIZATION" ] || { log "controller authorization JSON path is required"; exit 2; }
|
|
515
|
+
[ -d "$WORKSPACE" ] || { log "workspace '$WORKSPACE' is not a directory"; exit 2; }
|
|
516
|
+
[ -f "$PACKET" ] && [ ! -L "$PACKET" ] || { log "unit packet '$PACKET' is not a regular non-link file"; exit 2; }
|
|
517
|
+
[ -d "$RESULT_DIR" ] && [ ! -L "$RESULT_DIR" ] || { log "result dir '$RESULT_DIR' is not a directory"; exit 2; }
|
|
518
|
+
|
|
519
|
+
DISPATCH_AUTHORIZATION="$AUTHORIZATION"
|
|
520
|
+
DISPATCH_WORKSPACE="$WORKSPACE"
|
|
521
|
+
DISPATCH_PACKET="$PACKET"
|
|
522
|
+
DISPATCH_RESULT_DIR="$RESULT_DIR"
|
|
523
|
+
|
|
524
|
+
MAX_PACKET_BYTES="${CE_WORK_MAX_PACKET_BYTES:-200000}"
|
|
525
|
+
case "$MAX_PACKET_BYTES" in ''|*[!0-9]*) MAX_PACKET_BYTES=200000 ;; esac
|
|
526
|
+
|
|
527
|
+
SKILL_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 2
|
|
528
|
+
PERSONA="$SKILL_ROOT/references/agents/implementation-worker.md"
|
|
529
|
+
SCHEMA="$SKILL_ROOT/references/implementation-result-schema.json"
|
|
530
|
+
[ -f "$PERSONA" ] && [ -f "$SCHEMA" ] || { log "worker persona or result schema missing"; exit 2; }
|
|
531
|
+
|
|
532
|
+
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/ce-work-adapter-XXXXXX")" || exit 2
|
|
533
|
+
chmod 700 "$SCRATCH"
|
|
534
|
+
PROMPT_FILE="$SCRATCH/prompt.md"
|
|
535
|
+
# acpx prints the ACP stream on stdout; npm and adapter diagnostics go to stderr.
|
|
536
|
+
RAW_STDOUT="$SCRATCH/stdout.log"
|
|
537
|
+
RAW_STDERR="$SCRATCH/stderr.log"
|
|
538
|
+
STREAM="$SCRATCH/stream.redacted"
|
|
539
|
+
REPLY_TEXT="$SCRATCH/reply.txt"
|
|
540
|
+
SERVED_MODELS="$SCRATCH/served-models"
|
|
541
|
+
MCP_CONFIG="$SCRATCH/mcp.json"
|
|
542
|
+
CLAUDE_WRAPPER=""
|
|
543
|
+
GEMINI_HOME_DIR=""
|
|
544
|
+
SESSION_STALL_MARKER="$SCRATCH/session-stalled"
|
|
545
|
+
RAW_LIMIT_MARKER="$SCRATCH/raw-output-limit"
|
|
546
|
+
PACKET_SNAPSHOT="$SCRATCH/unit-packet"
|
|
547
|
+
AUTH_VALUES="$SCRATCH/authorization-values"
|
|
548
|
+
RESULT_FILE="$RESULT_DIR/implementation-result.json"
|
|
549
|
+
LOG_FILE="$RESULT_DIR/adapter.log"
|
|
550
|
+
LOG_RETAINED=0
|
|
551
|
+
trap 'rm -rf "$SCRATCH"' EXIT
|
|
552
|
+
|
|
553
|
+
# The controller's create-exclusive authorization artifact is the production
|
|
554
|
+
# dispatch capability. Read it once through a no-follow descriptor, validate
|
|
555
|
+
# its exact route/model/packet contract, and derive every dispatch identity
|
|
556
|
+
# field from those bytes before constructing a prompt or invoking a model CLI.
|
|
557
|
+
"$PY" - "$AUTHORIZATION" "$EXPECTED_PACKET_DIGEST" "$AUTH_VALUES" <<'PY'
|
|
558
|
+
import json, os, re, stat, sys
|
|
559
|
+
|
|
560
|
+
source, expected_packet_digest, output = sys.argv[1:]
|
|
561
|
+
required = {
|
|
562
|
+
"schema_version", "run_id", "unit_id", "attempt_id", "route", "target", "harness",
|
|
563
|
+
"intermediaries", "model_requested", "restriction_posture",
|
|
564
|
+
"restrictions", "activity_posture", "packet_digest",
|
|
565
|
+
}
|
|
566
|
+
contracts = {
|
|
567
|
+
"codex": ("codex", "codex", [], "cooperative"),
|
|
568
|
+
"claude": ("claude", "claude", [], "cooperative"),
|
|
569
|
+
"grok-cli": ("grok", "grok", [], "cooperative"),
|
|
570
|
+
"cursor": ("cursor", "cursor-agent", [], "cooperative"),
|
|
571
|
+
"composer": ("composer", "cursor-agent", ["cursor"], "cooperative"),
|
|
572
|
+
"grok-cursor": ("grok", "cursor-agent", ["cursor"], "cooperative"),
|
|
573
|
+
"opencode": ("opencode", "opencode", [], "cooperative"),
|
|
574
|
+
"antigravity": ("antigravity", "antigravity", [], "cooperative"),
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
def fail(message):
|
|
578
|
+
raise ValueError(message)
|
|
579
|
+
|
|
580
|
+
def model_allowed(route, model):
|
|
581
|
+
if not isinstance(model, str) or not model or "\n" in model or "\r" in model:
|
|
582
|
+
return False
|
|
583
|
+
if route in ("cursor", "composer", "grok-cursor"):
|
|
584
|
+
match = re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9._:/-]*)(?:\[[A-Za-z0-9._=,:-]*\])?", model)
|
|
585
|
+
if not match:
|
|
586
|
+
return False
|
|
587
|
+
base = match.group(1)
|
|
588
|
+
if route == "composer":
|
|
589
|
+
return bool(re.fullmatch(r"composer-[A-Za-z0-9._-]+", base))
|
|
590
|
+
if route == "grok-cursor":
|
|
591
|
+
return bool(re.fullmatch(r"(?:cursor-)?grok-[A-Za-z0-9._-]+", base))
|
|
592
|
+
lowered = base.lower()
|
|
593
|
+
return not (lowered in {"composer", "grok"} or lowered.startswith(("composer-", "grok-", "cursor-grok-")))
|
|
594
|
+
if route == "codex":
|
|
595
|
+
return model == "auto" or bool(re.fullmatch(r"(?:gpt-[A-Za-z0-9._-]+|o[0-9][A-Za-z0-9._-]*)", model))
|
|
596
|
+
if route == "claude":
|
|
597
|
+
return model in {"auto", "fable", "opus", "sonnet", "haiku"} or bool(re.fullmatch(r"claude-[A-Za-z0-9._-]+", model))
|
|
598
|
+
if route == "grok-cli":
|
|
599
|
+
return model == "auto" or bool(re.fullmatch(r"grok-[A-Za-z0-9._-]+", model))
|
|
600
|
+
if route == "opencode":
|
|
601
|
+
return model == "auto" or bool(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9._-]+", model))
|
|
602
|
+
if route == "antigravity":
|
|
603
|
+
return bool(re.fullmatch(r"gemini-[A-Za-z0-9._-]+", model))
|
|
604
|
+
return False
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
608
|
+
fd = os.open(os.path.abspath(source), flags)
|
|
609
|
+
try:
|
|
610
|
+
before = os.fstat(fd)
|
|
611
|
+
if not stat.S_ISREG(before.st_mode):
|
|
612
|
+
fail("authorization is not a regular file")
|
|
613
|
+
geteuid = getattr(os, "geteuid", None)
|
|
614
|
+
if geteuid is not None and before.st_uid != geteuid():
|
|
615
|
+
fail("authorization is not owned by the current user")
|
|
616
|
+
if stat.S_IMODE(before.st_mode) != 0o600:
|
|
617
|
+
fail("authorization mode is not 0600")
|
|
618
|
+
if before.st_size > 64 * 1024:
|
|
619
|
+
fail("authorization exceeds 65536 bytes")
|
|
620
|
+
chunks, total = [], 0
|
|
621
|
+
while True:
|
|
622
|
+
part = os.read(fd, min(65536, 65537 - total))
|
|
623
|
+
if not part:
|
|
624
|
+
break
|
|
625
|
+
chunks.append(part)
|
|
626
|
+
total += len(part)
|
|
627
|
+
if total > 65536:
|
|
628
|
+
fail("authorization grew past 65536 bytes")
|
|
629
|
+
after = os.fstat(fd)
|
|
630
|
+
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (
|
|
631
|
+
after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns
|
|
632
|
+
):
|
|
633
|
+
fail("authorization changed while being read")
|
|
634
|
+
finally:
|
|
635
|
+
os.close(fd)
|
|
636
|
+
try:
|
|
637
|
+
value = json.loads(b"".join(chunks))
|
|
638
|
+
except (ValueError, UnicodeDecodeError) as exc:
|
|
639
|
+
fail(f"authorization is malformed JSON: {exc}")
|
|
640
|
+
if not isinstance(value, dict) or set(value) - {"effort_requested"} != required:
|
|
641
|
+
fail("authorization keys do not match the exact controller schema")
|
|
642
|
+
if type(value["schema_version"]) is not int or value["schema_version"] != 1:
|
|
643
|
+
fail("authorization schema_version must be 1")
|
|
644
|
+
for key in ("run_id", "unit_id", "attempt_id"):
|
|
645
|
+
if not isinstance(value[key], str) or not re.fullmatch(r"[A-Za-z0-9._-]{1,128}", value[key]) or not value[key].strip("."):
|
|
646
|
+
fail(f"authorization {key} is unsafe")
|
|
647
|
+
route = value["route"]
|
|
648
|
+
if route not in contracts:
|
|
649
|
+
fail("authorization route is unsupported")
|
|
650
|
+
target, harness, intermediaries, posture = contracts[route]
|
|
651
|
+
if (value["target"], value["harness"], value["intermediaries"], value["restriction_posture"]) != (target, harness, intermediaries, posture):
|
|
652
|
+
fail("authorization route identity or restriction posture is inconsistent")
|
|
653
|
+
if value["activity_posture"] not in {"incremental", "hard-only"}:
|
|
654
|
+
fail("authorization activity_posture is invalid")
|
|
655
|
+
restrictions = value["restrictions"]
|
|
656
|
+
if not isinstance(restrictions, list) or not all(isinstance(item, str) for item in restrictions):
|
|
657
|
+
fail("authorization restrictions must be a string list")
|
|
658
|
+
if not model_allowed(route, value["model_requested"]):
|
|
659
|
+
fail("authorization model is incompatible with the fixed route")
|
|
660
|
+
effort = value.get("effort_requested", "")
|
|
661
|
+
if "effort_requested" in value and (not isinstance(effort, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,31}", effort)):
|
|
662
|
+
fail("authorization effort_requested is not a short plain token")
|
|
663
|
+
packet_digest = value["packet_digest"]
|
|
664
|
+
if not isinstance(packet_digest, str) or not re.fullmatch(r"[0-9a-f]{64}", packet_digest):
|
|
665
|
+
fail("authorization packet_digest is not lowercase SHA-256")
|
|
666
|
+
if packet_digest != expected_packet_digest:
|
|
667
|
+
fail("authorization packet digest does not match dispatch")
|
|
668
|
+
authorization_digest = __import__("hashlib").sha256(b"".join(chunks)).hexdigest()
|
|
669
|
+
fields = (
|
|
670
|
+
authorization_digest, value["run_id"], value["unit_id"], value["attempt_id"],
|
|
671
|
+
route, target, harness, value["model_requested"], value["activity_posture"], posture, effort,
|
|
672
|
+
)
|
|
673
|
+
out = os.open(output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
674
|
+
try:
|
|
675
|
+
os.write(out, b"\0".join(item.encode() for item in fields) + b"\0")
|
|
676
|
+
finally:
|
|
677
|
+
os.close(out)
|
|
678
|
+
except (OSError, ValueError) as exc:
|
|
679
|
+
print(f"controller authorization rejected: {exc}", file=sys.stderr)
|
|
680
|
+
raise SystemExit(2)
|
|
681
|
+
PY
|
|
682
|
+
AUTH_EXIT=$?
|
|
683
|
+
[ "$AUTH_EXIT" -eq 0 ] || { log "controller authorization rejected"; exit 2; }
|
|
684
|
+
|
|
685
|
+
AUTH_FIELDS=()
|
|
686
|
+
while IFS= read -r -d '' field; do AUTH_FIELDS+=("$field"); done < "$AUTH_VALUES"
|
|
687
|
+
[ "${#AUTH_FIELDS[@]}" -eq 11 ] || { log "controller authorization projection is incomplete"; exit 2; }
|
|
688
|
+
OBSERVED_AUTH_DIGEST="${AUTH_FIELDS[0]}"
|
|
689
|
+
RUN_ID="${AUTH_FIELDS[1]}"
|
|
690
|
+
UNIT_ID="${AUTH_FIELDS[2]}"
|
|
691
|
+
ATTEMPT_ID="${AUTH_FIELDS[3]}"
|
|
692
|
+
ROUTE="${AUTH_FIELDS[4]}"
|
|
693
|
+
AUTH_TARGET="${AUTH_FIELDS[5]}"
|
|
694
|
+
AUTH_HARNESS="${AUTH_FIELDS[6]}"
|
|
695
|
+
MODEL_REQUESTED="${AUTH_FIELDS[7]}"
|
|
696
|
+
ACTIVITY_POSTURE="${AUTH_FIELDS[8]}"
|
|
697
|
+
RESTRICTION_POSTURE="${AUTH_FIELDS[9]}"
|
|
698
|
+
EFFORT_REQUESTED="${AUTH_FIELDS[10]}"
|
|
699
|
+
RUNNER_JOB_ID="${CE_PEER_JOB_ID:-}"
|
|
700
|
+
[[ "$RUNNER_JOB_ID" =~ ^[A-Za-z0-9._-]{1,128}$ && "$RUNNER_JOB_ID" =~ [A-Za-z0-9_-] ]] || {
|
|
701
|
+
log "runner job identity is missing or unsafe"
|
|
702
|
+
exit 2
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
# A valid JSON file is not itself dispatch authority. Prove the exact no-follow
|
|
706
|
+
# snapshot and every raw controller-returned path back to the controller before
|
|
707
|
+
# prompt construction. Only its AUTHORIZED status permits external egress.
|
|
708
|
+
CONTROLLER="$SKILL_ROOT/scripts/unit-workspace.py"
|
|
709
|
+
AUTH_RESPONSE="$("$PY" "$CONTROLLER" authorize-dispatch \
|
|
710
|
+
--authorization "$DISPATCH_AUTHORIZATION" \
|
|
711
|
+
--authorization-digest "$OBSERVED_AUTH_DIGEST" \
|
|
712
|
+
--workspace "$DISPATCH_WORKSPACE" \
|
|
713
|
+
--packet "$DISPATCH_PACKET" \
|
|
714
|
+
--packet-digest "$EXPECTED_PACKET_DIGEST" \
|
|
715
|
+
--result-dir "$DISPATCH_RESULT_DIR" \
|
|
716
|
+
--run-id "$RUN_ID" --unit-id "$UNIT_ID" --attempt-id "$ATTEMPT_ID" --job-id "$RUNNER_JOB_ID" 2>&1)"
|
|
717
|
+
CONTROLLER_EXIT=$?
|
|
718
|
+
AUTH_STATUS="${AUTH_RESPONSE%%$'\n'*}"
|
|
719
|
+
if [ "$CONTROLLER_EXIT" -ne 0 ] || [ "$AUTH_STATUS" != "AUTHORIZED" ]; then
|
|
720
|
+
[ -n "$AUTH_RESPONSE" ] && printf '%s\n' "$AUTH_RESPONSE" >&2
|
|
721
|
+
log "controller dispatch authorization failed"
|
|
722
|
+
exit 2
|
|
723
|
+
fi
|
|
724
|
+
|
|
725
|
+
# Canonicalize operational paths only after the handshake. The controller
|
|
726
|
+
# compares the raw paths it returned, including platform compatibility symlinks.
|
|
727
|
+
WORKSPACE="$(cd "$WORKSPACE" && pwd -P)" || exit 2
|
|
728
|
+
PACKET="$(cd "$(dirname "$PACKET")" && pwd -P)/$(basename "$PACKET")" || exit 2
|
|
729
|
+
RESULT_DIR="$(cd "$RESULT_DIR" && pwd -P)" || exit 2
|
|
730
|
+
case "$RESULT_DIR/" in "$WORKSPACE/"*) log "result dir must be outside the worker workspace"; exit 2 ;; esac
|
|
731
|
+
case "$PACKET" in "$WORKSPACE"/*) log "unit packet must be outside the worker workspace"; exit 2 ;; esac
|
|
732
|
+
git -C "$WORKSPACE" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { log "workspace is not a Git worktree"; exit 2; }
|
|
733
|
+
chmod 700 "$RESULT_DIR" 2>/dev/null || { log "result dir could not be made private"; exit 2; }
|
|
734
|
+
RESULT_DIR_IDENTITY="$("$PY" - "$RESULT_DIR" <<'PY'
|
|
735
|
+
import os, stat, sys
|
|
736
|
+
|
|
737
|
+
path = sys.argv[1]
|
|
738
|
+
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
739
|
+
fd = os.open(path, flags)
|
|
740
|
+
try:
|
|
741
|
+
info = os.fstat(fd)
|
|
742
|
+
if not stat.S_ISDIR(info.st_mode):
|
|
743
|
+
raise OSError("result dir is not a directory")
|
|
744
|
+
print(f"{info.st_dev}:{info.st_ino}")
|
|
745
|
+
finally:
|
|
746
|
+
os.close(fd)
|
|
747
|
+
PY
|
|
748
|
+
)" || { log "result dir identity could not be captured"; exit 2; }
|
|
749
|
+
|
|
750
|
+
write_adapter_log() {
|
|
751
|
+
"$PY" -c '
|
|
752
|
+
import os, stat, sys
|
|
753
|
+
|
|
754
|
+
path, expected = sys.argv[1:]
|
|
755
|
+
dir_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
756
|
+
try:
|
|
757
|
+
dir_fd = os.open(path, dir_flags)
|
|
758
|
+
try:
|
|
759
|
+
directory = os.fstat(dir_fd)
|
|
760
|
+
if not stat.S_ISDIR(directory.st_mode):
|
|
761
|
+
raise OSError("result dir is not a directory")
|
|
762
|
+
if f"{directory.st_dev}:{directory.st_ino}" != expected:
|
|
763
|
+
raise OSError("result dir identity changed during route")
|
|
764
|
+
file_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
765
|
+
log_fd = os.open("adapter.log", file_flags, 0o600, dir_fd=dir_fd)
|
|
766
|
+
try:
|
|
767
|
+
target = os.fstat(log_fd)
|
|
768
|
+
if not stat.S_ISREG(target.st_mode):
|
|
769
|
+
raise OSError("adapter log is not a regular file")
|
|
770
|
+
os.fchmod(log_fd, 0o600)
|
|
771
|
+
while True:
|
|
772
|
+
chunk = sys.stdin.buffer.read(65536)
|
|
773
|
+
if not chunk:
|
|
774
|
+
break
|
|
775
|
+
view = memoryview(chunk)
|
|
776
|
+
while view:
|
|
777
|
+
view = view[os.write(log_fd, view):]
|
|
778
|
+
finally:
|
|
779
|
+
os.close(log_fd)
|
|
780
|
+
finally:
|
|
781
|
+
os.close(dir_fd)
|
|
782
|
+
except OSError as error:
|
|
783
|
+
print(f"adapter log retention refused: {error}", file=sys.stderr)
|
|
784
|
+
raise SystemExit(2)
|
|
785
|
+
' "$RESULT_DIR" "$RESULT_DIR_IDENTITY"
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
write_result_receipt() {
|
|
789
|
+
"$PY" -c '
|
|
790
|
+
import os, secrets, stat, sys
|
|
791
|
+
|
|
792
|
+
path, expected = sys.argv[1:]
|
|
793
|
+
dir_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
794
|
+
dir_fd = None
|
|
795
|
+
receipt_fd = None
|
|
796
|
+
tmp_name = None
|
|
797
|
+
try:
|
|
798
|
+
data = sys.stdin.buffer.read()
|
|
799
|
+
dir_fd = os.open(path, dir_flags)
|
|
800
|
+
directory = os.fstat(dir_fd)
|
|
801
|
+
if not stat.S_ISDIR(directory.st_mode):
|
|
802
|
+
raise OSError("result dir is not a directory")
|
|
803
|
+
if f"{directory.st_dev}:{directory.st_ino}" != expected:
|
|
804
|
+
raise OSError("result dir identity changed during route")
|
|
805
|
+
file_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
806
|
+
for _ in range(128):
|
|
807
|
+
candidate = f".result-{os.getpid()}-{secrets.token_hex(8)}"
|
|
808
|
+
try:
|
|
809
|
+
receipt_fd = os.open(candidate, file_flags, 0o600, dir_fd=dir_fd)
|
|
810
|
+
tmp_name = candidate
|
|
811
|
+
break
|
|
812
|
+
except FileExistsError:
|
|
813
|
+
continue
|
|
814
|
+
if receipt_fd is None:
|
|
815
|
+
raise OSError("could not reserve a result receipt temporary file")
|
|
816
|
+
target = os.fstat(receipt_fd)
|
|
817
|
+
if not stat.S_ISREG(target.st_mode):
|
|
818
|
+
raise OSError("result receipt temporary file is not regular")
|
|
819
|
+
os.fchmod(receipt_fd, 0o600)
|
|
820
|
+
view = memoryview(data)
|
|
821
|
+
while view:
|
|
822
|
+
view = view[os.write(receipt_fd, view):]
|
|
823
|
+
os.close(receipt_fd)
|
|
824
|
+
receipt_fd = None
|
|
825
|
+
os.replace(
|
|
826
|
+
tmp_name,
|
|
827
|
+
"implementation-result.json",
|
|
828
|
+
src_dir_fd=dir_fd,
|
|
829
|
+
dst_dir_fd=dir_fd,
|
|
830
|
+
)
|
|
831
|
+
tmp_name = None
|
|
832
|
+
except OSError as error:
|
|
833
|
+
print(f"result receipt publication refused: {error}", file=sys.stderr)
|
|
834
|
+
raise SystemExit(2)
|
|
835
|
+
finally:
|
|
836
|
+
if receipt_fd is not None:
|
|
837
|
+
os.close(receipt_fd)
|
|
838
|
+
if tmp_name is not None and dir_fd is not None:
|
|
839
|
+
try:
|
|
840
|
+
os.unlink(tmp_name, dir_fd=dir_fd)
|
|
841
|
+
except OSError:
|
|
842
|
+
pass
|
|
843
|
+
if dir_fd is not None:
|
|
844
|
+
os.close(dir_fd)
|
|
845
|
+
' "$RESULT_DIR" "$RESULT_DIR_IDENTITY"
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
# Read the packet once through a no-follow descriptor, hash those exact bytes,
|
|
849
|
+
# and build the prompt from the private snapshot. The controller-provided
|
|
850
|
+
# digest is therefore bound to the content that actually crosses the route.
|
|
851
|
+
OBSERVED_PACKET_DIGEST="$("$PY" - "$PACKET" "$PACKET_SNAPSHOT" "$MAX_PACKET_BYTES" <<'PY'
|
|
852
|
+
import hashlib, os, stat, sys
|
|
853
|
+
|
|
854
|
+
source, snapshot, raw_cap = sys.argv[1:]
|
|
855
|
+
cap = int(raw_cap)
|
|
856
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
857
|
+
fd = os.open(source, flags)
|
|
858
|
+
try:
|
|
859
|
+
info = os.fstat(fd)
|
|
860
|
+
if not stat.S_ISREG(info.st_mode):
|
|
861
|
+
raise OSError("unit packet is not a regular file")
|
|
862
|
+
chunks, total = [], 0
|
|
863
|
+
while True:
|
|
864
|
+
chunk = os.read(fd, min(65536, cap + 1 - total))
|
|
865
|
+
if not chunk:
|
|
866
|
+
break
|
|
867
|
+
chunks.append(chunk)
|
|
868
|
+
total += len(chunk)
|
|
869
|
+
if total > cap:
|
|
870
|
+
raise OSError(f"unit packet exceeds {cap} bytes")
|
|
871
|
+
finally:
|
|
872
|
+
os.close(fd)
|
|
873
|
+
data = b"".join(chunks)
|
|
874
|
+
out = os.open(snapshot, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
875
|
+
try:
|
|
876
|
+
view = memoryview(data)
|
|
877
|
+
while view:
|
|
878
|
+
written = os.write(out, view)
|
|
879
|
+
view = view[written:]
|
|
880
|
+
finally:
|
|
881
|
+
os.close(out)
|
|
882
|
+
print(hashlib.sha256(data).hexdigest())
|
|
883
|
+
PY
|
|
884
|
+
)" || { log "unit packet could not be snapshotted safely"; exit 2; }
|
|
885
|
+
[ "$OBSERVED_PACKET_DIGEST" = "$EXPECTED_PACKET_DIGEST" ] || {
|
|
886
|
+
log "unit packet digest mismatch (expected $EXPECTED_PACKET_DIGEST, observed $OBSERVED_PACKET_DIGEST)"
|
|
887
|
+
exit 2
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
redact_stream() {
|
|
891
|
+
CE_WORK_REDACT_FILE="${CE_WORK_REDACT_FILE:-}" "$PY" -c '
|
|
892
|
+
import json, os, sys
|
|
893
|
+
|
|
894
|
+
# The ACP stream is JSON, so a value is also matched in its escaped forms.
|
|
895
|
+
def forms(value):
|
|
896
|
+
yield value
|
|
897
|
+
try:
|
|
898
|
+
text = value.decode("utf-8")
|
|
899
|
+
except UnicodeDecodeError:
|
|
900
|
+
return
|
|
901
|
+
yield json.dumps(text, ensure_ascii=False)[1:-1].encode("utf-8")
|
|
902
|
+
yield json.dumps(text)[1:-1].encode("ascii")
|
|
903
|
+
|
|
904
|
+
p = os.environ.get("CE_WORK_REDACT_FILE", "")
|
|
905
|
+
if p:
|
|
906
|
+
try:
|
|
907
|
+
values = sorted(
|
|
908
|
+
{form for v in open(p, "rb").read().splitlines() if v for form in forms(v)},
|
|
909
|
+
key=lambda value: (-len(value), value),
|
|
910
|
+
)
|
|
911
|
+
except OSError:
|
|
912
|
+
values = []
|
|
913
|
+
else:
|
|
914
|
+
values = []
|
|
915
|
+
|
|
916
|
+
def emit(data):
|
|
917
|
+
while data:
|
|
918
|
+
written = os.write(sys.stdout.fileno(), data)
|
|
919
|
+
data = data[written:]
|
|
920
|
+
|
|
921
|
+
pending = b""
|
|
922
|
+
max_value_bytes = max((len(value) for value in values), default=1)
|
|
923
|
+
try:
|
|
924
|
+
if not values:
|
|
925
|
+
while True:
|
|
926
|
+
chunk = os.read(sys.stdin.fileno(), 65536)
|
|
927
|
+
if not chunk:
|
|
928
|
+
break
|
|
929
|
+
emit(chunk)
|
|
930
|
+
sys.exit(0)
|
|
931
|
+
|
|
932
|
+
while True:
|
|
933
|
+
chunk = os.read(sys.stdin.fileno(), 65536)
|
|
934
|
+
if not chunk:
|
|
935
|
+
break
|
|
936
|
+
pending += chunk
|
|
937
|
+
offset = 0
|
|
938
|
+
output = bytearray()
|
|
939
|
+
while len(pending) - offset >= max_value_bytes:
|
|
940
|
+
match = next((value for value in values if pending.startswith(value, offset)), None)
|
|
941
|
+
if match is not None:
|
|
942
|
+
output.extend(b"[REDACTED]")
|
|
943
|
+
offset += len(match)
|
|
944
|
+
else:
|
|
945
|
+
output.append(pending[offset])
|
|
946
|
+
offset += 1
|
|
947
|
+
emit(bytes(output))
|
|
948
|
+
pending = pending[offset:]
|
|
949
|
+
offset = 0
|
|
950
|
+
output = bytearray()
|
|
951
|
+
while offset < len(pending):
|
|
952
|
+
match = next((value for value in values if pending.startswith(value, offset)), None)
|
|
953
|
+
if match is not None:
|
|
954
|
+
output.extend(b"[REDACTED]")
|
|
955
|
+
offset += len(match)
|
|
956
|
+
else:
|
|
957
|
+
output.append(pending[offset])
|
|
958
|
+
offset += 1
|
|
959
|
+
emit(bytes(output))
|
|
960
|
+
except BrokenPipeError:
|
|
961
|
+
os._exit(0)
|
|
962
|
+
'
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
cap_stream() {
|
|
966
|
+
"$PY" -c '
|
|
967
|
+
import os, sys
|
|
968
|
+
|
|
969
|
+
remaining = int(sys.argv[1])
|
|
970
|
+
while True:
|
|
971
|
+
chunk = os.read(sys.stdin.fileno(), 65536)
|
|
972
|
+
if not chunk:
|
|
973
|
+
break
|
|
974
|
+
if remaining:
|
|
975
|
+
retained = chunk[:remaining]
|
|
976
|
+
while retained:
|
|
977
|
+
written = os.write(sys.stdout.fileno(), retained)
|
|
978
|
+
retained = retained[written:]
|
|
979
|
+
remaining -= min(len(chunk), remaining)
|
|
980
|
+
' "$MAX_RAW_BYTES"
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
{
|
|
984
|
+
cat "$PERSONA"
|
|
985
|
+
if [ "$ROUTE" = codex ]; then
|
|
986
|
+
printf '\n\nSocket binds, OS permission checks, peer credentials, and similar capability probes are host-owned. Preserve the host command and observed result; do not treat a sandbox EPERM as proof the host lacks the capability.\n'
|
|
987
|
+
fi
|
|
988
|
+
printf '\n\nThe required final-result JSON schema is:\n\n'
|
|
989
|
+
cat "$SCHEMA"
|
|
990
|
+
printf '\n\n--- BOUNDED IMPLEMENTATION UNIT PACKET ---\n\n'
|
|
991
|
+
redact_stream < "$PACKET_SNAPSHOT"
|
|
992
|
+
} > "$PROMPT_FILE"
|
|
993
|
+
chmod 600 "$PROMPT_FILE"
|
|
994
|
+
|
|
995
|
+
TARGET="$AUTH_TARGET"
|
|
996
|
+
HARNESS="$AUTH_HARNESS"
|
|
997
|
+
|
|
998
|
+
publish_unavailable() {
|
|
999
|
+
local reason terminal_status="${2:-unavailable}" actual_route="${3:-}"
|
|
1000
|
+
# Failure evidence can quote the stream, so it is redacted like the log.
|
|
1001
|
+
reason="$(printf '%s' "$1" | redact_stream)"
|
|
1002
|
+
if [ "$LOG_RETAINED" -ne 1 ]; then
|
|
1003
|
+
printf '%s\n' "$reason" | write_adapter_log || {
|
|
1004
|
+
log "result dir or adapter log identity changed during route"
|
|
1005
|
+
exit 2
|
|
1006
|
+
}
|
|
1007
|
+
LOG_RETAINED=1
|
|
1008
|
+
fi
|
|
1009
|
+
"$PY" - "$ROUTE" "$TARGET" "$HARNESS" "$MODEL_REQUESTED" "$EXPECTED_PACKET_DIGEST" "$LOG_FILE" "$reason" "$ACTIVITY_POSTURE" "$RESTRICTION_POSTURE" "$terminal_status" "$actual_route" "$EFFORT_REQUESTED" <<'PY' | write_result_receipt
|
|
1010
|
+
import json, sys
|
|
1011
|
+
route, target, harness, requested, packet_digest, log, reason, activity, restriction, terminal_status, actual_route, effort = sys.argv[1:]
|
|
1012
|
+
value = {
|
|
1013
|
+
"schema_version": 1, "terminal_status": terminal_status,
|
|
1014
|
+
"summary": "External route failed after launch" if terminal_status == "failed" else "External route unavailable",
|
|
1015
|
+
"changed_files": [], "evidence": [], "scope_expansion": None,
|
|
1016
|
+
"requested_route": route, "actual_route": actual_route or None, "target": target, "harness": harness,
|
|
1017
|
+
"intermediaries": ["cursor"] if route in ("composer", "grok-cursor") else [],
|
|
1018
|
+
"model_requested": requested, "model_actual": "unverified", "model_receipt_status": "unverified",
|
|
1019
|
+
"effort_requested": effort or None,
|
|
1020
|
+
"packet_digest": packet_digest,
|
|
1021
|
+
"activity_posture": activity, "restriction_posture": restriction,
|
|
1022
|
+
"failure_reason": reason, "raw_log": log,
|
|
1023
|
+
}
|
|
1024
|
+
json.dump(value, sys.stdout, indent=2)
|
|
1025
|
+
sys.stdout.write("\n")
|
|
1026
|
+
PY
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if [ "${CE_WORK_REQUIRE_ENFORCED_CONFINEMENT:-}" = "1" ] && [ "$RESTRICTION_POSTURE" != adapter-enforced ]; then
|
|
1030
|
+
publish_unavailable "route offers cooperative workspace restriction, not required enforceable confinement" || exit 2
|
|
1031
|
+
exit 2
|
|
1032
|
+
fi
|
|
1033
|
+
|
|
1034
|
+
validate_effort_override "$ROUTE" || {
|
|
1035
|
+
publish_unavailable "effort override '$EFFORT_REQUESTED' not compatible with route '$ROUTE'" || exit 2
|
|
1036
|
+
exit 2
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
# The Codex desktop app (Codex.app, or ChatGPT.app since the July 2026 merger)
|
|
1040
|
+
# ships `codex` at Contents/Resources without linking it onto PATH (#1272).
|
|
1041
|
+
# Append, never prepend, so a PATH-installed CLI stays authoritative.
|
|
1042
|
+
# CROSS_MODEL_CODEX_APP_DIRS (colon-separated) overrides the probed dirs.
|
|
1043
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
1044
|
+
OLDIFS="$IFS"; IFS=':'
|
|
1045
|
+
for d in ${CROSS_MODEL_CODEX_APP_DIRS-"${HOME:-}/Applications/ChatGPT.app/Contents/Resources:/Applications/ChatGPT.app/Contents/Resources:${HOME:-}/Applications/Codex.app/Contents/Resources:/Applications/Codex.app/Contents/Resources"}; do
|
|
1046
|
+
if [ -n "$d" ] && [ -x "$d/codex" ]; then PATH="${PATH:+$PATH:}$d"; export PATH; break; fi
|
|
1047
|
+
done
|
|
1048
|
+
IFS="$OLDIFS"
|
|
1049
|
+
fi
|
|
1050
|
+
|
|
1051
|
+
# Nothing is sent to a provider before this point.
|
|
1052
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
1053
|
+
publish_unavailable "transport unavailable (pre-egress, shared): jq not found; the ACP stream is read with jq" || exit 2
|
|
1054
|
+
exit 2
|
|
1055
|
+
fi
|
|
1056
|
+
acpx_preflight "$ROUTE" "$WORKSPACE" || {
|
|
1057
|
+
publish_unavailable "transport unavailable (pre-egress, $ACPX_SCOPE): $ACPX_UNAVAILABLE" || exit 2
|
|
1058
|
+
exit 2
|
|
1059
|
+
}
|
|
1060
|
+
if [ "$ROUTE" = claude ]; then
|
|
1061
|
+
CLAUDE_WRAPPER="$(acpx_claude_wrapper "$SCRATCH")" || {
|
|
1062
|
+
publish_unavailable "transport unavailable (pre-egress, route): cannot prepare the Claude --safe-mode launcher" || exit 2
|
|
1063
|
+
exit 2
|
|
1064
|
+
}
|
|
1065
|
+
fi
|
|
1066
|
+
if [ "$ROUTE" = antigravity ]; then
|
|
1067
|
+
GEMINI_HOME_DIR="$(acpx_antigravity_home "$SCRATCH")" || {
|
|
1068
|
+
publish_unavailable "transport unavailable (pre-egress, route): cannot prepare a private Antigravity home" || exit 2
|
|
1069
|
+
exit 2
|
|
1070
|
+
}
|
|
1071
|
+
fi
|
|
1072
|
+
printf '{"mcpServers":[]}\n' > "$MCP_CONFIG"
|
|
1073
|
+
|
|
1074
|
+
ARGS=()
|
|
1075
|
+
while IFS= read -r -d '' token; do ARGS+=("$token"); done < <(adapter_argv "$ROUTE")
|
|
1076
|
+
|
|
1077
|
+
MIN_ENV=(env -i "PATH=$PATH" "PYTHONDONTWRITEBYTECODE=1")
|
|
1078
|
+
[ -n "${HOME:-}" ] && MIN_ENV+=("HOME=$HOME")
|
|
1079
|
+
[ -n "${USER:-}" ] && MIN_ENV+=("USER=$USER")
|
|
1080
|
+
[ -n "${TMPDIR:-}" ] && MIN_ENV+=("TMPDIR=$TMPDIR")
|
|
1081
|
+
[ -n "${LANG:-}" ] && MIN_ENV+=("LANG=$LANG")
|
|
1082
|
+
[ -n "${LC_ALL:-}" ] && MIN_ENV+=("LC_ALL=$LC_ALL")
|
|
1083
|
+
[ -n "${XDG_CONFIG_HOME:-}" ] && MIN_ENV+=("XDG_CONFIG_HOME=$XDG_CONFIG_HOME")
|
|
1084
|
+
# npx reaches the user's npm cache, registry, and user config by these
|
|
1085
|
+
# locations. npm auth token values are never forwarded; npm reads its own
|
|
1086
|
+
# config file for them.
|
|
1087
|
+
for name in npm_config_cache NPM_CONFIG_CACHE npm_config_registry NPM_CONFIG_REGISTRY npm_config_userconfig NPM_CONFIG_USERCONFIG; do
|
|
1088
|
+
[ -n "${!name:-}" ] && MIN_ENV+=("$name=${!name}")
|
|
1089
|
+
done
|
|
1090
|
+
# Preserve route-specific config-directory pointers so existing CLI-native login
|
|
1091
|
+
# remains reachable. Credential-bearing API-key variables are intentionally not
|
|
1092
|
+
# forwarded; the worker gets paths to the CLI's own auth store, not secrets.
|
|
1093
|
+
case "$ROUTE" in
|
|
1094
|
+
codex) [ -n "${CODEX_HOME:-}" ] && MIN_ENV+=("CODEX_HOME=$CODEX_HOME") ;;
|
|
1095
|
+
claude) [ -n "${CLAUDE_CONFIG_DIR:-}" ] && MIN_ENV+=("CLAUDE_CONFIG_DIR=$CLAUDE_CONFIG_DIR") ;;
|
|
1096
|
+
grok-cli) [ -n "${GROK_CONFIG_HOME:-}" ] && MIN_ENV+=("GROK_CONFIG_HOME=$GROK_CONFIG_HOME") ;;
|
|
1097
|
+
opencode)
|
|
1098
|
+
[ -n "${OPENCODE_CONFIG_DIR:-}" ] && MIN_ENV+=("OPENCODE_CONFIG_DIR=$OPENCODE_CONFIG_DIR")
|
|
1099
|
+
[ -n "${OPENCODE_CONFIG:-}" ] && MIN_ENV+=("OPENCODE_CONFIG=$OPENCODE_CONFIG")
|
|
1100
|
+
;;
|
|
1101
|
+
cursor|composer|grok-cursor)
|
|
1102
|
+
[ -n "${CURSOR_CONFIG_DIR:-}" ] && MIN_ENV+=("CURSOR_CONFIG_DIR=$CURSOR_CONFIG_DIR")
|
|
1103
|
+
;;
|
|
1104
|
+
esac
|
|
1105
|
+
|
|
1106
|
+
ACTIVITY_POLL_SECS="${CE_WORK_ACTIVITY_POLL_SECS:-15}"
|
|
1107
|
+
case "$ACTIVITY_POLL_SECS" in ''|*[!0-9]*) ACTIVITY_POLL_SECS=15 ;; esac
|
|
1108
|
+
[ "$ACTIVITY_POLL_SECS" -lt 1 ] && ACTIVITY_POLL_SECS=1
|
|
1109
|
+
MAX_RAW_BYTES="${CE_WORK_MAX_RAW_BYTES:-10485760}"
|
|
1110
|
+
case "$MAX_RAW_BYTES" in ''|*[!0-9]*) MAX_RAW_BYTES=10485760 ;; esac
|
|
1111
|
+
[ "$MAX_RAW_BYTES" -lt 1 ] && MAX_RAW_BYTES=10485760
|
|
1112
|
+
|
|
1113
|
+
raw_byte_count() {
|
|
1114
|
+
local total=0 bytes file
|
|
1115
|
+
for file in "$RAW_STDOUT" "$RAW_STDERR"; do
|
|
1116
|
+
[ -f "$file" ] || continue
|
|
1117
|
+
bytes="$(wc -c < "$file" | tr -d '[:space:]')"
|
|
1118
|
+
case "$bytes" in ''|*[!0-9]*) bytes=0 ;; esac
|
|
1119
|
+
total=$((total + bytes))
|
|
1120
|
+
done
|
|
1121
|
+
printf '%s' "$total"
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
ACTIVE_ROUTE_PID=""
|
|
1125
|
+
ACTIVITY_PID=""
|
|
1126
|
+
# The route runs in its own process group (set -m below). npm launches acpx through
|
|
1127
|
+
# `sh -c`, and a shell that does not exec its command (Ubuntu's dash) would stop a
|
|
1128
|
+
# leader-only TERM short of acpx and the agent, so signal the whole group.
|
|
1129
|
+
stop_route() {
|
|
1130
|
+
kill -TERM -- -"$1" 2>/dev/null || kill -TERM "$1" 2>/dev/null || true
|
|
1131
|
+
}
|
|
1132
|
+
terminate_route() {
|
|
1133
|
+
[ -n "$ACTIVITY_PID" ] && kill "$ACTIVITY_PID" 2>/dev/null || true
|
|
1134
|
+
[ -n "$ACTIVE_ROUTE_PID" ] && stop_route "$ACTIVE_ROUTE_PID"
|
|
1135
|
+
[ -n "$ACTIVE_ROUTE_PID" ] && wait "$ACTIVE_ROUTE_PID" 2>/dev/null || true
|
|
1136
|
+
rm -rf "$SCRATCH"
|
|
1137
|
+
exit 143
|
|
1138
|
+
}
|
|
1139
|
+
trap 'terminate_route' TERM INT
|
|
1140
|
+
|
|
1141
|
+
set +e
|
|
1142
|
+
# npx resolves packages from its working directory's node_modules and .npmrc first;
|
|
1143
|
+
# acpx gets the agent's cwd from --cwd, so start it from private scratch.
|
|
1144
|
+
launch_route() {
|
|
1145
|
+
set -m
|
|
1146
|
+
(cd "$SCRATCH" && exec "${MIN_ENV[@]}" "${ARGS[@]}" < /dev/null > "$RAW_STDOUT" 2> "$RAW_STDERR") &
|
|
1147
|
+
ACTIVE_ROUTE_PID=$!
|
|
1148
|
+
set +m
|
|
1149
|
+
(
|
|
1150
|
+
# A foreground sleep would outlive this subshell's TERM and hold the script's
|
|
1151
|
+
# output open, so callers (bun 1.4 spawnSync) would wait out the poll interval.
|
|
1152
|
+
# Kill every background job, so a TERM between `sleep &` and `$!` still reaps it.
|
|
1153
|
+
trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM
|
|
1154
|
+
previous=0
|
|
1155
|
+
session=open
|
|
1156
|
+
[ "$ROUTE" = antigravity ] && session=starting
|
|
1157
|
+
started="$(date +%s)"
|
|
1158
|
+
while kill -0 "$ACTIVE_ROUTE_PID" 2>/dev/null; do
|
|
1159
|
+
current="$(raw_byte_count)"
|
|
1160
|
+
if [ "$current" -gt "$MAX_RAW_BYTES" ]; then
|
|
1161
|
+
: > "$RAW_LIMIT_MARKER"
|
|
1162
|
+
log "activity route=$ROUTE raw-output-limit bytes=$current cap=$MAX_RAW_BYTES"
|
|
1163
|
+
stop_route "$ACTIVE_ROUTE_PID"
|
|
1164
|
+
break
|
|
1165
|
+
fi
|
|
1166
|
+
if [ "$session" != open ] && [ $(( $(date +%s) - started )) -ge "$ACPX_SESSION_START_SECS" ]; then
|
|
1167
|
+
session="$(acpx_session_state "$RAW_STDOUT")"
|
|
1168
|
+
if [ "$session" = pending ]; then
|
|
1169
|
+
: > "$SESSION_STALL_MARKER"
|
|
1170
|
+
log "Antigravity opened no session within ${ACPX_SESSION_START_SECS}s; stopping the route"
|
|
1171
|
+
stop_route "$ACTIVE_ROUTE_PID"
|
|
1172
|
+
break
|
|
1173
|
+
fi
|
|
1174
|
+
fi
|
|
1175
|
+
if [ "$current" != "$previous" ]; then
|
|
1176
|
+
log "activity route=$ROUTE output-updated"
|
|
1177
|
+
previous="$current"
|
|
1178
|
+
fi
|
|
1179
|
+
sleep "$ACTIVITY_POLL_SECS" &
|
|
1180
|
+
wait $!
|
|
1181
|
+
done
|
|
1182
|
+
) &
|
|
1183
|
+
ACTIVITY_PID=$!
|
|
1184
|
+
wait "$ACTIVE_ROUTE_PID"
|
|
1185
|
+
ROUTE_EXIT=$?
|
|
1186
|
+
kill "$ACTIVITY_PID" 2>/dev/null || true
|
|
1187
|
+
wait "$ACTIVITY_PID" 2>/dev/null || true
|
|
1188
|
+
ACTIVE_ROUTE_PID=""
|
|
1189
|
+
ACTIVITY_PID=""
|
|
1190
|
+
}
|
|
1191
|
+
LAUNCH_STARTED="$(date +%s)"
|
|
1192
|
+
launch_route
|
|
1193
|
+
if acpx_stale_npm_metadata "$RAW_STDERR" "$RAW_STDOUT" && [ "$(acpx_outcome "$RAW_STDOUT")" = not-sent ]; then
|
|
1194
|
+
REMAINING=$(( ACPX_TIMEOUT - ( $(date +%s) - LAUNCH_STARTED ) ))
|
|
1195
|
+
if [ "$REMAINING" -gt 0 ]; then
|
|
1196
|
+
log "npm resolved against stale cached package metadata; retrying the same route once with fresh metadata in the ${REMAINING}s left (nothing was sent)"
|
|
1197
|
+
ACPX_NPM_REFRESH=true
|
|
1198
|
+
ACPX_TIMEOUT="$REMAINING"
|
|
1199
|
+
ARGS=()
|
|
1200
|
+
while IFS= read -r -d '' token; do ARGS+=("$token"); done < <(adapter_argv "$ROUTE")
|
|
1201
|
+
launch_route
|
|
1202
|
+
fi
|
|
1203
|
+
fi
|
|
1204
|
+
RAW_BYTES="$(raw_byte_count)"
|
|
1205
|
+
[ "$RAW_BYTES" -gt "$MAX_RAW_BYTES" ] && : > "$RAW_LIMIT_MARKER"
|
|
1206
|
+
# acpx echoes the outbound prompt and the contents of files the agent reads, so
|
|
1207
|
+
# everything retained or published comes from the redacted copy. The protocol is
|
|
1208
|
+
# parsed from the private raw stream: a redaction value can collide with ACP text.
|
|
1209
|
+
redact_stream < "$RAW_STDOUT" > "$STREAM"
|
|
1210
|
+
{
|
|
1211
|
+
cat "$STREAM"
|
|
1212
|
+
redact_stream < "$RAW_STDERR"
|
|
1213
|
+
} | cap_stream | write_adapter_log || {
|
|
1214
|
+
log "result dir or adapter log identity changed during route"
|
|
1215
|
+
exit 2
|
|
1216
|
+
}
|
|
1217
|
+
LOG_RETAINED=1
|
|
1218
|
+
|
|
1219
|
+
if [ -f "$RAW_LIMIT_MARKER" ]; then
|
|
1220
|
+
publish_unavailable "fixed route raw output exceeded ${MAX_RAW_BYTES} bytes" || exit 2
|
|
1221
|
+
exit 1
|
|
1222
|
+
fi
|
|
1223
|
+
|
|
1224
|
+
# The run's outcome is its own prompt's result, not acpx's exit code.
|
|
1225
|
+
OUTCOME="$(acpx_outcome "$RAW_STDOUT")"
|
|
1226
|
+
case "$OUTCOME" in
|
|
1227
|
+
end_turn) ;;
|
|
1228
|
+
not-sent)
|
|
1229
|
+
# Nothing reached the provider: npm could not fetch acpx (every route fails
|
|
1230
|
+
# alike), or the adapter refused before the prompt (this route only).
|
|
1231
|
+
REASON="$(grep -m1 '^npm error' "$RAW_STDERR" 2>/dev/null)"
|
|
1232
|
+
if [ -n "$REASON" ]; then
|
|
1233
|
+
REASON="transport unavailable (pre-egress, shared): ${REASON:0:200}"
|
|
1234
|
+
elif [ -f "$SESSION_STALL_MARKER" ]; then
|
|
1235
|
+
REASON="$SIGN_IN_STALL_REASON"
|
|
1236
|
+
else
|
|
1237
|
+
node_line="$(acpx_node_startup_line "$RAW_STDERR")"
|
|
1238
|
+
if [ -n "$node_line" ]; then
|
|
1239
|
+
REASON="transport unavailable (pre-egress, route): ${node_line:0:200}"
|
|
1240
|
+
else
|
|
1241
|
+
REASON="$(acpx_failure_evidence "$STREAM")"
|
|
1242
|
+
REASON="transport unavailable (pre-egress, route): ${REASON:-acpx exited $ROUTE_EXIT before sending the prompt}"
|
|
1243
|
+
fi
|
|
1244
|
+
fi
|
|
1245
|
+
publish_unavailable "$REASON" || exit 2
|
|
1246
|
+
exit 2
|
|
1247
|
+
;;
|
|
1248
|
+
*)
|
|
1249
|
+
REASON="$(acpx_permission_cancel_reason "$RAW_STDOUT" "$ROUTE_EXIT")"
|
|
1250
|
+
[ -n "$REASON" ] || REASON="$(acpx_failure_evidence "$STREAM")"
|
|
1251
|
+
publish_unavailable "fixed route ended with $OUTCOME (acpx exit $ROUTE_EXIT)${REASON:+: $REASON}" failed "$ROUTE" || exit 2
|
|
1252
|
+
exit 1
|
|
1253
|
+
;;
|
|
1254
|
+
esac
|
|
1255
|
+
|
|
1256
|
+
# The reply and served models are redacted with the rest of the receipt below.
|
|
1257
|
+
acpx_text "$RAW_STDOUT" "$REPLY_TEXT"
|
|
1258
|
+
acpx_served_models "$RAW_STDOUT" > "$SERVED_MODELS"
|
|
1259
|
+
set +e
|
|
1260
|
+
CE_WORK_REDACT_FILE="${CE_WORK_REDACT_FILE:-}" "$PY" - \
|
|
1261
|
+
"$REPLY_TEXT" "$SERVED_MODELS" "$ROUTE" "$TARGET" "$HARNESS" \
|
|
1262
|
+
"$MODEL_REQUESTED" "$EXPECTED_PACKET_DIGEST" "$LOG_FILE" "$ACTIVITY_POSTURE" "$RESTRICTION_POSTURE" "$EFFORT_REQUESTED" \
|
|
1263
|
+
"$(acpx_reply_evidence "$RAW_STDOUT")" "$(acpx_advertised_models "$RAW_STDOUT")" <<'PY' | write_result_receipt
|
|
1264
|
+
import json, os, re, sys
|
|
1265
|
+
reply, served_models, route, target, harness, requested, packet_digest, log, activity, restriction, effort, reply_evidence, advertised = sys.argv[1:]
|
|
1266
|
+
|
|
1267
|
+
def redactions():
|
|
1268
|
+
p=os.environ.get("CE_WORK_REDACT_FILE", "")
|
|
1269
|
+
if not p: return []
|
|
1270
|
+
try: return sorted(set(v for v in open(p, encoding="utf-8").read().splitlines() if v), key=lambda value: (-len(value), value))
|
|
1271
|
+
except OSError: return []
|
|
1272
|
+
|
|
1273
|
+
redaction_values=redactions()
|
|
1274
|
+
|
|
1275
|
+
def redact(value):
|
|
1276
|
+
if isinstance(value,str):
|
|
1277
|
+
for secret in redaction_values: value=value.replace(secret, "[REDACTED]")
|
|
1278
|
+
return value
|
|
1279
|
+
if isinstance(value,list): return [redact(child) for child in value]
|
|
1280
|
+
if isinstance(value,dict): return {key:redact(child) for key,child in value.items()}
|
|
1281
|
+
return value
|
|
1282
|
+
|
|
1283
|
+
def parse_text(text):
|
|
1284
|
+
found=[]
|
|
1285
|
+
decoder=json.JSONDecoder()
|
|
1286
|
+
def inspect(value):
|
|
1287
|
+
if isinstance(value,dict):
|
|
1288
|
+
if all(k in value for k in ("terminal_status","summary","changed_files","evidence","scope_expansion")):
|
|
1289
|
+
found.append(value)
|
|
1290
|
+
for child in value.values(): inspect(child)
|
|
1291
|
+
elif isinstance(value,list):
|
|
1292
|
+
for child in value: inspect(child)
|
|
1293
|
+
elif isinstance(value,str):
|
|
1294
|
+
inner=re.sub(r"^```(?:json)?\s*|\s*```$", "", value.strip(), flags=re.S)
|
|
1295
|
+
for i,ch in enumerate(inner):
|
|
1296
|
+
if ch not in "[{": continue
|
|
1297
|
+
try:
|
|
1298
|
+
child,_=decoder.raw_decode(inner,i); inspect(child)
|
|
1299
|
+
except Exception: pass
|
|
1300
|
+
inspect(text)
|
|
1301
|
+
for line in text.splitlines():
|
|
1302
|
+
try: inspect(json.loads(line))
|
|
1303
|
+
except Exception: pass
|
|
1304
|
+
return found[-1] if found else None
|
|
1305
|
+
|
|
1306
|
+
def normalize_served_model(value):
|
|
1307
|
+
# Strip terminal control sequences before validating the receipt token;
|
|
1308
|
+
# never publish a partially sanitized or otherwise unsafe identity.
|
|
1309
|
+
text=str(value)
|
|
1310
|
+
text=re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
|
|
1311
|
+
text=re.sub(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", "", text)
|
|
1312
|
+
text="".join(ch for ch in text if ord(ch) >= 0x20 and ord(ch) != 0x7f).strip()
|
|
1313
|
+
return text if 0 < len(text) <= 128 else "unverified"
|
|
1314
|
+
|
|
1315
|
+
try: text=open(reply, encoding="utf-8", errors="replace").read()
|
|
1316
|
+
except OSError: text=""
|
|
1317
|
+
worker=parse_text(text)
|
|
1318
|
+
valid=isinstance(worker,dict)
|
|
1319
|
+
worker_fields=("terminal_status", "summary", "changed_files", "evidence", "scope_expansion")
|
|
1320
|
+
if valid:
|
|
1321
|
+
valid=(set(worker) == set(worker_fields)
|
|
1322
|
+
and worker.get("terminal_status") in ("completed","blocked","scope_expansion")
|
|
1323
|
+
and isinstance(worker.get("summary"),str) and bool(worker["summary"])
|
|
1324
|
+
and isinstance(worker.get("changed_files"),list) and all(isinstance(x,str) and x for x in worker["changed_files"])
|
|
1325
|
+
and isinstance(worker.get("evidence"),list) and all(isinstance(x,str) and x for x in worker["evidence"])
|
|
1326
|
+
and ((worker["terminal_status"]=="scope_expansion" and isinstance(worker.get("scope_expansion"),dict))
|
|
1327
|
+
or (worker["terminal_status"]!="scope_expansion" and worker.get("scope_expansion") is None)))
|
|
1328
|
+
|
|
1329
|
+
# The adapter's _meta report is its own assertion of the served model, so a
|
|
1330
|
+
# match is recorded as "asserted", never "verified". Requested and served ids
|
|
1331
|
+
# are compared on the model id before any Cursor preset bracket; an alias
|
|
1332
|
+
# names its Claude family. An auxiliary model (Claude's Haiku) may also be
|
|
1333
|
+
# reported, so a served id in the requested family wins, else the heaviest.
|
|
1334
|
+
served=[]
|
|
1335
|
+
try:
|
|
1336
|
+
for line in open(served_models, encoding="utf-8", errors="replace").read().splitlines():
|
|
1337
|
+
model, _, tokens = line.rpartition(" ")
|
|
1338
|
+
model=normalize_served_model(model)
|
|
1339
|
+
if model != "unverified":
|
|
1340
|
+
served.append((model, int(tokens) if tokens.isdigit() else 0))
|
|
1341
|
+
except OSError:
|
|
1342
|
+
pass
|
|
1343
|
+
model_base=lambda value: value.split("[",1)[0].lower()
|
|
1344
|
+
family=model_base(requested)
|
|
1345
|
+
if family in ("fable","opus","sonnet","haiku"): family="claude-"+family
|
|
1346
|
+
in_family=[m for m,_ in served if model_base(m)==family or model_base(m).startswith(family+"-")]
|
|
1347
|
+
if not served: actual, receipt = "unverified", "unverified"
|
|
1348
|
+
elif requested == "auto": actual, receipt = max(served, key=lambda item: item[1])[0], "asserted"
|
|
1349
|
+
elif in_family: actual, receipt = in_family[0], "asserted"
|
|
1350
|
+
else: actual, receipt = max(served, key=lambda item: item[1])[0], "mismatch"
|
|
1351
|
+
|
|
1352
|
+
intermediaries=["cursor"] if route in ("composer","grok-cursor") else []
|
|
1353
|
+
base_receipt={
|
|
1354
|
+
"schema_version":1,
|
|
1355
|
+
"requested_route":route, "actual_route":route, "target":target, "harness":harness,
|
|
1356
|
+
"intermediaries":intermediaries, "model_requested":requested, "model_actual":actual,
|
|
1357
|
+
"model_receipt_status":receipt, "effort_requested":effort or None, "activity_posture":activity,
|
|
1358
|
+
"packet_digest":packet_digest,
|
|
1359
|
+
"restriction_posture":restriction,
|
|
1360
|
+
"failure_reason":None, "raw_log":log,
|
|
1361
|
+
}
|
|
1362
|
+
if valid:
|
|
1363
|
+
base_receipt.update({key:worker[key] for key in worker_fields})
|
|
1364
|
+
else:
|
|
1365
|
+
base_receipt.update({"terminal_status":"failed", "summary":"Adapter terminal output failed result schema",
|
|
1366
|
+
"changed_files":[], "evidence":[], "scope_expansion":None,
|
|
1367
|
+
"failure_reason":"; ".join(["terminal output failed implementation result schema"]
|
|
1368
|
+
+ ([f"reply: {reply_evidence}"] if reply_evidence else [])
|
|
1369
|
+
+ ([f"advertised models: {advertised}"] if advertised else []))})
|
|
1370
|
+
base_receipt=redact(base_receipt)
|
|
1371
|
+
json.dump(base_receipt,sys.stdout,indent=2)
|
|
1372
|
+
sys.stdout.write("\n")
|
|
1373
|
+
sys.exit(0 if valid else 4)
|
|
1374
|
+
PY
|
|
1375
|
+
NORMALIZE_STATUSES=("${PIPESTATUS[@]}")
|
|
1376
|
+
NORMALIZE_EXIT="${NORMALIZE_STATUSES[0]}"
|
|
1377
|
+
PUBLISH_EXIT="${NORMALIZE_STATUSES[1]}"
|
|
1378
|
+
if [ "$PUBLISH_EXIT" -ne 0 ]; then exit 2; fi
|
|
1379
|
+
if [ "$NORMALIZE_EXIT" -ne 0 ]; then exit 1; fi
|
|
1380
|
+
|
|
1381
|
+
TERMINAL_STATUS="$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["terminal_status"])' "$RESULT_FILE")"
|
|
1382
|
+
case "$TERMINAL_STATUS" in
|
|
1383
|
+
completed|blocked|scope_expansion) exit 0 ;;
|
|
1384
|
+
*) exit 1 ;;
|
|
1385
|
+
esac
|