@workerstack/repo 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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Workspace vs packaged repo skill content conflicts (warn-only). */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { isRecord } from "./hook-input.ts";
|
|
6
|
+
|
|
7
|
+
/** The workspace skills dir codex and antigravity read; sync fills it with links. */
|
|
8
|
+
export const WORKSPACE_SKILLS_REL = ".agents/skills";
|
|
9
|
+
/** The stock skills, under the stock root (this repo, or the installed package). */
|
|
10
|
+
export const STOCK_SKILLS_REL = "agents/skills";
|
|
11
|
+
export const PACKAGED_SKILLS_REL: string = path.join(
|
|
12
|
+
"node_modules",
|
|
13
|
+
"@workerstack",
|
|
14
|
+
"repo",
|
|
15
|
+
...STOCK_SKILLS_REL.split("/"),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export function listSkillDirNames(skillsRoot: string): string[] {
|
|
19
|
+
let entries: fs.Dirent[];
|
|
20
|
+
try {
|
|
21
|
+
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (isRecord(err) && err.code === "ENOENT") return [];
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
const out: string[] = [];
|
|
27
|
+
for (const ent of entries) {
|
|
28
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
29
|
+
const skillMd = path.join(skillsRoot, ent.name, "SKILL.md");
|
|
30
|
+
try {
|
|
31
|
+
if (!fs.statSync(skillMd).isFile()) continue;
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
out.push(ent.name);
|
|
36
|
+
}
|
|
37
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readSkillMd(skillsRoot: string, name: string): Buffer | null {
|
|
42
|
+
const abs = path.join(skillsRoot, name, "SKILL.md");
|
|
43
|
+
try {
|
|
44
|
+
return fs.readFileSync(abs);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Names in both trees whose SKILL.md bytes differ. */
|
|
51
|
+
export interface SkillConflict {
|
|
52
|
+
name: string;
|
|
53
|
+
workspacePath: string;
|
|
54
|
+
packagedPath: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function findSkillContentConflicts({
|
|
58
|
+
workspaceSkillsDir,
|
|
59
|
+
packagedSkillsDir,
|
|
60
|
+
}: { workspaceSkillsDir?: string; packagedSkillsDir?: string } = {}): SkillConflict[] {
|
|
61
|
+
if (!workspaceSkillsDir || !packagedSkillsDir) return [];
|
|
62
|
+
const packaged = listSkillDirNames(packagedSkillsDir);
|
|
63
|
+
if (!packaged.length) return [];
|
|
64
|
+
const conflicts: SkillConflict[] = [];
|
|
65
|
+
for (const name of packaged) {
|
|
66
|
+
const ws = readSkillMd(workspaceSkillsDir, name);
|
|
67
|
+
if (ws == null) continue;
|
|
68
|
+
const pkg = fs.readFileSync(path.join(packagedSkillsDir, name, "SKILL.md"));
|
|
69
|
+
if (Buffer.compare(ws, pkg) !== 0) {
|
|
70
|
+
conflicts.push({
|
|
71
|
+
name,
|
|
72
|
+
workspacePath: path.join(workspaceSkillsDir, name, "SKILL.md"),
|
|
73
|
+
packagedPath: path.join(packagedSkillsDir, name, "SKILL.md"),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return conflicts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatSkillConflictWarn(name: string): string {
|
|
81
|
+
return `skill \`${name}\` differs from packaged repo; re-init overwrites`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Text-to-speech (output only). Providers: elevenlabs, openai. */
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { resolveRoots } from "./workspace-root.ts";
|
|
8
|
+
import { loadOptionalConsumerEnv } from "./doctor.ts";
|
|
9
|
+
import { isRecord } from "./hook-input.ts";
|
|
10
|
+
import { readEnv } from "../env.ts";
|
|
11
|
+
import type { Env } from "../env.ts";
|
|
12
|
+
|
|
13
|
+
export type Provider = "elevenlabs" | "openai";
|
|
14
|
+
|
|
15
|
+
export interface SpeakCfg {
|
|
16
|
+
filePath: string;
|
|
17
|
+
providerPref: string;
|
|
18
|
+
elevenlabs: { apiKey: string; voiceId: string; modelId: string };
|
|
19
|
+
openai: { apiKey: string; voice: string; model: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The slice of a fetch Response this script reads. */
|
|
23
|
+
export interface FetchResult {
|
|
24
|
+
ok?: boolean;
|
|
25
|
+
status?: number;
|
|
26
|
+
text?: () => Promise<string>;
|
|
27
|
+
json?: () => Promise<unknown>;
|
|
28
|
+
arrayBuffer?: () => Promise<ArrayBuffer | Uint8Array>;
|
|
29
|
+
body?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FetchInit {
|
|
33
|
+
method?: string | undefined;
|
|
34
|
+
headers?: Record<string, string> | undefined;
|
|
35
|
+
body?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type FetchFn = (url: string, init: FetchInit) => Promise<FetchResult>;
|
|
39
|
+
|
|
40
|
+
export interface ParsedArgv {
|
|
41
|
+
_: string[];
|
|
42
|
+
flags: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SpeakLog {
|
|
46
|
+
log(msg: string): void;
|
|
47
|
+
error(msg: string): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SpeakMainOpts {
|
|
51
|
+
env?: Env | undefined;
|
|
52
|
+
home?: string | undefined;
|
|
53
|
+
cwd?: string | undefined;
|
|
54
|
+
fetchFn?: FetchFn | undefined;
|
|
55
|
+
log?: SpeakLog | undefined;
|
|
56
|
+
spawn?: SpeakSpawn | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ProviderOpts {
|
|
60
|
+
provider?: string | undefined;
|
|
61
|
+
cfg: SpeakCfg;
|
|
62
|
+
fetchFn?: FetchFn | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SayOpts extends ProviderOpts {
|
|
66
|
+
text?: string | undefined;
|
|
67
|
+
voice?: string | undefined;
|
|
68
|
+
out?: string | undefined;
|
|
69
|
+
play?: boolean | string | undefined;
|
|
70
|
+
cwd?: string | undefined;
|
|
71
|
+
spawn?: SpeakSpawn | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const DEFAULT_ELEVEN_VOICE = "JBFqnCBsd6RMkjVDRZzb";
|
|
75
|
+
export const DEFAULT_ELEVEN_MODEL = "eleven_multilingual_v2";
|
|
76
|
+
export const DEFAULT_OPENAI_VOICE = "coral";
|
|
77
|
+
export const DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts";
|
|
78
|
+
export const OPENAI_VOICES: string[] = [
|
|
79
|
+
"alloy",
|
|
80
|
+
"ash",
|
|
81
|
+
"ballad",
|
|
82
|
+
"coral",
|
|
83
|
+
"echo",
|
|
84
|
+
"fable",
|
|
85
|
+
"nova",
|
|
86
|
+
"onyx",
|
|
87
|
+
"sage",
|
|
88
|
+
"shimmer",
|
|
89
|
+
];
|
|
90
|
+
export const USAGE =
|
|
91
|
+
"usage: speak whoami | voices | say --text T [--provider elevenlabs|openai] [--voice ID] [--out path] [--play]";
|
|
92
|
+
const ELEVEN_API = "https://api.elevenlabs.io/v1";
|
|
93
|
+
const OPENAI_API = "https://api.openai.com/v1";
|
|
94
|
+
export const SETUP = "bun repo setup speak";
|
|
95
|
+
export const SPEAK_DIR = ".tmp/repo/speak";
|
|
96
|
+
export const NEED_KEY =
|
|
97
|
+
"missing ELEVENLABS_API_KEY or OPENAI_API_KEY (or ~/.workerstack/speak.json). setup: bun repo setup speak";
|
|
98
|
+
export const NEED_ELEVEN =
|
|
99
|
+
"missing ELEVENLABS_API_KEY (or ~/.workerstack/speak.json elevenlabs.apiKey). setup: bun repo setup speak";
|
|
100
|
+
export const NEED_OPENAI =
|
|
101
|
+
"missing OPENAI_API_KEY (or ~/.workerstack/speak.json openai.apiKey). setup: bun repo setup speak";
|
|
102
|
+
|
|
103
|
+
export function formatHttpError(status: unknown, body: unknown, provider: string): string {
|
|
104
|
+
const n = Number(status);
|
|
105
|
+
const raw = String(body || "");
|
|
106
|
+
if (/missing_permissions/i.test(raw)) {
|
|
107
|
+
return `${provider} key present but missing permission (${n || status}). setup: ${SETUP}`;
|
|
108
|
+
}
|
|
109
|
+
const auth = n === 401 || n === 403 || /unauthorized|invalid.?api.?key|authentication/i.test(raw);
|
|
110
|
+
if (auth) {
|
|
111
|
+
return `${provider} key missing or rejected (${n || status}). setup: ${SETUP}`;
|
|
112
|
+
}
|
|
113
|
+
return `${status} ${raw.slice(0, 200)}`.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseArgv(argv: unknown): ParsedArgv {
|
|
117
|
+
const out: ParsedArgv = { _: [], flags: {} };
|
|
118
|
+
const args: string[] = Array.isArray(argv) ? argv.filter((a): a is string => typeof a === "string") : [];
|
|
119
|
+
for (let i = 0; i < args.length; i++) {
|
|
120
|
+
const a = args[i]!; // i < args.length
|
|
121
|
+
if (a === "--") {
|
|
122
|
+
out._.push(...args.slice(i + 1));
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (a.startsWith("--") && a.length > 2) {
|
|
126
|
+
const key = a.slice(2);
|
|
127
|
+
const nxt = args[i + 1];
|
|
128
|
+
if (nxt && !nxt.startsWith("--")) {
|
|
129
|
+
out.flags[key] = nxt;
|
|
130
|
+
i += 1;
|
|
131
|
+
} else out.flags[key] = "1";
|
|
132
|
+
} else out._.push(a);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readJson(p: string): Record<string, unknown> {
|
|
138
|
+
try {
|
|
139
|
+
const j: unknown = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
140
|
+
if (isRecord(j)) return j;
|
|
141
|
+
} catch {
|
|
142
|
+
/* none */
|
|
143
|
+
}
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function str(v: unknown, fallback = ""): string {
|
|
148
|
+
const s = String(v ?? "").trim();
|
|
149
|
+
return s || fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function loadCfg({ env = readEnv(), home = os.homedir() }: { env?: Env; home?: string } = {}): SpeakCfg {
|
|
153
|
+
const filePath = path.join(home, ".workerstack", "speak.json");
|
|
154
|
+
const file = readJson(filePath);
|
|
155
|
+
const eleven = isRecord(file.elevenlabs) ? file.elevenlabs : {};
|
|
156
|
+
const openai = isRecord(file.openai) ? file.openai : {};
|
|
157
|
+
return {
|
|
158
|
+
filePath,
|
|
159
|
+
providerPref: str(env.SPEAK_PROVIDER || file.provider).toLowerCase(),
|
|
160
|
+
elevenlabs: {
|
|
161
|
+
apiKey: str(env.ELEVENLABS_API_KEY || eleven.apiKey || file.apiKey),
|
|
162
|
+
voiceId: str(env.ELEVENLABS_VOICE_ID || eleven.voiceId || file.voiceId, DEFAULT_ELEVEN_VOICE),
|
|
163
|
+
modelId: str(env.ELEVENLABS_MODEL_ID || eleven.modelId || file.modelId, DEFAULT_ELEVEN_MODEL),
|
|
164
|
+
},
|
|
165
|
+
openai: {
|
|
166
|
+
apiKey: str(env.OPENAI_API_KEY || openai.apiKey),
|
|
167
|
+
voice: str(env.OPENAI_TTS_VOICE || openai.voice, DEFAULT_OPENAI_VOICE),
|
|
168
|
+
model: str(env.OPENAI_TTS_MODEL || openai.model, DEFAULT_OPENAI_MODEL),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveProvider(cfg: SpeakCfg | null | undefined, flagProvider?: string): Provider {
|
|
174
|
+
const want = str(flagProvider || (cfg && cfg.providerPref)).toLowerCase();
|
|
175
|
+
if (want && want !== "elevenlabs" && want !== "openai") {
|
|
176
|
+
throw new Error("provider must be elevenlabs or openai");
|
|
177
|
+
}
|
|
178
|
+
if (want === "elevenlabs") {
|
|
179
|
+
if (!cfg?.elevenlabs.apiKey) throw new Error(NEED_ELEVEN);
|
|
180
|
+
return "elevenlabs";
|
|
181
|
+
}
|
|
182
|
+
if (want === "openai") {
|
|
183
|
+
if (!cfg?.openai.apiKey) throw new Error(NEED_OPENAI);
|
|
184
|
+
return "openai";
|
|
185
|
+
}
|
|
186
|
+
if (cfg?.elevenlabs.apiKey) return "elevenlabs";
|
|
187
|
+
if (cfg?.openai.apiKey) return "openai";
|
|
188
|
+
throw new Error(NEED_KEY);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface ApiOpts {
|
|
192
|
+
method?: string | undefined;
|
|
193
|
+
headers?: Record<string, string> | undefined;
|
|
194
|
+
body?: Record<string, unknown> | undefined;
|
|
195
|
+
raw?: boolean | undefined;
|
|
196
|
+
fetchFn?: FetchFn | undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function apiFetch(url: string, opts: ApiOpts & { raw: true }): Promise<Buffer>;
|
|
200
|
+
async function apiFetch(url: string, opts: ApiOpts): Promise<unknown>;
|
|
201
|
+
async function apiFetch(url: string, { method = "GET", headers, body, raw, fetchFn }: ApiOpts): Promise<unknown> {
|
|
202
|
+
// The global fetch returns a full Response, which carries every FetchResult field.
|
|
203
|
+
const fn: FetchFn | undefined = fetchFn || (globalThis.fetch as FetchFn | undefined);
|
|
204
|
+
if (typeof fn !== "function") throw new Error("fetch is not available");
|
|
205
|
+
const res = await fn(url, {
|
|
206
|
+
method,
|
|
207
|
+
headers,
|
|
208
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
209
|
+
});
|
|
210
|
+
if (!res || !res.ok) {
|
|
211
|
+
const status = res && res.status != null ? res.status : "err";
|
|
212
|
+
let t = "";
|
|
213
|
+
try {
|
|
214
|
+
t = res && typeof res.text === "function" ? await res.text() : "";
|
|
215
|
+
} catch {
|
|
216
|
+
/* none */
|
|
217
|
+
}
|
|
218
|
+
const provider = url.includes("elevenlabs") ? "elevenlabs" : "openai";
|
|
219
|
+
throw new Error(formatHttpError(status, t, provider));
|
|
220
|
+
}
|
|
221
|
+
if (raw) {
|
|
222
|
+
if (typeof res.arrayBuffer === "function") {
|
|
223
|
+
return Buffer.from(new Uint8Array(await res.arrayBuffer()));
|
|
224
|
+
}
|
|
225
|
+
if (Buffer.isBuffer(res.body)) return res.body;
|
|
226
|
+
return Buffer.from(Array.isArray(res.body) ? (res.body as number[]) : []);
|
|
227
|
+
}
|
|
228
|
+
if (typeof res.json === "function") return res.json();
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function whoami({ provider, cfg, fetchFn }: ProviderOpts): Promise<string> {
|
|
233
|
+
const p = resolveProvider(cfg, provider);
|
|
234
|
+
if (p === "elevenlabs") {
|
|
235
|
+
try {
|
|
236
|
+
const u = await apiFetch(`${ELEVEN_API}/user`, {
|
|
237
|
+
headers: { "xi-api-key": cfg.elevenlabs.apiKey },
|
|
238
|
+
fetchFn,
|
|
239
|
+
});
|
|
240
|
+
const user = isRecord(u) ? u : {};
|
|
241
|
+
const email = user.email ? String(user.email) : "ok";
|
|
242
|
+
const tier = isRecord(user.subscription) && user.subscription.tier ? String(user.subscription.tier) : "-";
|
|
243
|
+
return `elevenlabs ${email} tier=${tier}`;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
const msg = errMessage(e);
|
|
246
|
+
if (/missing permission/i.test(msg)) {
|
|
247
|
+
return "elevenlabs ok (scoped key; whoami needs user_read). voices/say still work";
|
|
248
|
+
}
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
await apiFetch(`${OPENAI_API}/models`, {
|
|
253
|
+
headers: { Authorization: `Bearer ${cfg.openai.apiKey}` },
|
|
254
|
+
fetchFn,
|
|
255
|
+
});
|
|
256
|
+
return "openai ok";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function voices({ provider, cfg, fetchFn }: ProviderOpts): Promise<string> {
|
|
260
|
+
const p = resolveProvider(cfg, provider);
|
|
261
|
+
if (p === "openai") return OPENAI_VOICES.join("\n");
|
|
262
|
+
const j = await apiFetch(`${ELEVEN_API}/voices`, {
|
|
263
|
+
headers: { "xi-api-key": cfg.elevenlabs.apiKey },
|
|
264
|
+
fetchFn,
|
|
265
|
+
});
|
|
266
|
+
const list: unknown[] = isRecord(j) && Array.isArray(j.voices) ? j.voices : [];
|
|
267
|
+
return list.map((v) => (isRecord(v) ? `${v.voice_id} ${v.name}` : "undefined undefined")).join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function errMessage(e: unknown): string {
|
|
271
|
+
const m = isRecord(e) ? e.message : undefined;
|
|
272
|
+
return String(m || e);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The speaker edge: plays one audio file with the platform's player. */
|
|
276
|
+
export type SpeakSpawn = (cmd: string, args: string[], opts: { stdio: "ignore" }) => unknown;
|
|
277
|
+
|
|
278
|
+
export function playPath(dest: string, spawn: SpeakSpawn, platform: NodeJS.Platform = process.platform): void {
|
|
279
|
+
const bin = platform === "darwin" ? "afplay" : "ffplay";
|
|
280
|
+
const args = platform === "darwin" ? [dest] : ["-nodisp", "-autoexit", dest];
|
|
281
|
+
spawn(bin, args, { stdio: "ignore" });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function say({
|
|
285
|
+
text,
|
|
286
|
+
voice,
|
|
287
|
+
out,
|
|
288
|
+
play,
|
|
289
|
+
provider,
|
|
290
|
+
cfg,
|
|
291
|
+
cwd = process.cwd(),
|
|
292
|
+
fetchFn,
|
|
293
|
+
spawn = spawnSync,
|
|
294
|
+
}: SayOpts): Promise<string> {
|
|
295
|
+
if (!str(text)) throw new Error("say needs --text");
|
|
296
|
+
const p = resolveProvider(cfg, provider);
|
|
297
|
+
let buf: Buffer;
|
|
298
|
+
if (p === "elevenlabs") {
|
|
299
|
+
const id = str(voice, cfg.elevenlabs.voiceId);
|
|
300
|
+
buf = await apiFetch(`${ELEVEN_API}/text-to-speech/${encodeURIComponent(id)}?output_format=mp3_44100_128`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
"xi-api-key": cfg.elevenlabs.apiKey,
|
|
304
|
+
"Content-Type": "application/json",
|
|
305
|
+
},
|
|
306
|
+
body: { text: str(text), model_id: cfg.elevenlabs.modelId },
|
|
307
|
+
raw: true,
|
|
308
|
+
fetchFn,
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
const v = str(voice, cfg.openai.voice);
|
|
312
|
+
buf = await apiFetch(`${OPENAI_API}/audio/speech`, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: {
|
|
315
|
+
Authorization: `Bearer ${cfg.openai.apiKey}`,
|
|
316
|
+
"Content-Type": "application/json",
|
|
317
|
+
},
|
|
318
|
+
body: { model: cfg.openai.model, input: str(text), voice: v },
|
|
319
|
+
raw: true,
|
|
320
|
+
fetchFn,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const dest = str(out) || path.join(resolveRoots({}, { cwd }).root, SPEAK_DIR, `say-${Date.now()}.mp3`);
|
|
324
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
325
|
+
fs.writeFileSync(dest, buf);
|
|
326
|
+
if (play === true || play === "true" || play === "1") playPath(dest, spawn);
|
|
327
|
+
return dest;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function main(argv: string[], opts: SpeakMainOpts = {}): Promise<number> {
|
|
331
|
+
const { _, flags } = parseArgv(argv);
|
|
332
|
+
if (!opts.env) await loadOptionalConsumerEnv();
|
|
333
|
+
const env = opts.env || readEnv();
|
|
334
|
+
const home = opts.home || os.homedir();
|
|
335
|
+
const cwd = opts.cwd || process.cwd();
|
|
336
|
+
const fetchFn = opts.fetchFn || (globalThis.fetch as FetchFn | undefined);
|
|
337
|
+
const log: SpeakLog = opts.log || console;
|
|
338
|
+
const cfg = loadCfg({ env, home });
|
|
339
|
+
const verb = String(_[0] || "").toLowerCase();
|
|
340
|
+
try {
|
|
341
|
+
if (verb === "whoami") {
|
|
342
|
+
log.log(await whoami({ provider: flags.provider, cfg, fetchFn }));
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
if (verb === "voices") {
|
|
346
|
+
log.log(await voices({ provider: flags.provider, cfg, fetchFn }));
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
if (verb === "say") {
|
|
350
|
+
const dest = await say({
|
|
351
|
+
text: flags.text,
|
|
352
|
+
voice: flags.voice,
|
|
353
|
+
out: flags.out,
|
|
354
|
+
play: flags.play,
|
|
355
|
+
provider: flags.provider,
|
|
356
|
+
cfg,
|
|
357
|
+
cwd,
|
|
358
|
+
fetchFn,
|
|
359
|
+
spawn: opts.spawn,
|
|
360
|
+
});
|
|
361
|
+
log.log(dest);
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
log.error(USAGE);
|
|
365
|
+
return 1;
|
|
366
|
+
} catch (e) {
|
|
367
|
+
log.error(errMessage(e));
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strategy domain: how a plan or roadmap gets implemented, and the things that follow from it.
|
|
3
|
+
*
|
|
4
|
+
* closing line the one line that asks for approval or a handoff, rendered and parsed here and nowhere else
|
|
5
|
+
* approval what a reply `y`, `y <strategy>`, `h`, `h fast` or `h deep` means
|
|
6
|
+
* session stamp which plan this session reviews; the plan file is the truth, the stamp only points at it
|
|
7
|
+
* mode how a session handles a question: ask, auto or away
|
|
8
|
+
*
|
|
9
|
+
* Node builtins and `canonicalPath` only.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { canonicalPath } from "./workspace-root.ts";
|
|
14
|
+
|
|
15
|
+
type Strategy = "default" | "review-fast" | "review-deep";
|
|
16
|
+
type ReviewStrategy = Exclude<Strategy, "default">;
|
|
17
|
+
type Approvable = { kind: "plan" | "roadmap"; serial: number; strategy: Strategy };
|
|
18
|
+
type Approval =
|
|
19
|
+
| { kind: "approve"; strategy: Strategy | null }
|
|
20
|
+
| { kind: "handoff"; strategy: Strategy | null }
|
|
21
|
+
| { kind: "unknown"; word: string };
|
|
22
|
+
export type ActiveReview = { plan: string; serial: number; strategy: ReviewStrategy };
|
|
23
|
+
|
|
24
|
+
export const STRATEGIES: readonly Strategy[] = ["default", "review-fast", "review-deep"];
|
|
25
|
+
|
|
26
|
+
export const MODES = ["ask", "auto", "away"] as const;
|
|
27
|
+
export type Mode = (typeof MODES)[number];
|
|
28
|
+
|
|
29
|
+
/** @param v @returns */
|
|
30
|
+
export function parseMode(v: unknown): Mode | null {
|
|
31
|
+
return MODES.find((m) => m === v) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Harnesses whose child and parent payloads differ by a marker the gate reads, so the review deny is enforced.
|
|
36
|
+
* Phase 6 probe (live, 2026-11-07):
|
|
37
|
+
* claude child has agent_id and agent_type; Grok's wire shape is Claude's (`harnessOf` says claude), and its child
|
|
38
|
+
* has subagentType, so "claude" covers Grok too
|
|
39
|
+
* codex child has agent_id and agent_type
|
|
40
|
+
* antigravity, cursor a child carries no marker; it only has a different session id. Not enforced.
|
|
41
|
+
*/
|
|
42
|
+
export const ENFORCED_HARNESSES: ReadonlySet<"claude" | "codex" | "antigravity" | "cursor"> = new Set<
|
|
43
|
+
"claude" | "codex" | "antigravity" | "cursor"
|
|
44
|
+
>(["claude", "codex"]);
|
|
45
|
+
|
|
46
|
+
/** @param v @returns */
|
|
47
|
+
export function parseStrategy(v: unknown): Strategy | null {
|
|
48
|
+
return STRATEGIES.find((s) => s === v) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The implementer agent for a review strategy. review-fast starts fast and escalates; there is no attempt 3.
|
|
53
|
+
* @param s
|
|
54
|
+
* @param attempt
|
|
55
|
+
* @returns
|
|
56
|
+
*/
|
|
57
|
+
export function implementerOf(s: ReviewStrategy, attempt: 1 | 2): "implementer-fast" | "implementer-deep" {
|
|
58
|
+
return s === "review-fast" && attempt === 1 ? "implementer-fast" : "implementer-deep";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const itemText = (i: Approvable) => `${i.kind} ${i.serial} (strategy: ${i.strategy})`;
|
|
62
|
+
|
|
63
|
+
const HANDOFFS = '"h" / "h fast" / "h deep" for a fresh session';
|
|
64
|
+
const handoffStrategy = (w: string): Strategy | null =>
|
|
65
|
+
w === "fast" ? "review-fast" : w === "deep" ? "review-deep" : null;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The only renderer of the approval line. The first `or` list holds every strategy not held by all items;
|
|
69
|
+
* the second offers a handoff to a fresh session.
|
|
70
|
+
* @param items
|
|
71
|
+
*/
|
|
72
|
+
export function closingLine(items: Approvable[]) {
|
|
73
|
+
if (!items.length) throw new Error("closingLine needs at least one item");
|
|
74
|
+
const others = STRATEGIES.filter((s) => !items.every((i) => i.strategy === s));
|
|
75
|
+
return `Reply "y" to run ${items.map(itemText).join(", then ")}, or ${others.map((s) => `"y ${s}"`).join(" / ")}, or ${HANDOFFS}.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const CLOSING_RE =
|
|
79
|
+
/^Reply "y" to run ((?:plan|roadmap) \d+ \(strategy: [a-z-]+\)(?:, then (?:plan|roadmap) \d+ \(strategy: [a-z-]+\))*), or /;
|
|
80
|
+
const ITEM_RE = /^(plan|roadmap) (\d+) \(strategy: ([a-z-]+)\)$/;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The items a closing line names, or null unless the line is exactly what `closingLine` renders for them.
|
|
84
|
+
* Markdown around the line (bold, backticks, a quote marker) is ignored.
|
|
85
|
+
* @param line
|
|
86
|
+
* @returns
|
|
87
|
+
*/
|
|
88
|
+
export function parseClosingLine(line: string): Approvable[] | null {
|
|
89
|
+
const text = String(line)
|
|
90
|
+
.replace(/^[\s`*_>]+/, "")
|
|
91
|
+
.replace(/[\s`*_]+$/, "");
|
|
92
|
+
const m = CLOSING_RE.exec(text);
|
|
93
|
+
if (!m) return null;
|
|
94
|
+
const items: Approvable[] = [];
|
|
95
|
+
for (const part of (m[1] /* group 1 is not optional in CLOSING_RE */ as string).split(", then ")) {
|
|
96
|
+
const it = ITEM_RE.exec(part);
|
|
97
|
+
const kind = it?.[1];
|
|
98
|
+
const strategy = parseStrategy(it?.[3]);
|
|
99
|
+
if (!it || (kind !== "plan" && kind !== "roadmap") || !strategy) return null;
|
|
100
|
+
items.push({ kind, serial: Number(it[2]), strategy });
|
|
101
|
+
}
|
|
102
|
+
return closingLine(items) === text ? items : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A reply of `y` (keep each file's strategy) or `y <strategy>`, or a handoff `h` (keep) or `h fast` / `h deep`.
|
|
107
|
+
* Null when the prompt is neither a y nor an h reply.
|
|
108
|
+
* @param prompt
|
|
109
|
+
* @returns
|
|
110
|
+
*/
|
|
111
|
+
export function parseApproval(prompt: string): Approval | null {
|
|
112
|
+
const m = /^([yh])(?:\s+(\S+))?$/i.exec(String(prompt).trim());
|
|
113
|
+
if (!m) return null;
|
|
114
|
+
const kind = m[1]?.toLowerCase() === "h" ? "handoff" : "approve";
|
|
115
|
+
const word = m[2];
|
|
116
|
+
if (word === undefined) return { kind, strategy: null };
|
|
117
|
+
const strategy = kind === "handoff" ? handoffStrategy(word.toLowerCase()) : parseStrategy(word.toLowerCase());
|
|
118
|
+
return strategy ? { kind, strategy } : { kind: "unknown", word };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @param root @param sid */
|
|
122
|
+
export function stampPath(root: string, sid: string): string {
|
|
123
|
+
return path.join(root, ".tmp", "repo", "sessions", String(sid).replace(/[^\w-]/g, ""), "strategy.json");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Point the session at a plan. Same content every time; a temp file in the same directory, then a rename,
|
|
128
|
+
* so a reader never sees half a stamp.
|
|
129
|
+
* @param root @param sid @param planAbs
|
|
130
|
+
*/
|
|
131
|
+
export function writeStamp(root: string, sid: string, planAbs: string): void {
|
|
132
|
+
const dest = stampPath(root, sid);
|
|
133
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
134
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
135
|
+
try {
|
|
136
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ plan: canonicalPath(path.resolve(planAbs)) })}\n`);
|
|
137
|
+
fs.renameSync(tmp, dest);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
fs.rmSync(tmp, { force: true });
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The review this session runs: the stamped plan, when its own frontmatter says in-progress and review-*.
|
|
146
|
+
* @param root @param sid
|
|
147
|
+
* @returns
|
|
148
|
+
*/
|
|
149
|
+
export function activeReview(root: string, sid: string): ActiveReview | null {
|
|
150
|
+
try {
|
|
151
|
+
const stamp = JSON.parse(fs.readFileSync(stampPath(root, sid), "utf8"));
|
|
152
|
+
const plan = typeof stamp === "object" && stamp !== null && "plan" in stamp ? stamp.plan : null;
|
|
153
|
+
if (typeof plan !== "string") return null;
|
|
154
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(fs.readFileSync(plan, "utf8"));
|
|
155
|
+
if (!m) return null;
|
|
156
|
+
const fm: Record<string, string> = {};
|
|
157
|
+
for (const line of (m[1] /* group 1 is not optional in the frontmatter regex */ as string).split(/\r?\n/)) {
|
|
158
|
+
const kv = /^([a-z]+):\s*(.*)$/.exec(line);
|
|
159
|
+
if (kv)
|
|
160
|
+
fm[kv[1] /* both groups always match */ as string] = (kv[2] /* both groups always match */ as string).trim();
|
|
161
|
+
}
|
|
162
|
+
const strategy = parseStrategy(fm.strategy);
|
|
163
|
+
if (fm.status !== "in-progress" || !strategy || strategy === "default") return null;
|
|
164
|
+
return { plan, serial: Number(fm.serial), strategy };
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|