portable-agent-layer 0.114.0 → 0.115.0-canary.2
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/README.md +4 -0
- package/assets/skills/scheduled-prompts/SKILL.md +57 -0
- package/assets/skills/timebox/SKILL.md +56 -0
- package/assets/skills/unattended/SKILL.md +84 -0
- package/assets/templates/settings.claude.json +1 -1
- package/package.json +1 -1
- package/src/cli/run-mode.ts +214 -0
- package/src/cli/schedule.ts +161 -0
- package/src/cli/tree.ts +5 -0
- package/src/hooks/CompactRecover.ts +15 -6
- package/src/hooks/StopOrchestrator.ts +19 -13
- package/src/hooks/handlers/inject-retrieval.ts +5 -0
- package/src/hooks/lib/adaptation-steering.ts +1 -1
- package/src/hooks/lib/claim-log.ts +1 -8
- package/src/hooks/lib/reply-checks.ts +24 -0
- package/src/hooks/lib/run-mode-gate.ts +130 -0
- package/src/hooks/lib/run-mode.ts +202 -0
- package/src/hooks/lib/scheduled-prompts.ts +233 -0
- package/src/hooks/lib/security-gate.ts +32 -1
- package/src/hooks/lib/security.ts +16 -0
- package/src/hooks/lib/send-back-guard.ts +37 -0
- package/src/hooks/lib/session-events.ts +4 -0
- package/src/hooks/lib/wall-clock.ts +12 -0
package/README.md
CHANGED
|
@@ -95,6 +95,9 @@ pal cli status # check your setup
|
|
|
95
95
|
| `pal cli wrapped [--year <yyyy>]` | A year of your work in numbers, read from the event log |
|
|
96
96
|
| `pal cli patterns` | How you work across projects, read from the event log |
|
|
97
97
|
| `pal cli progress [show\|list\|open\|enter\|park\|wait\|done\|stopped\|decide\|attach\|follow-up]` | The progress tree: what you are on in this project, where it stopped, what is parked or waiting, and the decisions and artifacts on each node. Detours are child nodes and `done` returns to the parent. The active node rides along in every session and prompt, and your own commits attach to it at turn end |
|
|
98
|
+
| `pal cli timebox [start\|status\|end]` | Stop work at a deadline: `start --for 15m` or `--until 18:00`. After the deadline every working tool call is refused except `pal cli` and edits to the report, so the agent stops and reports what it has |
|
|
99
|
+
| `pal cli schedule [add\|list\|remove]` | Prompts added at a time of day, for you or for one project: `add --from 20:00 "suggest tasks that can run overnight"`. A due schedule is added to the next prompt you write inside its window, once a day by default (`--every-session`, `--every-prompt`), and nothing fires while no session is open |
|
|
100
|
+
| `pal cli unattended [start\|log\|status\|done\|end]` | Work without you until a deadline: `start --until 05:30 --task … --fallback …`. Questions and subagents are refused, the agent cannot stop while approved tasks are left, and the rules, tasks and log come back after a compaction |
|
|
98
101
|
| `pal cli content preserve` | Copy every Claude Code transcript, gzipped, into `memory/content/` before Claude Code deletes it (30 days by default). Runs on its own whenever a session closes; the content store never leaves the machine, so `export` skips it |
|
|
99
102
|
| `pal cli events discord-nag <sent\|answered>` | For a bot outside PAL: record a nag it sent (`--kind <kind>`), or the answer to one and how long it took (`--after-sec <n>`), in the local event log |
|
|
100
103
|
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium\|large\|flagship` (on Claude: Haiku, Sonnet, Opus, Fable; on Codex: Luna, Sol, Sol at high effort, Astra; on Antigravity: Flash at low, medium and high effort, and no flagship; an agent without the tier asked refuses it), `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
|
|
@@ -221,6 +224,7 @@ PAL ships with built-in skills that extend your agent's capabilities:
|
|
|
221
224
|
| `reflect` | Diagnose why a PAL behavior didn't trigger |
|
|
222
225
|
| `research-entity` | Research a company or person on the web: links, registry entry, recent news |
|
|
223
226
|
| `rule-review` | Approve or deny the rules PAL drafted from your corrections |
|
|
227
|
+
| `scheduled-prompts` | Set up a prompt that arrives at a time of day from now on, for you or one project |
|
|
224
228
|
| `scientific-research` | Answer health and scientific questions from cited, verified sources: PubMed, guidelines, NEJM, Lancet, JAMA, BMJ |
|
|
225
229
|
| `telos` | Inspect or update goals, beliefs, strategies, narratives |
|
|
226
230
|
| `think` | Structured first-pass reasoning on a problem |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scheduled-prompts
|
|
3
|
+
license: MIT
|
|
4
|
+
description: "Sets up a recurring prompt that PAL delivers at a time of day from now on — a suggestion, a reminder or a standing rule that applies after 20:00, every weekday at 9, or every evening. Extracts the window, weekdays, scope and repeat from the request, rewrites the text as an instruction to the assistant, reads it back for approval, then runs pal cli schedule add. Use when the user wants something to happen or be suggested at a time of day on an ongoing basis, or wants to list or remove such schedules."
|
|
5
|
+
argument-hint: "<time or window> <what to prompt>"
|
|
6
|
+
metadata:
|
|
7
|
+
source: portable-agent-layer
|
|
8
|
+
triggers:
|
|
9
|
+
- "scheduled-prompts"
|
|
10
|
+
- "scheduled prompts"
|
|
11
|
+
- "every day at"
|
|
12
|
+
- "from now on at"
|
|
13
|
+
- "schedule a prompt"
|
|
14
|
+
- "every evening"
|
|
15
|
+
- "every weekday at"
|
|
16
|
+
- "every morning"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Scheduled prompts
|
|
20
|
+
|
|
21
|
+
Turn $ARGUMENTS into a schedule that PAL adds to the conversation at a time of day from now on. Delivery is passive: a due schedule is attached as a system reminder to the first prompt the user writes inside its window. Nothing fires while no session is open, a missed window never fires late, and background notifications do not count as prompts. Schedules live in PAL's memory, never in the repository, and times are read in the user's configured timezone (`pal cli timezone`).
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
1. **Extract** from the request:
|
|
26
|
+
- **Window** — the local time it opens (`--from HH:MM`) and, when given, the time it closes (`--to HH:MM`; default midnight; it may pass midnight, so 22:00 to 02:00 belongs to the day it opened). A word like "morning" or "evening" without a clock time becomes a proposed time (08:00, 18:00), marked as assumed in the read-back.
|
|
27
|
+
- **Days** — weekdays only, as `--days mon-fri`, `sat,sun` or `mon,wed,fri`. Omit for every day.
|
|
28
|
+
- **Scope** — `--project <name>` when the request is about this project's work (the default is the current registered project, or the current folder when none is registered); `--user` when it is about the user generally, in every project.
|
|
29
|
+
- **Text** — the prompt rewritten as an instruction to the assistant, in the second person, under 500 characters. "After 20:00 suggest overnight tasks" becomes "Suggest one or two tasks that could run overnight with the unattended skill."
|
|
30
|
+
2. **Pick the repeat** from the wording. An act-once request (suggest, remind, start, prepare, review) takes the default, daily: once per day per scope, so a user schedule fires once across all sessions and a project schedule once per project. A keep-in-mind rule (always, never, while, don't, keep) takes `--every-prompt`. Use `--every-session` only when the user asks for once per session.
|
|
31
|
+
3. **Check for duplicates.** Run `pal cli schedule list` and compare the intent of each line with the new one; when one already says the same thing, tell the user and offer to keep it or replace it (`pal cli schedule remove <id>`, then add) rather than adding a second. Then read the opinions and rules always injected into context: when one already says the same thing at all hours, tell the user it can be retired through rule review once the schedule exists. Do not retire it yourself.
|
|
32
|
+
4. **Read back** the schedule in one short block (window, days, scope, repeat, text). STOP and wait for explicit approval. Do not run `add` before it. Edits return to this step.
|
|
33
|
+
5. **Add** after approval:
|
|
34
|
+
```bash
|
|
35
|
+
pal cli schedule add "<text>" --from <HH:MM> [--to <HH:MM>] [--days <days>] [--project <name> | --user] [--every-session | --every-prompt]
|
|
36
|
+
```
|
|
37
|
+
Show its output line verbatim and tell the user how to remove it: `pal cli schedule remove <id>`.
|
|
38
|
+
|
|
39
|
+
To list schedules, run `pal cli schedule list` (`--here` for the current project only); each line carries id, scope, window, days, repeat, text and whether it is open or closed now. There is no edit command: a change is a remove followed by an add, read back as in step 4.
|
|
40
|
+
|
|
41
|
+
## Output format
|
|
42
|
+
|
|
43
|
+
- Before approval: one block with five labelled lines — **Window**, **Days**, **Scope**, **Repeat**, **Text** — followed by one approval question. Nothing runs before the answer.
|
|
44
|
+
- After approval: the `add` command's output line as printed, the remove command, and one sentence on delivery: the prompt arrives as a system reminder on the first prompt written inside the window; nothing arrives while no session is open, and a missed window never fires late.
|
|
45
|
+
- For a list request: the `pal cli schedule list` output, one line per schedule, unchanged.
|
|
46
|
+
|
|
47
|
+
## When to use
|
|
48
|
+
|
|
49
|
+
- The user asks for something to happen or be suggested at a time of day from now on: "after 20:00 suggest overnight tasks", "every weekday at 9 remind me to plan the day", "after 18:00 keep replies short", "every evening ask what to park".
|
|
50
|
+
- The user asks what is scheduled, or wants a scheduled prompt changed or removed.
|
|
51
|
+
|
|
52
|
+
## Do NOT use
|
|
53
|
+
|
|
54
|
+
- A one-off reminder at a specific date, or work under a deadline: that is the `timebox` skill.
|
|
55
|
+
- Work that should continue while the user is away: that is the `unattended` skill.
|
|
56
|
+
- A recurring job that must run with no session open, such as a nightly build or a backup: use cron or the agent's own scheduler. A schedule only reaches a prompt the user writes.
|
|
57
|
+
- A scheduled prompt has just arrived as a system reminder: act on it as the user's instruction. This skill creates schedules; it does not run them.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: timebox
|
|
3
|
+
license: MIT
|
|
4
|
+
description: "Works on a task under a user-given time limit and stops at the deadline with a report of what was found or done so far. Use when the request names a duration or a clock time for the work (\"stop in 15 minutes\", \"spend at most an hour\", \"stop at 6 PM\", \"timebox this\")."
|
|
5
|
+
argument-hint: <duration or HH:MM> <task>
|
|
6
|
+
metadata:
|
|
7
|
+
source: portable-agent-layer
|
|
8
|
+
triggers:
|
|
9
|
+
- "timebox"
|
|
10
|
+
- "time box"
|
|
11
|
+
- "stop in"
|
|
12
|
+
- "spend at most"
|
|
13
|
+
- "stop at"
|
|
14
|
+
- "time limit"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Timebox
|
|
18
|
+
|
|
19
|
+
Work on $ARGUMENTS until the deadline the user set, then stop and report, wherever the work stands. PAL enforces the deadline for the current project or folder through its hooks; this skill is how to work inside it.
|
|
20
|
+
|
|
21
|
+
## Workflow
|
|
22
|
+
|
|
23
|
+
1. **Parse the limit** from the request. A duration ("15 minutes", "an hour and a half") becomes `--for 15m` or `--for 1h30m`; a clock time ("by 6 PM", "at 18:00") becomes `--until 18:00`, the next such local time. If no limit is clear, this is not the skill — do the task normally.
|
|
24
|
+
2. **Start the box** before any other work:
|
|
25
|
+
```bash
|
|
26
|
+
pal cli timebox start --for <15m|2h|1h30m> --task "<short task name>"
|
|
27
|
+
pal cli timebox start --until <HH:MM> --task "<short task name>"
|
|
28
|
+
```
|
|
29
|
+
Add `--report <path>` only when the user asked for the result in a file. The command fails if a box is already running for this folder; then ask whether to replace it (`pal cli timebox end`, then start) or work inside the one that exists.
|
|
30
|
+
3. **Work in small steps.** Run `pal cli timebox status` between steps; it prints the end time and the minutes left. Each user prompt during the box also carries "Time box until HH:MM, N min left". Plan the last step so it finishes before the deadline. Start nothing that cannot finish in the time left — a long build, a large download, a deep research pass — because a hook cannot interrupt a running command.
|
|
31
|
+
4. **Stop at the deadline.** After it, PAL's PreToolUse hook refuses shell, edit and write, web search and fetch, subagent and question calls with a message starting "Time box ended at HH:MM … Stop working now". Only `pal cli …` commands (optionally piped into `head` or `tail`) and writes to the `--report` path still run. Read-only tools may still answer, but do not keep working with them. Do not work around a refusal; move straight to the report.
|
|
32
|
+
5. **Report** in the final message, or in the report file when `--report` was given. Keep it short.
|
|
33
|
+
6. **End the box**: `pal cli timebox end`. A user prompt sent after the deadline clears it automatically; run `end` anyway so the state is explicit.
|
|
34
|
+
|
|
35
|
+
If the task finishes before the deadline, report then and run `pal cli timebox end` right away.
|
|
36
|
+
|
|
37
|
+
## Output format
|
|
38
|
+
|
|
39
|
+
The final message (or report file) has five parts, each one line or a short list:
|
|
40
|
+
|
|
41
|
+
- **Done / found** — the concrete results so far.
|
|
42
|
+
- **Not covered** — what the box cut off, or what was never reached.
|
|
43
|
+
- **Next** — the first steps to take if the work resumes, with the box ended.
|
|
44
|
+
- **Confidence** — a one-line judgment and its weakest point.
|
|
45
|
+
- **Box** — `started HH:MM, ended HH:MM` and whether the task finished early or was cut at the deadline.
|
|
46
|
+
|
|
47
|
+
## When to use
|
|
48
|
+
|
|
49
|
+
- The user gives a duration or clock time for the work: "research this but stop in 15 minutes", "spend at most an hour on the refactor", "stop at 6 PM", "timebox this to 30 minutes".
|
|
50
|
+
- The user says "timebox" or "time box" for any task.
|
|
51
|
+
|
|
52
|
+
## Do NOT use
|
|
53
|
+
|
|
54
|
+
- No time limit was given. Do the task normally; a time box without a user-stated limit is an invention.
|
|
55
|
+
- The user is leaving and nobody will answer questions — that is the `unattended` skill, which sets its own deadline and handles the absence.
|
|
56
|
+
- The user wants a reminder or an alarm with no work attached to it.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unattended
|
|
3
|
+
license: MIT
|
|
4
|
+
description: "Runs an approved list of tasks while the user is away — asleep, in a meeting, out for the afternoon — with nobody there to answer, then leaves a report for their return. Proposes finishable tasks and fallbacks, gets approval and a return time, starts the run with pal cli unattended, logs every decision, and finalises the report at the deadline. Use when the user says they are leaving and wants work to continue unattended, overnight, while they sleep, or until they are back."
|
|
5
|
+
argument-hint: "[return time] [tasks or goal]"
|
|
6
|
+
metadata:
|
|
7
|
+
source: portable-agent-layer
|
|
8
|
+
triggers:
|
|
9
|
+
- "unattended"
|
|
10
|
+
- "while I'm away"
|
|
11
|
+
- "work on this while I sleep"
|
|
12
|
+
- "overnight tasks"
|
|
13
|
+
- "night task"
|
|
14
|
+
- "until I'm back"
|
|
15
|
+
- "I'm going to bed"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Unattended run
|
|
19
|
+
|
|
20
|
+
Work through approved tasks with nobody present to answer, then hand back a report. Three phases: propose, run, report. Do not start the run until the user has approved the plan.
|
|
21
|
+
|
|
22
|
+
## Phase A — Propose (before the user leaves)
|
|
23
|
+
|
|
24
|
+
1. Resolve the current project from the working folder (`pal cli project resume <slug>` when the user names one). Read its notes, open progress nodes and recent sessions: tasks come only from this project.
|
|
25
|
+
2. Draft candidate tasks. Keep a task only if it can be finished alone, with no decision that only the user can make. For each kept task write: what it produces, how it will be checked (tests or a build when the project has them, a visual comparison, sources cited, a reread against the brief), rough time, and resource cost (memory, network, paid APIs). Flag dropped tasks with the decision they need.
|
|
26
|
+
3. Propose 1-2 fallback tasks the run takes when the approved list is finished early: deterministic, low-risk work such as fixing lint warnings, adding missing tests, or updating documentation.
|
|
27
|
+
4. Ask, in one question with your agent's ask tool: the return time, where the report goes (a file path, a page the project serves, or an artifact), and extra rules. Suggest the rule categories: no paid or keyed APIs, no downloads, memory caps on heavy commands, files or services that are off-limits.
|
|
28
|
+
5. Repeat back in short form: tasks in order, fallbacks, return time, report location, extra rules on top of the defaults listed below. STOP and wait for explicit approval. Edits to the list return to step 5.
|
|
29
|
+
|
|
30
|
+
Default rules every run carries (`pal cli unattended start` stores them; `--rule` adds more):
|
|
31
|
+
- Never ask. Decide, log the decision and its reason, list the question in the report.
|
|
32
|
+
- Check notes and earlier sessions before listing a question.
|
|
33
|
+
- Approved tasks, then fallbacks, nothing else. Other ideas become suggestions in the report.
|
|
34
|
+
- Never delete, push, deploy or publish anything except the report.
|
|
35
|
+
- One heavy process at a time. No subagents, no workflows.
|
|
36
|
+
- Verify each step as if the user were present.
|
|
37
|
+
- Only in a git repository: work on a branch, one logical step per commit, stage only your own changes, never push.
|
|
38
|
+
|
|
39
|
+
## Phase B — Run (after approval)
|
|
40
|
+
|
|
41
|
+
1. Set the deadline about 30 minutes before the return time so the report is final when the user is back. Start the run:
|
|
42
|
+
```bash
|
|
43
|
+
pal cli unattended start --until <HH:MM> --task "<task 1>" --task "<task 2>" --fallback "<fallback>" --rule "<extra rule>" --report <path|url>
|
|
44
|
+
```
|
|
45
|
+
Then `pal cli progress open "<run title>"` for the run's progress node.
|
|
46
|
+
2. In a git repository, create the work branch. Take tasks in the approved order, one at a time.
|
|
47
|
+
3. For each task: do it; verify it the way Phase A agreed (when the project has tests, run them twice and break-check every new test; when it has a build, run it); in a git repository, commit. Then `pal cli unattended log "<result> and how it was verified"` and `pal cli progress stopped "<next step>"`. Commits attach to the progress node on their own; do not attach them by hand.
|
|
48
|
+
4. Log every non-obvious decision with its reason at the moment of deciding: `pal cli unattended log "<decision>: <reason>"`.
|
|
49
|
+
5. Run `pal cli unattended status` between steps. Compare the minutes left with the estimated length of the next step; never start a step that cannot finish before the deadline, because a hook cannot interrupt a running command. Pick a shorter step or move to the report instead.
|
|
50
|
+
6. Write the report to the agreed location as soon as the first task lands, with "(draft)" in its title. Update it after every task so a crash leaves a usable report behind.
|
|
51
|
+
7. When the approved tasks are finished, take the fallbacks in order. The Stop hook refuses to end the turn before `pal cli unattended done`, so finishing early means moving on, not idling.
|
|
52
|
+
8. Treat every refused tool call as a rule: questions to the user and subagent or workflow calls are refused during the run, and after the deadline every working tool is refused except `pal cli` and writes to the report path. The refusal depends on the agent's tool names being known to PAL, so never ask or delegate even when no hook stops you. Never work around a refusal; log it and continue with the next permitted step.
|
|
53
|
+
9. After a context compaction, run `pal cli unattended status` and resume from the last logged step. Agents with a compaction hook also get the run's rules, tasks and log re-injected; re-read them first.
|
|
54
|
+
|
|
55
|
+
## Phase C — Report (at the deadline)
|
|
56
|
+
|
|
57
|
+
1. Finalise the report, removing "(draft)". Sections, in order:
|
|
58
|
+
- **Done** — each task, how it was verified, and its commits, test counts and build result where the project has them.
|
|
59
|
+
- **Comparisons** — for visual work, reference next to result, with an honest "falls short" note where it does.
|
|
60
|
+
- **Failed, skipped, reverted** — what, why, and what was reverted.
|
|
61
|
+
- **Questions for the user** — each answerable in one line; only questions the notes could not answer.
|
|
62
|
+
- **Unfinished work** — files left mid-change and what state they are in; in a git repository, the branch and its uncommitted files.
|
|
63
|
+
Label every inference of your own as such ("my reading:", "unverified:").
|
|
64
|
+
2. If the report is served somewhere, open the served copy (the `playwright` skill, or fetch the URL) and confirm it matches the local file.
|
|
65
|
+
3. `pal cli progress done`, then `pal cli unattended done`.
|
|
66
|
+
4. When the user returns, reply first with a summary under 100 words: what landed, the report location, and the open questions. Then run `pal cli unattended end`.
|
|
67
|
+
|
|
68
|
+
## Output format
|
|
69
|
+
|
|
70
|
+
- Phase A ends with the short-form plan (tasks, fallbacks, return time, report location, extra rules) followed by one approval question. Nothing runs before the answer.
|
|
71
|
+
- Phase B produces the task results (commits on the branch in a git repository), `pal cli unattended log` entries, progress-tree updates, and the draft report at the agreed location.
|
|
72
|
+
- Phase C produces the final report with the five sections above and, on the user's return, a summary under 100 words naming the report location and the open questions.
|
|
73
|
+
|
|
74
|
+
## When to use
|
|
75
|
+
|
|
76
|
+
- The user is leaving (sleep, meeting, travel, the rest of the day) and wants work to continue with nobody there to answer.
|
|
77
|
+
- The user asks for an overnight or night task, or wants the work to keep going while they are away or until they are back.
|
|
78
|
+
- A list of self-contained tasks exists, or can be drawn from the current project, that needs no user decision mid-way.
|
|
79
|
+
|
|
80
|
+
## Do NOT use
|
|
81
|
+
|
|
82
|
+
- The user stays at the keyboard and only wants a time limit on the work: that is the `timebox` skill.
|
|
83
|
+
- A single long-running command that should run in the background and be checked later.
|
|
84
|
+
- Tasks that hinge on a decision only the user can make: propose them, flag the decision, and leave them out of the run.
|
package/package.json
CHANGED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli timebox / pal cli unattended — start, inspect and end a run with a
|
|
3
|
+
* deadline for the current project or folder. The hooks enforce it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { scopeOf } from "../hooks/lib/progress-tree";
|
|
7
|
+
import {
|
|
8
|
+
clearRun,
|
|
9
|
+
deadlineFrom,
|
|
10
|
+
describeRun,
|
|
11
|
+
newRun,
|
|
12
|
+
type Run,
|
|
13
|
+
type RunKind,
|
|
14
|
+
readRun,
|
|
15
|
+
unattendedRules,
|
|
16
|
+
untilLabel,
|
|
17
|
+
writeRun,
|
|
18
|
+
} from "../hooks/lib/run-mode";
|
|
19
|
+
import { isInGitRepo } from "../hooks/lib/session-events";
|
|
20
|
+
import { group, leaf, UsageError } from "../tools/lib/command";
|
|
21
|
+
|
|
22
|
+
const FOR = {
|
|
23
|
+
type: "string",
|
|
24
|
+
value: "<duration>",
|
|
25
|
+
description: "How long, e.g. 15m, 2h or 1h30m",
|
|
26
|
+
} as const;
|
|
27
|
+
const UNTIL = {
|
|
28
|
+
type: "string",
|
|
29
|
+
value: "<HH:MM>",
|
|
30
|
+
description: "Local time it ends (the next one), or an ISO time",
|
|
31
|
+
} as const;
|
|
32
|
+
const REPORT = {
|
|
33
|
+
type: "string",
|
|
34
|
+
value: "<path|url>",
|
|
35
|
+
description:
|
|
36
|
+
"Where the report goes; edits to a local path still run after the deadline",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
function currentScope(): string {
|
|
40
|
+
return scopeOf(process.cwd());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function deadline(values: { for?: string; until?: string }): Date {
|
|
44
|
+
try {
|
|
45
|
+
return deadlineFrom(values);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new UsageError((err as Error).message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runOf(kind: RunKind): Run | null {
|
|
52
|
+
const run = readRun(currentScope());
|
|
53
|
+
return run?.kind === kind ? run : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function noRun(kind: RunKind): number {
|
|
57
|
+
console.error(`No ${kind} run in ${currentScope()}.`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function refuseIfRunning(): void {
|
|
62
|
+
const run = readRun(currentScope());
|
|
63
|
+
if (run)
|
|
64
|
+
throw new UsageError(
|
|
65
|
+
`A ${run.kind} run until ${untilLabel(run)} is already on here; end it first`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printStatus(kind: RunKind): number {
|
|
70
|
+
const run = runOf(kind);
|
|
71
|
+
if (!run) return noRun(kind);
|
|
72
|
+
console.log(describeRun(run));
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function endRun(kind: RunKind): number {
|
|
77
|
+
const run = runOf(kind);
|
|
78
|
+
if (!run) return noRun(kind);
|
|
79
|
+
clearRun(run.scope);
|
|
80
|
+
console.log(`Ended the ${kind} run that was due at ${untilLabel(run)}.`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function startTimebox(values: {
|
|
85
|
+
for?: string;
|
|
86
|
+
until?: string;
|
|
87
|
+
task?: string;
|
|
88
|
+
report?: string;
|
|
89
|
+
}): number {
|
|
90
|
+
refuseIfRunning();
|
|
91
|
+
const run = newRun({
|
|
92
|
+
kind: "timebox",
|
|
93
|
+
scope: currentScope(),
|
|
94
|
+
until: deadline(values),
|
|
95
|
+
task: values.task,
|
|
96
|
+
report: values.report,
|
|
97
|
+
});
|
|
98
|
+
writeRun(run);
|
|
99
|
+
console.log(
|
|
100
|
+
`Time box set: ends at ${untilLabel(run)}. After that only \`pal cli\` and report edits run.`
|
|
101
|
+
);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function startUnattended(values: {
|
|
106
|
+
until?: string;
|
|
107
|
+
for?: string;
|
|
108
|
+
task?: string[];
|
|
109
|
+
fallback?: string[];
|
|
110
|
+
rule?: string[];
|
|
111
|
+
report?: string;
|
|
112
|
+
}): number {
|
|
113
|
+
refuseIfRunning();
|
|
114
|
+
if (!values.task?.length) throw new UsageError("give each approved task with --task");
|
|
115
|
+
const run = newRun({
|
|
116
|
+
kind: "unattended",
|
|
117
|
+
scope: currentScope(),
|
|
118
|
+
until: deadline(values),
|
|
119
|
+
report: values.report,
|
|
120
|
+
tasks: values.task,
|
|
121
|
+
fallbacks: values.fallback ?? [],
|
|
122
|
+
rules: unattendedRules(isInGitRepo(process.cwd()), values.rule),
|
|
123
|
+
});
|
|
124
|
+
writeRun(run);
|
|
125
|
+
console.log(describeRun(run));
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function logEntry(text: string): number {
|
|
130
|
+
const run = runOf("unattended");
|
|
131
|
+
if (!run) return noRun("unattended");
|
|
132
|
+
writeRun({ ...run, log: [...run.log, { at: new Date().toISOString(), entry: text }] });
|
|
133
|
+
console.log(`Logged (${run.log.length + 1} entries).`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function markDone(): number {
|
|
138
|
+
const run = runOf("unattended");
|
|
139
|
+
if (!run) return noRun("unattended");
|
|
140
|
+
writeRun({ ...run, done: true });
|
|
141
|
+
console.log("Marked done: the agent may stop. The run ends when the user is back.");
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const timeboxCommand = group({
|
|
146
|
+
summary: "Stop work at a deadline and report what was found",
|
|
147
|
+
commands: {
|
|
148
|
+
start: leaf({
|
|
149
|
+
summary: "Start a time box for the current project or folder",
|
|
150
|
+
options: {
|
|
151
|
+
for: FOR,
|
|
152
|
+
until: UNTIL,
|
|
153
|
+
task: { type: "string", value: "<text>", description: "What is being done" },
|
|
154
|
+
report: REPORT,
|
|
155
|
+
},
|
|
156
|
+
run: ({ values }) => startTimebox(values),
|
|
157
|
+
}),
|
|
158
|
+
status: leaf({
|
|
159
|
+
summary: "Show the time box and the time left",
|
|
160
|
+
run: () => printStatus("timebox"),
|
|
161
|
+
}),
|
|
162
|
+
end: leaf({ summary: "End the time box", run: () => endRun("timebox") }),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export const unattendedCommand = group({
|
|
167
|
+
summary: "Work without the user until a deadline: no questions, approved tasks only",
|
|
168
|
+
commands: {
|
|
169
|
+
start: leaf({
|
|
170
|
+
summary: "Start an unattended run with the approved tasks",
|
|
171
|
+
options: {
|
|
172
|
+
until: UNTIL,
|
|
173
|
+
for: FOR,
|
|
174
|
+
task: {
|
|
175
|
+
type: "string",
|
|
176
|
+
multiple: true,
|
|
177
|
+
value: "<text>",
|
|
178
|
+
description: "An approved task, in order (repeat)",
|
|
179
|
+
},
|
|
180
|
+
fallback: {
|
|
181
|
+
type: "string",
|
|
182
|
+
multiple: true,
|
|
183
|
+
value: "<text>",
|
|
184
|
+
description: "A pre-approved task for when the tasks run out (repeat)",
|
|
185
|
+
},
|
|
186
|
+
rule: {
|
|
187
|
+
type: "string",
|
|
188
|
+
multiple: true,
|
|
189
|
+
value: "<text>",
|
|
190
|
+
description: "A rule on top of the defaults (repeat)",
|
|
191
|
+
},
|
|
192
|
+
report: REPORT,
|
|
193
|
+
},
|
|
194
|
+
run: ({ values }) => startUnattended(values),
|
|
195
|
+
}),
|
|
196
|
+
log: leaf({
|
|
197
|
+
summary: "Append a timestamped entry to the run's log",
|
|
198
|
+
args: "<entry...>",
|
|
199
|
+
run: ({ positionals }) => logEntry(positionals.join(" ")),
|
|
200
|
+
}),
|
|
201
|
+
status: leaf({
|
|
202
|
+
summary: "Show the rules, tasks, log and time left",
|
|
203
|
+
run: () => printStatus("unattended"),
|
|
204
|
+
}),
|
|
205
|
+
done: leaf({
|
|
206
|
+
summary: "Every task and fallback is finished and the report is written",
|
|
207
|
+
run: markDone,
|
|
208
|
+
}),
|
|
209
|
+
end: leaf({
|
|
210
|
+
summary: "End the run (the user is back)",
|
|
211
|
+
run: () => endRun("unattended"),
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli schedule — prompts the user wants added at a time of day, for
|
|
3
|
+
* themselves or for one project. The prompt hook injects the due ones.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { scopeOf } from "../hooks/lib/progress-tree";
|
|
7
|
+
import { readAllProjects } from "../hooks/lib/projects";
|
|
8
|
+
import {
|
|
9
|
+
addSchedule,
|
|
10
|
+
appliesHere,
|
|
11
|
+
newSchedule,
|
|
12
|
+
openWindowDay,
|
|
13
|
+
type Repeat,
|
|
14
|
+
readSchedules,
|
|
15
|
+
removeSchedule,
|
|
16
|
+
type Schedule,
|
|
17
|
+
scheduleLine,
|
|
18
|
+
USER_SCOPE,
|
|
19
|
+
windowLabel,
|
|
20
|
+
} from "../hooks/lib/scheduled-prompts";
|
|
21
|
+
import { group, leaf, UsageError } from "../tools/lib/command";
|
|
22
|
+
|
|
23
|
+
interface AddValues {
|
|
24
|
+
from?: string;
|
|
25
|
+
to?: string;
|
|
26
|
+
days?: string;
|
|
27
|
+
project?: string;
|
|
28
|
+
user?: boolean;
|
|
29
|
+
"every-session"?: boolean;
|
|
30
|
+
"every-prompt"?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scopeFor(values: AddValues): string {
|
|
34
|
+
if (values.user && values.project)
|
|
35
|
+
throw new UsageError("give --user or --project, not both");
|
|
36
|
+
if (values.user) return USER_SCOPE;
|
|
37
|
+
return values.project === undefined
|
|
38
|
+
? scopeOf(process.cwd())
|
|
39
|
+
: registeredProject(values.project);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registeredProject(name: string): string {
|
|
43
|
+
if (readAllProjects().some((p) => p.name === name)) return name;
|
|
44
|
+
throw new UsageError(`no registered project "${name}"; see \`pal cli project list\``);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function repeatFor(values: AddValues): Repeat {
|
|
48
|
+
if (values["every-session"] && values["every-prompt"])
|
|
49
|
+
throw new UsageError("give --every-session or --every-prompt, not both");
|
|
50
|
+
if (values["every-prompt"]) return "every-prompt";
|
|
51
|
+
return values["every-session"] ? "every-session" : "daily";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scopeLabel(schedule: Schedule): string {
|
|
55
|
+
return schedule.scope === USER_SCOPE ? "you, in every project" : schedule.scope;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function built(values: AddValues, text: string): Schedule {
|
|
59
|
+
if (!values.from) throw new UsageError("give the start time with --from <HH:MM>");
|
|
60
|
+
try {
|
|
61
|
+
return newSchedule({
|
|
62
|
+
scope: scopeFor(values),
|
|
63
|
+
from: values.from,
|
|
64
|
+
to: values.to,
|
|
65
|
+
days: values.days,
|
|
66
|
+
repeat: repeatFor(values),
|
|
67
|
+
text,
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err instanceof UsageError) throw err;
|
|
71
|
+
throw new UsageError((err as Error).message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function add(values: AddValues, text: string): number {
|
|
76
|
+
const schedule = built(values, text);
|
|
77
|
+
addSchedule(schedule);
|
|
78
|
+
console.log(
|
|
79
|
+
`Scheduled ${schedule.id} for ${scopeLabel(schedule)}: ${schedule.repeat}, ${windowLabel(schedule)}. Remove it with \`pal cli schedule remove ${schedule.id}\`.`
|
|
80
|
+
);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function status(schedule: Schedule): string {
|
|
85
|
+
return openWindowDay(schedule) ? "open now" : "closed now";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function list(here: boolean): number {
|
|
89
|
+
const cwd = process.cwd();
|
|
90
|
+
const shown = readSchedules().filter((s) => !here || appliesHere(s, cwd));
|
|
91
|
+
if (!shown.length) {
|
|
92
|
+
console.log(here ? "No schedule applies here." : "No schedules.");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
for (const schedule of shown)
|
|
96
|
+
console.log(`${scheduleLine(schedule)} [${status(schedule)}]`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function remove(id: string): number {
|
|
101
|
+
if (!removeSchedule(id)) {
|
|
102
|
+
console.error(`No schedule ${id}.`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
console.log(`Removed ${id}.`);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const scheduleCommand = group({
|
|
110
|
+
summary: "Prompts added at a time of day, for you or for one project",
|
|
111
|
+
commands: {
|
|
112
|
+
add: leaf({
|
|
113
|
+
summary: "Schedule a prompt (once a day by default)",
|
|
114
|
+
args: "<text...>",
|
|
115
|
+
options: {
|
|
116
|
+
from: {
|
|
117
|
+
type: "string",
|
|
118
|
+
value: "<HH:MM>",
|
|
119
|
+
description: "Local time the window opens",
|
|
120
|
+
},
|
|
121
|
+
to: {
|
|
122
|
+
type: "string",
|
|
123
|
+
value: "<HH:MM>",
|
|
124
|
+
description: "Local time it closes (default midnight; may pass midnight)",
|
|
125
|
+
},
|
|
126
|
+
days: {
|
|
127
|
+
type: "string",
|
|
128
|
+
value: "<days>",
|
|
129
|
+
description: "Weekdays only, e.g. mon-fri or sat,sun",
|
|
130
|
+
},
|
|
131
|
+
project: {
|
|
132
|
+
type: "string",
|
|
133
|
+
value: "<name>",
|
|
134
|
+
description: "Only in this project (default: the current one)",
|
|
135
|
+
},
|
|
136
|
+
user: { type: "boolean", description: "For you, in every project" },
|
|
137
|
+
"every-session": {
|
|
138
|
+
type: "boolean",
|
|
139
|
+
description: "Once in each session inside the window",
|
|
140
|
+
},
|
|
141
|
+
"every-prompt": {
|
|
142
|
+
type: "boolean",
|
|
143
|
+
description: "On every prompt inside the window",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
run: ({ values, positionals }) => add(values, positionals.join(" ")),
|
|
147
|
+
}),
|
|
148
|
+
list: leaf({
|
|
149
|
+
summary: "List the schedules and whether their window is open",
|
|
150
|
+
options: {
|
|
151
|
+
here: { type: "boolean", description: "Only those that apply in this folder" },
|
|
152
|
+
},
|
|
153
|
+
run: ({ values }) => list(values.here === true),
|
|
154
|
+
}),
|
|
155
|
+
remove: leaf({
|
|
156
|
+
summary: "Remove a schedule",
|
|
157
|
+
args: "<id>",
|
|
158
|
+
run: ({ positionals }) => remove(positionals[0]),
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
});
|
package/src/cli/tree.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { patternsCommand } from "./patterns";
|
|
|
27
27
|
import { telosCommand, timezoneCommand } from "./personal-context";
|
|
28
28
|
import { progressCommand } from "./progress";
|
|
29
29
|
import { ruleCommand } from "./rule";
|
|
30
|
+
import { timeboxCommand, unattendedCommand } from "./run-mode";
|
|
31
|
+
import { scheduleCommand } from "./schedule";
|
|
30
32
|
import { serverCommand } from "./server";
|
|
31
33
|
import { skillCommand } from "./skill";
|
|
32
34
|
import { subagentCommand } from "./subagent";
|
|
@@ -187,6 +189,9 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
187
189
|
rule: ruleCommand,
|
|
188
190
|
correction: correctionCommand,
|
|
189
191
|
progress: progressCommand,
|
|
192
|
+
timebox: timeboxCommand,
|
|
193
|
+
unattended: unattendedCommand,
|
|
194
|
+
schedule: scheduleCommand,
|
|
190
195
|
server: serverCommand,
|
|
191
196
|
skill: skillCommand,
|
|
192
197
|
subagent: subagentCommand,
|