@justjoshn/pi-pstack 0.0.0-stage → 0.15.36-pi.1
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 +95 -2
- package/agents/comment-sicko.md +32 -0
- package/agents/poteto-agent.md +9 -0
- package/automations/benny/FOR_AGENTS.md +82 -0
- package/automations/benny/README.md +20 -0
- package/automations/benny/skills/reproduce-and-fix-issues/SKILL.md +310 -0
- package/automations/benny/skills/reproduce-and-fix-issues/references/control-adapter.md +169 -0
- package/automations/benny/skills/reproduce-and-fix-issues/references/feature-map.example.md +205 -0
- package/automations/benny/skills/reproduce-and-fix-issues/references/verify-existing-fix.md +93 -0
- package/automations/benny/skills/setup-benny/SKILL.md +253 -0
- package/automations/benny/skills/triage-issue-reports/SKILL.md +240 -0
- package/automations/benny/skills/triage-issue-reports/references/routing.example.md +61 -0
- package/automations/benny/templates/configuration.example.yaml +113 -0
- package/automations/benny/templates/reproduce-automation-prompt.md +49 -0
- package/automations/benny/templates/triage-automation-prompt.md +53 -0
- package/docs/guide/01-setup.md +67 -0
- package/docs/guide/02-poteto-mode.md +139 -0
- package/docs/guide/03-understand.md +79 -0
- package/docs/guide/04-design.md +133 -0
- package/docs/guide/05-build-and-clean.md +83 -0
- package/docs/guide/06-verify-and-ship.md +162 -0
- package/docs/guide/07-overnight.md +122 -0
- package/docs/guide/08-principles.md +72 -0
- package/docs/guide/09-make-it-yours.md +100 -0
- package/docs/guide/10-recipes-and-pitfalls.md +182 -0
- package/docs/guide/README.md +38 -0
- package/docs/guide/images/design.jpg +0 -0
- package/docs/guide/images/overnight.jpg +0 -0
- package/docs/guide/images/recipes.jpg +0 -0
- package/docs/guide/images/router.jpg +0 -0
- package/docs/guide/images/understanding.jpg +0 -0
- package/docs/guide/images/verification.jpg +0 -0
- package/docs/native-gaps.md +67 -0
- package/licenses/cursor-team-kit.LICENSE +21 -0
- package/licenses/pstack.LICENSE +21 -0
- package/package.json +51 -4
- package/scripts/live-e2e.mjs +257 -0
- package/skills/architect/SKILL.md +79 -0
- package/skills/architect/references/design-red-flags.md +57 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +48 -0
- package/skills/automate-me/SKILL.md +104 -0
- package/skills/benchmark-checklist/SKILL.md +39 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/control-cli/SKILL.md +109 -0
- package/skills/control-ui/SKILL.md +109 -0
- package/skills/correct/SKILL.md +32 -0
- package/skills/create-formal-verification-skill/SKILL.md +43 -0
- package/skills/create-skill/SKILL.md +62 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/deslop/SKILL.md +22 -0
- package/skills/figure-it-out/SKILL.md +52 -0
- package/skills/how/SKILL.md +36 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +35 -0
- package/skills/in-your-own-words/SKILL.md +7 -0
- package/skills/interrogate/SKILL.md +31 -0
- package/skills/ketchup/SKILL.md +43 -0
- package/skills/ketchup-video/SKILL.md +29 -0
- package/skills/maintain-formal-verification-skill/SKILL.md +58 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/make-bot-ui/SKILL.md +104 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/poteto-help/SKILL.md +161 -0
- package/skills/poteto-help/references/prompting.md +51 -0
- package/skills/poteto-help/references/recipes.md +51 -0
- package/skills/poteto-mode/SKILL.md +151 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +25 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +8 -0
- package/skills/poteto-mode/playbooks/babysit.md +23 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +15 -0
- package/skills/poteto-mode/playbooks/eval.md +25 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +155 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +34 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +25 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +25 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/skills/poteto-mode/playbooks/shipping.md +12 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +12 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/check-plan.mjs +205 -0
- package/skills/poteto-mode/scripts/check-plan.test.ts +110 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +129 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +498 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +726 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +482 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +842 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +414 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +156 -0
- package/skills/principle-attack-the-premise/SKILL.md +21 -0
- package/skills/principle-boundary-discipline/SKILL.md +34 -0
- package/skills/principle-build-the-lever/SKILL.md +23 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +11 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +9 -0
- package/skills/principle-experience-first/SKILL.md +19 -0
- package/skills/principle-explain-the-number/SKILL.md +18 -0
- package/skills/principle-fix-root-causes/SKILL.md +21 -0
- package/skills/principle-foundational-thinking/SKILL.md +21 -0
- package/skills/principle-guard-the-context-window/SKILL.md +12 -0
- package/skills/principle-laziness-protocol/SKILL.md +10 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +22 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +14 -0
- package/skills/principle-minimize-reader-load/SKILL.md +18 -0
- package/skills/principle-model-the-domain/SKILL.md +24 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +15 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
- package/skills/principle-prove-it-works/SKILL.md +22 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +17 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +18 -0
- package/skills/principle-test-behavior-not-implementation/SKILL.md +20 -0
- package/skills/principle-type-system-discipline/SKILL.md +13 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/reflect/SKILL.md +69 -0
- package/skills/reflect/references/divergent-reviewer.md +38 -0
- package/skills/reflect/references/judgment-reviewer.md +37 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +48 -0
- package/skills/setup-pstack/SKILL.md +74 -0
- package/skills/sharpen/SKILL.md +16 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +42 -0
- package/skills/statechart/SKILL.md +82 -0
- package/skills/statechart/references/render.md +93 -0
- package/skills/statechart/scripts/backends/cfg.ts +33 -0
- package/skills/statechart/scripts/backends/mermaid.ts +201 -0
- package/skills/statechart/scripts/backends/plain-text.ts +72 -0
- package/skills/statechart/scripts/backends/report.ts +177 -0
- package/skills/statechart/scripts/frontends/config.ts +276 -0
- package/skills/statechart/scripts/frontends/dot.ts +74 -0
- package/skills/statechart/scripts/frontends/marks.ts +93 -0
- package/skills/statechart/scripts/frontends/messages.ts +52 -0
- package/skills/statechart/scripts/frontends/spec.ts +114 -0
- package/skills/statechart/scripts/frontends/trace.ts +24 -0
- package/skills/statechart/scripts/frontends/value.ts +166 -0
- package/skills/statechart/scripts/frontends/verdict.ts +40 -0
- package/skills/statechart/scripts/frontends/view.ts +119 -0
- package/skills/statechart/scripts/ir.ts +547 -0
- package/skills/statechart/scripts/package.json +11 -0
- package/skills/statechart/scripts/passes/annotate.ts +128 -0
- package/skills/statechart/scripts/passes/label.ts +269 -0
- package/skills/statechart/scripts/passes/lift.ts +85 -0
- package/skills/statechart/scripts/passes/loop-check.ts +68 -0
- package/skills/statechart/scripts/passes/paths.ts +60 -0
- package/skills/statechart/scripts/passes/reach.ts +135 -0
- package/skills/statechart/scripts/passes/region-check.ts +97 -0
- package/skills/statechart/scripts/passes/regions.ts +295 -0
- package/skills/statechart/scripts/pipeline.ts +76 -0
- package/skills/statechart/scripts/run.ts +481 -0
- package/skills/statechart/scripts/statechart +29 -0
- package/skills/statechart/scripts/statechart.ts +164 -0
- package/skills/statechart/scripts/test/ConfigOracle.java +63 -0
- package/skills/statechart/scripts/test/chart.test.ts +1210 -0
- package/skills/statechart/scripts/test/charts/Braces.mmd +7 -0
- package/skills/statechart/scripts/test/charts/Escapes.mmd +11 -0
- package/skills/statechart/scripts/test/charts/Phases.mmd +29 -0
- package/skills/statechart/scripts/test/cli.test.ts +1314 -0
- package/skills/statechart/scripts/test/config-fuzz.test.ts +263 -0
- package/skills/statechart/scripts/test/corpus/Counter/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/Counter/Counter.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/Counter/Counter.tla +10 -0
- package/skills/statechart/scripts/test/corpus/Counter/chart.mmd +9 -0
- package/skills/statechart/scripts/test/corpus/Counter/graph.dot +16 -0
- package/skills/statechart/scripts/test/corpus/Counter/report.md +7 -0
- package/skills/statechart/scripts/test/corpus/Counter/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/Counter/verdict-1.out +49 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/ChartRun.out +60 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/CounterAction.cfg +4 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/CounterAction.tla +22 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/chart.mmd +26 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/graph.dot +19 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/report.md +20 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/verdict-2.out +82 -0
- package/skills/statechart/scripts/test/corpus/CounterAction/verdict-3.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/CounterDeadlock.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/CounterDeadlock.tla +11 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/chart.mmd +22 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/graph.dot +16 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/report.md +17 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-2.out +77 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/ChartRun.out +69 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/CounterFan.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/CounterFan.tla +11 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/chart.mmd +85 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/graph.dot +70 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/report.md +12 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/verdict-1.out +67 -0
- package/skills/statechart/scripts/test/corpus/CounterFan/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/ChartRun.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/ChartRun.out +85 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/CounterGuard.cfg +6 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/CounterGuard.tla +13 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/chart.mmd +5 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/graph.dot +10 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/report.md +31 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/verdict-1.out +64 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterGuard/verdict-2.out +77 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/ChartRun.out +55 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/CounterInitPast.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/CounterInitPast.tla +10 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/chart.mmd +12 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/graph.dot +6 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/report.md +12 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/verdict-1.out +33 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPast/verdict-2.out +54 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/ChartRun.out +54 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/CounterInitPastHolds.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/CounterInitPastHolds.tla +10 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/chart.mmd +1 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/graph.dot +6 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/report.md +7 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterInitPastHolds/verdict-1.out +46 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/CounterInstance.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/CounterInstance.tla +10 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/CounterSteps.tla +6 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/chart.mmd +17 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/graph.dot +15 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/report.md +14 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/sany.out +3 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterInstance/verdict-1.out +78 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/CounterJump.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/CounterJump.tla +19 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/chart.mmd +26 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/graph.dot +22 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/report.md +12 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/verdict-1.out +58 -0
- package/skills/statechart/scripts/test/corpus/CounterJump/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/CounterLoop.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/CounterLoop.tla +11 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/chart.mmd +17 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/graph.dot +15 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/report.md +14 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterLoop/verdict-1.out +76 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/CounterOrder.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/CounterOrder.tla +11 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/chart.mmd +19 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/graph.dot +10 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/report.md +15 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterOrder/verdict-2.out +67 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/ChartRun.out +62 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/CounterRules.cfg +6 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/CounterRules.tla +24 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/Moves.tla +5 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/chart.mmd +22 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/graph.dot +10 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/report.md +23 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/sany.out +3 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-1.out +58 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-2.out +66 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-3.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-3.out +74 -0
- package/skills/statechart/scripts/test/corpus/CounterRules/verdict-4.out +59 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/ChartRun.out +58 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/CounterStart.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/CounterStart.tla +15 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/chart.mmd +18 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/graph.dot +13 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/report.md +11 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/verdict-1.out +33 -0
- package/skills/statechart/scripts/test/corpus/CounterStart/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/CounterStop.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/CounterStop.tla +15 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/chart.mmd +7 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/graph.dot +13 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/report.md +6 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterStop/verdict-1.out +49 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/ChartRun.out +61 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/CounterTie.cfg +4 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/CounterTie.tla +14 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/chart.mmd +30 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/graph.dot +19 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/report.md +27 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-2.out +64 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-3.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-3.out +87 -0
- package/skills/statechart/scripts/test/corpus/CounterTie/verdict-4.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/ChartRun.out +60 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/CounterTieInGraph.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/CounterTieInGraph.tla +12 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/chart.mmd +23 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/graph.dot +19 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/report.md +11 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/CounterTieInGraph/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterView/ChartRun.out +61 -0
- package/skills/statechart/scripts/test/corpus/CounterView/CounterView.cfg +4 -0
- package/skills/statechart/scripts/test/corpus/CounterView/CounterView.tla +14 -0
- package/skills/statechart/scripts/test/corpus/CounterView/chart.mmd +23 -0
- package/skills/statechart/scripts/test/corpus/CounterView/graph.dot +19 -0
- package/skills/statechart/scripts/test/corpus/CounterView/report.md +27 -0
- package/skills/statechart/scripts/test/corpus/CounterView/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-2.out +64 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-3.exit +1 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-3.out +87 -0
- package/skills/statechart/scripts/test/corpus/CounterView/verdict-4.out +57 -0
- package/skills/statechart/scripts/test/corpus/CounterView/view.mjs +1 -0
- package/skills/statechart/scripts/test/corpus/DieHard/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/DieHard/DieHard.cfg +4 -0
- package/skills/statechart/scripts/test/corpus/DieHard/DieHard.tla +38 -0
- package/skills/statechart/scripts/test/corpus/DieHard/chart.mmd +92 -0
- package/skills/statechart/scripts/test/corpus/DieHard/graph.dot +126 -0
- package/skills/statechart/scripts/test/corpus/DieHard/report.md +17 -0
- package/skills/statechart/scripts/test/corpus/DieHard/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/DieHard/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/DieHard/verdict-1.out +91 -0
- package/skills/statechart/scripts/test/corpus/DieHard/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/ChartRun.out +61 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/FirstRule.cfg +6 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/FirstRule.tla +11 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/chart.mmd +22 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/graph.dot +16 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/report.md +25 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-2.out +64 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-3.exit +1 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-3.out +64 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-4.exit +1 -0
- package/skills/statechart/scripts/test/corpus/FirstRule/verdict-4.out +77 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/HiddenClock.cfg +4 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/HiddenClock.tla +11 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/chart.mmd +13 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/graph.dot +12 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/report.md +11 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/HiddenClock/verdict-1.out +72 -0
- package/skills/statechart/scripts/test/corpus/Login/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/Login/Login.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/Login/Login.tla +62 -0
- package/skills/statechart/scripts/test/corpus/Login/chart.mmd +27 -0
- package/skills/statechart/scripts/test/corpus/Login/graph.dot +40 -0
- package/skills/statechart/scripts/test/corpus/Login/report.md +7 -0
- package/skills/statechart/scripts/test/corpus/Login/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/Login/verdict-1.out +47 -0
- package/skills/statechart/scripts/test/corpus/Login/view.mjs +1 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/LoginGuarded.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/LoginGuarded.tla +64 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/chart.mmd +28 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/graph.dot +37 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/report.md +8 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/verdict-1.out +47 -0
- package/skills/statechart/scripts/test/corpus/LoginGuarded/view.mjs +1 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/ChartRun.out +60 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/LoginParallel.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/LoginParallel.tla +64 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/chart.mmd +44 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/graph.dot +58 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/report.md +17 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/verdict-1.out +83 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/LoginParallel/view.mjs +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/ChartRun.out +60 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/MediaPlayer.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/MediaPlayer.tla +103 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/chart.mmd +43 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/graph.dot +93 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/report.md +14 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/verdict-1.out +59 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/verdict-2.out +72 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayer/view.mjs +3 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/MediaPlayerFixed.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/MediaPlayerFixed.tla +100 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/chart.mmd +31 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/graph.dot +85 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/report.md +9 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/verdict-1.out +47 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerFixed/view.mjs +3 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/MediaPlayer.tla +103 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/MediaPlayerProps.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/MediaPlayerProps.tla +6 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/chart.mmd +42 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/graph.dot +93 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/report.md +19 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-1.out +61 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-2.out +59 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-3.out +61 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-4.exit +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-4.out +66 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-5.exit +1 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-5.out +58 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-6.out +49 -0
- package/skills/statechart/scripts/test/corpus/MediaPlayerProps/view.mjs +3 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/PickWorker.cfg +6 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/PickWorker.tla +11 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/chart.mmd +5 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/graph.dot +11 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/report.md +7 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/sany.out +5 -0
- package/skills/statechart/scripts/test/corpus/PickWorker/verdict-1.out +70 -0
- package/skills/statechart/scripts/test/corpus/Shadow/ChartRun.out +61 -0
- package/skills/statechart/scripts/test/corpus/Shadow/Shadow.cfg +4 -0
- package/skills/statechart/scripts/test/corpus/Shadow/Shadow.tla +11 -0
- package/skills/statechart/scripts/test/corpus/Shadow/ShadowLib.tla +6 -0
- package/skills/statechart/scripts/test/corpus/Shadow/ShadowSub.tla +3 -0
- package/skills/statechart/scripts/test/corpus/Shadow/chart.mmd +9 -0
- package/skills/statechart/scripts/test/corpus/Shadow/graph.dot +16 -0
- package/skills/statechart/scripts/test/corpus/Shadow/report.md +8 -0
- package/skills/statechart/scripts/test/corpus/Shadow/sany.out +4 -0
- package/skills/statechart/scripts/test/corpus/Shadow/verdict-1.out +65 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/ChartRun.out +59 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/SpacedNames.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/SpacedNames.tla +9 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/chart.mmd +23 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/graph.dot +17 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/report.md +15 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/verdict-1.out +65 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/SpacedNames/verdict-2.out +75 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/ChartRun.out +69 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/TwoPhase.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/TwoPhase.tla +75 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/chart.mmd +60 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/graph.dot +297 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/report.md +20 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/verdict-1.out +95 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/verdict-2.out +57 -0
- package/skills/statechart/scripts/test/corpus/TwoPhase/view.mjs +1 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/ChartRun.out +57 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/TwoPhaseFixed.cfg +3 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/TwoPhaseFixed.tla +75 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/chart.mmd +37 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/graph.dot +223 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/report.md +11 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/sany.out +1 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/verdict-1.out +47 -0
- package/skills/statechart/scripts/test/corpus/TwoPhaseFixed/view.mjs +1 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/ChartRun.exit +1 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/ChartRun.out +75 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/TypeGuard.cfg +5 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/TypeGuard.tla +11 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/chart.mmd +18 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/graph.dot +14 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/report.md +29 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/verdict-1.out +64 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/TypeGuard/verdict-2.out +75 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/ChartRun.out +58 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/UserNoDeadlock.cfg +2 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/UserNoDeadlock.tla +8 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/chart.mmd +22 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/graph.dot +16 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/report.md +15 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/sany.out +2 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-1.exit +1 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-1.out +56 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-2.exit +1 -0
- package/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-2.out +77 -0
- package/skills/statechart/scripts/test/corpus.test.ts +403 -0
- package/skills/statechart/scripts/test/differential.test.ts +565 -0
- package/skills/statechart/scripts/test/grid/Grid.cfg +2 -0
- package/skills/statechart/scripts/test/grid/Grid.tla +9 -0
- package/skills/statechart/scripts/test/grid/sany.out +2 -0
- package/skills/statechart/scripts/test/java.ts +63 -0
- package/skills/statechart/scripts/test/labels.ts +643 -0
- package/skills/statechart/scripts/test/messages.test.ts +212 -0
- package/skills/statechart/scripts/test/pipeline.test.ts +270 -0
- package/skills/statechart/scripts/test/plain-tlc.test.ts +255 -0
- package/skills/statechart/scripts/test/render.test.ts +81 -0
- package/skills/statechart/scripts/test/tlc-out.test.ts +90 -0
- package/skills/statechart/scripts/test/tlc.ts +158 -0
- package/skills/statechart/scripts/tsconfig.json +11 -0
- package/skills/statechart/scripts/worker.ts +35 -0
- package/skills/swarm/SKILL.md +41 -0
- package/skills/tdd/SKILL.md +42 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/technical-writing/SKILL.md +106 -0
- package/skills/typescript-best-practices/SKILL.md +32 -0
- package/skills/typescript-best-practices/references/patterns.md +328 -0
- package/skills/unslop/SKILL.md +62 -0
- package/skills/why/SKILL.md +98 -0
- package/skills/why/references/epistemics.md +71 -0
- package/skills/why/references/investigator-prompt.md +84 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +51 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +97 -0
- package/skills/why/references/sources/incident-postmortem.md +5 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +66 -0
- package/skills/why/references/sources/slack.md +17 -0
- package/skills/why/references/synthesizer-prompt.md +129 -0
- package/src/aliases.ts +64 -0
- package/src/ask.ts +131 -0
- package/src/autocomplete.ts +21 -0
- package/src/child-env.ts +44 -0
- package/src/index.ts +123 -0
- package/src/loop.ts +189 -0
- package/src/mode.ts +156 -0
- package/src/modes.ts +8 -0
- package/src/paths.ts +72 -0
- package/src/plan.ts +141 -0
- package/src/readonly-shell.ts +107 -0
- package/src/rule.ts +56 -0
- package/src/sections.ts +117 -0
- package/src/subscriptions/cron.ts +75 -0
- package/src/subscriptions/github.ts +149 -0
- package/src/subscriptions/index.ts +547 -0
- package/src/subscriptions/webhook.ts +209 -0
- package/src/task/agents.ts +55 -0
- package/src/task/card.ts +32 -0
- package/src/task/format.ts +88 -0
- package/src/task/models.ts +140 -0
- package/src/task/records.ts +232 -0
- package/src/task/registry.ts +77 -0
- package/src/task/runner.ts +370 -0
- package/src/task/status.ts +170 -0
- package/src/task/stop.ts +100 -0
- package/src/task/tool.ts +491 -0
- package/src/task/worktree.ts +67 -0
- package/src/todo.ts +122 -0
- package/src/visualize.ts +244 -0
- package/test/aliases.test.ts +57 -0
- package/test/ask.test.ts +85 -0
- package/test/autocomplete.test.ts +39 -0
- package/test/e2e/aliases.e2e.test.ts +81 -0
- package/test/e2e/ask.e2e.test.ts +135 -0
- package/test/e2e/harness.ts +179 -0
- package/test/e2e/host.e2e.test.ts +193 -0
- package/test/e2e/lifecycle.e2e.test.ts +99 -0
- package/test/e2e/mode.e2e.test.ts +160 -0
- package/test/e2e/package.e2e.test.ts +67 -0
- package/test/e2e/task.e2e.test.ts +734 -0
- package/test/e2e/todo.e2e.test.ts +59 -0
- package/test/fixtures/cursor-askquestion-calls.json +806 -0
- package/test/fixtures/fake-pi.mjs +96 -0
- package/test/fixtures/image-model.ts +16 -0
- package/test/fixtures/list-models.ts +15 -0
- package/test/fixtures/readonly-shell-exit.ts +17 -0
- package/test/fixtures/scripted-model.ts +113 -0
- package/test/loop.test.ts +209 -0
- package/test/mode.test.ts +143 -0
- package/test/plan.test.ts +126 -0
- package/test/readonly-shell-security.test.ts +84 -0
- package/test/readonly-shell.test.ts +44 -0
- package/test/records.test.ts +143 -0
- package/test/registry.test.ts +18 -0
- package/test/resources.test.ts +162 -0
- package/test/rule.test.ts +92 -0
- package/test/scripts.test.ts +182 -0
- package/test/sections.test.ts +56 -0
- package/test/subscriptions-cron.test.ts +60 -0
- package/test/subscriptions.test.ts +598 -0
- package/test/task-agents.test.ts +49 -0
- package/test/task-card.test.ts +34 -0
- package/test/task-fork.test.ts +233 -0
- package/test/task-format.test.ts +65 -0
- package/test/task-model.test.ts +141 -0
- package/test/task-runner.test.ts +259 -0
- package/test/task-schema.test.ts +50 -0
- package/test/task-status.test.ts +155 -0
- package/test/task-worktree.test.ts +106 -0
- package/test/todo.test.ts +74 -0
- package/test/visualize.test.ts +152 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
6
|
+
import { PACKAGE_ROOT } from '../../src/paths.ts';
|
|
7
|
+
import { assistantTexts, contentText, makeSandbox, type PiEvent, runJson, type Sandbox, script, startRpc, toolEnds } from './harness.ts';
|
|
8
|
+
|
|
9
|
+
const AGENT_ID = /Agent ID: ([0-9a-f-]{36})/;
|
|
10
|
+
|
|
11
|
+
let sandbox: Sandbox;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
sandbox = makeSandbox();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => sandbox.dispose());
|
|
16
|
+
|
|
17
|
+
const task = (args: Record<string, unknown>) => ({ tool: 'Task', args: { description: 'probe', ...args } });
|
|
18
|
+
const slug = (cwd: string) => `--${cwd.replace(/^\//, '').replace(/[/\\:]/g, '-')}--`;
|
|
19
|
+
const subagentDir = (cwd = sandbox.workDir) => join(sandbox.agentDir, 'pstack/workspaces', slug(cwd), 'subagents');
|
|
20
|
+
const transcriptOf = (agentId: string) => {
|
|
21
|
+
const file = readdirSync(subagentDir()).find((name) => name.endsWith(`_${agentId}.jsonl`));
|
|
22
|
+
if (file === undefined) throw new Error(`no transcript for ${agentId}`);
|
|
23
|
+
return readFileSync(join(subagentDir(), file), 'utf8')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => JSON.parse(line) as { type: string; message?: { role: string; content: unknown; thinkingLevel?: string } });
|
|
27
|
+
};
|
|
28
|
+
const idOf = (text: string | undefined) => {
|
|
29
|
+
const id = AGENT_ID.exec(text ?? '')?.[1];
|
|
30
|
+
if (id === undefined) throw new Error(`no agent ID in: ${text}`);
|
|
31
|
+
return id;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Sends one prompt to an RPC session and returns its events: up to its agent_settled, or its response for a command. */
|
|
35
|
+
async function rpcPrompt(rpc: ReturnType<typeof startRpc>, message: string): Promise<PiEvent[]> {
|
|
36
|
+
const mark = rpc.events.length;
|
|
37
|
+
const id = `prompt-${mark}`;
|
|
38
|
+
rpc.send({ id, type: 'prompt', message });
|
|
39
|
+
const response = await rpc.waitFor((event) => event.type === 'response' && event['id'] === id);
|
|
40
|
+
if ((response['data'] as { disposition?: string } | undefined)?.disposition !== 'handled') await rpc.waitFor((event) => event.type === 'agent_settled' && rpc.events.indexOf(event) >= mark);
|
|
41
|
+
return rpc.events.slice(mark);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runTask(args: Record<string, unknown>) {
|
|
45
|
+
const run = await runJson(sandbox, script([task(args)]), ['--no-session']);
|
|
46
|
+
const [result] = toolEnds(run.events, 'Task');
|
|
47
|
+
if (result === undefined) throw new Error(`Task never ran\n${run.stderr}`);
|
|
48
|
+
return { run, result };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('foreground', () => {
|
|
52
|
+
test('returns the response in Cursor format with agent ID, usage, and a transcript (TASK-002, TASK-012, TASK-014)', async () => {
|
|
53
|
+
const { result } = await runTask({ prompt: 'go\n@@say PONG-FG', run_in_background: false });
|
|
54
|
+
expect(result.isError).toBe(false);
|
|
55
|
+
expect(result.text).toMatch(/^This is the output of the subagent:\n\nresponse:\n<response>\nPONG-FG\n<\/response>\n\nAgent ID: [0-9a-f-]{36} \(pass it as `resume` to send a follow-up after it completes\)\nTranscript: /);
|
|
56
|
+
expect(result.usage?.totalTokens).toBeGreaterThan(100);
|
|
57
|
+
const entries = transcriptOf(idOf(result.text));
|
|
58
|
+
expect(entries[0]?.type).toBe('session');
|
|
59
|
+
expect(entries.filter((entry) => entry.message?.role === 'assistant').map((entry) => contentText(entry.message?.content as string))).toEqual(['PONG-FG']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a failed child returns an error status with its message (TASK-015)', async () => {
|
|
63
|
+
const { result } = await runTask({ prompt: 'go\n@@fail boom' });
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
expect(result.text.startsWith('Subagent failed: boom')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('an invalid model fails with the allowed list before any child starts (TASK-007)', async () => {
|
|
69
|
+
const { result } = await runTask({ prompt: 'go\n@@say X', model: 'no-such-model' });
|
|
70
|
+
expect(result.isError).toBe(true);
|
|
71
|
+
expect(result.text.split('\n').slice(0, 4)).toEqual([
|
|
72
|
+
'Invalid model selection "no-such-model". Model could not be resolved to a valid subagent model.',
|
|
73
|
+
'Allowed model slugs:',
|
|
74
|
+
'- inherit',
|
|
75
|
+
'- scripted/echo (thinking: off, minimal, low, medium, high)',
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a Cursor slug picks the model and thinking level (TASK-007)', async () => {
|
|
80
|
+
const { result } = await runTask({ prompt: 'go\n@@say HI', model: 'echo-high' });
|
|
81
|
+
expect(result.details).toMatchObject({ model: { provider: 'scripted', id: 'echo', thinking: 'high' } });
|
|
82
|
+
const assistant = transcriptOf(idOf(result.text)).find((entry) => entry.message?.role === 'assistant');
|
|
83
|
+
expect(assistant?.message?.thinkingLevel).toBe('high');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('file attachments reach the child first message (TASK-016)', async () => {
|
|
87
|
+
writeFileSync(join(sandbox.workDir, 'note.txt'), 'ATTACHED-CONTENT-7');
|
|
88
|
+
const { result } = await runTask({ prompt: 'go\n@@say GOT', file_attachments: ['note.txt'] });
|
|
89
|
+
const user = transcriptOf(idOf(result.text)).find((entry) => entry.message?.role === 'user');
|
|
90
|
+
expect(JSON.stringify(user?.message?.content)).toContain('ATTACHED-CONTENT-7');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('cloud worktrees (TASK-010)', () => {
|
|
95
|
+
const git = (...args: string[]) => execFileSync('git', ['-C', sandbox.workDir, '-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { encoding: 'utf8' }).trim();
|
|
96
|
+
const worktreeOf = (agentId: string) => join(sandbox.agentDir, 'pstack/workspaces', slug(sandbox.workDir), 'worktrees', agentId);
|
|
97
|
+
const response = (text: string) => /<response>\n([\s\S]*)\n<\/response>/.exec(text)?.[1];
|
|
98
|
+
const bash = (command: string) => `go\n@@script ${JSON.stringify([{ tool: 'bash', args: { command } }])}`;
|
|
99
|
+
const repoWith = (text: string) => {
|
|
100
|
+
git('init', '-q', '-b', 'main');
|
|
101
|
+
writeFileSync(join(sandbox.workDir, 'a.txt'), text);
|
|
102
|
+
git('add', 'a.txt');
|
|
103
|
+
git('commit', '-qm', 'first');
|
|
104
|
+
return git('rev-parse', 'HEAD');
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
test("runs the child in its own worktree on pstack/<agent id> from the parent's HEAD, without uncommitted changes", async () => {
|
|
108
|
+
const head = repoWith('COMMITTED');
|
|
109
|
+
writeFileSync(join(sandbox.workDir, 'a.txt'), 'DIRTY');
|
|
110
|
+
const { result } = await runTask({ prompt: bash('pwd; git branch --show-current; cat a.txt; echo; echo NEW > b.txt; git add b.txt; git -c user.email=t@t -c user.name=t commit -qm CHILD-COMMIT'), environment: 'cloud' });
|
|
111
|
+
const agentId = idOf(result.text);
|
|
112
|
+
const path = worktreeOf(agentId);
|
|
113
|
+
expect(result.isError, result.text).toBe(false);
|
|
114
|
+
expect(response(result.text)).toBe(`RESULTS:\n[bash] ${path}\npstack/${agentId}\nCOMMITTED\n`);
|
|
115
|
+
expect(result.details).toMatchObject({ worktree: { path, cwd: path, branch: `pstack/${agentId}`, base: 'HEAD', commit: head } });
|
|
116
|
+
expect(result.text).toContain(`\nWorktree: ${path} on branch pstack/${agentId}, from HEAD at ${head}. Merge pstack/${agentId} to take the subagent's commits.`);
|
|
117
|
+
expect([git('log', '-1', '--format=%s', `pstack/${agentId}`), git('rev-parse', `pstack/${agentId}~1`), git('branch', '--show-current')]).toEqual(['CHILD-COMMIT', head, 'main']);
|
|
118
|
+
const user = transcriptOf(agentId).find((entry) => entry.message?.role === 'user');
|
|
119
|
+
expect(contentText(user?.message?.content as string).startsWith(`You are running in an isolated git worktree at ${path} on branch pstack/${agentId}, created from HEAD at ${head}.`)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('cloud_base_branch picks the branch the worktree starts from', async () => {
|
|
123
|
+
repoWith('MAIN');
|
|
124
|
+
git('checkout', '-qb', 'feature');
|
|
125
|
+
writeFileSync(join(sandbox.workDir, 'a.txt'), 'FEATURE');
|
|
126
|
+
git('commit', '-qam', 'feature');
|
|
127
|
+
const feature = git('rev-parse', 'HEAD');
|
|
128
|
+
git('checkout', '-q', 'main');
|
|
129
|
+
const { result } = await runTask({ prompt: bash('cat a.txt'), environment: 'cloud', cloud_base_branch: 'feature' });
|
|
130
|
+
expect(response(result.text)).toBe('RESULTS:\n[bash] FEATURE');
|
|
131
|
+
expect(result.details).toMatchObject({ worktree: { base: 'feature', commit: feature } });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('a working directory outside git fails before any child starts', async () => {
|
|
135
|
+
const { result } = await runTask({ prompt: 'go\n@@say X', environment: 'cloud' });
|
|
136
|
+
expect(result.text).toBe(`environment "cloud" runs the subagent in its own git worktree, but ${sandbox.workDir} is not inside a git repository. Omit environment to run the subagent here.`);
|
|
137
|
+
expect(existsSync(subagentDir())).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('cloud_base_branch without environment cloud fails', async () => {
|
|
141
|
+
const { result } = await runTask({ prompt: 'go\n@@say X', cloud_base_branch: 'main' });
|
|
142
|
+
expect(result.text).toBe('cloud_base_branch needs environment "cloud": it names the branch the subagent\'s worktree starts from.');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('resume runs the agent in its recorded worktree, from the parent workspace', async () => {
|
|
146
|
+
repoWith('A');
|
|
147
|
+
const first = await runTask({ prompt: 'go\n@@say ONE', environment: 'cloud' });
|
|
148
|
+
const agentId = idOf(first.result.text);
|
|
149
|
+
const resumed = await runTask({ prompt: bash('pwd; git branch --show-current'), resume: agentId });
|
|
150
|
+
expect(response(resumed.result.text)).toBe(`RESULTS:\n[bash] ${worktreeOf(agentId)}\npstack/${agentId}\n`);
|
|
151
|
+
expect(resumed.result.details).toMatchObject({ worktree: { path: worktreeOf(agentId), branch: `pstack/${agentId}` } });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('resume fails when the recorded worktree is gone', async () => {
|
|
155
|
+
repoWith('A');
|
|
156
|
+
const first = await runTask({ prompt: 'go\n@@say ONE', environment: 'cloud' });
|
|
157
|
+
const agentId = idOf(first.result.text);
|
|
158
|
+
git('worktree', 'remove', '--force', worktreeOf(agentId));
|
|
159
|
+
const { result } = await runTask({ prompt: 'again\n@@say TWO', resume: agentId });
|
|
160
|
+
expect(result.text).toBe(`Agent ${agentId} ran in the git worktree ${worktreeOf(agentId)}, which no longer exists. Start a new agent instead.`);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('subagent types', () => {
|
|
165
|
+
test('comment-sicko runs with its agent body in the system prompt (TASK-006)', async () => {
|
|
166
|
+
const { result } = await runTask({ prompt: 'go\n@@sys', subagent_type: 'comment-sicko' });
|
|
167
|
+
expect(result.text).toContain('<pstack_agent>\n# Comment Sicko\n\nMy first output when spawned is exactly this.');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('readonly limits the child to read, grep, find, ls, a read-only shell, and its todo list (TASK-008, TASK-019)', async () => {
|
|
171
|
+
const { result } = await runTask({ prompt: 'go\n@@tools-list', readonly: true });
|
|
172
|
+
const tools = /<response>\n(.*)\n<\/response>/.exec(result.text)?.[1]?.split(',');
|
|
173
|
+
expect(tools?.toSorted()).toEqual(['TodoWrite', 'bash', 'find', 'grep', 'ls', 'read']);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test.skipIf(process.platform !== 'darwin' && process.platform !== 'linux')('a readonly child runs git log but cannot write files or reach the network (TASK-019)', async () => {
|
|
177
|
+
execFileSync('git', ['init', '-q'], { cwd: sandbox.workDir });
|
|
178
|
+
writeFileSync(join(sandbox.workDir, 'kept.txt'), 'KEPT\n');
|
|
179
|
+
execFileSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'RO-COMMIT-MARK', '--allow-empty'], { cwd: sandbox.workDir });
|
|
180
|
+
const outside = join(sandbox.root, 'outside-write.txt');
|
|
181
|
+
const steps = [
|
|
182
|
+
{ tool: 'bash', args: { command: 'git log --oneline -1' } },
|
|
183
|
+
{ tool: 'bash', args: { command: 'cat kept.txt | tr a-z A-Z' } },
|
|
184
|
+
{ tool: 'bash', args: { command: 'touch made.txt' } },
|
|
185
|
+
{ tool: 'bash', args: { command: `echo x > ${outside}` } },
|
|
186
|
+
{ tool: 'bash', args: { command: 'rm kept.txt' } },
|
|
187
|
+
{ tool: 'bash', args: { command: 'curl -sS -m 5 -o /dev/null https://example.com' } },
|
|
188
|
+
];
|
|
189
|
+
const { result } = await runTask({ prompt: `go\n@@script ${JSON.stringify(steps)}`, readonly: true });
|
|
190
|
+
const lines = /<response>\n([\s\S]*)\n<\/response>/.exec(result.text)?.[1] ?? '';
|
|
191
|
+
expect(lines).toContain('RO-COMMIT-MARK');
|
|
192
|
+
expect(lines).toContain('KEPT');
|
|
193
|
+
expect(lines.match(/\[bash error\]/g)?.length).toBe(4);
|
|
194
|
+
expect([existsSync(join(sandbox.workDir, 'made.txt')), existsSync(outside), existsSync(join(sandbox.workDir, 'kept.txt'))]).toEqual([false, false, true]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('a writable child keeps the ordinary shell (TASK-019)', async () => {
|
|
198
|
+
const { result } = await runTask({ prompt: `go\n@@script ${JSON.stringify([{ tool: 'bash', args: { command: 'touch made.txt && echo MADE' } }])}` });
|
|
199
|
+
expect(result.text).toContain('MADE');
|
|
200
|
+
expect(existsSync(join(sandbox.workDir, 'made.txt'))).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('a subagent gets the project resources its parent trusted (review r2: security)', async () => {
|
|
204
|
+
mkdirSync(join(sandbox.workDir, '.pi/skills/projskill'), { recursive: true });
|
|
205
|
+
writeFileSync(join(sandbox.workDir, '.pi/skills/projskill/SKILL.md'), '---\nname: projskill\ndescription: PROJECT-SKILL-MARKER\n---\nBody.\n');
|
|
206
|
+
const sees = async (flag: string) =>
|
|
207
|
+
(await runJson(sandbox, script([task({ prompt: 'go\n@@sys' })]), ['--no-session', flag])).events.some((event) => JSON.stringify(event).includes('PROJECT-SKILL-MARKER') && event.type === 'tool_execution_end');
|
|
208
|
+
expect([await sees('--approve'), await sees('--no-approve')]).toEqual([true, false]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("a child's system prompt names the main chat's store as the root agent's store (autopilot-full step 2)", async () => {
|
|
212
|
+
const { result } = await runTask({ prompt: 'go\n@@sys' });
|
|
213
|
+
const current = /"The current agent's store" is (\S+)\. /.exec(result.text)?.[1];
|
|
214
|
+
const root = /"The root agent's store" is (\S+), the store of the main chat/.exec(result.text)?.[1];
|
|
215
|
+
expect([current?.split('/').at(-2), root?.split('/').at(-2), root === current]).toEqual(['store', 'store', false]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('a normal child has write tools and the pstack tools', async () => {
|
|
219
|
+
const { result } = await runTask({ prompt: 'go\n@@tools-list' });
|
|
220
|
+
const tools = /<response>\n(.*)\n<\/response>/.exec(result.text)?.[1]?.split(',') ?? [];
|
|
221
|
+
expect(tools.filter((name) => ['bash', 'edit', 'write', 'Task', 'TodoWrite'].includes(name)).toSorted()).toEqual(['Task', 'TodoWrite', 'bash', 'edit', 'write']);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('an unknown type fails with the valid list (TASK-006)', async () => {
|
|
225
|
+
const { result } = await runTask({ prompt: 'go\n@@say X', subagent_type: 'explore' });
|
|
226
|
+
expect(result.text).toBe('Unknown subagent_type "explore". Available subagent types: generalPurpose, comment-sicko, poteto-agent.');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('a subagent of a subagent cannot launch more (TASK-011)', async () => {
|
|
230
|
+
const { result } = await runTask({ prompt: 'go\n@@task-chain 2' });
|
|
231
|
+
expect([result.text.includes('[Task] This is the output of the subagent:'), result.text.includes('[Task error] Tool Task not found')]).toEqual([true, true]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('background and parallel', () => {
|
|
236
|
+
test('returns at once and delivers a <task> notification after the turn (TASK-003)', async () => {
|
|
237
|
+
const run = await runJson(sandbox, script([task({ prompt: 'go\n@@sleep 1500 PONG-BG', run_in_background: true })]), ['--no-session']);
|
|
238
|
+
const [launch] = toolEnds(run.events, 'Task');
|
|
239
|
+
expect(launch?.text.startsWith('Subagent is running in the background. Its final response arrives automatically after your turn ends')).toBe(true);
|
|
240
|
+
const agentId = idOf(launch?.text);
|
|
241
|
+
const notice = run.events
|
|
242
|
+
.filter((event) => event.type === 'entry_appended')
|
|
243
|
+
.map((event) => event['entry'] as { type: string; customType?: string; content: string })
|
|
244
|
+
.filter((entry) => entry.type === 'custom_message' && entry.customType === 'pstack/task')
|
|
245
|
+
.map((entry) => entry.content);
|
|
246
|
+
expect(notice).toEqual([
|
|
247
|
+
`The following task has finished. If you already handled it, ignore this notification and do not restate earlier responses.\n\n<task>\nkind: subagent\nstatus: success\ntask_id: ${agentId}\ntitle: go @@sleep 1500 PONG-BG\ntool_call_id: ${(run.events.find((event) => event.type === 'tool_execution_end')?.['toolCallId'] as string) ?? ''}\ndetail: PONG-BG\n</task>`,
|
|
248
|
+
]);
|
|
249
|
+
const texts = assistantTexts(run.events).filter((text) => text !== '');
|
|
250
|
+
expect(texts).toHaveLength(2);
|
|
251
|
+
expect(texts[0]?.startsWith('RESULTS:\n[Task] Subagent is running in the background.')).toBe(true);
|
|
252
|
+
expect(texts[1]?.startsWith('ECHO: The following task has finished.')).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('while a subagent runs, its run lock names the child process that writes the transcript (recheck)', async () => {
|
|
256
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
257
|
+
try {
|
|
258
|
+
rpc.send({ type: 'prompt', message: script([task({ prompt: 'go\n@@sleep 4000 SLOW', run_in_background: true })]) });
|
|
259
|
+
const launch = await rpc.waitFor((event) => event.type === 'tool_execution_end' && event['toolName'] === 'Task');
|
|
260
|
+
const { agentId, pid } = (launch['result'] as { details: { agentId: string; pid: number } }).details;
|
|
261
|
+
expect(readFileSync(join(subagentDir(), `${agentId}.lock`), 'utf8')).toBe(String(pid));
|
|
262
|
+
} finally {
|
|
263
|
+
await rpc.close();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('the background notification records the child usage (review r2: architecture 1)', async () => {
|
|
268
|
+
const run = await runJson(sandbox, script([task({ prompt: 'go\n@@say BG', run_in_background: true })]), ['--no-session']);
|
|
269
|
+
const entry = run.events
|
|
270
|
+
.filter((event) => event.type === 'entry_appended')
|
|
271
|
+
.map((event) => event['entry'] as { customType?: string; details?: { agentId: string; usage?: { totalTokens: number } } })
|
|
272
|
+
.find((candidate) => candidate.customType === 'pstack/task');
|
|
273
|
+
const spent = transcriptOf(entry?.details?.agentId ?? '')
|
|
274
|
+
.filter((line) => line.message?.role === 'assistant')
|
|
275
|
+
.reduce((sum, line) => sum + ((line.message as { usage?: { totalTokens: number } }).usage?.totalTokens ?? 0), 0);
|
|
276
|
+
expect([spent > 0, entry?.details?.usage?.totalTokens]).toEqual([true, spent]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('a failed background child notifies with status error and its message (TASK-015)', async () => {
|
|
280
|
+
const run = await runJson(sandbox, script([task({ prompt: 'go\n@@fail boom', run_in_background: true })]), ['--no-session']);
|
|
281
|
+
const notice = run.events
|
|
282
|
+
.filter((event) => event.type === 'entry_appended')
|
|
283
|
+
.map((event) => event['entry'] as { customType?: string; content?: string })
|
|
284
|
+
.find((candidate) => candidate.customType === 'pstack/task');
|
|
285
|
+
expect(notice?.content?.split('\n').filter((line) => line.startsWith('status:') || line.startsWith('detail:'))).toEqual(['status: error', 'detail: boom']);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('poteto-agent runs in the background by default (TASK-009)', async () => {
|
|
289
|
+
const { result } = await runTask({ prompt: 'go\n@@say POTETO', subagent_type: 'poteto-agent' });
|
|
290
|
+
expect(result.text.startsWith('Subagent is running in the background.')).toBe(true);
|
|
291
|
+
expect(result.details).toMatchObject({ subagentType: 'poteto-agent', background: true });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('two Task calls in one message run at the same time (TASK-004)', async () => {
|
|
295
|
+
const run = await runJson(sandbox, script([[task({ prompt: 'go\n@@sleep 1200 A' }), task({ prompt: 'go\n@@sleep 1200 B' })]]), ['--no-session']);
|
|
296
|
+
const lifecycle = run.events.filter((event) => event['toolName'] === 'Task' && (event.type === 'tool_execution_start' || event.type === 'tool_execution_end')).map((event) => event.type);
|
|
297
|
+
expect(lifecycle).toEqual(['tool_execution_start', 'tool_execution_start', 'tool_execution_end', 'tool_execution_end']);
|
|
298
|
+
expect(
|
|
299
|
+
toolEnds(run.events, 'Task')
|
|
300
|
+
.map((end) => /<response>\n(.*)\n<\/response>/.exec(end.text)?.[1])
|
|
301
|
+
.toSorted(),
|
|
302
|
+
).toEqual(['A', 'B']);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('usage accounting', () => {
|
|
307
|
+
const tokens = (usage: { input: number; output: number; cacheRead: number; cacheWrite: number }) => usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
308
|
+
|
|
309
|
+
test('foreground usage includes what the child spent on its own subagent (review r2: architecture 2)', async () => {
|
|
310
|
+
const { result } = await runTask({ prompt: 'go\n@@task-chain 1' });
|
|
311
|
+
const entries = transcriptOf((result.details as { agentId: string }).agentId);
|
|
312
|
+
const spent = (role: string) => entries.filter((entry) => entry.message?.role === role).reduce((sum, entry) => sum + ((entry.message as { usage?: { totalTokens: number } }).usage?.totalTokens ?? 0), 0);
|
|
313
|
+
expect([spent('toolResult') > 0, result.usage?.totalTokens]).toEqual([true, spent('assistant') + spent('toolResult')]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('foreground subagent usage enters the session totals (TASK-014)', async () => {
|
|
317
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
318
|
+
try {
|
|
319
|
+
rpc.send({ type: 'prompt', message: script([task({ prompt: 'go\n@@say FG' })]) });
|
|
320
|
+
const end = await rpc.waitFor((event) => event.type === 'tool_execution_end' && event['toolName'] === 'Task');
|
|
321
|
+
await rpc.waitFor((event) => event.type === 'agent_settled');
|
|
322
|
+
rpc.send({ id: 'stats', type: 'get_session_stats' });
|
|
323
|
+
const stats = (await rpc.waitFor((event) => event['id'] === 'stats'))['data'] as { tokens: { total: number } };
|
|
324
|
+
const own = rpc.events
|
|
325
|
+
.filter((event) => event.type === 'message_end' && (event['message'] as { role: string }).role === 'assistant')
|
|
326
|
+
.reduce((sum, event) => sum + tokens((event['message'] as { usage: Parameters<typeof tokens>[0] }).usage), 0);
|
|
327
|
+
const child = tokens((end['result'] as { usage: Parameters<typeof tokens>[0] }).usage);
|
|
328
|
+
expect([child > 0, stats.tokens.total]).toEqual([true, own + child]);
|
|
329
|
+
} finally {
|
|
330
|
+
await rpc.close();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('resume over RPC', () => {
|
|
336
|
+
const settledAfter = (rpc: ReturnType<typeof startRpc>, mark: number) => (event: PiEvent) => event.type === 'agent_settled' && rpc.events.indexOf(event) >= mark;
|
|
337
|
+
|
|
338
|
+
test('continues the same transcript, steers a running agent, and refuses unknown agents (TASK-005, C-H4)', async () => {
|
|
339
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
340
|
+
try {
|
|
341
|
+
const prompt = async (message: string) => {
|
|
342
|
+
const mark = rpc.events.length;
|
|
343
|
+
rpc.send({ type: 'prompt', message });
|
|
344
|
+
await rpc.waitFor(settledAfter(rpc, mark));
|
|
345
|
+
return toolEnds(rpc.events.slice(mark), 'Task');
|
|
346
|
+
};
|
|
347
|
+
const [first] = await prompt(script([task({ prompt: 'go\n@@say ONE' })]));
|
|
348
|
+
const agentId = idOf(first?.text);
|
|
349
|
+
const [resumed] = await prompt(script([task({ prompt: 'again\n@@count', resume: agentId, model: 'scripted/plain' })]));
|
|
350
|
+
expect(/<response>\n(.*)\n<\/response>/.exec(resumed?.text ?? '')?.[1]).toBe('USERS=2');
|
|
351
|
+
expect(resumed?.text.endsWith('Note: model "scripted/plain" was ignored; a resumed agent keeps its earlier model.')).toBe(true);
|
|
352
|
+
|
|
353
|
+
const [slow] = await prompt(script([task({ prompt: 'go\n@@sleep 4000 SLOW', run_in_background: true })]));
|
|
354
|
+
const [steered] = await prompt(script([task({ prompt: 'change course\n@@say STEERED', resume: idOf(slow?.text), model: 'scripted/plain' })]));
|
|
355
|
+
expect([steered?.isError, steered?.text.split('\n')]).toEqual([
|
|
356
|
+
false,
|
|
357
|
+
[
|
|
358
|
+
`Sent to agent ${idOf(slow?.text)} while it runs: it reads the message after its current step. Its final response still arrives as its launch said, as a completion notification for a background agent or as the waiting Task result for a foreground one, so do not wait or poll for it.`,
|
|
359
|
+
'Note: model "scripted/plain" was ignored; a resumed agent keeps its earlier model.',
|
|
360
|
+
],
|
|
361
|
+
]);
|
|
362
|
+
const [unknown] = await prompt(script([task({ prompt: 'x', resume: '00000000-0000-4000-8000-000000000000' })]));
|
|
363
|
+
expect(unknown?.text).toBe('Unknown agent ID 00000000-0000-4000-8000-000000000000. Resume needs an agent ID returned by an earlier Task call in this workspace.');
|
|
364
|
+
const notice = await rpc.waitFor((event) => event.type === 'message_end' && (event['message'] as { role: string }).role === 'custom');
|
|
365
|
+
const text = contentText((notice['message'] as { content: string }).content);
|
|
366
|
+
expect([text.includes(`task_id: ${idOf(slow?.text)}\n`), text.includes('\ndetail: STEERED\n')]).toEqual([true, true]);
|
|
367
|
+
expect(transcriptOf(idOf(slow?.text)).filter((entry) => entry.message?.role === 'user').length).toBe(2);
|
|
368
|
+
} finally {
|
|
369
|
+
await rpc.close();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('resume across processes and workspaces', () => {
|
|
375
|
+
test('a new Pi process in the same workspace resumes the agent with its context (TASK-005)', async () => {
|
|
376
|
+
const first = await runJson(sandbox, script([task({ prompt: 'go\n@@say ONE' })]), ['--no-session']);
|
|
377
|
+
const agentId = idOf(toolEnds(first.events, 'Task')[0]?.text);
|
|
378
|
+
const second = await runJson(sandbox, script([task({ prompt: 'again\n@@count', resume: agentId })]), ['--no-session']);
|
|
379
|
+
expect(/<response>\n(.*)\n<\/response>/.exec(toolEnds(second.events, 'Task')[0]?.text ?? '')?.[1]).toBe('USERS=2');
|
|
380
|
+
expect(readdirSync(subagentDir()).filter((name) => name.endsWith(`_${agentId}.jsonl`))).toHaveLength(1);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('another workspace cannot resume it, so agents never cross workspaces (review: parity 4, security)', async () => {
|
|
384
|
+
const first = await runJson(sandbox, script([task({ prompt: 'go\n@@say ONE' })]), ['--no-session']);
|
|
385
|
+
const agentId = idOf(toolEnds(first.events, 'Task')[0]?.text);
|
|
386
|
+
const elsewhere = { ...sandbox, workDir: join(sandbox.root, 'elsewhere') };
|
|
387
|
+
mkdirSync(elsewhere.workDir);
|
|
388
|
+
const second = await runJson(elsewhere, script([task({ prompt: 'again\n@@count', resume: agentId })]), ['--no-session']);
|
|
389
|
+
expect(toolEnds(second.events, 'Task')[0]?.text).toBe(`Unknown agent ID ${agentId}. Resume needs an agent ID returned by an earlier Task call in this workspace.`);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('two calls in one message that resume the same agent get one writer: the second steers the first (review: security 3, architecture 2; C-H4)', async () => {
|
|
393
|
+
const first = await runJson(sandbox, script([task({ prompt: 'go\n@@say ONE' })]), ['--no-session']);
|
|
394
|
+
const agentId = idOf(toolEnds(first.events, 'Task')[0]?.text);
|
|
395
|
+
const second = await runJson(sandbox, script([[task({ prompt: 'a\n@@sleep 1500 A', resume: agentId }), task({ prompt: 'b\n@@say B', resume: agentId })]]), ['--no-session']);
|
|
396
|
+
const ends = toolEnds(second.events, 'Task');
|
|
397
|
+
expect(ends.map((end) => end.text.split('\n')[0]?.slice(0, 32)).toSorted()).toEqual([`Sent to agent ${agentId.slice(0, 18)}`, 'This is the output of the subagent:'.slice(0, 32)]);
|
|
398
|
+
expect(ends.map((end) => /<response>\n(.*)\n<\/response>/.exec(end.text)?.[1]).filter((text) => text !== undefined)).toEqual(['B']);
|
|
399
|
+
expect(readdirSync(subagentDir()).filter((name) => name.endsWith(`_${agentId}.jsonl`))).toHaveLength(1);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('a readonly call cannot steer a running agent that has write access (C-H4)', async () => {
|
|
403
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
404
|
+
try {
|
|
405
|
+
const [slow] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'go\n@@sleep 3000 SLOW', run_in_background: true })])), 'Task');
|
|
406
|
+
const [refused] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'x', resume: idOf(slow?.text), readonly: true })])), 'Task');
|
|
407
|
+
expect([refused?.isError, refused?.text]).toEqual([true, `Agent ${idOf(slow?.text)} is running with write access, so a readonly call cannot message it. Wait for its completion notification, or stop it with stop_agent.`]);
|
|
408
|
+
} finally {
|
|
409
|
+
await rpc.close();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('resume follows the subagent type background setting (C-M7)', async () => {
|
|
414
|
+
const first = await runJson(sandbox, script([task({ prompt: 'go\n@@say ONE', subagent_type: 'poteto-agent' })]), ['--no-session']);
|
|
415
|
+
const agentId = idOf(toolEnds(first.events, 'Task')[0]?.text);
|
|
416
|
+
const second = await runJson(sandbox, script([task({ prompt: 'again\n@@say TWO', resume: agentId })]), ['--no-session']);
|
|
417
|
+
const [resumed] = toolEnds(second.events, 'Task');
|
|
418
|
+
expect([resumed?.text.startsWith('Subagent is running in the background.'), resumed?.details]).toEqual([true, expect.objectContaining({ background: true, subagentType: 'poteto-agent' })]);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('resume edge cases', () => {
|
|
423
|
+
const record = (agentId: string, type: string) => {
|
|
424
|
+
mkdirSync(subagentDir(), { recursive: true });
|
|
425
|
+
writeFileSync(join(subagentDir(), `${agentId}.json`), JSON.stringify({ agentId, type, readonly: false, cwd: sandbox.workDir, createdAt: '2026-10-12T00:00:00.000Z' }));
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
test('honors readonly: true for an agent that started writable (review r2: security 2)', async () => {
|
|
429
|
+
const first = await runJson(sandbox, script([task({ prompt: 'go\n@@say ONE' })]), ['--no-session']);
|
|
430
|
+
const agentId = idOf(toolEnds(first.events, 'Task')[0]?.text);
|
|
431
|
+
const second = await runJson(sandbox, script([task({ prompt: `again\n@@script ${JSON.stringify([{ tool: 'bash', args: { command: 'touch made.txt' } }])}`, resume: agentId, readonly: true })]), ['--no-session']);
|
|
432
|
+
expect(toolEnds(second.events, 'Task')[0]?.text).toContain('[bash error]');
|
|
433
|
+
expect(existsSync(join(sandbox.workDir, 'made.txt'))).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test.for([undefined, false])('a raised readonly floor survives later resumes requesting %s', async (readonly) => {
|
|
437
|
+
const first = await runTask({ prompt: 'go\n@@tools-list' });
|
|
438
|
+
const agentId = idOf(first.result.text);
|
|
439
|
+
expect(first.result.text).toContain('write');
|
|
440
|
+
const path = join(subagentDir(), `${agentId}.json`);
|
|
441
|
+
const original = { ...JSON.parse(readFileSync(path, 'utf8')), extra: { preserved: true } };
|
|
442
|
+
writeFileSync(path, JSON.stringify(original));
|
|
443
|
+
|
|
444
|
+
const restricted = await runTask({ prompt: 'again\n@@tools-list', resume: agentId, readonly: true });
|
|
445
|
+
expect(restricted.result.details).toMatchObject({ readonly: true });
|
|
446
|
+
const resumed = await runTask({
|
|
447
|
+
prompt: `again\n@@script ${JSON.stringify([{ tool: 'write', args: { path: 'regained.txt', content: 'ESCAPED' } }])}`,
|
|
448
|
+
resume: agentId,
|
|
449
|
+
...(readonly === undefined ? {} : { readonly }),
|
|
450
|
+
});
|
|
451
|
+
expect(resumed.result.text).toContain('Tool write not found');
|
|
452
|
+
expect(resumed.result.details).toMatchObject({ readonly: true });
|
|
453
|
+
expect(existsSync(join(sandbox.workDir, 'regained.txt'))).toBe(false);
|
|
454
|
+
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({ ...original, readonly: true });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('resume honors a current readonly agent definition over an older writable record', async () => {
|
|
458
|
+
const extension = join(sandbox.root, 'task-definition.ts');
|
|
459
|
+
const definition = (readonly: boolean) =>
|
|
460
|
+
writeFileSync(
|
|
461
|
+
extension,
|
|
462
|
+
`
|
|
463
|
+
import { registerTask } from ${JSON.stringify(join(PACKAGE_ROOT, 'src/task/tool.ts'))};
|
|
464
|
+
import { GENERAL_PURPOSE } from ${JSON.stringify(join(PACKAGE_ROOT, 'src/task/agents.ts'))};
|
|
465
|
+
export default (pi) => {
|
|
466
|
+
// Each -e extension gets its own module graph, so a depth guard here can misread in the subagent, whose
|
|
467
|
+
// own Task comes first. Decide at session_start, when the existing providers are visible.
|
|
468
|
+
pi.on('session_start', () => {
|
|
469
|
+
if (pi.getActiveTools().includes('Task')) return;
|
|
470
|
+
registerTask(pi, { agents: [{ ...GENERAL_PURPOSE, readonly: ${readonly} }], depth: 0 });
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
`,
|
|
474
|
+
);
|
|
475
|
+
writeFileSync(
|
|
476
|
+
join(sandbox.agentDir, 'settings.json'),
|
|
477
|
+
JSON.stringify({
|
|
478
|
+
packages: [{ source: PACKAGE_ROOT, extensions: [] }],
|
|
479
|
+
extensions: [extension, join(PACKAGE_ROOT, 'test/fixtures/scripted-model.ts')],
|
|
480
|
+
quietStartup: true,
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
definition(false);
|
|
484
|
+
const first = await runTask({ prompt: 'go\n@@tools-list' });
|
|
485
|
+
const agentId = idOf(first.result.text);
|
|
486
|
+
expect(first.result.isError, first.result.text).toBe(false);
|
|
487
|
+
expect(first.result.details).toMatchObject({ readonly: false });
|
|
488
|
+
definition(true);
|
|
489
|
+
const resumed = await runTask({ prompt: 'again\n@@tools-list', resume: agentId, readonly: false });
|
|
490
|
+
expect(resumed.result.isError, resumed.result.text).toBe(false);
|
|
491
|
+
expect(resumed.result.details).toMatchObject({ readonly: true });
|
|
492
|
+
expect(/<response>\n(.*)\n<\/response>/.exec(resumed.result.text)?.[1]?.split(',')).not.toContain('write');
|
|
493
|
+
expect(JSON.parse(readFileSync(join(subagentDir(), `${agentId}.json`), 'utf8')).readonly).toBe(true);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('refuses an agent whose transcript is missing instead of starting it fresh (review r2: security)', async () => {
|
|
497
|
+
const agentId = '22222222-3333-4444-8555-666666666666';
|
|
498
|
+
record(agentId, 'generalPurpose');
|
|
499
|
+
const { result } = await runTask({ prompt: 'again\n@@count', resume: agentId });
|
|
500
|
+
expect(result.text).toBe(`Agent ${agentId} has no transcript to resume, so its earlier context is gone. Start a new agent instead.`);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('explains a recorded type this version no longer has (review r2: architecture 5)', async () => {
|
|
504
|
+
const agentId = '33333333-4444-4555-8666-777777777777';
|
|
505
|
+
record(agentId, 'Comment Sicko');
|
|
506
|
+
writeFileSync(join(subagentDir(), `2026-10-12T00-00-00-000Z_${agentId}.jsonl`), `${JSON.stringify({ type: 'session', version: 3, id: agentId, cwd: sandbox.workDir })}\n`);
|
|
507
|
+
const { result } = await runTask({ prompt: 'again', resume: agentId });
|
|
508
|
+
expect(result.text).toBe(`Agent ${agentId} was a "Comment Sicko" subagent, a type this version no longer has. Start a new agent instead.`);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('a subagent that resumes a depth-1 agent runs it one level down, so nesting stays capped (recheck r2: 1)', async () => {
|
|
512
|
+
const first = await runJson(sandbox, script([task({ prompt: 'go\n@@say ONE' })]), ['--no-session']);
|
|
513
|
+
const agentId = idOf(toolEnds(first.events, 'Task')[0]?.text);
|
|
514
|
+
const resumeFromChild = JSON.stringify([{ tool: 'Task', args: { description: 'r', prompt: 'again\n@@tools-list', resume: agentId } }]);
|
|
515
|
+
const { result } = await runTask({ prompt: `go\n@@script ${resumeFromChild}` });
|
|
516
|
+
const nested = /<response>\n([^\n]*)\n<\/response>/.exec(/<response>\n([\s\S]*)\n<\/response>/.exec(result.text)?.[1] ?? '')?.[1]?.split(',') ?? [];
|
|
517
|
+
expect([nested.includes('TodoWrite'), nested.includes('Task')]).toEqual([true, false]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test('refuses an agent whose record names another working directory (recheck r2)', async () => {
|
|
521
|
+
const agentId = '44444444-5555-4666-8777-888888888888';
|
|
522
|
+
mkdirSync(subagentDir(), { recursive: true });
|
|
523
|
+
writeFileSync(join(subagentDir(), `${agentId}.json`), JSON.stringify({ agentId, type: 'generalPurpose', readonly: false, cwd: '/elsewhere/with-a-colliding-name', createdAt: '2026-10-12T00:00:00.000Z' }));
|
|
524
|
+
writeFileSync(join(subagentDir(), `2026-10-12T00-00-00-000Z_${agentId}.jsonl`), `${JSON.stringify({ type: 'session', version: 3, id: agentId, cwd: '/elsewhere/with-a-colliding-name' })}\n`);
|
|
525
|
+
const { result } = await runTask({ prompt: 'again', resume: agentId });
|
|
526
|
+
expect(result.text).toBe(`Agent ${agentId} belongs to /elsewhere/with-a-colliding-name, not this workspace. Start a new agent instead.`);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('an agent keeps its depth on resume, so the root cannot lift a grandchild (review r2: architecture)', async () => {
|
|
530
|
+
const { result } = await runTask({ prompt: 'go\n@@task-chain 1' });
|
|
531
|
+
const grandchild = idOf(/<response>\n([\s\S]*)\n<\/response>/.exec(result.text)?.[1]);
|
|
532
|
+
const resumed = await runTask({ prompt: 'again\n@@tools-list', resume: grandchild });
|
|
533
|
+
const tools = /<response>\n(.*)\n<\/response>/.exec(resumed.result.text)?.[1]?.split(',') ?? [];
|
|
534
|
+
expect([tools.includes('TodoWrite'), tools.includes('Task')]).toEqual([true, false]);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe('stop_agent (C-H4)', () => {
|
|
539
|
+
const alive = (pid: number) => {
|
|
540
|
+
try {
|
|
541
|
+
process.kill(pid, 0);
|
|
542
|
+
return true;
|
|
543
|
+
} catch {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const stop = (agentId: string) => ({ tool: 'stop_agent', args: { agent_id: agentId } });
|
|
548
|
+
|
|
549
|
+
test('stops a background agent this chat runs; its notification reports the stop', async () => {
|
|
550
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
551
|
+
try {
|
|
552
|
+
const [launch] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'go\n@@sleep 60000 SLOW', run_in_background: true })])), 'Task');
|
|
553
|
+
const { agentId, pid } = (launch as { details: { agentId: string; pid: number } }).details;
|
|
554
|
+
const events = await rpcPrompt(rpc, script([stop(agentId)]));
|
|
555
|
+
const [stopped] = toolEnds(events, 'stop_agent');
|
|
556
|
+
expect([stopped?.isError, stopped?.text, alive(pid)]).toEqual([false, `Stopped agent ${agentId}: its run was aborted and its process has exited. Its result reports the stop; resume it with Task resume.`, false]);
|
|
557
|
+
const notice = await rpc.waitFor((event) => event.type === 'message_end' && (event['message'] as { role: string }).role === 'custom');
|
|
558
|
+
expect(
|
|
559
|
+
contentText((notice['message'] as { content: string }).content)
|
|
560
|
+
.split('\n')
|
|
561
|
+
.filter((line) => /^(status|detail):/.test(line)),
|
|
562
|
+
).toEqual(['status: error', 'detail: Subagent was stopped with stop_agent']);
|
|
563
|
+
expect(readFileSync(join(subagentDir(), `${agentId}.json`), 'utf8')).toContain(agentId);
|
|
564
|
+
} finally {
|
|
565
|
+
await rpc.close();
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('stops an agent another Pi process runs, which Task resume there refuses to message, and leaves an idle agent alone', async () => {
|
|
570
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
571
|
+
try {
|
|
572
|
+
const [launch] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'go\n@@sleep 60000 SLOW', run_in_background: true })])), 'Task');
|
|
573
|
+
const { agentId, pid } = (launch as { details: { agentId: string; pid: number } }).details;
|
|
574
|
+
// Pi writes the transcript once the child takes its prompt; before that a resume finds no context to continue.
|
|
575
|
+
await vi.waitFor(() => expect(readdirSync(subagentDir()).some((name) => name.endsWith(`_${agentId}.jsonl`))).toBe(true), { timeout: 15_000, interval: 50 });
|
|
576
|
+
const resumed = await runJson(sandbox, script([task({ prompt: 'x', resume: agentId })]), ['--no-session']);
|
|
577
|
+
expect(toolEnds(resumed.events, 'Task')[0]?.text).toBe(`Agent ${agentId} is still running (pid ${pid}; lock ${join(subagentDir(), `${agentId}.lock`)}). Wait for its completion before resuming it.`);
|
|
578
|
+
const other = await runJson(sandbox, script([stop(agentId)]), ['--no-session']);
|
|
579
|
+
expect([toolEnds(other.events, 'stop_agent')[0]?.text, alive(pid)]).toEqual([
|
|
580
|
+
`Stopped agent ${agentId}: another Pi process ran it (pid ${pid}), so it got SIGTERM and has exited. That process reports the stop as the agent's result.`,
|
|
581
|
+
false,
|
|
582
|
+
]);
|
|
583
|
+
const notice = await rpc.waitFor((event) => event.type === 'message_end' && (event['message'] as { role: string }).role === 'custom');
|
|
584
|
+
expect(contentText((notice['message'] as { content: string }).content)).toContain('status: error');
|
|
585
|
+
const again = await runJson(sandbox, script([stop(agentId)]), ['--no-session']);
|
|
586
|
+
expect(toolEnds(again.events, 'stop_agent')[0]?.text).toBe(`Agent ${agentId} is not running, so there is nothing to stop.`);
|
|
587
|
+
} finally {
|
|
588
|
+
await rpc.close();
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('plan mode launches (C-H3, C-M1)', () => {
|
|
594
|
+
const git = (...args: string[]) => execFileSync('git', ['-C', sandbox.workDir, '-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { encoding: 'utf8' }).trim();
|
|
595
|
+
const tools = (text: string | undefined) => /<response>\n(.*)\n<\/response>/.exec(text ?? '')?.[1]?.split(',') ?? [];
|
|
596
|
+
|
|
597
|
+
test('a cloud Task in plan mode runs here, readonly, and creates no worktree or branch', async () => {
|
|
598
|
+
git('init', '-q', '-b', 'main');
|
|
599
|
+
writeFileSync(join(sandbox.workDir, 'a.txt'), 'A');
|
|
600
|
+
git('add', 'a.txt');
|
|
601
|
+
git('commit', '-qm', 'first');
|
|
602
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
603
|
+
try {
|
|
604
|
+
await rpcPrompt(rpc, '/plan on');
|
|
605
|
+
const [result] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'go\n@@tools-list', environment: 'cloud' })])), 'Task');
|
|
606
|
+
expect(result?.isError, result?.text).toBe(false);
|
|
607
|
+
expect([tools(result?.text).includes('write'), result?.details]).toEqual([false, expect.objectContaining({ readonly: true, worktree: null })]);
|
|
608
|
+
expect(result?.text).toContain('Note: environment "cloud" was ignored; plan mode runs subagents here, readonly.');
|
|
609
|
+
expect([git('branch', '--list', 'pstack/*'), existsSync(join(sandbox.agentDir, 'pstack/workspaces', slug(sandbox.workDir), 'worktrees'))]).toEqual(['', false]);
|
|
610
|
+
} finally {
|
|
611
|
+
await rpc.close();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('a resume in plan mode runs readonly for that launch only, leaving the record writable', async () => {
|
|
616
|
+
const first = await runTask({ prompt: 'go\n@@say ONE' });
|
|
617
|
+
const agentId = idOf(first.result.text);
|
|
618
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
619
|
+
try {
|
|
620
|
+
await rpcPrompt(rpc, '/plan on');
|
|
621
|
+
const [planned] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'again\n@@tools-list', resume: agentId })])), 'Task');
|
|
622
|
+
expect([tools(planned?.text).includes('write'), planned?.details]).toEqual([false, expect.objectContaining({ readonly: true })]);
|
|
623
|
+
expect(JSON.parse(readFileSync(join(subagentDir(), `${agentId}.json`), 'utf8')).readonly).toBe(false);
|
|
624
|
+
await rpcPrompt(rpc, '/plan off');
|
|
625
|
+
const [after] = toolEnds(await rpcPrompt(rpc, script([task({ prompt: 'again\n@@tools-list', resume: agentId })])), 'Task');
|
|
626
|
+
expect(tools(after?.text).includes('write')).toBe(true);
|
|
627
|
+
} finally {
|
|
628
|
+
await rpc.close();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe('isolated workers share the root workspace (C-M2)', () => {
|
|
634
|
+
test("a cloud worker's own subagent is recorded in the root workspace, where agent_status in the root sees it", async () => {
|
|
635
|
+
execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: sandbox.workDir });
|
|
636
|
+
execFileSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'first', '--allow-empty'], { cwd: sandbox.workDir });
|
|
637
|
+
const { result } = await runTask({ prompt: 'go\n@@task-chain 1', environment: 'cloud' });
|
|
638
|
+
const worker = (result.details as { agentId: string }).agentId;
|
|
639
|
+
const grandchild = idOf(/<response>\n([\s\S]*)\n<\/response>/.exec(result.text)?.[1]);
|
|
640
|
+
expect(worker).not.toBe(grandchild);
|
|
641
|
+
expect(
|
|
642
|
+
readdirSync(subagentDir())
|
|
643
|
+
.filter((name) => name.endsWith('.json'))
|
|
644
|
+
.toSorted(),
|
|
645
|
+
).toEqual([`${grandchild}.json`, `${worker}.json`].toSorted());
|
|
646
|
+
expect(readdirSync(join(sandbox.agentDir, 'pstack/workspaces'))).toEqual([slug(sandbox.workDir)]);
|
|
647
|
+
const status = await runJson(sandbox, script([{ tool: 'agent_status', args: {} }]), ['--no-session']);
|
|
648
|
+
expect(toolEnds(status.events, 'agent_status')[0]?.text.startsWith('2 subagents in this workspace:')).toBe(true);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe('child environment', () => {
|
|
653
|
+
test('a bad attachment leaves no agent record behind (review: security 4, architecture 8)', async () => {
|
|
654
|
+
const { result } = await runTask({ prompt: 'go\n@@say X', file_attachments: ['missing.png'] });
|
|
655
|
+
expect([result.isError, result.text]).toEqual([true, `file_attachments: ${join(sandbox.workDir, 'missing.png')} does not exist`]);
|
|
656
|
+
expect(existsSync(subagentDir()) ? readdirSync(subagentDir()) : []).toEqual([]);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("commands a subagent runs do not inherit the port's variables (review: architecture)", async () => {
|
|
660
|
+
const { result } = await runTask({ prompt: 'go\n@@script [{"tool":"bash","args":{"command":"env | grep -c PI_PSTACK || true"}}]' });
|
|
661
|
+
expect(/<response>\n([\s\S]*)\n<\/response>/.exec(result.text)?.[1]).toBe('RESULTS:\n[bash] 0\n');
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('cancellation', () => {
|
|
666
|
+
// Pi sets process.title to "pi", so ps cannot find a child by its arguments; the Task details carry its pid.
|
|
667
|
+
const alive = (pid: number) => {
|
|
668
|
+
try {
|
|
669
|
+
process.kill(pid, 0);
|
|
670
|
+
return true;
|
|
671
|
+
} catch {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
async function gone(pids: readonly number[], timeoutMs = 12_000): Promise<boolean> {
|
|
676
|
+
const deadline = Date.now() + timeoutMs;
|
|
677
|
+
while (Date.now() < deadline) {
|
|
678
|
+
if (pids.every((pid) => !alive(pid))) return true;
|
|
679
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
680
|
+
}
|
|
681
|
+
return pids.every((pid) => !alive(pid));
|
|
682
|
+
}
|
|
683
|
+
const childrenOf = (pid: number) =>
|
|
684
|
+
execFileSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8' })
|
|
685
|
+
.trim()
|
|
686
|
+
.split('\n')
|
|
687
|
+
.map((line) => line.trim().split(/\s+/).map(Number))
|
|
688
|
+
.filter(([, parent]) => parent === pid)
|
|
689
|
+
.map(([child]) => child as number);
|
|
690
|
+
const detailsPid = (event: PiEvent, key: 'result' | 'partialResult') => (event[key] as { details: { pid: number } }).details.pid;
|
|
691
|
+
|
|
692
|
+
test('aborting the parent turn stops a foreground child (TASK-013)', async () => {
|
|
693
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
694
|
+
try {
|
|
695
|
+
rpc.send({ type: 'prompt', message: script([task({ prompt: 'go\n@@sleep 60000 SLOW' })]) });
|
|
696
|
+
const update = await rpc.waitFor((event) => event.type === 'tool_execution_update' && event['toolName'] === 'Task');
|
|
697
|
+
const pid = detailsPid(update, 'partialResult');
|
|
698
|
+
expect(alive(pid)).toBe(true);
|
|
699
|
+
rpc.send({ type: 'abort' });
|
|
700
|
+
await rpc.waitFor((event) => event.type === 'agent_settled');
|
|
701
|
+
expect(await gone([pid])).toBe(true);
|
|
702
|
+
} finally {
|
|
703
|
+
await rpc.close();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('closing the session stops a background child (TASK-013)', async () => {
|
|
708
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
709
|
+
rpc.send({ type: 'prompt', message: script([task({ prompt: 'go\n@@sleep 60000 SLOW', run_in_background: true })]) });
|
|
710
|
+
const launch = await rpc.waitFor((event) => event.type === 'tool_execution_end' && event['toolName'] === 'Task');
|
|
711
|
+
const pid = detailsPid(launch, 'result');
|
|
712
|
+
expect(alive(pid)).toBe(true);
|
|
713
|
+
await rpc.close();
|
|
714
|
+
// Pi's exit also orphans the child, and its watchdog would end it, so this checks the outcome and the notice.
|
|
715
|
+
// The shutdown kill itself is proven by the reload and new-session tests in lifecycle.e2e.test.ts.
|
|
716
|
+
const notice = rpc.events.find((event) => event.type === 'extension_ui_request' && event['method'] === 'notify' && String(event['message']).startsWith('Stopped '));
|
|
717
|
+
expect([notice?.['message'], await gone([pid])]).toEqual(['Stopped 1 background subagent (session quit). Resume one with its agent ID.', true]);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// A silent bash command writes nothing to stdout, so an orphaned child gets no EPIPE and only the watchdog ends it.
|
|
721
|
+
test('a subagent busy in a silent command exits when its parent is killed outright (review: parity 5)', async () => {
|
|
722
|
+
const rpc = startRpc(sandbox, ['--no-session']);
|
|
723
|
+
rpc.send({ type: 'prompt', message: script([task({ prompt: 'go\n@@script [{"tool":"bash","args":{"command":"sleep 45"}}]', run_in_background: true })]) });
|
|
724
|
+
const launch = await rpc.waitFor((event) => event.type === 'tool_execution_end' && event['toolName'] === 'Task');
|
|
725
|
+
const pid = detailsPid(launch, 'result');
|
|
726
|
+
const deadline = Date.now() + 10_000;
|
|
727
|
+
while (childrenOf(pid).length === 0 && Date.now() < deadline) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
728
|
+
const shell = childrenOf(pid);
|
|
729
|
+
expect([alive(pid), shell.length > 0]).toEqual([true, true]);
|
|
730
|
+
rpc.child.kill('SIGKILL');
|
|
731
|
+
await rpc.close();
|
|
732
|
+
expect(await gone([pid, ...shell])).toBe(true);
|
|
733
|
+
});
|
|
734
|
+
});
|