@zerwiz/ymir 0.1.3 → 0.1.5
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/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -39
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,2272 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# End-to-end and regression tests for the deterministic public-followup consumer.
|
|
3
|
+
#
|
|
4
|
+
# The failure this suite pins: firstmate promises a public final reply in an X or
|
|
5
|
+
# Discord thread, routes the work out, and then the session compacts or restarts.
|
|
6
|
+
# Nothing in memory survives. The promise is only kept if a terminal work result
|
|
7
|
+
# reconciles the typed obligation from DISK and the final reply lands in the
|
|
8
|
+
# ORIGINAL thread exactly once.
|
|
9
|
+
#
|
|
10
|
+
# Everything here is hermetic: the relay is a fakebin `curl`, so no port, no
|
|
11
|
+
# server, and no public post. tasks-axi and jq are the real tools, because
|
|
12
|
+
# tasks-axi owns the obligation state machine and stubbing it would test nothing.
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
# shellcheck source=tests/lib.sh
|
|
16
|
+
# shellcheck disable=SC1091
|
|
17
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
18
|
+
|
|
19
|
+
PF="$ROOT/bin/fm-public-followup.sh"
|
|
20
|
+
EMIT="$ROOT/bin/fm-public-followup-emit.sh"
|
|
21
|
+
POLL="$ROOT/bin/fm-x-poll.sh"
|
|
22
|
+
TEARDOWN="$ROOT/bin/fm-teardown.sh"
|
|
23
|
+
PROMOTE="$ROOT/bin/fm-promote.sh"
|
|
24
|
+
SESSION_START="$ROOT/bin/fm-session-start.sh"
|
|
25
|
+
TMP_ROOT=$(fm_test_tmproot fm-public-followup)
|
|
26
|
+
PF_TEST_NOW=1787539200
|
|
27
|
+
|
|
28
|
+
command -v jq >/dev/null 2>&1 || { echo "skip: jq not found"; exit 0; }
|
|
29
|
+
command -v tasks-axi >/dev/null 2>&1 || { echo "skip: tasks-axi not found"; exit 0; }
|
|
30
|
+
|
|
31
|
+
# A fakebin `curl` standing in for the relay. It logs every call so a test can
|
|
32
|
+
# prove exactly how many public posts happened, and honours FAKE_FOLLOWUP_CODE so
|
|
33
|
+
# a transport failure can be simulated.
|
|
34
|
+
make_fake_curl() { # <home>
|
|
35
|
+
local fakebin
|
|
36
|
+
fakebin=$(fm_fakebin "$1")
|
|
37
|
+
cat > "$fakebin/curl" <<'SH'
|
|
38
|
+
#!/usr/bin/env bash
|
|
39
|
+
ofile="" url="" data=""
|
|
40
|
+
while [ $# -gt 0 ]; do
|
|
41
|
+
case "$1" in
|
|
42
|
+
-o) ofile=$2; shift 2 ;;
|
|
43
|
+
--data-binary)
|
|
44
|
+
case "$2" in
|
|
45
|
+
@-) data=$(cat) ;;
|
|
46
|
+
@*) data=$(cat -- "${2#@}") ;;
|
|
47
|
+
*) data=$2 ;;
|
|
48
|
+
esac
|
|
49
|
+
shift 2
|
|
50
|
+
;;
|
|
51
|
+
-H|-m|-w|-X) shift 2 ;;
|
|
52
|
+
-s) shift ;;
|
|
53
|
+
http://*|https://*) url=$1; shift ;;
|
|
54
|
+
*) shift ;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
if [ -n "${FAKE_CURL_LOG:-}" ]; then
|
|
58
|
+
{ echo "url=$url"; echo "data=$data"; } >> "$FAKE_CURL_LOG"
|
|
59
|
+
fi
|
|
60
|
+
case "$url" in
|
|
61
|
+
*/connector/followup) printf '%s' "${FAKE_FOLLOWUP_CODE:-200}" ;;
|
|
62
|
+
*/connector/answer) printf '200' ;;
|
|
63
|
+
*/connector/request-context)
|
|
64
|
+
[ -n "$ofile" ] && printf '%s' "${FAKE_REQCTX_BODY:-}" > "$ofile"
|
|
65
|
+
printf '%s' "${FAKE_REQCTX_CODE:-404}"
|
|
66
|
+
;;
|
|
67
|
+
*/connector/poll) printf '204' ;;
|
|
68
|
+
*) printf '204' ;;
|
|
69
|
+
esac
|
|
70
|
+
exit 0
|
|
71
|
+
SH
|
|
72
|
+
chmod +x "$fakebin/curl"
|
|
73
|
+
printf '%s\n' "$fakebin"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# make_home <name> [relay-on|relay-off]: a firstmate home with its own backlog.
|
|
77
|
+
# relay-off omits .env entirely, which is exactly what a home that never opted
|
|
78
|
+
# into the myfirstmate relay looks like.
|
|
79
|
+
make_home() { # <name> [relay-on|relay-off]
|
|
80
|
+
local home="$TMP_ROOT/$1" relay=${2:-relay-on}
|
|
81
|
+
mkdir -p "$home/data" "$home/state" "$home/config" "$home/projects"
|
|
82
|
+
cp "$ROOT/.tasks.toml" "$home/.tasks.toml"
|
|
83
|
+
cat > "$home/data/backlog.md" <<'EOF'
|
|
84
|
+
## In flight
|
|
85
|
+
|
|
86
|
+
## Queued
|
|
87
|
+
|
|
88
|
+
## Done
|
|
89
|
+
EOF
|
|
90
|
+
[ "$relay" = relay-off ] || printf 'FMX_PAIRING_TOKEN=test-token\n' > "$home/.env"
|
|
91
|
+
make_fake_curl "$home" >/dev/null
|
|
92
|
+
fm_fake_exit0 "$home/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
93
|
+
printf '%s\n' "$home"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
run_pf() { # <home> <args...>
|
|
97
|
+
local home=$1
|
|
98
|
+
shift
|
|
99
|
+
PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
100
|
+
FM_STATE_OVERRIDE="$home/state" FAKE_CURL_LOG="${FAKE_CURL_LOG:-}" \
|
|
101
|
+
FAKE_FOLLOWUP_CODE="${FAKE_FOLLOWUP_CODE:-200}" \
|
|
102
|
+
FMX_NOW_OVERRIDE="${FMX_NOW_OVERRIDE:-$PF_TEST_NOW}" "$PF" "$@"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
tasks_in() { # <home> <tasks-axi args...>
|
|
106
|
+
local home=$1
|
|
107
|
+
shift
|
|
108
|
+
(cd "$home" && tasks-axi "$@")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# seed_commitment <home> <obligation> <request> <platform> <work-home> <work-id>
|
|
112
|
+
# Simulates the intake half that already works today: the relay mention arrives,
|
|
113
|
+
# the typed obligation is created with its opaque thread binding, the work is
|
|
114
|
+
# bound, and the private request context is retained.
|
|
115
|
+
seed_commitment() {
|
|
116
|
+
local home=$1 obligation=$2 request=$3 platform=$4 work_home=$5 work_id=$6
|
|
117
|
+
jq -n --arg r "$request" --arg p "$platform" \
|
|
118
|
+
'{request_id:$r, platform:$p,
|
|
119
|
+
context_binding:{version:"ctx1", value:("ctx1_" + $r)},
|
|
120
|
+
public_safe_summary:"fix worker placement when two spaces share a name",
|
|
121
|
+
received_at:"2026-07-30T10:00:00Z",
|
|
122
|
+
followup_expires_at:"2026-08-06T10:00:00Z",
|
|
123
|
+
reservation_expires_at:"2026-08-06T10:00:00Z"}' > "$home/request.json"
|
|
124
|
+
jq -n '{type:"pr-merged", project:"firstmate",
|
|
125
|
+
required_deliverables:["pr_url"], completion_policy:"all-required"}' \
|
|
126
|
+
> "$home/expected.json"
|
|
127
|
+
jq -n --arg h "$work_home" --arg w "$work_id" \
|
|
128
|
+
'{relation_id:"rel-code", work_ref:{home_id:$h, task_id:$w},
|
|
129
|
+
role:"fulfills", required:true, generation:1}' > "$home/relation.json"
|
|
130
|
+
|
|
131
|
+
tasks_in "$home" public-followup add "$obligation" \
|
|
132
|
+
--request-context-file "$home/request.json" --purpose promised-final \
|
|
133
|
+
--expected-final-file "$home/expected.json" --expires-at 2026-10-01T00:00:00Z >/dev/null \
|
|
134
|
+
|| fail "could not create the public commitment"
|
|
135
|
+
tasks_in "$home" public-followup bind-work "$obligation" \
|
|
136
|
+
--relation-file "$home/relation.json" >/dev/null \
|
|
137
|
+
|| fail "could not bind work to the public commitment"
|
|
138
|
+
|
|
139
|
+
# The mention payload and the durable per-request context, exactly as the relay
|
|
140
|
+
# poll records them at intake.
|
|
141
|
+
mkdir -p "$home/state/x-inbox"
|
|
142
|
+
jq -n --arg r "$request" --arg p "$platform" \
|
|
143
|
+
'{request_id:$r, platform:$p, text:"please fix worker placement"}' \
|
|
144
|
+
> "$home/state/x-inbox/$request.json"
|
|
145
|
+
chmod 700 "$home/state/x-inbox"
|
|
146
|
+
chmod 600 "$home/state/x-inbox/$request.json"
|
|
147
|
+
FM_HOME="$home" FMX_NOW_OVERRIDE="$PF_TEST_NOW" bash -c \
|
|
148
|
+
". '$ROOT/bin/fm-x-lib.sh'; fmx_context_registry_set '$home/state' '$request' '$platform' 1900" \
|
|
149
|
+
|| fail "could not retain the private request context"
|
|
150
|
+
|
|
151
|
+
run_pf "$home" register "$obligation" --relation rel-code \
|
|
152
|
+
--work-home "$work_home" --work-id "$work_id" --generation 1 >/dev/null \
|
|
153
|
+
|| fail "could not register the public commitment"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# The pi-rearm shape: a report-ready promised-final bound to a secondmate.
|
|
157
|
+
seed_repro_commitment() { # <home> <obligation> <request> <work-home> <work-id>
|
|
158
|
+
local home=$1 obligation=$2 request=$3 work_home=$4 work_id=$5
|
|
159
|
+
jq -n --arg r "$request" \
|
|
160
|
+
'{request_id:$r, platform:"discord",
|
|
161
|
+
context_binding:{version:"ctx1", value:("ctx1_" + $r)},
|
|
162
|
+
public_safe_summary:"reproduce a Pi recovery notification loop",
|
|
163
|
+
received_at:"2026-08-21T01:12:00Z",
|
|
164
|
+
followup_expires_at:"2026-08-28T01:12:00Z",
|
|
165
|
+
reservation_expires_at:"2026-08-28T01:12:00Z"}' > "$home/request.json"
|
|
166
|
+
jq -n '{type:"report-ready", project:"firstmate",
|
|
167
|
+
required_deliverables:["report_path"], completion_policy:"all-required"}' \
|
|
168
|
+
> "$home/expected.json"
|
|
169
|
+
jq -n --arg h "$work_home" --arg w "$work_id" \
|
|
170
|
+
'{relation_id:"rel-code", work_ref:{home_id:$h, task_id:$w},
|
|
171
|
+
role:"fulfills", required:true, generation:1}' > "$home/relation.json"
|
|
172
|
+
tasks_in "$home" public-followup add "$obligation" --request-context-file "$home/request.json" \
|
|
173
|
+
--purpose promised-final --expected-final-file "$home/expected.json" \
|
|
174
|
+
--expires-at 2026-10-01T00:00:00Z >/dev/null || fail "add failed"
|
|
175
|
+
tasks_in "$home" public-followup bind-work "$obligation" --relation-file "$home/relation.json" >/dev/null \
|
|
176
|
+
|| fail "bind-work failed"
|
|
177
|
+
FM_HOME="$home" FMX_NOW_OVERRIDE="$PF_TEST_NOW" bash -c \
|
|
178
|
+
". '$ROOT/bin/fm-x-lib.sh'; fmx_context_registry_set '$home/state' '$request' discord 2000" \
|
|
179
|
+
|| fail "context retain failed"
|
|
180
|
+
run_pf "$home" register "$obligation" --relation rel-code --work-home "$work_home" \
|
|
181
|
+
--work-id "$work_id" --generation 1 >/dev/null || fail "register failed"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
emit_terminal() { # <child-run-dir> <owning-home> <obligation> <work-home> <work-id> [pr-url] [outcome]
|
|
185
|
+
local owning=$2 obligation=$3 work_home=$4 work_id=$5
|
|
186
|
+
local pr=${6:-https://github.com/example/repo/pull/7} outcome=${7:-pr-merged}
|
|
187
|
+
"$EMIT" --home "$owning" --obligation "$obligation" --relation rel-code \
|
|
188
|
+
--source-home "$work_home" --work-id "$work_id" --generation 1 \
|
|
189
|
+
--outcome "$outcome" --deliverable "pr_url=$pr" \
|
|
190
|
+
--outcome-text 'Fixed: workers now land in the launching workspace even when two spaces share a name.'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
delivery_state() { # <home> <obligation>
|
|
194
|
+
tasks_in "$1" public-followup list --json 2>/dev/null \
|
|
195
|
+
| jq -r --arg id "$2" '(.public_followups // [])
|
|
196
|
+
| map(select(.id == $id)) | .[0].public_followup.delivery.state // "absent"'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
task_state() { # <home> <obligation>
|
|
200
|
+
tasks_in "$1" public-followup list --json 2>/dev/null \
|
|
201
|
+
| jq -r --arg id "$2" '(.public_followups // [])
|
|
202
|
+
| map(select(.id == $id)) | .[0].state // "absent"'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
followup_posts() { # <log>
|
|
206
|
+
local n
|
|
207
|
+
n=$(grep -c 'connector/followup' "$1" 2>/dev/null) || n=0
|
|
208
|
+
printf '%s' "${n:-0}"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# expect_failure <label> <command...>: run <command>, require a non-zero exit, and
|
|
212
|
+
# leave its combined output in EXPECT_OUT for the assertions that follow. Keeps
|
|
213
|
+
# refusal tests readable without toggling errexit around every case.
|
|
214
|
+
EXPECT_OUT=
|
|
215
|
+
expect_failure() {
|
|
216
|
+
local label=$1
|
|
217
|
+
shift
|
|
218
|
+
if EXPECT_OUT=$("$@" 2>&1); then
|
|
219
|
+
fail "$label (unexpectedly succeeded)"$'\n'"--- output ---"$'\n'"$EXPECT_OUT"
|
|
220
|
+
fi
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# --- 0. bounded, single-line, character-safe outcome text -----------------------
|
|
224
|
+
|
|
225
|
+
# The outcome sentence becomes a public reply, so bounding it must not mangle
|
|
226
|
+
# non-ASCII characters, and control characters must never survive into the typed
|
|
227
|
+
# event or the thread.
|
|
228
|
+
test_outcome_text_is_bounded_without_corrupting_characters() {
|
|
229
|
+
local home event text long
|
|
230
|
+
home=$(make_home outcome-text)
|
|
231
|
+
seed_commitment "$home" pf-text req-text discord main work-text
|
|
232
|
+
|
|
233
|
+
"$EMIT" --home "$home" --obligation pf-text --relation rel-code \
|
|
234
|
+
--source-home main --work-id work-text --generation 1 --outcome pr-merged \
|
|
235
|
+
--deliverable pr_url=https://github.com/example/repo/pull/3 \
|
|
236
|
+
--outcome-text "$(printf 'Shipped\tthe caf\xc3\xa9 fix \xe2\x80\x94 \xf0\x9f\x9a\xa2\nsecond line')" >/dev/null \
|
|
237
|
+
|| fail "emit failed for non-ASCII outcome text"
|
|
238
|
+
event=$(find "$home/state/public-followup/events" -name '*.json' | head -1)
|
|
239
|
+
text=$(jq -r '.public_safe_outcome' "$event") \
|
|
240
|
+
|| fail "the typed event must remain valid JSON with non-ASCII text"
|
|
241
|
+
[ "$text" = 'Shipped the café fix — 🚢 second line' ] \
|
|
242
|
+
|| fail "non-ASCII outcome text was corrupted or not collapsed: '$text'"
|
|
243
|
+
|
|
244
|
+
# A very long sentence is capped by codepoint, so the JSON stays valid.
|
|
245
|
+
rm -f "$event"
|
|
246
|
+
long=$(python3 -c 'print("é" * 5000, end="")')
|
|
247
|
+
"$EMIT" --home "$home" --obligation pf-text --relation rel-code \
|
|
248
|
+
--source-home main --work-id work-text --generation 1 --outcome pr-merged \
|
|
249
|
+
--deliverable pr_url=https://github.com/example/repo/pull/4 \
|
|
250
|
+
--outcome-text "$long" >/dev/null \
|
|
251
|
+
|| fail "emit failed for an over-long outcome text"
|
|
252
|
+
event=$(find "$home/state/public-followup/events" -name '*.json' | head -1)
|
|
253
|
+
text=$(jq -r '.public_safe_outcome' "$event") \
|
|
254
|
+
|| fail "an over-long outcome must still produce valid JSON"
|
|
255
|
+
[ "${#text}" -le 600 ] || fail "the outcome text was not bounded, got ${#text} characters"
|
|
256
|
+
case "$text" in
|
|
257
|
+
*[!é]*) fail "codepoint bounding split a multi-byte character" ;;
|
|
258
|
+
esac
|
|
259
|
+
pass "outcome text is collapsed to one line, bounded by codepoint, and never corrupts characters"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# --- 1. the restart end-to-end -------------------------------------------------
|
|
263
|
+
|
|
264
|
+
# The whole reported failure, start to finish, with no conversation memory
|
|
265
|
+
# anywhere: a Discord request becomes a typed commitment, a secondmate child
|
|
266
|
+
# lands the work and reports a TYPED terminal result, the session ends, and a
|
|
267
|
+
# cold reconciliation from disk delivers exactly one final reply into the
|
|
268
|
+
# original thread and closes the obligation.
|
|
269
|
+
test_restart_e2e_delivers_exactly_once() {
|
|
270
|
+
local home child log out posts receipt
|
|
271
|
+
home=$(make_home restart-e2e)
|
|
272
|
+
child=$(make_home restart-child relay-off)
|
|
273
|
+
log="$home/curl.log"; : > "$log"
|
|
274
|
+
printf '%s\n' fmdev > "$child/.fm-secondmate-home"
|
|
275
|
+
fm_write_meta "$home/state/fmdev.meta" "kind=secondmate" "home=$child"
|
|
276
|
+
seed_commitment "$home" pf-restart req-restart discord secondmate:fmdev work-code-q1
|
|
277
|
+
fm_write_meta "$child/state/work-code-q1.meta" \
|
|
278
|
+
"x_request=req-restart" "x_request_ts=1700000000" "x_followups=1"
|
|
279
|
+
|
|
280
|
+
# The reported failure, reproduced: with the work bound but no reconciled
|
|
281
|
+
# terminal result, the commitment is stranded at pending-work and nothing can
|
|
282
|
+
# be delivered - which is exactly how a promised final reply went unsent.
|
|
283
|
+
[ "$(delivery_state "$home" pf-restart)" = pending-work ] \
|
|
284
|
+
|| fail "a freshly bound commitment must sit at pending-work"
|
|
285
|
+
FAKE_CURL_LOG="$log" expect_failure "a commitment still waiting on its work must not be deliverable" \
|
|
286
|
+
run_pf "$home" deliver pf-restart
|
|
287
|
+
assert_contains "$EXPECT_OUT" "still waiting on its bound work" \
|
|
288
|
+
"the stranded state must be reported, not silently skipped"
|
|
289
|
+
[ "$(followup_posts "$log")" -eq 0 ] || fail "the stranded state must post nothing"
|
|
290
|
+
|
|
291
|
+
# The child home reports its terminal result as typed data. This is the step
|
|
292
|
+
# whose absence left the obligation stranded at pending-work.
|
|
293
|
+
emit_terminal "$home" "$home" pf-restart secondmate:fmdev work-code-q1 >/dev/null \
|
|
294
|
+
|| fail "the child could not report its typed terminal result"
|
|
295
|
+
|
|
296
|
+
# Simulate compaction/restart: nothing but disk survives, and the drained inbox
|
|
297
|
+
# is gone. The durable private request context is what keeps the thread binding
|
|
298
|
+
# resolvable.
|
|
299
|
+
rm -f "$home/state/x-inbox/req-restart.json"
|
|
300
|
+
|
|
301
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" consume) \
|
|
302
|
+
|| fail "cold reconciliation failed"
|
|
303
|
+
assert_contains "$out" "ready pf-restart req-restart discord" \
|
|
304
|
+
"reconciliation must report the commitment as delivery-ready"
|
|
305
|
+
[ "$(delivery_state "$home" pf-restart)" = ready ] \
|
|
306
|
+
|| fail "the typed terminal result must move the commitment to ready"
|
|
307
|
+
[ "$(followup_posts "$log")" -eq 0 ] \
|
|
308
|
+
|| fail "reconciliation must not post anything by itself"
|
|
309
|
+
|
|
310
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-restart) \
|
|
311
|
+
|| fail "delivery failed"
|
|
312
|
+
assert_contains "$out" "delivered pf-restart request=req-restart platform=discord" \
|
|
313
|
+
"delivery must report the original request binding"
|
|
314
|
+
|
|
315
|
+
posts=$(followup_posts "$log")
|
|
316
|
+
[ "$posts" -eq 1 ] || fail "expected exactly one public reply, got $posts"
|
|
317
|
+
assert_grep 'connector/followup' "$log" "the reply must use the follow-up endpoint"
|
|
318
|
+
assert_grep '"request_id":"req-restart"' "$log" \
|
|
319
|
+
"the reply must target the ORIGINAL request binding"
|
|
320
|
+
assert_grep 'workers now land in the launching workspace' "$log" \
|
|
321
|
+
"the reply must reuse the accepted terminal outcome verbatim"
|
|
322
|
+
|
|
323
|
+
receipt=$(tasks_in "$home" public-followup list --json \
|
|
324
|
+
| jq -r '(.public_followups // [])
|
|
325
|
+
| map(select(.id == "pf-restart")) | .[0].public_followup.delivery.receipt.state // "none"')
|
|
326
|
+
[ "$receipt" = posted ] || fail "a validated posted receipt must be recorded, got '$receipt'"
|
|
327
|
+
[ "$(task_state "$home" pf-restart)" = 'done' ] \
|
|
328
|
+
|| fail "the commitment must be Done only after the receipt"
|
|
329
|
+
assert_no_grep '^x_request=' "$child/state/work-code-q1.meta" \
|
|
330
|
+
"typed delivery must clear the secondmate's legacy X link"
|
|
331
|
+
pass "restart end-to-end: typed result reconciles from disk and delivers one reply to the original thread"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# --- 2. idempotency ------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
test_duplicate_event_and_replay_are_noops() {
|
|
337
|
+
local home log first second out posts
|
|
338
|
+
home=$(make_home idempotent)
|
|
339
|
+
log="$home/curl.log"; : > "$log"
|
|
340
|
+
seed_commitment "$home" pf-dup req-dup discord main work-dup
|
|
341
|
+
|
|
342
|
+
first=$(emit_terminal "$home" "$home" pf-dup main work-dup) || fail "first emit failed"
|
|
343
|
+
second=$(emit_terminal "$home" "$home" pf-dup main work-dup) || fail "second emit failed"
|
|
344
|
+
[ "$first" = "$second" ] \
|
|
345
|
+
|| fail "the same terminal result must derive the same event identity"
|
|
346
|
+
[ "$(find "$home/state/public-followup/events" -name '*.json' | wc -l | tr -d ' ')" -eq 1 ] \
|
|
347
|
+
|| fail "a duplicate emit must not create a second event file"
|
|
348
|
+
|
|
349
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "first consume failed"
|
|
350
|
+
# Replay the identical event after the fact, exactly as a restarted child would.
|
|
351
|
+
emit_terminal "$home" "$home" pf-dup main work-dup >/dev/null || fail "replay emit failed"
|
|
352
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" consume) || fail "replay consume failed"
|
|
353
|
+
[ -z "$out" ] || fail "replaying an accepted event must be silent, got: $out"
|
|
354
|
+
[ "$(delivery_state "$home" pf-dup)" = ready ] \
|
|
355
|
+
|| fail "replay must not disturb the delivery state"
|
|
356
|
+
|
|
357
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-dup >/dev/null || fail "delivery failed"
|
|
358
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-dup) || fail "second deliver must succeed silently"
|
|
359
|
+
assert_contains "$out" "already delivered" "a second delivery must report the existing receipt"
|
|
360
|
+
posts=$(followup_posts "$log")
|
|
361
|
+
[ "$posts" -eq 1 ] || fail "a repeated delivery must never double-post, got $posts posts"
|
|
362
|
+
pass "duplicate terminal results, restart replay, and repeated delivery are all no-ops"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# --- 3. refusals ---------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
# Everything tasks-axi is the authority on - source home, work id, generation,
|
|
368
|
+
# schema, and permitted deliverables - must be refused rather than half-applied,
|
|
369
|
+
# and quarantined rather than retried forever.
|
|
370
|
+
test_invalid_events_are_refused_and_quarantined() {
|
|
371
|
+
local home out events rejected
|
|
372
|
+
home=$(make_home refusals)
|
|
373
|
+
seed_commitment "$home" pf-refuse req-refuse discord secondmate:fmdev work-real
|
|
374
|
+
|
|
375
|
+
# Wrong source home and wrong work id are caught at the edge by the emitter,
|
|
376
|
+
# because the owning home's own registration disagrees.
|
|
377
|
+
expect_failure "a wrong source home must be refused" \
|
|
378
|
+
"$EMIT" --home "$home" --obligation pf-refuse --relation rel-code \
|
|
379
|
+
--source-home secondmate:other --work-id work-real --generation 1 \
|
|
380
|
+
--outcome pr-merged --deliverable pr_url=https://example.invalid/1 \
|
|
381
|
+
--outcome-text 'x'
|
|
382
|
+
assert_contains "$EXPECT_OUT" "does not match this home's registration" \
|
|
383
|
+
"the refusal must name the mismatch"
|
|
384
|
+
|
|
385
|
+
expect_failure "a wrong work id must be refused" \
|
|
386
|
+
"$EMIT" --home "$home" --obligation pf-refuse --relation rel-code \
|
|
387
|
+
--source-home secondmate:fmdev --work-id work-other --generation 1 \
|
|
388
|
+
--outcome pr-merged --deliverable pr_url=https://example.invalid/1 \
|
|
389
|
+
--outcome-text 'x'
|
|
390
|
+
expect_failure "a stale generation must be refused" \
|
|
391
|
+
"$EMIT" --home "$home" --obligation pf-refuse --relation rel-code \
|
|
392
|
+
--source-home secondmate:fmdev --work-id work-real --generation 0 \
|
|
393
|
+
--outcome pr-merged --deliverable pr_url=https://example.invalid/1 \
|
|
394
|
+
--outcome-text 'x'
|
|
395
|
+
|
|
396
|
+
events="$home/state/public-followup/events"
|
|
397
|
+
rejected="$home/state/public-followup/rejected"
|
|
398
|
+
|
|
399
|
+
# A malformed event that bypassed the emitter entirely.
|
|
400
|
+
printf 'not json at all\n' > "$events/deadbeef.json"
|
|
401
|
+
out=$(run_pf "$home" consume) || fail "consume must survive a malformed event"
|
|
402
|
+
assert_contains "$out" "rejected deadbeef" "a malformed event must be refused"
|
|
403
|
+
assert_absent "$events/deadbeef.json" "a refused event must leave the pending inbox"
|
|
404
|
+
assert_present "$rejected/deadbeef.reason" "a refusal must keep an inspectable reason"
|
|
405
|
+
|
|
406
|
+
# A deliverable the expected-final type does not permit. The emitter accepts the
|
|
407
|
+
# shape; tasks-axi is the authority that refuses the semantics.
|
|
408
|
+
"$EMIT" --home "$home" --obligation pf-refuse --relation rel-code \
|
|
409
|
+
--source-home secondmate:fmdev --work-id work-real --generation 1 \
|
|
410
|
+
--outcome pr-merged --deliverable report_path=data/x/report.md \
|
|
411
|
+
--outcome-text 'wrong deliverable for a merged PR' >/dev/null \
|
|
412
|
+
|| fail "the emitter should publish a shape-valid event"
|
|
413
|
+
out=$(run_pf "$home" consume) || fail "consume must survive an unsupported deliverable"
|
|
414
|
+
assert_contains "$out" "rejected " "an unsupported deliverable must be refused by tasks-axi"
|
|
415
|
+
[ "$(delivery_state "$home" pf-refuse)" = pending-work ] \
|
|
416
|
+
|| fail "a refused event must leave the commitment untouched"
|
|
417
|
+
|
|
418
|
+
# A hand-edited event whose id no longer matches its own identity fields.
|
|
419
|
+
jq -n '{schema_version:1, event_id:"forged", obligation_id:"pf-refuse",
|
|
420
|
+
relation_id:"rel-code", work_id:"work-real", generation:1,
|
|
421
|
+
source_home_id:"secondmate:fmdev", outcome_type:"pr-merged",
|
|
422
|
+
deliverables:{pr_url:"https://example.invalid/9"},
|
|
423
|
+
public_safe_outcome:"forged", occurred_at:"2026-07-30T12:00:00Z",
|
|
424
|
+
successor:null}' > "$events/forged.json"
|
|
425
|
+
out=$(run_pf "$home" consume) || fail "consume must survive a forged event"
|
|
426
|
+
assert_contains "$out" "rejected forged" "a forged event identity must be refused"
|
|
427
|
+
pass "wrong source, wrong work id, stale generation, malformed, unsupported deliverable, and forged identity are all refused"
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# --- 4. transport failure and late receipt -------------------------------------
|
|
431
|
+
|
|
432
|
+
test_relay_failure_holds_without_false_completion() {
|
|
433
|
+
local home log out posts
|
|
434
|
+
home=$(make_home retryable)
|
|
435
|
+
log="$home/curl.log"; : > "$log"
|
|
436
|
+
seed_commitment "$home" pf-retry req-retry discord main work-retry
|
|
437
|
+
emit_terminal "$home" "$home" pf-retry main work-retry >/dev/null || fail "emit failed"
|
|
438
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
439
|
+
|
|
440
|
+
FAKE_CURL_LOG="$log" FAKE_FOLLOWUP_CODE=500 \
|
|
441
|
+
expect_failure "a failed relay post must not report success" \
|
|
442
|
+
run_pf "$home" deliver pf-retry
|
|
443
|
+
assert_contains "$EXPECT_OUT" "recorded as retryable" "the failure must be typed as retryable"
|
|
444
|
+
[ "$(delivery_state "$home" pf-retry)" = retry-due ] \
|
|
445
|
+
|| fail "a failed post must leave a retryable state, got $(delivery_state "$home" pf-retry)"
|
|
446
|
+
[ "$(task_state "$home" pf-retry)" != 'done' ] \
|
|
447
|
+
|| fail "a failed post must never close the commitment"
|
|
448
|
+
|
|
449
|
+
# The retry succeeds and closes it, with exactly one successful post.
|
|
450
|
+
: > "$log"
|
|
451
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-retry >/dev/null || fail "the retry should deliver"
|
|
452
|
+
posts=$(followup_posts "$log")
|
|
453
|
+
[ "$posts" -eq 1 ] || fail "the retry must post exactly once, got $posts"
|
|
454
|
+
[ "$(task_state "$home" pf-retry)" = 'done' ] || fail "a successful retry must close the commitment"
|
|
455
|
+
pass "a relay transport failure is held as retryable with no false completion, and the retry posts once"
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
test_dry_run_does_not_close_commitment() {
|
|
459
|
+
local home log out posts
|
|
460
|
+
home=$(make_home dry-run)
|
|
461
|
+
log="$home/curl.log"; : > "$log"
|
|
462
|
+
seed_commitment "$home" pf-dry req-dry discord main work-dry
|
|
463
|
+
emit_terminal "$home" "$home" pf-dry main work-dry >/dev/null || fail "emit failed"
|
|
464
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
465
|
+
|
|
466
|
+
FMX_DRY_RUN=1 FAKE_CURL_LOG="$log" expect_failure \
|
|
467
|
+
"a dry-run must not close a public commitment" run_pf "$home" deliver pf-dry
|
|
468
|
+
assert_contains "$EXPECT_OUT" "recorded as retryable" \
|
|
469
|
+
"a dry-run must leave a retryable typed state"
|
|
470
|
+
[ "$(delivery_state "$home" pf-dry)" = retry-due ] \
|
|
471
|
+
|| fail "a dry-run must leave the obligation retryable, got $(delivery_state "$home" pf-dry)"
|
|
472
|
+
[ "$(task_state "$home" pf-dry)" != 'done' ] \
|
|
473
|
+
|| fail "a dry-run must never close the commitment"
|
|
474
|
+
posts=$(followup_posts "$log")
|
|
475
|
+
[ "$posts" -eq 0 ] || fail "a dry-run must not post to the relay, got $posts posts"
|
|
476
|
+
pass "a dry-run records no public delivery and leaves the commitment retryable"
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
test_late_receipt_closes_the_exact_attempt_without_reposting() {
|
|
480
|
+
local home log out posts attempt
|
|
481
|
+
home=$(make_home late-receipt)
|
|
482
|
+
log="$home/curl.log"; : > "$log"
|
|
483
|
+
seed_commitment "$home" pf-late req-late x main work-late
|
|
484
|
+
fm_write_meta "$home/state/work-late.meta" \
|
|
485
|
+
"x_request=req-late" "x_request_ts=1700000000" "x_followups=1"
|
|
486
|
+
emit_terminal "$home" "$home" pf-late main work-late >/dev/null || fail "emit failed"
|
|
487
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
488
|
+
|
|
489
|
+
FAKE_CURL_LOG="$log" FAKE_FOLLOWUP_CODE=503 run_pf "$home" deliver pf-late >/dev/null 2>&1 || true
|
|
490
|
+
attempt=$(tasks_in "$home" public-followup list --json \
|
|
491
|
+
| jq -r '(.public_followups // []) | map(select(.id == "pf-late"))
|
|
492
|
+
| .[0].public_followup.delivery.attempt_count')
|
|
493
|
+
[ "$attempt" = 1 ] || fail "the failed attempt must be recorded as attempt 1, got '$attempt'"
|
|
494
|
+
|
|
495
|
+
expect_failure "a late receipt must include its exact message count" \
|
|
496
|
+
run_pf "$home" record-posted pf-late --attempt 1
|
|
497
|
+
assert_contains "$EXPECT_OUT" "--chunks <n> is required" \
|
|
498
|
+
"a late receipt without a message count must be refused"
|
|
499
|
+
|
|
500
|
+
# The post actually landed; its receipt was simply lost. Close the exact attempt
|
|
501
|
+
# without sending anything else.
|
|
502
|
+
: > "$log"
|
|
503
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" record-posted pf-late --attempt 1 --chunks 1) \
|
|
504
|
+
|| fail "recording a late receipt for the exact attempt must succeed"
|
|
505
|
+
assert_contains "$out" "recorded pf-late attempt=1" "the late receipt must name its attempt"
|
|
506
|
+
posts=$(followup_posts "$log")
|
|
507
|
+
[ "$posts" -eq 0 ] || fail "recording a late receipt must post nothing, got $posts posts"
|
|
508
|
+
[ "$(task_state "$home" pf-late)" = 'done' ] || fail "a validated late receipt must close the commitment"
|
|
509
|
+
assert_no_grep '^x_request=' "$home/state/work-late.meta" \
|
|
510
|
+
"a late receipt must clear the legacy X link"
|
|
511
|
+
|
|
512
|
+
FAKE_CURL_LOG="$log" expect_failure "a receipt for a different attempt must be refused" \
|
|
513
|
+
run_pf "$home" record-posted pf-late --attempt 9 --chunks 1
|
|
514
|
+
pass "a late success receipt closes the exact attempt with no second post, and a mismatched attempt is refused"
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
test_typed_terminal_clear_only_removes_legacy_link() {
|
|
518
|
+
local home meta out
|
|
519
|
+
home=$(make_home typed-clear)
|
|
520
|
+
meta="$home/state/work-clear.meta"
|
|
521
|
+
printf '%s\n' 'status=working' 'x_request=req-clear' 'x_request_ts=1700000000' \
|
|
522
|
+
'x_followups=2' 'x_platform=discord' 'x_reply_max_chars=1900' > "$meta"
|
|
523
|
+
|
|
524
|
+
out=$(PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
525
|
+
FM_STATE_OVERRIDE="$home/state" "$ROOT/bin/fm-x-followup.sh" --clear work-clear) \
|
|
526
|
+
|| fail "the typed terminal clear transition must succeed"
|
|
527
|
+
[ "$out" = work-clear ] || fail "the clear-only transition must identify the task"
|
|
528
|
+
assert_grep 'status=working' "$meta" "clear-only transition must preserve unrelated task metadata"
|
|
529
|
+
assert_no_grep '^x_request=' "$meta" "clear-only transition must remove the request link"
|
|
530
|
+
assert_no_grep '^x_followups=' "$meta" "clear-only transition must remove the follow-up counter"
|
|
531
|
+
assert_no_grep '^x_platform=' "$meta" "clear-only transition must remove platform metadata"
|
|
532
|
+
pass "typed terminal cleanup clears the legacy link without posting"
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# A crash between the post and its receipt is the one case where we cannot know
|
|
536
|
+
# whether the thread already got a reply. Delivery must refuse rather than guess.
|
|
537
|
+
test_interrupted_delivery_refuses_to_repost() {
|
|
538
|
+
local home log out posts
|
|
539
|
+
home=$(make_home interrupted)
|
|
540
|
+
log="$home/curl.log"; : > "$log"
|
|
541
|
+
seed_commitment "$home" pf-crash req-crash discord main work-crash
|
|
542
|
+
emit_terminal "$home" "$home" pf-crash main work-crash >/dev/null || fail "emit failed"
|
|
543
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
544
|
+
|
|
545
|
+
# Reproduce the crash window directly through the state machine.
|
|
546
|
+
tasks_in "$home" public-followup begin-delivery pf-crash \
|
|
547
|
+
--payload-hash 0000000000000000000000000000000000000000000000000000000000000000 >/dev/null \
|
|
548
|
+
|| fail "could not stage the interrupted attempt"
|
|
549
|
+
|
|
550
|
+
FAKE_CURL_LOG="$log" expect_failure "an interrupted delivery must not silently post again" \
|
|
551
|
+
run_pf "$home" deliver pf-crash
|
|
552
|
+
assert_contains "$EXPECT_OUT" "mid-delivery" "the refusal must name the interrupted attempt"
|
|
553
|
+
posts=$(followup_posts "$log")
|
|
554
|
+
[ "$posts" -eq 0 ] || fail "an interrupted delivery must post nothing, got $posts posts"
|
|
555
|
+
pass "a delivery interrupted between post and receipt refuses to repost"
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# --- 5. ownership --------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
# The outward post belongs to the home holding the relay consent and the thread
|
|
561
|
+
# binding. A child home has neither, and must not be able to acquire them.
|
|
562
|
+
test_outward_delivery_stays_with_the_owning_home() {
|
|
563
|
+
local owner child log out
|
|
564
|
+
owner=$(make_home owner)
|
|
565
|
+
child=$(make_home child relay-off)
|
|
566
|
+
log="$owner/curl.log"; : > "$log"
|
|
567
|
+
printf '%s\n' child > "$child/.fm-secondmate-home"
|
|
568
|
+
fm_write_meta "$owner/state/child.meta" "kind=secondmate" "home=$child"
|
|
569
|
+
seed_commitment "$owner" pf-own req-own discord secondmate:child work-child
|
|
570
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
571
|
+
"x_request=req-own" "x_request_ts=1700000000" "x_followups=1"
|
|
572
|
+
|
|
573
|
+
FAKE_CURL_LOG="$log" emit_terminal "$owner" "$owner" pf-own secondmate:child work-child >/dev/null \
|
|
574
|
+
|| fail "the child could not report its typed result"
|
|
575
|
+
[ "$(followup_posts "$log")" -eq 0 ] \
|
|
576
|
+
|| fail "reporting a terminal result must never post publicly"
|
|
577
|
+
run_pf "$owner" consume >/dev/null || fail "the owning home could not consume the child's typed result"
|
|
578
|
+
|
|
579
|
+
# The child home has no commitment of its own and no relay consent, so it can
|
|
580
|
+
# neither deliver nor even see one.
|
|
581
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
582
|
+
FM_STATE_OVERRIDE="$child/state" FAKE_CURL_LOG="$log" \
|
|
583
|
+
expect_failure "a home without relay consent must not deliver a public reply" \
|
|
584
|
+
"$PF" deliver pf-own
|
|
585
|
+
assert_contains "$EXPECT_OUT" "has not opted into the myfirstmate relay" \
|
|
586
|
+
"the refusal must name the missing relay consent"
|
|
587
|
+
[ "$(followup_posts "$log")" -eq 0 ] || fail "the refused delivery must post nothing"
|
|
588
|
+
FAKE_CURL_LOG="$log" run_pf "$owner" deliver pf-own >/dev/null \
|
|
589
|
+
|| fail "the owning home must deliver the typed public reply"
|
|
590
|
+
assert_no_grep '^x_request=' "$child/state/work-child.meta" \
|
|
591
|
+
"typed delivery must clear the child task's legacy X link"
|
|
592
|
+
pass "a child home reports typed results but can never become the outward-post owner"
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
test_delivery_requires_registration_before_posting() {
|
|
596
|
+
local home log out
|
|
597
|
+
home=$(make_home missing-registration)
|
|
598
|
+
log="$home/curl.log"; : > "$log"
|
|
599
|
+
seed_commitment "$home" pf-missing req-missing x main work-missing
|
|
600
|
+
fm_write_meta "$home/state/work-missing.meta" \
|
|
601
|
+
"x_request=req-missing" "x_request_ts=1700000000" "x_followups=1"
|
|
602
|
+
emit_terminal "$home" "$home" pf-missing main work-missing >/dev/null || fail "emit failed"
|
|
603
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
604
|
+
rm -f "$home/state/public-followup/registry/pf-missing"
|
|
605
|
+
|
|
606
|
+
FAKE_CURL_LOG="$log" expect_failure "delivery without a registration must refuse" \
|
|
607
|
+
run_pf "$home" deliver pf-missing
|
|
608
|
+
assert_contains "$EXPECT_OUT" "registration for 'pf-missing' is missing or invalid" \
|
|
609
|
+
"missing registration must be an actionable delivery refusal"
|
|
610
|
+
[ "$(followup_posts "$log")" -eq 0 ] || fail "missing registration must prevent any public post"
|
|
611
|
+
[ "$(task_state "$home" pf-missing)" != 'done' ] \
|
|
612
|
+
|| fail "missing registration must not close the obligation"
|
|
613
|
+
assert_grep 'x_request=req-missing' "$home/state/work-missing.meta" \
|
|
614
|
+
"missing registration must leave the legacy link for reconciliation"
|
|
615
|
+
pass "typed delivery refuses to post when its cleanup registration is missing"
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
test_secondmate_teardown_requires_parent_binding() {
|
|
619
|
+
local parent child registry_before marker_before
|
|
620
|
+
parent=$(make_home teardown-parent)
|
|
621
|
+
child=$(make_home teardown-child)
|
|
622
|
+
printf '%s\n' mate > "$child/.fm-secondmate-home"
|
|
623
|
+
seed_commitment "$parent" pf-teardown req-teardown x secondmate:mate work-child
|
|
624
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
625
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
626
|
+
"window=firstmate:fm-work-child" "endpoint_task_id=work-child" \
|
|
627
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
628
|
+
|
|
629
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
630
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
631
|
+
expect_failure "marked child teardown without a parent must refuse cleanup" \
|
|
632
|
+
"$TEARDOWN" work-child
|
|
633
|
+
assert_contains "$EXPECT_OUT" "cannot resolve the primary home for marked secondmate mate" \
|
|
634
|
+
"missing parent binding must be an actionable teardown refusal"
|
|
635
|
+
assert_present "$child/state/work-child.meta" \
|
|
636
|
+
"missing parent binding must preserve the child work metadata"
|
|
637
|
+
|
|
638
|
+
parent=$(make_home teardown-valid-parent)
|
|
639
|
+
child=$(make_home teardown-valid-child)
|
|
640
|
+
printf '%s\n' mate > "$child/.fm-secondmate-home"
|
|
641
|
+
printf -- '- mate - synthetic (id is legacy); preserve this (home: %s; scope: synthetic (child); semicolon remains meaningful; projects: ; added 2026-07-30)\n' \
|
|
642
|
+
"$child" > "$parent/data/secondmates.md"
|
|
643
|
+
FM_HOME="$parent" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null \
|
|
644
|
+
|| fail "home-seed validation rejected a punctuation-bearing operational registry record"
|
|
645
|
+
registry_before=$(cat "$parent/data/secondmates.md")
|
|
646
|
+
marker_before=$(cat "$child/.fm-secondmate-home")
|
|
647
|
+
seed_commitment "$parent" pf-teardown-valid req-teardown-valid x secondmate:mate work-child
|
|
648
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
649
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
650
|
+
"window=firstmate:fm-work-child" "endpoint_task_id=work-child" \
|
|
651
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
652
|
+
assert_absent "$child/.fm-secondmate-parent" \
|
|
653
|
+
"the legacy env-only binding case must not gain a durable parent record"
|
|
654
|
+
|
|
655
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
656
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
657
|
+
FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$parent" \
|
|
658
|
+
expect_failure "marked child teardown with a valid parent must enforce the parent commitment" \
|
|
659
|
+
"$TEARDOWN" work-child
|
|
660
|
+
assert_contains "$EXPECT_OUT" "still owes a public reply" \
|
|
661
|
+
"valid parent binding must route cleanup through the parent commitment"
|
|
662
|
+
case "$EXPECT_OUT" in
|
|
663
|
+
*"cannot resolve the primary home"*) fail "valid parent binding was reported as unresolved" ;;
|
|
664
|
+
esac
|
|
665
|
+
assert_present "$child/state/work-child.meta" \
|
|
666
|
+
"an owed parent commitment must preserve the child work metadata"
|
|
667
|
+
[ "$registry_before" = "$(cat "$parent/data/secondmates.md")" ] \
|
|
668
|
+
|| fail "guarded cleanup refusal changed the parent registry"
|
|
669
|
+
[ "$marker_before" = "$(cat "$child/.fm-secondmate-home")" ] \
|
|
670
|
+
|| fail "guarded cleanup refusal changed the child identity marker"
|
|
671
|
+
pass "marked secondmate teardown resolves its parent and fails closed when unavailable"
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# The three tests below exercise the durable .fm-secondmate-parent record a real
|
|
675
|
+
# bin/fm-home-seed.sh seed now writes next to .fm-secondmate-home (fm-remote-sm-
|
|
676
|
+
# cleanup-parent-binding-s1 report, section 7). Before this record existed, the
|
|
677
|
+
# marked-child gate above could only ever see the parent through the launch-time
|
|
678
|
+
# FM_PUBLIC_FOLLOWUP_PRIMARY_HOME env var: a restart that dropped that prefix made
|
|
679
|
+
# the guard silently treat an actually-active parent relay as off, which could
|
|
680
|
+
# drop a real public-reply obligation without anyone noticing. Each test drives
|
|
681
|
+
# the real bin/fm-teardown.sh cleanup path against a home real fm-home-seed.sh
|
|
682
|
+
# produced, never a hand-crafted marker.
|
|
683
|
+
|
|
684
|
+
assert_local_secondmate_parent_record() {
|
|
685
|
+
local child=$1 parent=$2
|
|
686
|
+
cmp -s "$child/.fm-secondmate-parent" <(
|
|
687
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=local\nparent_home=%s\n' "$parent"
|
|
688
|
+
) || fail "real secondmate seeding must write the exact durable local parent record"
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
test_local_secondmate_seed_publishes_parent_before_identity() {
|
|
692
|
+
local parent child parent_resolved fakebin entered release manifest_out real_mv seed_pid wait_count
|
|
693
|
+
parent=$(make_home seed-publication-parent relay-off)
|
|
694
|
+
child="$TMP_ROOT/seed-publication-child"
|
|
695
|
+
parent_resolved=$(cd "$parent" && pwd -P)
|
|
696
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/seed-publication-fake")
|
|
697
|
+
entered="$TMP_ROOT/seed-publication-entered"
|
|
698
|
+
release="$TMP_ROOT/seed-publication-release"
|
|
699
|
+
manifest_out="$TMP_ROOT/seed-publication.out"
|
|
700
|
+
real_mv=$(command -v mv)
|
|
701
|
+
cat > "$fakebin/mv" <<'SH'
|
|
702
|
+
#!/usr/bin/env bash
|
|
703
|
+
destination=${!#}
|
|
704
|
+
case "$destination" in
|
|
705
|
+
*/.fm-secondmate-home)
|
|
706
|
+
touch "$FM_TEST_PUBLISH_ENTERED"
|
|
707
|
+
wait_count=0
|
|
708
|
+
while [ ! -f "$FM_TEST_PUBLISH_RELEASE" ]; do
|
|
709
|
+
wait_count=$((wait_count + 1))
|
|
710
|
+
[ "$wait_count" -le 250 ] || exit 97
|
|
711
|
+
sleep 0.02
|
|
712
|
+
done
|
|
713
|
+
;;
|
|
714
|
+
esac
|
|
715
|
+
exec "$FM_TEST_REAL_MV" "$@"
|
|
716
|
+
SH
|
|
717
|
+
chmod +x "$fakebin/mv"
|
|
718
|
+
PATH="$fakebin:$PATH" FM_HOME="$parent" \
|
|
719
|
+
FM_SECONDMATE_CHARTER='Local publication-order regression charter.' \
|
|
720
|
+
FM_TEST_REAL_MV="$real_mv" FM_TEST_PUBLISH_ENTERED="$entered" \
|
|
721
|
+
FM_TEST_PUBLISH_RELEASE="$release" \
|
|
722
|
+
"$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects > "$manifest_out" 2>&1 &
|
|
723
|
+
seed_pid=$!
|
|
724
|
+
wait_count=0
|
|
725
|
+
while [ ! -f "$entered" ]; do
|
|
726
|
+
kill -0 "$seed_pid" 2>/dev/null \
|
|
727
|
+
|| fail "local seeding exited before its identity completion marker: $(cat "$manifest_out")"
|
|
728
|
+
wait_count=$((wait_count + 1))
|
|
729
|
+
[ "$wait_count" -le 250 ] || fail "local seeding never reached its identity completion marker"
|
|
730
|
+
sleep 0.02
|
|
731
|
+
done
|
|
732
|
+
assert_local_secondmate_parent_record "$child" "$parent_resolved"
|
|
733
|
+
assert_absent "$child/.fm-secondmate-home" \
|
|
734
|
+
"the local identity marker must remain absent until durable parent publication completes"
|
|
735
|
+
touch "$release"
|
|
736
|
+
wait "$seed_pid" || fail "local seeding failed after publishing durable parent state: $(cat "$manifest_out")"
|
|
737
|
+
assert_present "$child/.fm-secondmate-home" \
|
|
738
|
+
"local seeding must publish its identity marker as the completion point"
|
|
739
|
+
pass "local seeding publishes durable parent state before its identity marker"
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
test_secondmate_teardown_resolves_parent_from_durable_record_when_env_lost() {
|
|
743
|
+
local parent child parent_resolved
|
|
744
|
+
parent=$(make_home teardown-durable-parent)
|
|
745
|
+
child="$TMP_ROOT/teardown-durable-child"
|
|
746
|
+
FM_SECONDMATE_CHARTER='Durable-record regression charter.' \
|
|
747
|
+
FM_HOME="$parent" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
748
|
+
|| fail "real secondmate seeding failed"
|
|
749
|
+
child=$(cd "$child" && pwd -P)
|
|
750
|
+
parent_resolved=$(cd "$parent" && pwd -P)
|
|
751
|
+
make_fake_curl "$child" >/dev/null
|
|
752
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
753
|
+
|
|
754
|
+
assert_local_secondmate_parent_record "$child" "$parent_resolved"
|
|
755
|
+
|
|
756
|
+
seed_commitment "$parent" pf-durable req-durable x secondmate:mate work-child
|
|
757
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
758
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
759
|
+
"window=firstmate:fm-work-child" "endpoint_task_id=work-child" \
|
|
760
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
761
|
+
|
|
762
|
+
# No FM_PUBLIC_FOLLOWUP_PRIMARY_HOME at all here: a restart of the secondmate
|
|
763
|
+
# agent that drops the launch-time prefix must still find the real parent
|
|
764
|
+
# through the durable record instead of silently treating the relay as off.
|
|
765
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
766
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
767
|
+
expect_failure "teardown with a lost launch binding must still find the real parent" \
|
|
768
|
+
"$TEARDOWN" work-child
|
|
769
|
+
assert_contains "$EXPECT_OUT" "still owes a public reply" \
|
|
770
|
+
"the durable record must resolve to the real parent's owed commitment"
|
|
771
|
+
case "$EXPECT_OUT" in
|
|
772
|
+
*"cannot resolve the primary home"*) fail "the durable local record was not used to resolve the parent" ;;
|
|
773
|
+
esac
|
|
774
|
+
assert_present "$child/state/work-child.meta" \
|
|
775
|
+
"a durably-resolved owed commitment must preserve the child work metadata"
|
|
776
|
+
pass "a lost launch-time parent binding is recovered from the durable local record"
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
test_secondmate_teardown_durable_record_missing_parent_registration_still_refuses() {
|
|
780
|
+
local parent child parent_resolved
|
|
781
|
+
parent=$(make_home teardown-durable-missing-parent relay-off)
|
|
782
|
+
child="$TMP_ROOT/teardown-durable-missing-child"
|
|
783
|
+
FM_SECONDMATE_CHARTER='Durable-record missing-registration regression charter.' \
|
|
784
|
+
FM_HOME="$parent" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
785
|
+
|| fail "real secondmate seeding failed"
|
|
786
|
+
child=$(cd "$child" && pwd -P)
|
|
787
|
+
parent_resolved=$(cd "$parent" && pwd -P)
|
|
788
|
+
make_fake_curl "$child" >/dev/null
|
|
789
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
790
|
+
assert_local_secondmate_parent_record "$child" "$parent_resolved"
|
|
791
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
792
|
+
"window=firstmate:fm-work-child" "endpoint_task_id=work-child" \
|
|
793
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
794
|
+
# No parent/state/mate.meta at all: the parent never recorded this secondmate's
|
|
795
|
+
# own agent, so its side of the binding is genuinely missing. A durable LOCAL
|
|
796
|
+
# record naming the real parent path must not be enough on its own to bypass
|
|
797
|
+
# the check; the real protection this guard exists for must survive the fix.
|
|
798
|
+
|
|
799
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
800
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
801
|
+
expect_failure "a durable local record with no parent-side registration must still refuse" \
|
|
802
|
+
"$TEARDOWN" work-child
|
|
803
|
+
assert_contains "$EXPECT_OUT" "cannot resolve the primary home for marked secondmate mate" \
|
|
804
|
+
"a genuinely missing parent-side registration must remain an actionable teardown refusal"
|
|
805
|
+
assert_present "$child/state/work-child.meta" \
|
|
806
|
+
"a genuinely missing parent binding must preserve the child work metadata"
|
|
807
|
+
pass "a durable local parent record does not bypass a genuinely missing parent-side registration"
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
test_secondmate_teardown_durable_record_with_unknown_field_succeeds() {
|
|
811
|
+
local parent parent_alias child parent_resolved rc out
|
|
812
|
+
parent=$(make_home teardown-durable-clean-parent relay-off)
|
|
813
|
+
child="$TMP_ROOT/teardown-durable-clean-child"
|
|
814
|
+
FM_SECONDMATE_CHARTER='Durable-record clean-cleanup regression charter.' \
|
|
815
|
+
FM_HOME="$parent" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
816
|
+
|| fail "real secondmate seeding failed"
|
|
817
|
+
child=$(cd "$child" && pwd -P)
|
|
818
|
+
parent_resolved=$(cd "$parent" && pwd -P)
|
|
819
|
+
make_fake_curl "$child" >/dev/null
|
|
820
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
821
|
+
assert_local_secondmate_parent_record "$child" "$parent_resolved"
|
|
822
|
+
printf 'some_future_field=value\n' >> "$child/.fm-secondmate-parent"
|
|
823
|
+
parent_alias="$TMP_ROOT/teardown-durable-clean-parent-alias"
|
|
824
|
+
ln -s "$parent" "$parent_alias"
|
|
825
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
826
|
+
fm_git_init_commit "$child/projects/worktree"
|
|
827
|
+
printf 'manual\n' > "$child/config/backlog-backend"
|
|
828
|
+
fm_write_meta "$child/state/work-clean.meta" \
|
|
829
|
+
"window=firstmate:fm-work-clean" "endpoint_task_id=work-clean" \
|
|
830
|
+
"worktree=$child/projects/worktree" "project=$child/projects/worktree" \
|
|
831
|
+
"kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
832
|
+
|
|
833
|
+
rc=0
|
|
834
|
+
out=$(PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
835
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
836
|
+
FM_CONFIG_OVERRIDE="$child/config" FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$parent_alias" \
|
|
837
|
+
"$TEARDOWN" work-clean 2>&1) || rc=$?
|
|
838
|
+
[ "$rc" -eq 0 ] || fail "a resolved parent with no owed commitment must allow cleanup (rc=$rc): $out"
|
|
839
|
+
assert_not_contains "$out" "cannot resolve the primary home" \
|
|
840
|
+
"a real durable-record-backed parent must resolve cleanly"
|
|
841
|
+
pass "unknown durable parent fields remain forward-compatible"
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
test_secondmate_teardown_rejects_conflicting_live_and_durable_parent_bindings() {
|
|
845
|
+
local durable_parent live_parent child parent_resolved
|
|
846
|
+
durable_parent=$(make_home teardown-durable-conflict-recorded relay-off)
|
|
847
|
+
live_parent=$(make_home teardown-durable-conflict-live relay-off)
|
|
848
|
+
child="$TMP_ROOT/teardown-durable-conflict-child"
|
|
849
|
+
FM_SECONDMATE_CHARTER='Durable-record conflict regression charter.' \
|
|
850
|
+
FM_HOME="$durable_parent" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
851
|
+
|| fail "real secondmate seeding failed"
|
|
852
|
+
child=$(cd "$child" && pwd -P)
|
|
853
|
+
parent_resolved=$(cd "$durable_parent" && pwd -P)
|
|
854
|
+
make_fake_curl "$child" >/dev/null
|
|
855
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
856
|
+
assert_local_secondmate_parent_record "$child" "$parent_resolved"
|
|
857
|
+
fm_write_meta "$durable_parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
858
|
+
fm_git_init_commit "$child/projects/worktree"
|
|
859
|
+
printf 'manual\n' > "$child/config/backlog-backend"
|
|
860
|
+
fm_write_meta "$child/state/work-conflict.meta" \
|
|
861
|
+
"window=firstmate:fm-work-conflict" "endpoint_task_id=work-conflict" \
|
|
862
|
+
"worktree=$child/projects/worktree" "project=$child/projects/worktree" \
|
|
863
|
+
"kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
864
|
+
|
|
865
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
866
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
867
|
+
FM_CONFIG_OVERRIDE="$child/config" FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$live_parent" \
|
|
868
|
+
expect_failure "conflicting live and durable parent bindings must refuse cleanup" \
|
|
869
|
+
"$TEARDOWN" work-conflict
|
|
870
|
+
assert_contains "$EXPECT_OUT" "cannot resolve the primary home for marked secondmate mate" \
|
|
871
|
+
"a conflicting live parent must produce the explicit durable-binding refusal"
|
|
872
|
+
assert_present "$child/state/work-conflict.meta" \
|
|
873
|
+
"a conflicting live parent must preserve child work metadata"
|
|
874
|
+
pass "conflicting live and durable parent bindings fail closed"
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
test_secondmate_teardown_rejects_unsafe_durable_parent_records() {
|
|
878
|
+
local case_name parent child parent_record
|
|
879
|
+
for case_name in symlink invalid-route duplicate-route remote-parent-home local-parent-host; do
|
|
880
|
+
parent=$(make_home "teardown-durable-$case_name-parent" relay-off)
|
|
881
|
+
child="$TMP_ROOT/teardown-durable-$case_name-child"
|
|
882
|
+
FM_SECONDMATE_CHARTER='Unsafe durable-record regression charter.' \
|
|
883
|
+
FM_HOME="$parent" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
884
|
+
|| fail "real secondmate seeding failed for $case_name"
|
|
885
|
+
child=$(cd "$child" && pwd -P)
|
|
886
|
+
make_fake_curl "$child" >/dev/null
|
|
887
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
888
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
889
|
+
"window=firstmate:fm-work-child" "endpoint_task_id=work-child" \
|
|
890
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
891
|
+
parent_record="$child/.fm-secondmate-parent"
|
|
892
|
+
case "$case_name" in
|
|
893
|
+
symlink)
|
|
894
|
+
mv "$parent_record" "$parent_record.valid"
|
|
895
|
+
ln -s .fm-secondmate-parent.valid "$parent_record"
|
|
896
|
+
;;
|
|
897
|
+
invalid-route)
|
|
898
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=garbage\n' > "$parent_record"
|
|
899
|
+
;;
|
|
900
|
+
duplicate-route)
|
|
901
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=local\nparent_home=%s\nroute=remote\n' \
|
|
902
|
+
"$parent" > "$parent_record"
|
|
903
|
+
;;
|
|
904
|
+
remote-parent-home)
|
|
905
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=remote\nparent_home=%s\n' \
|
|
906
|
+
"$parent" > "$parent_record"
|
|
907
|
+
;;
|
|
908
|
+
local-parent-host)
|
|
909
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=local\nparent_home=%s\nparent_host=remote-host\n' \
|
|
910
|
+
"$parent" > "$parent_record"
|
|
911
|
+
;;
|
|
912
|
+
esac
|
|
913
|
+
|
|
914
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
915
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
916
|
+
expect_failure "an unsafe $case_name durable parent record must refuse cleanup" \
|
|
917
|
+
"$TEARDOWN" work-child
|
|
918
|
+
assert_contains "$EXPECT_OUT" "cannot resolve the primary home for marked secondmate mate" \
|
|
919
|
+
"an unsafe $case_name durable parent record must produce the explicit binding refusal"
|
|
920
|
+
assert_present "$child/state/work-child.meta" \
|
|
921
|
+
"an unsafe $case_name durable parent record must preserve child work metadata"
|
|
922
|
+
done
|
|
923
|
+
pass "unsafe durable parent records fail closed before cleanup"
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
# A NUL byte inside the durable record must fail closed like every other
|
|
927
|
+
# malformed record. bash's read drops NUL bytes while parsing, and different
|
|
928
|
+
# bash generations disagree on the result (3.2 truncates the value at the NUL,
|
|
929
|
+
# 5.x splices the surrounding bytes together), so a NUL-bearing parent_home can
|
|
930
|
+
# resolve to a home the record's bytes never name contiguously - and teardown's
|
|
931
|
+
# parent reads (registration, registry, relay state) then land in that other
|
|
932
|
+
# home, with the promised-public-reply protection engaging or not depending on
|
|
933
|
+
# which interpreter ran the cleanup. The fixture is deliberately the proven
|
|
934
|
+
# clean-cleanup shape above (registered parent, landed worktree, quiet relay):
|
|
935
|
+
# with the NUL spliced mid-path the record reassembles the real registered
|
|
936
|
+
# parent under a NUL-dropping read, so before the parser rejected NUL this
|
|
937
|
+
# cleanup PROCEEDED - the refusal asserted here is the parser failing closed,
|
|
938
|
+
# not the fixture refusing for some unrelated reason.
|
|
939
|
+
test_secondmate_teardown_rejects_nul_bearing_durable_parent_record() {
|
|
940
|
+
local parent child parent_resolved pre suf record
|
|
941
|
+
parent=$(make_home teardown-durable-nul-parent relay-off)
|
|
942
|
+
child="$TMP_ROOT/teardown-durable-nul-child"
|
|
943
|
+
FM_SECONDMATE_CHARTER='Durable-record NUL regression charter.' \
|
|
944
|
+
FM_HOME="$parent" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
945
|
+
|| fail "real secondmate seeding failed"
|
|
946
|
+
child=$(cd "$child" && pwd -P)
|
|
947
|
+
parent_resolved=$(cd "$parent" && pwd -P)
|
|
948
|
+
make_fake_curl "$child" >/dev/null
|
|
949
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
950
|
+
assert_local_secondmate_parent_record "$child" "$parent_resolved"
|
|
951
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
952
|
+
fm_git_init_commit "$child/projects/worktree"
|
|
953
|
+
printf 'manual\n' > "$child/config/backlog-backend"
|
|
954
|
+
fm_write_meta "$child/state/work-child.meta" \
|
|
955
|
+
"window=firstmate:fm-work-child" "endpoint_task_id=work-child" \
|
|
956
|
+
"worktree=$child/projects/worktree" "project=$child/projects/worktree" \
|
|
957
|
+
"kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
958
|
+
pre=${parent_resolved%??????}
|
|
959
|
+
suf=${parent_resolved#"$pre"}
|
|
960
|
+
record="$child/.fm-secondmate-parent"
|
|
961
|
+
{
|
|
962
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=local\n'
|
|
963
|
+
printf 'parent_home=%s' "$pre"
|
|
964
|
+
printf '\0'
|
|
965
|
+
printf '%s\n' "$suf"
|
|
966
|
+
} > "$record"
|
|
967
|
+
|
|
968
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
969
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
970
|
+
FM_CONFIG_OVERRIDE="$child/config" \
|
|
971
|
+
expect_failure "a NUL-bearing durable parent record must refuse cleanup" \
|
|
972
|
+
"$TEARDOWN" work-child
|
|
973
|
+
assert_contains "$EXPECT_OUT" "cannot resolve the primary home for marked secondmate mate" \
|
|
974
|
+
"a NUL-bearing durable parent record must produce the explicit binding refusal"
|
|
975
|
+
assert_present "$child/state/work-child.meta" \
|
|
976
|
+
"a NUL-bearing durable parent record must preserve child work metadata"
|
|
977
|
+
pass "a NUL-bearing durable parent record fails closed before cleanup"
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
test_relay_disabled_unmarked_teardown_skips_public_path() {
|
|
981
|
+
local home tasks_log out rc
|
|
982
|
+
home=$(make_home teardown-disabled-unmarked relay-off)
|
|
983
|
+
fm_git_init_commit "$home/projects/worktree"
|
|
984
|
+
tasks_log="$home/tasks-axi.log"; : > "$tasks_log"
|
|
985
|
+
printf 'manual\n' > "$home/config/backlog-backend"
|
|
986
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
987
|
+
#!/usr/bin/env bash
|
|
988
|
+
echo "$*" >> "$FAKE_TASKS_AXI_LOG"
|
|
989
|
+
exit 99
|
|
990
|
+
SH
|
|
991
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
992
|
+
fm_write_meta "$home/state/work-disabled.meta" \
|
|
993
|
+
"window=firstmate:fm-work-disabled" "endpoint_task_id=work-disabled" \
|
|
994
|
+
"worktree=$home/projects/worktree" "project=$home/projects/worktree" \
|
|
995
|
+
"kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
996
|
+
|
|
997
|
+
rc=0
|
|
998
|
+
out=$(PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
999
|
+
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
|
|
1000
|
+
FM_CONFIG_OVERRIDE="$home/config" FAKE_TASKS_AXI_LOG="$tasks_log" \
|
|
1001
|
+
"$TEARDOWN" work-disabled 2>&1) || rc=$?
|
|
1002
|
+
[ "$rc" -eq 0 ] || fail "relay-disabled unmarked teardown must not refuse public-followup cleanup (rc=$rc): $out"
|
|
1003
|
+
[ ! -s "$tasks_log" ] || fail "relay-disabled unmarked teardown must not invoke tasks-axi: $(tr '\n' ';' < "$tasks_log")"
|
|
1004
|
+
assert_not_contains "$out" "still owes a public reply" \
|
|
1005
|
+
"relay-disabled unmarked teardown must not run the public commitment guard"
|
|
1006
|
+
assert_absent "$home/state/public-followup" \
|
|
1007
|
+
"relay-disabled unmarked teardown must not create a public-followup artifact"
|
|
1008
|
+
pass "relay-disabled unmarked teardown runs no public-followup work"
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
test_relay_disabled_parent_allows_marked_child_teardown() {
|
|
1012
|
+
local parent child tasks_log out rc
|
|
1013
|
+
parent=$(make_home teardown-disabled-parent relay-off)
|
|
1014
|
+
child=$(make_home teardown-disabled-child relay-off)
|
|
1015
|
+
fm_git_init_commit "$child/projects/worktree"
|
|
1016
|
+
printf '%s\n' disabled-mate > "$child/.fm-secondmate-home"
|
|
1017
|
+
printf -- '- disabled-mate - synthetic (home: %s; scope: synthetic; projects: ; added 2026-07-30)\n' \
|
|
1018
|
+
"$child" > "$parent/data/secondmates.md"
|
|
1019
|
+
fm_write_meta "$parent/state/disabled-mate.meta" "kind=secondmate" "home=$child"
|
|
1020
|
+
tasks_log="$child/tasks-axi.log"; : > "$tasks_log"
|
|
1021
|
+
printf 'manual\n' > "$child/config/backlog-backend"
|
|
1022
|
+
cat > "$child/fakebin/tasks-axi" <<'SH'
|
|
1023
|
+
#!/usr/bin/env bash
|
|
1024
|
+
echo "$*" >> "$FAKE_TASKS_AXI_LOG"
|
|
1025
|
+
exit 99
|
|
1026
|
+
SH
|
|
1027
|
+
chmod +x "$child/fakebin/tasks-axi"
|
|
1028
|
+
fm_write_meta "$child/state/work-disabled.meta" \
|
|
1029
|
+
"window=firstmate:fm-work-disabled" "endpoint_task_id=work-disabled" \
|
|
1030
|
+
"worktree=$child/projects/worktree" "project=$child/projects/worktree" \
|
|
1031
|
+
"kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
1032
|
+
|
|
1033
|
+
rc=0
|
|
1034
|
+
out=$(PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
1035
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
1036
|
+
FM_CONFIG_OVERRIDE="$child/config" \
|
|
1037
|
+
FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$parent" FAKE_TASKS_AXI_LOG="$tasks_log" \
|
|
1038
|
+
"$TEARDOWN" work-disabled 2>&1) || rc=$?
|
|
1039
|
+
[ "$rc" -eq 0 ] || fail "relay-disabled parent must allow marked-child teardown (rc=$rc): $out"
|
|
1040
|
+
[ ! -s "$tasks_log" ] || fail "relay-disabled parent must not invoke tasks-axi for a marked child"
|
|
1041
|
+
assert_not_contains "$out" "still owes a public reply" \
|
|
1042
|
+
"relay-disabled parent must not run the public commitment guard"
|
|
1043
|
+
assert_absent "$child/state/public-followup" \
|
|
1044
|
+
"relay-disabled parent must not create a public-followup artifact"
|
|
1045
|
+
pass "a marked child proceeds without tasks-axi when its parent relay is disabled"
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
test_secondmate_parent_binding_matches_literal_id() {
|
|
1049
|
+
local parent child
|
|
1050
|
+
parent=$(make_home teardown-literal-parent)
|
|
1051
|
+
child=$(make_home teardown-literal-child)
|
|
1052
|
+
printf '%s\n' 'mate.id' > "$child/.fm-secondmate-home"
|
|
1053
|
+
printf -- '- mateXid - synthetic (home: %s; scope: synthetic; projects: ; added 2026-07-30)\n' \
|
|
1054
|
+
"$child" > "$parent/data/secondmates.md"
|
|
1055
|
+
seed_commitment "$parent" pf-teardown-literal req-teardown-literal x secondmate:mate.id work-literal
|
|
1056
|
+
fm_write_meta "$parent/state/mate.id.meta" "kind=secondmate" "home=$child"
|
|
1057
|
+
fm_write_meta "$child/state/work-literal.meta" \
|
|
1058
|
+
"window=firstmate:fm-work-literal" "endpoint_task_id=work-literal" \
|
|
1059
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
1060
|
+
|
|
1061
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
1062
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
1063
|
+
FM_CONFIG_OVERRIDE="$child/config" FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$parent" \
|
|
1064
|
+
expect_failure "a near-match registry id must not satisfy a dotted parent binding" \
|
|
1065
|
+
"$TEARDOWN" work-literal
|
|
1066
|
+
assert_contains "$EXPECT_OUT" "cannot resolve the primary home for marked secondmate mate.id" \
|
|
1067
|
+
"a dotted id must be matched as an exact registry field"
|
|
1068
|
+
assert_present "$child/state/work-literal.meta" \
|
|
1069
|
+
"a near-match parent binding must preserve the child work metadata"
|
|
1070
|
+
pass "secondmate parent resolution matches the durable registry id literally"
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
test_traversal_registration_is_refused_before_delivery() {
|
|
1074
|
+
local home log out
|
|
1075
|
+
home=$(make_home traversal-registration)
|
|
1076
|
+
log="$home/curl.log"; : > "$log"
|
|
1077
|
+
seed_commitment "$home" pf-traversal req-traversal x main work-traversal
|
|
1078
|
+
emit_terminal "$home" "$home" pf-traversal main work-traversal >/dev/null \
|
|
1079
|
+
|| fail "emit failed for traversal registration"
|
|
1080
|
+
sed -i.bak 's/^work_home=.*/work_home=secondmate:..\/..\/x/' \
|
|
1081
|
+
"$home/state/public-followup/registry/pf-traversal"
|
|
1082
|
+
rm -f "$home/state/public-followup/registry/pf-traversal.bak"
|
|
1083
|
+
run_pf "$home" consume >/dev/null || fail "consume failed for traversal registration"
|
|
1084
|
+
|
|
1085
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-traversal 2>&1) && \
|
|
1086
|
+
fail "a traversal-shaped registration must not be deliverable"
|
|
1087
|
+
assert_contains "$out" "registration for 'pf-traversal' is missing or invalid" \
|
|
1088
|
+
"a traversal-shaped work home must be rejected before delivery"
|
|
1089
|
+
[ "$(followup_posts "$log")" -eq 0 ] || fail "an invalid work home must not post publicly"
|
|
1090
|
+
assert_present "$home/state/public-followup/registry/pf-traversal" \
|
|
1091
|
+
"an invalid work home must retain its registration for reconciliation"
|
|
1092
|
+
[ "$(task_state "$home" pf-traversal)" != 'done' ] \
|
|
1093
|
+
|| fail "an invalid work home must not close the obligation"
|
|
1094
|
+
pass "traversal-shaped registrations are rejected before path construction or posting"
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
test_pending_rejects_malformed_listing() {
|
|
1098
|
+
local home out
|
|
1099
|
+
home=$(make_home pending-malformed)
|
|
1100
|
+
seed_commitment "$home" pf-malformed req-malformed discord main work-malformed
|
|
1101
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
1102
|
+
#!/usr/bin/env bash
|
|
1103
|
+
printf '%s' '{"public_followups":['
|
|
1104
|
+
SH
|
|
1105
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
1106
|
+
|
|
1107
|
+
out=$(run_pf "$home" pending) || fail "pending must survive malformed tasks-axi output"
|
|
1108
|
+
assert_contains "$out" "cannot read this home's public commitments through tasks-axi" \
|
|
1109
|
+
"malformed backlog output must use the loud fallback"
|
|
1110
|
+
assert_present "$home/state/public-followup/registry/pf-malformed" \
|
|
1111
|
+
"malformed backlog output must retain the registration"
|
|
1112
|
+
pass "pending keeps registrations when tasks-axi returns malformed JSON"
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
test_private_context_survives_inbox_cleanup() {
|
|
1116
|
+
local home log posts
|
|
1117
|
+
home=$(make_home context-retention)
|
|
1118
|
+
log="$home/curl.log"; : > "$log"
|
|
1119
|
+
seed_commitment "$home" pf-ctx req-ctx discord main work-ctx
|
|
1120
|
+
emit_terminal "$home" "$home" pf-ctx main work-ctx >/dev/null || fail "emit failed"
|
|
1121
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1122
|
+
|
|
1123
|
+
# Drain the inbox exactly as answering the original mention does, and make any
|
|
1124
|
+
# relay fallback fail, so only the retained private context can resolve the
|
|
1125
|
+
# thread's platform and size budget.
|
|
1126
|
+
rm -f "$home/state/x-inbox/req-ctx.json"
|
|
1127
|
+
assert_present "$home/state/x-context/req-ctx.json" \
|
|
1128
|
+
"the private request context must outlive the inbox payload"
|
|
1129
|
+
|
|
1130
|
+
FAKE_CURL_LOG="$log" FAKE_REQCTX_CODE=500 run_pf "$home" deliver pf-ctx >/dev/null \
|
|
1131
|
+
|| fail "delivery must still resolve the thread from retained private context"
|
|
1132
|
+
posts=$(followup_posts "$log")
|
|
1133
|
+
[ "$posts" -eq 1 ] || fail "expected one reply after inbox cleanup, got $posts"
|
|
1134
|
+
assert_grep '"request_id":"req-ctx"' "$log" "the reply must still target the original thread"
|
|
1135
|
+
pass "the retained private request context keeps the original thread deliverable after inbox cleanup"
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
# --- 6. completion semantics ---------------------------------------------------
|
|
1139
|
+
|
|
1140
|
+
test_cleanup_refuses_while_a_public_reply_is_owed() {
|
|
1141
|
+
local home rc
|
|
1142
|
+
home=$(make_home cleanup-guard)
|
|
1143
|
+
seed_commitment "$home" pf-guard req-guard discord main ship-task
|
|
1144
|
+
tasks_in "$home" add ship-task "ship guarded by its public follow-up" --kind ship >/dev/null \
|
|
1145
|
+
|| fail "could not add the guarded ship to its home's backlog"
|
|
1146
|
+
tasks_in "$home" start ship-task >/dev/null \
|
|
1147
|
+
|| fail "could not mark the guarded ship In flight"
|
|
1148
|
+
fm_write_meta "$home/state/ship-task.meta" \
|
|
1149
|
+
"window=firstmate:fm-ship-task" \
|
|
1150
|
+
"worktree=$home/projects/gone" \
|
|
1151
|
+
"project=$home/projects/sample" \
|
|
1152
|
+
"harness=codex" \
|
|
1153
|
+
"kind=ship" \
|
|
1154
|
+
"mode=no-mistakes" \
|
|
1155
|
+
"spawn_gen=public-followup-guard"
|
|
1156
|
+
|
|
1157
|
+
rc=0
|
|
1158
|
+
PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
1159
|
+
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
|
|
1160
|
+
FM_CONFIG_OVERRIDE="$home/config" "$TEARDOWN" ship-task \
|
|
1161
|
+
> "$home/teardown.out" 2> "$home/teardown.err" || rc=$?
|
|
1162
|
+
[ "$rc" -ne 0 ] || fail "cleanup must refuse while a public reply is still owed"
|
|
1163
|
+
assert_grep "still owes a public reply" "$home/teardown.err" "the refusal must be explicit"
|
|
1164
|
+
assert_present "$home/state/ship-task.meta" "a refused cleanup must preserve the task record"
|
|
1165
|
+
|
|
1166
|
+
# Once the reply has landed, the same cleanup is allowed to proceed.
|
|
1167
|
+
emit_terminal "$home" "$home" pf-guard main ship-task >/dev/null || fail "emit failed"
|
|
1168
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1169
|
+
FAKE_CURL_LOG="$home/curl.log" run_pf "$home" deliver pf-guard >/dev/null || fail "delivery failed"
|
|
1170
|
+
rc=0
|
|
1171
|
+
PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
1172
|
+
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
|
|
1173
|
+
FM_CONFIG_OVERRIDE="$home/config" "$TEARDOWN" ship-task >/dev/null 2>&1 || rc=$?
|
|
1174
|
+
[ "$rc" -eq 0 ] || fail "cleanup must proceed once the public reply has landed (rc=$rc)"
|
|
1175
|
+
pass "cleanup refuses while a public reply is owed and proceeds once it has landed"
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
# --- 7. zero overhead for homes that do not use the relay ----------------------
|
|
1179
|
+
|
|
1180
|
+
# The hard acceptance criterion. A home that never opted into the myfirstmate
|
|
1181
|
+
# relay must see no process, no tasks-axi call, no scan, no output, and no file.
|
|
1182
|
+
test_relay_disabled_home_pays_nothing() {
|
|
1183
|
+
local home tasks_log out rc before after cmd
|
|
1184
|
+
home=$(make_home relay-disabled relay-off)
|
|
1185
|
+
tasks_log="$home/tasks-axi.log"; : > "$tasks_log"
|
|
1186
|
+
# Any tasks-axi invocation at all is a failure here, so make it loud.
|
|
1187
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
1188
|
+
#!/usr/bin/env bash
|
|
1189
|
+
echo "$*" >> "$FAKE_TASKS_AXI_LOG"
|
|
1190
|
+
exit 0
|
|
1191
|
+
SH
|
|
1192
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
1193
|
+
|
|
1194
|
+
before=$(find "$home/state" | LC_ALL=C sort)
|
|
1195
|
+
for cmd in "consume" "pending" "guard-work main any-task" "retire anything"; do
|
|
1196
|
+
rc=0
|
|
1197
|
+
# shellcheck disable=SC2086 # each cmd is a deliberate argument list
|
|
1198
|
+
out=$(PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
1199
|
+
FM_STATE_OVERRIDE="$home/state" FAKE_TASKS_AXI_LOG="$tasks_log" "$PF" $cmd 2>&1) || rc=$?
|
|
1200
|
+
[ "$rc" -eq 0 ] || fail "'$cmd' must be a silent success in a relay-disabled home (rc=$rc)"
|
|
1201
|
+
[ -z "$out" ] || fail "'$cmd' must print nothing in a relay-disabled home, got: $out"
|
|
1202
|
+
done
|
|
1203
|
+
|
|
1204
|
+
# New fields and the open-loop gate must not create work in a disabled home.
|
|
1205
|
+
# shellcheck disable=SC1091
|
|
1206
|
+
. "$ROOT/bin/fm-public-followup-lib.sh"
|
|
1207
|
+
fm_pf_has_open_loops "$home/state" \
|
|
1208
|
+
&& fail "a relay-disabled home must not grow an open-loop registry"
|
|
1209
|
+
fm_pf_has_delivered_open_loops "$home/state" \
|
|
1210
|
+
&& fail "a relay-disabled home must not grow a delivered open-loop registry"
|
|
1211
|
+
|
|
1212
|
+
rc=0
|
|
1213
|
+
PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
1214
|
+
FM_STATE_OVERRIDE="$home/state" FAKE_TASKS_AXI_LOG="$tasks_log" "$PF" active || rc=$?
|
|
1215
|
+
[ "$rc" -eq 1 ] || fail "'active' must report inactive in a relay-disabled home"
|
|
1216
|
+
|
|
1217
|
+
[ ! -s "$tasks_log" ] \
|
|
1218
|
+
|| fail "a relay-disabled home must never invoke tasks-axi: $(cat "$tasks_log")"
|
|
1219
|
+
after=$(find "$home/state" | LC_ALL=C sort)
|
|
1220
|
+
[ "$before" = "$after" ] \
|
|
1221
|
+
|| fail "a relay-disabled home must gain no public-followup artifact"
|
|
1222
|
+
assert_absent "$home/state/public-followup" \
|
|
1223
|
+
"a relay-disabled home must never get a public-followup directory"
|
|
1224
|
+
|
|
1225
|
+
# A child cannot force artifacts into a home that never opted in either.
|
|
1226
|
+
rc=0
|
|
1227
|
+
out=$("$EMIT" --home "$home" --obligation pf-x --relation rel-code \
|
|
1228
|
+
--source-home main --work-id w --generation 1 --outcome pr-merged \
|
|
1229
|
+
--outcome-text 'x' 2>&1) || rc=$?
|
|
1230
|
+
[ "$rc" -eq 0 ] || fail "emitting into a relay-disabled home must be a silent no-op (rc=$rc)"
|
|
1231
|
+
[ -z "$out" ] || fail "emitting into a relay-disabled home must produce no output: $out"
|
|
1232
|
+
assert_absent "$home/state/public-followup" \
|
|
1233
|
+
"a refused emit must not create a public-followup directory"
|
|
1234
|
+
pass "a relay-disabled home runs no tasks-axi call, prints nothing, and gains no artifact"
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
# An opted-in home that has never made a public commitment must not start paying
|
|
1238
|
+
# either: the second gate is a directory presence check, not a backlog scan.
|
|
1239
|
+
test_relay_enabled_empty_state_makes_no_calls() {
|
|
1240
|
+
local home tasks_log out rc cmd
|
|
1241
|
+
home=$(make_home relay-enabled-empty)
|
|
1242
|
+
tasks_log="$home/tasks-axi.log"; : > "$tasks_log"
|
|
1243
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
1244
|
+
#!/usr/bin/env bash
|
|
1245
|
+
echo "$*" >> "$FAKE_TASKS_AXI_LOG"
|
|
1246
|
+
exit 0
|
|
1247
|
+
SH
|
|
1248
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
1249
|
+
|
|
1250
|
+
for cmd in "consume" "pending" "guard-work main any-task"; do
|
|
1251
|
+
rc=0
|
|
1252
|
+
# shellcheck disable=SC2086 # each cmd is a deliberate argument list
|
|
1253
|
+
out=$(PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
1254
|
+
FM_STATE_OVERRIDE="$home/state" FAKE_TASKS_AXI_LOG="$tasks_log" "$PF" $cmd 2>&1) || rc=$?
|
|
1255
|
+
[ "$rc" -eq 0 ] || fail "'$cmd' must be a silent success with no commitments (rc=$rc)"
|
|
1256
|
+
[ -z "$out" ] || fail "'$cmd' must print nothing with no commitments, got: $out"
|
|
1257
|
+
done
|
|
1258
|
+
[ ! -s "$tasks_log" ] \
|
|
1259
|
+
|| fail "an empty relay home must not query the backlog: $(cat "$tasks_log")"
|
|
1260
|
+
pass "a relay-enabled home with no commitments makes no backlog call and stays silent"
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
# The relay's own refusal of an exhausted follow-up binding is a captain
|
|
1264
|
+
# decision, not something to retry into a public thread.
|
|
1265
|
+
test_exhausted_binding_is_not_retried() {
|
|
1266
|
+
local home log out posts
|
|
1267
|
+
home=$(make_home exhausted)
|
|
1268
|
+
log="$home/curl.log"; : > "$log"
|
|
1269
|
+
seed_commitment "$home" pf-gone req-gone x main work-gone
|
|
1270
|
+
emit_terminal "$home" "$home" pf-gone main work-gone >/dev/null || fail "emit failed"
|
|
1271
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1272
|
+
|
|
1273
|
+
FAKE_CURL_LOG="$log" FAKE_FOLLOWUP_CODE=409 \
|
|
1274
|
+
expect_failure "an exhausted binding must not be reported as delivered" \
|
|
1275
|
+
run_pf "$home" deliver pf-gone
|
|
1276
|
+
assert_contains "$EXPECT_OUT" "captain decision" "an exhausted binding must be escalated, not retried"
|
|
1277
|
+
[ "$(delivery_state "$home" pf-gone)" = expired-action-required ] \
|
|
1278
|
+
|| fail "an exhausted binding must be recorded as needing action, got $(delivery_state "$home" pf-gone)"
|
|
1279
|
+
[ "$(task_state "$home" pf-gone)" != 'done' ] \
|
|
1280
|
+
|| fail "an exhausted binding must never close the commitment"
|
|
1281
|
+
posts=$(followup_posts "$log")
|
|
1282
|
+
[ "$posts" -eq 1 ] || fail "the refused attempt is one relay call, got $posts"
|
|
1283
|
+
pass "a relay-exhausted follow-up binding is escalated rather than retried into the thread"
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
# The relay poll is the only thing that runs on a cadence in an opted-in home, so
|
|
1287
|
+
# it must stay a hard no-op without a token, and must not start scanning when a
|
|
1288
|
+
# relay-enabled home has no public commitments at all.
|
|
1289
|
+
test_relay_poll_stays_inert_and_surfaces_once() {
|
|
1290
|
+
local off on out first second
|
|
1291
|
+
off=$(make_home poll-off relay-off)
|
|
1292
|
+
out=$(PATH="$off/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$off" \
|
|
1293
|
+
FM_STATE_OVERRIDE="$off/state" "$POLL" 2>&1)
|
|
1294
|
+
[ -z "$out" ] || fail "the relay poll must stay silent without a token, got: $out"
|
|
1295
|
+
assert_absent "$off/state/public-followup" "an inert poll must create nothing"
|
|
1296
|
+
|
|
1297
|
+
on=$(make_home poll-on)
|
|
1298
|
+
out=$(PATH="$on/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$on" \
|
|
1299
|
+
FM_STATE_OVERRIDE="$on/state" "$POLL" 2>&1)
|
|
1300
|
+
assert_not_contains "$out" "public-followup" \
|
|
1301
|
+
"a relay home with no public commitments must not mention public follow-ups"
|
|
1302
|
+
|
|
1303
|
+
seed_commitment "$on" pf-poll req-poll discord main work-poll
|
|
1304
|
+
out=$(PATH="$on/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$on" \
|
|
1305
|
+
FM_STATE_OVERRIDE="$on/state" "$POLL" 2>&1)
|
|
1306
|
+
assert_not_contains "$out" "public-followup" \
|
|
1307
|
+
"a registered commitment with no terminal result yet must not wake the poll"
|
|
1308
|
+
|
|
1309
|
+
emit_terminal "$on" "$on" pf-poll main work-poll >/dev/null || fail "emit failed"
|
|
1310
|
+
first=$(PATH="$on/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$on" \
|
|
1311
|
+
FM_STATE_OVERRIDE="$on/state" "$POLL" 2>&1)
|
|
1312
|
+
assert_contains "$first" "public-followup terminal results are waiting" \
|
|
1313
|
+
"a new terminal result must surface through the existing relay poll"
|
|
1314
|
+
second=$(PATH="$on/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$on" \
|
|
1315
|
+
FM_STATE_OVERRIDE="$on/state" "$POLL" 2>&1)
|
|
1316
|
+
assert_not_contains "$second" "public-followup" \
|
|
1317
|
+
"an unchanged pending set must not wake firstmate again every cycle"
|
|
1318
|
+
pass "the relay poll stays inert without a token, silent with no commitments, and surfaces a new result once"
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
# --- 8. startup surfacing ------------------------------------------------------
|
|
1322
|
+
|
|
1323
|
+
test_session_start_surfaces_only_when_owed() {
|
|
1324
|
+
local off on out
|
|
1325
|
+
off=$(make_home startup-off relay-off)
|
|
1326
|
+
out=$(PATH="$off/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$off" \
|
|
1327
|
+
FM_STATE_OVERRIDE="$off/state" FM_DATA_OVERRIDE="$off/data" \
|
|
1328
|
+
FM_CONFIG_OVERRIDE="$off/config" "$SESSION_START" 2>&1)
|
|
1329
|
+
assert_not_contains "$out" "Public commitments" \
|
|
1330
|
+
"a relay-disabled home must not gain a public-commitments section at startup"
|
|
1331
|
+
|
|
1332
|
+
on=$(make_home startup-on)
|
|
1333
|
+
seed_commitment "$on" pf-start req-start discord main work-start
|
|
1334
|
+
out=$(PATH="$on/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$on" \
|
|
1335
|
+
FM_STATE_OVERRIDE="$on/state" FM_DATA_OVERRIDE="$on/data" \
|
|
1336
|
+
FM_CONFIG_OVERRIDE="$on/config" "$SESSION_START" 2>&1)
|
|
1337
|
+
assert_contains "$out" "Public commitments" \
|
|
1338
|
+
"an unresolved commitment must be surfaced at startup"
|
|
1339
|
+
assert_contains "$out" "unresolved pf-start state=pending-work platform=discord" \
|
|
1340
|
+
"the startup summary must be typed and actionable"
|
|
1341
|
+
assert_contains "$out" "fix worker placement when two spaces share a name" \
|
|
1342
|
+
"the startup summary must carry the public-safe summary"
|
|
1343
|
+
assert_not_contains "$out" "please fix worker placement" \
|
|
1344
|
+
"the startup summary must not carry raw request text"
|
|
1345
|
+
pass "startup surfaces unresolved public commitments only in a relay home that owes one"
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
# --- 9. typed records stay public-safe ----------------------------------------
|
|
1349
|
+
|
|
1350
|
+
test_typed_records_exclude_raw_public_material() {
|
|
1351
|
+
local home backlog event
|
|
1352
|
+
home=$(make_home privacy)
|
|
1353
|
+
seed_commitment "$home" pf-priv req-priv discord main work-priv
|
|
1354
|
+
emit_terminal "$home" "$home" pf-priv main work-priv >/dev/null || fail "emit failed"
|
|
1355
|
+
event=$(find "$home/state/public-followup/events" -name '*.json' | head -1)
|
|
1356
|
+
assert_no_grep 'please fix worker placement' "$event" \
|
|
1357
|
+
"a terminal event must not carry raw request text"
|
|
1358
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1359
|
+
|
|
1360
|
+
backlog="$home/data/backlog.md"
|
|
1361
|
+
assert_no_grep 'please fix worker placement' "$backlog" \
|
|
1362
|
+
"the backlog must never carry raw public message text"
|
|
1363
|
+
# The typed record is base64url canonical JSON, so check the decoded payload too.
|
|
1364
|
+
tasks_in "$home" public-followup list --json > "$home/typed.json"
|
|
1365
|
+
assert_no_grep 'please fix worker placement' "$home/typed.json" \
|
|
1366
|
+
"the typed obligation must never carry raw public message text"
|
|
1367
|
+
pass "typed public-followup records carry only public-safe summaries and deliverables"
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
# --- 10. delivery does not close the public loop --------------------------------
|
|
1371
|
+
|
|
1372
|
+
test_dropped_baton_now_surfaces_open_loop() {
|
|
1373
|
+
local parent child log
|
|
1374
|
+
parent=$(make_home baton-parent)
|
|
1375
|
+
child="$TMP_ROOT/baton-child"
|
|
1376
|
+
FM_SECONDMATE_CHARTER='Baton repro charter.' FM_HOME="$parent" \
|
|
1377
|
+
"$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null || fail "seed failed"
|
|
1378
|
+
child=$(cd "$child" && pwd -P)
|
|
1379
|
+
make_fake_curl "$child" >/dev/null
|
|
1380
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
1381
|
+
log="$TMP_ROOT/curl.log"; : > "$log"
|
|
1382
|
+
|
|
1383
|
+
seed_repro_commitment "$parent" public-final-pi-rearm-repro req-pirearm \
|
|
1384
|
+
secondmate:mate pi-rearm-loop-repro-s1
|
|
1385
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
1386
|
+
|
|
1387
|
+
"$EMIT" --home "$parent" --obligation public-final-pi-rearm-repro --relation rel-code \
|
|
1388
|
+
--source-home secondmate:mate --work-id pi-rearm-loop-repro-s1 --generation 1 \
|
|
1389
|
+
--outcome report-ready --deliverable report_path=data/pi-rearm-loop-repro-s1/report.md \
|
|
1390
|
+
--outcome-text 'Reproduced the loop. A bounded fix is scoped and waiting on you, captain.' >/dev/null \
|
|
1391
|
+
|| fail "emit failed"
|
|
1392
|
+
FAKE_CURL_LOG="$log" run_pf "$parent" consume | grep -q '^ready ' || fail "consume not ready"
|
|
1393
|
+
FAKE_CURL_LOG="$log" run_pf "$parent" deliver public-final-pi-rearm-repro >/dev/null || fail "deliver failed"
|
|
1394
|
+
[ "$(followup_posts "$log")" = 1 ] || fail "expected exactly one closing post"
|
|
1395
|
+
assert_present "$parent/state/public-followup/registry/public-final-pi-rearm-repro" \
|
|
1396
|
+
"delivery must retain the registration"
|
|
1397
|
+
|
|
1398
|
+
fm_write_meta "$child/state/pi-rearm-loop-fix-r1.meta" \
|
|
1399
|
+
"window=firstmate:fm-pi-rearm-loop-fix-r1" "endpoint_task_id=pi-rearm-loop-fix-r1" \
|
|
1400
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
1401
|
+
|
|
1402
|
+
PATH="$parent/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$parent" \
|
|
1403
|
+
FM_STATE_OVERRIDE="$parent/state" "$PF" guard-work secondmate:mate pi-rearm-loop-fix-r1 \
|
|
1404
|
+
|| fail "guard-work unexpectedly blocked the unregistered follow-on"
|
|
1405
|
+
|
|
1406
|
+
run_pf "$parent" pending > "$TMP_ROOT/pending.out"
|
|
1407
|
+
grep -q '^open-loop public-final-pi-rearm-repro ' "$TMP_ROOT/pending.out" \
|
|
1408
|
+
|| fail "pending must print the open-loop line after delivery"
|
|
1409
|
+
grep -q 'request=req-pirearm' "$TMP_ROOT/pending.out" \
|
|
1410
|
+
|| fail "the open-loop line must name the original request"
|
|
1411
|
+
|
|
1412
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
1413
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" FAKE_CURL_LOG="$log" \
|
|
1414
|
+
"$TEARDOWN" pi-rearm-loop-fix-r1 > "$TMP_ROOT/td.out" 2>&1 || true
|
|
1415
|
+
case "$(cat "$TMP_ROOT/td.out")" in
|
|
1416
|
+
*"still owes a public reply"*) fail "teardown unexpectedly guarded the unregistered follow-on" ;;
|
|
1417
|
+
esac
|
|
1418
|
+
[ "$(followup_posts "$log")" = 1 ] || fail "unexpected extra post"
|
|
1419
|
+
pass "dropped-baton regression: delivery retains the loop and pending prints open-loop"
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
test_control_registered_followon_is_guarded() {
|
|
1423
|
+
local parent child
|
|
1424
|
+
parent=$(make_home baton-control-parent)
|
|
1425
|
+
child="$TMP_ROOT/baton-control-child"
|
|
1426
|
+
FM_SECONDMATE_CHARTER='Baton control charter.' FM_HOME="$parent" \
|
|
1427
|
+
"$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null || fail "seed failed"
|
|
1428
|
+
child=$(cd "$child" && pwd -P)
|
|
1429
|
+
make_fake_curl "$child" >/dev/null
|
|
1430
|
+
fm_fake_exit0 "$child/fakebin" tmux treehouse no-mistakes gh gh-axi
|
|
1431
|
+
seed_repro_commitment "$parent" public-final-pi-rearm-ship req-pirearm2 \
|
|
1432
|
+
secondmate:mate pi-rearm-loop-fix-r1
|
|
1433
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
1434
|
+
fm_write_meta "$child/state/pi-rearm-loop-fix-r1.meta" \
|
|
1435
|
+
"window=firstmate:fm-pi-rearm-loop-fix-r1" "endpoint_task_id=pi-rearm-loop-fix-r1" \
|
|
1436
|
+
"worktree=$child" "project=$child" "kind=ship" "mode=local-only" "spawn_gen=public-followup-fixture"
|
|
1437
|
+
PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
1438
|
+
FM_STATE_OVERRIDE="$child/state" FM_DATA_OVERRIDE="$child/data" \
|
|
1439
|
+
expect_failure "registered follow-on must be guarded" "$TEARDOWN" pi-rearm-loop-fix-r1
|
|
1440
|
+
assert_contains "$EXPECT_OUT" "still owes a public reply" "the guard fires only on presence"
|
|
1441
|
+
pass "CONTROL: the identical teardown REFUSES the moment a commitment is registered"
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
test_rechain_delivers_second_post_on_same_thread() {
|
|
1445
|
+
local parent log out posts command command_log
|
|
1446
|
+
parent=$(make_home rechain-parent)
|
|
1447
|
+
log="$parent/curl.log"; : > "$log"
|
|
1448
|
+
seed_repro_commitment "$parent" public-final-a req-rechain main scout-a
|
|
1449
|
+
"$EMIT" --home "$parent" --obligation public-final-a --relation rel-code \
|
|
1450
|
+
--source-home main --work-id scout-a --generation 1 \
|
|
1451
|
+
--outcome report-ready --deliverable report_path=data/scout-a/report.md \
|
|
1452
|
+
--outcome-text 'Reproduced. A fix is waiting.' >/dev/null || fail "emit failed"
|
|
1453
|
+
FAKE_CURL_LOG="$log" run_pf "$parent" consume >/dev/null || fail "consume failed"
|
|
1454
|
+
FAKE_CURL_LOG="$log" run_pf "$parent" deliver public-final-a >/dev/null || fail "deliver failed"
|
|
1455
|
+
[ "$(followup_posts "$log")" = 1 ] || fail "expected the investigation post"
|
|
1456
|
+
|
|
1457
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$parent" rechain public-final-b --from public-final-a \
|
|
1458
|
+
--work-home main --work-id ship-b --expected pr-merged) \
|
|
1459
|
+
|| fail "rechain failed: $out"
|
|
1460
|
+
assert_contains "$out" "retired public-final-a reason=handed on to public-final-b" \
|
|
1461
|
+
"rechain must retire the source loop"
|
|
1462
|
+
assert_contains "$out" "--deliverable pr_url=<value>" \
|
|
1463
|
+
"rechain brief must name the actual required deliverable key"
|
|
1464
|
+
command_log="$parent/brief-command.args"
|
|
1465
|
+
cat > "$parent/fakebin/record-emit" <<'SH'
|
|
1466
|
+
#!/usr/bin/env bash
|
|
1467
|
+
printf '%s\n' "$@" > "$RECORD_ARGS"
|
|
1468
|
+
SH
|
|
1469
|
+
chmod +x "$parent/fakebin/record-emit"
|
|
1470
|
+
command=$(printf '%s\n' "$out" | awk '
|
|
1471
|
+
index($0, "/bin/fm-public-followup-emit.sh") { capture=1 }
|
|
1472
|
+
capture { if ($0 == "") exit; print }
|
|
1473
|
+
')
|
|
1474
|
+
assert_contains "$command" "--outcome-text" \
|
|
1475
|
+
"the exact rechain command must remain continuous through outcome text"
|
|
1476
|
+
command=${command/"$ROOT/bin/fm-public-followup-emit.sh"/"$parent/fakebin/record-emit"}
|
|
1477
|
+
command=${command//<value>/https://github.com/example/repo/pull/99}
|
|
1478
|
+
RECORD_ARGS="$command_log" bash -c "$command" \
|
|
1479
|
+
|| fail "the exact rechain command must execute after filling its deliverable value"
|
|
1480
|
+
assert_grep '--deliverable' "$command_log" \
|
|
1481
|
+
"the executable rechain command must pass its deliverable option"
|
|
1482
|
+
assert_grep '--outcome-text' "$command_log" \
|
|
1483
|
+
"the executable rechain command must pass its outcome text option"
|
|
1484
|
+
assert_absent "$parent/state/public-followup/registry/public-final-a" \
|
|
1485
|
+
"the source registration must be gone after rechain"
|
|
1486
|
+
assert_present "$parent/state/public-followup/registry/public-final-b" \
|
|
1487
|
+
"the follow-on registration must exist"
|
|
1488
|
+
|
|
1489
|
+
"$EMIT" --home "$parent" --obligation public-final-b --relation rel-1 \
|
|
1490
|
+
--source-home main --work-id ship-b --generation 1 \
|
|
1491
|
+
--outcome pr-merged --deliverable pr_url=https://github.com/example/repo/pull/99 \
|
|
1492
|
+
--outcome-text 'Shipped: the Pi recovery loop is fixed.' >/dev/null || fail "follow-on emit failed"
|
|
1493
|
+
FAKE_CURL_LOG="$log" run_pf "$parent" consume | grep -q '^ready public-final-b ' \
|
|
1494
|
+
|| fail "follow-on consume not ready"
|
|
1495
|
+
FAKE_CURL_LOG="$log" run_pf "$parent" deliver public-final-b >/dev/null || fail "follow-on deliver failed"
|
|
1496
|
+
posts=$(followup_posts "$log")
|
|
1497
|
+
[ "$posts" = 2 ] || fail "expected exactly two posts in the same thread, got $posts"
|
|
1498
|
+
assert_grep 'Shipped: the Pi recovery loop is fixed.' "$log" \
|
|
1499
|
+
"the second post must carry the shipped text"
|
|
1500
|
+
assert_grep '"request_id":"req-rechain"' "$log" \
|
|
1501
|
+
"both posts must target the original request"
|
|
1502
|
+
pass "rechain posts the shipped follow-on into the same thread"
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
test_rechain_resumes_after_partial_add() {
|
|
1506
|
+
local home log real_tasks marker out count
|
|
1507
|
+
home=$(make_home rechain-resume)
|
|
1508
|
+
log="$home/curl.log"; : > "$log"
|
|
1509
|
+
seed_repro_commitment "$home" public-final-resume-a req-resume main scout-resume
|
|
1510
|
+
"$EMIT" --home "$home" --obligation public-final-resume-a --relation rel-code \
|
|
1511
|
+
--source-home main --work-id scout-resume --generation 1 \
|
|
1512
|
+
--outcome report-ready --deliverable report_path=data/scout-resume/report.md \
|
|
1513
|
+
--outcome-text 'Reproduced. A fix is waiting.' >/dev/null || fail "emit failed"
|
|
1514
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1515
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver public-final-resume-a >/dev/null \
|
|
1516
|
+
|| fail "deliver failed"
|
|
1517
|
+
|
|
1518
|
+
real_tasks=$(command -v tasks-axi)
|
|
1519
|
+
marker="$home/rechain-bind-failed"
|
|
1520
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
1521
|
+
#!/usr/bin/env bash
|
|
1522
|
+
if [ "$1" = public-followup ] && [ "$2" = bind-work ] \
|
|
1523
|
+
&& [ "$3" = public-final-resume-b ] && [ ! -e "$RECHAIN_FAIL_MARKER" ]; then
|
|
1524
|
+
: > "$RECHAIN_FAIL_MARKER"
|
|
1525
|
+
exit 73
|
|
1526
|
+
fi
|
|
1527
|
+
exec "$REAL_TASKS_AXI" "$@"
|
|
1528
|
+
SH
|
|
1529
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
1530
|
+
|
|
1531
|
+
REAL_TASKS_AXI="$real_tasks" RECHAIN_FAIL_MARKER="$marker" \
|
|
1532
|
+
expect_failure "rechain must expose a resumable partial add" \
|
|
1533
|
+
run_pf "$home" rechain public-final-resume-b --from public-final-resume-a \
|
|
1534
|
+
--work-home main --work-id ship-resume --expected pr-merged
|
|
1535
|
+
assert_contains "$EXPECT_OUT" "retry this same rechain command" \
|
|
1536
|
+
"a partial add must direct the caller to the resumable path"
|
|
1537
|
+
count=$(tasks_in "$home" public-followup list --json \
|
|
1538
|
+
| jq '[.public_followups[] | select(.id == "public-final-resume-b")] | length')
|
|
1539
|
+
[ "$count" = 1 ] || fail "the interrupted rechain must leave exactly one recoverable obligation"
|
|
1540
|
+
assert_present "$home/state/public-followup/registry/public-final-resume-a" \
|
|
1541
|
+
"a partial rechain must retain the source loop"
|
|
1542
|
+
assert_absent "$home/state/public-followup/registry/public-final-resume-b" \
|
|
1543
|
+
"a failed bind must not publish a registration"
|
|
1544
|
+
|
|
1545
|
+
out=$(REAL_TASKS_AXI="$real_tasks" RECHAIN_FAIL_MARKER="$marker" \
|
|
1546
|
+
run_pf "$home" rechain public-final-resume-b --from public-final-resume-a \
|
|
1547
|
+
--work-home main --work-id ship-resume --expected pr-merged) \
|
|
1548
|
+
|| fail "retrying the same rechain command must resume: $out"
|
|
1549
|
+
assert_contains "$out" "retired public-final-resume-a" \
|
|
1550
|
+
"the resumed rechain must retire its source"
|
|
1551
|
+
count=$(tasks_in "$home" public-followup list --json \
|
|
1552
|
+
| jq '[.public_followups[] | select(.id == "public-final-resume-b")] | length')
|
|
1553
|
+
[ "$count" = 1 ] || fail "the resumed rechain must not duplicate the obligation"
|
|
1554
|
+
assert_present "$home/state/public-followup/registry/public-final-resume-b" \
|
|
1555
|
+
"the resumed rechain must publish the destination registration"
|
|
1556
|
+
assert_absent "$home/state/public-followup/registry/public-final-resume-a" \
|
|
1557
|
+
"the resumed rechain must close the source registration"
|
|
1558
|
+
pass "rechain resumes the same obligation after an interrupted bind"
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
test_rechain_claims_delivered_source_once() {
|
|
1562
|
+
local home log pid_b pid_c rc_b=0 rc_c=0 registry_count
|
|
1563
|
+
home=$(make_home rechain-claim)
|
|
1564
|
+
log="$home/curl.log"; : > "$log"
|
|
1565
|
+
seed_repro_commitment "$home" public-final-claim-a req-claim main scout-claim
|
|
1566
|
+
"$EMIT" --home "$home" --obligation public-final-claim-a --relation rel-code \
|
|
1567
|
+
--source-home main --work-id scout-claim --generation 1 \
|
|
1568
|
+
--outcome report-ready --deliverable report_path=data/scout-claim/report.md \
|
|
1569
|
+
--outcome-text 'Reproduced. One follow-on may claim this thread.' >/dev/null || fail "emit failed"
|
|
1570
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1571
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver public-final-claim-a >/dev/null || fail "deliver failed"
|
|
1572
|
+
|
|
1573
|
+
FMX_NOW_OVERRIDE=1787539200 run_pf "$home" rechain public-final-claim-b \
|
|
1574
|
+
--from public-final-claim-a --work-home main --work-id ship-claim-b \
|
|
1575
|
+
--expected pr-merged > "$home/rechain-b.out" 2>&1 &
|
|
1576
|
+
pid_b=$!
|
|
1577
|
+
FMX_NOW_OVERRIDE=1787539200 run_pf "$home" rechain public-final-claim-c \
|
|
1578
|
+
--from public-final-claim-a --work-home main --work-id ship-claim-c \
|
|
1579
|
+
--expected pr-merged > "$home/rechain-c.out" 2>&1 &
|
|
1580
|
+
pid_c=$!
|
|
1581
|
+
wait "$pid_b" || rc_b=$?
|
|
1582
|
+
wait "$pid_c" || rc_c=$?
|
|
1583
|
+
|
|
1584
|
+
if [ "$rc_b" -eq 0 ]; then
|
|
1585
|
+
[ "$rc_c" -ne 0 ] || fail "two concurrent rechains must not both claim one source"
|
|
1586
|
+
else
|
|
1587
|
+
[ "$rc_c" -eq 0 ] || fail "exactly one concurrent rechain must succeed"
|
|
1588
|
+
fi
|
|
1589
|
+
registry_count=$(find "$home/state/public-followup/registry" -type f \
|
|
1590
|
+
\( -name 'public-final-claim-b' -o -name 'public-final-claim-c' \) | wc -l | tr -d ' ')
|
|
1591
|
+
[ "$registry_count" = 1 ] || fail "one source must produce exactly one registered destination"
|
|
1592
|
+
assert_absent "$home/state/public-followup/registry/public-final-claim-a" \
|
|
1593
|
+
"the successfully claimed source must be retired"
|
|
1594
|
+
pass "concurrent rechains cannot fork one delivered source"
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
test_failed_rechain_retirement_keeps_source_claimed() {
|
|
1598
|
+
local home log registry_file out
|
|
1599
|
+
home=$(make_home rechain-retire-failure)
|
|
1600
|
+
log="$home/curl.log"; : > "$log"
|
|
1601
|
+
seed_repro_commitment "$home" public-final-retire-a req-retire-failure main scout-retire
|
|
1602
|
+
"$EMIT" --home "$home" --obligation public-final-retire-a --relation rel-code \
|
|
1603
|
+
--source-home main --work-id scout-retire --generation 1 \
|
|
1604
|
+
--outcome report-ready --deliverable report_path=data/scout-retire/report.md \
|
|
1605
|
+
--outcome-text 'Reproduced. A fix is waiting.' >/dev/null || fail "emit failed"
|
|
1606
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1607
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver public-final-retire-a >/dev/null \
|
|
1608
|
+
|| fail "deliver failed"
|
|
1609
|
+
|
|
1610
|
+
registry_file="$home/state/public-followup/registry/public-final-retire-a"
|
|
1611
|
+
cat > "$home/fakebin/rm" <<EOF
|
|
1612
|
+
#!/usr/bin/env bash
|
|
1613
|
+
for arg in "\$@"; do
|
|
1614
|
+
[ "\$arg" != '$registry_file' ] || exit 1
|
|
1615
|
+
done
|
|
1616
|
+
exec /bin/rm "\$@"
|
|
1617
|
+
EOF
|
|
1618
|
+
chmod +x "$home/fakebin/rm"
|
|
1619
|
+
expect_failure "rechain must surface a failed source retirement" \
|
|
1620
|
+
run_pf "$home" rechain public-final-retire-b --from public-final-retire-a \
|
|
1621
|
+
--work-home main --work-id ship-retire-b --expected pr-merged
|
|
1622
|
+
assert_contains "$EXPECT_OUT" "public loop remains open" \
|
|
1623
|
+
"failed retirement must report that the source remains open"
|
|
1624
|
+
|
|
1625
|
+
expect_failure "a failed retirement must not leave the source claimable by another destination" \
|
|
1626
|
+
run_pf "$home" rechain public-final-retire-c --from public-final-retire-a \
|
|
1627
|
+
--work-home main --work-id ship-retire-c --expected pr-merged
|
|
1628
|
+
assert_contains "$EXPECT_OUT" "already claimed by rechain destination 'public-final-retire-b'" \
|
|
1629
|
+
"a second destination must be refused after the first destination is published"
|
|
1630
|
+
assert_absent "$home/state/public-followup/registry/public-final-retire-c" \
|
|
1631
|
+
"a refused competing destination must not be registered"
|
|
1632
|
+
|
|
1633
|
+
/bin/rm "$home/fakebin/rm"
|
|
1634
|
+
out=$(run_pf "$home" rechain public-final-retire-b --from public-final-retire-a \
|
|
1635
|
+
--work-home main --work-id ship-retire-b --expected pr-merged) \
|
|
1636
|
+
|| fail "the claimed destination must remain resumable: $out"
|
|
1637
|
+
assert_contains "$out" "retired public-final-retire-a" \
|
|
1638
|
+
"resuming the claimed destination must finish source retirement"
|
|
1639
|
+
assert_absent "$registry_file" "successful retry must retire the source"
|
|
1640
|
+
assert_present "$home/state/public-followup/registry/public-final-retire-b" \
|
|
1641
|
+
"successful retry must retain the one claimed destination"
|
|
1642
|
+
pass "failed rechain retirement keeps the source claimed by one resumable destination"
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
test_registration_replay_preserves_delivery_and_retirement() {
|
|
1646
|
+
local home log registry snapshot
|
|
1647
|
+
home=$(make_home register-replay)
|
|
1648
|
+
log="$home/curl.log"; : > "$log"
|
|
1649
|
+
seed_commitment "$home" pf-register-replay req-register-replay discord main work-register-replay
|
|
1650
|
+
jq -n '{relation_id:"rel-alternate", work_ref:{home_id:"main", task_id:"work-alternate"},
|
|
1651
|
+
role:"fulfills", required:false, generation:1}' > "$home/alternate-relation.json"
|
|
1652
|
+
tasks_in "$home" public-followup bind-work pf-register-replay \
|
|
1653
|
+
--relation-file "$home/alternate-relation.json" >/dev/null \
|
|
1654
|
+
|| fail "could not add the alternate valid work binding"
|
|
1655
|
+
emit_terminal "$home" "$home" pf-register-replay main work-register-replay >/dev/null || fail "emit failed"
|
|
1656
|
+
run_pf "$home" consume >/dev/null || fail "initial consume failed"
|
|
1657
|
+
run_pf "$home" register pf-register-replay --relation rel-alternate --work-home main \
|
|
1658
|
+
--work-id work-alternate --generation 1 >/dev/null \
|
|
1659
|
+
|| fail "could not register the alternate valid work binding"
|
|
1660
|
+
"$EMIT" --home "$home" --obligation pf-register-replay --relation rel-alternate \
|
|
1661
|
+
--source-home main --work-id work-alternate --generation 1 --outcome pr-merged \
|
|
1662
|
+
--deliverable pr_url=https://github.com/example/repo/pull/8 \
|
|
1663
|
+
--outcome-text 'The alternate bound work also reached its terminal outcome.' >/dev/null \
|
|
1664
|
+
|| fail "alternate emit failed"
|
|
1665
|
+
run_pf "$home" consume >/dev/null || fail "alternate consume failed"
|
|
1666
|
+
run_pf "$home" register pf-register-replay --relation rel-code --work-home main \
|
|
1667
|
+
--work-id work-register-replay --generation 1 >/dev/null \
|
|
1668
|
+
|| fail "could not restore the original open registration"
|
|
1669
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-register-replay >/dev/null || fail "deliver failed"
|
|
1670
|
+
|
|
1671
|
+
run_pf "$home" register pf-register-replay --relation rel-code --work-home main \
|
|
1672
|
+
--work-id work-register-replay --generation 1 >/dev/null || fail "registration replay failed"
|
|
1673
|
+
registry="$home/state/public-followup/registry/pf-register-replay"
|
|
1674
|
+
grep -q '^state=delivered$' "$registry" \
|
|
1675
|
+
|| fail "registration replay must not downgrade a delivered loop"
|
|
1676
|
+
|
|
1677
|
+
snapshot="$home/registry-before-replay"
|
|
1678
|
+
cp "$registry" "$snapshot"
|
|
1679
|
+
run_pf "$home" register pf-register-replay --relation rel-alternate --work-home main \
|
|
1680
|
+
--work-id work-alternate --generation 1 --platform x --request req-alternate >/dev/null \
|
|
1681
|
+
|| fail "delivered registration replay against another valid binding failed"
|
|
1682
|
+
cmp -s "$snapshot" "$registry" \
|
|
1683
|
+
|| fail "delivered registration replay must preserve the complete retained baton"
|
|
1684
|
+
|
|
1685
|
+
run_pf "$home" retire pf-register-replay --reason "finished after replay" >/dev/null \
|
|
1686
|
+
|| fail "retire after replay failed"
|
|
1687
|
+
expect_failure "registration replay must not reopen a retired loop" \
|
|
1688
|
+
run_pf "$home" register pf-register-replay --relation rel-code --work-home main \
|
|
1689
|
+
--work-id work-register-replay --generation 1
|
|
1690
|
+
assert_contains "$EXPECT_OUT" "already been retired" \
|
|
1691
|
+
"a retirement receipt must make registration fail closed"
|
|
1692
|
+
assert_absent "$home/state/public-followup/registry/pf-register-replay" \
|
|
1693
|
+
"registration replay must not recreate a retired loop"
|
|
1694
|
+
pass "registration replay preserves delivered and retired loop states"
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
test_redelivery_does_not_report_retired_loop_open() {
|
|
1698
|
+
local home log out
|
|
1699
|
+
home=$(make_home redelivery-retired)
|
|
1700
|
+
log="$home/curl.log"; : > "$log"
|
|
1701
|
+
seed_commitment "$home" pf-redelivery-retired req-redelivery-retired discord main work-redelivery-retired
|
|
1702
|
+
emit_terminal "$home" "$home" pf-redelivery-retired main work-redelivery-retired >/dev/null \
|
|
1703
|
+
|| fail "emit failed"
|
|
1704
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1705
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-redelivery-retired >/dev/null \
|
|
1706
|
+
|| fail "initial delivery failed"
|
|
1707
|
+
run_pf "$home" retire pf-redelivery-retired --reason "thread finished" >/dev/null \
|
|
1708
|
+
|| fail "retire failed"
|
|
1709
|
+
|
|
1710
|
+
out=$(FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-redelivery-retired) \
|
|
1711
|
+
|| fail "idempotent redelivery failed"
|
|
1712
|
+
assert_contains "$out" "already delivered pf-redelivery-retired" \
|
|
1713
|
+
"redelivery must remain idempotent"
|
|
1714
|
+
case "$out" in
|
|
1715
|
+
*"still OPEN"*) fail "redelivery must not report a retired loop as open: $out" ;;
|
|
1716
|
+
esac
|
|
1717
|
+
[ "$(grep -c '^url=.*connector/followup' "$log" || true)" -eq 1 ] \
|
|
1718
|
+
|| fail "redelivery must not post a second public reply"
|
|
1719
|
+
pass "redelivery does not report a retired loop as open"
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
test_retire_after_secondmate_home_removal() {
|
|
1723
|
+
local home child log out
|
|
1724
|
+
home=$(make_home retire-removed-secondmate)
|
|
1725
|
+
child="$home/removed-mate"
|
|
1726
|
+
mkdir -p "$child/state"
|
|
1727
|
+
printf 'mate\n' > "$child/.fm-secondmate-home"
|
|
1728
|
+
fm_write_meta "$home/state/mate.meta" "kind=secondmate" "home=$child"
|
|
1729
|
+
log="$home/curl.log"; : > "$log"
|
|
1730
|
+
seed_repro_commitment "$home" pf-removed-mate req-removed-mate secondmate:mate scout-removed
|
|
1731
|
+
"$EMIT" --home "$home" --obligation pf-removed-mate --relation rel-code \
|
|
1732
|
+
--source-home secondmate:mate --work-id scout-removed --generation 1 \
|
|
1733
|
+
--outcome report-ready --deliverable report_path=data/scout-removed/report.md \
|
|
1734
|
+
--outcome-text 'The removed child completed its investigation.' >/dev/null || fail "emit failed"
|
|
1735
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1736
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-removed-mate >/dev/null || fail "deliver failed"
|
|
1737
|
+
|
|
1738
|
+
rm -rf "$child"
|
|
1739
|
+
rm -f "$home/state/mate.meta"
|
|
1740
|
+
out=$(run_pf "$home" retire pf-removed-mate --reason "child home was torn down") \
|
|
1741
|
+
|| fail "retire must accept an already-absent child legacy link: $out"
|
|
1742
|
+
assert_contains "$out" "retired pf-removed-mate" \
|
|
1743
|
+
"retire must close a delivered loop after its secondmate home is removed"
|
|
1744
|
+
assert_absent "$home/state/public-followup/registry/pf-removed-mate" \
|
|
1745
|
+
"retire must remove the registration after child teardown"
|
|
1746
|
+
assert_present "$home/state/public-followup/retired/pf-removed-mate" \
|
|
1747
|
+
"retire must still record its receipt"
|
|
1748
|
+
pass "retire closes delivered loops after secondmate home removal"
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
test_retire_refuses_unbound_existing_secondmate() {
|
|
1752
|
+
local home child log
|
|
1753
|
+
home=$(make_home retire-unbound-secondmate)
|
|
1754
|
+
child="$home/unbound-mate"
|
|
1755
|
+
mkdir -p "$child/state"
|
|
1756
|
+
child=$(cd "$child" && pwd -P)
|
|
1757
|
+
printf 'mate\n' > "$child/.fm-secondmate-home"
|
|
1758
|
+
fm_write_meta "$home/state/mate.meta" "kind=secondmate" "home=$child"
|
|
1759
|
+
log="$home/curl.log"; : > "$log"
|
|
1760
|
+
seed_repro_commitment "$home" pf-unbound-mate req-unbound-mate secondmate:mate scout-unbound
|
|
1761
|
+
"$EMIT" --home "$home" --obligation pf-unbound-mate --relation rel-code \
|
|
1762
|
+
--source-home secondmate:mate --work-id scout-unbound --generation 1 \
|
|
1763
|
+
--outcome report-ready --deliverable report_path=data/scout-unbound/report.md \
|
|
1764
|
+
--outcome-text 'The unbound child completed its investigation.' >/dev/null || fail "emit failed"
|
|
1765
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1766
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-unbound-mate >/dev/null || fail "deliver failed"
|
|
1767
|
+
fm_write_meta "$child/state/scout-unbound.meta" "status=working" "x_request=req-unbound-mate"
|
|
1768
|
+
assert_grep "work_home_path=$child" \
|
|
1769
|
+
"$home/state/public-followup/registry/pf-unbound-mate" \
|
|
1770
|
+
"registration must retain the canonical secondmate path"
|
|
1771
|
+
rm -f "$home/state/mate.meta"
|
|
1772
|
+
|
|
1773
|
+
expect_failure "retire must not assume an unbound existing child link is cleared" \
|
|
1774
|
+
run_pf "$home" retire pf-unbound-mate --reason "child binding disappeared"
|
|
1775
|
+
assert_contains "$EXPECT_OUT" "could not clear the legacy X link" \
|
|
1776
|
+
"retire must report an unverifiable secondmate legacy link"
|
|
1777
|
+
assert_present "$home/state/public-followup/registry/pf-unbound-mate" \
|
|
1778
|
+
"an unverifiable child link must retain the registration"
|
|
1779
|
+
assert_absent "$home/state/public-followup/retired/pf-unbound-mate" \
|
|
1780
|
+
"an unverifiable child link must not create a retirement receipt"
|
|
1781
|
+
assert_grep 'x_request=req-unbound-mate' "$child/state/scout-unbound.meta" \
|
|
1782
|
+
"failed retirement must preserve the unresolved legacy link"
|
|
1783
|
+
pass "retire fails closed for an unbound existing secondmate"
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
test_retire_refuses_reassigned_secondmate_home() {
|
|
1787
|
+
local home original replacement log
|
|
1788
|
+
home=$(make_home retire-reassigned-secondmate)
|
|
1789
|
+
original="$home/original-mate"
|
|
1790
|
+
replacement="$home/replacement-mate"
|
|
1791
|
+
mkdir -p "$original/state"
|
|
1792
|
+
printf 'mate\n' > "$original/.fm-secondmate-home"
|
|
1793
|
+
fm_write_meta "$home/state/mate.meta" "kind=secondmate" "home=$original"
|
|
1794
|
+
log="$home/curl.log"; : > "$log"
|
|
1795
|
+
seed_repro_commitment "$home" pf-reassigned-mate req-reassigned-mate secondmate:mate scout-reassigned
|
|
1796
|
+
"$EMIT" --home "$home" --obligation pf-reassigned-mate --relation rel-code \
|
|
1797
|
+
--source-home secondmate:mate --work-id scout-reassigned --generation 1 \
|
|
1798
|
+
--outcome report-ready --deliverable report_path=data/scout-reassigned/report.md \
|
|
1799
|
+
--outcome-text 'The original child completed its investigation.' >/dev/null || fail "emit failed"
|
|
1800
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1801
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-reassigned-mate >/dev/null || fail "deliver failed"
|
|
1802
|
+
|
|
1803
|
+
rm -rf "$original"
|
|
1804
|
+
mkdir -p "$replacement/state"
|
|
1805
|
+
replacement=$(cd "$replacement" && pwd -P)
|
|
1806
|
+
printf 'mate\n' > "$replacement/.fm-secondmate-home"
|
|
1807
|
+
fm_write_meta "$replacement/state/scout-reassigned.meta" \
|
|
1808
|
+
"status=working" "x_request=req-unrelated-replacement"
|
|
1809
|
+
fm_write_meta "$home/state/mate.meta" "kind=secondmate" "home=$replacement"
|
|
1810
|
+
|
|
1811
|
+
expect_failure "retire must not clear a reassigned secondmate home" \
|
|
1812
|
+
run_pf "$home" retire pf-reassigned-mate --reason "original child was removed"
|
|
1813
|
+
assert_contains "$EXPECT_OUT" "could not clear the legacy X link" \
|
|
1814
|
+
"retirement must fail when the stable ID resolves to a different home"
|
|
1815
|
+
assert_present "$home/state/public-followup/registry/pf-reassigned-mate" \
|
|
1816
|
+
"a reassigned child must retain the registration"
|
|
1817
|
+
assert_absent "$home/state/public-followup/retired/pf-reassigned-mate" \
|
|
1818
|
+
"a reassigned child must not create a retirement receipt"
|
|
1819
|
+
assert_grep 'x_request=req-unrelated-replacement' "$replacement/state/scout-reassigned.meta" \
|
|
1820
|
+
"failed retirement must preserve the replacement home's Relay link"
|
|
1821
|
+
pass "retire fails closed when a secondmate ID is reassigned"
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
test_rechain_refuses_unclaimed_existing_destination() {
|
|
1825
|
+
local home log out
|
|
1826
|
+
home=$(make_home rechain-existing-destination)
|
|
1827
|
+
log="$home/curl.log"; : > "$log"
|
|
1828
|
+
seed_repro_commitment "$home" public-final-existing-a req-existing main scout-existing
|
|
1829
|
+
"$EMIT" --home "$home" --obligation public-final-existing-a --relation rel-code \
|
|
1830
|
+
--source-home main --work-id scout-existing --generation 1 \
|
|
1831
|
+
--outcome report-ready --deliverable report_path=data/scout-existing/report.md \
|
|
1832
|
+
--outcome-text 'Investigation complete.' >/dev/null || fail "emit failed"
|
|
1833
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1834
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver public-final-existing-a >/dev/null \
|
|
1835
|
+
|| fail "deliver failed"
|
|
1836
|
+
|
|
1837
|
+
jq -n '{type:"pr-merged", project:"firstmate", required_deliverables:["pr_url"],
|
|
1838
|
+
completion_policy:"all-required"}' > "$home/collision-expected.json"
|
|
1839
|
+
tasks_in "$home" public-followup add public-final-existing-b \
|
|
1840
|
+
--request-context-file "$home/request.json" --purpose promised-final \
|
|
1841
|
+
--expected-final-file "$home/collision-expected.json" \
|
|
1842
|
+
--expires-at 2026-08-28T01:12:00Z >/dev/null || fail "could not seed destination collision"
|
|
1843
|
+
|
|
1844
|
+
expect_failure "a first rechain must not adopt an unrelated existing obligation" \
|
|
1845
|
+
run_pf "$home" rechain public-final-existing-b --from public-final-existing-a \
|
|
1846
|
+
--work-home main --work-id ship-existing --expected pr-merged
|
|
1847
|
+
assert_contains "$EXPECT_OUT" "was not created by this rechain" \
|
|
1848
|
+
"the collision refusal must identify the unclaimed destination"
|
|
1849
|
+
out=$(cat "$home/state/public-followup/registry/public-final-existing-a")
|
|
1850
|
+
case "$out" in
|
|
1851
|
+
*rechain_to=*) fail "a destination collision must not claim the source" ;;
|
|
1852
|
+
esac
|
|
1853
|
+
assert_absent "$home/state/public-followup/registry/public-final-existing-b" \
|
|
1854
|
+
"an unrelated obligation must not become a registered destination"
|
|
1855
|
+
pass "rechain refuses an unrelated existing destination"
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
test_pending_skips_concurrent_retirement() {
|
|
1859
|
+
local home log real_tasks pending_pid locker_pid rc=0
|
|
1860
|
+
home=$(make_home pending-retirement-race)
|
|
1861
|
+
log="$home/curl.log"; : > "$log"
|
|
1862
|
+
seed_commitment "$home" pf-race req-race discord main work-race
|
|
1863
|
+
emit_terminal "$home" "$home" pf-race main work-race >/dev/null || fail "race emit failed"
|
|
1864
|
+
run_pf "$home" consume >/dev/null || fail "race consume failed"
|
|
1865
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-race >/dev/null || fail "race deliver failed"
|
|
1866
|
+
sed -e 's/^state=delivered$/state=open/' \
|
|
1867
|
+
-e '/^delivered_at=/d' -e '/^delivered_obligation=/d' \
|
|
1868
|
+
"$home/state/public-followup/registry/pf-race" > "$home/race-open"
|
|
1869
|
+
mv "$home/race-open" "$home/state/public-followup/registry/pf-race"
|
|
1870
|
+
chmod 600 "$home/state/public-followup/registry/pf-race"
|
|
1871
|
+
|
|
1872
|
+
seed_commitment "$home" pf-race-other req-race-other discord main work-race-other
|
|
1873
|
+
|
|
1874
|
+
FM_RACE_HOME="$home" FM_RACE_ROOT="$ROOT" bash -c '
|
|
1875
|
+
. "$FM_RACE_ROOT/bin/fm-public-followup-lib.sh"
|
|
1876
|
+
fm_pf_registry_lock_acquire "$FM_RACE_HOME/state" pf-race || exit 1
|
|
1877
|
+
: > "$FM_RACE_HOME/lock-ready"
|
|
1878
|
+
while [ ! -e "$FM_RACE_HOME/release-lock" ]; do sleep 0.02; done
|
|
1879
|
+
sleep 0.1
|
|
1880
|
+
mkdir -p "$FM_RACE_HOME/state/public-followup/retired"
|
|
1881
|
+
printf "reason=concurrent close\nretired_at=2026-08-01T00:00:00Z\n" \
|
|
1882
|
+
> "$FM_RACE_HOME/state/public-followup/retired/pf-race"
|
|
1883
|
+
chmod 600 "$FM_RACE_HOME/state/public-followup/retired/pf-race"
|
|
1884
|
+
rm -f "$FM_RACE_HOME/state/public-followup/registry/pf-race"
|
|
1885
|
+
fm_pf_registry_lock_release "$FM_RACE_HOME/state" pf-race
|
|
1886
|
+
' &
|
|
1887
|
+
locker_pid=$!
|
|
1888
|
+
for _ in $(seq 1 100); do [ -e "$home/lock-ready" ] && break; sleep 0.02; done
|
|
1889
|
+
[ -e "$home/lock-ready" ] || fail "race locker did not start"
|
|
1890
|
+
|
|
1891
|
+
real_tasks=$(command -v tasks-axi)
|
|
1892
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
1893
|
+
#!/usr/bin/env bash
|
|
1894
|
+
"$REAL_TASKS_AXI" "$@"
|
|
1895
|
+
rc=$?
|
|
1896
|
+
if [ "$1" = public-followup ] && [ "$2" = list ]; then
|
|
1897
|
+
: > "$PENDING_LISTED"
|
|
1898
|
+
fi
|
|
1899
|
+
exit "$rc"
|
|
1900
|
+
SH
|
|
1901
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
1902
|
+
REAL_TASKS_AXI="$real_tasks" PENDING_LISTED="$home/pending-listed" \
|
|
1903
|
+
run_pf "$home" pending > "$home/pending-race.out" 2>&1 &
|
|
1904
|
+
pending_pid=$!
|
|
1905
|
+
for _ in $(seq 1 100); do [ -e "$home/pending-listed" ] && break; sleep 0.02; done
|
|
1906
|
+
[ -e "$home/pending-listed" ] || fail "pending did not snapshot the backlog"
|
|
1907
|
+
: > "$home/release-lock"
|
|
1908
|
+
wait "$locker_pid" || fail "race retirement failed"
|
|
1909
|
+
wait "$pending_pid" || rc=$?
|
|
1910
|
+
[ "$rc" -eq 0 ] || fail "pending aborted on concurrent retirement: $(cat "$home/pending-race.out")"
|
|
1911
|
+
assert_grep 'unresolved pf-race-other ' "$home/pending-race.out" \
|
|
1912
|
+
"pending must continue surfacing unrelated loops after concurrent retirement: $(cat "$home/pending-race.out")"
|
|
1913
|
+
pass "pending skips a registration retired during settlement"
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
test_retire_reason_closes_the_open_loop() {
|
|
1917
|
+
local home log out registry_file receipt_mode
|
|
1918
|
+
home=$(make_home retire-reason)
|
|
1919
|
+
log="$home/curl.log"; : > "$log"
|
|
1920
|
+
seed_commitment "$home" pf-retire req-retire discord main work-retire
|
|
1921
|
+
emit_terminal "$home" "$home" pf-retire main work-retire >/dev/null || fail "emit failed"
|
|
1922
|
+
FAKE_CURL_LOG="$log" run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1923
|
+
FAKE_CURL_LOG="$log" run_pf "$home" deliver pf-retire >/dev/null || fail "deliver failed"
|
|
1924
|
+
run_pf "$home" pending | grep -q '^open-loop pf-retire ' \
|
|
1925
|
+
|| fail "pending must show the delivered open loop"
|
|
1926
|
+
expect_failure "retire without --reason must refuse" run_pf "$home" retire pf-retire
|
|
1927
|
+
assert_contains "$EXPECT_OUT" "--reason" "the refusal must name the required reason"
|
|
1928
|
+
assert_present "$home/state/public-followup/registry/pf-retire" \
|
|
1929
|
+
"a reason-less retire must keep the registration"
|
|
1930
|
+
registry_file="$home/state/public-followup/registry/pf-retire"
|
|
1931
|
+
cat > "$home/fakebin/rm" <<EOF
|
|
1932
|
+
#!/usr/bin/env bash
|
|
1933
|
+
for arg in "\$@"; do
|
|
1934
|
+
[ "\$arg" != '$registry_file' ] || exit 1
|
|
1935
|
+
done
|
|
1936
|
+
exec /bin/rm "\$@"
|
|
1937
|
+
EOF
|
|
1938
|
+
chmod +x "$home/fakebin/rm"
|
|
1939
|
+
expect_failure "retire must report registration removal failure" \
|
|
1940
|
+
run_pf "$home" retire pf-retire --reason "the public loop is finished"
|
|
1941
|
+
assert_contains "$EXPECT_OUT" "public loop remains open" \
|
|
1942
|
+
"retire removal failure must report the truthful loop state"
|
|
1943
|
+
assert_present "$registry_file" \
|
|
1944
|
+
"retire removal failure must retain the registration"
|
|
1945
|
+
/bin/rm "$home/fakebin/rm"
|
|
1946
|
+
out=$(run_pf "$home" retire pf-retire --reason "the public loop is finished") \
|
|
1947
|
+
|| fail "retire --reason failed"
|
|
1948
|
+
assert_contains "$out" "retired pf-retire reason=the public loop is finished" \
|
|
1949
|
+
"retire must report the reason"
|
|
1950
|
+
assert_present "$home/state/public-followup/retired/pf-retire" \
|
|
1951
|
+
"retire must persist a private receipt before removing the registration"
|
|
1952
|
+
assert_grep 'reason=the public loop is finished' \
|
|
1953
|
+
"$home/state/public-followup/retired/pf-retire" \
|
|
1954
|
+
"the retirement receipt must preserve the reason"
|
|
1955
|
+
assert_grep 'retired_at=' "$home/state/public-followup/retired/pf-retire" \
|
|
1956
|
+
"the retirement receipt must preserve its timestamp"
|
|
1957
|
+
receipt_mode=$(stat -c %a "$home/state/public-followup/retired/pf-retire" 2>/dev/null \
|
|
1958
|
+
|| stat -f %Lp "$home/state/public-followup/retired/pf-retire" 2>/dev/null) \
|
|
1959
|
+
|| fail "could not inspect the retirement receipt mode"
|
|
1960
|
+
[ "$receipt_mode" = 600 ] || fail "the retirement receipt must be private"
|
|
1961
|
+
assert_absent "$home/state/public-followup/registry/pf-retire" \
|
|
1962
|
+
"retire is the only removal"
|
|
1963
|
+
out=$(run_pf "$home" pending || true)
|
|
1964
|
+
case "$out" in
|
|
1965
|
+
*pf-retire*) fail "pending must stop listing a retired loop: $out" ;;
|
|
1966
|
+
esac
|
|
1967
|
+
pass "retire --reason closes the loop and drops the open-loop line"
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
test_retention_creates_no_false_teardown_refusal() {
|
|
1971
|
+
local home home2 rc out registry tmp
|
|
1972
|
+
home=$(make_home retain-teardown)
|
|
1973
|
+
seed_commitment "$home" pf-retain req-retain discord main ship-retain
|
|
1974
|
+
tasks_in "$home" add ship-retain "ship with a retained delivered registration" --kind ship >/dev/null \
|
|
1975
|
+
|| fail "could not add the retained-registration ship to its home's backlog"
|
|
1976
|
+
tasks_in "$home" start ship-retain >/dev/null \
|
|
1977
|
+
|| fail "could not mark the retained-registration ship In flight"
|
|
1978
|
+
fm_write_meta "$home/state/ship-retain.meta" \
|
|
1979
|
+
"window=firstmate:fm-ship-retain" \
|
|
1980
|
+
"worktree=$home/projects/gone" \
|
|
1981
|
+
"project=$home/projects/sample" \
|
|
1982
|
+
"harness=codex" \
|
|
1983
|
+
"kind=ship" \
|
|
1984
|
+
"mode=no-mistakes" \
|
|
1985
|
+
"spawn_gen=public-followup-retain"
|
|
1986
|
+
emit_terminal "$home" "$home" pf-retain main ship-retain >/dev/null || fail "emit failed"
|
|
1987
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
1988
|
+
FAKE_CURL_LOG="$home/curl.log" run_pf "$home" deliver pf-retain >/dev/null || fail "delivery failed"
|
|
1989
|
+
run_pf "$home" guard-work main ship-retain \
|
|
1990
|
+
|| fail "guard-work must pass for the work whose reply already landed"
|
|
1991
|
+
assert_present "$home/state/public-followup/registry/pf-retain" \
|
|
1992
|
+
"the delivered registration must still be present"
|
|
1993
|
+
rc=0
|
|
1994
|
+
PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
1995
|
+
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
|
|
1996
|
+
FM_CONFIG_OVERRIDE="$home/config" "$TEARDOWN" ship-retain \
|
|
1997
|
+
> "$home/td.out" 2> "$home/td.err" || rc=$?
|
|
1998
|
+
[ "$rc" -eq 0 ] || fail "teardown must proceed with a retained delivered registration (rc=$rc)"
|
|
1999
|
+
case "$(cat "$home/td.err")" in
|
|
2000
|
+
*"still owes a public reply"*) fail "retention must not create a false public-reply refusal" ;;
|
|
2001
|
+
esac
|
|
2002
|
+
# pending still sees the loop because teardown does not retire it
|
|
2003
|
+
home2=$(make_home retain-pending)
|
|
2004
|
+
seed_commitment "$home2" pf-keep req-keep discord main work-keep
|
|
2005
|
+
emit_terminal "$home2" "$home2" pf-keep main work-keep >/dev/null || fail "emit2 failed"
|
|
2006
|
+
run_pf "$home2" consume >/dev/null || fail "consume2 failed"
|
|
2007
|
+
FAKE_CURL_LOG="$home2/curl.log" run_pf "$home2" deliver pf-keep >/dev/null || fail "deliver2 failed"
|
|
2008
|
+
registry="$home2/state/public-followup/registry/pf-keep"
|
|
2009
|
+
tmp="$registry.tmp"
|
|
2010
|
+
grep -v -E '^(state|delivered_at|delivered_obligation)=' "$registry" > "$tmp"
|
|
2011
|
+
printf 'state=open\n' >> "$tmp"
|
|
2012
|
+
chmod 600 "$tmp"
|
|
2013
|
+
mv "$tmp" "$registry"
|
|
2014
|
+
out=$(run_pf "$home2" pending)
|
|
2015
|
+
assert_contains "$out" "open-loop pf-keep" "pending must keep a delivered registration as an open loop"
|
|
2016
|
+
assert_grep 'state=delivered' "$registry" \
|
|
2017
|
+
"pending must repair a settled registration left open after legacy cleanup failed"
|
|
2018
|
+
pass "retention creates no false teardown refusal and pending no longer prunes"
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
test_expiry_escalation_uses_now_override() {
|
|
2022
|
+
local home out exp now_closing now_expired registry tmp
|
|
2023
|
+
home=$(make_home expiry-window)
|
|
2024
|
+
seed_repro_commitment "$home" pf-exp req-exp main work-exp
|
|
2025
|
+
exp=$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' '2026-08-28T01:12:00Z' +%s 2>/dev/null) \
|
|
2026
|
+
|| exp=$(date -u -d '2026-08-28T01:12:00Z' +%s)
|
|
2027
|
+
now_closing=$((exp - 3600))
|
|
2028
|
+
now_expired=$((exp + 60))
|
|
2029
|
+
out=$(FMX_NOW_OVERRIDE="$now_expired" run_pf "$home" pending)
|
|
2030
|
+
assert_contains "$out" "unresolved pf-exp" "an owed reply must remain listed after expiry"
|
|
2031
|
+
assert_contains "$out" "can no longer be reached" \
|
|
2032
|
+
"an expired unresolved reply must escalate the unreachable thread"
|
|
2033
|
+
assert_contains "$out" "captain decision" \
|
|
2034
|
+
"an expired unresolved reply must name the captain call"
|
|
2035
|
+
"$EMIT" --home "$home" --obligation pf-exp --relation rel-code \
|
|
2036
|
+
--source-home main --work-id work-exp --generation 1 \
|
|
2037
|
+
--outcome report-ready --deliverable report_path=data/x/report.md \
|
|
2038
|
+
--outcome-text 'Reproduced.' >/dev/null || fail "emit failed"
|
|
2039
|
+
run_pf "$home" consume >/dev/null || fail "consume failed"
|
|
2040
|
+
FAKE_CURL_LOG="$home/curl.log" run_pf "$home" deliver pf-exp >/dev/null || fail "deliver failed"
|
|
2041
|
+
out=$(FMX_NOW_OVERRIDE="$now_closing" run_pf "$home" pending)
|
|
2042
|
+
assert_contains "$out" "open-loop pf-exp" "closing window must still list the loop"
|
|
2043
|
+
assert_contains "$out" "DEADLINE:" "a window under 48 hours must escalate"
|
|
2044
|
+
assert_contains "$out" "under 48 hours" "the closing wording must name the remaining window"
|
|
2045
|
+
out=$(FMX_NOW_OVERRIDE="$now_expired" run_pf "$home" pending)
|
|
2046
|
+
assert_contains "$out" "can no longer be reached" "a past expiry must name the unreachable thread"
|
|
2047
|
+
assert_contains "$out" "captain decision" "a past expiry is a captain call"
|
|
2048
|
+
FMX_NOW_OVERRIDE="$now_expired" expect_failure "rechain past expiry must refuse" \
|
|
2049
|
+
run_pf "$home" rechain pf-exp-next --from pf-exp --work-home main --work-id work-next --expected pr-merged
|
|
2050
|
+
assert_contains "$EXPECT_OUT" "can no longer be reached" "rechain must name the closed window"
|
|
2051
|
+
registry="$home/state/public-followup/registry/pf-exp"
|
|
2052
|
+
tmp="$registry.tmp"
|
|
2053
|
+
awk '
|
|
2054
|
+
/^followup_expires_at=/ { print "followup_expires_at=not-a-time"; next }
|
|
2055
|
+
{ print }
|
|
2056
|
+
' "$registry" > "$tmp"
|
|
2057
|
+
chmod 600 "$tmp"
|
|
2058
|
+
mv "$tmp" "$registry"
|
|
2059
|
+
expect_failure "rechain with an unknown expiry window must refuse" \
|
|
2060
|
+
run_pf "$home" rechain pf-exp-next --from pf-exp --work-home main --work-id work-next --expected pr-merged
|
|
2061
|
+
assert_contains "$EXPECT_OUT" "thread window cannot be checked" \
|
|
2062
|
+
"rechain must fail closed when its retained expiry cannot be parsed"
|
|
2063
|
+
pass "expiry escalation is pinned by FMX_NOW_OVERRIDE"
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
test_brief_fails_without_typed_deliverable_keys() {
|
|
2067
|
+
local home real_tasks invalid
|
|
2068
|
+
home=$(make_home brief-keys)
|
|
2069
|
+
seed_commitment "$home" pf-brief req-brief discord main work-brief
|
|
2070
|
+
real_tasks=$(command -v tasks-axi)
|
|
2071
|
+
cat > "$home/fakebin/tasks-axi" <<'SH'
|
|
2072
|
+
#!/usr/bin/env bash
|
|
2073
|
+
if [ "${FAKE_INVALID_KEYS:-}" = unreadable ]; then
|
|
2074
|
+
exit 69
|
|
2075
|
+
fi
|
|
2076
|
+
"$REAL_TASKS_AXI" "$@" | jq --argjson invalid "$FAKE_INVALID_KEYS" '
|
|
2077
|
+
if .public_followups then
|
|
2078
|
+
.public_followups |= map(
|
|
2079
|
+
if .id == "pf-brief" then
|
|
2080
|
+
.public_followup.expected_final.required_deliverables = $invalid
|
|
2081
|
+
else . end)
|
|
2082
|
+
else . end'
|
|
2083
|
+
SH
|
|
2084
|
+
chmod +x "$home/fakebin/tasks-axi"
|
|
2085
|
+
REAL_TASKS_AXI="$real_tasks" FAKE_INVALID_KEYS=unreadable expect_failure \
|
|
2086
|
+
"brief must fail when typed deliverable keys cannot be read" \
|
|
2087
|
+
run_pf "$home" brief pf-brief
|
|
2088
|
+
assert_contains "$EXPECT_OUT" "could not read public-followup obligation" \
|
|
2089
|
+
"brief must explain why it cannot produce executable instructions"
|
|
2090
|
+
assert_not_contains "$EXPECT_OUT" "<key>=<value>" \
|
|
2091
|
+
"brief must never substitute a generic deliverable placeholder"
|
|
2092
|
+
|
|
2093
|
+
for invalid in '["pr_url",7]' '["pr_url",""]' '["PR_URL"]' '["pr-url"]'; do
|
|
2094
|
+
REAL_TASKS_AXI="$real_tasks" FAKE_INVALID_KEYS="$invalid" expect_failure \
|
|
2095
|
+
"brief must reject an invalid required deliverable array" \
|
|
2096
|
+
run_pf "$home" brief pf-brief
|
|
2097
|
+
assert_contains "$EXPECT_OUT" "no readable required deliverable keys" \
|
|
2098
|
+
"brief must reject the complete contract when any key is invalid"
|
|
2099
|
+
assert_not_contains "$EXPECT_OUT" "--deliverable pr_url=<value>" \
|
|
2100
|
+
"brief must not emit a partial contract from an invalid key array"
|
|
2101
|
+
done
|
|
2102
|
+
pass "brief fails explicitly when typed deliverable keys are unavailable"
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
test_prechange_registration_is_open_and_unrechainable() {
|
|
2106
|
+
local home file out
|
|
2107
|
+
home=$(make_home prechange)
|
|
2108
|
+
mkdir -p "$home/state/public-followup/registry"
|
|
2109
|
+
chmod 700 "$home/state/public-followup" "$home/state/public-followup/registry"
|
|
2110
|
+
file="$home/state/public-followup/registry/pf-legacy"
|
|
2111
|
+
printf 'obligation_id=pf-legacy\nrelation_id=rel-code\nwork_home=main\nwork_id=work-legacy\ngeneration=1\nplatform=discord\nrequest_id=req-legacy\n' \
|
|
2112
|
+
> "$file"
|
|
2113
|
+
chmod 600 "$file"
|
|
2114
|
+
out=$(run_pf "$home" pending) || fail "pending must not crash on a pre-change registration"
|
|
2115
|
+
assert_contains "$out" "open-loop pf-legacy" "a pre-change record is an open loop"
|
|
2116
|
+
assert_contains "$out" "unrechainable" "a pre-change record must be reported un-rechainable"
|
|
2117
|
+
expect_failure "rechain of a pre-change record must refuse" \
|
|
2118
|
+
run_pf "$home" rechain pf-new --from pf-legacy --work-home main --work-id work-next --expected pr-merged
|
|
2119
|
+
case "$EXPECT_OUT" in
|
|
2120
|
+
*un-rechainable*) ;;
|
|
2121
|
+
*'not state=delivered'*) ;;
|
|
2122
|
+
*) fail "rechain must refuse a pre-change record without crashing: $EXPECT_OUT" ;;
|
|
2123
|
+
esac
|
|
2124
|
+
printf 'state=delivered\ndelivered_at=2026-08-21T00:00:00Z\n' >> "$file"
|
|
2125
|
+
expect_failure "delivered pre-change record without context is un-rechainable" \
|
|
2126
|
+
run_pf "$home" rechain pf-new --from pf-legacy --work-home main --work-id work-next --expected pr-merged
|
|
2127
|
+
assert_contains "$EXPECT_OUT" "un-rechainable" "missing request_context_b64 must be named"
|
|
2128
|
+
pass "pre-change registrations are open loops and un-rechainable, never a crash"
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
test_x_request_teardown_warns_when_final_unposted() {
|
|
2132
|
+
local home rc
|
|
2133
|
+
home=$(make_home xreq-warn)
|
|
2134
|
+
tasks_in "$home" add linked-task "ship with a legacy Relay request link" --kind ship >/dev/null \
|
|
2135
|
+
|| fail "could not add the legacy-link ship to its home's backlog"
|
|
2136
|
+
tasks_in "$home" start linked-task >/dev/null \
|
|
2137
|
+
|| fail "could not mark the legacy-link ship In flight"
|
|
2138
|
+
fm_write_meta "$home/state/linked-task.meta" \
|
|
2139
|
+
"window=firstmate:fm-linked-task" \
|
|
2140
|
+
"worktree=$home/projects/gone" \
|
|
2141
|
+
"project=$home/projects/sample" \
|
|
2142
|
+
"kind=ship" \
|
|
2143
|
+
"mode=local-only" \
|
|
2144
|
+
"spawn_gen=public-followup-legacy-link" \
|
|
2145
|
+
"x_request=req-legacy-final"
|
|
2146
|
+
rc=0
|
|
2147
|
+
PATH="$home/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
2148
|
+
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
|
|
2149
|
+
FM_CONFIG_OVERRIDE="$home/config" "$TEARDOWN" linked-task \
|
|
2150
|
+
> "$home/td.out" 2> "$home/td.err" || rc=$?
|
|
2151
|
+
[ "$rc" -eq 0 ] || fail "legacy-link warning must not block teardown (rc=$rc)"
|
|
2152
|
+
assert_grep "still carries an unreconciled Relay request link (req-legacy-final) on its task record" "$home/td.err" \
|
|
2153
|
+
"teardown must report the remaining link without claiming the post failed"
|
|
2154
|
+
assert_no_grep "never posted" "$home/td.err" \
|
|
2155
|
+
"a remaining legacy link must not be treated as proof that no post landed"
|
|
2156
|
+
pass "teardown reports an unreconciled legacy Relay link"
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
test_secondmate_promotion_uses_teardown_parent_resolution() {
|
|
2160
|
+
local parent stale child remote_child out
|
|
2161
|
+
parent=$(make_home promote-parent)
|
|
2162
|
+
stale=$(make_home promote-stale-parent)
|
|
2163
|
+
child=$(make_home promote-child relay-off)
|
|
2164
|
+
printf '%s\n' mate > "$child/.fm-secondmate-home"
|
|
2165
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=local\nparent_home=%s\n' \
|
|
2166
|
+
"$stale" > "$child/.fm-secondmate-parent"
|
|
2167
|
+
printf -- '- mate - synthetic (home: %s; scope: synthetic; projects: ; added 2026-08-21)\n' \
|
|
2168
|
+
"$child" > "$parent/data/secondmates.md"
|
|
2169
|
+
fm_write_meta "$parent/state/mate.meta" "kind=secondmate" "home=$child"
|
|
2170
|
+
mkdir -p "$parent/state/public-followup/registry" "$stale/state/public-followup/registry"
|
|
2171
|
+
printf 'obligation_id=pf-valid\nwork_home=secondmate:mate\nwork_id=promote-legacy\nstate=delivered\n' \
|
|
2172
|
+
> "$parent/state/public-followup/registry/pf-valid"
|
|
2173
|
+
printf 'obligation_id=pf-stale\nwork_home=secondmate:mate\nwork_id=promote-conflict\nstate=delivered\n' \
|
|
2174
|
+
> "$stale/state/public-followup/registry/pf-stale"
|
|
2175
|
+
chmod 600 "$parent/state/public-followup/registry/pf-valid" \
|
|
2176
|
+
"$stale/state/public-followup/registry/pf-stale"
|
|
2177
|
+
|
|
2178
|
+
fm_write_meta "$child/state/promote-conflict.meta" \
|
|
2179
|
+
"window=firstmate:fm-promote-conflict" "kind=scout"
|
|
2180
|
+
out=$(PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
2181
|
+
FM_STATE_OVERRIDE="$child/state" FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$parent" \
|
|
2182
|
+
"$PROMOTE" promote-conflict --mode local-only --yolo off 2>&1) \
|
|
2183
|
+
|| fail "promotion must not block on conflicting parent bindings: $out"
|
|
2184
|
+
assert_contains "$out" "promoted promote-conflict to ship" \
|
|
2185
|
+
"parent-resolution trouble must never refuse the kind flip"
|
|
2186
|
+
assert_contains "$out" "could not resolve the consent-holding parent home" \
|
|
2187
|
+
"conflicting live and durable bindings must warn"
|
|
2188
|
+
assert_not_contains "$out" "--from pf-stale" \
|
|
2189
|
+
"a stale durable parent must not produce a rechain hint"
|
|
2190
|
+
|
|
2191
|
+
rm -f "$child/.fm-secondmate-parent"
|
|
2192
|
+
fm_write_meta "$child/state/promote-legacy.meta" \
|
|
2193
|
+
"window=firstmate:fm-promote-legacy" "kind=scout"
|
|
2194
|
+
out=$(PATH="$child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$child" \
|
|
2195
|
+
FM_STATE_OVERRIDE="$child/state" FM_PUBLIC_FOLLOWUP_PRIMARY_HOME="$parent" \
|
|
2196
|
+
"$PROMOTE" promote-legacy --mode local-only --yolo off 2>&1) \
|
|
2197
|
+
|| fail "legacy parent recovery must not block promotion: $out"
|
|
2198
|
+
assert_contains "$out" "next: FM_HOME=" \
|
|
2199
|
+
"a recovered legacy parent must identify the consent-holding home"
|
|
2200
|
+
assert_contains "$out" "--from pf-valid --work-home secondmate:mate --work-id promote-legacy" \
|
|
2201
|
+
"legacy parent recovery must print the rechain hint"
|
|
2202
|
+
|
|
2203
|
+
remote_child=$(make_home promote-remote-child relay-off)
|
|
2204
|
+
printf '%s\n' remote-mate > "$remote_child/.fm-secondmate-home"
|
|
2205
|
+
printf 'schema=fm-secondmate-parent.v1\nroute=remote\nparent_host=remote.example\n' \
|
|
2206
|
+
> "$remote_child/.fm-secondmate-parent"
|
|
2207
|
+
printf 'FMX_PAIRING_TOKEN=child-local-token\n' > "$remote_child/.env"
|
|
2208
|
+
fm_write_meta "$remote_child/state/promote-remote.meta" \
|
|
2209
|
+
"window=firstmate:fm-promote-remote" "kind=scout"
|
|
2210
|
+
out=$(PATH="$remote_child/fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$remote_child" \
|
|
2211
|
+
FM_STATE_OVERRIDE="$remote_child/state" \
|
|
2212
|
+
"$PROMOTE" promote-remote --mode local-only --yolo off 2>&1) \
|
|
2213
|
+
|| fail "a remote parent route must not block promotion: $out"
|
|
2214
|
+
assert_contains "$out" "promoted promote-remote to ship" \
|
|
2215
|
+
"an unresolved remote parent must never refuse the kind flip"
|
|
2216
|
+
assert_contains "$out" "could not resolve the consent-holding parent home" \
|
|
2217
|
+
"a remote route with a local Relay token must warn as teardown does"
|
|
2218
|
+
pass "secondmate promotion matches teardown parent resolution"
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
test_outcome_text_is_bounded_without_corrupting_characters
|
|
2222
|
+
test_restart_e2e_delivers_exactly_once
|
|
2223
|
+
test_duplicate_event_and_replay_are_noops
|
|
2224
|
+
test_invalid_events_are_refused_and_quarantined
|
|
2225
|
+
test_relay_failure_holds_without_false_completion
|
|
2226
|
+
test_dry_run_does_not_close_commitment
|
|
2227
|
+
test_late_receipt_closes_the_exact_attempt_without_reposting
|
|
2228
|
+
test_typed_terminal_clear_only_removes_legacy_link
|
|
2229
|
+
test_interrupted_delivery_refuses_to_repost
|
|
2230
|
+
test_outward_delivery_stays_with_the_owning_home
|
|
2231
|
+
test_delivery_requires_registration_before_posting
|
|
2232
|
+
test_secondmate_teardown_requires_parent_binding
|
|
2233
|
+
test_local_secondmate_seed_publishes_parent_before_identity
|
|
2234
|
+
test_secondmate_teardown_resolves_parent_from_durable_record_when_env_lost
|
|
2235
|
+
test_secondmate_teardown_durable_record_missing_parent_registration_still_refuses
|
|
2236
|
+
test_secondmate_teardown_durable_record_with_unknown_field_succeeds
|
|
2237
|
+
test_secondmate_teardown_rejects_conflicting_live_and_durable_parent_bindings
|
|
2238
|
+
test_secondmate_teardown_rejects_unsafe_durable_parent_records
|
|
2239
|
+
test_secondmate_teardown_rejects_nul_bearing_durable_parent_record
|
|
2240
|
+
test_relay_disabled_unmarked_teardown_skips_public_path
|
|
2241
|
+
test_relay_disabled_parent_allows_marked_child_teardown
|
|
2242
|
+
test_secondmate_parent_binding_matches_literal_id
|
|
2243
|
+
test_traversal_registration_is_refused_before_delivery
|
|
2244
|
+
test_pending_rejects_malformed_listing
|
|
2245
|
+
test_private_context_survives_inbox_cleanup
|
|
2246
|
+
test_cleanup_refuses_while_a_public_reply_is_owed
|
|
2247
|
+
test_relay_disabled_home_pays_nothing
|
|
2248
|
+
test_relay_enabled_empty_state_makes_no_calls
|
|
2249
|
+
test_exhausted_binding_is_not_retried
|
|
2250
|
+
test_relay_poll_stays_inert_and_surfaces_once
|
|
2251
|
+
test_session_start_surfaces_only_when_owed
|
|
2252
|
+
test_typed_records_exclude_raw_public_material
|
|
2253
|
+
test_dropped_baton_now_surfaces_open_loop
|
|
2254
|
+
test_control_registered_followon_is_guarded
|
|
2255
|
+
test_rechain_delivers_second_post_on_same_thread
|
|
2256
|
+
test_rechain_resumes_after_partial_add
|
|
2257
|
+
test_rechain_claims_delivered_source_once
|
|
2258
|
+
test_failed_rechain_retirement_keeps_source_claimed
|
|
2259
|
+
test_registration_replay_preserves_delivery_and_retirement
|
|
2260
|
+
test_redelivery_does_not_report_retired_loop_open
|
|
2261
|
+
test_retire_after_secondmate_home_removal
|
|
2262
|
+
test_retire_refuses_unbound_existing_secondmate
|
|
2263
|
+
test_retire_refuses_reassigned_secondmate_home
|
|
2264
|
+
test_rechain_refuses_unclaimed_existing_destination
|
|
2265
|
+
test_pending_skips_concurrent_retirement
|
|
2266
|
+
test_retire_reason_closes_the_open_loop
|
|
2267
|
+
test_retention_creates_no_false_teardown_refusal
|
|
2268
|
+
test_expiry_escalation_uses_now_override
|
|
2269
|
+
test_brief_fails_without_typed_deliverable_keys
|
|
2270
|
+
test_prechange_registration_is_open_and_unrechainable
|
|
2271
|
+
test_x_request_teardown_warns_when_final_unposted
|
|
2272
|
+
test_secondmate_promotion_uses_teardown_parent_resolution
|