pi-cohort 7.0.2 → 7.1.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.1.0] - 2026-09-20
4
+
5
+ ### Changed
6
+
7
+ - `/handoff` no longer expands; `/skill:handoff [--out <path> | --key <stem>]` replaces it, writes to `<tmpdir>/pi-handoff/<primary>--<leaf>.md` by default and ends with `Handoff written: <path>`; `## Process state` is no longer produced here; argument-less `gauntlet-resume` lookup of that path lands on the pi-gauntlet side - until then resume takes the printed path. ([#18](https://github.com/jjuraszek/pi-cohort/issues/18), [pi-gauntlet#40](https://github.com/jjuraszek/pi-gauntlet/issues/40))
8
+
3
9
  ## [7.0.2] - 2026-09-20
4
10
 
5
11
  ### Changed
package/README.md CHANGED
@@ -117,7 +117,7 @@ Run parallel reviewers: one for correctness, one for tests, and one for unnecess
117
117
 
118
118
  That's the whole surface for day-to-day use. More phrasing patterns: [doc/commands.md](doc/commands.md#prompt-cookbook-appendix).
119
119
 
120
- Review and delivery workflows live in pi-gauntlet; pi-cohort ships delegation primitives plus `/investigate` and `/handoff`.
120
+ Review and delivery workflows live in pi-gauntlet; pi-cohort ships delegation primitives plus `/investigate` and `/skill:handoff [--out <path> | --key <stem>]` (the brief lands in `<tmpdir>/pi-handoff/<primary>--<leaf>.md` by default; contract in [doc/handoff-template.md](doc/handoff-template.md)).
121
121
 
122
122
  ## Architecture
123
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.0.2",
3
+ "version": "7.1.0",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: handoff
3
+ description: Use when the context window is nearly full and the work must continue in a fresh session.
4
+ ---
5
+
6
+ # Handoff brief
7
+
8
+ Write a handoff brief for a fresh session. The brief is the contract in `doc/handoff-template.md` of pi-cohort: section names and order are fixed. This file carries every rule a producer needs; a caller that follows it reads nothing else. Nothing is written into the repository.
9
+
10
+ ## Argument
11
+
12
+ The argument text, if any, follows this block. Tokenize it shell-like (unquoted tokens end at whitespace; `".."`/`'..'` keep whitespace; `--opt=value` is one token). An option's value is the next token; a next token that is itself `--out`, `--key`, or an `--out=`/`--key=` form, or no next token, or an empty `--opt=` value, means the option has no value. `--out <path>` names the output file (relative resolves against cwd); `--key <stem>` names the file inside the shared mailbox directory; every other token is ignored.
13
+
14
+ Argument failures end the reply with the line and write nothing:
15
+
16
+ - an option with an empty or missing value -> `Handoff not written: <option> has no value`
17
+ - an option given twice -> `Handoff not written: <option> given twice`
18
+ - both options -> `Handoff not written: --out and --key are exclusive`
19
+
20
+ ## Rules
21
+
22
+ - Scratch dir, invocation-unique so overlapping invocations never read each other's snapshot: `SCRATCH=$(node -p "require('fs').mkdtempSync(require('path').join(require('os').tmpdir(), 'pi-handoff-'))")`. If `node` is missing -> `Handoff not written: cannot resolve tmpdir`.
23
+ - The snapshot dispatch: `async: false` and `context: "fresh"` at the top level; `reads: false`; absolute `output` under `$SCRATCH`. If it returns an async handle, a `forceTopLevelAsync` setting is active: report that in the reply, do not poll, relaunch, or continue the child, and write the brief with `## Repo state: unavailable (async handle returned)`.
24
+ - The child is read-only against the repository; its only write is its `output` file.
25
+ - You write `## Intent`, `## Decisions`, `## Open questions`, and `## Skills loaded` from your own transcript, before the call and after it returns. Do not fork a child for this: forking a near-full transcript is the cost this handoff avoids.
26
+
27
+ ## Run worktree
28
+
29
+ Before the snapshot dispatch, determine the run worktree from your own transcript and interpolate it into the scout task as `Run worktree: <abs path>` or `Run worktree: none`:
30
+
31
+ - A flow-level worktree the run created (any `git worktree add <path>` you ran, or a `Worktree ready at <path>` report from the skill that set up the worktree) or was told to work in (a resume brief's `worktree: yes <path>`, or a user instruction).
32
+ - Never inferred from the session cwd; a created or assigned worktree still qualifies when it happens to equal the cwd. Never a per-task worktree from `tasks[].worktree: true` or `worktree: true` dispatches - those are ephemeral.
33
+ - Two flow-level worktrees in the transcript: the candidate is the one most recently used by later work (a dispatch `cwd`, a `git -C <path>`, or a `cd <path>`); if that does not distinguish them, the most recent creation or assignment. The other goes into `## Open questions` as `Also seen: <path> - not recorded as the run worktree`.
34
+ - No flow-level worktree created or named: `Run worktree: none`.
35
+
36
+ ## Repo snapshot
37
+
38
+ ```
39
+ subagent({ async: false, context: "fresh", tasks: [
40
+ { agent: "scout", cwd: "<cwd>", reads: false, output: "<SCRATCH>/repo.md",
41
+ task: "Read-only; write only to your output path. Run worktree: <abs path|none>. Steps: (1) linked set: git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' - the first line is the primary checkout, the rest are the linked worktrees; a relative candidate resolves against the primary toplevel, not cwd. (2) candidate `none` or line absent -> target is cwd, go to (4). Otherwise if the candidate is relative, prefix it with the primary toplevel (first linked-set line) first; C=$(git -C <candidate> rev-parse --show-toplevel); valid when that succeeds and C equals one of the linked lines exactly (a line after the first; equality with the first, primary line is invalid); anything else (failure, not listed, the primary itself, prunable) is invalid -> target is cwd and emit the open-question line in (5). (3) valid -> target is $C and `worktree: yes $C`. (4) target cwd -> T=$(git rev-parse --show-toplevel); `worktree: yes $T` when $T equals one of the linked lines exactly (a line after the first; the first, primary line gives `worktree: no`), else `worktree: no`. (5) report each field on its own line, `unavailable` when a command fails, every git command as `git -C <target>`: toplevel (git -C <target> rev-parse --show-toplevel); the worktree line from (3) or (4); branch (git -C <target> branch --show-current, `detached` when empty); HEAD SHA; base (origin/HEAD short name, else origin/main or origin/master if present, else `base: unknown`); dirty (git -C <target> status --porcelain, or `clean`); diff-stat (git -C <target> diff --stat <base>...HEAD; `diff-stat: unavailable` when base is unknown); test command (<target>/package.json scripts or <target>/AGENTS.md). Invalid candidate: after the fields add the single line `open-question: producer named <candidate as submitted> as the run worktree; it is not a linked worktree of this repo`. cwd not a git repo -> the single line `not a git repo`, plus the same open-question line if a candidate was submitted." }
42
+ ]})
43
+ ```
44
+
45
+ Outcomes: the fields become `## Repo state`; any `open-question:` line goes into `## Open questions` without its prefix and never into `## Repo state`; `not a git repo` -> `## Repo state: not a git repo`; child failed, file missing or empty, or async handle -> `## Repo state: unavailable (<one-line reason>)`. The brief is written in every outcome - the transcript-derived sections are the point of a handoff.
46
+
47
+ ## Destination
48
+
49
+ Resolve after the snapshot (the default key needs its `worktree:` line). Precedence: `--out`, then `--key`, then the default key.
50
+
51
+ - `--out <path>`: that path, made absolute. Its parent directory must exist, else `Handoff not written: parent directory <dir> does not exist`; never fall back to the default when the caller named a path.
52
+ - `--key <stem>`: an exact caller-owned basename stem, written verbatim (no encoding) with `.md` appended - `--key foo.md` gives `foo.md.md`. Valid stem: non-empty after trimming, not `.` or `..`, none of `/`, `\`, NUL, control characters, or `<>:"|?*`, and not starting with `<primary>--` (the default-key prefix). Invalid -> `Handoff not written: --key is not a valid file name stem`; prefixed -> `Handoff not written: --key is reserved for the default key`. Destination `path.join(<tmpdir>, 'pi-handoff', <stem> + '.md')`.
53
+ - Default key `<primary>--<leaf>` at `<tmpdir>/pi-handoff/<primary>--<leaf>.md`, overwritten on each run:
54
+
55
+ ```bash
56
+ TMP=$(node -p "require('os').tmpdir()")
57
+ PRIMARY=$(basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")")
58
+ # LEAF: basename of the run worktree when the snapshot says `worktree: yes <path>`;
59
+ # else `git branch --show-current`; else `detached`
60
+ enc() { printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/-/g'; }
61
+ OUT="$TMP/pi-handoff/$(enc "$PRIMARY")--$(enc "$LEAF").md"
62
+ ```
63
+
64
+ Outside a git repo the key is `no-repo--<encoded cwd basename>`. The encoder `[^A-Za-z0-9._-]` -> `-` applies to the two generated components only, never to a `--key` value. Examples: primary checkout on `main` with no run worktree -> `pi-cohort--main.md`; run worktree `.worktrees/gh-18-handoff-skill` -> `pi-cohort--gh-18-handoff-skill.md`; branch `feat/x` -> `pi-cohort--feat-x.md`.
65
+ - For `--key` and the default, `mkdir -p "$TMP/pi-handoff"` first. Paths are joined with `/`; `os.tmpdir()` covers Windows (`%LOCALAPPDATA%\Temp`).
66
+
67
+ ## Brief
68
+
69
+ ```markdown
70
+ # Handoff: <one line>
71
+ ## Intent
72
+ ## Repo state
73
+ ## Decisions
74
+ ## Open questions
75
+ ## Skills loaded
76
+ ```
77
+
78
+ - `## Repo state`: the snapshot fields, or one of the heading variants above.
79
+ - `## Decisions`: bullets; rejected alternatives marked `rejected:`.
80
+ - `## Open questions`: bullets, including any copied `open-question:` line and any `Also seen:` line.
81
+ - `## Skills loaded`: the frontmatter `name` of every skill whose SKILL.md body is in your context (a `<skill>` block or a tool result on a `*/SKILL.md` path); it always includes `handoff`.
82
+
83
+ Write nothing after `## Skills loaded`: a caller that follows this skill and continues in the same session may append its own `##` sections there. Then `rm -rf "$SCRATCH"`, echo the brief, and end the reply with the line `Handoff written: <abs path>`.
@@ -1,54 +0,0 @@
1
- ---
2
- description: "Use when the context window is nearly full and the work must continue in a fresh session."
3
- ---
4
-
5
- Write a handoff brief for a fresh session. The template is the contract in `doc/handoff-template.md` of pi-cohort; section names are fixed.
6
-
7
- Optional `--out <path>` (relative resolves against cwd):
8
-
9
- $@
10
-
11
- ## Rules
12
-
13
- - `DIR=$(mktemp -d)`. The dispatch: `async: false` and `context: "fresh"` at the top level; `reads: false`; absolute `output` under `$DIR`. If it returns an async handle, a `forceTopLevelAsync` setting is active: stop and report it. Do not poll, relaunch, or continue.
14
- - The child is read-only against the repository; its only write is its `output` file.
15
- - You write `## Intent`, `## Decisions`, `## Open questions`, and `## Skills loaded` from your own transcript, before the call and after it returns. Do not fork a child for this: forking a near-full transcript is the cost this handoff avoids.
16
- - Write nothing into the repository; the brief goes to `--out` or `$DIR/handoff.md`.
17
-
18
- ## Run worktree
19
-
20
- Before the snapshot dispatch, determine the run worktree from your own transcript and interpolate it into the scout task as `Run worktree: <abs path>` or `Run worktree: none`:
21
-
22
- - A flow-level worktree the run created (any `git worktree add <path>` you ran, or a `Worktree ready at <path>` report from `/skill:using-git-worktrees`) or was told to work in (a resume brief's `worktree: yes <path>`, or a user instruction).
23
- - Never inferred from the session cwd; a created or assigned worktree still qualifies when it happens to equal the cwd. Never a per-task worktree from `tasks[].worktree: true` or `worktree: true` dispatches - those are ephemeral.
24
- - Two flow-level worktrees in the transcript: the candidate is the one most recently used by later work (a dispatch `cwd`, a `git -C <path>`, or a `cd <path>`); if that does not distinguish them, the most recent creation or assignment. The other goes into `## Open questions` as `Also seen: <path> - not recorded as the run worktree`.
25
- - No flow-level worktree created or named: `Run worktree: none`.
26
-
27
- After the scout returns, copy any `open-question:` line from `repo.md` into `## Open questions` without the `open-question:` prefix; it never appears in `## Repo state`.
28
-
29
- ## Repo snapshot
30
-
31
- ```
32
- subagent({ async: false, context: "fresh", tasks: [
33
- { agent: "scout", cwd: "<cwd>", reads: false, output: "<DIR>/repo.md",
34
- task: "Read-only; write only to your output path. Run worktree: <abs path|none>. Steps: (1) linked set: git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' - the first line is the primary checkout, the rest are the linked worktrees; a relative candidate resolves against the primary toplevel, not cwd. (2) candidate `none` or line absent -> target is cwd, go to (4). Otherwise if the candidate is relative, prefix it with the primary toplevel (first linked-set line) first; C=$(git -C <candidate> rev-parse --show-toplevel); valid when that succeeds and C equals one of the linked lines exactly (a line after the first; equality with the first, primary line is invalid); anything else (failure, not listed, the primary itself, prunable) is invalid -> target is cwd and emit the open-question line in (5). (3) valid -> target is $C and `worktree: yes $C`. (4) target cwd -> T=$(git rev-parse --show-toplevel); `worktree: yes $T` when $T equals one of the linked lines exactly (a line after the first; the first, primary line gives `worktree: no`), else `worktree: no`. (5) report each field on its own line, `unavailable` when a command fails, every git command as `git -C <target>`: toplevel (git -C <target> rev-parse --show-toplevel); the worktree line from (3) or (4); branch (git -C <target> branch --show-current, `detached` when empty); HEAD SHA; base (origin/HEAD short name, else origin/main or origin/master if present, else `base: unknown`); dirty (git -C <target> status --porcelain, or `clean`); diff-stat (git -C <target> diff --stat <base>...HEAD; `diff-stat: unavailable` when base is unknown); test command (<target>/package.json scripts or <target>/AGENTS.md). Invalid candidate: after the fields add the single line `open-question: producer named <candidate as submitted> as the run worktree; it is not a linked worktree of this repo`. cwd not a git repo -> the single line `not a git repo`, plus the same open-question line if a candidate was submitted." }
35
- ]})
36
- ```
37
-
38
- ## Process state
39
-
40
- Include `## Process state` only when both `phase_tracker` and `plan_tracker` tools exist AND `phase_tracker status` shows a phase `in_progress`. Then: `phase_tracker status` and `plan_tracker status` outputs verbatim; `Active task: <task name>` (the task you are working on; `none` when no plan is active or no task is active); the line `Gate history not restored - re-validate before advancing.`. Tools absent, all phases pending, or hotfix flow -> omit the section.
41
-
42
- ## Brief
43
-
44
- ```markdown
45
- # Handoff: <one line>
46
- ## Intent
47
- ## Repo state (the snapshot fields; `## Repo state: not a git repo` when so)
48
- ## Decisions (bullets; rejected alternatives marked `rejected:`)
49
- ## Open questions
50
- ## Skills loaded (frontmatter `name` of every skill whose SKILL.md body is in context - a `<skill>` block or a tool result on a `*/SKILL.md` path; `## Skills loaded: none` when there are none)
51
- ## Process state (only per the rule above)
52
- ```
53
-
54
- Write it, print the path and the brief.