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,793 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deterministic state engine for the feedback sweep (ce-sweep).
|
|
3
|
+
|
|
4
|
+
This helper owns ALL reads and writes of the sweep state file. Peer agents
|
|
5
|
+
(source connectors, the analyzer, the orchestrator) never edit the file by
|
|
6
|
+
hand — they go through these subcommands so the schema contract, the id-keyed
|
|
7
|
+
merge, the single-writer lease, and the closed-item evidence rule are enforced
|
|
8
|
+
in exactly one place. See `references/state-schema.md` for the cross-agent
|
|
9
|
+
contract this script implements.
|
|
10
|
+
|
|
11
|
+
Design rules (shared with the repo's other state helpers):
|
|
12
|
+
- Pure Python 3 stdlib. No third-party dependencies.
|
|
13
|
+
- Every OPERATIONAL failure path prints a parseable STATUS WORD on line 1 and
|
|
14
|
+
exits 0 — it never raises a traceback to the caller. Only genuine CLI
|
|
15
|
+
misuse (bad/missing subcommand args) exits non-zero via argparse.
|
|
16
|
+
- Writes are atomic: a temp file in the state dir + os.replace (atomic on
|
|
17
|
+
POSIX), so a concurrent reader never sees a torn file.
|
|
18
|
+
- The script never calls the wall clock for the values it stores EXCEPT the
|
|
19
|
+
lease timestamp (staleness needs "now"). Tests pin it with --now / stamp
|
|
20
|
+
values with --timestamp so behavior is reproducible.
|
|
21
|
+
|
|
22
|
+
STATUS WORDS (line 1 of stdout for every subcommand):
|
|
23
|
+
OK success (an optional JSON payload follows on line 2)
|
|
24
|
+
NO-STATE read: the state file does not exist yet
|
|
25
|
+
CORRUPT the state file exists but does not parse as our schema
|
|
26
|
+
LOCKED lease-acquire: a live lease is held by another writer
|
|
27
|
+
STALE-RECLAIMED lease-acquire: an expired lease was taken over (JSON payload)
|
|
28
|
+
LEASE-LOST a mutating call was made by a writer that does not own the
|
|
29
|
+
lease (or a release of another writer's lease); no write
|
|
30
|
+
REFUSED cursor-advance: unknown past-item, or non-monotonic cursor
|
|
31
|
+
ERROR an unexpected internal error (defensive; never a traceback)
|
|
32
|
+
|
|
33
|
+
The state file is genuine YAML restricted to a small, deliberate subset so a
|
|
34
|
+
hand-written stdlib serializer/parser round-trips it deterministically. See
|
|
35
|
+
`references/state-schema.md` (section "YAML subset") for the exact grammar:
|
|
36
|
+
non-empty dict values become block mappings (any depth); scalars are emitted as
|
|
37
|
+
JSON tokens (strings always double-quoted on one line); lists and empty dicts
|
|
38
|
+
are emitted as inline JSON flow on a single line — itself valid YAML.
|
|
39
|
+
"""
|
|
40
|
+
import argparse
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import sys
|
|
44
|
+
import tempfile
|
|
45
|
+
from datetime import datetime, timezone
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import fcntl # POSIX advisory locks (macOS, Linux — this repo's Unix targets)
|
|
49
|
+
_HAS_FCNTL = True
|
|
50
|
+
except ImportError: # non-POSIX; degrade to unlocked (single-writer by convention)
|
|
51
|
+
_HAS_FCNTL = False
|
|
52
|
+
|
|
53
|
+
SCHEMA_VERSION = 1
|
|
54
|
+
|
|
55
|
+
# The closed lifecycle status enum is documented in references/state-schema.md.
|
|
56
|
+
# Unknown statuses are preserved on write-back, never dropped — nothing here
|
|
57
|
+
# whitelists values.
|
|
58
|
+
|
|
59
|
+
# A `closed` item MUST carry all three evidence fields; `validate` downgrades
|
|
60
|
+
# any closed item missing any of them back to `fix_pending`.
|
|
61
|
+
EVIDENCE_FIELDS = ("fix_ref", "verified_merge_sha", "verified_at")
|
|
62
|
+
|
|
63
|
+
DEFAULT_TTL_MINUTES = 60
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --------------------------------------------------------------------------- #
|
|
67
|
+
# Minimal YAML subset: serializer + parser
|
|
68
|
+
# --------------------------------------------------------------------------- #
|
|
69
|
+
# Grammar (2-space indent, no tabs):
|
|
70
|
+
# - Block mappings: `<key>: <value>` or `<key>:` (nested mapping follows).
|
|
71
|
+
# - Keys: a bare token matching _SAFE_KEY, else a JSON double-quoted string.
|
|
72
|
+
# - Scalars: null / true / false / integers / floats emitted bare; strings
|
|
73
|
+
# always emitted as JSON double-quoted (single line, fully escaped).
|
|
74
|
+
# - Nested containers (dict/list) that are NOT part of the known block
|
|
75
|
+
# structure are emitted as inline JSON flow on one line — valid YAML.
|
|
76
|
+
# The emitter only produces block MAPPINGS (never block sequences); the parser
|
|
77
|
+
# tolerates arbitrary-depth block mappings plus scalar / inline-JSON values.
|
|
78
|
+
|
|
79
|
+
import re
|
|
80
|
+
|
|
81
|
+
_SAFE_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")
|
|
82
|
+
|
|
83
|
+
# Preferred key orders for deterministic, human-legible output. Any keys not
|
|
84
|
+
# listed are appended in sorted order so unknown/preserved fields stay stable.
|
|
85
|
+
_DOC_ORDER = ("schema_version", "lease", "sources", "items", "last_run")
|
|
86
|
+
_LEASE_ORDER = ("writer", "timestamp", "ttl_minutes")
|
|
87
|
+
_ITEM_ORDER = (
|
|
88
|
+
"source", "status", "sensitive", "title", "url", "body", "quote",
|
|
89
|
+
"fix_ref", "verified_merge_sha", "verified_at",
|
|
90
|
+
)
|
|
91
|
+
_LAST_RUN_ORDER = ("timestamp", "outcome", "writer", "counts")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _ordered_keys(d, preferred):
|
|
95
|
+
seen = [k for k in preferred if k in d]
|
|
96
|
+
rest = sorted(k for k in d if k not in preferred)
|
|
97
|
+
return seen + rest
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _emit_key(key):
|
|
101
|
+
key = str(key)
|
|
102
|
+
if _SAFE_KEY.match(key):
|
|
103
|
+
return key
|
|
104
|
+
return json.dumps(key, ensure_ascii=False)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _emit_scalar(v):
|
|
108
|
+
if v is None:
|
|
109
|
+
return "null"
|
|
110
|
+
if v is True:
|
|
111
|
+
return "true"
|
|
112
|
+
if v is False:
|
|
113
|
+
return "false"
|
|
114
|
+
if isinstance(v, int):
|
|
115
|
+
return str(v)
|
|
116
|
+
if isinstance(v, float):
|
|
117
|
+
return repr(v)
|
|
118
|
+
if isinstance(v, str):
|
|
119
|
+
return json.dumps(v, ensure_ascii=False)
|
|
120
|
+
# dict / list -> inline JSON flow (valid YAML), sorted for determinism.
|
|
121
|
+
return json.dumps(v, ensure_ascii=False, sort_keys=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _emit_mapping(d, indent, preferred):
|
|
125
|
+
"""Emit a dict as block YAML. Only nests block-style for child dicts;
|
|
126
|
+
lists and any other container leaf are emitted inline via _emit_scalar."""
|
|
127
|
+
pad = " " * indent
|
|
128
|
+
lines = []
|
|
129
|
+
for key in _ordered_keys(d, preferred):
|
|
130
|
+
val = d[key]
|
|
131
|
+
ks = _emit_key(key)
|
|
132
|
+
if isinstance(val, dict) and val:
|
|
133
|
+
lines.append(f"{pad}{ks}:")
|
|
134
|
+
# Nested item/source/lease/last_run maps: no special preferred order
|
|
135
|
+
# for arbitrary nested dicts; only the top structural maps below get
|
|
136
|
+
# a preferred order (handled by their callers).
|
|
137
|
+
child_pref = _child_preferred(indent, key)
|
|
138
|
+
lines.extend(_emit_mapping(val, indent + 1, child_pref))
|
|
139
|
+
else:
|
|
140
|
+
lines.append(f"{pad}{ks}: {_emit_scalar(val)}")
|
|
141
|
+
return lines
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _child_preferred(parent_indent, key):
|
|
145
|
+
"""Preferred child-key order for the known structural containers."""
|
|
146
|
+
if parent_indent == 0:
|
|
147
|
+
if key == "lease":
|
|
148
|
+
return _LEASE_ORDER
|
|
149
|
+
if key == "last_run":
|
|
150
|
+
return _LAST_RUN_ORDER
|
|
151
|
+
return () # sources / items: children are id-keyed, no field order
|
|
152
|
+
# A source entry or item entry: apply the item field order (harmless for
|
|
153
|
+
# source entries, which only carry `cursor`/`sensitive`).
|
|
154
|
+
return _ITEM_ORDER
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def emit_document(state):
|
|
158
|
+
lines = _emit_mapping(state, 0, _DOC_ORDER)
|
|
159
|
+
return "\n".join(lines) + "\n"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _split_key(content):
|
|
163
|
+
"""Split a mapping line into (key, rest-after-colon). Raises ValueError on
|
|
164
|
+
a line with no colon so a malformed file surfaces as CORRUPT."""
|
|
165
|
+
if content.startswith('"'):
|
|
166
|
+
key, end = json.JSONDecoder().raw_decode(content)
|
|
167
|
+
rest = content[end:]
|
|
168
|
+
if not rest.startswith(":"):
|
|
169
|
+
raise ValueError("expected ':' after quoted key")
|
|
170
|
+
return key, rest[1:]
|
|
171
|
+
idx = content.find(":")
|
|
172
|
+
if idx == -1:
|
|
173
|
+
raise ValueError("expected ':' in mapping line")
|
|
174
|
+
return content[:idx], content[idx + 1:]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_BLOCK = object() # sentinel: value is a nested block, parse deeper lines
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_value(rest):
|
|
181
|
+
rest = rest.strip()
|
|
182
|
+
if rest == "":
|
|
183
|
+
return _BLOCK
|
|
184
|
+
if rest == "null":
|
|
185
|
+
return None
|
|
186
|
+
if rest == "true":
|
|
187
|
+
return True
|
|
188
|
+
if rest == "false":
|
|
189
|
+
return False
|
|
190
|
+
first = rest[0]
|
|
191
|
+
if first in '"{[':
|
|
192
|
+
return json.loads(rest) # JSON string, object, or array (flow style)
|
|
193
|
+
if first == "-" or first.isdigit():
|
|
194
|
+
try:
|
|
195
|
+
if "." in rest or "e" in rest or "E" in rest:
|
|
196
|
+
return float(rest)
|
|
197
|
+
return int(rest)
|
|
198
|
+
except ValueError:
|
|
199
|
+
pass
|
|
200
|
+
# Emitter never produces bare unquoted strings; a bare token here is a
|
|
201
|
+
# hand-edit — keep it verbatim rather than failing.
|
|
202
|
+
return rest
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_mapping(rows, cursor, indent):
|
|
206
|
+
result = {}
|
|
207
|
+
while cursor["i"] < len(rows):
|
|
208
|
+
cur_indent, content = rows[cursor["i"]]
|
|
209
|
+
if cur_indent < indent:
|
|
210
|
+
break
|
|
211
|
+
if cur_indent > indent:
|
|
212
|
+
# Orphan deeper line with no parent key; skip defensively.
|
|
213
|
+
cursor["i"] += 1
|
|
214
|
+
continue
|
|
215
|
+
cursor["i"] += 1
|
|
216
|
+
key, rest = _split_key(content)
|
|
217
|
+
val = _parse_value(rest)
|
|
218
|
+
if val is _BLOCK:
|
|
219
|
+
if cursor["i"] < len(rows) and rows[cursor["i"]][0] > indent:
|
|
220
|
+
val = _parse_mapping(rows, cursor, rows[cursor["i"]][0])
|
|
221
|
+
else:
|
|
222
|
+
val = {}
|
|
223
|
+
result[key] = val
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def parse_document(text):
|
|
228
|
+
rows = []
|
|
229
|
+
for raw in text.split("\n"):
|
|
230
|
+
stripped = raw.strip()
|
|
231
|
+
if not stripped or stripped.startswith("#"):
|
|
232
|
+
continue
|
|
233
|
+
indent = len(raw) - len(raw.lstrip(" "))
|
|
234
|
+
rows.append((indent, stripped))
|
|
235
|
+
if not rows:
|
|
236
|
+
return {}
|
|
237
|
+
return _parse_mapping(rows, {"i": 0}, rows[0][0])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------- #
|
|
241
|
+
# State load / save
|
|
242
|
+
# --------------------------------------------------------------------------- #
|
|
243
|
+
|
|
244
|
+
def _item_key(source, item_id):
|
|
245
|
+
"""Storage key for an item. The items keyspace is flat, so a bare id can
|
|
246
|
+
collide across sources (two Slack channels emitting the same ts, or a
|
|
247
|
+
source that reuses numeric ids). Namespacing by source keeps each source's
|
|
248
|
+
id space independent, matching the composite keys documented in
|
|
249
|
+
references/state-schema.md."""
|
|
250
|
+
return "{}:{}".format(source, item_id)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def load_state(path):
|
|
254
|
+
"""Return (status, data): ('absent', None), ('corrupt', None), or
|
|
255
|
+
('ok', dict). A file that parses but lacks schema_version is corrupt."""
|
|
256
|
+
try:
|
|
257
|
+
with open(path, encoding="utf-8") as f:
|
|
258
|
+
# A machine-local state file can live under world-shared /tmp, and
|
|
259
|
+
# it is a correctness dependency (lease, cursors, closed status) as
|
|
260
|
+
# well as an injection sink (item bodies re-read into agent
|
|
261
|
+
# context). Reject a file not owned by us so a co-tenant cannot
|
|
262
|
+
# plant a forged lease/cursor or attacker-authored item text. Skip
|
|
263
|
+
# where geteuid is unavailable (non-POSIX), where the threat does
|
|
264
|
+
# not apply.
|
|
265
|
+
geteuid = getattr(os, "geteuid", None)
|
|
266
|
+
if geteuid is not None and os.fstat(f.fileno()).st_uid != geteuid():
|
|
267
|
+
return ("corrupt", None)
|
|
268
|
+
text = f.read()
|
|
269
|
+
except FileNotFoundError:
|
|
270
|
+
return ("absent", None)
|
|
271
|
+
except (OSError, UnicodeDecodeError):
|
|
272
|
+
return ("corrupt", None)
|
|
273
|
+
if not text.strip():
|
|
274
|
+
return ("absent", None)
|
|
275
|
+
try:
|
|
276
|
+
data = parse_document(text)
|
|
277
|
+
except Exception:
|
|
278
|
+
return ("corrupt", None)
|
|
279
|
+
if not isinstance(data, dict) or "schema_version" not in data:
|
|
280
|
+
return ("corrupt", None)
|
|
281
|
+
data.setdefault("sources", {})
|
|
282
|
+
data.setdefault("items", {})
|
|
283
|
+
return ("ok", data)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def new_state():
|
|
287
|
+
return {"schema_version": SCHEMA_VERSION, "sources": {}, "items": {}}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def write_state(path, state):
|
|
291
|
+
"""Atomic write of the state file. Returns True on success."""
|
|
292
|
+
state["schema_version"] = SCHEMA_VERSION
|
|
293
|
+
text = emit_document(state)
|
|
294
|
+
d = os.path.dirname(os.path.abspath(path))
|
|
295
|
+
os.makedirs(d, exist_ok=True)
|
|
296
|
+
fd, tmp = tempfile.mkstemp(dir=d, prefix=".tmp-sweep-", suffix=".yml")
|
|
297
|
+
try:
|
|
298
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
|
|
299
|
+
f.write(text)
|
|
300
|
+
os.replace(tmp, path)
|
|
301
|
+
except BaseException:
|
|
302
|
+
try:
|
|
303
|
+
os.unlink(tmp)
|
|
304
|
+
except OSError:
|
|
305
|
+
pass
|
|
306
|
+
raise
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# --------------------------------------------------------------------------- #
|
|
311
|
+
# Output + time helpers
|
|
312
|
+
# --------------------------------------------------------------------------- #
|
|
313
|
+
|
|
314
|
+
def emit(status_word, payload=None):
|
|
315
|
+
print(status_word)
|
|
316
|
+
if payload is not None:
|
|
317
|
+
print(json.dumps(payload))
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def resolve_now(args):
|
|
322
|
+
"""The 'current time' for lease staleness + lease re-stamping. Pinned by
|
|
323
|
+
--now in tests; otherwise the real UTC clock."""
|
|
324
|
+
now = getattr(args, "now", None)
|
|
325
|
+
if now:
|
|
326
|
+
return now
|
|
327
|
+
return datetime.now(timezone.utc).isoformat()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _parse_iso(s):
|
|
331
|
+
if not isinstance(s, str) or not s:
|
|
332
|
+
return None
|
|
333
|
+
try:
|
|
334
|
+
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
335
|
+
except ValueError:
|
|
336
|
+
return None
|
|
337
|
+
if dt.tzinfo is None:
|
|
338
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
339
|
+
return dt
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def current_lease(state):
|
|
343
|
+
"""The active lease dict (with a non-empty writer), or None."""
|
|
344
|
+
lease = state.get("lease")
|
|
345
|
+
if isinstance(lease, dict) and lease.get("writer"):
|
|
346
|
+
return lease
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def lease_is_stale(lease, now_iso):
|
|
351
|
+
"""True only when we can PROVE the lease is older than its TTL. If either
|
|
352
|
+
timestamp is unparseable we cannot prove staleness -> treat as live (do not
|
|
353
|
+
reclaim). Conservative: never stomp a lease we cannot show is expired."""
|
|
354
|
+
ts = _parse_iso(lease.get("timestamp", ""))
|
|
355
|
+
now = _parse_iso(now_iso)
|
|
356
|
+
if ts is None or now is None:
|
|
357
|
+
return False
|
|
358
|
+
try:
|
|
359
|
+
ttl = int(lease.get("ttl_minutes", DEFAULT_TTL_MINUTES))
|
|
360
|
+
except (TypeError, ValueError):
|
|
361
|
+
ttl = DEFAULT_TTL_MINUTES
|
|
362
|
+
return (now - ts).total_seconds() > ttl * 60
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def restamp_lease(state, writer, now_iso):
|
|
366
|
+
"""Refresh the owning writer's lease timestamp so a long sweep keeps it
|
|
367
|
+
alive across many writes. Only called after ownership is confirmed."""
|
|
368
|
+
lease = state.get("lease")
|
|
369
|
+
if isinstance(lease, dict) and lease.get("writer") == writer:
|
|
370
|
+
lease["timestamp"] = now_iso
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def owns_lease(state, writer):
|
|
374
|
+
lease = current_lease(state)
|
|
375
|
+
return lease is not None and lease.get("writer") == writer
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# --------------------------------------------------------------------------- #
|
|
379
|
+
# Subcommands
|
|
380
|
+
# --------------------------------------------------------------------------- #
|
|
381
|
+
|
|
382
|
+
def cmd_read(args):
|
|
383
|
+
st, data = load_state(args.state)
|
|
384
|
+
if st == "absent":
|
|
385
|
+
return emit("NO-STATE")
|
|
386
|
+
if st == "corrupt":
|
|
387
|
+
return emit("CORRUPT")
|
|
388
|
+
return emit("OK", data)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def cmd_validate(args):
|
|
392
|
+
st, data = load_state(args.state)
|
|
393
|
+
if st == "corrupt":
|
|
394
|
+
return emit("CORRUPT")
|
|
395
|
+
if st == "absent":
|
|
396
|
+
return emit("OK", {"downgraded": []})
|
|
397
|
+
downgraded = []
|
|
398
|
+
for item_id, item in data.get("items", {}).items():
|
|
399
|
+
if not isinstance(item, dict):
|
|
400
|
+
continue
|
|
401
|
+
if item.get("status") == "closed" and any(
|
|
402
|
+
not item.get(f) for f in EVIDENCE_FIELDS
|
|
403
|
+
):
|
|
404
|
+
item["status"] = "fix_pending"
|
|
405
|
+
downgraded.append(item_id)
|
|
406
|
+
if downgraded:
|
|
407
|
+
write_state(args.state, data)
|
|
408
|
+
return emit("OK", {"downgraded": sorted(downgraded)})
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _load_owned_state(args):
|
|
412
|
+
"""Load state for a lease-gated mutation. Returns (data, None) when
|
|
413
|
+
args.writer holds the lease, else (None, status_word) for the caller to
|
|
414
|
+
emit. Absent state means no lease to own -> the caller is not the owner."""
|
|
415
|
+
st, data = load_state(args.state)
|
|
416
|
+
if st == "corrupt":
|
|
417
|
+
return None, "CORRUPT"
|
|
418
|
+
if st == "absent" or not owns_lease(data, args.writer):
|
|
419
|
+
return None, "LEASE-LOST"
|
|
420
|
+
return data, None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _commit_owned(args, data):
|
|
424
|
+
"""Shared tail for lease-gated mutations: re-stamp the lease, persist."""
|
|
425
|
+
restamp_lease(data, args.writer, resolve_now(args))
|
|
426
|
+
write_state(args.state, data)
|
|
427
|
+
return emit("OK")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def cmd_upsert_item(args):
|
|
431
|
+
data, err = _load_owned_state(args)
|
|
432
|
+
if err:
|
|
433
|
+
return emit(err)
|
|
434
|
+
try:
|
|
435
|
+
incoming = json.loads(args.json)
|
|
436
|
+
except (ValueError, TypeError):
|
|
437
|
+
return emit("ERROR")
|
|
438
|
+
if not isinstance(incoming, dict):
|
|
439
|
+
return emit("ERROR")
|
|
440
|
+
|
|
441
|
+
items = data.setdefault("items", {})
|
|
442
|
+
key = _item_key(args.source, args.id)
|
|
443
|
+
existing = items.get(key)
|
|
444
|
+
merged = dict(existing) if isinstance(existing, dict) else {}
|
|
445
|
+
# id-keyed merge: only the keys present in the incoming item are replaced;
|
|
446
|
+
# unknown fields already on the item survive untouched.
|
|
447
|
+
merged.update(incoming)
|
|
448
|
+
merged["source"] = args.source
|
|
449
|
+
merged["id"] = args.id
|
|
450
|
+
|
|
451
|
+
source_entry = data.get("sources", {}).get(args.source, {})
|
|
452
|
+
is_sensitive = (
|
|
453
|
+
merged.get("sensitive") is True
|
|
454
|
+
or (isinstance(source_entry, dict) and source_entry.get("sensitive") is True)
|
|
455
|
+
)
|
|
456
|
+
if is_sensitive:
|
|
457
|
+
for f in ("body", "quote"):
|
|
458
|
+
merged.pop(f, None)
|
|
459
|
+
|
|
460
|
+
items[key] = merged
|
|
461
|
+
return _commit_owned(args, data)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def cmd_cursor_get(args):
|
|
465
|
+
st, data = load_state(args.state)
|
|
466
|
+
if st == "corrupt":
|
|
467
|
+
return emit("CORRUPT")
|
|
468
|
+
if st == "absent":
|
|
469
|
+
return emit("OK", {"cursor": None})
|
|
470
|
+
entry = data.get("sources", {}).get(args.source, {})
|
|
471
|
+
cursor = entry.get("cursor") if isinstance(entry, dict) else None
|
|
472
|
+
return emit("OK", {"cursor": cursor})
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def cmd_cursor_advance(args):
|
|
476
|
+
data, err = _load_owned_state(args)
|
|
477
|
+
if err:
|
|
478
|
+
return emit(err)
|
|
479
|
+
# The cursor may only advance past an item that actually exists in state,
|
|
480
|
+
# so a resume never skips unrecorded items.
|
|
481
|
+
if _item_key(args.source, args.past_item) not in data.get("items", {}):
|
|
482
|
+
return emit("REFUSED")
|
|
483
|
+
entry = data.setdefault("sources", {}).setdefault(args.source, {})
|
|
484
|
+
current = entry.get("cursor")
|
|
485
|
+
# Monotonic guard. A new cursor sorting strictly before the current one is
|
|
486
|
+
# refused; equal is allowed (idempotent re-advance).
|
|
487
|
+
if current is not None and _cursor_lt(str(args.to), str(current)):
|
|
488
|
+
return emit("REFUSED")
|
|
489
|
+
entry["cursor"] = args.to
|
|
490
|
+
return _commit_owned(args, data)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _cursor_lt(a, b):
|
|
494
|
+
"""True when cursor `a` precedes `b`. Pure-digit cursors compare
|
|
495
|
+
numerically so an unpadded id like '9' correctly precedes '10'; everything
|
|
496
|
+
else (Slack fixed-width ts, ISO timestamps) already sorts correctly as a
|
|
497
|
+
string, so fall back to lexical order."""
|
|
498
|
+
if a.isdigit() and b.isdigit():
|
|
499
|
+
return int(a) < int(b)
|
|
500
|
+
return a < b
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def cmd_lease_acquire(args):
|
|
504
|
+
st, data = load_state(args.state)
|
|
505
|
+
if st == "corrupt":
|
|
506
|
+
return emit("CORRUPT")
|
|
507
|
+
if st == "absent":
|
|
508
|
+
data = new_state()
|
|
509
|
+
now = resolve_now(args)
|
|
510
|
+
ttl = args.ttl_minutes if args.ttl_minutes is not None else DEFAULT_TTL_MINUTES
|
|
511
|
+
lease = current_lease(data)
|
|
512
|
+
if lease is None or lease.get("writer") == args.writer:
|
|
513
|
+
# Free, or re-entrant acquire by the same writer: (re)stamp and take it.
|
|
514
|
+
data["lease"] = {"writer": args.writer, "timestamp": now, "ttl_minutes": ttl}
|
|
515
|
+
write_state(args.state, data)
|
|
516
|
+
return emit("OK")
|
|
517
|
+
if lease_is_stale(lease, now):
|
|
518
|
+
prev = {
|
|
519
|
+
"previous_writer": lease.get("writer"),
|
|
520
|
+
"previous_timestamp": lease.get("timestamp"),
|
|
521
|
+
}
|
|
522
|
+
data["lease"] = {"writer": args.writer, "timestamp": now, "ttl_minutes": ttl}
|
|
523
|
+
write_state(args.state, data)
|
|
524
|
+
return emit("STALE-RECLAIMED", prev)
|
|
525
|
+
return emit("LOCKED")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def cmd_lease_release(args):
|
|
529
|
+
st, data = load_state(args.state)
|
|
530
|
+
if st == "corrupt":
|
|
531
|
+
return emit("CORRUPT")
|
|
532
|
+
if st == "absent":
|
|
533
|
+
return emit("OK") # nothing to release
|
|
534
|
+
lease = current_lease(data)
|
|
535
|
+
if lease is None:
|
|
536
|
+
return emit("OK")
|
|
537
|
+
if lease.get("writer") != args.writer:
|
|
538
|
+
return emit("LEASE-LOST") # never release another writer's lease
|
|
539
|
+
data.pop("lease", None)
|
|
540
|
+
write_state(args.state, data)
|
|
541
|
+
return emit("OK")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def cmd_run_record(args):
|
|
545
|
+
# Intentionally lease-agnostic: an `aborted-locked` run could not acquire
|
|
546
|
+
# the lease yet must still record its outcome. In local-commit mode there
|
|
547
|
+
# is a single writer per checkout, so this bookkeeping write is safe.
|
|
548
|
+
st, data = load_state(args.state)
|
|
549
|
+
if st == "corrupt":
|
|
550
|
+
return emit("CORRUPT")
|
|
551
|
+
if st == "absent":
|
|
552
|
+
data = new_state()
|
|
553
|
+
try:
|
|
554
|
+
counts = json.loads(args.counts)
|
|
555
|
+
except (ValueError, TypeError):
|
|
556
|
+
counts = {}
|
|
557
|
+
data["last_run"] = {
|
|
558
|
+
"timestamp": args.timestamp,
|
|
559
|
+
"outcome": args.outcome,
|
|
560
|
+
"writer": args.writer,
|
|
561
|
+
"counts": counts,
|
|
562
|
+
}
|
|
563
|
+
write_state(args.state, data)
|
|
564
|
+
return emit("OK")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def cmd_import_legacy(args):
|
|
568
|
+
"""Best-effort import of a Cora-style legacy state file. Liberal on input:
|
|
569
|
+
map what matches the known shapes, skip what doesn't, never fail."""
|
|
570
|
+
st, data = load_state(args.state)
|
|
571
|
+
if st == "corrupt":
|
|
572
|
+
return emit("CORRUPT")
|
|
573
|
+
if st == "absent":
|
|
574
|
+
data = new_state()
|
|
575
|
+
|
|
576
|
+
# Optional map from legacy channel id -> configured source id, so imported
|
|
577
|
+
# cursors land under the id the live connector reads via `cursor-get
|
|
578
|
+
# --source <config-id>`. Without it, a legacy "C42" cursor would be orphaned
|
|
579
|
+
# from a source configured as "slack-alpha" and the first sweep re-ingests
|
|
580
|
+
# everything. Absent or unparseable -> identity (legacy id used verbatim).
|
|
581
|
+
source_map = {}
|
|
582
|
+
if getattr(args, "source_map", None):
|
|
583
|
+
try:
|
|
584
|
+
parsed = json.loads(args.source_map)
|
|
585
|
+
if isinstance(parsed, dict):
|
|
586
|
+
source_map = {str(k): str(v) for k, v in parsed.items()}
|
|
587
|
+
except (ValueError, TypeError):
|
|
588
|
+
source_map = {}
|
|
589
|
+
|
|
590
|
+
legacy = _read_legacy(args.file)
|
|
591
|
+
cursors_imported = 0
|
|
592
|
+
items_imported = 0
|
|
593
|
+
if isinstance(legacy, dict):
|
|
594
|
+
cursors_imported = _import_channels(legacy, data, source_map)
|
|
595
|
+
items_imported = _import_legacy_items(legacy, data)
|
|
596
|
+
|
|
597
|
+
# Persist only when something changed: a fresh state file was seeded, or
|
|
598
|
+
# the import actually brought data in. A no-op import writes nothing.
|
|
599
|
+
if st == "absent" or cursors_imported or items_imported:
|
|
600
|
+
write_state(args.state, data)
|
|
601
|
+
return emit("OK", {
|
|
602
|
+
"cursors_imported": cursors_imported,
|
|
603
|
+
"items_imported": items_imported,
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _read_legacy(path):
|
|
608
|
+
try:
|
|
609
|
+
with open(path, encoding="utf-8") as f:
|
|
610
|
+
raw = f.read()
|
|
611
|
+
except (OSError, UnicodeDecodeError):
|
|
612
|
+
return None
|
|
613
|
+
# Try JSON first (Cora persists JSON); fall back to our YAML subset.
|
|
614
|
+
try:
|
|
615
|
+
return json.loads(raw)
|
|
616
|
+
except ValueError:
|
|
617
|
+
pass
|
|
618
|
+
try:
|
|
619
|
+
return parse_document(raw)
|
|
620
|
+
except Exception:
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _import_channels(legacy, data, source_map=None):
|
|
625
|
+
channels = legacy.get("channels")
|
|
626
|
+
if not isinstance(channels, dict):
|
|
627
|
+
return 0
|
|
628
|
+
source_map = source_map or {}
|
|
629
|
+
sources = data.setdefault("sources", {})
|
|
630
|
+
count = 0
|
|
631
|
+
for chan_id, chan in channels.items():
|
|
632
|
+
if not isinstance(chan, dict):
|
|
633
|
+
continue
|
|
634
|
+
cursor = chan.get("last_processed_ts") or chan.get("cursor")
|
|
635
|
+
if cursor is None:
|
|
636
|
+
continue
|
|
637
|
+
source_id = source_map.get(str(chan_id), str(chan_id))
|
|
638
|
+
entry = sources.setdefault(source_id, {})
|
|
639
|
+
# Never regress an already-advanced cursor: a re-import against live
|
|
640
|
+
# state must not rewind a source to the legacy value and re-ingest
|
|
641
|
+
# (and re-acknowledge) everything since. Seed only when absent, or when
|
|
642
|
+
# the legacy value is not older than the current one.
|
|
643
|
+
current = entry.get("cursor")
|
|
644
|
+
if current is not None and _cursor_lt(str(cursor), str(current)):
|
|
645
|
+
continue
|
|
646
|
+
entry["cursor"] = cursor
|
|
647
|
+
count += 1
|
|
648
|
+
return count
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _import_legacy_items(legacy, data):
|
|
652
|
+
raw_items = legacy.get("items")
|
|
653
|
+
items = data.setdefault("items", {})
|
|
654
|
+
count = 0
|
|
655
|
+
|
|
656
|
+
def add(item_id, fields):
|
|
657
|
+
if not item_id:
|
|
658
|
+
return 0
|
|
659
|
+
merged = dict(items.get(str(item_id), {}))
|
|
660
|
+
for k in ("status", "source", "channel", "title", "url"):
|
|
661
|
+
if k in fields and fields[k] is not None:
|
|
662
|
+
key = "source" if k == "channel" else k
|
|
663
|
+
merged.setdefault(key, fields[k])
|
|
664
|
+
items[str(item_id)] = merged
|
|
665
|
+
return 1
|
|
666
|
+
|
|
667
|
+
if isinstance(raw_items, dict):
|
|
668
|
+
for item_id, fields in raw_items.items():
|
|
669
|
+
if isinstance(fields, dict):
|
|
670
|
+
count += add(item_id, fields)
|
|
671
|
+
elif isinstance(raw_items, list):
|
|
672
|
+
for entry in raw_items:
|
|
673
|
+
if isinstance(entry, dict):
|
|
674
|
+
count += add(entry.get("id"), entry)
|
|
675
|
+
return count
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# --------------------------------------------------------------------------- #
|
|
679
|
+
# CLI wiring
|
|
680
|
+
# --------------------------------------------------------------------------- #
|
|
681
|
+
|
|
682
|
+
def build_parser():
|
|
683
|
+
p = argparse.ArgumentParser(description="ce-sweep deterministic state engine")
|
|
684
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
685
|
+
|
|
686
|
+
def with_state(sp):
|
|
687
|
+
sp.add_argument("--state", required=True)
|
|
688
|
+
return sp
|
|
689
|
+
|
|
690
|
+
with_state(sub.add_parser("read"))
|
|
691
|
+
with_state(sub.add_parser("validate"))
|
|
692
|
+
|
|
693
|
+
up = with_state(sub.add_parser("upsert-item"))
|
|
694
|
+
up.add_argument("--id", required=True)
|
|
695
|
+
up.add_argument("--source", required=True)
|
|
696
|
+
up.add_argument("--json", required=True)
|
|
697
|
+
up.add_argument("--writer", required=True)
|
|
698
|
+
up.add_argument("--now")
|
|
699
|
+
|
|
700
|
+
cg = with_state(sub.add_parser("cursor-get"))
|
|
701
|
+
cg.add_argument("--source", required=True)
|
|
702
|
+
|
|
703
|
+
ca = with_state(sub.add_parser("cursor-advance"))
|
|
704
|
+
ca.add_argument("--source", required=True)
|
|
705
|
+
ca.add_argument("--to", required=True)
|
|
706
|
+
ca.add_argument("--past-item", required=True)
|
|
707
|
+
ca.add_argument("--writer", required=True)
|
|
708
|
+
ca.add_argument("--now")
|
|
709
|
+
|
|
710
|
+
la = with_state(sub.add_parser("lease-acquire"))
|
|
711
|
+
la.add_argument("--writer", required=True)
|
|
712
|
+
la.add_argument("--ttl-minutes", type=int, default=None)
|
|
713
|
+
la.add_argument("--now")
|
|
714
|
+
|
|
715
|
+
lr = with_state(sub.add_parser("lease-release"))
|
|
716
|
+
lr.add_argument("--writer", required=True)
|
|
717
|
+
|
|
718
|
+
rr = with_state(sub.add_parser("run-record"))
|
|
719
|
+
rr.add_argument("--writer", required=True)
|
|
720
|
+
rr.add_argument(
|
|
721
|
+
"--outcome", required=True,
|
|
722
|
+
choices=("completed", "aborted-locked", "partial", "failed"),
|
|
723
|
+
)
|
|
724
|
+
rr.add_argument("--counts", required=True)
|
|
725
|
+
rr.add_argument("--timestamp", required=True)
|
|
726
|
+
|
|
727
|
+
il = with_state(sub.add_parser("import-legacy"))
|
|
728
|
+
il.add_argument("--file", required=True)
|
|
729
|
+
il.add_argument(
|
|
730
|
+
"--source-map",
|
|
731
|
+
help='JSON object mapping legacy channel id -> configured source id, '
|
|
732
|
+
'e.g. \'{"C42":"slack-alpha"}\'. Absent -> legacy ids used verbatim.',
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
return p
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
_HANDLERS = {
|
|
739
|
+
"read": cmd_read,
|
|
740
|
+
"validate": cmd_validate,
|
|
741
|
+
"upsert-item": cmd_upsert_item,
|
|
742
|
+
"cursor-get": cmd_cursor_get,
|
|
743
|
+
"cursor-advance": cmd_cursor_advance,
|
|
744
|
+
"lease-acquire": cmd_lease_acquire,
|
|
745
|
+
"lease-release": cmd_lease_release,
|
|
746
|
+
"run-record": cmd_run_record,
|
|
747
|
+
"import-legacy": cmd_import_legacy,
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
# Subcommands that read-modify-write the state file. The lease is a high-level
|
|
751
|
+
# "who owns the sweep" guard, but some writes are deliberately lease-agnostic
|
|
752
|
+
# (run-record for an aborted-locked run, validate, import-legacy). Two
|
|
753
|
+
# concurrent invocations (an overlapping cron and manual sweep) could otherwise
|
|
754
|
+
# interleave load -> mutate -> write and lose an update — e.g. an aborted run's
|
|
755
|
+
# stale-snapshot write clobbering the holder's just-committed upsert. An OS
|
|
756
|
+
# advisory lock held across each mutating RMW makes them mutually exclusive
|
|
757
|
+
# regardless of lease ownership.
|
|
758
|
+
_MUTATING = {
|
|
759
|
+
"validate", "upsert-item", "cursor-advance", "lease-acquire",
|
|
760
|
+
"lease-release", "run-record", "import-legacy",
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _run_locked(handler, args):
|
|
765
|
+
lock_path = str(args.state) + ".lock"
|
|
766
|
+
try:
|
|
767
|
+
lock_fd = open(lock_path, "w", encoding="utf-8")
|
|
768
|
+
except OSError:
|
|
769
|
+
return handler(args) # cannot create a lock file; degrade to unlocked
|
|
770
|
+
try:
|
|
771
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
772
|
+
return handler(args)
|
|
773
|
+
finally:
|
|
774
|
+
try:
|
|
775
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
776
|
+
finally:
|
|
777
|
+
lock_fd.close()
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def main(argv):
|
|
781
|
+
args = build_parser().parse_args(argv[1:])
|
|
782
|
+
handler = _HANDLERS[args.cmd]
|
|
783
|
+
try:
|
|
784
|
+
if _HAS_FCNTL and args.cmd in _MUTATING:
|
|
785
|
+
return _run_locked(handler, args)
|
|
786
|
+
return handler(args)
|
|
787
|
+
except Exception as exc: # never leak a traceback to the caller
|
|
788
|
+
sys.stderr.write(f"sweep-state: internal error: {exc}\n")
|
|
789
|
+
return emit("ERROR")
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
if __name__ == "__main__":
|
|
793
|
+
sys.exit(main(sys.argv))
|