@jaybeeuu/agent-cortex 1.43.0 → 2.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/.agents/skills/style-versioning/SKILL.md +5 -6
- package/.github/workflows/ci.yml +4 -11
- package/AGENTS.md +13 -17
- package/CHANGELOG.md +66 -0
- package/README.md +57 -57
- package/agents/README.md +9 -23
- package/agents/ralph/agent.md +13 -17
- package/agents/ralph/claude/polling.md +1 -1
- package/agents-native/ralph.md +15 -5
- package/bin/agent-cortex.mjs +0 -16
- package/bin/installers/claude.mjs +82 -11
- package/docs/claude-hooks.md +16 -0
- package/docs/ideas/improve-pi-system-prompt.md +9 -3
- package/docs/research/disable-model-invocation.md +5 -26
- package/docs/skills/skill-anatomy.md +5 -3
- package/docs/technical-direction/harness-config.md +6 -14
- package/docs/user-preferences.md +2 -2
- package/extensions/agent-modes/README.md +3 -3
- package/extensions/agent-modes/discover.ts +6 -6
- package/extensions/output-style/README.md +58 -0
- package/extensions/output-style/index.ts +23 -0
- package/extensions/output-style/output-style.md +43 -0
- package/extensions/output-style/output-style.test.ts +50 -0
- package/extensions/output-style/output-style.ts +32 -0
- package/extensions/output-style/package.json +19 -0
- package/extensions/output-style/tsconfig.json +12 -0
- package/extensions/subagent/README.md +2 -2
- package/extensions/subagent/agents.ts +1 -2
- package/lib/cli.mjs +5 -7
- package/package.json +5 -6
- package/scripts/check-skill-frontmatter.mjs +21 -0
- package/scripts/lib/compose-agent.mjs +10 -10
- package/scripts/lib/skill-frontmatter.mjs +135 -0
- package/skills/workflow/ralph/REFERENCE.md +34 -11
- package/skills/workflow/ralph/SKILL.md +10 -10
- package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
- package/test/cli.test.mjs +11 -10
- package/test/installer.test.mjs +143 -1
- package/test/pending-gates.test.mjs +147 -0
- package/test/skill-frontmatter.test.mjs +137 -0
- package/token-map.README.md +9 -12
- package/token-map.json +26 -31
- package/agents/plan/copilot/frontmatter.json +0 -6
- package/agents/plan.agent.md +0 -59
- package/agents/ralph/copilot/frontmatter.json +0 -6
- package/agents/ralph/copilot/polling.md +0 -62
- package/agents/ralph-plan/copilot/frontmatter.json +0 -6
- package/agents/ralph-plan.agent.md +0 -126
- package/agents/ralph.agent.md +0 -331
- package/agents/strategy/copilot/frontmatter.json +0 -6
- package/agents/strategy.agent.md +0 -112
- package/bin/installers/copilot.mjs +0 -124
- package/hooks.json +0 -10
- package/plugin.json +0 -35
- package/scripts/build-copilot-agents.mjs +0 -30
- package/scripts/install-instructions.sh +0 -82
- package/scripts/sync-plugin-version.sh +0 -17
- package/test/copilot-installer.test.mjs +0 -308
|
@@ -74,13 +74,37 @@ When the `document` chore for a parent task completes:
|
|
|
74
74
|
3. Report the PR URL in chat as soon as it is created.
|
|
75
75
|
4. Find the child HITL task bead for this parent with label `lifecycle:feature-pr` (created by `create-task`).
|
|
76
76
|
5. Update that HITL bead with the PR URL/status (comment or note) so the reviewer has the link.
|
|
77
|
-
6. Do not schedule new parent features while this
|
|
78
|
-
7. Once the PR is merged, a human closes the HITL PR gate bead. After that, close the parent feature bead:
|
|
77
|
+
6. Do not schedule new parent features while this gate is **unblocked** — an unblocked gate is a feature genuinely awaiting a human merge (see _Gate predicate_ below).
|
|
78
|
+
7. Once the PR is merged, a human closes the HITL PR gate bead, which unblocks the scheduling brake. After that, close the parent feature bead:
|
|
79
79
|
```bash
|
|
80
80
|
bd close <parent-id>
|
|
81
81
|
```
|
|
82
82
|
Only then continue with next parent features.
|
|
83
83
|
|
|
84
|
+
### Gate predicate — parked vs pending
|
|
85
|
+
|
|
86
|
+
Ralph's only scheduling brake is a gate that is **unblocked**. `create-chores` pre-creates a `lifecycle:feature-pr` gate for *every* planned feature, blocked behind that feature's own `document` chore; such a gate is **parked** — the feature has not reached its PR stage — and must never stop dispatch. Only when the `document` chore closes does the gate unblock, and that is the state in which a human must merge.
|
|
87
|
+
|
|
88
|
+
Resolve the question with one command instead of ad-hoc `bd` queries:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node skills/workflow/ralph/scripts/pending-gates.mjs
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Field | Meaning | Effect on ralph |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `pendingHumanAction` | Unblocked `lifecycle:feature-pr` gates (all blockers closed) plus epics tagged `awaiting-epic-pr-merge`, each as `{ id, title, kind }` | Non-empty → do **not** schedule new parent features; the same list is the **Pending Human Action** table |
|
|
97
|
+
| `parked` | Open gates still blocked behind their own pipeline | Informational only — never stops dispatch, never appears in the table |
|
|
98
|
+
|
|
99
|
+
Gate queries match the HITL axis on either `execution:hitl` (current) or `implementation-type:hitl` (legacy), so gates created on either side of the two-axis migration are counted.
|
|
100
|
+
|
|
101
|
+
**Invariant — what may block a gate.** `parked` means "blocked by its own pipeline". A gate blocked by its parent **feature** bead is not parked in any harness: that bead stays open until after the merge, so the gate never reaches `bd ready`, never appears in `pendingHumanAction`, and a human merge is silently never requested. Keep the two shapes apart:
|
|
102
|
+
|
|
103
|
+
- A `create-chores` gate unblocks exactly once, when its `document` chore closes. Until then it is parked and must not stop dispatch.
|
|
104
|
+
- A gate must never carry a `blocks` dependency onto its own feature bead (the native-Claude shape: `bd create ... --labels ...lifecycle:feature-pr` then dep it to `<id>`). Remove it with `bd dep rm <gate> <id>` if present, so the gate is unblocked while its PR awaits merge.
|
|
105
|
+
|
|
106
|
+
So the predicate is sensitive to *what* blocks a gate, not only whether something does. `agents-native/ralph.md` (the Claude harness) keeps the open-gate form for this reason: its gates stay blocked by design, and its `document` chores never close; F10 (agnt-ctx-s5xd.10) unifies the two predicates.
|
|
107
|
+
|
|
84
108
|
---
|
|
85
109
|
|
|
86
110
|
## Fix Loop
|
|
@@ -116,7 +140,7 @@ When a verify or review stage fails:
|
|
|
116
140
|
|
|
117
141
|
## Log Polling
|
|
118
142
|
|
|
119
|
-
Run whenever
|
|
143
|
+
Run whenever completion handling is triggered (an agent completion):
|
|
120
144
|
|
|
121
145
|
1. **Query in-flight beads**: `bd list --status=in_progress --type=chore`. Extract each bead's ID and title.
|
|
122
146
|
2. For each in-flight bead, read new lines from its **parent's** log file — logs are keyed by **parent** bead ID, so all stages of a feature share one file. Track the next unread line number per parent in memory, starting at 1:
|
|
@@ -137,7 +161,7 @@ Run whenever polling is triggered (timer or agent completion):
|
|
|
137
161
|
|
|
138
162
|
## HITL Pause
|
|
139
163
|
|
|
140
|
-
Proceed here when no chore beads are in-flight, no `stage:*` chore beads are ready, and
|
|
164
|
+
Proceed here when no chore beads are in-flight, no `stage:*` chore beads are ready, and `pending-gates.mjs` reports a non-empty `pendingHumanAction` — an **unblocked** `lifecycle:feature-pr` gate or an epic tagged `awaiting-epic-pr-merge` (see _Gate predicate_). Gates still parked behind their own pipeline do not count. Ralph stops rather than burning requests on idle polls.
|
|
141
165
|
|
|
142
166
|
1. Regenerate `.agent-cortex/ralph/progress.md` one final time — do not delete it.
|
|
143
167
|
2. For each **multi-feature** epic whose feature beads are all closed but not yet tagged `awaiting-epic-pr-merge`, open/update an epic PR to main (skip single-feature epics — their feature branch PRs already target `main`):
|
|
@@ -149,12 +173,11 @@ Proceed here when no chore beads are in-flight, no `stage:*` chore beads are rea
|
|
|
149
173
|
```bash
|
|
150
174
|
bd dolt push
|
|
151
175
|
```
|
|
152
|
-
4. Collect
|
|
176
|
+
4. Collect the table rows from `pendingHumanAction`:
|
|
153
177
|
```bash
|
|
154
|
-
|
|
155
|
-
bd list -l awaiting-epic-pr-merge
|
|
178
|
+
node skills/workflow/ralph/scripts/pending-gates.mjs
|
|
156
179
|
```
|
|
157
|
-
For each
|
|
180
|
+
For each entry, run `bd show <id>` to retrieve the PR URL from bead notes. `kind: "feature-pr"` rows read "review and merge the feature PR"; `kind: "epic-pr"` rows read "review and merge the epic PR into main".
|
|
158
181
|
5. Output the **Pending Human Action** summary:
|
|
159
182
|
```
|
|
160
183
|
⏸️ Ralph is paused — human action required before work can continue.
|
|
@@ -167,7 +190,7 @@ Proceed here when no chore beads are in-flight, no `stage:*` chore beads are rea
|
|
|
167
190
|
|
|
168
191
|
When you've completed the above, prompt me to continue.
|
|
169
192
|
```
|
|
170
|
-
6. **Stop completely.** Do not
|
|
193
|
+
6. **Stop completely.** Do not continue the event loop. Wait for the user to re-prompt before doing any further work.
|
|
171
194
|
|
|
172
195
|
---
|
|
173
196
|
|
|
@@ -209,7 +232,7 @@ When `bd list --status=in_progress --type=chore` returns no results AND `bd read
|
|
|
209
232
|
| File | Purpose |
|
|
210
233
|
|------|---------|
|
|
211
234
|
| `.agent-cortex/ralph/progress.md` | Human-readable snapshot. Regenerate: `workspace="/abs/path"; pnpm --prefix skills/workflow/run-pipeline-stage/scripts exec tsx generate-progress.ts --workspace "$workspace" > "$workspace/.agent-cortex/ralph/progress.md"`. `workspace` must be an **absolute** path — never `.` or `$(pwd)`. **Never hand-edit.** |
|
|
212
|
-
| `.agent-cortex/ralph/state.json` | Runtime bookkeeping only: the
|
|
235
|
+
| `.agent-cortex/ralph/state.json` | Runtime bookkeeping only: the in-flight agent-ID→bead mapping. Shape: `{ "inflight": [ { "choreId": "...", "parentId": "...", "title": "...", "agentId": "...", "logLine": 1 } ] }`. Initialise at startup with `{ "inflight": [] }`. Update after every dispatch and every agent completion. **Never hand-edit.** |
|
|
213
236
|
| `.agent-cortex/ralph/ralph-*.log` | Per-parent log files written by subagents — every stage chore of a feature appends to `.agent-cortex/ralph/ralph-<parent-id>.log` (e.g. `ralph-abc-123.log`). |
|
|
214
237
|
|
|
215
238
|
All task state is derived from beads; `state.json` holds only runtime bookkeeping:
|
|
@@ -221,5 +244,5 @@ All task state is derived from beads; `state.json` holds only runtime bookkeepin
|
|
|
221
244
|
| What stage is a bead in? | Read the `stage:*` label from `bd show <id>` |
|
|
222
245
|
| Which agent-ID maps to which bead? | `inflight` entries in `.agent-cortex/ralph/state.json` |
|
|
223
246
|
| How many fix rounds? | Count chore beads with label `stage:fix` that are children of the parent task. Read `maxFixRounds` from `skills/planning/create-task/pipeline.json`. |
|
|
224
|
-
| Which features are review-gated? |
|
|
247
|
+
| Which features are review-gated? | `pendingHumanAction` entries with `kind: "feature-pr"` from `pending-gates.mjs` — unblocked `lifecycle:feature-pr` gates on either HITL axis. A gate in `parked` is pre-created but not yet actionable |
|
|
225
248
|
| Which epics are review-gated? | `bd list -l awaiting-epic-pr-merge` |
|
|
@@ -7,7 +7,7 @@ description: "Run all pending beads end-to-end with up to 5 concurrent pipeline-
|
|
|
7
7
|
|
|
8
8
|
Ralph is a parallel orchestration loop. It advances AFK task beads through pipeline-stage chores (`code` → `verify` → `review` → `document`), dispatching each stage as a background subagent in the feature's worktree, then opens review-gated feature and epic PRs and pauses for human merges. Ralph never writes code itself — it dispatches, tracks, and reports.
|
|
9
9
|
|
|
10
|
-
State model: task state lives in beads (ready/in_progress via `bd`); the only local file is `.agent-cortex/ralph/state.json`, holding the
|
|
10
|
+
State model: task state lives in beads (ready/in_progress via `bd`); the only local file is `.agent-cortex/ralph/state.json`, holding the agent-ID→bead mapping. Pipeline configuration comes from `skills/planning/create-task/pipeline.json`, and stage execution belongs to the `run-pipeline-stage` skill.
|
|
11
11
|
|
|
12
12
|
## When to use
|
|
13
13
|
|
|
@@ -28,14 +28,14 @@ State model: task state lives in beads (ready/in_progress via `bd`); the only lo
|
|
|
28
28
|
1. Run `bd prime` and hold the output verbatim in memory.
|
|
29
29
|
2. Ensure `.agent-cortex/` and `.agent-cortex/worktrees/` are in the project's `.gitignore` (append any that are missing).
|
|
30
30
|
3. Create the workspace: `mkdir -p .agent-cortex/ralph`.
|
|
31
|
-
4. Initialize state: write `.agent-cortex/ralph/state.json` with `{ "
|
|
31
|
+
4. Initialize state: write `.agent-cortex/ralph/state.json` with `{ "inflight": [] }`.
|
|
32
32
|
5. Read `skills/planning/create-task/pipeline.json` and hold it in memory — you need its `maxFixRounds` for the fix loop.
|
|
33
33
|
6. Run `bd ready` and classify each available bead:
|
|
34
34
|
- **Chore with a `stage:*` label**: pipeline stage bead — eligible for dispatch.
|
|
35
35
|
- **Task or other type without an `implementation-type:*` label**: unexpected here — classification happens at planning time via `create-task`. Do not invoke `classify-bead`; treat as needs-refinement and note it for the shutdown summary.
|
|
36
|
-
- **Task labelled `implementation-type:hitl
|
|
36
|
+
- **Task labelled `execution:hitl`** (or the legacy `implementation-type:hitl`): skip — record the ID for the Pending Human Action summary at shutdown.
|
|
37
37
|
7. For each ready chore bead (up to 5), dispatch it from its parent's feature worktree (see _Dispatching a chore bead_ in REFERENCE.md — create branches/worktrees first).
|
|
38
|
-
8. Hand over to your harness's completion-detection mechanism:
|
|
38
|
+
8. Hand over to your harness's completion-detection mechanism: PI keeps a {{TOOL:wait_for_agents}} call outstanding over every in-flight agent ID. If no chore was dispatched and `pending-gates.mjs` reports a non-empty `pendingHumanAction` (an unblocked `lifecycle:feature-pr` gate, or an epic tagged `awaiting-epic-pr-merge`), go straight to HITL Pause (REFERENCE.md).
|
|
39
39
|
9. Regenerate `.agent-cortex/ralph/progress.md`:
|
|
40
40
|
```bash
|
|
41
41
|
# workspace must be the absolute path you cd'd into — never . or $(pwd)
|
|
@@ -45,7 +45,7 @@ State model: task state lives in beads (ready/in_progress via `bd`); the only lo
|
|
|
45
45
|
|
|
46
46
|
### 2. Event loop
|
|
47
47
|
|
|
48
|
-
On each completion notification (background agent finished
|
|
48
|
+
On each completion notification (a background agent finished):
|
|
49
49
|
|
|
50
50
|
1. Poll that parent's log for new lines — `tail -n +<nextLine> .agent-cortex/ralph/ralph-<parent-id>.log` (logs are keyed by parent bead ID, so every stage of a feature shares one file) — and post only new key events (stage transitions, test/lint results), never empty updates.
|
|
51
51
|
2. Read the completed agent's output with {{TOOL:read_agent}} and parse its `---REPORT---` block for `BEAD_ID`, `STAGE_COMPLETED`, and the outcome.
|
|
@@ -62,8 +62,8 @@ On each completion notification (background agent finished, or the poll timer fi
|
|
|
62
62
|
| `review` | `REVIEW_OUTCOME: CHANGES_REQUESTED` | Run the fix loop (REFERENCE.md) |
|
|
63
63
|
| `document` | — | Open/update the feature PR now (`feature/<parent-id>` → its base branch), report the URL, and update the child HITL gate bead (`lifecycle:feature-pr`). Wait for a human to close that bead after merge, then `bd close <parent-id>` |
|
|
64
64
|
|
|
65
|
-
5. Run `bd ready` and dispatch new ready `stage:*` chore beads while the in-flight count (`bd list --status=in_progress --type=chore`) is under 5. Do not schedule new parent features while
|
|
66
|
-
6. If no chores are in-flight and `bd ready` returns no `stage:*` chores, proceed to HITL Pause (REFERENCE.md) when
|
|
65
|
+
5. Run `bd ready` and dispatch new ready `stage:*` chore beads while the in-flight count (`bd list --status=in_progress --type=chore`) is under 5. Do not schedule new parent features while `pending-gates.mjs` reports a non-empty `pendingHumanAction` — that is, while a `lifecycle:feature-pr` gate is **unblocked**. Gates still blocked behind their own pipeline (pre-created for a feature that has not reached its PR stage) are parked and never stop dispatch.
|
|
66
|
+
6. If no chores are in-flight and `bd ready` returns no `stage:*` chores, proceed to HITL Pause (REFERENCE.md) when `pendingHumanAction` is non-empty, otherwise to Shutdown (REFERENCE.md).
|
|
67
67
|
7. Regenerate `.agent-cortex/ralph/progress.md`.
|
|
68
68
|
|
|
69
69
|
### 3. Branching and review model
|
|
@@ -75,7 +75,7 @@ On each completion notification (background agent finished, or the poll timer fi
|
|
|
75
75
|
|
|
76
76
|
### 4. HITL pause and shutdown
|
|
77
77
|
|
|
78
|
-
- **HITL Pause** — when no chores are in-flight, none ready, and
|
|
78
|
+
- **HITL Pause** — when no chores are in-flight, none ready, and `pending-gates.mjs` reports a non-empty `pendingHumanAction`: regenerate `progress.md`, open/update epic PRs to `main` for multi-feature epics and tag each epic `awaiting-epic-pr-merge` (single-feature epics need none — their feature PRs already target `main`), run `bd dolt push`, output the Pending Human Action table, then stop completely until re-prompted.
|
|
79
79
|
- **Shutdown** — when all work is complete: regenerate `progress.md`, open/update epic PRs to `main` for multi-feature epics and tag them (single-feature epics need none), run `bd dolt push`, then output the Pending Human Action / Needs Refinement / cap-blocked / Pending Epic Review summaries that apply, or "All beads complete."
|
|
80
80
|
|
|
81
81
|
## Red Flags
|
|
@@ -97,7 +97,7 @@ On each completion notification (background agent finished, or the poll timer fi
|
|
|
97
97
|
|
|
98
98
|
## Philosophy / rationale
|
|
99
99
|
|
|
100
|
-
- Orchestration state derives from beads so a crash loses nothing but an in-memory ID mapping: `state.json` holds only the
|
|
100
|
+
- Orchestration state derives from beads so a crash loses nothing but an in-memory ID mapping: `state.json` holds only the agent-ID→bead mapping, never task state.
|
|
101
101
|
- Classification happens up-front at planning time (`create-task`), so the loop can trust `bd ready` instead of re-interpreting each bead's intent.
|
|
102
102
|
- Human review gates are non-negotiable because agent-authored changes still need a person to merge them.
|
|
103
103
|
|
|
@@ -109,7 +109,7 @@ On each completion notification (background agent finished, or the poll timer fi
|
|
|
109
109
|
## Cross-skill references
|
|
110
110
|
|
|
111
111
|
- The `run-pipeline-stage` skill owns the stage-runner prompt, per-stage playbooks, the `---REPORT---` format, and fix-loop feedback beads — read it before dispatching any stage.
|
|
112
|
-
- The `create-task` skill owns `pipeline.json` and creates the `lifecycle:feature-pr` HITL gate beads.
|
|
112
|
+
- The `create-task` skill owns `pipeline.json` and creates the `lifecycle:feature-pr` HITL gate beads. `skills/workflow/ralph/scripts/pending-gates.mjs` owns the gate predicate — parked vs pending — and is the only place the gate queries live.
|
|
113
113
|
- Do not invoke `classify-bead` from this loop — classification was already applied at planning time.
|
|
114
114
|
|
|
115
115
|
## Verification checklist
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Resolves which beads are waiting on a human, and which are merely parked
|
|
2
|
+
// behind their own pipeline.
|
|
3
|
+
//
|
|
4
|
+
// Ralph schedules new parent features only when nothing is pending human action.
|
|
5
|
+
// The distinction that matters is *unblocked*, not *open*: `create-chores`
|
|
6
|
+
// pre-creates a `lifecycle:feature-pr` gate for every planned feature, blocked
|
|
7
|
+
// behind that feature's own document chore. Such a gate is parked — the feature
|
|
8
|
+
// has not reached its PR stage — and must never stop dispatch. Only once the
|
|
9
|
+
// document chore closes does the gate unblock, and that is the state in which a
|
|
10
|
+
// feature genuinely awaits a human merge.
|
|
11
|
+
//
|
|
12
|
+
// Zero dependencies: run with `node pending-gates.mjs`.
|
|
13
|
+
|
|
14
|
+
import { execFile } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
|
|
18
|
+
/** The structural label every feature PR gate carries, whatever its axis. */
|
|
19
|
+
export const FEATURE_GATE_LABEL = 'lifecycle:feature-pr';
|
|
20
|
+
|
|
21
|
+
/** Epics whose PR into `main` awaits a human merge. */
|
|
22
|
+
export const EPIC_GATE_LABEL = 'awaiting-epic-pr-merge';
|
|
23
|
+
|
|
24
|
+
// Two-axis migration: gates created before the `execution:afk/hitl` migration
|
|
25
|
+
// carry `implementation-type:hitl`, gates created after carry `execution:hitl`.
|
|
26
|
+
// Both mean "a human must act", so the query matches either.
|
|
27
|
+
export const HITL_GATE_LABELS = ['execution:hitl', 'implementation-type:hitl'];
|
|
28
|
+
|
|
29
|
+
/** @typedef {{ id: string, title?: string }} Bead */
|
|
30
|
+
/** @typedef {{ id: string, title: string, kind: 'feature-pr' | 'epic-pr' }} PendingAction */
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} GateState
|
|
34
|
+
* @property {PendingAction[]} pendingHumanAction Beads a human must act on now — non-empty means "do not schedule new parent features".
|
|
35
|
+
* @property {Array<{ id: string, title: string }>} parked Open gates still blocked behind their own pipeline; informational only.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Split open gates into those awaiting a human now and those still parked.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} input
|
|
42
|
+
* @param {Bead[]} input.readyGates Open gates with no active blockers.
|
|
43
|
+
* @param {Bead[]} input.openGates Every open gate, blocked or not.
|
|
44
|
+
* @param {Bead[]} input.epicsAwaitingMerge Epics tagged `awaiting-epic-pr-merge`.
|
|
45
|
+
* @returns {GateState}
|
|
46
|
+
*/
|
|
47
|
+
export function resolveGateState({ readyGates, openGates, epicsAwaitingMerge }) {
|
|
48
|
+
const actionable = new Set(readyGates.map((bead) => bead.id));
|
|
49
|
+
|
|
50
|
+
const pendingHumanAction = [
|
|
51
|
+
...readyGates.map((bead) => pendingAction(bead, 'feature-pr')),
|
|
52
|
+
...epicsAwaitingMerge.map((bead) => pendingAction(bead, 'epic-pr')),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const parked = openGates
|
|
56
|
+
.filter((bead) => !actionable.has(bead.id))
|
|
57
|
+
.map((bead) => ({ id: bead.id, title: bead.title ?? '' }));
|
|
58
|
+
|
|
59
|
+
return { pendingHumanAction, parked };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Bead} bead
|
|
64
|
+
* @param {'feature-pr' | 'epic-pr'} kind
|
|
65
|
+
* @returns {PendingAction}
|
|
66
|
+
*/
|
|
67
|
+
function pendingAction(bead, kind) {
|
|
68
|
+
return { id: bead.id, title: bead.title ?? '', kind };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// BD_PATH env override lets machines where bd is not on PATH pin a location.
|
|
72
|
+
const BD_PATH = process.env.BD_PATH || 'bd';
|
|
73
|
+
const execFileAsync = promisify(execFile);
|
|
74
|
+
|
|
75
|
+
/** @type {(args: string[]) => Promise<string>} */
|
|
76
|
+
async function defaultExec(args) {
|
|
77
|
+
const { stdout } = await execFileAsync(BD_PATH, args, { encoding: 'utf-8' });
|
|
78
|
+
return stdout;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Query bd for the current gate state.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} [options]
|
|
85
|
+
* @param {(args: string[]) => Promise<string>} [options.exec] bd runner; injectable for tests.
|
|
86
|
+
* @returns {Promise<GateState>}
|
|
87
|
+
*/
|
|
88
|
+
export async function fetchGateState({ exec = defaultExec } = {}) {
|
|
89
|
+
const gateFilter = ['-l', FEATURE_GATE_LABEL, '--label-any', HITL_GATE_LABELS.join(',')];
|
|
90
|
+
|
|
91
|
+
// `bd ready` is blocker-aware: it returns only gates whose document chore has
|
|
92
|
+
// closed, i.e. the ones genuinely awaiting a human merge.
|
|
93
|
+
const readyGates = JSON.parse(await exec(['ready', '--json', ...gateFilter]));
|
|
94
|
+
const openGates = JSON.parse(await exec(['list', '--json', '--limit', '0', ...gateFilter]));
|
|
95
|
+
const epicsAwaitingMerge = JSON.parse(await exec(['list', '--json', '--limit', '0', '-l', EPIC_GATE_LABEL]));
|
|
96
|
+
|
|
97
|
+
return resolveGateState({ readyGates, openGates, epicsAwaitingMerge });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
const USAGE = 'Usage: node pending-gates.mjs\n\nPrints { pendingHumanAction, parked } as JSON.\n';
|
|
103
|
+
|
|
104
|
+
class UsageError extends Error {}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {object} options
|
|
108
|
+
* @param {string[]} options.argv CLI arguments (without node/script).
|
|
109
|
+
* @param {(args: string[]) => Promise<string>} [options.exec] bd runner; injectable for tests.
|
|
110
|
+
* @param {(chunk: string) => void} [options.write] stdout sink; injectable for tests.
|
|
111
|
+
* @returns {Promise<number>} process exit code.
|
|
112
|
+
*/
|
|
113
|
+
export async function runCli({ argv, exec = defaultExec, write = (chunk) => process.stdout.write(chunk) }) {
|
|
114
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
115
|
+
write(USAGE);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (argv.length > 0) throw new UsageError(`unexpected argument: ${argv[0]}`);
|
|
119
|
+
|
|
120
|
+
write(`${JSON.stringify(await fetchGateState({ exec }), null, 2)}\n`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Only run the CLI when invoked directly, so tests can import the pure resolver.
|
|
125
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
126
|
+
runCli({ argv: process.argv.slice(2) })
|
|
127
|
+
.then((code) => {
|
|
128
|
+
process.exitCode = code;
|
|
129
|
+
})
|
|
130
|
+
.catch((error) => {
|
|
131
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
132
|
+
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
133
|
+
});
|
|
134
|
+
}
|
package/test/cli.test.mjs
CHANGED
|
@@ -54,11 +54,6 @@ describe("parseArgs", () => {
|
|
|
54
54
|
assert.deepStrictEqual(result, { command: "help" });
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it("returns { command: 'install', harness: 'copilot' } for install copilot", async () => {
|
|
58
|
-
const result = parseArgs(["install", "copilot"]);
|
|
59
|
-
assert.deepStrictEqual(result, { command: "install", harness: "copilot" });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
57
|
it("returns { command: 'install', harness: 'claude' } for install claude", async () => {
|
|
63
58
|
const result = parseArgs(["install", "claude"]);
|
|
64
59
|
assert.deepStrictEqual(result, { command: "install", harness: "claude" });
|
|
@@ -169,11 +164,17 @@ describe("buildHelpText", () => {
|
|
|
169
164
|
|
|
170
165
|
describe("validateHarness", () => {
|
|
171
166
|
it("returns ok for supported harnesses", async () => {
|
|
172
|
-
for (const h of ["
|
|
167
|
+
for (const h of ["claude", "pi"]) {
|
|
173
168
|
assert.deepStrictEqual(validateHarness(h), { ok: true });
|
|
174
169
|
}
|
|
175
170
|
});
|
|
176
171
|
|
|
172
|
+
it("returns error for copilot, which is no longer a deploy target", async () => {
|
|
173
|
+
const result = validateHarness("copilot");
|
|
174
|
+
assert.equal(result.ok, false);
|
|
175
|
+
assert.ok(result.error.includes("Unknown harness"));
|
|
176
|
+
});
|
|
177
|
+
|
|
177
178
|
it("returns error when harness is null", async () => {
|
|
178
179
|
const result = validateHarness(null);
|
|
179
180
|
assert.equal(result.ok, false);
|
|
@@ -207,10 +208,10 @@ describe("CLI integration", () => {
|
|
|
207
208
|
assert.ok(stdout.includes("Usage:"));
|
|
208
209
|
});
|
|
209
210
|
|
|
210
|
-
it("exits
|
|
211
|
-
const { exitCode,
|
|
212
|
-
assert.equal(exitCode,
|
|
213
|
-
assert.ok(
|
|
211
|
+
it("exits 1 for install copilot, which is no longer a deploy target", async () => {
|
|
212
|
+
const { exitCode, stderr } = await runCli(["install", "copilot"]);
|
|
213
|
+
assert.equal(exitCode, 1);
|
|
214
|
+
assert.ok(stderr.includes("Unknown harness"));
|
|
214
215
|
});
|
|
215
216
|
|
|
216
217
|
it("exits 0 for install claude (into a temp output dir)", async () => {
|
package/test/installer.test.mjs
CHANGED
|
@@ -661,4 +661,146 @@ describe("install claude — settings.json", () => {
|
|
|
661
661
|
await fx.cleanup();
|
|
662
662
|
}
|
|
663
663
|
});
|
|
664
|
-
});
|
|
664
|
+
});
|
|
665
|
+
// ─── installClaude — managed CLAUDE.md section ────────────────────────────────
|
|
666
|
+
|
|
667
|
+
const MANAGED_BEGIN = "<!-- BEGIN agent-cortex:output-style -->";
|
|
668
|
+
const MANAGED_END = "<!-- END agent-cortex:output-style -->";
|
|
669
|
+
const OUTPUT_SOURCE = "## Output style (agent-cortex)\n\nRules for replies.\n";
|
|
670
|
+
|
|
671
|
+
/** Minimal package root carrying the output-style source, enough for
|
|
672
|
+
* installClaude to materialise the managed CLAUDE.md section. */
|
|
673
|
+
async function makeInstructionsFixture({ source = OUTPUT_SOURCE, memory } = {}) {
|
|
674
|
+
const root = await makeTmp();
|
|
675
|
+
await writeFile(
|
|
676
|
+
join(root, "package.json"),
|
|
677
|
+
JSON.stringify({ name: "agent-cortex", version: "1.2.3", license: "MIT" }),
|
|
678
|
+
);
|
|
679
|
+
await writeFile(
|
|
680
|
+
join(root, "token-map.json"),
|
|
681
|
+
JSON.stringify({ tools: {}, paths: { plugin_root: { claude: "PLUGIN_ROOT" } } }),
|
|
682
|
+
);
|
|
683
|
+
await mkdir(join(root, "agents"));
|
|
684
|
+
await mkdir(join(root, "skills"));
|
|
685
|
+
if (source !== null) {
|
|
686
|
+
await mkdir(join(root, "extensions", "output-style"), { recursive: true });
|
|
687
|
+
await writeFile(join(root, "extensions", "output-style", "output-style.md"), source);
|
|
688
|
+
}
|
|
689
|
+
const output = join(root, "out");
|
|
690
|
+
const instructionsPath = join(root, "claude-home", "CLAUDE.md");
|
|
691
|
+
if (memory !== undefined) {
|
|
692
|
+
await mkdir(dirname(instructionsPath), { recursive: true });
|
|
693
|
+
await writeFile(instructionsPath, memory);
|
|
694
|
+
}
|
|
695
|
+
return { root, output, instructionsPath, cleanup: () => rm(root, { recursive: true, force: true }) };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const MANAGED_SECTION = `${MANAGED_BEGIN}\n${OUTPUT_SOURCE.trimEnd()}\n${MANAGED_END}\n`;
|
|
699
|
+
|
|
700
|
+
describe("installClaude — managed CLAUDE.md section", () => {
|
|
701
|
+
it("creates CLAUDE.md with the managed section when the file does not exist", async () => {
|
|
702
|
+
const fx = await makeInstructionsFixture();
|
|
703
|
+
try {
|
|
704
|
+
const result = await installClaude({ root: fx.root, output: fx.output, instructionsPath: fx.instructionsPath });
|
|
705
|
+
|
|
706
|
+
assert.equal(result.instructions.action, "written");
|
|
707
|
+
assert.equal(await readFile(fx.instructionsPath, "utf-8"), MANAGED_SECTION);
|
|
708
|
+
} finally {
|
|
709
|
+
await fx.cleanup();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("appends the section to existing memory and keeps the user's text", async () => {
|
|
714
|
+
const fx = await makeInstructionsFixture({ memory: "# My memory\n\nAlways run the tests.\n" });
|
|
715
|
+
try {
|
|
716
|
+
await installClaude({ root: fx.root, output: fx.output, instructionsPath: fx.instructionsPath });
|
|
717
|
+
|
|
718
|
+
const written = await readFile(fx.instructionsPath, "utf-8");
|
|
719
|
+
assert.ok(written.startsWith("# My memory\n\nAlways run the tests.\n\n"), "personal text kept at the top");
|
|
720
|
+
assert.ok(written.endsWith(MANAGED_SECTION), "managed section appended");
|
|
721
|
+
} finally {
|
|
722
|
+
await fx.cleanup();
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("replaces only the managed section, keeping memory text on both sides", async () => {
|
|
727
|
+
const memory = `# Top\n\n${MANAGED_BEGIN}\n## Output style (agent-cortex)\n\nStale rules.\n${MANAGED_END}\n\n# Bottom\n\nKeep me.\n`;
|
|
728
|
+
const fx = await makeInstructionsFixture({ memory });
|
|
729
|
+
try {
|
|
730
|
+
await installClaude({ root: fx.root, output: fx.output, instructionsPath: fx.instructionsPath });
|
|
731
|
+
|
|
732
|
+
const written = await readFile(fx.instructionsPath, "utf-8");
|
|
733
|
+
assert.equal(written, `# Top\n\n${MANAGED_SECTION}\n# Bottom\n\nKeep me.\n`);
|
|
734
|
+
assert.equal(written.includes("Stale rules."), false, "stale block replaced");
|
|
735
|
+
} finally {
|
|
736
|
+
await fx.cleanup();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("re-running is idempotent — the second install reports unchanged", async () => {
|
|
741
|
+
const fx = await makeInstructionsFixture({ memory: "# My memory\n" });
|
|
742
|
+
try {
|
|
743
|
+
await installClaude({ root: fx.root, output: fx.output, instructionsPath: fx.instructionsPath });
|
|
744
|
+
const first = await readFile(fx.instructionsPath, "utf-8");
|
|
745
|
+
|
|
746
|
+
const second = await installClaude({ root: fx.root, output: fx.output, instructionsPath: fx.instructionsPath });
|
|
747
|
+
assert.equal(second.instructions.action, "unchanged");
|
|
748
|
+
assert.equal(await readFile(fx.instructionsPath, "utf-8"), first);
|
|
749
|
+
assert.equal(first.split(MANAGED_BEGIN).length - 1, 1, "exactly one managed section");
|
|
750
|
+
} finally {
|
|
751
|
+
await fx.cleanup();
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("leaves a file with an unterminated section unchanged and warns", async () => {
|
|
756
|
+
const memory = `# My memory\n\n${MANAGED_BEGIN}\nHand-edited, no end marker.\n`;
|
|
757
|
+
const fx = await makeInstructionsFixture({ memory });
|
|
758
|
+
try {
|
|
759
|
+
const warnings = [];
|
|
760
|
+
const result = await installClaude({
|
|
761
|
+
root: fx.root,
|
|
762
|
+
output: fx.output,
|
|
763
|
+
instructionsPath: fx.instructionsPath,
|
|
764
|
+
warn: (m) => warnings.push(m),
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
assert.equal(result.instructions.action, "skipped-unmanaged");
|
|
768
|
+
assert.equal(await readFile(fx.instructionsPath, "utf-8"), memory, "file untouched");
|
|
769
|
+
assert.ok(warnings.some((w) => w.includes("unterminated")), "unmatched markers are warned about");
|
|
770
|
+
} finally {
|
|
771
|
+
await fx.cleanup();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("dry-run plans the CLAUDE.md write without touching the filesystem", async () => {
|
|
776
|
+
const fx = await makeInstructionsFixture();
|
|
777
|
+
try {
|
|
778
|
+
const result = await installClaude({
|
|
779
|
+
root: fx.root,
|
|
780
|
+
output: fx.output,
|
|
781
|
+
instructionsPath: fx.instructionsPath,
|
|
782
|
+
dryRun: true,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
assert.equal(result.instructions.action, "would-write");
|
|
786
|
+
assert.equal(await pathExists(fx.instructionsPath), false, "nothing written in dry-run");
|
|
787
|
+
} finally {
|
|
788
|
+
await fx.cleanup();
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("ships the real output-style source the pi extension reads", async () => {
|
|
793
|
+
const real = await readFile(join(ROOT, "extensions", "output-style", "output-style.md"), "utf-8");
|
|
794
|
+
assert.ok(real.startsWith("## Output style (agent-cortex)"), "canonical heading");
|
|
795
|
+
|
|
796
|
+
const fx = await makeInstructionsFixture({ source: real });
|
|
797
|
+
try {
|
|
798
|
+
await installClaude({ root: fx.root, output: fx.output, instructionsPath: fx.instructionsPath });
|
|
799
|
+
const written = await readFile(fx.instructionsPath, "utf-8");
|
|
800
|
+
assert.ok(written.includes("### Write Simplified Technical English (ASD-STE100)"));
|
|
801
|
+
assert.ok(written.includes("### Restate the context, never assume it"));
|
|
802
|
+
} finally {
|
|
803
|
+
await fx.cleanup();
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
EPIC_GATE_LABEL,
|
|
6
|
+
FEATURE_GATE_LABEL,
|
|
7
|
+
HITL_GATE_LABELS,
|
|
8
|
+
fetchGateState,
|
|
9
|
+
resolveGateState,
|
|
10
|
+
runCli,
|
|
11
|
+
} from '../skills/workflow/ralph/scripts/pending-gates.mjs';
|
|
12
|
+
|
|
13
|
+
function createBead({ id, title = `${id} title` }) {
|
|
14
|
+
return { id, title };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fake `bd` for the query-contract tests: answers the three queries ralph makes
|
|
19
|
+
* and refuses anything else, so an unexpected query fails loudly.
|
|
20
|
+
*/
|
|
21
|
+
function makeExec({ ready = [], open = [], epics = [] } = {}) {
|
|
22
|
+
const calls = [];
|
|
23
|
+
|
|
24
|
+
const exec = async (args) => {
|
|
25
|
+
calls.push(args);
|
|
26
|
+
if (args[0] === 'ready') return JSON.stringify(ready);
|
|
27
|
+
if (args[0] === 'list' && args.includes(EPIC_GATE_LABEL)) return JSON.stringify(epics);
|
|
28
|
+
if (args[0] === 'list') return JSON.stringify(open);
|
|
29
|
+
throw new Error(`unexpected bd call: ${args.join(' ')}`);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { exec, calls };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function capture() {
|
|
36
|
+
let output = '';
|
|
37
|
+
return { write: (chunk) => { output += chunk; }, read: () => JSON.parse(output) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('resolveGateState', () => {
|
|
41
|
+
it('leaves dispatch unblocked while every gate is still parked behind its own pipeline', () => {
|
|
42
|
+
const state = resolveGateState({
|
|
43
|
+
readyGates: [],
|
|
44
|
+
openGates: [
|
|
45
|
+
createBead({ id: 'proj-1.5' }),
|
|
46
|
+
createBead({ id: 'proj-2.5' }),
|
|
47
|
+
createBead({ id: 'proj-3.5' }),
|
|
48
|
+
createBead({ id: 'proj-4.5' }),
|
|
49
|
+
],
|
|
50
|
+
epicsAwaitingMerge: [],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(state.pendingHumanAction, []);
|
|
54
|
+
assert.deepEqual(state.parked, [
|
|
55
|
+
{ id: 'proj-1.5', title: 'proj-1.5 title' },
|
|
56
|
+
{ id: 'proj-2.5', title: 'proj-2.5 title' },
|
|
57
|
+
{ id: 'proj-3.5', title: 'proj-3.5 title' },
|
|
58
|
+
{ id: 'proj-4.5', title: 'proj-4.5 title' },
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reports an unblocked gate as pending human action', () => {
|
|
63
|
+
const state = resolveGateState({
|
|
64
|
+
readyGates: [createBead({ id: 'proj-1.5', title: 'PR Review and Merge' })],
|
|
65
|
+
openGates: [createBead({ id: 'proj-1.5', title: 'PR Review and Merge' })],
|
|
66
|
+
epicsAwaitingMerge: [],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(state.pendingHumanAction, [
|
|
70
|
+
{ id: 'proj-1.5', title: 'PR Review and Merge', kind: 'feature-pr' },
|
|
71
|
+
]);
|
|
72
|
+
assert.deepEqual(state.parked, []);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('separates an unblocked gate from gates that are still parked', () => {
|
|
76
|
+
const state = resolveGateState({
|
|
77
|
+
readyGates: [createBead({ id: 'proj-1.5' })],
|
|
78
|
+
openGates: [createBead({ id: 'proj-1.5' }), createBead({ id: 'proj-2.5' })],
|
|
79
|
+
epicsAwaitingMerge: [],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(state.pendingHumanAction.map((gate) => gate.id), ['proj-1.5']);
|
|
83
|
+
assert.deepEqual(state.parked.map((gate) => gate.id), ['proj-2.5']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('reports an epic awaiting merge as pending human action alongside feature gates', () => {
|
|
87
|
+
const state = resolveGateState({
|
|
88
|
+
readyGates: [],
|
|
89
|
+
openGates: [],
|
|
90
|
+
epicsAwaitingMerge: [createBead({ id: 'epic-9', title: 'Merge epic into main' })],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(state.pendingHumanAction, [
|
|
94
|
+
{ id: 'epic-9', title: 'Merge epic into main', kind: 'epic-pr' },
|
|
95
|
+
]);
|
|
96
|
+
assert.deepEqual(state.parked, []);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('fetchGateState', () => {
|
|
101
|
+
it('asks bd for unblocked gates, all gates, and epics awaiting merge on either HITL axis', async () => {
|
|
102
|
+
const { exec, calls } = makeExec();
|
|
103
|
+
|
|
104
|
+
await fetchGateState({ exec });
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(calls, [
|
|
107
|
+
['ready', '--json', '-l', FEATURE_GATE_LABEL, '--label-any', HITL_GATE_LABELS.join(',')],
|
|
108
|
+
['list', '--json', '--limit', '0', '-l', FEATURE_GATE_LABEL, '--label-any', HITL_GATE_LABELS.join(',')],
|
|
109
|
+
['list', '--json', '--limit', '0', '-l', EPIC_GATE_LABEL],
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('covers both the current and the legacy HITL label axis', () => {
|
|
114
|
+
assert.deepEqual(HITL_GATE_LABELS, ['execution:hitl', 'implementation-type:hitl']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('treats a gate that only the list query returns as parked', async () => {
|
|
118
|
+
const { exec } = makeExec({ ready: [], open: [createBead({ id: 'proj-1.5' })] });
|
|
119
|
+
|
|
120
|
+
const state = await fetchGateState({ exec });
|
|
121
|
+
|
|
122
|
+
assert.deepEqual(state.pendingHumanAction, []);
|
|
123
|
+
assert.deepEqual(state.parked, [{ id: 'proj-1.5', title: 'proj-1.5 title' }]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('pending-gates CLI', () => {
|
|
128
|
+
it('prints the resolved gate state as JSON', async () => {
|
|
129
|
+
const { exec } = makeExec({
|
|
130
|
+
ready: [createBead({ id: 'proj-1.5' })],
|
|
131
|
+
open: [createBead({ id: 'proj-1.5' }), createBead({ id: 'proj-2.5' })],
|
|
132
|
+
epics: [createBead({ id: 'epic-9' })],
|
|
133
|
+
});
|
|
134
|
+
const out = capture();
|
|
135
|
+
|
|
136
|
+
const code = await runCli({ argv: [], exec, write: out.write });
|
|
137
|
+
|
|
138
|
+
assert.equal(code, 0);
|
|
139
|
+
assert.deepEqual(out.read(), {
|
|
140
|
+
pendingHumanAction: [
|
|
141
|
+
{ id: 'proj-1.5', title: 'proj-1.5 title', kind: 'feature-pr' },
|
|
142
|
+
{ id: 'epic-9', title: 'epic-9 title', kind: 'epic-pr' },
|
|
143
|
+
],
|
|
144
|
+
parked: [{ id: 'proj-2.5', title: 'proj-2.5 title' }],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|