jackherd 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/INSTALL.md +4 -2
- package/{NOTICE → NOTICE.txt} +1 -1
- package/README.md +94 -1033
- package/assets/icons/bark-dark.svg +1 -1
- package/assets/icons/bark.svg +1 -1
- package/assets/icons/bowl-dark.svg +1 -1
- package/assets/icons/bowl.svg +1 -1
- package/assets/icons/decide-dark.svg +1 -1
- package/assets/icons/decide.svg +1 -1
- package/assets/icons/ear-dark.svg +1 -1
- package/assets/icons/ear.svg +1 -1
- package/assets/icons/lanes-dark.svg +1 -1
- package/assets/icons/lanes.svg +1 -1
- package/assets/icons/plan-dark.svg +1 -1
- package/assets/icons/plan.svg +1 -1
- package/dist/{chunk-OBUWTQ6Z.js → chunk-M6DDZEW4.js} +24 -10
- package/dist/config.d.ts +11 -5
- package/dist/config.js +53 -21
- package/dist/plan-format.d.ts +73 -7
- package/dist/plan-format.js +180 -100
- package/docs/workflow-dark.svg +1 -1
- package/docs/workflow.svg +1 -1
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/herdr-panes.ts +2 -12
- package/kit/hooks/hooks/approval-gate/index.ts +10 -8
- package/kit/hooks/hooks/approval-gate/run.ts +21 -12
- package/kit/hooks/hooks/ask-hint/decision.ts +3 -3
- package/kit/hooks/hooks/git-guard/decision.ts +189 -15
- package/kit/hooks/hooks/git-guard/index.ts +13 -3
- package/kit/hooks/hooks/git-guard/push-target.ts +85 -0
- package/kit/hooks/hooks/git-guard/repository.ts +63 -0
- package/kit/hooks/hooks/merge-guard/check-free.ts +1 -13
- package/kit/hooks/hooks/merge-guard/decision.ts +342 -102
- package/kit/hooks/hooks/merge-guard/index.ts +6 -4
- package/kit/hooks/hooks/merge-guard/plain-path.ts +15 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +23 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-settings.ts +142 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +196 -0
- package/kit/hooks/hooks/merge-guard/plan-approval.ts +168 -0
- package/kit/hooks/hooks/merge-guard/plan-growth.ts +40 -0
- package/kit/hooks/hooks/merge-guard/plan-pointers.ts +194 -0
- package/kit/hooks/hooks/merge-guard/pull-request.ts +67 -21
- package/kit/hooks/hooks/merge-guard/run.ts +118 -29
- package/kit/hooks/hooks/merge-guard/settings.ts +8 -8
- package/kit/hooks/hooks/merge-guard/worker.ts +3 -3
- package/kit/hooks/hooks/shared/base-branch.ts +23 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +93 -0
- package/kit/hooks/hooks/shared/command-text-runners.ts +71 -0
- package/kit/hooks/hooks/shared/command.ts +14 -2
- package/kit/hooks/hooks/shared/commit.ts +2 -0
- package/kit/hooks/hooks/shared/dialog-answers.ts +4 -1
- package/kit/hooks/hooks/shared/json.ts +10 -0
- package/kit/hooks/hooks/shared/lane.ts +57 -0
- package/kit/hooks/hooks/shared/once.ts +8 -0
- package/kit/hooks/hooks/shared/own-pane.ts +31 -0
- package/kit/hooks/hooks/shared/pane-agent.ts +24 -0
- package/kit/hooks/hooks/shared/plan-open-phases.ts +114 -0
- package/kit/hooks/hooks/shared/program-position.ts +148 -5
- package/kit/hooks/hooks/shared/shell-aliases.ts +258 -0
- package/kit/hooks/hooks/shared/shell-snapshots.ts +68 -0
- package/kit/hooks/hooks/shared/shell.ts +167 -20
- package/kit/hooks/hooks.json +11 -2
- package/kit/hooks/statusline.sh +29 -16
- package/kit/review/skills/diagnose/SKILL.md +1 -1
- package/kit/review/skills/review/SKILL.md +2 -0
- package/kit/rules/rules/kit/testing.md +2 -5
- package/kit/rules/rules/kit/writing-for-agents.md +3 -1
- package/kit/workflow/agents/spec-review.md +1 -1
- package/kit/workflow/skills/follow/SKILL.md +3 -3
- package/kit/workflow/skills/grill/SKILL.md +1 -1
- package/kit/workflow/skills/implement/SKILL.md +10 -11
- package/kit/workflow/skills/orchestrate/SKILL.md +155 -117
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +17 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +27 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -0
- package/kit/workflow/skills/orchestrate/references/sensitive-input.md +23 -0
- package/kit/workflow/skills/plan/SKILL.md +5 -2
- package/kit/workflow/skills/plan/references/template-English.md +6 -0
- package/kit/workflow/skills/plan/references/template-German.md +6 -0
- package/package.json +3 -2
- package/recipes/README.md +21 -0
- package/recipes/github-issues.md +23 -0
- package/recipes/github-projects.md +22 -0
- package/recipes/jira-cli.md +26 -0
- package/recipes/jira-rest.md +24 -0
- package/recipes/linear.md +25 -0
- package/recipes/sentry.md +18 -0
- package/recipes/sources/jira.ts +51 -0
- package/recipes/sources/linear.ts +19 -0
- package/recipes/sources/sentry.ts +12 -0
- package/recipes/sources.md +80 -0
- package/recipes/status-page.md +19 -0
- package/schemas/jackherd.schema.json +42 -18
- package/src/check/command.ts +2 -2
- package/src/check/parallel-report.ts +19 -9
- package/src/cli.ts +1 -0
- package/src/config/context-shares.ts +130 -0
- package/src/config/legacy-names.ts +8 -0
- package/src/config/read-config.ts +12 -4
- package/src/config/schema.ts +75 -20
- package/src/context/auto-compact.ts +68 -0
- package/src/context/command.ts +68 -17
- package/src/context/coordinator-name.ts +27 -9
- package/src/context/index.ts +0 -1
- package/src/context/level.ts +18 -11
- package/src/context/report.ts +6 -3
- package/src/plan-format/dependencies.ts +144 -32
- package/src/plan-format/index.ts +6 -0
- package/src/plan-format/lanes.ts +6 -0
- package/src/plan-format/progress.ts +19 -8
- package/src/plans/command.ts +14 -5
- package/src/plans/current-phase.ts +63 -7
- package/src/plans/current-task.ts +51 -0
- package/src/plans/ensure-panes.ts +25 -8
- package/src/plans/frame.ts +99 -0
- package/src/plans/git.ts +8 -0
- package/src/plans/herd-memory.ts +107 -0
- package/src/plans/lane-plans.ts +5 -3
- package/src/plans/lane-summary.ts +222 -0
- package/src/plans/layout-phase.ts +148 -0
- package/src/plans/layout-summary.ts +140 -0
- package/src/plans/layout.ts +164 -420
- package/src/plans/live.ts +11 -6
- package/src/plans/plan-panes.ts +15 -0
- package/src/plans/planned.ts +9 -42
- package/src/plans/selection.ts +2 -6
- package/src/plans/settings.ts +2 -7
- package/src/plans/snapshot.ts +38 -39
- package/src/plans/symbols.ts +2 -2
- package/src/plans/texts.ts +32 -31
- package/src/plans/view.ts +0 -1
- package/src/plans/worked-phases.ts +2 -2
- package/src/settled/board-row.ts +133 -0
- package/src/settled/check.ts +129 -0
- package/src/settled/command.ts +65 -0
- package/src/settled/evidence-version.ts +14 -0
- package/src/settled/options.ts +69 -0
- package/src/settled/send.ts +27 -0
- package/src/settled/settle.ts +87 -0
- package/src/settled/topics.ts +56 -0
- package/src/sync/command.ts +16 -1
- package/src/sync/render.ts +13 -4
- package/src/sync/sync.ts +25 -5
- package/src/terminal/herdr.ts +46 -10
- package/src/terminal/none.ts +1 -0
- package/src/terminal/types.ts +15 -2
- package/src/update/command.ts +4 -2
- package/src/update/update.ts +33 -9
- package/src/vocabulary/index.ts +47 -11
- package/src/watch/appointments.ts +205 -0
- package/src/watch/command.ts +62 -19
- package/src/watch/lane-report.ts +11 -3
- package/src/watch/texts.ts +35 -59
- package/src/watch/watch-events.ts +21 -28
- package/kit/hooks/hooks/approval-gate/lane.ts +0 -14
package/docs/workflow.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="708" viewBox="0 0 960 708" role="img" aria-labelledby="t d"><title id="t">jackherd in herdr</title><desc id="d">Animated schematic of one herdr window, told in five numbered steps. 1, Lane search works: its state is a teal dot and its progress counts up. 2, It hits an open question: search turns to an amber diamond and sends the question as text. 3, The coordinator asks you: the question appears in the coordinator pane as a choice. 4, You pick: option 1 is selected. 5, The lane works on: the answer goes back to search, which is a teal dot again, and the herd counts one more box.</desc><defs><style>.s{font-family:ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif}.m{font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace}@keyframes k1{0%{opacity:1.0}0%{opacity:1.0}38.12%{opacity:1.0}40%{opacity:0.42}80%{opacity:0.42}81.88%{opacity:1.0}100%{opacity:1.0}}.k1{animation:k1 16s linear infinite both}@keyframes k2{0%{opacity:0.42}40%{opacity:0.42}41.88%{opacity:1.0}81.88%{opacity:1.0}83.75%{opacity:0.42}100%{opacity:0.42}}.k2{animation:k2 16s linear infinite both}@keyframes k3{0%{opacity:0.42}100%{opacity:0.42}}.k3{animation:k3 16s linear infinite both}@keyframes k4{0%{opacity:0}21.25%{opacity:0}23.75%{opacity:1}82.5%{opacity:1}85%{opacity:0}100%{opacity:0}}.k4{animation:k4 16s linear infinite both}@keyframes k5{0%{opacity:0;transform:translate(64px,198px)}23.75%{opacity:0;transform:translate(64px,198px)}23.94%{opacity:1;transform:translate(64px,198px)}24.32%{opacity:1;transform:translate(60.4px,197.5px)}24.9%{opacity:1;transform:translate(57.1px,196.1px)}25.47%{opacity:1;transform:translate(54.2px,193.9px)}26.04%{opacity:1;transform:translate(51.6px,191.1px)}26.61%{opacity:1;transform:translate(49.4px,187.6px)}27.19%{opacity:1;transform:translate(47.6px,183.8px)}27.76%{opacity:1;transform:translate(46.3px,179.6px)}28.33%{opacity:1;transform:translate(45.4px,175.2px)}28.91%{opacity:1;transform:translate(45px,170.7px)}29.48%{opacity:1;transform:translate(45.1px,166.3px)}30.05%{opacity:1;transform:translate(45.8px,162px)}30.63%{opacity:0;transform:translate(47px,158px)}100%{opacity:0;transform:translate(47px,158px)}}.k5{animation:k5 16s linear infinite both}@keyframes k6{0%{opacity:0;transform:translate(47px,158px)}81.25%{opacity:0;transform:translate(47px,158px)}81.44%{opacity:1;transform:translate(47px,158px)}81.67%{opacity:1;transform:translate(45.8px,162px)}82.08%{opacity:1;transform:translate(45.1px,166.3px)}82.5%{opacity:1;transform:translate(45px,170.7px)}82.92%{opacity:1;transform:translate(45.4px,175.2px)}83.33%{opacity:1;transform:translate(46.3px,179.6px)}83.75%{opacity:1;transform:translate(47.6px,183.8px)}84.17%{opacity:1;transform:translate(49.4px,187.6px)}84.58%{opacity:1;transform:translate(51.6px,191.1px)}85%{opacity:1;transform:translate(54.2px,193.9px)}85.42%{opacity:1;transform:translate(57.1px,196.1px)}85.83%{opacity:1;transform:translate(60.4px,197.5px)}86.25%{opacity:0;transform:translate(64px,198px)}100%{opacity:0;transform:translate(64px,198px)}}.k6{animation:k6 16s linear infinite both}@keyframes k7{0%{opacity:0}31.25%{opacity:0}33.75%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k7{animation:k7 16s linear infinite both}@keyframes k8{0%{opacity:0}41.25%{opacity:0}43.75%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k8{animation:k8 16s linear infinite both}@keyframes k9{0%{opacity:0}63.75%{opacity:0}66.25%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k9{animation:k9 16s linear infinite both}@keyframes k10{0%{opacity:0}68.75%{opacity:0}71.25%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k10{animation:k10 16s linear infinite both}@keyframes k11{0%{opacity:0}81.25%{opacity:0}83.75%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k11{animation:k11 16s linear infinite both}@keyframes k12{0%{opacity:1}21.25%{opacity:1}23.75%{opacity:0}82.5%{opacity:0}85%{opacity:1}100%{opacity:1}}.k12{animation:k12 16s linear infinite both}@keyframes k13{0%{opacity:0}8.75%{opacity:0}10.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k13{animation:k13 16s linear infinite both}@keyframes k14{0%{opacity:0}88.75%{opacity:0}90.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k14{animation:k14 16s linear infinite both}@keyframes k15{0%{opacity:1}8.75%{opacity:1}10.62%{opacity:0}96.25%{opacity:0}100%{opacity:1}}.k15{animation:k15 16s linear infinite both}@keyframes k16{0%{opacity:0}8.75%{opacity:0}10.62%{opacity:1}88.75%{opacity:1}90.62%{opacity:0}100%{opacity:0}}.k16{animation:k16 16s linear infinite both}@keyframes k17{0%{opacity:0}88.75%{opacity:0}90.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k17{animation:k17 16s linear infinite both}@keyframes k18{0%{opacity:1}8.75%{opacity:1}10.62%{opacity:0}96.25%{opacity:0}100%{opacity:1}}.k18{animation:k18 16s linear infinite both}@keyframes k19{0%{opacity:0}8.75%{opacity:0}10.62%{opacity:1}88.75%{opacity:1}90.62%{opacity:0}100%{opacity:0}}.k19{animation:k19 16s linear infinite both}@keyframes k20{0%{opacity:0}88.75%{opacity:0}90.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k20{animation:k20 16s linear infinite both}@keyframes k21{0%{opacity:1}18.13%{opacity:1}20%{opacity:0}100%{opacity:0}}.k21{animation:k21 16s linear infinite both}@keyframes k22{0%{opacity:1}18.13%{opacity:1}20%{opacity:0.62}100%{opacity:0.62}}.k22{animation:k22 16s linear infinite both}@keyframes k23{0%{opacity:0}20%{opacity:0}21.88%{opacity:1}38.12%{opacity:1}40%{opacity:0}100%{opacity:0}}.k23{animation:k23 16s linear infinite both}@keyframes k24{0%{opacity:0.62}20%{opacity:0.62}21.88%{opacity:1.0}38.12%{opacity:1.0}40%{opacity:0.62}100%{opacity:0.62}}.k24{animation:k24 16s linear infinite both}@keyframes k25{0%{opacity:0}40%{opacity:0}41.88%{opacity:1}58.12%{opacity:1}60%{opacity:0}100%{opacity:0}}.k25{animation:k25 16s linear infinite both}@keyframes k26{0%{opacity:0.62}40%{opacity:0.62}41.88%{opacity:1.0}58.12%{opacity:1.0}60%{opacity:0.62}100%{opacity:0.62}}.k26{animation:k26 16s linear infinite both}@keyframes k27{0%{opacity:0}60%{opacity:0}61.88%{opacity:1}78.12%{opacity:1}80%{opacity:0}100%{opacity:0}}.k27{animation:k27 16s linear infinite both}@keyframes k28{0%{opacity:0.62}60%{opacity:0.62}61.88%{opacity:1.0}78.12%{opacity:1.0}80%{opacity:0.62}100%{opacity:0.62}}.k28{animation:k28 16s linear infinite both}@keyframes k29{0%{opacity:0}80%{opacity:0}81.88%{opacity:1}98.12%{opacity:1}100%{opacity:0}}.k29{animation:k29 16s linear infinite both}@keyframes k30{0%{opacity:0.62}80%{opacity:0.62}81.88%{opacity:1}98.12%{opacity:1}100%{opacity:0.62}}.k30{animation:k30 16s linear infinite both}@media (prefers-reduced-motion:reduce){*{animation:none!important}}</style><clipPath id="jack-clip"><rect width="240" height="240" rx="50"/></clipPath><symbol id="jack" viewBox="0 0 240 240"><rect width="240" height="240" rx="50" fill="#212E34"/><g clip-path="url(#jack-clip)"><g transform="translate(34 8)"><path fill="#4A4E51" d="M80 50.5c.2 .1 .2 .1 1 1c-.2 .6-.3 1.3-.5 2c-.7 .4-.7 .4-4 2c-6 3.3-6 3.3-7 6.5c1.2-.4 2.3-.6 3.5-1c.8 0 1.7 0 2.5 0c.2 .1 .2 .1 1 1c-.5 2-.5 2-5.5 6.5c-3 5-3 5-5.5 11c.2 .3 .2 .3 1 2c.2 .3 .3 .7 .5 1c-.7 .7-1.3 1.3-2 2c-1.3-.2-2.6-.3-4-.5c0-1.2 0-2.3 0-3.5c-.2 .1-.2 .1-1.5 .5c-1.2 2.7-2.4 5.3-3.5 8c-1 2.1-2 4.2-3 6.3c-2.5 5.7-2.5 5.7-3 8.2c.5 0 1 0 1.5 0c.1-.8 .4-1.7 .5-2.5c.6 0 1.3 0 2 0c.5 2 1 3.9 1.6 6c1 4 2.2 8 3.4 11.9c2.1 8.4 1.1 15.5-3 23.1c-.6-.2-1.3-.3-2-.5c-.1-1.5-.3-3-.5-4.6c-.3-2.6-.5-5.2-1-7.9c-.5 0-1 0-1.5 0c-.1 1.7-.4 3.3-.5 5c-2.7 10.2-2.7 10.2-6 13.5c-6.1-3.1-9-14.9-11-21c-1.1-8.2 .9-14.7 3.5-22.5c-8.4 8.3-8.3 20.5-9 31.5c-.6-.2-1.3-.3-2-.5c-5.4-8.9-5.1-15-3-25c.5-1 1-2 1.5-3c-3 1.5-3 1.5-6.5 5c-.4 0-.6 0-1 0c.8-6.5 3.2-10.2 7.5-15c.6-.8 .6-.8 3.5-5c2.6-2.6 5.2-5.1 8-7.5c.6-.6 .6-.6 3.5-3.5c-.5-2-.5-2-1-3.5c3.6-6 6.5-9.2 12.5-13c.4-.6 .6-1.3 1-2c-.5-.6-1-1.3-1.5-2c2-3 2-3 4.5-4.5c.5 1 .5 1 1 3.5c2.5-1 2.5-1 8.5-5.5c.3 0 .7 0 1 0c-.1-.4-.1-.4-.5-2.5c4.3-3.3 10-2.5 15-1zM153 69.5c1.2 .2 2.3 .3 3.5 .5c.3 .7 .7 1.3 1 2c.3 0 .7 0 1 0c.5 1.2 1 2.3 1.5 3.5c3.4 7.6 6.2 13.7 12.2 19.6c3.3 3.4 3.3 3.4 3.8 5.9c-.2 .2-.3 .3-.5 .5c.1 .7 .1 .7 .5 4.5c-.5 .3-1 .7-1.5 1c-.8-.3-1.7-.7-2.5-1c.3 .7 .7 1.3 1 2c1.2 5.8 1.4 10 1 16c-.7 0-1.3 0-2 0c-.4-1.3-.8-2.7-1.3-4.1c-2.1-6.1-5.2-11.4-8.7-16.9c-.2-.8-.2-.8-1.5-5c-.3-.6-.3-.6-2.1-3.5c-1.9-3.5-1.9-3.5-2.9-8.5c.7 0 1.3 0 2 0c.5 .5 1 1 1.5 1.5c-.8-2.3-1.7-4.6-2.5-7c-.7-2-1.3-4-2-6c-.7-1-1.3-2-2-3c.2-.7 .3-1.3 .5-2zM86.5 100.5c1 0 2 0 3 0c1.3 .8 2.7 1.7 4 2.5c-2.5 2.5-2.5 2.5-8 5.5c-5.5 4.3-8.6 7-10 14c-.8 2.2-1.7 4.3-2.5 6.5c-1.2 4.6-.5 9.3 0 14c-.5 0-1 0-1.5 0c-.2 2-.3 3.9-.5 6c-.7 0-1.3 0-2 0c-.3-.6-.3-.6-2-3.5c-1-.8-2-1.7-3-2.5c-.5-1.5-.5-1.5 1.5-8.5c.3-10.3-1-18.2-4-28c0-4 0-4 1-6c2 2 2 2 3 6.5c2.5-.5 2.5-.5 8-4c9.8-3.5 9.8-3.5 13-2.5zM43 161.5c2.5 2.5 2.5 2.5 3 5c-1.5 2.5-1.5 2.5-4 5c.5 3.5 .5 3.5 4 6.5c-.1 1.2-.4 2.3-.5 3.5c-5.3 3.5-5.3 3.5-8.5 3.5c-6-2.5-6-2.5-11.5-7c0-3 0-3 4-8c9.5-8.5 9.5-8.5 13.5-8.5z"/><path fill="#9B9D9E" d="M142.5 60.5c.5 1 .5 1-.5 4.5c1.2 1.5 2.3 3 3.5 4.5c2 4.5 2 4.5 2.5 9.5c-.5 .2-1 .3-1.5 .5c-.1 .2-.1 .2-.5 1c-2-1-3.9-2-6-3c-.8-.3-1.7-.7-2.5-1c-.3-.7-.7-1.3-1-2c.3-.5 .7-1 1-1.5c-.2-.7-.3-1.3-.5-2c-3.5 .5-3.5 .5-7.5 4c-.5 3-.5 3 0 5.5c-2 4-2 4-3.5 5.5c-.3 0-.7 0-1 0c0 .7 0 1.3 0 2c-2-1-2-1-2.5-2.5c.3-13.7 .3-13.7 4.8-18.5c4.1-3.9 6.3-4.8 11.7-6c1-.5 1-.5 3.5-.5zM98 70.5c.5 .3 1 .7 1.5 1c1.3 .3 2.7 .7 4 1c4.4 5.8 6.5 10.3 6.5 17.5c-2 2-2 2-5 2.5c-.3 .2-.3 .2-2 1c-.2-.2-.2-.2-1.5-1c.5-3 .5-3 1-5c-.7-.8-1.3-1.7-2-2.5c-.2-1.7-.3-3.3-.5-5c-4.5-2.7-8.5-3-13.5-2.5c-.1 .3-.1 .3-.5 1.5c1.3 .8 2.7 1.7 4 2.5c-.1 .4-.1 .4-.5 2.5c-2.3 1.2-4.6 2.3-7 3.5c-5.5 5.5-5.5 5.5-6.5 7c0 .5 0 1 0 1.5c.9-.1 .9-.1 5.5-.5c1 1.3 2 2.7 3 4c-.2 .5-.3 1-.5 1.5c-1.7 .5-3.2 1-5 1.6c-5.5 1.9-5.5 1.9-11 5.4c-.4 .1-.4 .1-2.5 .5c-1.5-1.5-1.5-1.5-2.5-6c-.4-.3-.6-.7-1-1c0-6.7 .9-11.7 3-18c.2-.6 .2-.6 1-3.5c7.8-9.7 20.5-14.1 32-9.5z"/><path fill="#FAF9F8" d="M103.5 40c0 .4 0 .6 0 1c2.2-.1 4.3-.4 6.5-.5c7 0 7 0 9.5 1c-.2 .6-.3 1.3-.5 2c-1.8 .6-3.7 1.3-5.5 2c-.2 .5-.3 1-.5 1.5c.7-.1 1.3-.4 2-.5c13.4-.3 13.4-.3 19.5 3.5c-.3 .6-.7 1.3-1 2c-1 0-2 0-3 0c0 .5 0 1 0 1.5c1.2 .1 1.2 .1 7 .5c3.5 2 3.5 2 5.5 4.5c0 1 0 2 0 3c-3.3 .9-6.7 1.6-10 2.5c-7.5 5-7.5 5-8.5 9c-.3 4.5-.4 9-.5 13.5c1.5 1 1.5 1 7.5 2.5c.9 .6 .9 .6 5.5 3.5c2 1 2 1 9 2.5c7.9 4 11.3 8.3 15 16.5c0 .5 0 1 0 1.5c-1 .2-2 .3-3 .5c.5 1.8 1 3.7 1.5 5.5c.3 8.5-.8 13.6-6 20.5c-.7-.2-1.3-.3-2-.5c0-1.7 0-3.3 0-5c-.7 .2-1.3 .3-2 .5c-1.3 2.7-2.7 5.3-4 8c-3.2 3.7-6.4 6.2-11 8c-1-1-1-1-1.5-3c-2.5 1.3-4.8 2.9-7 4.5c-6.3 2.8-10.8 3.2-17.6 1c-1.3-.5-2.6-1-3.9-1.5c.2 1.2 .3 2.3 .5 3.5c-.2 .3-.3 .7-.5 1c-5.9 0-9-1.8-14-5c-3-3.5-3-3.5-3-4.5c-.7 0-1.3 0-2 0c.5 2.2 1 4.3 1.5 6.5c-.3 .3-.7 .7-1 1c-4.5-.9-7.6-3.6-11-6.5c-4-6-4-9.3-4-16.5c2.5-7.5 2.5-7.5 3.5-9.5c.3-1.7 .7-3.3 1-5c6.4-8 14.7-13 24.5-16c6.4-6.3 6.4-6.3 8.5-10.5c0-6-1.6-9.8-4.5-15c-.2-.5-.3-1-.5-1.5c-1.7-.3-3.3-.7-5-1c-1.3-.7-2.7-1.3-4-2c-5.7-1.9-10.2-.3-15.5 1.5c-10 5.5-10 5.5-13 10c-.3-.2-.3-.2-2-1c1.3-6.8 4.6-11.7 9.5-16.5c.5-.3 1-.6 1.5-1c.2-.5 .3-1 .5-1.5c-1 .1-1 .1-6 1c-.3-.4-.7-.6-1-1c.5-2 .5-2 4-6c5-2.5 5-2.5 7.5-3c0-.5 0-1 0-1.5c-10.5-1.5-18.5 1-27 7c-.5 .9-1 1.6-1.5 2.5c-.9 .6-.9 .6-5.5 4c-4 3.5-7.6 7-11 11c-4.6 3.8-8.7 6.8-14 9.5c-.7-.3-1.3-.7-2-1c.2-.7 .3-1.3 .5-2c1.8-1 3.7-2 5.5-3c5.2-4.2 9.7-8.5 14-13.5c3.1-2.8 6.2-5.5 9.5-8c1.3-1.3 2.6-2.6 4-4c5.6-3.6 10.4-6.1 17-7c.5-1 1-2 1.5-3c7.2-2.4 12.5-2.2 20-.5c.2 .1 .2 .1 1 .5c3.5 0 3.5 0 8.5-3.5c1-.5 1-.5 3-.5zM118 92.5c-1 1.2-2 2.3-3 3.5c.5 2.5 .5 2.5 3 5c1.8 .8 3.7 1.7 5.5 2.5c.5 1 .5 1 .5 4.5c-4 4.8-6.1 5-12.5 6c0 .7 0 1.3 0 2c4.7 1.5 7.7 .2 12-1.5c1.2-.7 2.3-1.3 3.5-2c1.3 .3 2.7 .7 4 1c3.5 0 3.5 0 5.5-1.5c-.2-.5-.3-1-.5-1.5c-1.7-.3-3.3-.7-5-1c-2.5-2-2.5-2-3-5.5c2-3 2-3 4-5.5c.5-3.5 .5-3.5-1.5-6.5c-4.3-2.2-8.2-1-12.5 .5zM146 58.5c5.8 1.1 8.3 3.9 12.5 8c3.7 5.9 4.4 7.3 5 14c2.8 6.2 5.4 10 10.5 14.5c5.2 5.2 7.2 10.3 9 17.5c0 3.5 0 3.5-2 11c-.2 1.3-.2 1.3-1 7.5c-3.8 10.7-3.8 10.7-8.5 13.5c-.5 .2-.5 .2-3 1c-1 1.3-2 2.7-3 4c-4.5 3-4.5 3-10.5 5c-2.7-.6-5.3-1.3-8-2c-1.7 1.2-3.3 2.3-5 3.5c-2 1-2 1-4 .5c.5-1.5 .5-1.5 6-5c.3-.3 .3-.3 2-2c2.4 0 4.9 0 7.5 0c.5 .5 1 1 1.5 1.5c4.3-.6 7.1-2.3 10.5-5c3.5-5.5 3.5-5.5 4-7c1.5 0 2.9 0 4.5 0c4.3-8.7 4.6-14 2.5-23.5c-.3-1-.7-2-1-3c.3 .1 .3 .1 2 .5c1.5 2.5 1.5 2.5 1.5 4c.5-.2 1-.3 1.5-.5c-.8-10-4.3-16-11.4-22.8c-3.7-3.8-5.5-6.9-7.6-11.7c-.7-1.5-1.5-2.9-2.3-4.4c-1.7-3.6-1.7-3.6-1.7-5.6c.3-.2 .7-.3 1-.5c-.5-3-.5-3-4.5-7c-2-1-3.9-2-6-3c-.7-.4-1.3-.6-2-1c0-.6 0-1.3 0-2zM33.5 83.5c-1 4-5.1 6.4-8 9c-8.6 8.1-14.6 17.2-15.2 29.2c.3 8.1 .3 8.1 2.2 11.8c.5 0 1 0 1.5 0c.4-2.9 .6-6 1-9c.6 0 1.3 0 2 0c.2 1.3 .4 2.7 .6 4.1c1.8 9.3 5.3 14.8 12.9 20.4c2.3 1.2 4.6 2.3 7 3.5c.7 .6 .7 .6 4.5 3.5c3.5 0 3.5 0 8.5-1.5c1 0 2 0 3 0c-1 2-1 2-2.5 3.5c-6.5 1.7-9.6 .9-16-1.5c-1.1-1-2.3-2-3.5-3c-1.5-.7-2.9-1.3-4.5-2c-3.9-3.4-6.9-6.5-9.5-11c-1.1-.5-2.3-1-3.5-1.5c-5.9-6.7-8-12.7-8-21.5c2.4-12.2 7.2-19.8 16.5-28c4.5-4 4.5-4 6-5.5c3.5-1 3.5-1 5-.5zM50.5 172c2.5 1 2.5 1 7 6.5c6 6.2 12 11.8 18.8 17.1c5.7 4.5 11.3 9.1 16.7 13.9c1.3 1.2 2.7 2.3 4 3.5c.7 .7 1.3 1.3 2 2c-.2 .8-.3 1.7-.5 2.5c-6 2.9-11.7 4-18.5 4c-2.6-2.6-4.3-5.5-5.5-9c-.3 .1-.3 .1-1.5 .5c0 2.3 0 4.6 0 7c-.3 .5-.7 1-1 1.5c-13.3 0-24.6-4.6-34.5-13.5c-4.5-4.5-4.5-4.5-8.5-11c.5-2.5 .5-2.5 4.5-7c5-3 5-3 8-3.5c.5 .3 1 .7 1.5 1c2-.5 2-.5 4-2c.9-4.2 1.7-8.3 2.5-12.5c.4-.3 .6-.7 1-1z"/><path fill="#03B5BD" d="M58.5 149.5c2.5 .5 2.5 .5 7 3.5c9.8 5.2 18.9 7.9 30 8.5c2.2 .5 4.3 1 6.5 1.5c7 0 7 0 9-.5c2.3-.2 4.6-.3 7-.5c3.2-.9 6.3-1.9 9.5-3c1.3-.2 2.7-.3 4-.5c.2 .2 .3 .3 .5 .5c-.8 6.3-4.1 10.2-8 15c-.2 .5-.3 1-.5 1.5c1.7-1 3.3-2 5-3c.7 .2 1.3 .3 2 .5c-3 12.2-9.3 20.3-17.5 29.5c-.7 .7-.7 .7-4 4.5c-1 .8-2 1.7-3 2.5c-3.5-.5-3.5-.5-8-5c-2.3-1.8-4.7-3.7-7-5.5c-2.7-2.2-5.3-4.3-8-6.5c-.8-.6-.8-.6-4.5-3.5c-11-9.3-20.7-18.8-28.5-31c3-4 3-4 8.5-8.5z"/><ellipse cx="94" cy="94" rx="6.5" ry="6.5" fill="#212E34"/><ellipse cx="135" cy="85.6" rx="6.2" ry="5.8" fill="#212E34"/><g fill="#FAF9F8"><circle cx="91.5" cy="91.5" r="2.1"/><circle cx="134.6" cy="84.3" r="2"/></g><ellipse cx="124" cy="93.8" rx="3.4" ry="1.1" fill="#9B9D9E" transform="rotate(-6 124 93.8)"/></g></g><rect x="5" y="5" width="230" height="230" rx="45" fill="none"/></symbol></defs>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="724" viewBox="0 0 960 724" role="img" aria-labelledby="t d"><title id="t">jackherd in herdr</title><desc id="d">Animated schematic of one herdr window, told in five numbered steps. 1, Lane search works: its state is a teal dot and its progress counts up. 2, It hits an open question: search turns to an amber diamond and sends the question as text. 3, The coordinator asks you: the question appears in the coordinator pane as a choice. 4, You pick: option 1 is selected. 5, The lane works on: the answer goes back to search, which is a teal dot again, and the herd counts one more box.</desc><defs><style>.s{font-family:ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif}.m{font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace}@keyframes k1{0%{opacity:1.0}0%{opacity:1.0}38.12%{opacity:1.0}40%{opacity:0.42}80%{opacity:0.42}81.88%{opacity:1.0}100%{opacity:1.0}}.k1{animation:k1 16s linear infinite both}@keyframes k2{0%{opacity:0.42}40%{opacity:0.42}41.88%{opacity:1.0}81.88%{opacity:1.0}83.75%{opacity:0.42}100%{opacity:0.42}}.k2{animation:k2 16s linear infinite both}@keyframes k3{0%{opacity:0.42}100%{opacity:0.42}}.k3{animation:k3 16s linear infinite both}@keyframes k4{0%{opacity:0}21.25%{opacity:0}23.75%{opacity:1}82.5%{opacity:1}85%{opacity:0}100%{opacity:0}}.k4{animation:k4 16s linear infinite both}@keyframes k5{0%{opacity:0;transform:translate(64px,198px)}23.75%{opacity:0;transform:translate(64px,198px)}23.94%{opacity:1;transform:translate(64px,198px)}24.32%{opacity:1;transform:translate(60.4px,197.5px)}24.9%{opacity:1;transform:translate(57.1px,196.1px)}25.47%{opacity:1;transform:translate(54.2px,193.9px)}26.04%{opacity:1;transform:translate(51.6px,191.1px)}26.61%{opacity:1;transform:translate(49.4px,187.6px)}27.19%{opacity:1;transform:translate(47.6px,183.8px)}27.76%{opacity:1;transform:translate(46.3px,179.6px)}28.33%{opacity:1;transform:translate(45.4px,175.2px)}28.91%{opacity:1;transform:translate(45px,170.7px)}29.48%{opacity:1;transform:translate(45.1px,166.3px)}30.05%{opacity:1;transform:translate(45.8px,162px)}30.63%{opacity:0;transform:translate(47px,158px)}100%{opacity:0;transform:translate(47px,158px)}}.k5{animation:k5 16s linear infinite both}@keyframes k6{0%{opacity:0;transform:translate(47px,158px)}81.25%{opacity:0;transform:translate(47px,158px)}81.44%{opacity:1;transform:translate(47px,158px)}81.67%{opacity:1;transform:translate(45.8px,162px)}82.08%{opacity:1;transform:translate(45.1px,166.3px)}82.5%{opacity:1;transform:translate(45px,170.7px)}82.92%{opacity:1;transform:translate(45.4px,175.2px)}83.33%{opacity:1;transform:translate(46.3px,179.6px)}83.75%{opacity:1;transform:translate(47.6px,183.8px)}84.17%{opacity:1;transform:translate(49.4px,187.6px)}84.58%{opacity:1;transform:translate(51.6px,191.1px)}85%{opacity:1;transform:translate(54.2px,193.9px)}85.42%{opacity:1;transform:translate(57.1px,196.1px)}85.83%{opacity:1;transform:translate(60.4px,197.5px)}86.25%{opacity:0;transform:translate(64px,198px)}100%{opacity:0;transform:translate(64px,198px)}}.k6{animation:k6 16s linear infinite both}@keyframes k7{0%{opacity:0}31.25%{opacity:0}33.75%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k7{animation:k7 16s linear infinite both}@keyframes k8{0%{opacity:0}41.25%{opacity:0}43.75%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k8{animation:k8 16s linear infinite both}@keyframes k9{0%{opacity:0}63.75%{opacity:0}66.25%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k9{animation:k9 16s linear infinite both}@keyframes k10{0%{opacity:0}68.75%{opacity:0}71.25%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k10{animation:k10 16s linear infinite both}@keyframes k11{0%{opacity:0}81.25%{opacity:0}83.75%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k11{animation:k11 16s linear infinite both}@keyframes k12{0%{opacity:1}21.25%{opacity:1}23.75%{opacity:0}82.5%{opacity:0}85%{opacity:1}100%{opacity:1}}.k12{animation:k12 16s linear infinite both}@keyframes k13{0%{opacity:0}8.75%{opacity:0}10.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k13{animation:k13 16s linear infinite both}@keyframes k14{0%{opacity:0}88.75%{opacity:0}90.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k14{animation:k14 16s linear infinite both}@keyframes k15{0%{opacity:1}8.75%{opacity:1}10.62%{opacity:0}96.25%{opacity:0}100%{opacity:1}}.k15{animation:k15 16s linear infinite both}@keyframes k16{0%{opacity:0}8.75%{opacity:0}10.62%{opacity:1}88.75%{opacity:1}90.62%{opacity:0}100%{opacity:0}}.k16{animation:k16 16s linear infinite both}@keyframes k17{0%{opacity:0}88.75%{opacity:0}90.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k17{animation:k17 16s linear infinite both}@keyframes k18{0%{opacity:1}8.75%{opacity:1}10.62%{opacity:0}96.25%{opacity:0}100%{opacity:1}}.k18{animation:k18 16s linear infinite both}@keyframes k19{0%{opacity:0}8.75%{opacity:0}10.62%{opacity:1}88.75%{opacity:1}90.62%{opacity:0}100%{opacity:0}}.k19{animation:k19 16s linear infinite both}@keyframes k20{0%{opacity:0}88.75%{opacity:0}90.62%{opacity:1}96.25%{opacity:1}100%{opacity:0}}.k20{animation:k20 16s linear infinite both}@keyframes k21{0%{opacity:1}18.13%{opacity:1}20%{opacity:0}100%{opacity:0}}.k21{animation:k21 16s linear infinite both}@keyframes k22{0%{opacity:1}18.13%{opacity:1}20%{opacity:0.62}100%{opacity:0.62}}.k22{animation:k22 16s linear infinite both}@keyframes k23{0%{opacity:0}20%{opacity:0}21.88%{opacity:1}38.12%{opacity:1}40%{opacity:0}100%{opacity:0}}.k23{animation:k23 16s linear infinite both}@keyframes k24{0%{opacity:0.62}20%{opacity:0.62}21.88%{opacity:1.0}38.12%{opacity:1.0}40%{opacity:0.62}100%{opacity:0.62}}.k24{animation:k24 16s linear infinite both}@keyframes k25{0%{opacity:0}40%{opacity:0}41.88%{opacity:1}58.12%{opacity:1}60%{opacity:0}100%{opacity:0}}.k25{animation:k25 16s linear infinite both}@keyframes k26{0%{opacity:0.62}40%{opacity:0.62}41.88%{opacity:1.0}58.12%{opacity:1.0}60%{opacity:0.62}100%{opacity:0.62}}.k26{animation:k26 16s linear infinite both}@keyframes k27{0%{opacity:0}60%{opacity:0}61.88%{opacity:1}78.12%{opacity:1}80%{opacity:0}100%{opacity:0}}.k27{animation:k27 16s linear infinite both}@keyframes k28{0%{opacity:0.62}60%{opacity:0.62}61.88%{opacity:1.0}78.12%{opacity:1.0}80%{opacity:0.62}100%{opacity:0.62}}.k28{animation:k28 16s linear infinite both}@keyframes k29{0%{opacity:0}80%{opacity:0}81.88%{opacity:1}98.12%{opacity:1}100%{opacity:0}}.k29{animation:k29 16s linear infinite both}@keyframes k30{0%{opacity:0.62}80%{opacity:0.62}81.88%{opacity:1}98.12%{opacity:1}100%{opacity:0.62}}.k30{animation:k30 16s linear infinite both}@media (prefers-reduced-motion:reduce){*{animation:none!important}}</style><clipPath id="jack-clip"><rect width="240" height="240" rx="50"/></clipPath><symbol id="jack" viewBox="0 0 240 240"><rect width="240" height="240" rx="50" fill="#212E34"/><g clip-path="url(#jack-clip)"><g transform="translate(34 8)"><path fill="#4A4E51" d="M80 50.5c.2 .1 .2 .1 1 1c-.2 .6-.3 1.3-.5 2c-.7 .4-.7 .4-4 2c-6 3.3-6 3.3-7 6.5c1.2-.4 2.3-.6 3.5-1c.8 0 1.7 0 2.5 0c.2 .1 .2 .1 1 1c-.5 2-.5 2-5.5 6.5c-3 5-3 5-5.5 11c.2 .3 .2 .3 1 2c.2 .3 .3 .7 .5 1c-.7 .7-1.3 1.3-2 2c-1.3-.2-2.6-.3-4-.5c0-1.2 0-2.3 0-3.5c-.2 .1-.2 .1-1.5 .5c-1.2 2.7-2.4 5.3-3.5 8c-1 2.1-2 4.2-3 6.3c-2.5 5.7-2.5 5.7-3 8.2c.5 0 1 0 1.5 0c.1-.8 .4-1.7 .5-2.5c.6 0 1.3 0 2 0c.5 2 1 3.9 1.6 6c1 4 2.2 8 3.4 11.9c2.1 8.4 1.1 15.5-3 23.1c-.6-.2-1.3-.3-2-.5c-.1-1.5-.3-3-.5-4.6c-.3-2.6-.5-5.2-1-7.9c-.5 0-1 0-1.5 0c-.1 1.7-.4 3.3-.5 5c-2.7 10.2-2.7 10.2-6 13.5c-6.1-3.1-9-14.9-11-21c-1.1-8.2 .9-14.7 3.5-22.5c-8.4 8.3-8.3 20.5-9 31.5c-.6-.2-1.3-.3-2-.5c-5.4-8.9-5.1-15-3-25c.5-1 1-2 1.5-3c-3 1.5-3 1.5-6.5 5c-.4 0-.6 0-1 0c.8-6.5 3.2-10.2 7.5-15c.6-.8 .6-.8 3.5-5c2.6-2.6 5.2-5.1 8-7.5c.6-.6 .6-.6 3.5-3.5c-.5-2-.5-2-1-3.5c3.6-6 6.5-9.2 12.5-13c.4-.6 .6-1.3 1-2c-.5-.6-1-1.3-1.5-2c2-3 2-3 4.5-4.5c.5 1 .5 1 1 3.5c2.5-1 2.5-1 8.5-5.5c.3 0 .7 0 1 0c-.1-.4-.1-.4-.5-2.5c4.3-3.3 10-2.5 15-1zM153 69.5c1.2 .2 2.3 .3 3.5 .5c.3 .7 .7 1.3 1 2c.3 0 .7 0 1 0c.5 1.2 1 2.3 1.5 3.5c3.4 7.6 6.2 13.7 12.2 19.6c3.3 3.4 3.3 3.4 3.8 5.9c-.2 .2-.3 .3-.5 .5c.1 .7 .1 .7 .5 4.5c-.5 .3-1 .7-1.5 1c-.8-.3-1.7-.7-2.5-1c.3 .7 .7 1.3 1 2c1.2 5.8 1.4 10 1 16c-.7 0-1.3 0-2 0c-.4-1.3-.8-2.7-1.3-4.1c-2.1-6.1-5.2-11.4-8.7-16.9c-.2-.8-.2-.8-1.5-5c-.3-.6-.3-.6-2.1-3.5c-1.9-3.5-1.9-3.5-2.9-8.5c.7 0 1.3 0 2 0c.5 .5 1 1 1.5 1.5c-.8-2.3-1.7-4.6-2.5-7c-.7-2-1.3-4-2-6c-.7-1-1.3-2-2-3c.2-.7 .3-1.3 .5-2zM86.5 100.5c1 0 2 0 3 0c1.3 .8 2.7 1.7 4 2.5c-2.5 2.5-2.5 2.5-8 5.5c-5.5 4.3-8.6 7-10 14c-.8 2.2-1.7 4.3-2.5 6.5c-1.2 4.6-.5 9.3 0 14c-.5 0-1 0-1.5 0c-.2 2-.3 3.9-.5 6c-.7 0-1.3 0-2 0c-.3-.6-.3-.6-2-3.5c-1-.8-2-1.7-3-2.5c-.5-1.5-.5-1.5 1.5-8.5c.3-10.3-1-18.2-4-28c0-4 0-4 1-6c2 2 2 2 3 6.5c2.5-.5 2.5-.5 8-4c9.8-3.5 9.8-3.5 13-2.5zM43 161.5c2.5 2.5 2.5 2.5 3 5c-1.5 2.5-1.5 2.5-4 5c.5 3.5 .5 3.5 4 6.5c-.1 1.2-.4 2.3-.5 3.5c-5.3 3.5-5.3 3.5-8.5 3.5c-6-2.5-6-2.5-11.5-7c0-3 0-3 4-8c9.5-8.5 9.5-8.5 13.5-8.5z"/><path fill="#9B9D9E" d="M142.5 60.5c.5 1 .5 1-.5 4.5c1.2 1.5 2.3 3 3.5 4.5c2 4.5 2 4.5 2.5 9.5c-.5 .2-1 .3-1.5 .5c-.1 .2-.1 .2-.5 1c-2-1-3.9-2-6-3c-.8-.3-1.7-.7-2.5-1c-.3-.7-.7-1.3-1-2c.3-.5 .7-1 1-1.5c-.2-.7-.3-1.3-.5-2c-3.5 .5-3.5 .5-7.5 4c-.5 3-.5 3 0 5.5c-2 4-2 4-3.5 5.5c-.3 0-.7 0-1 0c0 .7 0 1.3 0 2c-2-1-2-1-2.5-2.5c.3-13.7 .3-13.7 4.8-18.5c4.1-3.9 6.3-4.8 11.7-6c1-.5 1-.5 3.5-.5zM98 70.5c.5 .3 1 .7 1.5 1c1.3 .3 2.7 .7 4 1c4.4 5.8 6.5 10.3 6.5 17.5c-2 2-2 2-5 2.5c-.3 .2-.3 .2-2 1c-.2-.2-.2-.2-1.5-1c.5-3 .5-3 1-5c-.7-.8-1.3-1.7-2-2.5c-.2-1.7-.3-3.3-.5-5c-4.5-2.7-8.5-3-13.5-2.5c-.1 .3-.1 .3-.5 1.5c1.3 .8 2.7 1.7 4 2.5c-.1 .4-.1 .4-.5 2.5c-2.3 1.2-4.6 2.3-7 3.5c-5.5 5.5-5.5 5.5-6.5 7c0 .5 0 1 0 1.5c.9-.1 .9-.1 5.5-.5c1 1.3 2 2.7 3 4c-.2 .5-.3 1-.5 1.5c-1.7 .5-3.2 1-5 1.6c-5.5 1.9-5.5 1.9-11 5.4c-.4 .1-.4 .1-2.5 .5c-1.5-1.5-1.5-1.5-2.5-6c-.4-.3-.6-.7-1-1c0-6.7 .9-11.7 3-18c.2-.6 .2-.6 1-3.5c7.8-9.7 20.5-14.1 32-9.5z"/><path fill="#FAF9F8" d="M103.5 40c0 .4 0 .6 0 1c2.2-.1 4.3-.4 6.5-.5c7 0 7 0 9.5 1c-.2 .6-.3 1.3-.5 2c-1.8 .6-3.7 1.3-5.5 2c-.2 .5-.3 1-.5 1.5c.7-.1 1.3-.4 2-.5c13.4-.3 13.4-.3 19.5 3.5c-.3 .6-.7 1.3-1 2c-1 0-2 0-3 0c0 .5 0 1 0 1.5c1.2 .1 1.2 .1 7 .5c3.5 2 3.5 2 5.5 4.5c0 1 0 2 0 3c-3.3 .9-6.7 1.6-10 2.5c-7.5 5-7.5 5-8.5 9c-.3 4.5-.4 9-.5 13.5c1.5 1 1.5 1 7.5 2.5c.9 .6 .9 .6 5.5 3.5c2 1 2 1 9 2.5c7.9 4 11.3 8.3 15 16.5c0 .5 0 1 0 1.5c-1 .2-2 .3-3 .5c.5 1.8 1 3.7 1.5 5.5c.3 8.5-.8 13.6-6 20.5c-.7-.2-1.3-.3-2-.5c0-1.7 0-3.3 0-5c-.7 .2-1.3 .3-2 .5c-1.3 2.7-2.7 5.3-4 8c-3.2 3.7-6.4 6.2-11 8c-1-1-1-1-1.5-3c-2.5 1.3-4.8 2.9-7 4.5c-6.3 2.8-10.8 3.2-17.6 1c-1.3-.5-2.6-1-3.9-1.5c.2 1.2 .3 2.3 .5 3.5c-.2 .3-.3 .7-.5 1c-5.9 0-9-1.8-14-5c-3-3.5-3-3.5-3-4.5c-.7 0-1.3 0-2 0c.5 2.2 1 4.3 1.5 6.5c-.3 .3-.7 .7-1 1c-4.5-.9-7.6-3.6-11-6.5c-4-6-4-9.3-4-16.5c2.5-7.5 2.5-7.5 3.5-9.5c.3-1.7 .7-3.3 1-5c6.4-8 14.7-13 24.5-16c6.4-6.3 6.4-6.3 8.5-10.5c0-6-1.6-9.8-4.5-15c-.2-.5-.3-1-.5-1.5c-1.7-.3-3.3-.7-5-1c-1.3-.7-2.7-1.3-4-2c-5.7-1.9-10.2-.3-15.5 1.5c-10 5.5-10 5.5-13 10c-.3-.2-.3-.2-2-1c1.3-6.8 4.6-11.7 9.5-16.5c.5-.3 1-.6 1.5-1c.2-.5 .3-1 .5-1.5c-1 .1-1 .1-6 1c-.3-.4-.7-.6-1-1c.5-2 .5-2 4-6c5-2.5 5-2.5 7.5-3c0-.5 0-1 0-1.5c-10.5-1.5-18.5 1-27 7c-.5 .9-1 1.6-1.5 2.5c-.9 .6-.9 .6-5.5 4c-4 3.5-7.6 7-11 11c-4.6 3.8-8.7 6.8-14 9.5c-.7-.3-1.3-.7-2-1c.2-.7 .3-1.3 .5-2c1.8-1 3.7-2 5.5-3c5.2-4.2 9.7-8.5 14-13.5c3.1-2.8 6.2-5.5 9.5-8c1.3-1.3 2.6-2.6 4-4c5.6-3.6 10.4-6.1 17-7c.5-1 1-2 1.5-3c7.2-2.4 12.5-2.2 20-.5c.2 .1 .2 .1 1 .5c3.5 0 3.5 0 8.5-3.5c1-.5 1-.5 3-.5zM118 92.5c-1 1.2-2 2.3-3 3.5c.5 2.5 .5 2.5 3 5c1.8 .8 3.7 1.7 5.5 2.5c.5 1 .5 1 .5 4.5c-4 4.8-6.1 5-12.5 6c0 .7 0 1.3 0 2c4.7 1.5 7.7 .2 12-1.5c1.2-.7 2.3-1.3 3.5-2c1.3 .3 2.7 .7 4 1c3.5 0 3.5 0 5.5-1.5c-.2-.5-.3-1-.5-1.5c-1.7-.3-3.3-.7-5-1c-2.5-2-2.5-2-3-5.5c2-3 2-3 4-5.5c.5-3.5 .5-3.5-1.5-6.5c-4.3-2.2-8.2-1-12.5 .5zM146 58.5c5.8 1.1 8.3 3.9 12.5 8c3.7 5.9 4.4 7.3 5 14c2.8 6.2 5.4 10 10.5 14.5c5.2 5.2 7.2 10.3 9 17.5c0 3.5 0 3.5-2 11c-.2 1.3-.2 1.3-1 7.5c-3.8 10.7-3.8 10.7-8.5 13.5c-.5 .2-.5 .2-3 1c-1 1.3-2 2.7-3 4c-4.5 3-4.5 3-10.5 5c-2.7-.6-5.3-1.3-8-2c-1.7 1.2-3.3 2.3-5 3.5c-2 1-2 1-4 .5c.5-1.5 .5-1.5 6-5c.3-.3 .3-.3 2-2c2.4 0 4.9 0 7.5 0c.5 .5 1 1 1.5 1.5c4.3-.6 7.1-2.3 10.5-5c3.5-5.5 3.5-5.5 4-7c1.5 0 2.9 0 4.5 0c4.3-8.7 4.6-14 2.5-23.5c-.3-1-.7-2-1-3c.3 .1 .3 .1 2 .5c1.5 2.5 1.5 2.5 1.5 4c.5-.2 1-.3 1.5-.5c-.8-10-4.3-16-11.4-22.8c-3.7-3.8-5.5-6.9-7.6-11.7c-.7-1.5-1.5-2.9-2.3-4.4c-1.7-3.6-1.7-3.6-1.7-5.6c.3-.2 .7-.3 1-.5c-.5-3-.5-3-4.5-7c-2-1-3.9-2-6-3c-.7-.4-1.3-.6-2-1c0-.6 0-1.3 0-2zM33.5 83.5c-1 4-5.1 6.4-8 9c-8.6 8.1-14.6 17.2-15.2 29.2c.3 8.1 .3 8.1 2.2 11.8c.5 0 1 0 1.5 0c.4-2.9 .6-6 1-9c.6 0 1.3 0 2 0c.2 1.3 .4 2.7 .6 4.1c1.8 9.3 5.3 14.8 12.9 20.4c2.3 1.2 4.6 2.3 7 3.5c.7 .6 .7 .6 4.5 3.5c3.5 0 3.5 0 8.5-1.5c1 0 2 0 3 0c-1 2-1 2-2.5 3.5c-6.5 1.7-9.6 .9-16-1.5c-1.1-1-2.3-2-3.5-3c-1.5-.7-2.9-1.3-4.5-2c-3.9-3.4-6.9-6.5-9.5-11c-1.1-.5-2.3-1-3.5-1.5c-5.9-6.7-8-12.7-8-21.5c2.4-12.2 7.2-19.8 16.5-28c4.5-4 4.5-4 6-5.5c3.5-1 3.5-1 5-.5zM50.5 172c2.5 1 2.5 1 7 6.5c6 6.2 12 11.8 18.8 17.1c5.7 4.5 11.3 9.1 16.7 13.9c1.3 1.2 2.7 2.3 4 3.5c.7 .7 1.3 1.3 2 2c-.2 .8-.3 1.7-.5 2.5c-6 2.9-11.7 4-18.5 4c-2.6-2.6-4.3-5.5-5.5-9c-.3 .1-.3 .1-1.5 .5c0 2.3 0 4.6 0 7c-.3 .5-.7 1-1 1.5c-13.3 0-24.6-4.6-34.5-13.5c-4.5-4.5-4.5-4.5-8.5-11c.5-2.5 .5-2.5 4.5-7c5-3 5-3 8-3.5c.5 .3 1 .7 1.5 1c2-.5 2-.5 4-2c.9-4.2 1.7-8.3 2.5-12.5c.4-.3 .6-.7 1-1z"/><path fill="#03B5BD" d="M58.5 149.5c2.5 .5 2.5 .5 7 3.5c9.8 5.2 18.9 7.9 30 8.5c2.2 .5 4.3 1 6.5 1.5c7 0 7 0 9-.5c2.3-.2 4.6-.3 7-.5c3.2-.9 6.3-1.9 9.5-3c1.3-.2 2.7-.3 4-.5c.2 .2 .3 .3 .5 .5c-.8 6.3-4.1 10.2-8 15c-.2 .5-.3 1-.5 1.5c1.7-1 3.3-2 5-3c.7 .2 1.3 .3 2 .5c-3 12.2-9.3 20.3-17.5 29.5c-.7 .7-.7 .7-4 4.5c-1 .8-2 1.7-3 2.5c-3.5-.5-3.5-.5-8-5c-2.3-1.8-4.7-3.7-7-5.5c-2.7-2.2-5.3-4.3-8-6.5c-.8-.6-.8-.6-4.5-3.5c-11-9.3-20.7-18.8-28.5-31c3-4 3-4 8.5-8.5z"/><ellipse cx="94" cy="94" rx="6.5" ry="6.5" fill="#212E34"/><ellipse cx="135" cy="85.6" rx="6.2" ry="5.8" fill="#212E34"/><g fill="#FAF9F8"><circle cx="91.5" cy="91.5" r="2.1"/><circle cx="134.6" cy="84.3" r="2"/></g><ellipse cx="124" cy="93.8" rx="3.4" ry="1.1" fill="#9B9D9E" transform="rotate(-6 124 93.8)"/></g></g><rect x="5" y="5" width="230" height="230" rx="45" fill="none"/></symbol></defs>
|
|
2
2
|
<rect x="4" y="4" width="952" height="632" rx="18" fill="#FAF9F8" stroke="#C4CBCE" stroke-width="1.5"/>
|
|
3
3
|
<path d="M4 22a18 18 0 0 1 18-18h916a18 18 0 0 1 18 18v24H4z" fill="#212E34"/>
|
|
4
4
|
<circle cx="26" cy="25" r="5" fill="#C4CBCE" opacity="0.55"/>
|
package/install.sh
CHANGED
|
@@ -9,7 +9,7 @@ set -eu
|
|
|
9
9
|
|
|
10
10
|
# The default is the version this file ships with. The release bump changes it together with
|
|
11
11
|
# package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
|
|
12
|
-
JACKHERD_VERSION="${JACKHERD_VERSION:-1.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.8.0}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
|
-
import { isRecord
|
|
3
|
-
|
|
4
|
-
/** A client's session id as a UUID; nothing else may reach a path. */
|
|
5
|
-
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
6
|
-
|
|
7
|
-
const sessionOf = (pane: JsonObject): string | null => {
|
|
8
|
-
const session = isRecord(pane.agent_session) ? pane.agent_session.value : undefined
|
|
9
|
-
return typeof pane.agent === 'string' && pane.agent !== '' && typeof session === 'string' && SESSION_ID.test(session)
|
|
10
|
-
? session
|
|
11
|
-
: null
|
|
12
|
-
}
|
|
2
|
+
import { isRecord } from '../shared/json'
|
|
3
|
+
import { sessionOf } from '../shared/pane-agent'
|
|
13
4
|
|
|
14
5
|
/**
|
|
15
6
|
* The sessions of the agents that run in the main checkout, from the `result` of `herdr pane list`: the coordinator
|
|
@@ -21,4 +12,3 @@ export const coordinatorSessions = (listing: unknown, main: string): string[] =>
|
|
|
21
12
|
typeof pane.cwd === 'string' && resolve(pane.cwd) === resolve(main) ? (sessionOf(pane) ?? []) : [],
|
|
22
13
|
)
|
|
23
14
|
}
|
|
24
|
-
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// PermissionRequest hook of Claude Code, shipped by jackherd: the client adapter of the approval gate. In a lane
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// into the hook entry by `sync`.
|
|
1
|
+
// PermissionRequest hook of Claude Code, shipped by jackherd: the client adapter of the approval gate. In a lane, known
|
|
2
|
+
// by the herdr agent of the pane and not by the directory of the call, it holds a call that would open a permission
|
|
3
|
+
// dialog, records it for the coordinator and answers with the user's decision from the coordinator's dialog; elsewhere,
|
|
4
|
+
// and on any failure, it answers nothing and the dialog stays. argv[2..5]: the allow label, the deny label, the text for
|
|
5
|
+
// a call still pending after the wait, the text for a denied call, rendered into the hook entry by `sync`.
|
|
6
6
|
import { homedir } from 'node:os'
|
|
7
7
|
import { mainCheckoutOf } from '../shared/checkout'
|
|
8
8
|
import { commandOutput } from '../shared/command'
|
|
9
9
|
import { readConfig } from '../shared/config'
|
|
10
10
|
import { isRecord } from '../shared/json'
|
|
11
|
+
import { ownPaneAgent } from '../shared/own-pane'
|
|
11
12
|
import { coordinatorSessions } from './herdr-panes'
|
|
12
13
|
import { type ClientCall, type GateDependencies, type GateOutcome, runGate } from './run'
|
|
13
14
|
import { approvalsDirectory, claimRequest, pruneRequests, readRequests, writeRequest } from './store'
|
|
@@ -22,15 +23,16 @@ const herdrPanes = (): unknown => {
|
|
|
22
23
|
return answer.result
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/** Claude Code's hook input: `cwd`, `tool_name`, `tool_input`. */
|
|
26
|
+
/** Claude Code's hook input: `cwd`, `session_id`, `tool_name`, `tool_input`. */
|
|
26
27
|
const clientCallOf = (text: string): ClientCall | null => {
|
|
27
28
|
const input: unknown = JSON.parse(text)
|
|
28
29
|
if (!isRecord(input) || typeof input.cwd !== 'string' || typeof input.tool_name !== 'string') return null
|
|
29
|
-
|
|
30
|
+
if (typeof input.session_id !== 'string') return null
|
|
31
|
+
return { cwd: input.cwd, session: input.session_id, tool: input.tool_name, input: input.tool_input ?? null }
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const dependencies: GateDependencies = {
|
|
33
|
-
|
|
35
|
+
paneAgent: () => ownPaneAgent(),
|
|
34
36
|
mainCheckoutOf,
|
|
35
37
|
config: (main) => readConfig([main, homedir()]),
|
|
36
38
|
readRequests: (main) => readRequests(approvalsDirectory(main)),
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import type { DialogAnswer } from '../shared/dialog-answer'
|
|
2
|
+
import { ASK_USER_QUESTION_TOOL } from '../shared/dialog-answers'
|
|
2
3
|
import { isFresh } from '../shared/fresh'
|
|
4
|
+
import { laneOfAgent } from '../shared/lane'
|
|
5
|
+
import type { PaneAgent } from '../shared/pane-agent'
|
|
3
6
|
import { gateDecision, type GateLabels } from './decision'
|
|
4
|
-
import { laneOf } from './lane'
|
|
5
7
|
import { type ApprovalRequest, callHash, type GateCall, isStorableInput, newRequest, requestId } from './request'
|
|
6
8
|
import { gateSettingsOf } from './settings'
|
|
7
9
|
|
|
8
10
|
/** A call as the client's hook hands it over, before it is known to be a lane's. */
|
|
9
|
-
export type ClientCall = { cwd: string; tool: string; input: unknown }
|
|
11
|
+
export type ClientCall = { cwd: string; session: string; tool: string; input: unknown }
|
|
10
12
|
|
|
11
13
|
/** What the gate decided; null decides nothing, so the client's own dialog stays. */
|
|
12
14
|
export type GateOutcome = { kind: 'allow' } | { kind: 'denied' } | { kind: 'pending' } | null
|
|
13
15
|
|
|
14
16
|
/** Everything with a side effect, passed in so a test reaches the flow without a multiplexer, git or files. */
|
|
15
17
|
export type GateDependencies = {
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
+
/** The agent of the multiplexer pane this session runs in; null outside the multiplexer. */
|
|
19
|
+
paneAgent: () => PaneAgent | null
|
|
18
20
|
mainCheckoutOf: (cwd: string) => string
|
|
19
21
|
config: (main: string) => Record<string, unknown>
|
|
20
22
|
readRequests: (main: string) => ApprovalRequest[]
|
|
@@ -70,24 +72,31 @@ const awaitDecision = async (
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/** A question to the user asks for an answer, not for a permission: the lane asks the coordinator in its text instead. */
|
|
76
|
+
const isHeld = ({ tool, input }: ClientCall): boolean => tool !== ASK_USER_QUESTION_TOOL && isStorableInput(input)
|
|
77
|
+
|
|
73
78
|
/**
|
|
74
79
|
* The gate for a call that would open a permission dialog in a lane: it records the call as a request the watcher
|
|
75
|
-
* reports, waits for the user's answer to the coordinator's dialog, and allows or denies.
|
|
76
|
-
*
|
|
80
|
+
* reports, waits for the user's answer to the coordinator's dialog, and allows or denies. The lane is the agent of the
|
|
81
|
+
* pane, never the directory of the call. Outside a lane, for a question or a call too large to store, and on any failure
|
|
82
|
+
* (the multiplexer or git cannot be asked), it decides nothing, so the dialog stays.
|
|
77
83
|
*/
|
|
78
84
|
export const runGate = async (
|
|
79
85
|
clientCall: ClientCall,
|
|
80
86
|
labels: GateLabels,
|
|
81
87
|
dependencies: GateDependencies,
|
|
82
88
|
): Promise<GateOutcome> => {
|
|
83
|
-
if (!
|
|
89
|
+
if (!isHeld(clientCall)) return null
|
|
84
90
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
91
|
+
const agent = dependencies.paneAgent()
|
|
92
|
+
if (agent === null || agent.cwd === null) return null
|
|
93
|
+
const main = dependencies.mainCheckoutOf(agent.cwd)
|
|
94
|
+
const { worktreeDirectory, waitMinutes } = gateSettingsOf(dependencies.config(main))
|
|
95
|
+
const lane = laneOfAgent({ agent, session: clientCall.session, main, worktreeDirectory })
|
|
88
96
|
if (!lane) return null
|
|
89
|
-
const
|
|
90
|
-
|
|
97
|
+
const { cwd, tool, input } = clientCall
|
|
98
|
+
const request = openRequest(main, { lane, cwd, tool, input }, dependencies)
|
|
99
|
+
return await awaitDecision(main, request.hash, labels, waitMinutes, dependencies)
|
|
91
100
|
} catch {
|
|
92
101
|
return null
|
|
93
102
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ASK_USER_QUESTION_TOOL } from '../shared/dialog-answers'
|
|
2
|
+
|
|
1
3
|
export type HintInput = {
|
|
2
4
|
message: string
|
|
3
5
|
tools: string[]
|
|
@@ -8,8 +10,6 @@ export type HintInput = {
|
|
|
8
10
|
|
|
9
11
|
export type HintDecision = { kind: 'silent' } | { kind: 'hint'; message: string }
|
|
10
12
|
|
|
11
|
-
const ASK_TOOL = 'AskUserQuestion'
|
|
12
|
-
|
|
13
13
|
/** A lane prompt names the lane in its first line; a lane started from a hand-over file reads the file first. */
|
|
14
14
|
const LANE_PROMPT = /^\s*(<pasted_content[^>]*>\s*)?(Du bist die Spur|You are the lane|Read `\.claude\/handover\/)/i
|
|
15
15
|
|
|
@@ -23,6 +23,6 @@ export const isLanePrompt = (firstPrompt: string): boolean => LANE_PROMPT.test(f
|
|
|
23
23
|
|
|
24
24
|
export const decideHint = (input: HintInput): HintDecision => {
|
|
25
25
|
if (!input.enabled || input.stopHookActive || input.lane) return { kind: 'silent' }
|
|
26
|
-
if (!endsWithQuestion(input.message) || input.tools.includes(
|
|
26
|
+
if (!endsWithQuestion(input.message) || input.tools.includes(ASK_USER_QUESTION_TOOL)) return { kind: 'silent' }
|
|
27
27
|
return { kind: 'hint', message: HINT }
|
|
28
28
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { posix } from 'node:path'
|
|
2
|
+
import { commandTextRunnerIndex, searchableText } from '../shared/command-text-runners'
|
|
2
3
|
import {
|
|
3
4
|
ASSIGNMENT,
|
|
4
5
|
inlineScript,
|
|
@@ -7,8 +8,26 @@ import {
|
|
|
7
8
|
programName,
|
|
8
9
|
readsScriptFromInput,
|
|
9
10
|
ruleStart,
|
|
11
|
+
runsInterpreterProgram,
|
|
10
12
|
} from '../shared/program-position'
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
expandingHereDocumentAdvice,
|
|
15
|
+
isExpanded,
|
|
16
|
+
parseShell,
|
|
17
|
+
ShellParseError,
|
|
18
|
+
type SimpleCommand,
|
|
19
|
+
UNREADABLE_ADVICE,
|
|
20
|
+
type Word,
|
|
21
|
+
} from '../shared/shell'
|
|
22
|
+
import {
|
|
23
|
+
definesShellAlias,
|
|
24
|
+
mayRunDefinedFunction,
|
|
25
|
+
NO_DEFINITIONS,
|
|
26
|
+
type ShellDefinitions,
|
|
27
|
+
shellDefinitionAt,
|
|
28
|
+
shellDefinitionHint,
|
|
29
|
+
} from '../shared/shell-aliases'
|
|
30
|
+
import { type PushCall, type PushContext, pushTargetDenial } from './push-target'
|
|
12
31
|
|
|
13
32
|
export type GitGuardDecision = { kind: 'pass' } | { kind: 'deny'; reason: string }
|
|
14
33
|
|
|
@@ -64,6 +83,9 @@ type SplitArguments = { options: string[]; operands: string[] }
|
|
|
64
83
|
|
|
65
84
|
const PASS: GitGuardDecision = { kind: 'pass' }
|
|
66
85
|
|
|
86
|
+
/** The programs whose alias or function the guard refuses: one that runs git or gh hides it from the rules (D42). */
|
|
87
|
+
const DEFINITION_PROGRAMS = ['git', 'gh']
|
|
88
|
+
|
|
67
89
|
const FORCE_PUSH =
|
|
68
90
|
'A force push is refused. Use `--force-with-lease` instead, and only after the user agreed to rewrite the remote branch.'
|
|
69
91
|
const SKIP_HOOKS =
|
|
@@ -75,6 +97,10 @@ const PROGRAM_HINT =
|
|
|
75
97
|
'Refused: the program is expanded from a variable or a substitution and is followed by a protected git verb, so the permission rules cannot see it. Write `git` plainly.'
|
|
76
98
|
const BRACE_HINT =
|
|
77
99
|
'Refused: an option of this git call is written as a brace expansion, so neither the permission rules nor this hook can tell what git receives. Write the option plainly.'
|
|
100
|
+
const INLINE_EXPANSION_HINT =
|
|
101
|
+
'Refused: `eval` or `sh -c` runs a script held in a variable or a substitution, so neither the permission rules nor this hook can read it. Write the commands out.'
|
|
102
|
+
const EXPANDED_FUNCTION_HINT =
|
|
103
|
+
'Refused: the program is expanded from a variable or a substitution, and it may name a shell function that runs git or gh, which hides the command from the permission rules. Run the command written out instead.'
|
|
78
104
|
const ALIAS_HINT =
|
|
79
105
|
'Refused: `-c alias.…` defines a git alias inline, which hides the verb from the permission rules. Run the git command it stands for directly.'
|
|
80
106
|
|
|
@@ -149,6 +175,19 @@ const HOOK_SKIPPING_VARIABLE = /^(HUSKY|LEFTHOOK|SKIP)\+?=/
|
|
|
149
175
|
const REDIRECTING_VARIABLE = /^(GIT_CONFIG\w*|GIT_DIR|GIT_WORK_TREE|GIT_INDEX_FILE)\+?=/
|
|
150
176
|
/** An option the shell rewrites before git sees it: `--forc{e,}` reaches git as `--force`. */
|
|
151
177
|
const BRACE_IN_OPTION = /^-.*\{.*(,|\.\.).*\}/
|
|
178
|
+
/** Push options that send every local branch, the base branch among them. */
|
|
179
|
+
const ALL_BRANCHES = ['--all', '--branches', '--mirror']
|
|
180
|
+
const DIRECTORY_CHANGERS = new Set(['cd', 'pushd', 'popd'])
|
|
181
|
+
/** Git verbs that can change which branch `HEAD` names, or where a push of it goes. */
|
|
182
|
+
const BRANCH_MOVING_VERBS = new Set([
|
|
183
|
+
PROTECTED_VERB.checkout,
|
|
184
|
+
'switch',
|
|
185
|
+
'branch',
|
|
186
|
+
'config',
|
|
187
|
+
'symbolic-ref',
|
|
188
|
+
'update-ref',
|
|
189
|
+
'worktree',
|
|
190
|
+
])
|
|
152
191
|
const PROTECTED_MENTION = new RegExp(`\\bgit\\b[\\s\\S]*\\b(${Object.values(PROTECTED_VERB).join('|')})\\b`)
|
|
153
192
|
|
|
154
193
|
const isProtectedVerb = (value: string): value is ProtectedVerb =>
|
|
@@ -320,16 +359,65 @@ const denyIfMentionsProtectedVerb = (script: string, reason: string): GitGuardDe
|
|
|
320
359
|
return verb ? deny(`Refused: ${reason}, and it mentions \`git ${verb}\`. ${plainInstead(verb)}`) : PASS
|
|
321
360
|
}
|
|
322
361
|
|
|
323
|
-
const
|
|
362
|
+
const judgeDefinition = (
|
|
363
|
+
words: SimpleCommand,
|
|
364
|
+
index: number,
|
|
365
|
+
definitions: () => ShellDefinitions,
|
|
366
|
+
): GitGuardDecision => {
|
|
367
|
+
const definition = shellDefinitionAt(words, index, definitions, DEFINITION_PROGRAMS)
|
|
368
|
+
return definition ? deny(`Refused: ${shellDefinitionHint(definition)}`) : PASS
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A program that runs command text it is given (`watch '…'`, sed's `e`) is refused when that text names a protected
|
|
373
|
+
* verb; when the text is itself an expansion, the searchable script is searched instead.
|
|
374
|
+
*/
|
|
375
|
+
const judgeCommandText = (words: SimpleCommand, searchable: string): GitGuardDecision => {
|
|
376
|
+
const runner = commandTextRunnerIndex(words)
|
|
377
|
+
if (runner === -1) return PASS
|
|
378
|
+
const given = words.slice(runner + 1)
|
|
379
|
+
const text = given.some(({ value }) => isExpanded(value)) ? searchable : given.map(({ value }) => value).join(' ')
|
|
380
|
+
return denyIfMentionsProtectedVerb(text, 'a program runs command text it is given, which no rule inspects')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const judgeWord = (
|
|
384
|
+
words: SimpleCommand,
|
|
385
|
+
index: number,
|
|
386
|
+
context: ScriptContext,
|
|
387
|
+
definitions: () => ShellDefinitions,
|
|
388
|
+
searchable: string,
|
|
389
|
+
): GitGuardDecision => {
|
|
324
390
|
const script = inlineScript(words, index)
|
|
325
|
-
if (script !== undefined) return judgeScript(script, SCRIPT_CONTEXT.nested)
|
|
326
|
-
if (!isGit(words[index])) return
|
|
391
|
+
if (script !== undefined) return judgeScript(script, SCRIPT_CONTEXT.nested, definitions, searchable)
|
|
392
|
+
if (!isGit(words[index])) return judgeDefinition(words, index, definitions)
|
|
327
393
|
const invocation = readInvocation(words, index)
|
|
328
394
|
return invocation ? judgeInvocation(invocation, gitPosition(words, index, context)) : PASS
|
|
329
395
|
}
|
|
330
396
|
|
|
331
|
-
|
|
332
|
-
|
|
397
|
+
/** Inside `eval` or `sh -c`, an expanded program means the script was held in a variable: refused after D22. */
|
|
398
|
+
const judgeInlineExpansion = (words: SimpleCommand, context: ScriptContext): GitGuardDecision =>
|
|
399
|
+
context === SCRIPT_CONTEXT.nested && runsExpandedProgram(words) ? deny(INLINE_EXPANSION_HINT) : PASS
|
|
400
|
+
|
|
401
|
+
const judgeExpandedFunction = (
|
|
402
|
+
words: SimpleCommand,
|
|
403
|
+
definitions: () => ShellDefinitions,
|
|
404
|
+
searchable: string,
|
|
405
|
+
): GitGuardDecision =>
|
|
406
|
+
mayRunDefinedFunction(words, definitions, DEFINITION_PROGRAMS, searchable) ? deny(EXPANDED_FUNCTION_HINT) : PASS
|
|
407
|
+
|
|
408
|
+
const judgeCommand = (
|
|
409
|
+
words: SimpleCommand,
|
|
410
|
+
context: ScriptContext,
|
|
411
|
+
definitions: () => ShellDefinitions,
|
|
412
|
+
searchable: string,
|
|
413
|
+
): GitGuardDecision =>
|
|
414
|
+
firstDenial([
|
|
415
|
+
...words.map((_word, index) => judgeWord(words, index, context, definitions, searchable)),
|
|
416
|
+
judgeInlineExpansion(words, context),
|
|
417
|
+
judgeExpandedProgram(words),
|
|
418
|
+
judgeCommandText(words, searchable),
|
|
419
|
+
judgeExpandedFunction(words, definitions, searchable),
|
|
420
|
+
])
|
|
333
421
|
|
|
334
422
|
/**
|
|
335
423
|
* What no single command shows. Each check reads the whole script, so a harmless git call beside
|
|
@@ -340,21 +428,101 @@ const judgeScriptWide = (script: string, commands: SimpleCommand[]): GitGuardDec
|
|
|
340
428
|
if (commands.some(readsScriptFromInput)) {
|
|
341
429
|
return denyIfMentionsProtectedVerb(script, 'a shell reads its script from input here, which no rule inspects')
|
|
342
430
|
}
|
|
431
|
+
if (commands.some(runsInterpreterProgram)) {
|
|
432
|
+
return denyIfMentionsProtectedVerb(
|
|
433
|
+
script,
|
|
434
|
+
'an interpreter runs program text given inline or on its input, which no rule inspects',
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
if (commands.some(definesShellAlias)) {
|
|
438
|
+
return denyIfMentionsProtectedVerb(
|
|
439
|
+
script,
|
|
440
|
+
'the script defines a shell alias, which hides its command from the rules',
|
|
441
|
+
)
|
|
442
|
+
}
|
|
343
443
|
if (commands.some(setsGitVariable)) {
|
|
344
|
-
return denyIfMentionsProtectedVerb(
|
|
444
|
+
return denyIfMentionsProtectedVerb(
|
|
445
|
+
script,
|
|
446
|
+
'a `HUSKY`, `LEFTHOOK`, `SKIP` or `GIT_*` variable is set, which changes the git calls',
|
|
447
|
+
)
|
|
345
448
|
}
|
|
346
449
|
if (commands.some(runsExpandedProgram)) {
|
|
347
|
-
return denyIfMentionsProtectedVerb(
|
|
450
|
+
return denyIfMentionsProtectedVerb(
|
|
451
|
+
script,
|
|
452
|
+
'a program is expanded from a variable or a substitution and could be git',
|
|
453
|
+
)
|
|
348
454
|
}
|
|
349
455
|
return PASS
|
|
350
456
|
}
|
|
351
457
|
|
|
352
|
-
|
|
458
|
+
/** `whole` is the text the mention search reads at the outer level: a nested script's variables are set there. */
|
|
459
|
+
const judgeParsed = (
|
|
460
|
+
commands: SimpleCommand[],
|
|
461
|
+
context: ScriptContext,
|
|
462
|
+
definitions: () => ShellDefinitions,
|
|
463
|
+
whole: string,
|
|
464
|
+
): GitGuardDecision => {
|
|
465
|
+
const text = searchableText(whole, commands)
|
|
466
|
+
return firstDenial([
|
|
467
|
+
...commands.map((words) => judgeCommand(words, context, definitions, text)),
|
|
468
|
+
judgeScriptWide(text, commands),
|
|
469
|
+
])
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const judgeScript = (
|
|
473
|
+
script: string,
|
|
474
|
+
context: ScriptContext,
|
|
475
|
+
definitions: () => ShellDefinitions,
|
|
476
|
+
whole: string,
|
|
477
|
+
): GitGuardDecision => judgeParsed(parseShell(script), context, definitions, whole)
|
|
478
|
+
|
|
479
|
+
/** A `cd` or a git call that switches, renames or reconfigures the branch: `HEAD` at a later push is another one. */
|
|
480
|
+
const movesHead = (words: SimpleCommand): boolean =>
|
|
481
|
+
words.some((word, index) => {
|
|
482
|
+
if (!isProgramPosition(words, index)) return false
|
|
483
|
+
if (DIRECTORY_CHANGERS.has(programName(word))) return true
|
|
484
|
+
return isGit(word) && BRANCH_MOVING_VERBS.has(readInvocation(words, index)?.verb.value ?? '')
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
/** A plain `git push` where a rule matches; every other form of it is refused before this is asked. */
|
|
488
|
+
const pushCallOf = (words: SimpleCommand, headMoves: boolean): PushCall | undefined => {
|
|
489
|
+
const start = ruleStart(words)
|
|
490
|
+
const program = words.at(start)
|
|
491
|
+
if (program === undefined || !isGit(program) || words[start + 1]?.value !== PROTECTED_VERB.push) return undefined
|
|
492
|
+
const { options, operands } = splitArguments(words.slice(start + 2), VERB_SYNTAX.push)
|
|
493
|
+
const allBranches = options.some((option) => ALL_BRANCHES.some((name) => matchesLong(option, name)))
|
|
494
|
+
return { operands, allBranches, headMoves }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const judgePushes = (commands: SimpleCommand[], context: PushContext): GitGuardDecision => {
|
|
498
|
+
const headMoves = commands.some(movesHead)
|
|
499
|
+
const calls = commands.flatMap((words) => pushCallOf(words, headMoves) ?? [])
|
|
500
|
+
const reason = calls.map((call) => pushTargetDenial(call, context)).find((each) => each !== undefined)
|
|
501
|
+
return reason ? deny(reason) : PASS
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Quotes split a word (`g\it`, `pu''sh`), an expansion hides a command: in text the parse cannot follow, no search
|
|
506
|
+
* can tell whether a protected git verb runs, so every unreadable script is refused (fail closed).
|
|
507
|
+
*/
|
|
508
|
+
const denyUnreadable = ({ message }: ShellParseError): GitGuardDecision =>
|
|
509
|
+
deny(
|
|
510
|
+
`Refused: the command could not be read (${message}), so neither the permission rules nor this hook can tell whether it runs git. ${UNREADABLE_ADVICE}`,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
/** A refusal of a script with an expanding here-document says how to keep its text from running. */
|
|
514
|
+
const withHereDocumentAdvice = (decision: GitGuardDecision, script: string): GitGuardDecision => {
|
|
515
|
+
const advice = expandingHereDocumentAdvice(script)
|
|
516
|
+
return decision.kind === 'deny' && advice !== '' ? deny(`${decision.reason} ${advice}`) : decision
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const judgeTop = (command: string, push: PushContext, definitions: () => ShellDefinitions): GitGuardDecision => {
|
|
353
520
|
try {
|
|
354
|
-
const commands = parseShell(
|
|
355
|
-
|
|
521
|
+
const commands = parseShell(command)
|
|
522
|
+
const decision = judgeParsed(commands, SCRIPT_CONTEXT.top, definitions, command)
|
|
523
|
+
return decision.kind === 'deny' ? decision : judgePushes(commands, push)
|
|
356
524
|
} catch (error) {
|
|
357
|
-
if (error instanceof ShellParseError) return
|
|
525
|
+
if (error instanceof ShellParseError) return denyUnreadable(error)
|
|
358
526
|
throw error
|
|
359
527
|
}
|
|
360
528
|
}
|
|
@@ -363,7 +531,13 @@ const judgeScript = (script: string, context: ScriptContext): GitGuardDecision =
|
|
|
363
531
|
* Decides on a Bash command before it runs. Denies a protected git verb that a permission rule
|
|
364
532
|
* written as `git <verb> …` would not match (options or `GIT_*` variables before the verb, a
|
|
365
533
|
* wrapper, a path to git, a quoted verb, a line continuation, `sh -c`, `eval`, a script piped into
|
|
366
|
-
* a shell), and denies a force push, a skipped hook and a hard reset in any
|
|
367
|
-
*
|
|
534
|
+
* a shell, an interpreter's program text), and denies a force push, a skipped hook and a hard reset in any
|
|
535
|
+
* form. Denies a push that updates the base branch, or whose target it cannot tell, every script it cannot read,
|
|
536
|
+
* and a shell alias or function (`definitions`, read only for a word that is no known program) that runs git or gh.
|
|
537
|
+
* Everything else passes on to the project's own permission rules.
|
|
368
538
|
*/
|
|
369
|
-
export const decideBashCommand = (
|
|
539
|
+
export const decideBashCommand = (
|
|
540
|
+
command: string,
|
|
541
|
+
push: PushContext,
|
|
542
|
+
definitions: () => ShellDefinitions = () => NO_DEFINITIONS,
|
|
543
|
+
): GitGuardDecision => withHereDocumentAdvice(judgeTop(command, push, definitions), command)
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
// PreToolUse hook for Bash, shipped by jackherd. Reads the hook input on stdin and
|
|
2
2
|
// answers with a deny decision on stdout, or with nothing to leave the call to the permission rules.
|
|
3
3
|
// Any failure denies: a guard that crashes must not wave the command through.
|
|
4
|
+
import { once } from '../shared/once'
|
|
5
|
+
import { readShellDefinitions } from '../shared/shell-snapshots'
|
|
4
6
|
import { decideBashCommand, type GitGuardDecision } from './decision'
|
|
7
|
+
import { repositoryPushContext } from './repository'
|
|
5
8
|
|
|
6
|
-
type HookInput = { tool_input?: { command?: unknown } }
|
|
9
|
+
type HookInput = { cwd?: unknown; tool_input?: { command?: unknown } }
|
|
7
10
|
|
|
8
11
|
const decideInput = (text: string): GitGuardDecision | undefined => {
|
|
9
|
-
const
|
|
10
|
-
|
|
12
|
+
const input = JSON.parse(text) as HookInput
|
|
13
|
+
const command = input.tool_input?.command
|
|
14
|
+
const cwd = typeof input.cwd === 'string' ? input.cwd : process.cwd()
|
|
15
|
+
if (typeof command !== 'string') return undefined
|
|
16
|
+
return decideBashCommand(
|
|
17
|
+
command,
|
|
18
|
+
repositoryPushContext(cwd),
|
|
19
|
+
once(() => readShellDefinitions()),
|
|
20
|
+
)
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
const failureDecision = (error: unknown): GitGuardDecision => ({
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { expandsBraces, isExpanded } from '../shared/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What only the repository knows about a push, asked only when the script holds one: a guard that reads the config
|
|
5
|
+
* or runs git for every Bash call would cost each of them.
|
|
6
|
+
*/
|
|
7
|
+
export type PushContext = {
|
|
8
|
+
/** The project's base branch; undefined when the config cannot be read or names one that is no branch name. */
|
|
9
|
+
baseBranch: () => string | undefined
|
|
10
|
+
/**
|
|
11
|
+
* The branches a push without a refspec, or with `HEAD`, updates: the current branch and the one its push goes to;
|
|
12
|
+
* undefined when git cannot tell (a detached HEAD).
|
|
13
|
+
*/
|
|
14
|
+
defaultPushBranches: () => string[] | undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A plain `git push` of the script, as the guard reads it. */
|
|
18
|
+
export type PushCall = {
|
|
19
|
+
/** The words after `git push` that are neither options nor their values: the remote and the refspecs. */
|
|
20
|
+
operands: string[]
|
|
21
|
+
/** `--all`, `--branches` or `--mirror`: every local branch, the base branch among them. */
|
|
22
|
+
allBranches: boolean
|
|
23
|
+
/** The script changes directory or branch, so the branch `HEAD` names at the push is not the one git reports now. */
|
|
24
|
+
headMoves: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HEAD_NAMES = new Set(['HEAD', '@'])
|
|
28
|
+
/** A `?` or `[` glob; `*` is a refspec wildcard as well and counts as the base branch below. */
|
|
29
|
+
const GLOB = /[?[]/
|
|
30
|
+
const BRANCH_PREFIX = /^(refs\/)?(heads\/)?/
|
|
31
|
+
|
|
32
|
+
const branchOf = (ref: string): string => ref.replace(BRANCH_PREFIX, '')
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The branch a refspec updates on the remote: the part after `:`, or the source when there is none (`main`,
|
|
36
|
+
* `+main`, `main:`); empty for the matching refspec `:`; undefined for `HEAD`, which pushes the current branch under its
|
|
37
|
+
* push name.
|
|
38
|
+
*/
|
|
39
|
+
const destinationOf = (refspec: string): string | undefined => {
|
|
40
|
+
const plain = refspec.replace(/^\+/, '')
|
|
41
|
+
const colon = plain.indexOf(':')
|
|
42
|
+
const destination = colon === -1 || colon === plain.length - 1 ? plain.replace(/:$/, '') : plain.slice(colon + 1)
|
|
43
|
+
return HEAD_NAMES.has(destination) ? undefined : branchOf(destination)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isRewrittenByShell = (operand: string): boolean => GLOB.test(operand) || expandsBraces(operand)
|
|
47
|
+
|
|
48
|
+
/** A wildcard refspec, or the matching refspec `:` whose empty destination stands for every branch both sides have. */
|
|
49
|
+
const reachesEveryBranch = (destination: string | undefined): boolean =>
|
|
50
|
+
destination !== undefined && (destination === '' || destination.includes('*'))
|
|
51
|
+
|
|
52
|
+
const toBase = (base: string): string =>
|
|
53
|
+
`Refused: this push updates \`${base}\`, the base branch (baseBranch in .jackherd/config.json), and goes around the merge guard. Push your own branch and merge it through a pull request.`
|
|
54
|
+
|
|
55
|
+
const unknownTarget = (why: string): string =>
|
|
56
|
+
`Refused: the guard cannot tell which branch this push updates (${why}), and a push to the base branch goes around the merge guard. Name the branch plainly, \`git push origin <branch>\`, as a command of its own.`
|
|
57
|
+
|
|
58
|
+
const INVALID_BASE =
|
|
59
|
+
'Refused: .jackherd/config.json cannot be read or its baseBranch is no branch name, so the guard cannot tell whether this push updates the base branch. Fix the config.'
|
|
60
|
+
|
|
61
|
+
/** Whether HEAD decides the target: no refspec, or one whose destination is `HEAD`. */
|
|
62
|
+
const pushesHead = (operands: string[], destinations: (string | undefined)[]): boolean =>
|
|
63
|
+
operands.length <= 1 || destinations.includes(undefined)
|
|
64
|
+
|
|
65
|
+
const headDenial = (call: PushCall, context: PushContext, base: string): string | undefined => {
|
|
66
|
+
if (call.headMoves) return unknownTarget('the script changes directory or branch before it')
|
|
67
|
+
const branches = context.defaultPushBranches()
|
|
68
|
+
if (branches === undefined) return unknownTarget('git cannot tell the current branch')
|
|
69
|
+
return branches.some((branch) => branchOf(branch) === base) ? toBase(base) : undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Why a plain `git push` is refused, or undefined when it does not reach the base branch. Every operand counts as a
|
|
74
|
+
* refspec, the remote too (`--repo` makes the first one a refspec): that can only refuse more, never less.
|
|
75
|
+
*/
|
|
76
|
+
export const pushTargetDenial = (call: PushCall, context: PushContext): string | undefined => {
|
|
77
|
+
if (call.operands.some(isExpanded)) return unknownTarget('a refspec is expanded')
|
|
78
|
+
if (call.operands.some(isRewrittenByShell)) return unknownTarget('the shell rewrites a refspec by a glob or braces')
|
|
79
|
+
const base = context.baseBranch()
|
|
80
|
+
if (base === undefined) return INVALID_BASE
|
|
81
|
+
if (call.allBranches) return toBase(base)
|
|
82
|
+
const destinations = call.operands.map(destinationOf)
|
|
83
|
+
if (destinations.some((destination) => reachesEveryBranch(destination) || destination === base)) return toBase(base)
|
|
84
|
+
return pushesHead(call.operands, destinations) ? headDenial(call, context, base) : undefined
|
|
85
|
+
}
|