@zerwiz/ymir 0.1.4 → 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 -59
- 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,1000 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared config resolution for the X-mode connector client (fm-x-poll.sh and
|
|
3
|
+
# fm-x-reply.sh). X mode is opt-in: a user drops a non-empty FMX_PAIRING_TOKEN
|
|
4
|
+
# into the firstmate home's .env. FMX_ENV_FILE can point direct client calls at
|
|
5
|
+
# another .env-style file, but bootstrap activation still checks $FM_HOME/.env.
|
|
6
|
+
# Until then polling is a hard no-op; replies can still run in FMX_DRY_RUN
|
|
7
|
+
# preview mode without a token.
|
|
8
|
+
#
|
|
9
|
+
# This file is sourced, never executed. It defines:
|
|
10
|
+
# fmx_env_get <key> <file> - read one KEY=VALUE from a .env-style file
|
|
11
|
+
# fmx_load_config - resolve FMX_TOKEN, FMX_RELAY, FMX_DRY, FMX_MAX,
|
|
12
|
+
# and FMX_THREAD_MAX (env wins over .env)
|
|
13
|
+
# fmx_auth_header_file - write the bearer header to a 0600 temp file
|
|
14
|
+
# fmx_extract_reply_context <json-file> - the single owner of reply-context
|
|
15
|
+
# extraction: infer {platform, reply_max_chars}
|
|
16
|
+
# from any mention/relay payload file
|
|
17
|
+
# fmx_request_inbox_context <state> <request_id> - reply context from a stashed
|
|
18
|
+
# mention payload (wrapper over the extractor)
|
|
19
|
+
# fmx_request_relay_context <request_id> - resolve reply platform/limit
|
|
20
|
+
# AUTHORITATIVELY from the relay by request_id when
|
|
21
|
+
# no local inbox payload survives
|
|
22
|
+
# fmx_context_registry_set <state> <request_id> <platform> <reply-max> [refresh]
|
|
23
|
+
# - persist the durable per-request reply context;
|
|
24
|
+
# refresh=1 resets its retention timestamp
|
|
25
|
+
# fmx_offer_registry_claim <state> <request_id> - atomically claim the durable
|
|
26
|
+
# one-wake offer marker; 0=new, 1=existing, 2=error
|
|
27
|
+
# fmx_context_registry_prune <state> - remove records older than seven days
|
|
28
|
+
# fmx_context_registry_get <state> <request_id> - read the durable per-request
|
|
29
|
+
# reply context, or the empty shape when absent
|
|
30
|
+
# fmx_context_registry_clear <state> <request_id> - drop the durable record
|
|
31
|
+
# fmx_resolve_reply_context <state> <request_id> <allow-relay> - resolve reply
|
|
32
|
+
# context through registry -> inbox -> relay
|
|
33
|
+
# fmx_reply_limit_for_platform <platform> <explicit-limit> - pick split budget
|
|
34
|
+
# fmx_split_thread <max> <cap> - split a reply (stdin) into a numbered thread
|
|
35
|
+
# fmx_image_payload_file <path> <client> <payload-file> - encode one image
|
|
36
|
+
# attachment to a JSON file and print preview JSON
|
|
37
|
+
# fmx_reply_payload_json <request_id> <chunks> <n> [image-json-file]
|
|
38
|
+
# - build the answer/followup POST body
|
|
39
|
+
# fmx_reply_outbox_json <request_id> <chunks> <n> <followup-0|1> [image-preview-json]
|
|
40
|
+
# - build the dry-run record without image bytes
|
|
41
|
+
# fmx_post_json <endpoint> <payload-file> [body-file] - POST JSON to the relay,
|
|
42
|
+
# printing HTTP code and writing response body
|
|
43
|
+
# fmx_meta_get <meta> <key> - read one key=value line from a task meta file
|
|
44
|
+
# fmx_meta_link_set <meta> <request_id> <epoch> [followups] [platform] [max]
|
|
45
|
+
# - (re)write the X-request link, defaulting
|
|
46
|
+
# followups to 0
|
|
47
|
+
# fmx_meta_followups_set <meta> <n> - rewrite just the follow-up counter
|
|
48
|
+
# fmx_meta_link_clear <meta> - remove the X-request link entirely
|
|
49
|
+
# Callers must have FM_HOME set before calling fmx_load_config.
|
|
50
|
+
|
|
51
|
+
_FM_X_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
52
|
+
if ! command -v fm_backlog_atomic_transition >/dev/null 2>&1; then
|
|
53
|
+
# shellcheck source=bin/fm-tasks-axi-lib.sh
|
|
54
|
+
. "$_FM_X_LIB_DIR/fm-tasks-axi-lib.sh"
|
|
55
|
+
# shellcheck source=bin/fm-backlog-transition-lib.sh
|
|
56
|
+
. "$_FM_X_LIB_DIR/fm-backlog-transition-lib.sh"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Read the value of KEY from a .env-style file: last assignment wins; tolerates a
|
|
60
|
+
# leading "export ", surrounding whitespace, and one layer of matching single or
|
|
61
|
+
# double quotes. Prints nothing (and succeeds) when the file or key is absent, so
|
|
62
|
+
# callers can treat empty output as "unset".
|
|
63
|
+
fmx_env_get() {
|
|
64
|
+
local key=$1 file=$2 line val
|
|
65
|
+
[ -f "$file" ] || return 0
|
|
66
|
+
line=$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file" 2>/dev/null | tail -n1) || return 0
|
|
67
|
+
[ -n "$line" ] || return 0
|
|
68
|
+
val=${line#*=}
|
|
69
|
+
val=${val#"${val%%[![:space:]]*}"} # strip leading whitespace
|
|
70
|
+
val=${val%"${val##*[![:space:]]}"} # strip trailing whitespace (incl. CR)
|
|
71
|
+
case "$val" in
|
|
72
|
+
\"*\") val=${val#\"}; val=${val%\"} ;;
|
|
73
|
+
\'*\') val=${val#\'}; val=${val%\'} ;;
|
|
74
|
+
esac
|
|
75
|
+
printf '%s' "$val"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fmx_poll_shim_content() {
|
|
79
|
+
local home=$1 root=$2
|
|
80
|
+
printf '%s\n' \
|
|
81
|
+
'#!/usr/bin/env bash' \
|
|
82
|
+
'# Auto-generated by fm-bootstrap.sh - X mode connector poll shim.' \
|
|
83
|
+
'# The watcher validates these bytes, then dispatches the trusted poll script.' \
|
|
84
|
+
"export FM_HOME=$(printf '%q' "$home")" \
|
|
85
|
+
"exec $(printf '%q' "$root/bin/fm-x-poll.sh")"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fmx_single_link_file_valid() {
|
|
89
|
+
local file=$1 expected_device=${2-} links device
|
|
90
|
+
[ -f "$file" ] && [ ! -L "$file" ] || return 1
|
|
91
|
+
if [ "$(uname)" = Darwin ]; then
|
|
92
|
+
links=$(stat -f %l "$file" 2>/dev/null) || return 1
|
|
93
|
+
device=$(stat -f %d "$file" 2>/dev/null) || return 1
|
|
94
|
+
else
|
|
95
|
+
links=$(stat -c %h "$file" 2>/dev/null) || return 1
|
|
96
|
+
device=$(stat -c %d "$file" 2>/dev/null) || return 1
|
|
97
|
+
fi
|
|
98
|
+
[ "$links" = 1 ] || return 1
|
|
99
|
+
[ -z "$expected_device" ] || [ "$device" = "$expected_device" ]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fmx_single_link_file_mode_valid() {
|
|
103
|
+
local file=$1 expected_mode=$2 expected_device=${3-} mode
|
|
104
|
+
fmx_single_link_file_valid "$file" "$expected_device" || return 1
|
|
105
|
+
if [ "$(uname)" = Darwin ]; then
|
|
106
|
+
mode=$(stat -f %Lp "$file" 2>/dev/null) || return 1
|
|
107
|
+
else
|
|
108
|
+
mode=$(stat -c %a "$file" 2>/dev/null) || return 1
|
|
109
|
+
fi
|
|
110
|
+
[ "$mode" = "$expected_mode" ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fmx_private_artifact_dir_device() {
|
|
114
|
+
local dir=$1 mode device
|
|
115
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] || return 1
|
|
116
|
+
if [ "$(uname)" = Darwin ]; then
|
|
117
|
+
mode=$(stat -f %Lp "$dir" 2>/dev/null) || return 1
|
|
118
|
+
device=$(stat -f %d "$dir" 2>/dev/null) || return 1
|
|
119
|
+
else
|
|
120
|
+
mode=$(stat -c %a "$dir" 2>/dev/null) || return 1
|
|
121
|
+
device=$(stat -c %d "$dir" 2>/dev/null) || return 1
|
|
122
|
+
fi
|
|
123
|
+
[ "$mode" = 700 ] || return 1
|
|
124
|
+
printf '%s\n' "$device"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fmx_private_artifact_dir_prepare() {
|
|
128
|
+
local dir=$1 parent
|
|
129
|
+
parent=${dir%/*}
|
|
130
|
+
if [ "$parent" != "$dir" ]; then
|
|
131
|
+
if [ -e "$parent" ] || [ -L "$parent" ]; then
|
|
132
|
+
[ -d "$parent" ] && [ ! -L "$parent" ] || return 1
|
|
133
|
+
else
|
|
134
|
+
(umask 077; mkdir -p "$parent" 2>/dev/null) || return 1
|
|
135
|
+
[ -d "$parent" ] && [ ! -L "$parent" ] || return 1
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
if [ -e "$dir" ] || [ -L "$dir" ]; then
|
|
139
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] || return 1
|
|
140
|
+
else
|
|
141
|
+
(umask 077; mkdir -p "$dir" 2>/dev/null) || return 1
|
|
142
|
+
fi
|
|
143
|
+
fmx_private_artifact_dir_device "$dir"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fmx_private_artifact_publish_stdin() {
|
|
147
|
+
local dir=$1 base=$2 mode=$3 device tmp dest
|
|
148
|
+
case "$base" in
|
|
149
|
+
''|.*|*/*) return 1 ;;
|
|
150
|
+
esac
|
|
151
|
+
case "$mode" in
|
|
152
|
+
600|700) ;;
|
|
153
|
+
*) return 1 ;;
|
|
154
|
+
esac
|
|
155
|
+
device=$(fmx_private_artifact_dir_prepare "$dir") || return 1
|
|
156
|
+
dest="$dir/$base"
|
|
157
|
+
tmp=$(umask 077; mktemp "$dir/.${base}.fm-x.XXXXXX" 2>/dev/null) || return 1
|
|
158
|
+
if ! cat > "$tmp" \
|
|
159
|
+
|| ! chmod "$mode" "$tmp" 2>/dev/null \
|
|
160
|
+
|| ! fmx_single_link_file_mode_valid "$tmp" "$mode" "$device"; then
|
|
161
|
+
rm -f -- "$tmp"
|
|
162
|
+
return 1
|
|
163
|
+
fi
|
|
164
|
+
if { [ -e "$dest" ] || [ -L "$dest" ]; } \
|
|
165
|
+
&& ! fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
|
|
166
|
+
rm -f -- "$tmp"
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
if ! mv -f -- "$tmp" "$dest" 2>/dev/null; then
|
|
170
|
+
rm -f -- "$tmp"
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
if ! fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
|
|
174
|
+
rm -f -- "$dest"
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Publish stdin as a new private artifact without replacing an existing path.
|
|
180
|
+
# The hard-link claim is atomic within the prepared directory, so concurrent
|
|
181
|
+
# callers cannot both create the destination. Returns 0 when this caller created
|
|
182
|
+
# it, 1 when another valid private artifact already owns the path, and 2 on an
|
|
183
|
+
# unsafe path or publication failure.
|
|
184
|
+
fmx_private_artifact_publish_stdin_once() {
|
|
185
|
+
local dir=$1 base=$2 mode=$3 device tmp dest
|
|
186
|
+
case "$base" in
|
|
187
|
+
''|.*|*/*) return 2 ;;
|
|
188
|
+
esac
|
|
189
|
+
case "$mode" in
|
|
190
|
+
600|700) ;;
|
|
191
|
+
*) return 2 ;;
|
|
192
|
+
esac
|
|
193
|
+
device=$(fmx_private_artifact_dir_prepare "$dir") || return 2
|
|
194
|
+
dest="$dir/$base"
|
|
195
|
+
tmp=$(umask 077; mktemp "$dir/.${base}.fm-x.XXXXXX" 2>/dev/null) || return 2
|
|
196
|
+
if ! cat > "$tmp" \
|
|
197
|
+
|| ! chmod "$mode" "$tmp" 2>/dev/null \
|
|
198
|
+
|| ! fmx_single_link_file_mode_valid "$tmp" "$mode" "$device"; then
|
|
199
|
+
rm -f -- "$tmp"
|
|
200
|
+
return 2
|
|
201
|
+
fi
|
|
202
|
+
if ln -- "$tmp" "$dest" 2>/dev/null; then
|
|
203
|
+
rm -f -- "$tmp"
|
|
204
|
+
if fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
|
|
205
|
+
return 0
|
|
206
|
+
fi
|
|
207
|
+
rm -f -- "$dest"
|
|
208
|
+
return 2
|
|
209
|
+
fi
|
|
210
|
+
rm -f -- "$tmp"
|
|
211
|
+
if fmx_single_link_file_mode_valid "$dest" "$mode" "$device"; then
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
return 2
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fmx_private_artifact_file_valid() {
|
|
218
|
+
local dir=$1 base=$2 mode=$3 device
|
|
219
|
+
case "$base" in
|
|
220
|
+
''|.*|*/*) return 1 ;;
|
|
221
|
+
esac
|
|
222
|
+
case "$mode" in
|
|
223
|
+
600|700) ;;
|
|
224
|
+
*) return 1 ;;
|
|
225
|
+
esac
|
|
226
|
+
device=$(fmx_private_artifact_dir_device "$dir") || return 1
|
|
227
|
+
fmx_single_link_file_mode_valid "$dir/$base" "$mode" "$device"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fmx_poll_shim_identity_valid() {
|
|
231
|
+
fmx_single_link_file_mode_valid "$1" "$2" "${3-}"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fmx_poll_shim_private_identity_valid() {
|
|
235
|
+
fmx_poll_shim_identity_valid "$1" 700
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fmx_poll_shim_valid() {
|
|
239
|
+
local file=$1 home=$2 root=$3
|
|
240
|
+
fmx_poll_shim_private_identity_valid "$file" || return 1
|
|
241
|
+
cmp -s "$file" <(fmx_poll_shim_content "$home" "$root")
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Resolve the X-mode settings into FMX_TOKEN, FMX_RELAY, FMX_DRY, FMX_MAX,
|
|
245
|
+
# FMX_DISCORD_MAX, and FMX_THREAD_MAX. An explicit environment variable always
|
|
246
|
+
# wins over the .env file; the relay URL defaults to the production host so a
|
|
247
|
+
# normal user configures only the token. FMX_RELAY has any trailing slash trimmed
|
|
248
|
+
# so callers can append "/connector/..." cleanly.
|
|
249
|
+
# FMX_DRY is set to "1" when FMX_DRY_RUN is a truthy value (anything other than
|
|
250
|
+
# unset/empty/0/false/no/off), and "" otherwise: preview mode, where the client
|
|
251
|
+
# composes a reply but records it instead of posting (see fm-x-reply.sh).
|
|
252
|
+
fmx_load_config() {
|
|
253
|
+
local env_file="${FMX_ENV_FILE:-$FM_HOME/.env}" dry
|
|
254
|
+
if [ -n "${FMX_PAIRING_TOKEN+x}" ]; then
|
|
255
|
+
FMX_TOKEN=${FMX_PAIRING_TOKEN-}
|
|
256
|
+
else
|
|
257
|
+
FMX_TOKEN=$(fmx_env_get FMX_PAIRING_TOKEN "$env_file")
|
|
258
|
+
fi
|
|
259
|
+
if [ -n "${FMX_RELAY_URL+x}" ]; then
|
|
260
|
+
FMX_RELAY=${FMX_RELAY_URL-}
|
|
261
|
+
else
|
|
262
|
+
FMX_RELAY=$(fmx_env_get FMX_RELAY_URL "$env_file")
|
|
263
|
+
fi
|
|
264
|
+
[ -n "$FMX_RELAY" ] || FMX_RELAY="https://myfirstmate.io"
|
|
265
|
+
FMX_RELAY=${FMX_RELAY%/}
|
|
266
|
+
if [ -n "${FMX_DRY_RUN+x}" ]; then
|
|
267
|
+
dry=${FMX_DRY_RUN-}
|
|
268
|
+
else
|
|
269
|
+
dry=$(fmx_env_get FMX_DRY_RUN "$env_file")
|
|
270
|
+
fi
|
|
271
|
+
# shellcheck disable=SC2034 # FMX_DRY is read by callers (fm-x-reply.sh) after sourcing.
|
|
272
|
+
case "$(printf '%s' "$dry" | tr '[:upper:]' '[:lower:]')" in
|
|
273
|
+
''|0|false|no|off) FMX_DRY="" ;;
|
|
274
|
+
*) FMX_DRY=1 ;;
|
|
275
|
+
esac
|
|
276
|
+
|
|
277
|
+
# Per-message character budgets for thread-splitting, and the maximum number
|
|
278
|
+
# of messages in one auto-split thread (anti-spam cap).
|
|
279
|
+
local maxraw discordraw threadraw
|
|
280
|
+
if [ -n "${FMX_X_REPLY_MAX_CHARS+x}" ]; then maxraw=${FMX_X_REPLY_MAX_CHARS-}; else maxraw=$(fmx_env_get FMX_X_REPLY_MAX_CHARS "$env_file"); fi
|
|
281
|
+
case "$maxraw" in ''|*[!0-9]*) maxraw=280 ;; esac
|
|
282
|
+
[ "$maxraw" -ge 50 ] 2>/dev/null || maxraw=50
|
|
283
|
+
# shellcheck disable=SC2034 # FMX_MAX is read by callers (fm-x-reply.sh) after sourcing.
|
|
284
|
+
FMX_MAX=$maxraw
|
|
285
|
+
if [ -n "${FMX_DISCORD_REPLY_MAX_CHARS+x}" ]; then discordraw=${FMX_DISCORD_REPLY_MAX_CHARS-}; else discordraw=$(fmx_env_get FMX_DISCORD_REPLY_MAX_CHARS "$env_file"); fi
|
|
286
|
+
case "$discordraw" in ''|*[!0-9]*) discordraw=1900 ;; esac
|
|
287
|
+
[ "$discordraw" -ge 50 ] 2>/dev/null || discordraw=50
|
|
288
|
+
[ "$discordraw" -le 2000 ] 2>/dev/null || discordraw=1900
|
|
289
|
+
# shellcheck disable=SC2034 # FMX_DISCORD_MAX is read by callers (fm-x-reply.sh) after sourcing.
|
|
290
|
+
FMX_DISCORD_MAX=$discordraw
|
|
291
|
+
if [ -n "${FMX_X_THREAD_MAX+x}" ]; then threadraw=${FMX_X_THREAD_MAX-}; else threadraw=$(fmx_env_get FMX_X_THREAD_MAX "$env_file"); fi
|
|
292
|
+
case "$threadraw" in ''|*[!0-9]*) threadraw=25 ;; esac
|
|
293
|
+
[ "$threadraw" -ge 1 ] 2>/dev/null || threadraw=25
|
|
294
|
+
# shellcheck disable=SC2034 # FMX_THREAD_MAX is read by callers (fm-x-reply.sh) after sourcing.
|
|
295
|
+
FMX_THREAD_MAX=$threadraw
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# fmx_extract_reply_context <json-file>: the SINGLE owner of reply-context
|
|
299
|
+
# extraction. Print {"platform":"...","reply_max_chars":"..."} inferred from a
|
|
300
|
+
# mention/relay payload file. Explicit relay-provided platform/limit fields win;
|
|
301
|
+
# absent those, the legacy tweet_id shape is used ("discord:<channel>:<message>"
|
|
302
|
+
# means Discord, a numeric id means X). Empty fields mean unknown, and callers
|
|
303
|
+
# must default safely. A missing file yields the empty shape. The inbox, relay,
|
|
304
|
+
# and poll paths all feed their payload through this one function so platform
|
|
305
|
+
# inference can never drift between them.
|
|
306
|
+
fmx_extract_reply_context() {
|
|
307
|
+
local file=$1
|
|
308
|
+
if [ ! -f "$file" ]; then
|
|
309
|
+
printf '{"platform":"","reply_max_chars":""}\n'
|
|
310
|
+
return 0
|
|
311
|
+
fi
|
|
312
|
+
jq -c '
|
|
313
|
+
def norm_platform:
|
|
314
|
+
tostring | ascii_downcase
|
|
315
|
+
| if . == "discord" or . == "discordapp" then "discord"
|
|
316
|
+
elif . == "x" or . == "twitter" then "x"
|
|
317
|
+
else "" end;
|
|
318
|
+
def first_string($items):
|
|
319
|
+
[$items[] | select(type == "string" and length > 0)][0] // "";
|
|
320
|
+
def first_limit($items):
|
|
321
|
+
[$items[]
|
|
322
|
+
| select(type == "number" or type == "string")
|
|
323
|
+
| tostring
|
|
324
|
+
| select(test("^[0-9]+$"))][0] // "";
|
|
325
|
+
(first_string([.reply_platform, .platform, .target_platform, .source_platform, .provider]) | norm_platform) as $explicit_platform
|
|
326
|
+
| ((.tweet_id // "") | tostring) as $tweet_id
|
|
327
|
+
| {
|
|
328
|
+
platform: (if $explicit_platform != "" then $explicit_platform
|
|
329
|
+
elif ($tweet_id | startswith("discord:")) then "discord"
|
|
330
|
+
elif ($tweet_id | test("^[0-9]+$")) then "x"
|
|
331
|
+
else "" end),
|
|
332
|
+
reply_max_chars: first_limit([.reply_max_chars, .reply_max_characters, .message_max_chars, .message_limit, .max_chars])
|
|
333
|
+
}
|
|
334
|
+
' "$file"
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# fmx_request_inbox_context <state> <request_id>: reply context from a stashed
|
|
338
|
+
# mention payload (state/x-inbox/<request_id>.json), or the empty shape when the
|
|
339
|
+
# inbox file is absent. Thin wrapper over fmx_extract_reply_context.
|
|
340
|
+
fmx_request_inbox_context() {
|
|
341
|
+
local state=$1 rid=$2
|
|
342
|
+
if ! fmx_private_artifact_file_valid "$state/x-inbox" "$rid.json" 600; then
|
|
343
|
+
printf '{"platform":"","reply_max_chars":""}\n'
|
|
344
|
+
return 0
|
|
345
|
+
fi
|
|
346
|
+
fmx_extract_reply_context "$state/x-inbox/$rid.json"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# fmx_request_relay_context <request_id>: resolve the reply platform/limit
|
|
350
|
+
# AUTHORITATIVELY from the relay by request_id when local per-request registry or
|
|
351
|
+
# inbox context is missing an axis. The request_id is the durable key the relay
|
|
352
|
+
# still holds within the follow-up window, so live follow-ups can recover missing
|
|
353
|
+
# context without using a local platform or budget default.
|
|
354
|
+
#
|
|
355
|
+
# POSTs {request_id} to $RELAY/connector/request-context and prints
|
|
356
|
+
# {"platform":"...","reply_max_chars":"..."} in the SAME shape as
|
|
357
|
+
# fmx_request_inbox_context, so callers feed both through the identical
|
|
358
|
+
# normalization and fmx_reply_limit_for_platform path.
|
|
359
|
+
#
|
|
360
|
+
# Best-effort by design: it prints the empty-context shape and returns non-zero
|
|
361
|
+
# when the query cannot run (no token, no curl/jq) or the relay does not resolve
|
|
362
|
+
# it (non-2xx - e.g. an older relay without this endpoint, or a request already
|
|
363
|
+
# swept past its window). Callers must treat that as "unknown" and warn loudly
|
|
364
|
+
# rather than silently defaulting to the X budget. Requires fmx_load_config to
|
|
365
|
+
# have populated FMX_TOKEN and FMX_RELAY first.
|
|
366
|
+
fmx_request_relay_context() {
|
|
367
|
+
local rid=$1 payload_file body_file code rc ctx empty='{"platform":"","reply_max_chars":""}'
|
|
368
|
+
[ -n "${FMX_TOKEN:-}" ] || { printf '%s\n' "$empty"; return 1; }
|
|
369
|
+
command -v curl >/dev/null 2>&1 || { printf '%s\n' "$empty"; return 1; }
|
|
370
|
+
command -v jq >/dev/null 2>&1 || { printf '%s\n' "$empty"; return 1; }
|
|
371
|
+
payload_file=$(mktemp "${TMPDIR:-/tmp}/fm-x-reqctx.XXXXXX") || { printf '%s\n' "$empty"; return 1; }
|
|
372
|
+
body_file=$(mktemp "${TMPDIR:-/tmp}/fm-x-reqctx-body.XXXXXX") || { rm -f "$payload_file"; printf '%s\n' "$empty"; return 1; }
|
|
373
|
+
if ! jq -cn --arg rid "$rid" '{request_id:$rid}' > "$payload_file" 2>/dev/null; then
|
|
374
|
+
rm -f "$payload_file" "$body_file"; printf '%s\n' "$empty"; return 1
|
|
375
|
+
fi
|
|
376
|
+
code=$(fmx_post_json request-context "$payload_file" "$body_file"); rc=$?
|
|
377
|
+
rm -f "$payload_file"
|
|
378
|
+
if [ "$rc" != 0 ]; then rm -f "$body_file"; printf '%s\n' "$empty"; return 1; fi
|
|
379
|
+
case "$code" in
|
|
380
|
+
2[0-9][0-9]) ;;
|
|
381
|
+
*) rm -f "$body_file"; printf '%s\n' "$empty"; return 1 ;;
|
|
382
|
+
esac
|
|
383
|
+
# Same extraction as the inbox path, so a relay-resolved context and an
|
|
384
|
+
# inbox-resolved one normalize identically.
|
|
385
|
+
ctx=$(fmx_extract_reply_context "$body_file" 2>/dev/null) || ctx=
|
|
386
|
+
rm -f "$body_file"
|
|
387
|
+
[ -n "$ctx" ] || { printf '%s\n' "$empty"; return 1; }
|
|
388
|
+
# A 200 that resolved neither a platform nor a limit is treated as unresolved so
|
|
389
|
+
# the caller warns instead of recording a link with no split budget.
|
|
390
|
+
if [ "$(printf '%s' "$ctx" | jq -r '.platform // ""')" = "" ] \
|
|
391
|
+
&& [ "$(printf '%s' "$ctx" | jq -r '.reply_max_chars // ""')" = "" ]; then
|
|
392
|
+
printf '%s\n' "$empty"; return 1
|
|
393
|
+
fi
|
|
394
|
+
printf '%s\n' "$ctx"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# --- durable per-request reply-context registry (state/x-context/<rid>.json) ---
|
|
398
|
+
#
|
|
399
|
+
# A single x_request per task collides across concurrent public requests routed
|
|
400
|
+
# through one persistent secondmate: linking request B onto a task overwrites
|
|
401
|
+
# request A's recorded platform/budget, so A's later follow-up loses its context.
|
|
402
|
+
# And the inbox payload is drained right after the acknowledgement, so a delayed
|
|
403
|
+
# request-id follow-up has no local platform source at all. This registry is the
|
|
404
|
+
# durable fix: one small JSON per request_id, keyed independently of any task
|
|
405
|
+
# link, written at poll time from the authoritative relay payload. It survives
|
|
406
|
+
# inbox cleanup, process restart, and concurrent requests, so
|
|
407
|
+
# fmx_resolve_reply_context can always recover the ORIGINAL platform/budget for a
|
|
408
|
+
# request without depending on task-link state. Entries are volatile runtime
|
|
409
|
+
# state under state/ and are pruned after the relay's 7-day follow-up window.
|
|
410
|
+
|
|
411
|
+
fmx_context_registry_mtime() {
|
|
412
|
+
local file=$1 mtime
|
|
413
|
+
mtime=$(stat -f '%m' "$file" 2>/dev/null) || mtime=$(stat -c '%Y' "$file" 2>/dev/null) || return 1
|
|
414
|
+
case "$mtime" in
|
|
415
|
+
''|*[!0-9]*) return 1 ;;
|
|
416
|
+
esac
|
|
417
|
+
printf '%s\n' "$mtime"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fmx_context_registry_recorded_at() {
|
|
421
|
+
local file=$1 now=${2:-} recorded_at
|
|
422
|
+
recorded_at=$(jq -r '
|
|
423
|
+
.recorded_at
|
|
424
|
+
| if type == "number" and floor == . and . >= 0 then tostring
|
|
425
|
+
elif type == "string" and test("^[0-9]+$") then .
|
|
426
|
+
else "" end
|
|
427
|
+
' "$file" 2>/dev/null) || recorded_at=
|
|
428
|
+
case "$recorded_at" in
|
|
429
|
+
''|*[!0-9]*) recorded_at= ;;
|
|
430
|
+
esac
|
|
431
|
+
[ "${#recorded_at}" -le 18 ] || recorded_at=
|
|
432
|
+
if [ -n "$recorded_at" ] && [ -n "$now" ] && [ "$recorded_at" -gt "$now" ]; then
|
|
433
|
+
recorded_at=
|
|
434
|
+
fi
|
|
435
|
+
if [ -z "$recorded_at" ]; then
|
|
436
|
+
recorded_at=$(fmx_context_registry_mtime "$file") || return 1
|
|
437
|
+
if [ -n "$now" ] && [ "$recorded_at" -gt "$now" ]; then
|
|
438
|
+
return 1
|
|
439
|
+
fi
|
|
440
|
+
fi
|
|
441
|
+
printf '%s\n' "$recorded_at"
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fmx_context_registry_prune() {
|
|
445
|
+
local state=$1 dir now max_age file recorded_at age dir_device
|
|
446
|
+
dir="$state/x-context"
|
|
447
|
+
dir_device=$(fmx_private_artifact_dir_device "$dir" 2>/dev/null) || return 0
|
|
448
|
+
now=${FMX_NOW_OVERRIDE:-$(date +%s)}
|
|
449
|
+
case "$now" in
|
|
450
|
+
''|*[!0-9]*) return 0 ;;
|
|
451
|
+
esac
|
|
452
|
+
[ "${#now}" -le 18 ] || return 0
|
|
453
|
+
max_age=${FMX_FOLLOWUP_MAX_AGE_SECS:-604800}
|
|
454
|
+
case "$max_age" in
|
|
455
|
+
''|*[!0-9]*) max_age=604800 ;;
|
|
456
|
+
esac
|
|
457
|
+
[ "${#max_age}" -le 18 ] || max_age=604800
|
|
458
|
+
[ "$max_age" -le 604800 ] || max_age=604800
|
|
459
|
+
while IFS= read -r -d '' file; do
|
|
460
|
+
if ! fmx_single_link_file_mode_valid "$file" 600 "$dir_device"; then
|
|
461
|
+
rm -f -- "$file" 2>/dev/null || true
|
|
462
|
+
continue
|
|
463
|
+
fi
|
|
464
|
+
if ! recorded_at=$(fmx_context_registry_recorded_at "$file" "$now"); then
|
|
465
|
+
rm -f -- "$file" 2>/dev/null || true
|
|
466
|
+
continue
|
|
467
|
+
fi
|
|
468
|
+
age=$((10#$now - 10#$recorded_at))
|
|
469
|
+
if [ "$age" -gt "$max_age" ]; then
|
|
470
|
+
rm -f -- "$file" 2>/dev/null || true
|
|
471
|
+
fi
|
|
472
|
+
done < <(find "$dir" -type f -name '*.json' -print0 2>/dev/null)
|
|
473
|
+
return 0
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# fmx_context_registry_set <state> <request_id> <platform> <reply-max> [refresh]:
|
|
477
|
+
# persist the durable per-request reply context atomically. Normalizes platform
|
|
478
|
+
# (twitter -> x, anything unrecognized -> empty) and requires a numeric budget.
|
|
479
|
+
# A refresh value of 1 resets the retention timestamp; ordinary writes preserve
|
|
480
|
+
# it. A no-op (success) when neither a platform nor a budget is known, so callers
|
|
481
|
+
# never write an empty, useless record. Returns non-zero only on invalid input or
|
|
482
|
+
# a write failure; callers treat the write as best-effort.
|
|
483
|
+
fmx_context_registry_set() {
|
|
484
|
+
local state=$1 rid=$2 platform=${3:-} reply_max=${4:-} refresh=${5:-0} dir file dir_device now recorded_at
|
|
485
|
+
case "$rid" in
|
|
486
|
+
''|.*|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
487
|
+
esac
|
|
488
|
+
case "$platform" in
|
|
489
|
+
discord|x) ;;
|
|
490
|
+
twitter) platform=x ;;
|
|
491
|
+
*) platform= ;;
|
|
492
|
+
esac
|
|
493
|
+
case "$reply_max" in
|
|
494
|
+
''|*[!0-9]*) reply_max= ;;
|
|
495
|
+
esac
|
|
496
|
+
case "$refresh" in
|
|
497
|
+
0|1) ;;
|
|
498
|
+
*) return 1 ;;
|
|
499
|
+
esac
|
|
500
|
+
if [ -z "$platform" ] && [ -z "$reply_max" ]; then
|
|
501
|
+
return 0
|
|
502
|
+
fi
|
|
503
|
+
dir="$state/x-context"
|
|
504
|
+
dir_device=$(fmx_private_artifact_dir_prepare "$dir") || return 1
|
|
505
|
+
file="$dir/$rid.json"
|
|
506
|
+
if { [ -e "$file" ] || [ -L "$file" ]; } \
|
|
507
|
+
&& ! fmx_single_link_file_mode_valid "$file" 600 "$dir_device"; then
|
|
508
|
+
return 1
|
|
509
|
+
fi
|
|
510
|
+
fmx_context_registry_prune "$state"
|
|
511
|
+
now=${FMX_NOW_OVERRIDE:-$(date +%s)}
|
|
512
|
+
case "$now" in
|
|
513
|
+
''|*[!0-9]*) return 1 ;;
|
|
514
|
+
esac
|
|
515
|
+
[ "${#now}" -le 18 ] || return 1
|
|
516
|
+
recorded_at=
|
|
517
|
+
if [ "$refresh" = 0 ] && [ -f "$file" ]; then
|
|
518
|
+
recorded_at=$(fmx_context_registry_recorded_at "$file" "$now") || recorded_at=
|
|
519
|
+
fi
|
|
520
|
+
if [ -z "$recorded_at" ]; then
|
|
521
|
+
recorded_at=$now
|
|
522
|
+
fi
|
|
523
|
+
(set -o pipefail; jq -cn --arg rid "$rid" --arg platform "$platform" --arg max "$reply_max" \
|
|
524
|
+
--argjson recorded_at "$recorded_at" \
|
|
525
|
+
'{request_id:$rid, platform:$platform, reply_max_chars:$max, recorded_at:$recorded_at}' \
|
|
526
|
+
| fmx_private_artifact_publish_stdin "$dir" "$rid.json" 600) || return 1
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# fmx_offer_registry_claim <state> <request_id>: atomically claim the durable
|
|
530
|
+
# one-wake marker at state/x-context/<request_id>.offered.json. The marker uses
|
|
531
|
+
# the context registry's recorded_at retention contract, so its first claim
|
|
532
|
+
# survives inbox cleanup and expires with the relay's bounded follow-up window.
|
|
533
|
+
# Returns 0 only to the caller that created the marker, 1 when a valid marker
|
|
534
|
+
# already exists, and 2 on invalid input or a publication failure.
|
|
535
|
+
fmx_offer_registry_claim() {
|
|
536
|
+
local state=$1 rid=$2 dir now record rc
|
|
537
|
+
case "$rid" in
|
|
538
|
+
''|.*|*[!A-Za-z0-9._-]*) return 2 ;;
|
|
539
|
+
esac
|
|
540
|
+
fmx_context_registry_prune "$state"
|
|
541
|
+
now=${FMX_NOW_OVERRIDE:-$(date +%s)}
|
|
542
|
+
case "$now" in
|
|
543
|
+
''|*[!0-9]*) return 2 ;;
|
|
544
|
+
esac
|
|
545
|
+
[ "${#now}" -le 18 ] || return 2
|
|
546
|
+
record=$(jq -cn --arg rid "$rid" --argjson recorded_at "$now" \
|
|
547
|
+
'{request_id:$rid, recorded_at:$recorded_at}') || return 2
|
|
548
|
+
dir="$state/x-context"
|
|
549
|
+
printf '%s\n' "$record" \
|
|
550
|
+
| fmx_private_artifact_publish_stdin_once "$dir" "$rid.offered.json" 600
|
|
551
|
+
rc=$?
|
|
552
|
+
return "$rc"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# fmx_context_registry_get <state> <request_id>: print the durable per-request
|
|
556
|
+
# reply context as {"platform":"...","reply_max_chars":"..."} (the same shape as
|
|
557
|
+
# the inbox and relay extractors), or the empty shape when no record exists.
|
|
558
|
+
fmx_context_registry_get() {
|
|
559
|
+
local state=$1 rid=$2 dir file
|
|
560
|
+
case "$rid" in
|
|
561
|
+
''|.*|*[!A-Za-z0-9._-]*) printf '{"platform":"","reply_max_chars":""}\n'; return 0 ;;
|
|
562
|
+
esac
|
|
563
|
+
dir="$state/x-context"
|
|
564
|
+
file="$dir/$rid.json"
|
|
565
|
+
if ! fmx_private_artifact_file_valid "$dir" "$rid.json" 600; then
|
|
566
|
+
printf '{"platform":"","reply_max_chars":""}\n'
|
|
567
|
+
return 0
|
|
568
|
+
fi
|
|
569
|
+
fmx_context_registry_prune "$state"
|
|
570
|
+
jq -c '{platform:(.platform // ""), reply_max_chars:(.reply_max_chars // "")}' "$file" 2>/dev/null \
|
|
571
|
+
|| printf '{"platform":"","reply_max_chars":""}\n'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# fmx_context_registry_clear <state> <request_id>: drop the durable record.
|
|
575
|
+
# Idempotent and best-effort; a dismiss (no follow-up will ever come) uses it so
|
|
576
|
+
# a skipped mention leaves no stray context behind.
|
|
577
|
+
fmx_context_registry_clear() {
|
|
578
|
+
local state=$1 rid=$2 dir
|
|
579
|
+
case "$rid" in
|
|
580
|
+
''|.*|*[!A-Za-z0-9._-]*) return 0 ;;
|
|
581
|
+
esac
|
|
582
|
+
dir="$state/x-context"
|
|
583
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] || return 0
|
|
584
|
+
rm -f "$dir/$rid.json" 2>/dev/null || true
|
|
585
|
+
return 0
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# fmx_resolve_reply_context <state> <request_id> <allow-relay>: resolve the reply
|
|
589
|
+
# platform/budget for a request through the durable sources, in order:
|
|
590
|
+
# 1. the per-request context registry (durable, survives inbox cleanup, restart,
|
|
591
|
+
# and concurrent requests - the primary source after this fix);
|
|
592
|
+
# 2. the still-present inbox payload;
|
|
593
|
+
# 3. when <allow-relay> is 1, an AUTHORITATIVE relay lookup by request_id.
|
|
594
|
+
# Prints {"platform":"...","reply_max_chars":"..."}; each axis is filled from
|
|
595
|
+
# the first source that provides it, continuing through later sources until both
|
|
596
|
+
# are present or the sources are exhausted. <allow-relay>
|
|
597
|
+
# must be 0 in dry-run / no-token / no-network contexts; the caller gates it
|
|
598
|
+
# (typically: follow-up + live + token) so the answer path and dry-run stay
|
|
599
|
+
# network-free. Requires fmx_load_config to have run when <allow-relay> is 1.
|
|
600
|
+
fmx_resolve_reply_context() {
|
|
601
|
+
# Bash local accepts p= and m= as explicit empty assignment arguments.
|
|
602
|
+
# shellcheck disable=SC1007
|
|
603
|
+
local state=$1 rid=$2 allow_relay=${3:-0} src ctx source_p source_m p= m=
|
|
604
|
+
for src in registry inbox relay; do
|
|
605
|
+
case "$src" in
|
|
606
|
+
registry) ctx=$(fmx_context_registry_get "$state" "$rid" 2>/dev/null) || ctx= ;;
|
|
607
|
+
inbox) ctx=$(fmx_request_inbox_context "$state" "$rid" 2>/dev/null) || ctx= ;;
|
|
608
|
+
relay)
|
|
609
|
+
[ "$allow_relay" = 1 ] || continue
|
|
610
|
+
ctx=$(fmx_request_relay_context "$rid" 2>/dev/null) || ctx=
|
|
611
|
+
;;
|
|
612
|
+
esac
|
|
613
|
+
[ -n "$ctx" ] || continue
|
|
614
|
+
source_p=$(printf '%s' "$ctx" | jq -r '.platform // ""' 2>/dev/null) || source_p=
|
|
615
|
+
source_m=$(printf '%s' "$ctx" | jq -r '.reply_max_chars // ""' 2>/dev/null) || source_m=
|
|
616
|
+
case "$source_p" in discord|x) [ -n "$p" ] || p=$source_p ;; esac
|
|
617
|
+
case "$source_m" in ''|*[!0-9]*) ;; *) [ -n "$m" ] || m=$source_m ;; esac
|
|
618
|
+
[ -n "$p" ] && [ -n "$m" ] && break
|
|
619
|
+
done
|
|
620
|
+
jq -cn --arg platform "$p" --arg max "$m" \
|
|
621
|
+
'{platform:$platform, reply_max_chars:$max}'
|
|
622
|
+
return 0
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# fmx_reply_limit_for_platform <platform> <explicit-limit>: choose the split
|
|
626
|
+
# budget for one outbound message. X keeps the existing FMX_X_REPLY_MAX_CHARS
|
|
627
|
+
# default of 280. Discord uses 1900 by default, below Discord's 2000-character
|
|
628
|
+
# message limit so relay/client metadata or small counting differences have
|
|
629
|
+
# headroom. A relay-provided explicit limit is honored when present.
|
|
630
|
+
fmx_reply_limit_for_platform() {
|
|
631
|
+
local platform=${1:-} explicit=${2:-}
|
|
632
|
+
case "$explicit" in
|
|
633
|
+
''|*[!0-9]*) ;;
|
|
634
|
+
*) [ "$explicit" -ge 50 ] 2>/dev/null && { printf '%s\n' "$explicit"; return 0; } ;;
|
|
635
|
+
esac
|
|
636
|
+
case "$platform" in
|
|
637
|
+
discord) printf '%s\n' "${FMX_DISCORD_MAX:-1900}" ;;
|
|
638
|
+
*) printf '%s\n' "${FMX_MAX:-280}" ;;
|
|
639
|
+
esac
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
# Split a reply into a numbered thread of <=<max>-codepoint chunks, packing first
|
|
643
|
+
# on fenced-code, paragraph, and line boundaries, then on word boundaries, and
|
|
644
|
+
# hard-splitting only a single over-long unit. A reply that already fits in one
|
|
645
|
+
# message is returned as a single UNNUMBERED chunk; longer replies get " (k/n)"
|
|
646
|
+
# suffixes. At most <cap> messages are produced; if the reply would need more,
|
|
647
|
+
# the last kept message is marked with an ellipsis. Reads the reply text on stdin
|
|
648
|
+
# and prints a compact JSON array of chunks. Length is codepoint-based (via jq);
|
|
649
|
+
# the relay remains the final authority and trims.
|
|
650
|
+
fmx_split_thread() {
|
|
651
|
+
jq -Rsc --argjson limit "$1" --argjson cap "$2" '
|
|
652
|
+
def trim: gsub("^[[:space:]]+|[[:space:]]+$"; "");
|
|
653
|
+
def fence_marker: test("^[[:space:]]*```");
|
|
654
|
+
def fence_count: ((split("```") | length) - 1);
|
|
655
|
+
def numbered($i; $n):
|
|
656
|
+
"(\($i + 1)/\($n))" as $mark
|
|
657
|
+
| if ((fence_count % 2) == 0) and (split("\n")[-1] | fence_marker)
|
|
658
|
+
then . + "\n" + $mark
|
|
659
|
+
else . + " " + $mark
|
|
660
|
+
end;
|
|
661
|
+
def hardsplit($b): . as $s | [range(0; ($s|length); $b) as $i | $s[$i:$i+$b]];
|
|
662
|
+
def wordsplit($b):
|
|
663
|
+
(gsub("[[:space:]]+"; " ") | trim) as $norm
|
|
664
|
+
| if ($norm | length) == 0 then []
|
|
665
|
+
else
|
|
666
|
+
[ $norm | split(" ")[] | if (length > $b) then hardsplit($b)[] else . end ] as $words
|
|
667
|
+
| (reduce $words[] as $w ({chunks: [], cur: ""};
|
|
668
|
+
(if .cur == "" then $w else .cur + " " + $w end) as $cand
|
|
669
|
+
| if ($cand | length) <= $b then .cur = $cand
|
|
670
|
+
else .chunks += (if .cur == "" then [] else [.cur] end) | .cur = $w end
|
|
671
|
+
)) as $st
|
|
672
|
+
| $st.chunks + (if $st.cur != "" then [$st.cur] else [] end)
|
|
673
|
+
end;
|
|
674
|
+
def split_units:
|
|
675
|
+
split("\n") as $lines
|
|
676
|
+
| (reduce $lines[] as $line ({units: [], cur: "", fence: false};
|
|
677
|
+
if .fence then
|
|
678
|
+
.cur = (if .cur == "" then $line else .cur + "\n" + $line end)
|
|
679
|
+
| if ($line | fence_marker) then .units += [.cur] | .cur = "" | .fence = false else . end
|
|
680
|
+
elif ($line | fence_marker) then
|
|
681
|
+
(if .cur != "" then .units += [.cur] | .cur = "" else . end)
|
|
682
|
+
| .cur = $line
|
|
683
|
+
| .fence = true
|
|
684
|
+
elif ($line | test("^[[:space:]]*$")) then
|
|
685
|
+
if .cur != "" then .units += [.cur] | .cur = "" else . end
|
|
686
|
+
else
|
|
687
|
+
($line | trim) as $clean
|
|
688
|
+
| .cur = (if .cur == "" then $clean else .cur + " " + $clean end)
|
|
689
|
+
end
|
|
690
|
+
)) as $st
|
|
691
|
+
| ($st.units + (if $st.cur != "" then [$st.cur] else [] end))
|
|
692
|
+
| map(select((trim | length) > 0));
|
|
693
|
+
def pack_units($units; $b):
|
|
694
|
+
(reduce $units[] as $u ({chunks: [], cur: ""};
|
|
695
|
+
if ($u | length) > $b then
|
|
696
|
+
(if .cur != "" then .chunks += [.cur] | .cur = "" else . end)
|
|
697
|
+
| .chunks += ($u | wordsplit($b))
|
|
698
|
+
else
|
|
699
|
+
(if .cur == "" then $u else .cur + "\n\n" + $u end) as $cand
|
|
700
|
+
| if ($cand | length) <= $b then .cur = $cand
|
|
701
|
+
else .chunks += (if .cur == "" then [] else [.cur] end) | .cur = $u end
|
|
702
|
+
end
|
|
703
|
+
)) as $st
|
|
704
|
+
| $st.chunks + (if $st.cur != "" then [$st.cur] else [] end);
|
|
705
|
+
def split_thread($limit; $cap):
|
|
706
|
+
trim as $norm
|
|
707
|
+
| if ($norm | length) == 0 then []
|
|
708
|
+
elif ($norm | length) <= $limit then [$norm]
|
|
709
|
+
else
|
|
710
|
+
($cap | tostring | length) as $digits
|
|
711
|
+
| (4 + 2 * $digits) as $suffixw
|
|
712
|
+
| (if ($limit - $suffixw - 1) < 1 then 1 else ($limit - $suffixw - 1) end) as $budget
|
|
713
|
+
| ($norm | split_units) as $units
|
|
714
|
+
| pack_units($units; $budget) as $raw
|
|
715
|
+
| (if ($raw | length) > $cap
|
|
716
|
+
then ($raw[0:$cap] | (.[($cap - 1)] += "…"))
|
|
717
|
+
else $raw end) as $kept
|
|
718
|
+
| ($kept | length) as $n
|
|
719
|
+
| [ range(0; $n) as $i | $kept[$i] | numbered($i; $n) ]
|
|
720
|
+
end;
|
|
721
|
+
split_thread($limit; $cap)
|
|
722
|
+
'
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
fmx_auth_header_file() {
|
|
726
|
+
local file
|
|
727
|
+
case "$FMX_TOKEN" in
|
|
728
|
+
*$'\n'*|*$'\r'*) return 1 ;;
|
|
729
|
+
esac
|
|
730
|
+
file=$(umask 077; mktemp "${TMPDIR:-/tmp}/fm-x-auth.XXXXXX") || return 1
|
|
731
|
+
chmod 600 "$file" 2>/dev/null || { rm -f "$file"; return 1; }
|
|
732
|
+
printf 'Authorization: Bearer %s\n' "$FMX_TOKEN" > "$file" || { rm -f "$file"; return 1; }
|
|
733
|
+
printf '%s\n' "$file"
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
fmx_image_media_type_from_path() {
|
|
737
|
+
local path=$1 lower detected
|
|
738
|
+
lower=$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')
|
|
739
|
+
case "$lower" in
|
|
740
|
+
*.png) printf 'image/png\n' ;;
|
|
741
|
+
*.jpg|*.jpeg) printf 'image/jpeg\n' ;;
|
|
742
|
+
*.gif) printf 'image/gif\n' ;;
|
|
743
|
+
*.webp) printf 'image/webp\n' ;;
|
|
744
|
+
*.bmp) printf 'image/bmp\n' ;;
|
|
745
|
+
*.tif|*.tiff) printf 'image/tiff\n' ;;
|
|
746
|
+
*)
|
|
747
|
+
if command -v file >/dev/null 2>&1; then
|
|
748
|
+
detected=$(file --mime-type -b -- "$path" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
749
|
+
case "$detected" in
|
|
750
|
+
image/png|image/jpeg|image/pjpeg|image/gif|image/webp|image/bmp|image/tiff) printf '%s\n' "$detected" ;;
|
|
751
|
+
*) return 1 ;;
|
|
752
|
+
esac
|
|
753
|
+
else
|
|
754
|
+
return 1
|
|
755
|
+
fi
|
|
756
|
+
;;
|
|
757
|
+
esac
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
# fmx_image_payload_file <path> <client-name> <payload-file>: validate and encode
|
|
761
|
+
# a local outbound image. The relay payload object is written to <payload-file>.
|
|
762
|
+
# The compact preview object is printed for FMX_DRY_RUN outbox records.
|
|
763
|
+
fmx_image_payload_file() {
|
|
764
|
+
local path=$1 client=${2:-fm-x-reply} payload_file=${3:-} media_type bytes
|
|
765
|
+
if [ -z "$payload_file" ]; then
|
|
766
|
+
echo "$client: missing image payload destination" >&2
|
|
767
|
+
return 1
|
|
768
|
+
fi
|
|
769
|
+
if [ ! -e "$path" ]; then
|
|
770
|
+
echo "$client: image file does not exist: $path" >&2
|
|
771
|
+
return 1
|
|
772
|
+
fi
|
|
773
|
+
if [ ! -f "$path" ]; then
|
|
774
|
+
echo "$client: image path is not a regular file: $path" >&2
|
|
775
|
+
return 1
|
|
776
|
+
fi
|
|
777
|
+
if [ ! -r "$path" ]; then
|
|
778
|
+
echo "$client: image file is not readable: $path" >&2
|
|
779
|
+
return 1
|
|
780
|
+
fi
|
|
781
|
+
media_type=$(fmx_image_media_type_from_path "$path") || {
|
|
782
|
+
echo "$client: unsupported image media type for: $path" >&2
|
|
783
|
+
return 1
|
|
784
|
+
}
|
|
785
|
+
command -v base64 >/dev/null 2>&1 || {
|
|
786
|
+
echo "$client: base64 not found" >&2
|
|
787
|
+
return 1
|
|
788
|
+
}
|
|
789
|
+
bytes=$(wc -c < "$path" | tr -d '[:space:]') || {
|
|
790
|
+
echo "$client: cannot stat image file: $path" >&2
|
|
791
|
+
return 1
|
|
792
|
+
}
|
|
793
|
+
if [ "$bytes" = 0 ]; then
|
|
794
|
+
echo "$client: image file is empty: $path" >&2
|
|
795
|
+
return 1
|
|
796
|
+
fi
|
|
797
|
+
if ! (set -o pipefail; base64 < "$path" | tr -d '\n\r' \
|
|
798
|
+
| jq -Rsc --arg media_type "$media_type" \
|
|
799
|
+
'{media_type:$media_type,data_base64:.}' > "$payload_file"); then
|
|
800
|
+
rm -f "$payload_file"
|
|
801
|
+
echo "$client: cannot read image file: $path" >&2
|
|
802
|
+
return 1
|
|
803
|
+
fi
|
|
804
|
+
jq -cn \
|
|
805
|
+
--arg media_type "$media_type" \
|
|
806
|
+
--arg source_path "$path" \
|
|
807
|
+
--argjson bytes "$bytes" \
|
|
808
|
+
'{media_type:$media_type,bytes:$bytes,source_path:$source_path}'
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
fmx_reply_payload_json() {
|
|
812
|
+
local rid=$1 chunks=$2 n=$3 image_json_file=${4:-}
|
|
813
|
+
if [ -n "$image_json_file" ]; then
|
|
814
|
+
if [ "$n" -le 1 ]; then
|
|
815
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" --slurpfile image "$image_json_file" \
|
|
816
|
+
'{request_id:$rid, text:(.[0] // ""), image:$image[0]}'
|
|
817
|
+
else
|
|
818
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" --slurpfile image "$image_json_file" \
|
|
819
|
+
'{request_id:$rid, text:.[0], texts:., image:$image[0]}'
|
|
820
|
+
fi
|
|
821
|
+
else
|
|
822
|
+
if [ "$n" -le 1 ]; then
|
|
823
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:(.[0] // "")}'
|
|
824
|
+
else
|
|
825
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:.[0], texts:.}'
|
|
826
|
+
fi
|
|
827
|
+
fi
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
fmx_reply_outbox_json() {
|
|
831
|
+
local rid=$1 chunks=$2 n=$3 followup=$4 image_preview_json=${5:-}
|
|
832
|
+
if [ -n "$image_preview_json" ]; then
|
|
833
|
+
if [ "$followup" = 1 ]; then
|
|
834
|
+
if [ "$n" -le 1 ]; then
|
|
835
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
|
|
836
|
+
'{request_id:$rid, text:(.[0] // ""), image:$image, endpoint:"followup"}'
|
|
837
|
+
else
|
|
838
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
|
|
839
|
+
'{request_id:$rid, text:.[0], texts:., image:$image, endpoint:"followup"}'
|
|
840
|
+
fi
|
|
841
|
+
else
|
|
842
|
+
if [ "$n" -le 1 ]; then
|
|
843
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
|
|
844
|
+
'{request_id:$rid, text:(.[0] // ""), image:$image}'
|
|
845
|
+
else
|
|
846
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" --argjson image "$image_preview_json" \
|
|
847
|
+
'{request_id:$rid, text:.[0], texts:., image:$image}'
|
|
848
|
+
fi
|
|
849
|
+
fi
|
|
850
|
+
else
|
|
851
|
+
if [ "$followup" = 1 ]; then
|
|
852
|
+
if [ "$n" -le 1 ]; then
|
|
853
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" \
|
|
854
|
+
'{request_id:$rid, text:(.[0] // ""), endpoint:"followup"}'
|
|
855
|
+
else
|
|
856
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" \
|
|
857
|
+
'{request_id:$rid, text:.[0], texts:., endpoint:"followup"}'
|
|
858
|
+
fi
|
|
859
|
+
else
|
|
860
|
+
if [ "$n" -le 1 ]; then
|
|
861
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:(.[0] // "")}'
|
|
862
|
+
else
|
|
863
|
+
printf '%s' "$chunks" | jq -c --arg rid "$rid" '{request_id:$rid, text:.[0], texts:.}'
|
|
864
|
+
fi
|
|
865
|
+
fi
|
|
866
|
+
fi
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
fmx_post_json() (
|
|
870
|
+
local endpoint=$1 payload_file=$2 body_file=${3:-/dev/null} auth_header_file code rc
|
|
871
|
+
command -v curl >/dev/null 2>&1 || return 127
|
|
872
|
+
[ -r "$payload_file" ] || return 2
|
|
873
|
+
auth_header_file=$(fmx_auth_header_file) || return 3
|
|
874
|
+
trap 'rm -f "$auth_header_file"' EXIT
|
|
875
|
+
trap 'rm -f "$auth_header_file"; exit 143' HUP INT TERM
|
|
876
|
+
code=$(curl -m 10 -s -o "$body_file" -w '%{http_code}' \
|
|
877
|
+
-X POST \
|
|
878
|
+
-H "@$auth_header_file" \
|
|
879
|
+
-H 'Content-Type: application/json' \
|
|
880
|
+
--data-binary "@$payload_file" \
|
|
881
|
+
"$FMX_RELAY/connector/$endpoint" 2>/dev/null)
|
|
882
|
+
rc=$?
|
|
883
|
+
rm -f "$auth_header_file"
|
|
884
|
+
trap - EXIT HUP INT TERM
|
|
885
|
+
[ "$rc" = 0 ] || return 4
|
|
886
|
+
printf '%s\n' "$code"
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# --- task <-> X-request link (state/<id>.meta backed) -----------------------
|
|
890
|
+
#
|
|
891
|
+
# When an X/Discord mention spawns real work, the task is linked to its
|
|
892
|
+
# originating mention by state/<id>.meta lines:
|
|
893
|
+
# x_request=<request_id> the relay-issued id the follow-up posts against
|
|
894
|
+
# x_request_ts=<epoch> when the link was made, for the 7-day follow-up window
|
|
895
|
+
# x_followups=<n> follow-ups already posted against this binding (0..3)
|
|
896
|
+
# x_platform=<platform> optional reply platform for follow-up split budget
|
|
897
|
+
# x_reply_max_chars=<n> optional recorded per-message split budget
|
|
898
|
+
# fm-x-followup.sh posts against that link (within the window, up to the cap),
|
|
899
|
+
# then either records the incremented count or clears the link. These helpers
|
|
900
|
+
# own the read/write/clear so fm-x-link.sh and fm-x-followup.sh never hand-edit
|
|
901
|
+
# meta and the rewrite stays atomic and preserves every other meta line.
|
|
902
|
+
|
|
903
|
+
# fmx_meta_get <meta> <key>: print the value of the last "key=value" line in
|
|
904
|
+
# <meta>, or nothing (and succeed) when the file or key is absent. Callers treat
|
|
905
|
+
# empty output as "unset".
|
|
906
|
+
fmx_meta_get() {
|
|
907
|
+
local meta=$1 key=$2 line
|
|
908
|
+
[ -f "$meta" ] || return 0
|
|
909
|
+
line=$(grep -E "^${key}=" "$meta" 2>/dev/null | tail -n1) || return 0
|
|
910
|
+
[ -n "$line" ] || return 0
|
|
911
|
+
printf '%s' "${line#*=}"
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
fmx_meta_tmp() {
|
|
915
|
+
local meta=$1 dir base
|
|
916
|
+
dir=${meta%/*}
|
|
917
|
+
base=${meta##*/}
|
|
918
|
+
[ "$dir" != "$meta" ] || dir=.
|
|
919
|
+
[ -d "$dir" ] || return 1
|
|
920
|
+
mktemp "$dir/.${base}.fm-x.XXXXXX"
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
# fmx_meta_link_set <meta> <request_id> <epoch> [followups] [platform] [max]:
|
|
924
|
+
# atomically (re)write the x_request/x_request_ts/x_followups lines plus optional
|
|
925
|
+
# reply-platform context, dropping any prior link and preserving every other meta
|
|
926
|
+
# line. <followups> defaults to 0 (a fresh link); pass the prior task's count to
|
|
927
|
+
# carry it forward onto a successor task instead of granting a fresh follow-up
|
|
928
|
+
# budget against a binding the relay already knows about. Returns non-zero if
|
|
929
|
+
# <meta> is missing or the rewrite fails.
|
|
930
|
+
fmx_meta_link_set() {
|
|
931
|
+
local meta=$1 rid=$2 ts=$3 followups=${4:-0} platform=${5:-} reply_max=${6:-} tmp lock
|
|
932
|
+
[ -f "$meta" ] || return 1
|
|
933
|
+
lock=$(fm_meta_lock_path "$meta") || return 1
|
|
934
|
+
fm_lock_acquire_wait "$lock"
|
|
935
|
+
[ -f "$meta" ] || { fm_lock_release "$lock"; return 1; }
|
|
936
|
+
tmp=$(fmx_meta_tmp "$meta") || { fm_lock_release "$lock"; return 1; }
|
|
937
|
+
if ! { grep -vE '^x_request=|^x_request_ts=|^x_followups=|^x_platform=|^x_reply_max_chars=' "$meta" || true; } > "$tmp"; then
|
|
938
|
+
rm -f "$tmp"; fm_lock_release "$lock"; return 1
|
|
939
|
+
fi
|
|
940
|
+
printf 'x_request=%s\n' "$rid" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
|
|
941
|
+
printf 'x_request_ts=%s\n' "$ts" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
|
|
942
|
+
printf 'x_followups=%s\n' "$followups" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
|
|
943
|
+
if [ -n "$platform" ]; then
|
|
944
|
+
printf 'x_platform=%s\n' "$platform" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
|
|
945
|
+
fi
|
|
946
|
+
case "$reply_max" in
|
|
947
|
+
''|*[!0-9]*) ;;
|
|
948
|
+
*) printf 'x_reply_max_chars=%s\n' "$reply_max" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; } ;;
|
|
949
|
+
esac
|
|
950
|
+
# STATE is the caller's authorized state directory, never dirname of $meta.
|
|
951
|
+
# shellcheck disable=SC2153
|
|
952
|
+
if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
|
|
953
|
+
rm -f "$tmp"; fm_lock_release "$lock"; return 1
|
|
954
|
+
fi
|
|
955
|
+
fm_lock_release "$lock"
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
# fmx_meta_followups_set <meta> <n>: atomically rewrite just the x_followups
|
|
959
|
+
# line, preserving every other meta line including link and reply context.
|
|
960
|
+
# Returns non-zero if <meta> is missing or the rewrite fails.
|
|
961
|
+
fmx_meta_followups_set() {
|
|
962
|
+
local meta=$1 n=$2 tmp lock
|
|
963
|
+
[ -f "$meta" ] || return 1
|
|
964
|
+
lock=$(fm_meta_lock_path "$meta") || return 1
|
|
965
|
+
fm_lock_acquire_wait "$lock"
|
|
966
|
+
[ -f "$meta" ] || { fm_lock_release "$lock"; return 1; }
|
|
967
|
+
tmp=$(fmx_meta_tmp "$meta") || { fm_lock_release "$lock"; return 1; }
|
|
968
|
+
if ! { grep -vE '^x_followups=' "$meta" || true; } > "$tmp"; then
|
|
969
|
+
rm -f "$tmp"; fm_lock_release "$lock"; return 1
|
|
970
|
+
fi
|
|
971
|
+
printf 'x_followups=%s\n' "$n" >> "$tmp" || { rm -f "$tmp"; fm_lock_release "$lock"; return 1; }
|
|
972
|
+
# shellcheck disable=SC2153
|
|
973
|
+
if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
|
|
974
|
+
rm -f "$tmp"; fm_lock_release "$lock"; return 1
|
|
975
|
+
fi
|
|
976
|
+
fm_lock_release "$lock"
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
# fmx_meta_link_clear <meta>: atomically remove the x_request/x_request_ts/
|
|
980
|
+
# x_followups and reply-platform lines while preserving every other meta line. Idempotent:
|
|
981
|
+
# succeeds whether or not a link is present, and is a no-op when <meta> is
|
|
982
|
+
# missing.
|
|
983
|
+
fmx_meta_link_clear() {
|
|
984
|
+
local meta=$1 tmp lock
|
|
985
|
+
[ ! -L "$meta" ] || return 1
|
|
986
|
+
[ -f "$meta" ] || return 0
|
|
987
|
+
lock=$(fm_meta_lock_path "$meta") || return 1
|
|
988
|
+
fm_lock_acquire_wait "$lock"
|
|
989
|
+
[ ! -L "$meta" ] || { fm_lock_release "$lock"; return 1; }
|
|
990
|
+
[ -f "$meta" ] || { fm_lock_release "$lock"; return 0; }
|
|
991
|
+
tmp=$(fmx_meta_tmp "$meta") || { fm_lock_release "$lock"; return 1; }
|
|
992
|
+
if ! { grep -vE '^x_request=|^x_request_ts=|^x_followups=|^x_platform=|^x_reply_max_chars=' "$meta" || true; } > "$tmp"; then
|
|
993
|
+
rm -f "$tmp"; fm_lock_release "$lock"; return 1
|
|
994
|
+
fi
|
|
995
|
+
# shellcheck disable=SC2153
|
|
996
|
+
if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
|
|
997
|
+
rm -f "$tmp"; fm_lock_release "$lock"; return 1
|
|
998
|
+
fi
|
|
999
|
+
fm_lock_release "$lock"
|
|
1000
|
+
}
|