@jaybeeuu/agent-cortex 2.6.0 → 2.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 +269 -0
- package/README.md +102 -8
- package/agents/README.md +2 -2
- package/agents/ralph/agent.md +19 -16
- package/agents/ralph/claude/wake.md +4 -4
- package/agents/ralph/pi/wake.md +15 -20
- package/bin/agent-cortex.mjs +176 -67
- package/bin/installers/claude.mjs +33 -8
- package/bin/installers/pi.mjs +11 -1
- package/docs/skills/skill-anatomy.md +212 -0
- package/extensions/post-merge-watch/README.md +133 -0
- package/extensions/post-merge-watch/index.test.ts +247 -0
- package/extensions/post-merge-watch/index.ts +123 -0
- package/extensions/post-merge-watch/package.json +16 -0
- package/extensions/post-merge-watch/watch.test.ts +469 -0
- package/extensions/post-merge-watch/watch.ts +431 -0
- package/extensions/session-clear/README.md +74 -0
- package/extensions/session-clear/index.test.ts +524 -0
- package/extensions/session-clear/index.ts +159 -0
- package/extensions/session-clear/package.json +16 -0
- package/lib/cli.mjs +88 -1
- package/lib/harness-update.mjs +254 -0
- package/lib/harness-versions.mjs +36 -2
- package/lib/self-update.mjs +553 -0
- package/package.json +2 -1
- package/scripts/lib/docs.mjs +67 -0
- package/skills/engineering/implement-task/SKILL.md +124 -0
- package/skills/engineering/tdd/SKILL.md +2 -0
- package/skills/planning/create-task/REFERENCE.md +20 -3
- package/skills/planning/create-task/SKILL.md +1 -1
- package/skills/planning/create-task/pipeline.json +16 -0
- package/skills/planning/create-task/scripts/create-chores.mjs +51 -24
- package/skills/planning/create-task/templates/code.md +8 -3
- package/skills/planning/create-task/templates/document.md +8 -3
- package/skills/planning/create-task/templates/fix.md +8 -3
- package/skills/planning/create-task/templates/housekeeping.md +69 -0
- package/skills/planning/create-task/templates/post-merge.md +67 -0
- package/skills/planning/create-task/templates/review.md +8 -3
- package/skills/planning/create-task/templates/verify.md +8 -3
- package/skills/productivity/bd-tool/SKILL.md +55 -0
- package/skills/productivity/write-a-skill/SKILL.md +2 -2
- package/skills/review/refactor-skill/SKILL.md +2 -2
- package/skills/review/review-defects/EXAMPLES.md +122 -0
- package/skills/review/review-defects/SKILL.md +91 -0
- package/skills/style/style-tests/EXAMPLES.md +30 -0
- package/skills/style/style-tests/SKILL.md +10 -6
- package/skills/workflow/plan/SKILL.md +14 -3
- package/skills/workflow/ralph/REFERENCE.md +137 -30
- package/skills/workflow/ralph/SKILL.md +26 -16
- package/skills/workflow/ralph/scripts/post-merge-check.mjs +651 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +41 -23
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +14 -8
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +2 -2
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -2
- package/skills/workflow/run-pipeline-stage/playbooks/housekeeping.md +58 -0
- package/skills/workflow/run-pipeline-stage/playbooks/post-merge.md +54 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +34 -3
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +2 -2
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +66 -10
- package/skills/workflow/run-pipeline-stage/scripts/recover-brief.mjs +805 -0
- package/skills/workflow/run-pipeline-stage/scripts/trail-entry.mjs +702 -0
- package/skills/workflow/using-agent-skills/SKILL.md +15 -7
- package/token-map.README.md +41 -4
- package/token-map.json +16 -5
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Skill Anatomy
|
|
2
|
+
|
|
3
|
+
This document is the **canonical skill anatomy** for `agent-cortex` skills.
|
|
4
|
+
Use it as the source of truth for `SKILL.md` structure, language, and review quality gates.
|
|
5
|
+
|
|
6
|
+
## Front-matter spec
|
|
7
|
+
|
|
8
|
+
Required fields:
|
|
9
|
+
- `name`: kebab-case skill identifier.
|
|
10
|
+
- `description`: max 1024 characters.
|
|
11
|
+
|
|
12
|
+
Frontmatter is machine-validated: `pnpm lint` runs `check:skills`, which fails on a
|
|
13
|
+
frontmatter block that would make pi silently drop the skill.
|
|
14
|
+
|
|
15
|
+
Optional fields:
|
|
16
|
+
- `argument-hint`: input prompt shown to users at invocation.
|
|
17
|
+
- `disable-model-invocation`: hides the skill from automatic model routing; explicit
|
|
18
|
+
`/skill-name` invocation still works. Set it when the danger is a fuzzy description
|
|
19
|
+
match, e.g. a skill whose steps are destructive. Preserved and honoured by pi and
|
|
20
|
+
Claude Code. See `docs/research/disable-model-invocation.md`.
|
|
21
|
+
|
|
22
|
+
## Description format rules
|
|
23
|
+
|
|
24
|
+
- Target ≤200 characters even though max is 1024.
|
|
25
|
+
- Write in third-person declarative voice.
|
|
26
|
+
- Sentence 1: what the skill does.
|
|
27
|
+
- Sentence 2: starts with `Use when ...`.
|
|
28
|
+
- Include 2-4 natural user trigger phrases (quoted) such as "diagnose this" or "something is broken".
|
|
29
|
+
- Prefer user vocabulary over internal jargon.
|
|
30
|
+
|
|
31
|
+
## Voice and tone rules
|
|
32
|
+
|
|
33
|
+
- Write the body in imperative second-person: "Run X", "Check Y", "Report Z".
|
|
34
|
+
- Use positive framing; reserve "do not" only for hard safety/correctness boundaries.
|
|
35
|
+
- Add one rationale sentence for non-obvious rules so behavior generalizes in novel cases.
|
|
36
|
+
- Avoid ALL-CAPS emphasis.
|
|
37
|
+
|
|
38
|
+
## Tool references
|
|
39
|
+
|
|
40
|
+
Reference agent tools by token, never by a hardcoded harness-specific name such as `bash`,
|
|
41
|
+
`task`, or `read_agent`. A tool token is the tool's canonical authoring name prefixed with
|
|
42
|
+
`TOOL:` and wrapped in doubled curly braces — two `{` before it and two `}` after it. The
|
|
43
|
+
wrapper is literal syntax: installers match it and substitute the per-harness name from
|
|
44
|
+
`token-map.json` at install time. Add any new tool to the map's `tools` section in the same
|
|
45
|
+
change; unknown tokens are hard errors. See `token-map.README.md` for the full authoring rules.
|
|
46
|
+
|
|
47
|
+
## Required sections
|
|
48
|
+
|
|
49
|
+
Every skill must include:
|
|
50
|
+
1. `## When to use`
|
|
51
|
+
2. `## Workflow` (numbered steps)
|
|
52
|
+
3. `## Verification checklist` (`- [ ]` items)
|
|
53
|
+
|
|
54
|
+
## Optional sections
|
|
55
|
+
|
|
56
|
+
For each optional section, define **what it is**, include an **example snippet**, and state **when to include** and **when to omit**.
|
|
57
|
+
|
|
58
|
+
| Section | What it is | Example snippet | When to include | When to omit |
|
|
59
|
+
|---|---|---|---|---|
|
|
60
|
+
| `When NOT to use` | Explicit out-of-scope invocation boundaries | `- When NOT to use: single-line typo fixes with no process risk.` | Skills with similar neighbors where invocation precision matters | Truly unique one-purpose utility skills |
|
|
61
|
+
| `Red Flags` | Detectable anti-patterns the agent should self-correct during execution | `- Red flag: skipping boundary validation to save time.` | Discipline-enforcement skills (security, TDD, review) | Simple utility wrappers |
|
|
62
|
+
| `Common Rationalizations` | Excuses models use to skip process with rebuttals | `| "This is small" | Small changes still need invariants checked. |` | Process-critical skills where shortcut pressure is common | Mechanical one-step skills |
|
|
63
|
+
| `Philosophy / rationale` | Why the method exists and what trade-offs it protects | `- We front-load validation because downstream rollback cost is higher.` | Opinionated skills expected to handle novel variants | Purely mechanical instructions |
|
|
64
|
+
| `Phase-gate checklists` | Mid-workflow gates that must pass before proceeding | `- [ ] Phase 1 complete: baseline tests captured.` | Multi-phase workflows (3+ distinct phases) | Linear single-phase tasks |
|
|
65
|
+
| `See Also` | Explicit handoff to another skill for a real sub-task: a relative link to its `SKILL.md` plus the reason to load it | `- [review-security](../../review/review-security/SKILL.md) — run before committing.` | Another skill owns a sub-domain better than restating it | Self-contained skills |
|
|
66
|
+
| `Examples` | Full worked sample of expected outcome/output | `- Input: ... Output: ...` | Any non-trivial output contract | Trivially self-evident skills |
|
|
67
|
+
| `Workflow diagrams` | Inline ASCII diagram showing step flow and decision points | Fenced block of `[Box]`, `|`, `-`, `>`, `v` arrows, e.g. `[Start] --> [Validate] --> [Run]` | Multi-step or branching workflows where a glanceable flow improves comprehension | Simple linear workflows already clear from numbered steps |
|
|
68
|
+
|
|
69
|
+
`## See Also` entries are one-line handoffs: a relative link to the related skill's `SKILL.md`, an
|
|
70
|
+
em-dash, and the reason a reader of this skill would load that one. A bare skill name leaves the
|
|
71
|
+
reader guessing whether the link matters; the reason is what turns a link list into routing.
|
|
72
|
+
|
|
73
|
+
Write the path against the authored tree (`../../<group>/<skill>/SKILL.md` from a skill directory).
|
|
74
|
+
The Claude Code installer flattens skills and rewrites these targets at copy time, so a hand-written
|
|
75
|
+
link that resolves here resolves in both install layouts. List only genuine handoffs — inline calls
|
|
76
|
+
inside `## Workflow` already cover skills used as a step, and `using-agent-skills` already maps the
|
|
77
|
+
whole corpus, so a duplicate entry adds noise.
|
|
78
|
+
|
|
79
|
+
## Workflow diagram style guide
|
|
80
|
+
|
|
81
|
+
When a skill includes a workflow diagram, follow these rules so it renders
|
|
82
|
+
reliably in any CLI:
|
|
83
|
+
|
|
84
|
+
- **ASCII only** — never Mermaid, images, or unicode box-drawing characters.
|
|
85
|
+
Keep to `|`, `-`, `>`, `+`, and `v`.
|
|
86
|
+
- **Fenced block** — wrap the diagram in a fenced code block so monospace is
|
|
87
|
+
preserved.
|
|
88
|
+
- **At most 80 characters per line** — narrow enough for terminal-width readers.
|
|
89
|
+
- **Short box labels** — imperative verbs or nouns (e.g. `[Validate scope]`),
|
|
90
|
+
never full sentences.
|
|
91
|
+
- **Read left-to-right, top-down** — prefer one branch level over dense joins.
|
|
92
|
+
- **Mirror the numbered workflow** — the diagram is a glanceable summary of the
|
|
93
|
+
`## Workflow` steps, never a source of truth that contradicts them.
|
|
94
|
+
|
|
95
|
+
## Section ordering
|
|
96
|
+
|
|
97
|
+
### When to use
|
|
98
|
+
|
|
99
|
+
State concrete invocation triggers using natural language phrases users actually type.
|
|
100
|
+
|
|
101
|
+
### When NOT to use
|
|
102
|
+
|
|
103
|
+
Place this section immediately after `When to use` whenever present.
|
|
104
|
+
|
|
105
|
+
### Workflow
|
|
106
|
+
|
|
107
|
+
Provide ordered, deterministic steps with observable outcomes.
|
|
108
|
+
|
|
109
|
+
### Verification checklist
|
|
110
|
+
|
|
111
|
+
Provide completion gates that are testable and stage-scoped.
|
|
112
|
+
|
|
113
|
+
## File layout and progressive disclosure
|
|
114
|
+
|
|
115
|
+
- Target ~150 lines for `SKILL.md`.
|
|
116
|
+
- Split long docs into purpose-named support files:
|
|
117
|
+
- `EXAMPLES.md` for worked examples.
|
|
118
|
+
- `FORMAT.md` for schemas/templates.
|
|
119
|
+
- `REFERENCE.md` for rare edge cases.
|
|
120
|
+
- Use slash-command references when another skill should be invoked.
|
|
121
|
+
|
|
122
|
+
## Acceptance criteria checklist
|
|
123
|
+
|
|
124
|
+
A skill passes anatomy review when:
|
|
125
|
+
- [ ] Front matter uses required fields and valid description format.
|
|
126
|
+
- [ ] Tool references use doubled-brace `TOOL:` tokens, not hardcoded tool names.
|
|
127
|
+
- [ ] Required sections exist and follow canonical ordering.
|
|
128
|
+
- [ ] Optional sections (if present) include what/when/example guidance.
|
|
129
|
+
- [ ] `See Also` entries link the target `SKILL.md` relatively and state why to load it.
|
|
130
|
+
- [ ] Workflow diagrams (if present) are ASCII-only and follow the workflow diagram style guide.
|
|
131
|
+
- [ ] Verification checklist contains concrete, machine-checkable items.
|
|
132
|
+
- [ ] Supporting docs are split by purpose when `SKILL.md` would exceed ~150 lines.
|
|
133
|
+
|
|
134
|
+
## Annotated template
|
|
135
|
+
|
|
136
|
+
```md
|
|
137
|
+
---
|
|
138
|
+
name: skill-name
|
|
139
|
+
description: This skill performs a specific capability. Use when a user asks for a matching workflow such as "triage this", "investigate this", or "prepare a handoff".
|
|
140
|
+
argument-hint: Optional prompt shown to users when extra input is needed.
|
|
141
|
+
disable-model-invocation: false
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
# Skill Name
|
|
145
|
+
|
|
146
|
+
## When to use
|
|
147
|
+
|
|
148
|
+
- Trigger phrases users actually say, e.g. "triage this failure".
|
|
149
|
+
- Include 2-4 concrete patterns.
|
|
150
|
+
|
|
151
|
+
## When NOT to use
|
|
152
|
+
|
|
153
|
+
- Out-of-scope cases that should route elsewhere.
|
|
154
|
+
|
|
155
|
+
## Workflow
|
|
156
|
+
|
|
157
|
+
1. Gather required context and validate scope boundaries.
|
|
158
|
+
2. Reject early with the reason when validation fails; otherwise continue.
|
|
159
|
+
3. Execute deterministic steps in order, recording key decisions.
|
|
160
|
+
4. Produce the required output artifact or terminal response.
|
|
161
|
+
|
|
162
|
+
Workflow at a glance (include only when the flow branches):
|
|
163
|
+
|
|
164
|
+
[Trigger]
|
|
165
|
+
|
|
|
166
|
+
v
|
|
167
|
+
[Gather context]
|
|
168
|
+
|
|
|
169
|
+
v
|
|
170
|
+
[Validate scope] --(invalid)--> [Reject early]
|
|
171
|
+
|
|
|
172
|
+
(valid)
|
|
173
|
+
v
|
|
174
|
+
[Run deterministic steps]
|
|
175
|
+
|
|
|
176
|
+
v
|
|
177
|
+
[Produce output artifact]
|
|
178
|
+
|
|
179
|
+
## Red Flags
|
|
180
|
+
|
|
181
|
+
- Red flag: skipping validation of unknown external input.
|
|
182
|
+
|
|
183
|
+
## Common Rationalizations
|
|
184
|
+
|
|
185
|
+
| Rationalization | Rebuttal |
|
|
186
|
+
|---|---|
|
|
187
|
+
| "This is too small for process" | Process protects against regressions regardless of size. |
|
|
188
|
+
|
|
189
|
+
## Philosophy / rationale
|
|
190
|
+
|
|
191
|
+
- We optimize for explicit boundaries because they reduce rework and improve reliability.
|
|
192
|
+
|
|
193
|
+
## Phase-gate checklist
|
|
194
|
+
|
|
195
|
+
- [ ] Phase 1 complete and baseline checks captured.
|
|
196
|
+
- [ ] Phase 2 complete and contract validation passed.
|
|
197
|
+
|
|
198
|
+
## See Also
|
|
199
|
+
|
|
200
|
+
- [review-security](../../review/review-security/SKILL.md) — run before committing any externally exposed change.
|
|
201
|
+
|
|
202
|
+
## Examples
|
|
203
|
+
|
|
204
|
+
Input: "Investigate this flaky suite"
|
|
205
|
+
Output: concise root-cause summary, failing tests list, and next action.
|
|
206
|
+
|
|
207
|
+
## Verification checklist
|
|
208
|
+
|
|
209
|
+
- [ ] Output matches requested format.
|
|
210
|
+
- [ ] Required tests/lints were run and results recorded.
|
|
211
|
+
- [ ] No unrelated files were changed.
|
|
212
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Post-Merge Watch Extension
|
|
2
|
+
|
|
3
|
+
Wakes an idle PI session with the verdict of a post-merge CI run.
|
|
4
|
+
|
|
5
|
+
A human merges the feature PR, and the CI run for the merge commit decides what
|
|
6
|
+
happens next: nothing, a fix, or a revert. That merge lands outside the session,
|
|
7
|
+
so nothing tells the agent when the run concludes. This extension polls the
|
|
8
|
+
deterministic post-merge check and delivers the verdict as a follow-up message,
|
|
9
|
+
which starts a turn in an idle session.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi --post-merge-watch <bead>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Input | Meaning |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `--post-merge-watch <bead>` | Watch the post-merge check for this bead and wake the session with the verdict |
|
|
20
|
+
| `AGENT_CORTEX_POST_MERGE_CHECK` | Path of the check script, when the shipped one is not the one to run |
|
|
21
|
+
|
|
22
|
+
The flag is registered at load time, so it is accepted on the command line. A
|
|
23
|
+
session without the flag starts no watch, spawns no process, and writes no state.
|
|
24
|
+
|
|
25
|
+
The default script is `skills/workflow/ralph/scripts/post-merge-check.mjs`,
|
|
26
|
+
resolved relative to this extension's own file. In a staged install
|
|
27
|
+
(`extensions/` beside `skills/`) and in a checkout of this repository the same
|
|
28
|
+
relative path resolves, so no absolute path is baked in.
|
|
29
|
+
|
|
30
|
+
## What it does
|
|
31
|
+
|
|
32
|
+
1. At session start, with the flag set, it records the watch and runs the check.
|
|
33
|
+
2. It runs the check every 15 seconds, up to 40 times (about 10 minutes).
|
|
34
|
+
3. When the check exits `0` (passed) or `1` (failed), or the attempt cap is
|
|
35
|
+
reached, it sends one message and stops watching.
|
|
36
|
+
|
|
37
|
+
Each invocation passes `--wait 0`, so the script reports once instead of waiting
|
|
38
|
+
inside itself: the poll interval and the bound belong to this extension, and a
|
|
39
|
+
stopped watch never blocks on a running child process.
|
|
40
|
+
|
|
41
|
+
The wake uses the confirmed PI signature
|
|
42
|
+
`pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true })`: an idle
|
|
43
|
+
session starts a turn from it, and a session in the middle of a turn reads it
|
|
44
|
+
after that turn instead of being interrupted.
|
|
45
|
+
|
|
46
|
+
The message names the bead, the verdict, the exit code, the reason, the run URL,
|
|
47
|
+
the project, and the number of checks made. It carries the same fields as message
|
|
48
|
+
`details`, so a renderer can read them without parsing the text.
|
|
49
|
+
|
|
50
|
+
## What it never does
|
|
51
|
+
|
|
52
|
+
- **No verdict logic.** The check's exit code is the verdict: `0` passed, `1`
|
|
53
|
+
failed, every other code (including a timeout kill) inconclusive. The run URL
|
|
54
|
+
and the reason are read from the script's JSON report and forwarded verbatim.
|
|
55
|
+
The extension never decides whether a run passed and never re-runs a workflow.
|
|
56
|
+
- **No model call.** No prompt, no summarisation, no title generation. One turn
|
|
57
|
+
happens only because the delivered message starts one.
|
|
58
|
+
- **No `@earendil-works/*` value import.** Types only, so the staged install
|
|
59
|
+
loads with pi absent from the extension's own `node_modules`
|
|
60
|
+
(`test/pi-extensions-load.test.mjs` proves this).
|
|
61
|
+
- **No second watch for the same bead.** A session reload re-fires
|
|
62
|
+
`session_start`; the extension starts one watch per bead.
|
|
63
|
+
|
|
64
|
+
## State
|
|
65
|
+
|
|
66
|
+
| Item | Value |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Directory | `~/.pi/agent-cortex/post-merge-watch/` |
|
|
69
|
+
| File name | `<project dir name>-<bead>-<digest of full project path and bead>.json` |
|
|
70
|
+
| Written | At watch start, then after each poll |
|
|
71
|
+
| Removed | When the watch settles, when the session shuts down, and again if a write was still in flight |
|
|
72
|
+
|
|
73
|
+
The digest covers the whole project path, so two checkouts whose directory names
|
|
74
|
+
match — or two projects watching the same bead — never share a file. A watch that
|
|
75
|
+
is still polling is the only thing the directory should hold.
|
|
76
|
+
|
|
77
|
+
The write and the removal can race: a stop removes the file, and a write that is
|
|
78
|
+
still in flight then recreates it. The write therefore re-checks whether the
|
|
79
|
+
watch stopped once it completes, and removes the file again, so a stop cannot
|
|
80
|
+
leave a state file behind. One narrow case survives: a write that **fails** while
|
|
81
|
+
a stop is in progress returns before that re-check, so it can leave a partial
|
|
82
|
+
file. No code reads the directory, so a partial file is cosmetic; do not read it
|
|
83
|
+
as a live watch.
|
|
84
|
+
|
|
85
|
+
The in-flight record is diagnostic: it names the bead, the project, the script,
|
|
86
|
+
the pid, the attempt count, and the last exit code and reason. Nothing reads it
|
|
87
|
+
back, so a watch never resumes from it; a new session with the same flag starts a
|
|
88
|
+
new watch.
|
|
89
|
+
|
|
90
|
+
## Bounds and shutdown
|
|
91
|
+
|
|
92
|
+
| Bound | Value |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| Poll interval | 15 s |
|
|
95
|
+
| Checks | 40 |
|
|
96
|
+
| One check invocation | 120 s, then pi kills it |
|
|
97
|
+
|
|
98
|
+
The pending timer is unref'd, so a watch never holds the host process open. On
|
|
99
|
+
`session_shutdown` the timer is cancelled, a check already in flight is ignored,
|
|
100
|
+
and the state file is removed: an abandoned watch wakes nobody and leaves nothing
|
|
101
|
+
behind.
|
|
102
|
+
|
|
103
|
+
## What has been proven
|
|
104
|
+
|
|
105
|
+
The wake path has been run once end to end against a stub check script: the RPC
|
|
106
|
+
wake itself was reproduced (`agent_settled` → `agent_start`, the custom message
|
|
107
|
+
arriving with no client prompt), and a real model turn ran on it. That run did not
|
|
108
|
+
read a real CI run, because the shipped `post-merge-check.mjs` was replaced by a
|
|
109
|
+
stub: the poll-and-wake path is proven, and the check's own judgement is covered
|
|
110
|
+
by `test/post-merge-check.test.mjs`, not by any test here. Treat the pair as one
|
|
111
|
+
path only after a watch has read a genuine merge-commit run.
|
|
112
|
+
|
|
113
|
+
## Claude Code parity
|
|
114
|
+
|
|
115
|
+
PI-only. Claude Code has its own watch tools, which the Claude side uses instead of
|
|
116
|
+
an extension.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pnpm test # the poll lifecycle, the wake payload, the state file, the wiring
|
|
122
|
+
pnpm typecheck # tsc --noEmit
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`watch.test.ts` drives the whole lifecycle with a scheduler it steps by hand and a
|
|
126
|
+
check runner that returns scripted exit codes, so no test sleeps or waits on a
|
|
127
|
+
timer. `index.test.ts` drives the extension through a partial `ExtensionAPI`
|
|
128
|
+
double; the complete-literal guard that fails `tsc` when pi's interface grows is
|
|
129
|
+
`extensions/agent-modes/index.test.ts`.
|
|
130
|
+
|
|
131
|
+
To watch the poll end to end against a script that is not the shipped one, point
|
|
132
|
+
`AGENT_CORTEX_POST_MERGE_CHECK` at any executable that follows the check's
|
|
133
|
+
contract: JSON on stdout and exit `0`/`1`/`2`.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { createTempRoot } from "../../test/helpers/temp-root.mjs";
|
|
8
|
+
import postMergeWatchExtension, { CHECK_SCRIPT_ENV, WATCH_FLAG, checkScript } from "./index.ts";
|
|
9
|
+
|
|
10
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const tempRoot = createTempRoot("post-merge-watch-index-");
|
|
13
|
+
|
|
14
|
+
interface FlagRegistration {
|
|
15
|
+
name: string;
|
|
16
|
+
options: { description?: string; type: "string" | "boolean"; default?: string | boolean };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SentMessage {
|
|
20
|
+
message: { customType: string; content: string | unknown[]; display: boolean; details?: unknown };
|
|
21
|
+
options: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" } | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ExecCall {
|
|
25
|
+
command: string;
|
|
26
|
+
args: string[];
|
|
27
|
+
options: { cwd?: string; timeout?: number } | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** An event handler as `pi.on` recorded it, called through `Reflect.apply`. */
|
|
31
|
+
type RecordedHandler = (...args: never[]) => unknown;
|
|
32
|
+
|
|
33
|
+
interface FakePi {
|
|
34
|
+
pi: ExtensionAPI;
|
|
35
|
+
flags: FlagRegistration[];
|
|
36
|
+
sent: SentMessage[];
|
|
37
|
+
execs: ExecCall[];
|
|
38
|
+
notifications: Array<{ message: string; type: string | undefined }>;
|
|
39
|
+
/** Fire every handler pi registered for `event`. */
|
|
40
|
+
fire(event: string, payload: unknown, context: unknown): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A double for the five `ExtensionAPI` members this extension reaches.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately partial: the complete-literal guard is
|
|
47
|
+
* `extensions/agent-modes/index.test.ts`, whose fake fails `tsc` when pi's
|
|
48
|
+
* interface grows a member.
|
|
49
|
+
*/
|
|
50
|
+
function createFakePi(options: {
|
|
51
|
+
flag: string | undefined;
|
|
52
|
+
scriptExitCode: number;
|
|
53
|
+
scriptStdout?: string;
|
|
54
|
+
}): FakePi {
|
|
55
|
+
const flags: FlagRegistration[] = [];
|
|
56
|
+
const sent: SentMessage[] = [];
|
|
57
|
+
const execs: ExecCall[] = [];
|
|
58
|
+
const notifications: Array<{ message: string; type: string | undefined }> = [];
|
|
59
|
+
const handlers = new Map<string, RecordedHandler[]>();
|
|
60
|
+
|
|
61
|
+
const pi = {
|
|
62
|
+
registerFlag: (name: string, registration: FlagRegistration["options"]) => {
|
|
63
|
+
flags.push({ name, options: registration });
|
|
64
|
+
},
|
|
65
|
+
getFlag: (name: string) => (name === WATCH_FLAG ? options.flag : undefined),
|
|
66
|
+
on: (event: string, handler: RecordedHandler) => {
|
|
67
|
+
handlers.set(event, [...(handlers.get(event) ?? []), handler]);
|
|
68
|
+
return () => {};
|
|
69
|
+
},
|
|
70
|
+
exec: async (command: string, args: string[], execOptions: ExecCall["options"]) => {
|
|
71
|
+
execs.push({ command, args, options: execOptions });
|
|
72
|
+
return {
|
|
73
|
+
stdout: options.scriptStdout ?? "",
|
|
74
|
+
stderr: "",
|
|
75
|
+
code: options.scriptExitCode,
|
|
76
|
+
killed: false,
|
|
77
|
+
truncated: false,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
sendMessage: (message: SentMessage["message"], sendOptions?: SentMessage["options"]) => {
|
|
81
|
+
sent.push({ message, options: sendOptions });
|
|
82
|
+
},
|
|
83
|
+
} as unknown as ExtensionAPI;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
pi,
|
|
87
|
+
flags,
|
|
88
|
+
sent,
|
|
89
|
+
execs,
|
|
90
|
+
notifications,
|
|
91
|
+
async fire(event, payload, context) {
|
|
92
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
93
|
+
await Reflect.apply(handler, undefined, [
|
|
94
|
+
payload,
|
|
95
|
+
{
|
|
96
|
+
cwd: "/projects/one",
|
|
97
|
+
ui: { notify: (message: string, type?: string) => notifications.push({ message, type }) },
|
|
98
|
+
...(context as object),
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The startup event pi sends before a session is usable. */
|
|
107
|
+
const SESSION_START = { type: "session_start", reason: "startup" } as const;
|
|
108
|
+
|
|
109
|
+
/** Run one session start with `HOME` pointed at a temp root, so no test reads the real home. */
|
|
110
|
+
async function withTempHome<T>(run: (home: string) => Promise<T>): Promise<T> {
|
|
111
|
+
const home = await tempRoot.createWorkspace();
|
|
112
|
+
const previousHome = process.env.HOME;
|
|
113
|
+
process.env.HOME = home;
|
|
114
|
+
try {
|
|
115
|
+
return await run(home);
|
|
116
|
+
} finally {
|
|
117
|
+
process.env.HOME = previousHome;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The state directory the extension uses for a given home. */
|
|
122
|
+
function stateDirFor(home: string): string {
|
|
123
|
+
return join(home, ".pi", "agent-cortex", "post-merge-watch");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function exists(path: string): Promise<boolean> {
|
|
127
|
+
try {
|
|
128
|
+
await stat(path);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Flag registration ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe("post-merge-watch extension", () => {
|
|
138
|
+
it("registers its CLI flag in the factory, before any session starts", async () => {
|
|
139
|
+
const fake = createFakePi({ flag: undefined, scriptExitCode: 2 });
|
|
140
|
+
|
|
141
|
+
postMergeWatchExtension(fake.pi);
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(
|
|
144
|
+
fake.flags.map((flag) => ({ name: flag.name, type: flag.options.type })),
|
|
145
|
+
[{ name: "post-merge-watch", type: "string" }],
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("resolves the shipped check script without a hardcoded absolute path", () => {
|
|
150
|
+
const previous = process.env[CHECK_SCRIPT_ENV];
|
|
151
|
+
delete process.env[CHECK_SCRIPT_ENV];
|
|
152
|
+
try {
|
|
153
|
+
assert.match(checkScript(), /skills[/\\]workflow[/\\]ralph[/\\]scripts[/\\]post-merge-check\.mjs$/);
|
|
154
|
+
} finally {
|
|
155
|
+
if (previous !== undefined) process.env[CHECK_SCRIPT_ENV] = previous;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("polls the script the environment names, so an install can be pointed at a stub", async () => {
|
|
160
|
+
const previous = process.env[CHECK_SCRIPT_ENV];
|
|
161
|
+
process.env[CHECK_SCRIPT_ENV] = "/stub/post-merge-check.mjs";
|
|
162
|
+
try {
|
|
163
|
+
assert.equal(checkScript(), "/stub/post-merge-check.mjs");
|
|
164
|
+
} finally {
|
|
165
|
+
if (previous === undefined) delete process.env[CHECK_SCRIPT_ENV];
|
|
166
|
+
else process.env[CHECK_SCRIPT_ENV] = previous;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── Session lifecycle ─────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
it("starts one watch at session start and wakes the session when the check settles", async () => {
|
|
173
|
+
await withTempHome(async (home) => {
|
|
174
|
+
const fake = createFakePi({
|
|
175
|
+
flag: "agnt-ctx-86u4.2",
|
|
176
|
+
scriptExitCode: 1,
|
|
177
|
+
scriptStdout: JSON.stringify({ reason: "run-failed", runs: [{ url: "https://gh/run/9" }] }),
|
|
178
|
+
});
|
|
179
|
+
postMergeWatchExtension(fake.pi);
|
|
180
|
+
|
|
181
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
182
|
+
|
|
183
|
+
assert.equal(fake.execs.length, 1);
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
{ args: fake.execs[0].args.slice(1), cwd: fake.execs[0].options?.cwd },
|
|
186
|
+
{ args: ["--wait", "0"], cwd: "/projects/one" },
|
|
187
|
+
);
|
|
188
|
+
assert.equal(fake.sent.length, 1);
|
|
189
|
+
assert.deepEqual(fake.sent[0].options, { deliverAs: "followUp", triggerTurn: true });
|
|
190
|
+
assert.match(fake.sent[0].message.content as string, /agnt-ctx-86u4\.2/);
|
|
191
|
+
assert.deepEqual(fake.sent[0].message.details, {
|
|
192
|
+
bead: "agnt-ctx-86u4.2",
|
|
193
|
+
verdict: "failed",
|
|
194
|
+
runUrl: "https://gh/run/9",
|
|
195
|
+
exitCode: 1,
|
|
196
|
+
reason: "run-failed",
|
|
197
|
+
attempts: 1,
|
|
198
|
+
project: "/projects/one",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("starts no watch when the flag is absent", async () => {
|
|
204
|
+
await withTempHome(async (home) => {
|
|
205
|
+
const fake = createFakePi({ flag: undefined, scriptExitCode: 1 });
|
|
206
|
+
postMergeWatchExtension(fake.pi);
|
|
207
|
+
|
|
208
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
209
|
+
|
|
210
|
+
assert.deepEqual(fake.execs, []);
|
|
211
|
+
assert.equal(await exists(stateDirFor(home)), false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("records the watch under the agent-cortex state directory, then clears it on session shutdown", async () => {
|
|
216
|
+
await withTempHome(async (home) => {
|
|
217
|
+
const fake = createFakePi({ flag: "agnt-ctx-86u4.2", scriptExitCode: 2 });
|
|
218
|
+
postMergeWatchExtension(fake.pi);
|
|
219
|
+
|
|
220
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
221
|
+
|
|
222
|
+
assert.deepEqual(
|
|
223
|
+
(await readdir(stateDirFor(home))).length,
|
|
224
|
+
1,
|
|
225
|
+
"the in-flight watch is recorded under ~/.pi/agent-cortex",
|
|
226
|
+
);
|
|
227
|
+
assert.equal(fake.sent.length, 0, "no verdict yet, so no wake");
|
|
228
|
+
|
|
229
|
+
await fake.fire("session_shutdown", { type: "session_shutdown", reason: "quit" }, {});
|
|
230
|
+
|
|
231
|
+
assert.deepEqual(await readdir(stateDirFor(home)), [], "an abandoned watch leaves no state file");
|
|
232
|
+
assert.deepEqual(fake.sent, [], "an abandoned watch wakes nobody");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("starts no second watch when the session reloads for the same bead", async () => {
|
|
237
|
+
await withTempHome(async () => {
|
|
238
|
+
const fake = createFakePi({ flag: "agnt-ctx-86u4.2", scriptExitCode: 2 });
|
|
239
|
+
postMergeWatchExtension(fake.pi);
|
|
240
|
+
|
|
241
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
242
|
+
await fake.fire("session_start", { type: "session_start", reason: "reload" }, {});
|
|
243
|
+
|
|
244
|
+
assert.equal(fake.execs.length, 1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|