@daniellins/power-claude 0.16.3
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.
Potentially problematic release.
This version of @daniellins/power-claude might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/bin/cli.js +124 -0
- package/installer/__tests__/doctor-ready-oracles.test.cjs +140 -0
- package/installer/__tests__/merge-settings.test.cjs +330 -0
- package/installer/adopt.js +161 -0
- package/installer/copy.js +203 -0
- package/installer/doctor.js +266 -0
- package/installer/install.js +155 -0
- package/installer/manifest.js +102 -0
- package/installer/motor-scripts.js +50 -0
- package/installer/motor-scripts.json +39 -0
- package/installer/uninstall.js +99 -0
- package/installer/update.js +376 -0
- package/installer/util.js +113 -0
- package/installer/wizard.js +64 -0
- package/package.json +26 -0
- package/scripts/__fixtures__/anti-path-demo/agents/ap-writer.md +13 -0
- package/scripts/__fixtures__/anti-path-demo/templates/write-tmpl.md +7 -0
- package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.smoke.json +10 -0
- package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.yaml +25 -0
- package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/agents/content-chief.md +1 -0
- package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/workflows/wf-criacao-redteam.yaml +3 -0
- package/scripts/__fixtures__/assess-tier/retrofit-leve/pod-spec.json +13 -0
- package/scripts/__fixtures__/fidelity/fake-green-average-in-band.yaml +21 -0
- package/scripts/__fixtures__/fidelity/fake-green-average.yaml +18 -0
- package/scripts/__fixtures__/fidelity/fake-green-denominator.yaml +19 -0
- package/scripts/__fixtures__/fidelity/fake-green-missing-evidence.yaml +18 -0
- package/scripts/__fixtures__/fidelity/fake-green-threshold.yaml +18 -0
- package/scripts/__fixtures__/fidelity/fake-green-weight.yaml +18 -0
- package/scripts/__fixtures__/fidelity/golden.yaml +31 -0
- package/scripts/__fixtures__/fidelity/trindade-incoherent.yaml +15 -0
- package/scripts/__fixtures__/forge-canonical/pod-spec-example.json +37 -0
- package/scripts/__fixtures__/migrate-squad/authored/agents/offer-engineer.md +17 -0
- package/scripts/__fixtures__/migrate-squad/authored/knowledge/no-source.md +7 -0
- package/scripts/__fixtures__/migrate-squad/authored/knowledge/value-stack-method.md +8 -0
- package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.smoke.json +24 -0
- package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.spec.json +45 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/HEADLINE.md +3 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/README.md +4 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/agents/hormozi-offer.md +14 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/dead-redundant.md +4 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/method-guide.md +6 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/veto-screen.md +4 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/config/config.yaml +2 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/data/case-library.md +3 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/scripts/validate-mvn.py +16 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/sops/sop-sample.md +3 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/squad-io.yaml +14 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/tasks/gather-context.md +7 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/templates/output-tmpl.md +5 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/workflows/wf-sample.yaml +24 -0
- package/scripts/__fixtures__/pipeline/pipelines/pipeline-checkpoint.yaml +21 -0
- package/scripts/__fixtures__/pipeline/pipelines/pipeline-demo.yaml +22 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/agents/maker.md +13 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.consolidated.md +8 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.jsonl +83 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/templates/make-tmpl.md +8 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/workflows/wf-a.yaml +16 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/agents/user.md +13 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.consolidated.md +8 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.jsonl +14 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/templates/use-tmpl.md +6 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/workflows/wf-b.yaml +16 -0
- package/scripts/__fixtures__/pod-run/agents/forge-chief.md +13 -0
- package/scripts/__fixtures__/pod-run/agents/pod-critic.md +13 -0
- package/scripts/__fixtures__/pod-run/templates/build-tmpl.md +8 -0
- package/scripts/__fixtures__/pod-run/templates/gate-tmpl.md +6 -0
- package/scripts/__fixtures__/pod-run/workflows/wf-min.yaml +43 -0
- package/scripts/__fixtures__/pod-run/workflows/wf-skip-modes.yaml +50 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-all-skippable.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-criteria-escalar-de-bloco.yaml +40 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-skip-mode-desconhecido.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-unknown-key.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/invalid-cycle.yaml +26 -0
- package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-backedge.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-forward.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/invalid-handoff-open.yaml +27 -0
- package/scripts/__fixtures__/pod-workflow/invalid-knowledge-shape.yaml +18 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-agent.yaml +15 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-outputs.yaml +15 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-template.yaml +15 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-veto.yaml +13 -0
- package/scripts/__fixtures__/pod-workflow/invalid-template-not-on-disk.yaml +16 -0
- package/scripts/__fixtures__/pod-workflow/invalid-veto-prose.yaml +17 -0
- package/scripts/__fixtures__/pod-workflow/knowledge/a.md +4 -0
- package/scripts/__fixtures__/pod-workflow/knowledge/b.md +4 -0
- package/scripts/__fixtures__/pod-workflow/knowledge/c.md +4 -0
- package/scripts/__fixtures__/pod-workflow/templates/step-tmpl.md +7 -0
- package/scripts/__fixtures__/pod-workflow/valid-checkpoint-skip-modes.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/valid-fix-loop.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/valid-knowledge.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/valid-veto-always-true.yaml +20 -0
- package/scripts/__fixtures__/pod-workflow/valid.yaml +27 -0
- package/scripts/__fixtures__/smoke-canary/agents/canary-reporter.md +13 -0
- package/scripts/__fixtures__/smoke-canary/agents/canary-researcher.md +13 -0
- package/scripts/__fixtures__/smoke-canary/agents/canary-synth.md +13 -0
- package/scripts/__fixtures__/smoke-canary/memory/learnings.consolidated.md +10 -0
- package/scripts/__fixtures__/smoke-canary/memory/learnings.jsonl +448 -0
- package/scripts/__fixtures__/smoke-canary/templates/report-tmpl.md +9 -0
- package/scripts/__fixtures__/smoke-canary/templates/research-tmpl.md +7 -0
- package/scripts/__fixtures__/smoke-canary/templates/synth-tmpl.md +9 -0
- package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.smoke.json +11 -0
- package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.yaml +53 -0
- package/scripts/__fixtures__/source-audit/fake-green-bronze-unsignaled.yaml +10 -0
- package/scripts/__fixtures__/source-audit/fake-green-counts.yaml +9 -0
- package/scripts/__fixtures__/source-audit/fake-green-missing-evidence.yaml +9 -0
- package/scripts/__fixtures__/source-audit/fake-green-unclassified.yaml +10 -0
- package/scripts/__fixtures__/source-audit/fake-green-verdict.yaml +10 -0
- package/scripts/__fixtures__/source-audit/golden-blocked.yaml +10 -0
- package/scripts/__fixtures__/source-audit/golden.yaml +11 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-count.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-field.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-fileref.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-json.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-section.md +13 -0
- package/scripts/__fixtures__/sweep-forms/specs/wf-forms.spec.json +5 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_count.md +5 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_field.md +3 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_fileref.md +3 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_json.md +0 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_section.md +3 -0
- package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.smoke.json +13 -0
- package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.yaml +67 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-1-noop-state-json.fixture.md +18 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-2-crash-buildworkflowyaml.fixture.md +26 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-3-nasce-verde-skip-modes.fixture.md +29 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-4-guarda-sem-irmao.fixture.md +17 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5a-diff-stat-sem-guarda.fixture.md +17 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5b-test-f-sobre-diretorio.fixture.md +20 -0
- package/scripts/__fixtures__/veto-checks/copy-clean.md +4 -0
- package/scripts/__fixtures__/veto-checks/copy-dashes.md +4 -0
- package/scripts/__fixtures__/veto-checks/n8n-bad.md +18 -0
- package/scripts/__fixtures__/veto-checks/n8n-ok.md +22 -0
- package/scripts/__fixtures__/veto-checks/page-bad.html +7 -0
- package/scripts/__fixtures__/veto-checks/phash/copy.png +0 -0
- package/scripts/__fixtures__/veto-checks/phash/distinct.png +0 -0
- package/scripts/__fixtures__/veto-checks/phash/distinct2.png +0 -0
- package/scripts/__fixtures__/veto-checks/phash/make-fixtures.cjs +203 -0
- package/scripts/__fixtures__/veto-checks/phash/ref.png +0 -0
- package/scripts/__fixtures__/veto-checks/slide-ok.html +15 -0
- package/scripts/__fixtures__/veto-checks/slide-overflow.html +14 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/agents/vc-checker.md +13 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/templates/check-tmpl.md +6 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.smoke.json +10 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.yaml +19 -0
- package/scripts/__fixtures__/voz-do-dono/README.md +23 -0
- package/scripts/__fixtures__/voz-do-dono/dono-escalar.yaml +9 -0
- package/scripts/__fixtures__/voz-do-dono/dono-guia.yaml +10 -0
- package/scripts/__fixtures__/voz-do-dono/peca-3a-pessoa.md +6 -0
- package/scripts/__fixtures__/voz-do-dono/peca-com-lexico-proibido.md +6 -0
- package/scripts/__fixtures__/voz-do-dono/peca-limpa-1a-pessoa.md +6 -0
- package/scripts/__tests__/derive-ready-transitions.test.cjs +269 -0
- package/scripts/__tests__/enforce-cycle-authority.test.cjs +869 -0
- package/scripts/__tests__/enforce-git-push-authority.test.cjs +701 -0
- package/scripts/__tests__/enforce-ready-oracles.test.cjs +745 -0
- package/scripts/__tests__/motor-scripts.test.cjs +339 -0
- package/scripts/__tests__/pc-run-pod-counter.test.cjs +58 -0
- package/scripts/__tests__/pc-run-pod.test.cjs +578 -0
- package/scripts/__tests__/validate-pod-workflow.test.cjs +90 -0
- package/scripts/__tests__/validate-story-acs.test.cjs +607 -0
- package/scripts/anti-path.test.cjs +163 -0
- package/scripts/arena.cjs +514 -0
- package/scripts/arena.test.cjs +213 -0
- package/scripts/brief-leitura-obrigatoria.test.cjs +364 -0
- package/scripts/consolidate-knowledge-runner.test.cjs +249 -0
- package/scripts/default-funcional.test.cjs +153 -0
- package/scripts/derive-ready-transitions.cjs +325 -0
- package/scripts/forge-additive.test.cjs +158 -0
- package/scripts/forge-assess-tier.cjs +205 -0
- package/scripts/forge-assess-tier.test.cjs +114 -0
- package/scripts/forge-canonical.test.cjs +624 -0
- package/scripts/forge-generate.cjs +1303 -0
- package/scripts/forge-harden-learning.cjs +921 -0
- package/scripts/forge-harden-learning.test.cjs +242 -0
- package/scripts/forge-ingest-memory.cjs +136 -0
- package/scripts/forge-upgrade-pod.cjs +368 -0
- package/scripts/forge.test.cjs +609 -0
- package/scripts/harden-knowledge.test.cjs +612 -0
- package/scripts/harness-g5-lite.cjs +243 -0
- package/scripts/harness-g5-lite.test.cjs +134 -0
- package/scripts/hn-carousel-contrato-entrada.test.cjs +96 -0
- package/scripts/migrate-squad-orchestration.test.cjs +146 -0
- package/scripts/migration-manifest-sf3.2.test.cjs +382 -0
- package/scripts/migration-manifest-sf3.3.test.cjs +170 -0
- package/scripts/migration-manifest-sf5.5.test.cjs +189 -0
- package/scripts/migration-manifest.cjs +1146 -0
- package/scripts/migration-manifest.test.cjs +186 -0
- package/scripts/npm-scope-invocation.test.cjs +194 -0
- package/scripts/owner-layer.cjs +442 -0
- package/scripts/owner-layer.test.cjs +358 -0
- package/scripts/pc-run-pipeline.cjs +289 -0
- package/scripts/pc-run-pipeline.test.cjs +256 -0
- package/scripts/pc-run-pod-countgte.test.cjs +66 -0
- package/scripts/pc-run-pod.cjs +1355 -0
- package/scripts/phash-gate.test.cjs +433 -0
- package/scripts/pod-export-compile.cjs +487 -0
- package/scripts/pod-export-compile.test.cjs +312 -0
- package/scripts/pod-export-executor.cjs +678 -0
- package/scripts/pod-export-executor.test.cjs +1341 -0
- package/scripts/pod-export-hardening.test.cjs +531 -0
- package/scripts/pod-export-postprocess.cjs +427 -0
- package/scripts/pod-export-postprocess.test.cjs +558 -0
- package/scripts/pod-export-queue-race.test.cjs +865 -0
- package/scripts/pod-export-release.cjs +473 -0
- package/scripts/pod-export-release.test.cjs +267 -0
- package/scripts/pod-export-service.cjs +1297 -0
- package/scripts/pod-export-service.test.cjs +489 -0
- package/scripts/pod-export-tools.cjs +470 -0
- package/scripts/pod-export-ui.cjs +805 -0
- package/scripts/pod-export-ui.test.cjs +301 -0
- package/scripts/pod-memory-budget.test.cjs +309 -0
- package/scripts/pod-memory-note.cjs +357 -0
- package/scripts/pod-memory-note.test.cjs +452 -0
- package/scripts/pod-memory.cjs +1252 -0
- package/scripts/pod-memory.test.cjs +951 -0
- package/scripts/render-png.test.cjs +307 -0
- package/scripts/smoke-isolation.test.cjs +100 -0
- package/scripts/smoke-pod-workflow.cjs +755 -0
- package/scripts/smoke-sweep.test.cjs +170 -0
- package/scripts/validate-adopt.js +235 -0
- package/scripts/validate-branding.js +84 -0
- package/scripts/validate-budgets.js +140 -0
- package/scripts/validate-fidelity-output.cjs +267 -0
- package/scripts/validate-fidelity-output.test.cjs +101 -0
- package/scripts/validate-install.js +230 -0
- package/scripts/validate-no-clone.cjs +220 -0
- package/scripts/validate-no-clone.test.cjs +77 -0
- package/scripts/validate-pipeline.cjs +145 -0
- package/scripts/validate-pod-workflow.cjs +878 -0
- package/scripts/validate-source-audit.cjs +170 -0
- package/scripts/validate-source-audit.test.cjs +90 -0
- package/scripts/validate-story-acs.cjs +862 -0
- package/scripts/validate-update-scope-motor.js +421 -0
- package/scripts/veto-checks/html-valid.cjs +92 -0
- package/scripts/veto-checks/lexical-check.cjs +62 -0
- package/scripts/veto-checks/phash-gate.cjs +589 -0
- package/scripts/veto-checks/render-gate.cjs +165 -0
- package/scripts/veto-checks/render-png.cjs +358 -0
- package/scripts/veto-checks.test.cjs +219 -0
- package/scripts/watcher-audit.cjs +178 -0
- package/template/.claude/CLAUDE.md +103 -0
- package/template/.claude/agents/pc-analyst.md +100 -0
- package/template/.claude/agents/pc-architect.md +32 -0
- package/template/.claude/agents/pc-dev.md +27 -0
- package/template/.claude/agents/pc-devops.md +45 -0
- package/template/.claude/agents/pc-planner.md +32 -0
- package/template/.claude/agents/pc-po.md +25 -0
- package/template/.claude/agents/pc-qa.md +25 -0
- package/template/.claude/commands/power/academy.md +9 -0
- package/template/.claude/commands/power/automation.md +9 -0
- package/template/.claude/commands/power/content-carousel.md +267 -0
- package/template/.claude/commands/power/content.md +9 -0
- package/template/.claude/commands/power/dev.md +9 -0
- package/template/.claude/commands/power/forge.md +224 -0
- package/template/.claude/commands/power/hormozi.md +221 -0
- package/template/.claude/commands/power/modo.md +57 -0
- package/template/.claude/hooks/README.md +114 -0
- package/template/.claude/hooks/context-budget.cjs +64 -0
- package/template/.claude/hooks/enforce-cycle-authority.cjs +457 -0
- package/template/.claude/hooks/enforce-git-push-authority.cjs +925 -0
- package/template/.claude/hooks/enforce-ready-oracles.cjs +502 -0
- package/template/.claude/rules/authority.md +76 -0
- package/template/.claude/rules/curadoria-de-conhecimento.md +30 -0
- package/template/.claude/rules/model-matrix.md +34 -0
- package/template/.claude/rules/orchestration.md +133 -0
- package/template/.claude/settings.json +50 -0
- package/template/.claude/skills/pc-apply-qa-fixes/SKILL.md +108 -0
- package/template/.claude/skills/pc-apply-qa-fixes/references/fix-loop.md +40 -0
- package/template/.claude/skills/pc-apply-qa-fixes/references/fix-request-template.md +70 -0
- package/template/.claude/skills/pc-arena/SKILL.md +96 -0
- package/template/.claude/skills/pc-close-story/SKILL.md +105 -0
- package/template/.claude/skills/pc-commit/SKILL.md +108 -0
- package/template/.claude/skills/pc-develop-story/SKILL.md +116 -0
- package/template/.claude/skills/pc-full-cycle/SKILL.md +150 -0
- package/template/.claude/skills/pc-pipeline/SKILL.md +63 -0
- package/template/.claude/skills/pc-pipeline/references/gate-templates.md +46 -0
- package/template/.claude/skills/pc-review-story/SKILL.md +129 -0
- package/template/.claude/skills/pc-review-story/references/false-positive-detection.md +50 -0
- package/template/.claude/skills/pc-review-story/references/nfr-assess.md +47 -0
- package/template/.claude/skills/pc-review-story/references/risk-profile.md +75 -0
- package/template/.claude/skills/pc-review-story/references/security-checklist.md +69 -0
- package/template/.claude/skills/pc-review-story/references/test-design.md +37 -0
- package/template/.claude/skills/pc-review-story/references/trace-requirements.md +46 -0
- package/template/.claude/skills/pc-run-pod/SKILL.md +123 -0
- package/template/.claude/skills/pc-validate-story/SKILL.md +114 -0
- package/template/.claude/skills/pc-wave/SKILL.md +133 -0
- package/template/.env.example +10 -0
- package/template/knowledge/analyst/brainstorming.md +40 -0
- package/template/knowledge/analyst/brief-skeleton.md +32 -0
- package/template/knowledge/analyst/competitor-skeleton.md +39 -0
- package/template/knowledge/analyst/elicitation-menu.md +49 -0
- package/template/knowledge/analyst/market-research-skeleton.md +36 -0
- package/template/knowledge/analyst/research-prompt-structure.md +48 -0
- package/template/knowledge/analyst/roi-skeleton.md +51 -0
- package/template/knowledge/voice-dna/README.md +99 -0
- package/template/knowledge/voice-dna/voice-dna.example.yaml +157 -0
- package/template/pods/README.md +64 -0
- package/template/pods/academy/README.md +37 -0
- package/template/pods/academy/agents/academy-chief.md +27 -0
- package/template/pods/academy/examples/palestra.example.md +142 -0
- package/template/pods/academy/pod-spec.json +44 -0
- package/template/pods/academy/templates/materiais-tmpl.md +17 -0
- package/template/pods/academy/templates/revisao-didatica-tmpl.md +20 -0
- package/template/pods/academy/workflows/wf-materiais-revisao.yaml +30 -0
- package/template/pods/authority-content/README.md +43 -0
- package/template/pods/authority-content/agents/content-chief.md +28 -0
- package/template/pods/authority-content/pod-spec.json +45 -0
- package/template/pods/authority-content/templates/criacao-na-voz-tmpl.md +17 -0
- package/template/pods/authority-content/templates/red-team-factual-tmpl.md +23 -0
- package/template/pods/authority-content/workflows/wf-criacao-redteam.yaml +30 -0
- package/template/pods/automation/README.md +37 -0
- package/template/pods/automation/agents/langgraph-architect.md +30 -0
- package/template/pods/automation/agents/n8n-architect.md +30 -0
- package/template/pods/automation/examples/README.md +46 -0
- package/template/pods/automation/examples/n8n-content-radar.example.json +380 -0
- package/template/pods/automation/knowledge/langgraph-patterns.md +126 -0
- package/template/pods/automation/knowledge/n8n-patterns.md +131 -0
- package/template/pods/automation/memory/README.md +16 -0
- package/template/pods/automation/memory/learnings.consolidated.md +8 -0
- package/template/pods/automation/memory/learnings.jsonl +0 -0
- package/template/pods/automation/pod-spec.json +109 -0
- package/template/pods/automation/specs/wf-langgraph-build.spec.json +107 -0
- package/template/pods/automation/squad-io.yaml +28 -0
- package/template/pods/automation/templates/lg-build-tmpl.md +17 -0
- package/template/pods/automation/templates/lg-design-tmpl.md +23 -0
- package/template/pods/automation/templates/lg-discover-tmpl.md +20 -0
- package/template/pods/automation/templates/lg-handoff-tmpl.md +20 -0
- package/template/pods/automation/templates/lg-test-tmpl.md +20 -0
- package/template/pods/automation/templates/n8n-build-tmpl.md +14 -0
- package/template/pods/automation/templates/n8n-design-tmpl.md +23 -0
- package/template/pods/automation/templates/n8n-discover-tmpl.md +20 -0
- package/template/pods/automation/templates/n8n-handoff-tmpl.md +20 -0
- package/template/pods/automation/templates/n8n-test-tmpl.md +20 -0
- package/template/pods/automation/workflows/wf-langgraph-build.smoke.json +11 -0
- package/template/pods/automation/workflows/wf-langgraph-build.yaml +64 -0
- package/template/pods/automation/workflows/wf-n8n-build.smoke.json +11 -0
- package/template/pods/automation/workflows/wf-n8n-build.yaml +65 -0
- package/template/pods/content-carousel/README.md +91 -0
- package/template/pods/content-carousel/agents/carousel-builder.md +28 -0
- package/template/pods/content-carousel/agents/carousel-visual.md +24 -0
- package/template/pods/content-carousel/agents/hn-curator.md +74 -0
- package/template/pods/content-carousel/config/heuristic-watcher-rubric.yaml +59 -0
- package/template/pods/content-carousel/knowledge/aquisicao-ancora.md +443 -0
- package/template/pods/content-carousel/knowledge/cta-brief.md +25 -0
- package/template/pods/content-carousel/knowledge/dono.example.yaml +45 -0
- package/template/pods/content-carousel/knowledge/identidade-visual-mascote.md +62 -0
- package/template/pods/content-carousel/knowledge/image-generation.md +62 -0
- package/template/pods/content-carousel/knowledge/selecao-de-ancora.md +60 -0
- package/template/pods/content-carousel/knowledge/slides-html.md +56 -0
- package/template/pods/content-carousel/memory/README.md +18 -0
- package/template/pods/content-carousel/memory/learnings.consolidated.md +6 -0
- package/template/pods/content-carousel/memory/learnings.jsonl +0 -0
- package/template/pods/content-carousel/pod-spec.json +100 -0
- package/template/pods/content-carousel/squad-io.yaml +10 -0
- package/template/pods/content-carousel/templates/assembly-tmpl.md +23 -0
- package/template/pods/content-carousel/templates/digest-tmpl.md +29 -0
- package/template/pods/content-carousel/templates/visuals-tmpl.md +20 -0
- package/template/pods/content-carousel/workflows/wf-content-carousel.smoke.json +23 -0
- package/template/pods/content-carousel/workflows/wf-content-carousel.yaml +80 -0
- package/template/pods/dev-ia/README.md +45 -0
- package/template/pods/dev-ia/agents/db-architect.md +33 -0
- package/template/pods/dev-ia/agents/ux-architect.md +29 -0
- package/template/pods/dev-ia/knowledge/db/migration-protocol.md +95 -0
- package/template/pods/dev-ia/knowledge/db/rls-patterns.md +101 -0
- package/template/pods/dev-ia/knowledge/db/schema-review-checklist.md +76 -0
- package/template/pods/dev-ia/memory/README.md +16 -0
- package/template/pods/dev-ia/memory/learnings.consolidated.md +8 -0
- package/template/pods/dev-ia/memory/learnings.jsonl +0 -0
- package/template/pods/dev-ia/pod-spec.json +34 -0
- package/template/pods/dev-ia/templates/schema-review-tmpl.md +23 -0
- package/template/pods/dev-ia/templates/ux-flow-review-tmpl.md +23 -0
- package/template/pods/dev-ia/tools/db-ops/backup-manager.sh +321 -0
- package/template/pods/dev-ia/tools/db-ops/best-practices-auditor.sh +534 -0
- package/template/pods/dev-ia/tools/db-ops/bootstrap-runner.sh +255 -0
- package/template/pods/dev-ia/tools/db-ops/data-loader.sh +616 -0
- package/template/pods/dev-ia/tools/db-ops/detect-database.sh +113 -0
- package/template/pods/dev-ia/tools/db-ops/explain-analyzer.sh +285 -0
- package/template/pods/dev-ia/tools/db-ops/health-checker.sh +323 -0
- package/template/pods/dev-ia/tools/db-ops/migration-runner.sh +308 -0
- package/template/pods/dev-ia/tools/db-ops/postgresql-schema-loader.sh +334 -0
- package/template/pods/dev-ia/tools/db-ops/query-runner.sh +171 -0
- package/template/pods/dev-ia/tools/db-ops/rls-policy-installer.sh +480 -0
- package/template/pods/dev-ia/tools/db-ops/rls-test-suite.sh +172 -0
- package/template/pods/dev-ia/tools/db-ops/schema-context-loader.sh +176 -0
- package/template/pods/hormozi-method/README.md +52 -0
- package/template/pods/hormozi-method/agents/acquisition-strategist.md +33 -0
- package/template/pods/hormozi-method/agents/conversion-copywriter.md +34 -0
- package/template/pods/hormozi-method/agents/monetization-architect.md +35 -0
- package/template/pods/hormozi-method/agents/offer-engineer.md +37 -0
- package/template/pods/hormozi-method/knowledge/core-four-leads.md +51 -0
- package/template/pods/hormozi-method/knowledge/grand-slam-offer.md +66 -0
- package/template/pods/hormozi-method/knowledge/growth-decision.md +38 -0
- package/template/pods/hormozi-method/knowledge/hooks-and-ads.md +56 -0
- package/template/pods/hormozi-method/knowledge/launch-email.md +31 -0
- package/template/pods/hormozi-method/knowledge/money-model.md +46 -0
- package/template/pods/hormozi-method/knowledge/opportunity-screening.md +44 -0
- package/template/pods/hormozi-method/knowledge/pages.md +25 -0
- package/template/pods/hormozi-method/knowledge/pos-lancamento-leads.md +56 -0
- package/template/pods/hormozi-method/knowledge/pricing.md +36 -0
- package/template/pods/hormozi-method/knowledge/retention-ltv.md +37 -0
- package/template/pods/hormozi-method/knowledge/scale-roadmap.md +35 -0
- package/template/pods/hormozi-method/knowledge/swipefiles-curados.md +44 -0
- package/template/pods/hormozi-method/knowledge/value-equation-diagnostic.md +77 -0
- package/template/pods/hormozi-method/memory/README.md +16 -0
- package/template/pods/hormozi-method/memory/learnings.consolidated.md +6 -0
- package/template/pods/hormozi-method/memory/learnings.jsonl +0 -0
- package/template/pods/hormozi-method/pod-spec.json +50 -0
- package/template/pods/hormozi-method/specs/wf-full-launch-sequence.spec.json +149 -0
- package/template/pods/hormozi-method/specs/wf-lead-magnet-pipeline.spec.json +125 -0
- package/template/pods/hormozi-method/specs/wf-money-model-design.spec.json +117 -0
- package/template/pods/hormozi-method/specs/wf-paid-ads-campaign.spec.json +132 -0
- package/template/pods/hormozi-method/specs/wf-scaling-diagnostic.spec.json +121 -0
- package/template/pods/hormozi-method/squad-io.yaml +43 -0
- package/template/pods/hormozi-method/templates/conversion-copywriter-tmpl.md +20 -0
- package/template/pods/hormozi-method/templates/launch-execucao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/launch-fechamento-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/launch-pos-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/launch-preparacao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/lm-copy-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/lm-distribuicao-tmpl.md +18 -0
- package/template/pods/hormozi-method/templates/lm-estrategia-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/mm-arquitetura-tmpl.md +18 -0
- package/template/pods/hormozi-method/templates/mm-copy-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/mm-diagnostico-tmpl.md +14 -0
- package/template/pods/hormozi-method/templates/monetization-architect-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/offer-engineer-tmpl.md +20 -0
- package/template/pods/hormozi-method/templates/paid-criativos-tmpl.md +19 -0
- package/template/pods/hormozi-method/templates/paid-fundacao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/paid-teste-escala-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/scaling-diagnostico-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/scaling-priorizacao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/scaling-roadmap-tmpl.md +17 -0
- package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.smoke.json +12 -0
- package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.yaml +56 -0
- package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.smoke.json +11 -0
- package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.yaml +45 -0
- package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.smoke.json +9 -0
- package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.yaml +45 -0
- package/template/pods/hormozi-method/workflows/wf-money-model-design.smoke.json +11 -0
- package/template/pods/hormozi-method/workflows/wf-money-model-design.yaml +45 -0
- package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.smoke.json +9 -0
- package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.yaml +46 -0
- package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.smoke.json +11 -0
- package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.yaml +44 -0
- package/template/pods/squad-forge/README.md +53 -0
- package/template/pods/squad-forge/agents/forge-chief.md +31 -0
- package/template/pods/squad-forge/agents/mind-extractor.md +29 -0
- package/template/pods/squad-forge/agents/pod-critic.md +26 -0
- package/template/pods/squad-forge/config/heuristic-watcher-rubric.yaml +74 -0
- package/template/pods/squad-forge/knowledge/fidelity-rubric.yaml +176 -0
- package/template/pods/squad-forge/knowledge/g6-memory-check.md +17 -0
- package/template/pods/squad-forge/knowledge/memory-ingest.md +16 -0
- package/template/pods/squad-forge/knowledge/source-audit-rubric.yaml +60 -0
- package/template/pods/squad-forge/memory/README.md +16 -0
- package/template/pods/squad-forge/memory/learnings.consolidated.md +8 -0
- package/template/pods/squad-forge/memory/learnings.jsonl +0 -0
- package/template/pods/squad-forge/pod-spec.json +166 -0
- package/template/pods/squad-forge/squad-io.yaml +13 -0
- package/template/pods/squad-forge/tasks/pipeline.md +158 -0
- package/template/pods/squad-forge/templates/discovery-tmpl.md +40 -0
- package/template/pods/squad-forge/templates/generation-tmpl.md +18 -0
- package/template/pods/squad-forge/templates/red-team-tmpl.md +17 -0
- package/template/pods/squad-forge/templates/register-tmpl.md +17 -0
- package/template/pods/squad-forge/templates/workflow-tmpl.yaml +101 -0
- package/template/pods/squad-forge/workflows/wf-forge.smoke.json +12 -0
- package/template/pods/squad-forge/workflows/wf-forge.yaml +60 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* pod-export-service.test.cjs — SMOKE HERMÉTICO do serviço-irmão (Story SF10.3, F2).
|
|
5
|
+
*
|
|
6
|
+
* NÃO exige SSH/VPS/produção. Simula a topologia (a) do serviço-irmão LOCALMENTE (paths,
|
|
7
|
+
* resolução de blessed, leitura de env, pickup de fila) e PROVA a MESMA rejeição de script
|
|
8
|
+
* fora-da-whitelist que `pc-run-pod.cjs` garante em produção — é isto que dá ao QA como validar
|
|
9
|
+
* a fronteira command_check sem produção. Rede/executor/relógio INJETADOS; zero rede real.
|
|
10
|
+
*
|
|
11
|
+
* Cobre:
|
|
12
|
+
* AC2 — chaves lidas do env; ausência FALHA ALTO (fail-loud), nunca por request.
|
|
13
|
+
* AC3 — fronteira command_check: blessed ACEITO, não-blessed/traversal RECUSADO no MESMO
|
|
14
|
+
* code-path (motor.evalVeto, VETO_CHECKS_ROOT ancorado no checkout) + arquivo estranho
|
|
15
|
+
* PLANTADO em veto-checks/ ainda é RECUSADO (Guard B do manifesto — defesa em profundidade).
|
|
16
|
+
* AC4 — Chrome aferido no ARRANQUE; ausência FALHA ALTO (fail-loud herdado de ADR-011).
|
|
17
|
+
* AC6 — pickup de fila (contrato de diretório DF2-5): pending → processing → done/blocked/failed.
|
|
18
|
+
* AC3 — coerência framework_version FIADA no preflight (MISMATCH recusa o arranque).
|
|
19
|
+
*
|
|
20
|
+
* Fix pós-deploy (achados 4/5/6 do smoke real VPS): roteamento por RESULTADO + approve via serviço.
|
|
21
|
+
* Um run BLOQUEADO no veto (achado 4) ou PAUSADO num checkpoint (achado 5) sai exit 0 e NÃO pode
|
|
22
|
+
* cair em done/ (verde-falso semântico). O approve via o serviço (achado 6) passa --runs-root da
|
|
23
|
+
* config (sem EACCES) e reconcilia o marcador de awaiting/. Cobrimos a LÓGICA sem VPS:
|
|
24
|
+
* parseExecutorResult (parsing puro), classifyOutcome (crash > awaiting/blocked > done), roteamento
|
|
25
|
+
* fim-a-fim de blocked/awaiting injetados → blocked//awaiting/ (nunca done/), buildApproveArgs
|
|
26
|
+
* (--runs-root threading), deriveRunKind (estado real do run) e approveRun (marcador sai de awaiting/).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const svc = require('./pod-export-service.cjs');
|
|
34
|
+
const motor = require('./pc-run-pod.cjs');
|
|
35
|
+
|
|
36
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
37
|
+
const VETO_CHECKS = path.join(REPO_ROOT, 'scripts', 'veto-checks');
|
|
38
|
+
const VALID_HTML = path.join(REPO_ROOT, 'scripts', '__fixtures__', 'veto-checks', 'slide-ok.html');
|
|
39
|
+
|
|
40
|
+
let failures = 0;
|
|
41
|
+
function assert(ok, msg) {
|
|
42
|
+
if (ok) console.log(`[OK ] ${msg}`);
|
|
43
|
+
else { console.error(`[FALHA] ${msg}`); failures++; }
|
|
44
|
+
}
|
|
45
|
+
function throwsFail(fn) { try { fn(); return false; } catch { return true; } }
|
|
46
|
+
function tmpDir(label) { return fs.mkdtempSync(path.join(os.tmpdir(), `sf10.3-${label}-`)); }
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// AC2 — chaves da ORG do env (padrão getAnthropicApiKey); ausência FALHA ALTO.
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
function envKeyTests() {
|
|
52
|
+
console.log('\n--- AC2: chaves da ORG lidas do env, fail-loud na ausência ---');
|
|
53
|
+
const good = { ANTHROPIC_API_KEY: 'sk-ant-XXX', OPENROUTER_API_KEY: 'sk-or-XXX' };
|
|
54
|
+
assert(svc.getAnthropicApiKey(good) === 'sk-ant-XXX', 'getAnthropicApiKey lê do env fornecido');
|
|
55
|
+
assert(svc.getOpenRouterApiKey(good) === 'sk-or-XXX', 'getOpenRouterApiKey lê do env fornecido');
|
|
56
|
+
assert(throwsFail(() => svc.getAnthropicApiKey({})), 'ANTHROPIC_API_KEY ausente → FAIL-LOUD (não silencioso)');
|
|
57
|
+
assert(throwsFail(() => svc.getOpenRouterApiKey({ ANTHROPIC_API_KEY: 'x' })), 'OPENROUTER_API_KEY ausente → FAIL-LOUD');
|
|
58
|
+
assert(throwsFail(() => svc.getAnthropicApiKey({ ANTHROPIC_API_KEY: ' ' })), 'chave em branco NÃO conta como presente (fail-loud)');
|
|
59
|
+
// O valor da chave NUNCA aparece na mensagem de erro (nunca logar chaves).
|
|
60
|
+
let msg = '';
|
|
61
|
+
try { svc.getApiKey('ANTHROPIC_API_KEY', {}); } catch (e) { msg = e.message; }
|
|
62
|
+
assert(/nunca hardcoded, nunca por request/.test(msg), 'mensagem de fail-loud reforça a doutrina "nunca por request"');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// AC3 (crítico) — fronteira command_check PROVADA na topologia do serviço-irmão, sem VPS.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function frontierTests() {
|
|
69
|
+
console.log('\n--- AC3: fronteira command_check reproduzida na topologia do serviço-irmão (sem VPS) ---');
|
|
70
|
+
|
|
71
|
+
// (a) self-check do serviço usa o MESMO motor.evalVeto (code-path de produção).
|
|
72
|
+
const s = svc.assertCommandCheckFrontier({ validHtmlPath: VALID_HTML });
|
|
73
|
+
assert(s.rejectedNonBlessed, 'self-check: script NÃO-blessed RECUSADO pela resolução do motor (sem execução)');
|
|
74
|
+
assert(s.rejectedTraversal, 'self-check: traversal ../ para fora de veto-checks/ RECUSADO');
|
|
75
|
+
assert(s.acceptedBlessed, 'self-check: blessed html-valid.cjs ACEITO sobre HTML válido (resolução do checkout funciona)');
|
|
76
|
+
|
|
77
|
+
// (b) Prova direta contra o motor (a MESMA garantia que pc-run-pod dá em produção): um nome
|
|
78
|
+
// fora da whitelist é recusado ANTES de qualquer execFileSync.
|
|
79
|
+
const roots = { runDir: os.tmpdir(), podRoot: REPO_ROOT };
|
|
80
|
+
const outside = motor.evalVeto({ type: 'command_check', command: 'nao-existe-blessed.cjs' }, '', path.join(os.tmpdir(), 'x'), roots);
|
|
81
|
+
assert(!outside.ok && /blessed|resolve|não é/.test(outside.reason), 'motor: script fora da whitelist RECUSADO (mesma rejeição que produção)');
|
|
82
|
+
const blessed = motor.evalVeto({ type: 'command_check', command: 'html-valid.cjs' }, '', VALID_HTML, roots);
|
|
83
|
+
assert(blessed.ok, 'motor: blessed html-valid.cjs ACEITO (fronteira não bloqueia o legítimo)');
|
|
84
|
+
|
|
85
|
+
// (c) Defesa em profundidade (Guard B): arquivo ESTRANHO PLANTADO dentro de veto-checks/ — nome
|
|
86
|
+
// que NÃO está no manifesto motor-scripts.json — ainda é RECUSADO. Prova que a fronteira não
|
|
87
|
+
// confia na mera presença no diretório (o cenário exato do ataque à VPS). Cleanup garantido.
|
|
88
|
+
const planted = path.join(VETO_CHECKS, '__sf10_3_evil_probe__.cjs');
|
|
89
|
+
let plantedExisted = fs.existsSync(planted);
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(planted, `require('node:fs').writeFileSync(${JSON.stringify(planted + '.pwned')}, 'x'); process.exit(0);`);
|
|
92
|
+
const r = motor.evalVeto({ type: 'command_check', command: '__sf10_3_evil_probe__.cjs' }, '', VALID_HTML, roots);
|
|
93
|
+
assert(!r.ok, 'PLANTADO em veto-checks/ mas fora do manifesto → RECUSADO (Guard B; não confia no dir)');
|
|
94
|
+
assert(!fs.existsSync(planted + '.pwned'), 'o payload plantado NUNCA executou (sem side-effect no disco)');
|
|
95
|
+
} finally {
|
|
96
|
+
if (!plantedExisted) { try { fs.unlinkSync(planted); } catch {} }
|
|
97
|
+
try { fs.unlinkSync(planted + '.pwned'); } catch {}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// AC4 — Chrome aferido no ARRANQUE; ausência FALHA ALTO (herdado de ADR-011).
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
function chromePreflightTests() {
|
|
105
|
+
console.log('\n--- AC4: Chrome aferido no arranque, fail-loud se ausente ---');
|
|
106
|
+
const env = { ANTHROPIC_API_KEY: 'x', OPENROUTER_API_KEY: 'y' };
|
|
107
|
+
// Chrome ausente (probe injetado → null) deve FALHAR ALTO antes de aceitar qualquer run.
|
|
108
|
+
let msg = '';
|
|
109
|
+
try {
|
|
110
|
+
svc.preflight({}, { env, probeChrome: () => null, evalVeto: motor.evalVeto });
|
|
111
|
+
} catch (e) { msg = e.message; }
|
|
112
|
+
assert(/Chrome headless AUSENTE/.test(msg), 'Chrome ausente no arranque → FAIL-LOUD (nunca skip-silencioso)');
|
|
113
|
+
|
|
114
|
+
// Chrome presente (probe injetado) + fronteira íntegra → preflight passa (sem bundle = sem coerência).
|
|
115
|
+
const s = svc.preflight({}, { env, probeChrome: () => '/usr/bin/chromium', evalVeto: motor.evalVeto, validHtmlPath: VALID_HTML });
|
|
116
|
+
assert(s.chrome === 'chromium' && s.keys.anthropic && s.keys.openrouter, 'preflight OK com Chrome presente + chaves + fronteira íntegra');
|
|
117
|
+
assert(s.frontier && s.frontier.rejectedNonBlessed && s.frontier.acceptedBlessed, 'preflight embute o self-check da fronteira (AC3)');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// AC3 — coerência framework_version FIADA no arranque (MISMATCH recusa o serviço).
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function coherencePreflightTests() {
|
|
124
|
+
console.log('\n--- AC3/DF2-1: coerência framework_version fiada no arranque ---');
|
|
125
|
+
const env = { ANTHROPIC_API_KEY: 'x', OPENROUTER_API_KEY: 'y' };
|
|
126
|
+
const bundleDir = tmpDir('bundle');
|
|
127
|
+
// bundle mínimo que loadBundle aceita.
|
|
128
|
+
fs.writeFileSync(path.join(bundleDir, 'bundle.json'), JSON.stringify({ pod: 'demo', modelo_pinado: { agents: { p1: 'm' } }, framework_version: '9.9.9' }));
|
|
129
|
+
fs.writeFileSync(path.join(bundleDir, 'workflow.yaml'), 'id: wf-demo\n');
|
|
130
|
+
const lockPath = path.join(bundleDir, 'export.lock.json');
|
|
131
|
+
|
|
132
|
+
// MISMATCH: lock diz 9.9.9, motor injetado diz 1.0.0 → RECUSA (usa a função REAL do executor).
|
|
133
|
+
fs.writeFileSync(lockPath, JSON.stringify({ framework_version: '9.9.9' }));
|
|
134
|
+
const cfg = { bundleDir, lockOverride: lockPath };
|
|
135
|
+
const deps = {
|
|
136
|
+
env, probeChrome: () => '/usr/bin/chromium', evalVeto: motor.evalVeto, validHtmlPath: VALID_HTML,
|
|
137
|
+
assertFrameworkCoherence: (bundle, lp) => require('./pod-export-executor.cjs').assertFrameworkCoherence(bundle, lp, '1.0.0'),
|
|
138
|
+
};
|
|
139
|
+
assert(throwsFail(() => svc.preflight(cfg, deps)), 'framework_version MISMATCH (motor 1.0.0 != lock 9.9.9) → arranque RECUSADO');
|
|
140
|
+
|
|
141
|
+
// MATCH: motor injetado == lock → passa.
|
|
142
|
+
const depsOk = { ...deps, assertFrameworkCoherence: (bundle, lp) => require('./pod-export-executor.cjs').assertFrameworkCoherence(bundle, lp, '9.9.9') };
|
|
143
|
+
const s = svc.preflight(cfg, depsOk);
|
|
144
|
+
assert(s.coherence && s.coherence.engineVersion === '9.9.9', 'framework_version coerente → preflight passa e reporta a versão');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// AC6 — pickup de fila (contrato de diretório DF2-5), executor INJETADO (sem rede/LLM).
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
async function queuePickupTests() {
|
|
151
|
+
console.log('\n--- AC6/DF2-5: pickup da fila por contrato de diretório (pending→processing→done/blocked/failed) ---');
|
|
152
|
+
const queueDir = tmpDir('queue');
|
|
153
|
+
// bundleDir=null: coerência de versão é coberta pelo teste dedicado acima; aqui o foco é o
|
|
154
|
+
// contrato de fila. O executor é INJETADO, então nenhum bundle real é necessário.
|
|
155
|
+
const cfg = { queueDir, bundleDir: null, runsRoot: tmpDir('runs'), pollMs: 1 };
|
|
156
|
+
|
|
157
|
+
// Dropar 4 payloads em pending/: (1) concluído (exit 0, complete); (2) CRASH (exit 1);
|
|
158
|
+
// (3) BLOQUEADO no veto (exit 0 + {blocked} — achado 4); (4) PAUSADO no checkpoint (exit 0 +
|
|
159
|
+
// {awaiting} — achado 5: NÃO pode ir a done/, é retomável via approve).
|
|
160
|
+
const dirs = svc.ensureQueue(queueDir);
|
|
161
|
+
fs.writeFileSync(path.join(dirs.pending, 'a-ok.json'), JSON.stringify({ pod: 'demo', n: 1 }));
|
|
162
|
+
fs.writeFileSync(path.join(dirs.pending, 'b-fail.json'), JSON.stringify({ pod: 'demo', n: 2 }));
|
|
163
|
+
fs.writeFileSync(path.join(dirs.pending, 'c-blocked.json'), JSON.stringify({ pod: 'demo', n: 3 }));
|
|
164
|
+
fs.writeFileSync(path.join(dirs.pending, 'd-awaiting.json'), JSON.stringify({ pod: 'demo', n: 4 }));
|
|
165
|
+
assert(svc.listPending(queueDir).length === 4, 'listPending enxerga os 4 payloads dropados em pending/');
|
|
166
|
+
|
|
167
|
+
// Executor INJETADO: lê o payload movido para processing/ e decide por 'n' (prova que o
|
|
168
|
+
// pickup entrega o payload correto ao executor). Sem rede, sem LLM, sem spawn real.
|
|
169
|
+
// n=1 → done; n=2 → crash; n=3 → blocked (achado 4); n=4 → awaiting/checkpoint (achado 5).
|
|
170
|
+
const seen = [];
|
|
171
|
+
const deps = {
|
|
172
|
+
log: () => {},
|
|
173
|
+
runExecutor: async (c, processingPath) => {
|
|
174
|
+
const p = JSON.parse(fs.readFileSync(processingPath, 'utf8'));
|
|
175
|
+
seen.push({ n: p.n, existsInProcessing: fs.existsSync(processingPath) });
|
|
176
|
+
if (p.n === 2) return { code: 1, result: null }; // crash real
|
|
177
|
+
if (p.n === 3) return { code: 0, result: { blocked: 'lexical-check' } }; // veto esgotado (exit 0)
|
|
178
|
+
if (p.n === 4) return { code: 0, result: { awaiting: 'assembly' } }; // checkpoint (exit 0)
|
|
179
|
+
return { code: 0, result: { complete: true } }; // pipeline completo
|
|
180
|
+
},
|
|
181
|
+
sleep: async () => {},
|
|
182
|
+
stopAfter: 1,
|
|
183
|
+
};
|
|
184
|
+
const res = await svc.serve(
|
|
185
|
+
cfg,
|
|
186
|
+
{ ...deps, env: { ANTHROPIC_API_KEY: 'x', OPENROUTER_API_KEY: 'y' }, probeChrome: () => '/usr/bin/chromium', evalVeto: motor.evalVeto, validHtmlPath: VALID_HTML },
|
|
187
|
+
);
|
|
188
|
+
assert(res.processed.length === 4, 'serve processou os 4 payloads num ciclo');
|
|
189
|
+
assert(seen.every((x) => x.existsInProcessing), 'o payload foi ENTREGUE ao executor a partir de processing/ (movido do pending)');
|
|
190
|
+
assert(svc.listPending(queueDir).length === 0, 'pending/ esvaziou após o pickup');
|
|
191
|
+
const okRun = res.processed.find((r) => r.kind === 'done');
|
|
192
|
+
const failRun = res.processed.find((r) => r.kind === 'failed');
|
|
193
|
+
const blockedRun = res.processed.find((r) => r.kind === 'blocked');
|
|
194
|
+
assert(okRun && okRun.ok && fs.existsSync(okRun.finalPath) && okRun.finalPath.includes(`${path.sep}done${path.sep}`), 'run COMPLETO (exit 0, result.complete) foi para done/');
|
|
195
|
+
assert(failRun && !failRun.ok && fs.existsSync(failRun.finalPath) && failRun.finalPath.includes(`${path.sep}failed${path.sep}`), 'run com exit != 0 (crash) foi para failed/ (veredito pelo exit, não pelo relato)');
|
|
196
|
+
// Achado 4 — o coração do fix: bloqueado sai exit 0 mas NÃO é sucesso.
|
|
197
|
+
assert(blockedRun && !blockedRun.ok, 'run BLOQUEADO NÃO é marcado ok (exit 0 sozinho não basta)');
|
|
198
|
+
assert(blockedRun && fs.existsSync(blockedRun.finalPath) && blockedRun.finalPath.includes(`${path.sep}blocked${path.sep}`), 'achado 4: run BLOQUEADO (exit 0 + result.blocked) foi para blocked/, NUNCA done/');
|
|
199
|
+
assert(!blockedRun.finalPath.includes(`${path.sep}done${path.sep}`), 'achado 4: run bloqueado NÃO contaminou done/ (verde-falso evitado)');
|
|
200
|
+
const blockedResult = JSON.parse(fs.readFileSync(blockedRun.resultPath, 'utf8'));
|
|
201
|
+
assert(blockedResult.kind === 'blocked' && blockedResult.blocked === 'lexical-check' && blockedResult.ok === false, 'result.json do bloqueado registra kind=blocked, a fase e ok=false');
|
|
202
|
+
// Achado 5 — checkpoint humano NÃO é conclusão: vai para awaiting/, retomável via approve.
|
|
203
|
+
const awaitingRun = res.processed.find((r) => r.kind === 'awaiting');
|
|
204
|
+
assert(awaitingRun && !awaitingRun.ok, 'run PAUSADO no checkpoint NÃO é marcado ok (não concluiu)');
|
|
205
|
+
assert(awaitingRun && fs.existsSync(awaitingRun.finalPath) && awaitingRun.finalPath.includes(`${path.sep}awaiting${path.sep}`), 'achado 5: run com {awaiting:"assembly"} foi para awaiting/, NUNCA done/');
|
|
206
|
+
assert(!awaitingRun.finalPath.includes(`${path.sep}done${path.sep}`), 'achado 5: checkpoint NÃO contaminou done/ (verde-falso evitado)');
|
|
207
|
+
const awaitingResult = JSON.parse(fs.readFileSync(awaitingRun.resultPath, 'utf8'));
|
|
208
|
+
assert(awaitingResult.kind === 'awaiting' && awaitingResult.awaiting === 'assembly' && awaitingResult.ok === false, 'result.json do pausado registra kind=awaiting, a fase e ok=false');
|
|
209
|
+
assert(fs.existsSync(okRun.resultPath) && JSON.parse(fs.readFileSync(okRun.resultPath, 'utf8')).ok === true, 'result.json registra o veredito do run OK');
|
|
210
|
+
|
|
211
|
+
// approve é CLI SEPARADA — nunca chega pela fila (DF2-5). Confirma que enqueue valida JSON.
|
|
212
|
+
const badPayload = path.join(tmpDir('bad'), 'bad.json');
|
|
213
|
+
fs.writeFileSync(badPayload, '{not json');
|
|
214
|
+
assert(throwsFail(() => svc.enqueue(cfg, badPayload)), 'enqueue RECUSA payload que não é JSON válido (não polui a fila)');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Achado 4 (smoke real VPS) — parsing do RESULTADO + classificação do desfecho, LÓGICA pura.
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
function outcomeRoutingTests() {
|
|
221
|
+
console.log('\n--- Achado 4: parseExecutorResult + classifyOutcome (roteamento por RESULTADO, sem VPS) ---');
|
|
222
|
+
|
|
223
|
+
// parseExecutorResult: pega a ÚLTIMA linha `RESULTADO: {json}` da saída do executor.
|
|
224
|
+
const stdoutBlocked = 'runDir=/x\n [lexical-check] VETO DISPAROU (tentativa 3): em-dash: 23\n>>> BLOQUEIO: fase "lexical-check"\nRESULTADO: {"blocked":"lexical-check"}\n';
|
|
225
|
+
assert(svc.parseExecutorResult(stdoutBlocked).blocked === 'lexical-check', 'parseExecutorResult lê {blocked:"<fase>"} do stdout do executor');
|
|
226
|
+
assert(svc.parseExecutorResult('foo\nRESULTADO: {"complete":true}\n').complete === true, 'parseExecutorResult lê {complete:true}');
|
|
227
|
+
assert(svc.parseExecutorResult('RESULTADO: {"awaiting":"aprovacao"}').awaiting === 'aprovacao', 'parseExecutorResult lê {awaiting:"<fase>"} (checkpoint)');
|
|
228
|
+
assert(svc.parseExecutorResult('sem resultado aqui') === null, 'parseExecutorResult devolve null quando não há linha RESULTADO');
|
|
229
|
+
assert(svc.parseExecutorResult('RESULTADO: {quebrado') === null, 'parseExecutorResult devolve null em JSON ilegível (não crasha)');
|
|
230
|
+
// Última RESULTADO vence (resume pode imprimir mais de uma).
|
|
231
|
+
assert(svc.parseExecutorResult('RESULTADO: {"blocked":"a"}\nRESULTADO: {"complete":true}').complete === true, 'parseExecutorResult usa o ÚLTIMO RESULTADO impresso');
|
|
232
|
+
|
|
233
|
+
// classifyOutcome: precedência crash > awaiting/blocked > done.
|
|
234
|
+
assert(svc.classifyOutcome({ code: 1, result: null }).kind === 'failed', 'classify: exit != 0 → failed (crash), mesmo sem RESULTADO');
|
|
235
|
+
assert(svc.classifyOutcome({ code: 1, result: { blocked: 'x' } }).kind === 'failed', 'classify: crash tem precedência sobre blocked (exit != 0 → failed)');
|
|
236
|
+
assert(svc.classifyOutcome({ code: 1, result: { awaiting: 'x' } }).kind === 'failed', 'classify: crash tem precedência sobre awaiting (exit != 0 → failed)');
|
|
237
|
+
const b = svc.classifyOutcome({ code: 0, result: { blocked: 'lexical-check' } });
|
|
238
|
+
assert(b.kind === 'blocked' && b.blocked === 'lexical-check', 'classify: exit 0 + result.blocked → blocked/ (achado 4)');
|
|
239
|
+
const aw = svc.classifyOutcome({ code: 0, result: { awaiting: 'assembly' } });
|
|
240
|
+
assert(aw.kind === 'awaiting' && aw.awaiting === 'assembly', 'classify: exit 0 + result.awaiting → awaiting/ (achado 5, NÃO done/)');
|
|
241
|
+
assert(svc.classifyOutcome({ code: 0, result: { complete: true } }).kind === 'done', 'classify: exit 0 + complete → done');
|
|
242
|
+
assert(svc.classifyOutcome({ code: 0, result: null }).kind === 'done', 'classify: exit 0 sem RESULTADO → done (compat)');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Achado 6 (smoke real VPS) — approve via o serviço: --runs-root da config + reconciliação do
|
|
247
|
+
// marcador de awaiting/. LÓGICA testável sem VPS (args puros, fs, motor injetado).
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
function approveWiringTests() {
|
|
250
|
+
console.log('\n--- Achado 6: approve via o serviço (--runs-root de PODEXPORT_RUNS_DIR + reconciliação) ---');
|
|
251
|
+
|
|
252
|
+
// buildApproveArgs: o CORAÇÃO do achado 6 — o executor recebe --runs-root da config do serviço
|
|
253
|
+
// (não o default .power-claude/... que dá EACCES para o usuário do serviço).
|
|
254
|
+
const cfg = svc.loadConfig({ PODEXPORT_BUNDLE_DIR: '/opt/bundle', PODEXPORT_QUEUE_DIR: '/var/q', PODEXPORT_RUNS_DIR: '/var/lib/pod-export/runs' });
|
|
255
|
+
const args = svc.buildApproveArgs(cfg, 'pex-abc');
|
|
256
|
+
const i = args.indexOf('--runs-root');
|
|
257
|
+
assert(i !== -1 && args[i + 1] === path.resolve('/var/lib/pod-export/runs'), 'buildApproveArgs passa --runs-root = PODEXPORT_RUNS_DIR (achado 6: sem EACCES no default)');
|
|
258
|
+
assert(args.includes('approve') && args.includes('pex-abc') && args[0] === svc.EXECUTOR_PATH, 'buildApproveArgs delega ao executor: approve <runId>');
|
|
259
|
+
const cfgLock = { ...cfg, lockOverride: '/opt/bundle/export.lock.json' };
|
|
260
|
+
assert(svc.buildApproveArgs(cfgLock, 'pex-abc').includes('--lock'), 'buildApproveArgs propaga --lock quando há override');
|
|
261
|
+
|
|
262
|
+
// deriveRunKind: lê o estado REAL do run (motor injetado) — não adivinha.
|
|
263
|
+
const depAwaiting = { findStateByRunId: () => '/x/state.json', readState: () => ({ steps: { s1: { status: 'done' }, s2: { status: 'awaiting_approval' } } }) };
|
|
264
|
+
assert(svc.deriveRunKind(cfg, 'r', depAwaiting) === 'awaiting', 'deriveRunKind: step em awaiting_approval → awaiting (outro checkpoint)');
|
|
265
|
+
const depDone = { findStateByRunId: () => '/x/state.json', readState: () => ({ steps: { s1: { status: 'done' }, s2: { status: 'done' } } }) };
|
|
266
|
+
assert(svc.deriveRunKind(cfg, 'r', depDone) === 'done', 'deriveRunKind: todos os steps done → done');
|
|
267
|
+
const depFail = { findStateByRunId: () => '/x/state.json', readState: () => ({ steps: { s1: { status: 'failed' } } }) };
|
|
268
|
+
assert(svc.deriveRunKind(cfg, 'r', depFail) === 'failed', 'deriveRunKind: step failed → failed');
|
|
269
|
+
const depNone = { findStateByRunId: () => null };
|
|
270
|
+
assert(svc.deriveRunKind(cfg, 'r', depNone) === null, 'deriveRunKind: run não encontrado → null (nada a reconciliar)');
|
|
271
|
+
|
|
272
|
+
// reconcileMarker + approveRun: o marcador SAI de awaiting/ quando aprovado (achado 5+6 juntos).
|
|
273
|
+
const queueDir = tmpDir('approve-q');
|
|
274
|
+
const cfg2 = { queueDir, bundleDir: '/opt/bundle', runsRoot: tmpDir('approve-runs'), pollMs: 1 };
|
|
275
|
+
const dirs = svc.ensureQueue(queueDir);
|
|
276
|
+
const runId = 'pex-hn-carousel-1';
|
|
277
|
+
fs.writeFileSync(path.join(dirs.awaiting, `${runId}.json`), JSON.stringify({ pod: 'hn-carousel' })); // pausado no checkpoint
|
|
278
|
+
|
|
279
|
+
// approveRun com spawn INJETADO (sem executor real): simula exit 0 + RESULTADO {approved} e o run
|
|
280
|
+
// agora com todos os steps done → reconcilia awaiting/ → done/.
|
|
281
|
+
const EventEmitter = require('node:events');
|
|
282
|
+
const fakeSpawn = () => {
|
|
283
|
+
const child = new EventEmitter();
|
|
284
|
+
child.stdout = new EventEmitter();
|
|
285
|
+
setImmediate(() => { child.stdout.emit('data', Buffer.from('\nRESULTADO: {"approved":true,"next":"complete"}\n')); child.emit('exit', 0); });
|
|
286
|
+
return child;
|
|
287
|
+
};
|
|
288
|
+
const capturedArgs = [];
|
|
289
|
+
const spyingSpawn = (bin, a, opts) => { capturedArgs.push(a); return fakeSpawn(bin, a, opts); };
|
|
290
|
+
// O approve agora é GATEADO (SF10.4/DF3-3): passa a autorização válida (aprovador + segredo certo)
|
|
291
|
+
// para exercitar a reconciliação (achado 6). O enforcement em si é coberto por identityTests.
|
|
292
|
+
const idModel = { identities: [{ id: 'daniel', is_approver: true }], approver: { id: 'daniel', is_approver: true }, byId: { daniel: { id: 'daniel', is_approver: true } } };
|
|
293
|
+
const secretHash = svc.hashApproverSecret('correct horse');
|
|
294
|
+
return svc.approveRun(cfg2, runId, {
|
|
295
|
+
spawn: spyingSpawn, log: () => {},
|
|
296
|
+
identityModel: idModel, asId: 'daniel', approverSecret: 'correct horse', approverSecretHash: secretHash,
|
|
297
|
+
findStateByRunId: () => '/x/state.json',
|
|
298
|
+
readState: () => ({ steps: { digest: { status: 'done' }, visuals: { status: 'done' }, assembly: { status: 'done' } } }),
|
|
299
|
+
}).then((r) => {
|
|
300
|
+
assert(r.code === 0, 'approveRun resolve exit 0 do executor');
|
|
301
|
+
const passedArgs = capturedArgs[0];
|
|
302
|
+
assert(passedArgs.indexOf('--runs-root') !== -1 && passedArgs[passedArgs.indexOf('--runs-root') + 1] === cfg2.runsRoot, 'approveRun invocou o executor COM --runs-root da config (achado 6)');
|
|
303
|
+
assert(!fs.existsSync(path.join(dirs.awaiting, `${runId}.json`)), 'achado 5: o marcador SAIU de awaiting/ após o approve');
|
|
304
|
+
assert(fs.existsSync(path.join(dirs.done, `${runId}.json`)), 'achado 5: run aprovado e concluído → marcador reconciliado para done/');
|
|
305
|
+
const rr = JSON.parse(fs.readFileSync(path.join(dirs.done, `${runId}.result.json`), 'utf8'));
|
|
306
|
+
assert(rr.kind === 'done' && rr.reconciledBy === 'approve', 'result.json do reconciliado registra kind=done e reconciledBy=approve');
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ===========================================================================
|
|
311
|
+
// SF10.4 (F3) — modelo de identidade: started_by (seleção) + approve (segredo) + ledger de uso.
|
|
312
|
+
// ===========================================================================
|
|
313
|
+
|
|
314
|
+
// Escreve um arquivo de config de identidades e devolve seu path.
|
|
315
|
+
function writeIdentities(obj) {
|
|
316
|
+
const p = path.join(tmpDir('ids'), 'identities.json');
|
|
317
|
+
fs.writeFileSync(p, JSON.stringify(obj));
|
|
318
|
+
return p;
|
|
319
|
+
}
|
|
320
|
+
const VALID_IDS = { identities: [{ id: 'daniel', label: 'Daniel', is_approver: true }, { id: 'alice', label: 'Alice' }, { id: 'bob', label: 'Bob' }] };
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// AC2/AC6 + invariante — loader fail-loud da config de identidades (exatamente 1 is_approver).
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
function identityConfigTests() {
|
|
326
|
+
console.log('\n--- SF10.4 AC6/DF3-5: loader de identidades fail-loud (exatamente 1 is_approver) ---');
|
|
327
|
+
const okFile = writeIdentities(VALID_IDS);
|
|
328
|
+
const m = svc.loadIdentities({ file: okFile });
|
|
329
|
+
assert(m.identities.length === 3 && m.approver.id === 'daniel', 'config válida: carrega e identifica o aprovador único');
|
|
330
|
+
assert(svc.isKnownIdentity(m, 'alice') && !svc.isKnownIdentity(m, 'mallory'), 'isKnownIdentity distingue conhecido de desconhecido');
|
|
331
|
+
|
|
332
|
+
assert(throwsFail(() => svc.loadIdentities({ file: undefined, env: {} })), 'PODEXPORT_IDENTITIES ausente → FAIL-LOUD');
|
|
333
|
+
assert(throwsFail(() => svc.loadIdentities({ file: '/nao/existe/ids.json' })), 'arquivo inexistente/ilegível → FAIL-LOUD');
|
|
334
|
+
const badJson = path.join(tmpDir('ids'), 'bad.json'); fs.writeFileSync(badJson, '{quebrado');
|
|
335
|
+
assert(throwsFail(() => svc.loadIdentities({ file: badJson })), 'JSON inválido → FAIL-LOUD');
|
|
336
|
+
assert(throwsFail(() => svc.loadIdentities({ file: writeIdentities({ identities: [] }) })), 'lista vazia → FAIL-LOUD');
|
|
337
|
+
assert(throwsFail(() => svc.loadIdentities({ file: writeIdentities({ identities: [{ id: 'a' }, { id: 'b' }] }) })), 'ZERO is_approver → FAIL-LOUD (aprovador designado é invariante)');
|
|
338
|
+
assert(throwsFail(() => svc.loadIdentities({ file: writeIdentities({ identities: [{ id: 'a', is_approver: true }, { id: 'b', is_approver: true }] }) })), '>1 is_approver → FAIL-LOUD (aprovador deve ser ÚNICO)');
|
|
339
|
+
assert(throwsFail(() => svc.loadIdentities({ file: writeIdentities({ identities: [{ id: 'dup', is_approver: true }, { id: 'dup' }] }) })), 'id duplicado → FAIL-LOUD');
|
|
340
|
+
assert(throwsFail(() => svc.loadIdentities({ file: writeIdentities({ identities: [{ id: '', is_approver: true }] }) })), 'id vazio → FAIL-LOUD');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// AC1/AC2 — started_by obrigatório e validado no enqueue (nunca run anônimo).
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
function startedByEnqueueTests() {
|
|
347
|
+
console.log('\n--- SF10.4 AC1/AC2: started_by obrigatório e validado no enqueue ---');
|
|
348
|
+
const queueDir = tmpDir('q-sb');
|
|
349
|
+
const idFile = writeIdentities(VALID_IDS);
|
|
350
|
+
const cfg = svc.loadConfig({ PODEXPORT_QUEUE_DIR: queueDir, PODEXPORT_IDENTITIES: idFile });
|
|
351
|
+
assert(cfg.identitiesFile === idFile, 'loadConfig expõe PODEXPORT_IDENTITIES em cfg.identitiesFile');
|
|
352
|
+
|
|
353
|
+
function payloadFile(obj) { const p = path.join(tmpDir('pl'), 'p.json'); fs.writeFileSync(p, JSON.stringify(obj)); return p; }
|
|
354
|
+
|
|
355
|
+
const good = svc.enqueue(cfg, payloadFile({ pod: 'hn-carousel', started_by: 'alice' }));
|
|
356
|
+
assert(fs.existsSync(good) && JSON.parse(fs.readFileSync(good, 'utf8')).started_by === 'alice', 'enqueue com started_by conhecido → enfileira e persiste started_by');
|
|
357
|
+
assert(throwsFail(() => svc.enqueue(cfg, payloadFile({ pod: 'x' }))), 'started_by AUSENTE → recusa fail-loud (nunca run anônimo)');
|
|
358
|
+
assert(throwsFail(() => svc.enqueue(cfg, payloadFile({ pod: 'x', started_by: 'mallory' }))), 'started_by DESCONHECIDO (fora da lista) → recusa fail-loud');
|
|
359
|
+
assert(throwsFail(() => svc.enqueue(cfg, payloadFile({ pod: 'x', started_by: ' ' }))), 'started_by em branco → recusa fail-loud');
|
|
360
|
+
// Açúcar --started-by injeta o campo num payload que não o traz.
|
|
361
|
+
const injected = svc.enqueue(cfg, payloadFile({ pod: 'x' }), { startedBy: 'bob' });
|
|
362
|
+
assert(JSON.parse(fs.readFileSync(injected, 'utf8')).started_by === 'bob', '--started-by injeta started_by quando o payload não o traz');
|
|
363
|
+
assert(throwsFail(() => svc.enqueue(cfg, payloadFile({ pod: 'x' }), { startedBy: 'mallory' })), '--started-by com id desconhecido → recusa (o açúcar não contorna a validação)');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// AC3/AC5 — started_by persistido no result.json + ledger de uso agregado por pessoa.
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
async function persistenceAndUsageTests() {
|
|
370
|
+
console.log('\n--- SF10.4 AC3/AC5: started_by persistido + ledger de uso por started_by ---');
|
|
371
|
+
const queueDir = tmpDir('q-usage');
|
|
372
|
+
const cfg = { queueDir, bundleDir: null, runsRoot: tmpDir('runs-u'), pollMs: 1 };
|
|
373
|
+
const dirs = svc.ensureQueue(queueDir);
|
|
374
|
+
// 3 runs: alice (usage 100/50/2), bob (200/10/0), alice de novo (50/25/1).
|
|
375
|
+
fs.writeFileSync(path.join(dirs.pending, 'a1.json'), JSON.stringify({ pod: 'hn', started_by: 'alice', n: 1 }));
|
|
376
|
+
fs.writeFileSync(path.join(dirs.pending, 'b1.json'), JSON.stringify({ pod: 'hn', started_by: 'bob', n: 2 }));
|
|
377
|
+
fs.writeFileSync(path.join(dirs.pending, 'a2.json'), JSON.stringify({ pod: 'hn', started_by: 'alice', n: 3 }));
|
|
378
|
+
const deps = {
|
|
379
|
+
log: () => {},
|
|
380
|
+
runExecutor: async (c, processingPath) => {
|
|
381
|
+
const p = JSON.parse(fs.readFileSync(processingPath, 'utf8'));
|
|
382
|
+
// `usage` no formato EXATO que o executor agora expõe no RESULTADO (DF3-4 fechado): o ledger
|
|
383
|
+
// registra uso REAL, nunca 0. Ver a prova de agregação por fase em pod-export-executor.test.cjs.
|
|
384
|
+
const u = { 1: { input_tokens: 100, output_tokens: 50, images: 2 }, 2: { input_tokens: 200, output_tokens: 10, images: 0 }, 3: { input_tokens: 50, output_tokens: 25, images: 1 } }[p.n];
|
|
385
|
+
return { code: 0, result: { complete: true, usage: u } };
|
|
386
|
+
},
|
|
387
|
+
sleep: async () => {}, stopAfter: 1,
|
|
388
|
+
env: { ANTHROPIC_API_KEY: 'x', OPENROUTER_API_KEY: 'y' }, probeChrome: () => '/usr/bin/chromium', evalVeto: motor.evalVeto, validHtmlPath: VALID_HTML,
|
|
389
|
+
};
|
|
390
|
+
const res = await svc.serve(cfg, deps);
|
|
391
|
+
assert(res.processed.length === 3, 'serve processou os 3 runs');
|
|
392
|
+
const doneRun = res.processed.find((r) => r.kind === 'done');
|
|
393
|
+
const rj = JSON.parse(fs.readFileSync(doneRun.resultPath, 'utf8'));
|
|
394
|
+
assert(typeof rj.started_by === 'string', 'AC3: result.json persiste started_by do run');
|
|
395
|
+
|
|
396
|
+
const agg = svc.aggregateUsage(cfg);
|
|
397
|
+
assert(agg.alice && agg.alice.runs === 2 && agg.alice.tokens_in === 150 && agg.alice.tokens_out === 75 && agg.alice.imagens === 3, 'AC5: usage agregado de alice (2 runs, 150 in / 75 out / 3 img)');
|
|
398
|
+
assert(agg.bob && agg.bob.runs === 1 && agg.bob.tokens_in === 200, 'AC5: usage agregado de bob (1 run, 200 in)');
|
|
399
|
+
assert(fs.existsSync(svc.usageLogPath(cfg)), 'ledger usage.log.jsonl append-only foi escrito');
|
|
400
|
+
// Ledger é freio, não billing: só agrega, não capa (nenhum campo de limite/enforcement).
|
|
401
|
+
assert(!('limit' in agg.alice) && !('cap' in agg.alice), 'ledger é VER (agrega), NUNCA CAPAR (sem limite/cap) — linha 127 do ADR');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// AC4 (CRÍTICO, alvo do QA adversarial) — approve: SELEÇÃO ≠ AUTORIZAÇÃO (segredo do aprovador).
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
async function approveEnforcementTests() {
|
|
408
|
+
console.log('\n--- SF10.4 AC4 (adversarial): approve exige is_approver E segredo (scrypt+timingSafeEqual) ---');
|
|
409
|
+
const idModel = svc.loadIdentities({ file: writeIdentities(VALID_IDS) });
|
|
410
|
+
const SECRET = 'correct horse battery staple';
|
|
411
|
+
const HASH = svc.hashApproverSecret(SECRET);
|
|
412
|
+
|
|
413
|
+
// Roundtrip de hash/verify + timing-safe.
|
|
414
|
+
assert(svc.verifyApproverSecret(SECRET, HASH), 'verifyApproverSecret aceita o segredo certo (roundtrip scrypt)');
|
|
415
|
+
assert(!svc.verifyApproverSecret('errado', HASH), 'verifyApproverSecret RECUSA segredo errado');
|
|
416
|
+
assert(!svc.verifyApproverSecret(SECRET, 'formato-invalido'), 'verifyApproverSecret RECUSA hash malformado (sem crashar)');
|
|
417
|
+
assert(!svc.verifyApproverSecret('', HASH), 'verifyApproverSecret RECUSA segredo vazio');
|
|
418
|
+
assert(svc.hashApproverSecret(SECRET) !== svc.hashApproverSecret(SECRET), 'hash usa salt aleatório (dois hashes do mesmo segredo diferem)');
|
|
419
|
+
|
|
420
|
+
// authorizeApproval — a função PURA do gate. Cada furo que o QA tentaria:
|
|
421
|
+
const A = (over) => svc.authorizeApproval({ identityModel: idModel, asId: 'daniel', providedSecret: SECRET, secretHash: HASH, ...over });
|
|
422
|
+
assert(A().ok, 'aprovador designado (daniel) + segredo CERTO → AUTORIZADO');
|
|
423
|
+
assert(!A({ providedSecret: 'errado' }).ok, '(a) segredo ERRADO → RECUSADO (mesmo sendo o aprovador)');
|
|
424
|
+
assert(!A({ providedSecret: '' }).ok && /segredo do aprovador ausente/.test(A({ providedSecret: '' }).reason), '(a) segredo AUSENTE → RECUSADO');
|
|
425
|
+
assert(!A({ asId: 'alice' }).ok && /NÃO é a aprovadora/.test(A({ asId: 'alice' }).reason), '(b) identidade NÃO-aprovadora (alice) + segredo certo → RECUSADO (seleção não autoriza)');
|
|
426
|
+
assert(!A({ asId: 'mallory' }).ok, '(b) identidade DESCONHECIDA → RECUSADO');
|
|
427
|
+
assert(!A({ asId: '' }).ok, '(b) identidade VAZIA → RECUSADO');
|
|
428
|
+
assert(!A({ asId: 'Daniel' }).ok, '(b) variação de CAIXA (Daniel≠daniel) → RECUSADO (não é igualdade frouxa)');
|
|
429
|
+
assert(!A({ asId: undefined }).ok, '(b) SEM parâmetro de identidade → RECUSADO');
|
|
430
|
+
assert(!A({ secretHash: undefined }).ok && /não configurado/.test(A({ secretHash: undefined }).reason), '(e) hash do aprovador NÃO configurado → RECUSADO fail-loud');
|
|
431
|
+
|
|
432
|
+
// O segredo NUNCA aparece na razão da recusa (nunca vaza em log).
|
|
433
|
+
const reasonLeaks = [A({ providedSecret: SECRET, asId: 'alice' }), A({ providedSecret: 'ERRADO-SECRETO' }), A({ asId: 'mallory', providedSecret: SECRET })]
|
|
434
|
+
.some((r) => String(r.reason).includes(SECRET) || String(r.reason).includes('ERRADO-SECRETO'));
|
|
435
|
+
assert(!reasonLeaks, 'razão de recusa NUNCA contém o segredo (fornecido ou correto) em claro');
|
|
436
|
+
|
|
437
|
+
// approveRun (backend, MESMO code-path da CLI/UI): chamada DIRETA com segredo errado é RECUSADA,
|
|
438
|
+
// sem spawn e sem mover o marcador de awaiting/ (o teste adversarial do QA — padrão SF10.3).
|
|
439
|
+
const queueDir = tmpDir('q-approve');
|
|
440
|
+
const cfg = { queueDir, bundleDir: '/opt/bundle', runsRoot: tmpDir('runs-a'), pollMs: 1, identitiesFile: writeIdentities(VALID_IDS) };
|
|
441
|
+
const dirs = svc.ensureQueue(queueDir);
|
|
442
|
+
const runId = 'pex-guarded-1';
|
|
443
|
+
fs.writeFileSync(path.join(dirs.awaiting, `${runId}.json`), JSON.stringify({ pod: 'hn', started_by: 'alice' }));
|
|
444
|
+
let spawned = 0; const spySpawn = () => { spawned++; throw new Error('spawn NÃO deveria ocorrer numa recusa'); };
|
|
445
|
+
const logs = [];
|
|
446
|
+
const denied = await svc.approveRun(cfg, runId, { spawn: spySpawn, log: (m) => logs.push(m), asId: 'daniel', approverSecret: 'ERRADO', approverSecretHash: HASH });
|
|
447
|
+
assert(denied.denied && denied.code !== 0, 'approveRun com segredo ERRADO → RECUSADO (code != 0), sem aprovar');
|
|
448
|
+
assert(spawned === 0, 'approveRun recusado NÃO chamou spawn (nenhum efeito no executor)');
|
|
449
|
+
assert(fs.existsSync(path.join(dirs.awaiting, `${runId}.json`)), 'marcador PERMANECE em awaiting/ após recusa (nada foi reconciliado)');
|
|
450
|
+
assert(logs.some((m) => /RECUSADO/.test(m)) && !logs.some((m) => m.includes(SECRET) || m.includes('ERRADO')), 'recusa é LOGADA mas NUNCA loga o segredo (claro ou fornecido)');
|
|
451
|
+
|
|
452
|
+
// Não-aprovadora com segredo certo → recusada, também sem spawn.
|
|
453
|
+
const denied2 = await svc.approveRun(cfg, runId, { spawn: spySpawn, log: () => {}, asId: 'alice', approverSecret: SECRET, approverSecretHash: HASH });
|
|
454
|
+
assert(denied2.denied && spawned === 0, 'approveRun por identidade não-aprovadora (alice) mesmo com segredo certo → RECUSADO, sem spawn');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// loadConfig — parametrização por env (genérico, sem instância VPS embutida).
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
function configTests() {
|
|
461
|
+
console.log('\n--- Config genérico por ENV (sem instância VPS hardcoded) ---');
|
|
462
|
+
const cfg = svc.loadConfig({ PODEXPORT_BUNDLE_DIR: '/b', PODEXPORT_QUEUE_DIR: '/q', PODEXPORT_POLL_MS: '500' });
|
|
463
|
+
assert(cfg.bundleDir === path.resolve('/b') && cfg.queueDir === path.resolve('/q'), 'loadConfig resolve bundle/queue do env');
|
|
464
|
+
assert(cfg.pollMs === 500, 'loadConfig lê PODEXPORT_POLL_MS');
|
|
465
|
+
const empty = svc.loadConfig({});
|
|
466
|
+
assert(empty.bundleDir === null && empty.queueDir === null, 'sem env → cfg sem paths (start exige explicitamente, fail-loud no CLI)');
|
|
467
|
+
assert(empty.runsRoot.includes('.power-claude'), 'runsRoot tem default sob .power-claude');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function run() {
|
|
471
|
+
console.log('=== SF10.3 — smoke hermético do serviço-irmão (sem VPS) ===');
|
|
472
|
+
envKeyTests();
|
|
473
|
+
frontierTests();
|
|
474
|
+
chromePreflightTests();
|
|
475
|
+
coherencePreflightTests();
|
|
476
|
+
await queuePickupTests();
|
|
477
|
+
outcomeRoutingTests();
|
|
478
|
+
await approveWiringTests();
|
|
479
|
+
identityConfigTests();
|
|
480
|
+
startedByEnqueueTests();
|
|
481
|
+
await persistenceAndUsageTests();
|
|
482
|
+
await approveEnforcementTests();
|
|
483
|
+
configTests();
|
|
484
|
+
|
|
485
|
+
console.log(`\n=== ${failures === 0 ? 'PASS' : 'FALHOU'} — ${failures} falha(s) ===`);
|
|
486
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
run().catch((e) => { console.error('[ERRO FATAL]', e); process.exit(1); });
|