@agentproto/worktree 0.4.1 → 0.4.3

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.
@@ -0,0 +1,113 @@
1
+ # Routines here have no runtime yet — a bridge proposal (DESIGN ONLY)
2
+
3
+ `worktree-gc/ROUTINE.md` in this directory is a real, schema-valid AIP-41
4
+ routine manifest (`schema: routine/v1`), shipped `enabled: false` since PR
5
+ #626. Nothing in this repo turns it into a scheduled fire. This note records
6
+ the gap precisely and sketches a bridge — **not built, not decided**; the
7
+ call on whether/how to build it belongs to Jeremy.
8
+
9
+ ## The gap, precisely
10
+
11
+ - `@agentproto/routine` (`packages/routine/src`) parses and validates a
12
+ `ROUTINE.md` — `parseRoutineManifest` → `routineFromManifest` →
13
+ `defineRoutine` (`define-routine.ts`) — into a frozen `RoutineHandle`. That
14
+ handle is **pure data**. `defineRoutine`'s `build()` just spreads the
15
+ validated definition into a new object; there is no `execute()`, no
16
+ scheduling hook, nothing that fires anything.
17
+ - Nothing scans a workspace's `.routines/` directory. `grep`ing the whole
18
+ `packages/` tree for a reader of `.routines/*/ROUTINE.md` outside this
19
+ package's own manifest parser and its tests turns up nothing.
20
+ - The one LIVE scheduling primitive in the daemon is
21
+ `packages/runtime/src/cron-scheduler.ts`'s `CronScheduler` — persisted,
22
+ ticks every 20s, fires a `CronAction`. Its `CronAction` union
23
+ (`cron-scheduler.ts:50-72`) has exactly three kinds: `command` (spawn a
24
+ shell command), `agent` (spawn a fresh agent session), `prompt-session`
25
+ (reprompt an existing one). **None of them is "call an in-process
26
+ AIP-14 tool by id."**
27
+ - AIP-41's own `TargetTool` (`packages/routine/src/types.ts:163-171`) is
28
+ exactly that: `{ tool: string, inputs?: {...} }` — a TOOL ref, not a shell
29
+ command or an agent prompt. There is no `CronAction` shape it maps onto
30
+ today.
31
+
32
+ That mismatch is why the daily worktree-gc sweep operationalized today as a
33
+ `kind:"command"` cron job (`worktree-janitor-daily`) that shells out to
34
+ `agentproto worktree gc --apply` — a subprocess, re-spawning a whole CLI
35
+ process, re-resolving `git` from scratch — instead of the daemon just
36
+ calling its own already-registered `worktree_gc` tool in-process, the way
37
+ `worktree_gc` MCP calls do it today. That subprocess indirection is exactly
38
+ where Fix A's `spawn git ENOENT` (a narrow inherited PATH baked into the
39
+ launchd plist) came from. A `kind:"tool"` bridge doesn't just close the
40
+ routine-runtime gap — it removes the entire class of "spawn a CLI to call a
41
+ tool the daemon already hosts" bugs for anything wired through it.
42
+
43
+ ## Proposed bridge (not built)
44
+
45
+ 1. **New `CronAction` kind: `"tool"`.**
46
+ ```ts
47
+ | { kind: "tool"; tool: string; inputs?: Record<string, unknown> }
48
+ ```
49
+ Dispatched in-process against the same tool registry the daemon's MCP
50
+ surface already calls `worktree_gc` through — no subprocess, so no PATH
51
+ to get wrong. Whether an argv-prefix-style allowlist (mirroring
52
+ `command-tools.ts`'s `AllowlistEntry`) should gate which tool ids a cron
53
+ job may target is an open question below, not resolved here.
54
+
55
+ 2. **A registrar** — a new module, e.g.
56
+ `packages/runtime/src/routine-cron-bridge.ts` — that:
57
+ - Scans `<workspace>/.routines/*/ROUTINE.md` (the path the existing
58
+ `worktree-gc/ROUTINE.md` doc already documents as the install target).
59
+ - Parses each with `@agentproto/routine`'s existing
60
+ `parseRoutineManifest` / `routineFromManifest` — reuse, not a second
61
+ parser.
62
+ - Keeps only `enabled: true` entries whose `schedule.kind === "cron"`
63
+ (interval/calendar/manual/event schedules are out of scope for a
64
+ *cron*-scheduler bridge by construction; see open questions).
65
+ - Maps `target`: a `TargetTool` becomes `{kind:"tool", tool, inputs}`
66
+ directly. A `TargetAction` or `TargetWorkflow` target has no obvious
67
+ `CronAction` mapping yet — v1 logs "unsupported target kind" and skips
68
+ rather than guessing.
69
+ - Calls `cronScheduler.create(...)`, keyed so a re-scan **upserts**
70
+ rather than accumulates duplicates — needs a stable identifier
71
+ (e.g. a `sourceRoutineId` field added to `CronJob`, or a deterministic
72
+ `label: "routine:<id>"` the registrar diffs against) so editing or
73
+ removing a `ROUTINE.md` is reflected, not just additive.
74
+ - `@agentproto/routine`'s only deps are `gray-matter` + `zod` (checked:
75
+ `packages/routine/package.json`) — light enough that `@agentproto/runtime`
76
+ can depend on it directly, unlike `@agentproto/worktree`, which
77
+ `worktree-gc.ts`'s own docblock deliberately keeps out of the runtime's
78
+ dependency graph as "the heavy dependency."
79
+
80
+ 3. **Trigger points**: run the scan once at daemon boot (`serve.ts`, next to
81
+ where `cron-scheduler.ts` is already constructed), plus a new explicit
82
+ verb (`agentproto routine reload` / a `routines_reload` MCP tool,
83
+ mirroring the existing `cron_list`/`cron_create` surface) so editing a
84
+ `.routines/*.md` doesn't require a daemon restart to take effect.
85
+
86
+ ## Open questions (Jeremy's call, not resolved here)
87
+
88
+ - **Retry / failure routing.** `CronJob` has no `retry` or `on_failure`
89
+ fields today (`cron-scheduler.ts:74-88`) — a fire either succeeds or
90
+ throws once. AIP-41's `retry.max_attempts` / `on_failure.create_work_item`
91
+ / `fires_events` (AIP-37 vocabulary) have no home on the cron side yet.
92
+ `worktree-gc/ROUTINE.md`'s own `retry.max_attempts: 1` happens to already
93
+ match cron's today-single-attempt behavior, which is why this particular
94
+ routine doesn't surface the gap — a routine that actually wants retries
95
+ or `fires_events` would.
96
+ - **Allowlisting `kind:"tool"` cron jobs.** `kind:"command"` gates through
97
+ `.agentproto/allowed-commands.json`. Should an in-process tool call get an
98
+ analogous allowlist, or is "the tool is already registered on this
99
+ daemon" sufficient authorization? Registered tools can mutate a lot
100
+ (`worktree_gc --apply` deletes branches and directories) — this deserves
101
+ its own decision, not a default inherited from the command-allowlist
102
+ model by accident.
103
+ - **Where the bridge module lives.** Proposed above as
104
+ `packages/runtime/src/routine-cron-bridge.ts`, but it could equally live
105
+ in the CLI host (next to `makeWorktreeGcRunner`) if the intent is to keep
106
+ `packages/runtime` free of anything that isn't a pure port/injection
107
+ boundary.
108
+ - **Non-cron schedules** (`interval`, `calendar`, `manual`, `event`) aren't
109
+ addressed at all — this proposal only closes the `schedule.kind: "cron"`
110
+ case, which is what `worktree-gc/ROUTINE.md` uses.
111
+ - **Multi-workspace scope.** v1 above only scans the active workspace's
112
+ `.routines/`, matching how `worktree_status` / `worktree_gc` themselves
113
+ resolve a repo today (`resolveWorktreeQueryRoot` → `getActiveWorkspace`).
@@ -0,0 +1,96 @@
1
+ ---
2
+ schema: routine/v1
3
+ id: worktree-gc
4
+ description: |
5
+ Scheduled garbage collection of merged/integrated git worktrees. Fires the
6
+ `worktree_gc` tool with `apply: true` so worktrees whose branch is merged
7
+ (or fresh) and whose tree is clean are reclaimed hands-off — the worktree is
8
+ removed and its branch deleted. An OPEN PR, live sessions, or anything
9
+ unresolved is always held and never touched; a dirty-but-integrated worktree
10
+ is left in place unless `salvageDirty` is turned on (it is not, here).
11
+ Ships DISABLED (`enabled: false`) — install it into a workspace's `.routines/`
12
+ and flip `enabled: true` to activate.
13
+ version: "1.0.0"
14
+ schedule:
15
+ kind: cron
16
+ cron: "0 4 * * *"
17
+ timezone: "UTC"
18
+ catchup: skip
19
+ target:
20
+ tool: worktree_gc
21
+ inputs:
22
+ apply: true
23
+ salvageDirty: false
24
+ retry:
25
+ max_attempts: 1
26
+ backoff: fixed
27
+ on_failure:
28
+ create_work_item: true
29
+ fire_event: worktree.gc.failed
30
+ fires_events:
31
+ - worktree.gc.completed
32
+ - worktree.gc.failed
33
+ enabled: false
34
+ tags: [worktree, gc, maintenance]
35
+ ---
36
+
37
+ # Worktree GC Routine
38
+
39
+ Runs daily at **04:00 UTC** and invokes the `worktree_gc` tool (shipped in
40
+ [`@agentproto/worktree`](../../README.md), exposed over MCP + HTTP by the
41
+ daemon) with `apply: true`. This reaps merged worktrees automatically so a
42
+ long-running workspace doesn't accumulate stale `_worktrees/*` trees and their
43
+ dead `wt/*` branches.
44
+
45
+ ## What it does
46
+
47
+ Each fire re-runs the same plan/apply engine (`planGc` / `applyGc` in
48
+ `packages/worktree/src/gc.ts`) that backs `agentproto worktree gc --apply`:
49
+
50
+ - **`reclaim`** — branch is merged (or fresh/zero-commit) **and** the tree is
51
+ clean → the worktree is removed and its branch deleted.
52
+ - **`salvage`** — integrated but the tree is dirty → **held** here, because
53
+ `salvageDirty` is `false`. Turn that input on only if you want dirty
54
+ integrated worktrees archived (snapshot-then-remove) rather than left alone.
55
+ - **`hold`** — an open PR, live sessions, or anything unresolved → never
56
+ touched, regardless of any flag.
57
+
58
+ Every entry is re-classified from scratch immediately before it is acted on,
59
+ so a plan that has gone stale between planning and applying is refused rather
60
+ than acted on.
61
+
62
+ ## Safety invariants (inherited from the engine)
63
+
64
+ These are enforced by the engine, not by this manifest — the routine cannot
65
+ weaken them:
66
+
67
+ - **Merge-gated reclaim.** Teardown only fires when integration ∈
68
+ {merged, fresh} **and** the tree is clean.
69
+ - **Open = hold.** A worktree with an open PR or live sessions is always held.
70
+ - **Dirty = salvage-only.** A dirty integrated worktree is only ever archived
71
+ (never silently discarded), and only when `salvageDirty` is `true`.
72
+
73
+ ## Repo scope at fire time
74
+
75
+ `target.inputs` carries no `repoRoot`, so the daemon resolves the repo to gc
76
+ from the **active workspace** (`resolveWorktreeQueryRoot` →
77
+ `getActiveWorkspace`). Pin a specific repo by adding `repoRoot: <abs path>` or
78
+ `workspaceSlug: <slug>` to `target.inputs` before enabling — `repoRoot` wins
79
+ over `workspaceSlug`, which wins over the active workspace.
80
+
81
+ ## Enabling
82
+
83
+ This routine is opt-in. To activate it in a workspace:
84
+
85
+ 1. Copy this directory to the workspace's routine library —
86
+ `<workspace>/.routines/worktree-gc/ROUTINE.md` (the path
87
+ `@agentproto/routine`'s `routineSpec.pathOf` expects).
88
+ 2. Set `enabled: true` in the frontmatter (and, if desired, pin `repoRoot` /
89
+ `workspaceSlug` per above).
90
+ 3. Reload routines so the daemon registers the schedule.
91
+
92
+ ## Failure routing
93
+
94
+ If a fire fails, `retry` gives it a single attempt (no backoff), then
95
+ `on_failure` opens a work item and fires `worktree.gc.failed`. A clean run
96
+ fires `worktree.gc.completed`.
@@ -0,0 +1,70 @@
1
+ ---
2
+ schema: routine/v1
3
+ id: worktree-gc-notify
4
+ description: |
5
+ Scheduled worktree gc + Telegram notification, as ONE `target.workflow`
6
+ routine instead of a bare `target.tool` fire — the sibling `worktree-gc`
7
+ routine (`../worktree-gc/ROUTINE.md`) gc's but never reports out; this
8
+ fires `worktree-gc-notify/WORKFLOW.md`, whose three tool steps gc, format,
9
+ and notify hosted agentpush in one run. Ships DISABLED (`enabled: false`)
10
+ — install it into a workspace's `.routines/` and flip `enabled: true` to
11
+ activate. See the WORKFLOW.md alongside this file for what each step does.
12
+ version: "1.0.0"
13
+ schedule:
14
+ kind: cron
15
+ cron: "0 4 * * *"
16
+ timezone: "UTC"
17
+ catchup: skip
18
+ target:
19
+ workflow:
20
+ file: /Volumes/SSDExternalMacStudio/Code/products/agentik/agentik-studio/projects/agentproto/ts/packages/worktree/routines/worktree-gc-notify/WORKFLOW.md
21
+ inputs:
22
+ chatId: "<REPLACE_WITH_YOUR_TELEGRAM_CHAT_ID>"
23
+ retry:
24
+ max_attempts: 1
25
+ backoff: fixed
26
+ on_failure:
27
+ create_work_item: true
28
+ fire_event: worktree.gc-notify.failed
29
+ fires_events:
30
+ - worktree.gc-notify.completed
31
+ - worktree.gc-notify.failed
32
+ enabled: false
33
+ tags: [worktree, gc, maintenance, notify]
34
+ ---
35
+
36
+ # Worktree GC + notify routine
37
+
38
+ Runs daily at **04:00 UTC**, firing `worktree-gc-notify/WORKFLOW.md` via
39
+ `workflow_run_file` (the same lowering every `target.workflow.file` routine
40
+ goes through — see `routine-registrar.ts`'s `routineTargetToToolCall`). That
41
+ workflow's three tool steps (`gc` → `format` → `notify`) are documented in
42
+ the WORKFLOW.md itself.
43
+
44
+ ## Why a workflow target instead of `target.tool`
45
+
46
+ `../worktree-gc/ROUTINE.md` already fires `worktree_gc` directly as a bare
47
+ `target.tool` — simplest form, no reporting. This routine dogfoods the OTHER
48
+ target shape (`target.workflow`), which needed the daemon-side
49
+ `compileWorkflow` tool-registry fix to run at all: before that fix, ANY
50
+ `tool` step in a WORKFLOW.md (this one has three) failed to resolve
51
+ ("no tool registered"), so a `target.workflow` routine whose workflow used
52
+ tool steps could never actually complete.
53
+
54
+ ## Enabling
55
+
56
+ 1. Copy this directory to `<workspace>/.routines/worktree-gc-notify/`
57
+ (ROUTINE.md + WORKFLOW.md + entry.mjs together).
58
+ 2. Allowlist `bash` in that workspace's `.agentproto/allowed-commands.json`
59
+ (`command_execute`'s `notify` step needs it) — see the WORKFLOW.md.
60
+ 3. Set `enabled: true`, update `target.workflow.file` to wherever the
61
+ WORKFLOW.md actually lives in that environment, and replace
62
+ `target.inputs.chatId` with your own Telegram chat id.
63
+ 4. Reload routines, or fire it once ad-hoc via `routine_trigger` without
64
+ waiting for the schedule or flipping `enabled`.
65
+
66
+ ## Failure routing
67
+
68
+ Same convention as `worktree-gc`: one retry attempt (no backoff), then
69
+ `on_failure` opens a work item and fires `worktree.gc-notify.failed`. A
70
+ clean run fires `worktree.gc-notify.completed`.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: Worktree GC + notify
3
+ id: worktree-gc-notify
4
+ description: |
5
+ Garbage-collects merged/fresh+clean worktrees under agentproto/ts, then
6
+ reports the outcome to Telegram via the hosted agentpush `send_message`
7
+ tool. Dogfoods a real multi-tool-step WORKFLOW.md now that the daemon's
8
+ `compileWorkflow` resolves `tool` steps through `dispatchTool` (see
9
+ `packages/runtime/src/workflow-tool-registry.ts`) instead of failing every
10
+ `tool` step with "no tool registered". The `format` step is entry-based —
11
+ there is no string expression language for a `transform` step's `compute`
12
+ in the declarative manifest.
13
+ version: 0.1.0
14
+ entry: ./entry.mjs
15
+ inputs:
16
+ chatId:
17
+ type: string
18
+ description: Telegram chat id `notify` reports to. Operator-supplied — never hardcoded.
19
+ outputs: {}
20
+ steps:
21
+ - id: gc
22
+ kind: tool
23
+ tool: worktree_gc
24
+ inputs:
25
+ apply: true
26
+ salvageDirty: false
27
+ repoRoot: /Volumes/SSDExternalMacStudio/Code/products/agentik/agentik-studio/projects/agentproto/ts
28
+ - id: format
29
+ kind: transform
30
+ - id: notify
31
+ kind: tool
32
+ tool: command_execute
33
+ inputs:
34
+ command: bash
35
+ stdin: $steps.format
36
+ ---
37
+
38
+ # Worktree GC + notify
39
+
40
+ Three tool steps, in order:
41
+
42
+ 1. **`gc`** — `worktree_gc` with `apply: true`, `salvageDirty: false`. Same
43
+ safety invariants as the sibling `worktree-gc` routine (merge-gated
44
+ reclaim, open-PR-is-always-hold, dirty-is-salvage-only): this workflow
45
+ inherits them from the engine, it doesn't re-implement them.
46
+ 2. **`format`** — an entry-based `transform` step (see `entry.mjs`) that
47
+ turns `$steps.gc`'s structured `{ mode, outcomes }` into the exact JSON
48
+ body hosted agentpush's `POST /tools/send_message` expects (mirrors
49
+ `.plans/agentproto-maintenance-crons/notify-worktree-gc.sh`'s formatting).
50
+ 3. **`notify`** — `command_execute` running `bash -c '<curl to hosted
51
+ agentpush>'`, fed `format`'s output as `stdin` (`curl --data-binary @-`).
52
+ The LOCAL agentpush MCP is down; this goes straight to
53
+ `https://api.agentpush.io/tools/send_message` with the Bearer key sourced
54
+ from `envs/agentpush/.env.local`, same as the working cron script.
55
+
56
+ ## Enabling
57
+
58
+ Ships wired to `routines/worktree-gc-notify/ROUTINE.md`, disabled
59
+ (`enabled: false`). To activate in a workspace:
60
+
61
+ 1. Copy this directory to `<workspace>/.routines/worktree-gc-notify/`.
62
+ 2. `command_execute` requires `bash` (and whatever `curl` needs) allowlisted
63
+ in that workspace's `.agentproto/allowed-commands.json`.
64
+ 3. Set `enabled: true` on the copied `ROUTINE.md`, update
65
+ `target.workflow.file` if the workflow isn't installed at this same
66
+ absolute path in that environment (`workflow_run_file`'s `path` is read
67
+ as-is, with no anchoring to the routine's own directory), and set
68
+ `target.inputs.chatId` to your own Telegram chat id.
69
+ 4. Reload routines so the daemon registers the schedule (or fire it once
70
+ ad-hoc via `routine_trigger`).
@@ -0,0 +1,77 @@
1
+ // Runtime source of truth for WORKFLOW.md's step graph. The frontmatter
2
+ // mirrors this (id + kind) for governance, per `reconcileEntry`; `format`'s
3
+ // `compute` is a real function, which is why this step can only be
4
+ // entry-based — there is no string expression language for it in the
5
+ // declarative manifest.
6
+ export default {
7
+ name: "Worktree GC + notify",
8
+ id: "worktree-gc-notify",
9
+ description:
10
+ "Garbage-collect merged/fresh+clean worktrees, then report the outcome to Telegram via hosted agentpush.",
11
+ version: "0.1.0",
12
+ // `chatId` is the Telegram chat id `notify` reports to — operator-supplied
13
+ // at invocation (routine `target.inputs` / `workflow_run_file`'s `input`),
14
+ // never hardcoded, since this workflow ships in a public repo.
15
+ inputs: { chatId: { type: "string" } },
16
+ outputs: {},
17
+ steps: [
18
+ {
19
+ id: "gc",
20
+ kind: "tool",
21
+ tool: "worktree_gc",
22
+ inputs: {
23
+ apply: true,
24
+ salvageDirty: false,
25
+ repoRoot:
26
+ "/Volumes/SSDExternalMacStudio/Code/products/agentik/agentik-studio/projects/agentproto/ts",
27
+ },
28
+ },
29
+ {
30
+ id: "format",
31
+ kind: "transform",
32
+ // Turns the `gc` step's structured result into the exact JSON body
33
+ // hosted agentpush's `POST /tools/send_message` expects. The `$steps.*`
34
+ // ref grammar can substitute `$steps.gc` whole into another step's
35
+ // inputs, but can't serialize an object into a plain string a later
36
+ // tool step's `stdin` can carry — that's what this step is for.
37
+ compute: (b) => {
38
+ const gc = b.steps.gc ?? {}
39
+ const outcomes = Array.isArray(gc.outcomes) ? gc.outcomes : []
40
+ const counts = {}
41
+ for (const o of outcomes) counts[o.result] = (counts[o.result] || 0) + 1
42
+ const reaped = outcomes
43
+ .filter(o => o.result === "reclaimed")
44
+ .map(o => o.branch || o.path.split("/").pop())
45
+ const line =
46
+ Object.entries(counts)
47
+ .map(([k, v]) => `${v} ${k}`)
48
+ .join(", ") || "nothing to do"
49
+ const stamp = new Date().toISOString().slice(0, 16).replace("T", " ")
50
+ let text = `🧹 worktree-gc (${stamp} UTC) — ${line}`
51
+ if (reaped.length) text += `\nreaped: ${reaped.join(", ")}`
52
+ return JSON.stringify({
53
+ to: { channel: "telegram", address: b.input?.chatId },
54
+ content: { text },
55
+ })
56
+ },
57
+ },
58
+ {
59
+ id: "notify",
60
+ kind: "tool",
61
+ tool: "command_execute",
62
+ inputs: {
63
+ command: "bash",
64
+ args: [
65
+ "-c",
66
+ [
67
+ "set -a",
68
+ ". /Volumes/SSDExternalMacStudio/Code/products/agentik/agentik-studio/envs/agentpush/.env.local 2>/dev/null || true",
69
+ "set +a",
70
+ 'curl -s --max-time 30 -X POST -H "Authorization: Bearer ${AGENTPUSH_API_KEY:-}" -H "Content-Type: application/json" https://api.agentpush.io/tools/send_message --data-binary @-',
71
+ ].join("\n"),
72
+ ],
73
+ stdin: "$steps.format",
74
+ },
75
+ },
76
+ ],
77
+ }