dev-loops 0.5.0 → 0.6.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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/commands/auto.md +7 -0
- package/.claude/commands/continue.md +15 -0
- package/.claude/commands/info.md +7 -0
- package/.claude/commands/start-spike.md +16 -0
- package/.claude/commands/start.md +7 -0
- package/.claude/commands/status.md +6 -0
- package/.claude/hooks/_run-context.mjs +11 -4
- package/.claude/skills/dev-loop/SKILL.md +11 -6
- package/.claude/skills/docs/release-runbook.md +45 -0
- package/.claude/skills/docs/ui-e2e-scoping-step.md +102 -0
- package/.claude/skills/local-implementation/SKILL.md +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +20 -1
- package/agents/dev-loop.agent.md +8 -1
- package/cli/index.mjs +2 -0
- package/extension/index.ts +10 -1
- package/extension/presentation.ts +15 -0
- package/lib/dev-loops-core.mjs +141 -0
- package/package.json +5 -2
- package/scripts/claude/generate-claude-assets.mjs +15 -1
- package/scripts/github/comment-issue.mjs +181 -0
- package/scripts/github/fetch-ci-logs.mjs +215 -0
- package/scripts/github/list-issues.mjs +191 -0
- package/scripts/loop/_handoff-contract.mjs +1 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +31 -1
- package/scripts/loop/run-conductor-cycle.mjs +5 -0
- package/scripts/projects/resolve-active-board-item.mjs +193 -0
- package/scripts/refine/scaffold-spike-file.mjs +183 -0
- package/scripts/release/extract-changelog-section.mjs +111 -0
- package/skills/dev-loop/SKILL.md +14 -2
- package/skills/docs/release-runbook.md +45 -0
- package/skills/docs/ui-e2e-scoping-step.md +102 -0
- package/skills/local-implementation/SKILL.md +1 -1
|
@@ -22,7 +22,7 @@ The envelope is the primary handoff artifact — it is derived from resolver out
|
|
|
22
22
|
|
|
23
23
|
**Construction sequence:**
|
|
24
24
|
|
|
25
|
-
1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops@0.
|
|
25
|
+
1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops@0.6.0 loop startup --issue <n>` for issues, or `npx dev-loops@0.6.0 loop startup --pr <n>` for PRs.
|
|
26
26
|
2. Pass the resolver output, resolved settings (merged from `.devloops` and `.pi/dev-loop/defaults.yaml`), and current gate state to `buildDevLoopHandoffEnvelope()`.
|
|
27
27
|
3. **Validate the envelope** with `validateHandoffEnvelope()` before consuming any field. If validation returns `ok: false`, reject the handoff with the structured error — do not load requiredReads, do not execute nextAction, do not delegate.
|
|
28
28
|
4. Read the envelope as the first artifact.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Run an autonomous dev loop on a GitHub issue until the human approval checkpoint."
|
|
3
|
+
argument-hint: "<issue>"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/auto.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Run the `dev-loop` skill with the public intent `auto dev loop on issue $ARGUMENTS`. Resolve authoritative state first, then continue autonomously through the routed loop until a stop condition or the human approval checkpoint. Do not pick an internal strategy name yourself.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Continue the dev loop — the current in-progress board item, or a given issue/PR."
|
|
3
|
+
argument-hint: "[issue|pr]"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/continue.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Continue a dev loop. Two forms, both handed to the `dev-loop` skill — do NOT pick an internal strategy yourself.
|
|
8
|
+
|
|
9
|
+
- With an argument (`$ARGUMENTS` is an issue or PR — `123`, `#123`, or a GitHub URL): run the `dev-loop` skill with the public intent `continue dev loop on $ARGUMENTS`. Resolve that artifact's authoritative state first, then route; ignore board position.
|
|
10
|
+
|
|
11
|
+
- Bare (no `$ARGUMENTS`): pick up the single in-progress board item. Resolve the board's repo and project the same way the queue commands do, then run `node scripts/projects/resolve-active-board-item.mjs --repo <owner/name> --project <number>`.
|
|
12
|
+
- It returns `{ ok: true, target: { kind, number } }` for exactly one in-progress item → run the `dev-loop` skill with the public intent `continue dev loop on #<number>` (the resolved `target.number`), so the intent carries the concrete target instead of re-resolving board state.
|
|
13
|
+
- It returns `{ ok: false, reason }` when there are zero or more than one in-progress items → FAIL CLOSED: print the reason verbatim (it lists the items) and instruct the user to run `/continue #N` explicitly. Do NOT guess.
|
|
14
|
+
|
|
15
|
+
Resolve authoritative state before routing. All paths terminate in the dev-loop skill's `loop startup` → build-envelope → route, stopping at the human-approval checkpoint as usual. No new routing logic here.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Show the read-only dev-loop state summary for an issue or PR."
|
|
3
|
+
argument-hint: "<issue|pr>"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/info.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Resolve the read-only dev-loop state for `$ARGUMENTS` via the `loop info` shortcut — no full dev-loop run. If `$ARGUMENTS` is an issue number, run `npx dev-loops@0.6.0 loop info --issue $ARGUMENTS`; if it is a PR number, run `npx dev-loops@0.6.0 loop info --pr $ARGUMENTS`. Report the strategy, route, linked PR / branch, CI, and next action. Do not start implementation.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Start a time-boxed dev-loop spike from a question (or a pre-authored findings file)."
|
|
3
|
+
argument-hint: "<question> | --file <path>"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/start-spike.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Start a spike — a time-boxed exploration whose deliverable is a findings document. Two forms; both end in `loop startup --spike <path>` and hand the bundle to the `dev-loop` skill. Do NOT pick an internal strategy yourself, and do NOT invent new spike behavior — this wraps the shipped `--spike` intake (see `skills/docs/spike-mode-contract.md`).
|
|
8
|
+
|
|
9
|
+
- Inline question (`$ARGUMENTS` is free text, not `--file ...`): scaffold a startable findings artifact, then enter spike mode.
|
|
10
|
+
- Pick a local spike path (e.g. `docs/spikes/<slug>.md` derived from the question; do not overwrite an existing file).
|
|
11
|
+
- Run `node scripts/refine/scaffold-spike-file.mjs --question "$ARGUMENTS" --out <path> --json`. It writes `## Question` (from the arg) plus stubbed `## Approach`/`## Findings` so the exploration scaffold passes `validateSpikeExplorationSections`; `## Recommendation` is left for the spike to fill in. It returns `{ ok: true, path, question }`.
|
|
12
|
+
- Then run `node scripts/loop/resolve-dev-loop-startup.mjs --spike <path>` and route the resulting bundle through the `dev-loop` skill.
|
|
13
|
+
|
|
14
|
+
- Pre-authored file (`$ARGUMENTS` begins with `--file <path>`): skip scaffolding and run `node scripts/loop/resolve-dev-loop-startup.mjs --spike <path>` directly, then route. The file must already carry the exploration scaffold (Question/Approach/Findings).
|
|
15
|
+
|
|
16
|
+
The spike runs under the relaxed `gates.spike` profile. When the exploration reaches a `## Recommendation`, conclude it with `node scripts/refine/exit-spike.mjs --spike-file <path> --disposition <discard|graduate> [--plan-file <path>]` per the spike-mode contract — discard leaves nothing behind, graduate emits a plan file into the local-planning flow. No new routing logic here.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Start a dev loop on a GitHub issue."
|
|
3
|
+
argument-hint: "<issue>"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/start.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Run the `dev-loop` skill with the public intent `start dev loop on issue $ARGUMENTS`. Resolve authoritative state first, then route through the deterministic internal strategy. Do not pick an internal strategy name yourself.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Report dev-loop readiness (gh auth, git repo, subagent availability)."
|
|
3
|
+
---
|
|
4
|
+
<!-- GENERATED from commands/status.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
5
|
+
|
|
6
|
+
Report dev-loop readiness for this environment. Check, in one pass: `gh` is installed (`command -v gh`), `gh` is authenticated (`gh auth status`), the working directory is inside a git repo (`git rev-parse --is-inside-work-tree`), and the `subagent`/`Agent` capability is available. Summarize which checks passed and the suggested next step. Read-only — do not start implementation.
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
* The dev-loop async path keys off the harness-neutral `DEVLOOPS_RUN_ID` env var to
|
|
6
6
|
* identify an inspectable per-subagent run (runner ownership, async-start enforcement,
|
|
7
7
|
* human-comment gating), and provides a mint-and-propagate path for harnesses (e.g. Claude
|
|
8
|
-
* Code) that inject no native per-subagent run id.
|
|
9
|
-
* dispatching an async subagent.
|
|
8
|
+
* Code) that inject no native per-subagent run id. For those harnesses dev-loops itself mints
|
|
9
|
+
* and sets `DEVLOOPS_RUN_ID` when dispatching an async subagent.
|
|
10
|
+
*
|
|
11
|
+
* Other harnesses may already inject their own run-id var: the Pi runtime injects
|
|
12
|
+
* `PI_SUBAGENT_RUN_ID` (not `DEVLOOPS_RUN_ID`) into each async subagent's child env, so that
|
|
13
|
+
* name is honored as a recognized run-id alias (precedence after the neutral primary). It is
|
|
14
|
+
* an externally-injected Pi-runtime contract var, not a dev-loops-owned var — dev-loops still
|
|
15
|
+
* mints/propagates only the neutral `DEVLOOPS_RUN_ID`.
|
|
10
16
|
*
|
|
11
17
|
* This module is pure except for the explicit file/IO helpers (writeRunContext/readRunContext),
|
|
12
18
|
* which take an injectable `fs` and `root` for testability.
|
|
@@ -18,9 +24,10 @@ import path from "node:path";
|
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Env var names that carry the async-context run id, in resolution precedence order.
|
|
21
|
-
* The neutral `DEVLOOPS_RUN_ID` is the
|
|
27
|
+
* The neutral `DEVLOOPS_RUN_ID` is primary; `PI_SUBAGENT_RUN_ID` is the alias the Pi
|
|
28
|
+
* runtime injects into async-subagent child envs (the only run-id marker present under Pi).
|
|
22
29
|
*/
|
|
23
|
-
export const RUN_ID_MARKERS = Object.freeze(["DEVLOOPS_RUN_ID"]);
|
|
30
|
+
export const RUN_ID_MARKERS = Object.freeze(["DEVLOOPS_RUN_ID", "PI_SUBAGENT_RUN_ID"]);
|
|
24
31
|
|
|
25
32
|
/** Neutral env var name used when minting/propagating a run id. */
|
|
26
33
|
export const NEUTRAL_RUN_ID_VAR = "DEVLOOPS_RUN_ID";
|
|
@@ -25,7 +25,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
|
|
|
25
25
|
|
|
26
26
|
> Under the Claude Code harness the dev-loop runs as a single agent: run these steps directly — no read-only boundary and no separate async-subagent dispatch. See [Main Agent Contract](../docs/main-agent-contract.md).
|
|
27
27
|
|
|
28
|
-
Resolve authoritative state via the startup resolver (`npx dev-loops@0.
|
|
28
|
+
Resolve authoritative state via the startup resolver (`npx dev-loops@0.6.0 loop startup --issue <n>` for issues, `npx dev-loops@0.6.0 loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `npx dev-loops@0.6.0 loop build-envelope --input <resolver-output.json>`. The envelope determines `requiredReads`, `nextAction`, `stopRules`, and `acceptance` — load only those files, execute only that bounded task. It is the first handoff artifact consumed before loading any route pack. See [Workflow Handoff Contract](../docs/workflow-handoff-contract.md) for the derivation contract.
|
|
29
29
|
|
|
30
30
|
**Retrospective checkpoint gate:** the resolver reads `.pi/dev-loop-retrospective-checkpoint.json` and injects the state. When the checkpoint is `missing` and the repo config `workflow.requireRetrospective` (set via `.devloops` at repo root) is `true`, the resolver returns `needs_reconcile`. Complete or explicitly skip the retrospective before starting.
|
|
31
31
|
|
|
@@ -99,10 +99,10 @@ When `@dev-loops/core` is available again, switch back to the full helper. The f
|
|
|
99
99
|
|
|
100
100
|
## Read-only info shortcut
|
|
101
101
|
|
|
102
|
-
Info/handoff requests can be served directly via `npx dev-loops@0.
|
|
103
|
-
- `npx dev-loops@0.
|
|
104
|
-
- `npx dev-loops@0.
|
|
105
|
-
- `npx dev-loops@0.
|
|
102
|
+
Info/handoff requests can be served directly via `npx dev-loops@0.6.0 loop info` (read-only; no full dev-loop run required):
|
|
103
|
+
- `npx dev-loops@0.6.0 loop info --issue <n>` — human-readable issue state summary (strategy, route, linked PR, next action)
|
|
104
|
+
- `npx dev-loops@0.6.0 loop info --pr <n>` — human-readable PR state summary (branch, CI, threads, rounds, action)
|
|
105
|
+
- `npx dev-loops@0.6.0 loop info --issue <n> --json` — machine-readable JSON output
|
|
106
106
|
|
|
107
107
|
## Reading tool output (token-economical convention)
|
|
108
108
|
|
|
@@ -111,7 +111,12 @@ When you need a fact from a dev-loops JSON-emitting script, climb this ladder an
|
|
|
111
111
|
1. **Prefer the dev-loops subcommand / concise mode.** Use `loop info` or a script's `--concise`/`--summary` mode (e.g. `run-watch-cycle.mjs --concise`, `probe-copilot-review.mjs --concise`) for a human-readable digest. The concise modes surface loop state, Copilot round count, unresolved/actionable thread counts, round-cap-clean eligibility, CI status, next action, and the current round's new Copilot comment bodies.
|
|
112
112
|
2. **`--silent` / `-s` for a yes/no check.** Reads ZERO output: `… --jq '<predicate>' --silent; echo $?` exits `0` for true / `1` for false. Without `--jq`, `--silent` maps the script's success (`ok:true`) to exit `0`, failure to `1`. Example: `probe-copilot-review.mjs --repo o/r --pr N --jq '.status=="idle"' -s`.
|
|
113
113
|
3. **`--jq <filter>` to extract a single field.** The `--jq`-wired scripts (`probe-copilot-review.mjs`, `capture-review-threads.mjs`, `upsert-checkpoint-verdict.mjs`, the `scripts/projects/*` queue scripts) accept a gh-style `--jq` filter (jq subset: field access, `.[]`/`.[N]`, `|`, `select(...)`, `==`/`!=`/`<`/`<=`/`>`/`>=`, `length`, `keys`). It prints only the filtered value. An invalid filter fails closed (stderr + exit `2`), distinct from a clean predicate-false (silent exit `1`).
|
|
114
|
-
4.
|
|
114
|
+
4. **Use a dev-loops wrapper for `gh` reads — never an agent-level raw `gh`.** With `workflow.requireRetrospectiveInternalTooling: true`, a raw `gh` call is a recorded retro violation (fail-closed). If no script covers the read you need, treat it as a tooling gap: file/build a thin wrapper (reuse `scripts/lib/jq-output.mjs`, like the three below), don't shell out. The reads that already have wrappers:
|
|
115
|
+
- CI run-log tail (a failing PR's job log) → `scripts/github/fetch-ci-logs.mjs --repo <o/r> --pr <n> [--failed-only] [--tail <n>]`, **never raw `gh run view --log`/`--log-failed`**. (`probe-ci-status.mjs` names the failed checks; this returns the LOG.)
|
|
116
|
+
- Issue list/filter → `scripts/github/list-issues.mjs --repo <o/r> [--state <open|closed|all>] [--label <l>] [--limit <n>]`, **never raw `gh issue list`**. (The queue tool lists the project board; this is for arbitrary issue queries.)
|
|
117
|
+
- Issue comment → `scripts/github/comment-issue.mjs --repo <o/r> --issue <n> (--body <text> | --body-file <path>)`, **never raw `gh issue comment`**. Returns `{ ok, commentUrl }`.
|
|
118
|
+
|
|
119
|
+
All three accept the same `--jq`/`--silent` output flags as the other JSON-emitting scripts.
|
|
115
120
|
5. **NEVER `| python3` or `node -e`** to parse tool JSON. If a field you need is missing from a script's output, add it to the script (or its concise mode), not an inline parser.
|
|
116
121
|
|
|
117
122
|
## Guard rules
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Release runbook
|
|
2
|
+
|
|
3
|
+
Releasing is fully automated from a tag. **Pushing a `v<version>` tag is the only
|
|
4
|
+
manual step.** Everything after the tag is hands-off.
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. On `main` (merged, green), bump the version and add the matching
|
|
9
|
+
`## <version> - <date>` section to `CHANGELOG.md` (the empty `## Unreleased`
|
|
10
|
+
heading stays above the latest version).
|
|
11
|
+
2. Tag the release commit and push the tag:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git tag v<version>
|
|
15
|
+
git push origin v<version>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That is the whole manual flow. Do **not** create the GitHub Release by hand —
|
|
19
|
+
the workflow does it (and is idempotent if you already created one).
|
|
20
|
+
|
|
21
|
+
## What happens automatically
|
|
22
|
+
|
|
23
|
+
- **`.github/workflows/release.yml`** fires on the `v*` tag push. It verifies the
|
|
24
|
+
tagged commit is on `origin/main`, extracts the `## <version>` block from
|
|
25
|
+
`CHANGELOG.md` via `scripts/release/extract-changelog-section.mjs`, and creates
|
|
26
|
+
the GitHub Release (`--latest`, notes = that CHANGELOG section). It is
|
|
27
|
+
**idempotent** (no-op if a Release for the tag already exists) and **fails
|
|
28
|
+
closed** if the version has no CHANGELOG section — an undocumented version never
|
|
29
|
+
gets an empty release.
|
|
30
|
+
- **`.github/workflows/npm-publish.yml`** fires on `release: published` (created by
|
|
31
|
+
the step above) and publishes the packages to npm.
|
|
32
|
+
|
|
33
|
+
## Failure modes
|
|
34
|
+
|
|
35
|
+
- Tag not on `main`: the release workflow fails the on-main guard — re-tag the
|
|
36
|
+
correct commit.
|
|
37
|
+
- Missing CHANGELOG section: the extraction step exits non-zero and no Release is
|
|
38
|
+
created. The workflow checks out the tagged commit, so editing `CHANGELOG.md`
|
|
39
|
+
alone is not enough — commit the `## <version>` section, then move the tag to
|
|
40
|
+
the new commit and re-push:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git tag -f v<version>
|
|
44
|
+
git push --force origin v<version>
|
|
45
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# UI e2e scoping step
|
|
2
|
+
|
|
3
|
+
Canonical owner for **when the shared UI/mobile e2e loop is required**. Inclusion
|
|
4
|
+
is **path-triggered and deterministic** — it does not depend on a human annotating
|
|
5
|
+
the PR or a phase doc.
|
|
6
|
+
|
|
7
|
+
## Trigger: a rendered-artifact change requires UI e2e coverage
|
|
8
|
+
|
|
9
|
+
A PR that **adds or modifies a rendered HTML artifact** MUST run the shared UI e2e
|
|
10
|
+
assertions (mobile + desktop) AND register that artifact in the e2e suite. A
|
|
11
|
+
"rendered artifact" is anything that renders to a served page or component:
|
|
12
|
+
|
|
13
|
+
- a presentation deck — `docs/presentations/*.html` (registered in `DECK_REGISTRY`)
|
|
14
|
+
- an article page — `docs/articles/*.html` (registered in `ARTICLE_REGISTRY`; the
|
|
15
|
+
intro article is the published landing page)
|
|
16
|
+
- the inspect-run viewer's served page/component — `scripts/loop/inspect-run-viewer.mjs`
|
|
17
|
+
|
|
18
|
+
The trigger is the PR's **changed-file set** matched against these explicit globs.
|
|
19
|
+
It is conservative on purpose (issue #976 scope): only artifacts that render to a
|
|
20
|
+
page/component, matched by exact directory + single-segment `*.html` globs (no
|
|
21
|
+
recursion) and the viewer source path.
|
|
22
|
+
|
|
23
|
+
Registration is keyed on the **full repo-relative path**, not the basename, so
|
|
24
|
+
`docs/articles/X.html` and `docs/presentations/X.html` (which share basenames,
|
|
25
|
+
e.g. `introducing-dev-loops.html`) are **distinct** artifacts and can never alias
|
|
26
|
+
onto each other — editing the article never counts as deck coverage and vice versa.
|
|
27
|
+
|
|
28
|
+
### Examples — required
|
|
29
|
+
|
|
30
|
+
- `docs/presentations/introducing-dev-loops.html` edited → UI e2e **required**
|
|
31
|
+
(deck-smoke CI job runs the deck fit specs).
|
|
32
|
+
- `docs/articles/introducing-dev-loops.html` (the landing page) edited → UI e2e
|
|
33
|
+
**required** (article-smoke CI job runs the article fit specs); it is its own
|
|
34
|
+
registration, NOT covered by the same-named deck.
|
|
35
|
+
- A new `docs/articles/new-page.html` added → UI e2e **required**, and because the
|
|
36
|
+
new page is not yet in `ARTICLE_REGISTRY` the gate **fails closed** until it is
|
|
37
|
+
registered (add an entry + a thin spec calling `defineArticleSuite`).
|
|
38
|
+
- `scripts/loop/inspect-run-viewer.mjs` edited → UI e2e **required** (the viewer is
|
|
39
|
+
registered in `VIEWER_REGISTRY`).
|
|
40
|
+
|
|
41
|
+
### Examples — not required
|
|
42
|
+
|
|
43
|
+
- `packages/core/src/loop/copilot-loop-state.mjs`, `README.md`, `*.test.mjs`,
|
|
44
|
+
`docs/articles/foo.md` (not `.html`), or a nested `docs/articles/sub/x.html`
|
|
45
|
+
(the glob is single-segment) → UI e2e **not required**; the gate passes through.
|
|
46
|
+
|
|
47
|
+
## Register the artifact in the suite
|
|
48
|
+
|
|
49
|
+
"Registered" means the artifact appears in the shared registries:
|
|
50
|
+
|
|
51
|
+
- decks → `DECK_REGISTRY` in `test/playwright/harness/deck-fit-harness.mjs`
|
|
52
|
+
(served from `docs/presentations/<deck>`) with a thin spec calling `defineDeckSuite`.
|
|
53
|
+
- articles → `ARTICLE_REGISTRY` in `test/playwright/harness/deck-fit-harness.mjs`
|
|
54
|
+
(served from `docs/articles/<file>`) with a thin spec calling `defineArticleSuite`
|
|
55
|
+
(shared fit/CSP/no-horizontal-scroll assertions, minus the deck's per-section
|
|
56
|
+
named captures).
|
|
57
|
+
- the viewer → `VIEWER_REGISTRY` in
|
|
58
|
+
`test/playwright/harness/inspect-run-viewer-harness.mjs`.
|
|
59
|
+
|
|
60
|
+
The deterministic membership list the gate checks against lives in
|
|
61
|
+
`packages/core/src/loop/ui-e2e-scoping.mjs` (`REGISTERED_ARTIFACT_PATHS`, keyed by
|
|
62
|
+
full repo-relative path; `VIEWER_ARTIFACT_ID`). A sync test keeps
|
|
63
|
+
`REGISTERED_ARTIFACT_PATHS` from drifting from `DECK_REGISTRY` + `ARTICLE_REGISTRY`;
|
|
64
|
+
the single-valued `VIEWER_ARTIFACT_ID` is not import-checked against `VIEWER_REGISTRY`
|
|
65
|
+
(that harness pulls `@playwright/test` into core), so keep the two in sync by hand.
|
|
66
|
+
|
|
67
|
+
## Fail-closed semantics
|
|
68
|
+
|
|
69
|
+
The check is wired as a gate precondition in `evaluatePrGateCoordination`
|
|
70
|
+
(`packages/core/src/loop/pr-gate-coordination.mjs`), at a seam distinct from the
|
|
71
|
+
mergeability (#980) and retrospective (#982) preconditions. It fails closed:
|
|
72
|
+
|
|
73
|
+
- **Unregistered rendered artifact touched** → gate blocks with
|
|
74
|
+
`nextAction: run_ui_e2e_suite`, reason naming the artifact and that it needs
|
|
75
|
+
registration in the suite.
|
|
76
|
+
- **Registered, but UI e2e suite did not pass for this head** (`uiE2ePassed` is
|
|
77
|
+
`false`/unknown) → gate blocks with the same action, reason that the suite must
|
|
78
|
+
run and pass first.
|
|
79
|
+
- **Non-UI change** → `required: false`, gate passes through untouched.
|
|
80
|
+
|
|
81
|
+
The detect layer reads the changed-file set from `gh pr view --json files` and
|
|
82
|
+
derives `uiE2ePassed` from the `statusCheckRollup` UI e2e check(s)
|
|
83
|
+
(`UI_E2E_CHECK_NAMES`): present UI e2e checks must all be `SUCCESS`; if none is
|
|
84
|
+
present the value is unknown and the gate fails closed.
|
|
85
|
+
|
|
86
|
+
Each rendered-artifact family has a stable, satisfiable CI job in
|
|
87
|
+
`.github/workflows/ci.yml`, path/diff-conditioned in the `changes` job and named
|
|
88
|
+
to match `UI_E2E_CHECK_NAMES` so the gate can read a real signal:
|
|
89
|
+
|
|
90
|
+
- `viewer-smoke` → inspect-run viewer spec (triggered by the viewer source set).
|
|
91
|
+
- `deck-smoke` → both presentation deck fit specs (triggered by
|
|
92
|
+
`docs/presentations/**` and the deck specs/harness/configs).
|
|
93
|
+
- `article-smoke` → both article fit specs (triggered by `docs/articles/**` and the
|
|
94
|
+
article specs/harness/configs).
|
|
95
|
+
|
|
96
|
+
So a deck- or article-only PR has a CI check that can actually satisfy the gate;
|
|
97
|
+
without these jobs such a PR would fail closed with no satisfiable signal.
|
|
98
|
+
|
|
99
|
+
## Non-goals
|
|
100
|
+
|
|
101
|
+
Not always-on screenshot testing; not mandatory multi-browser. The criterion is
|
|
102
|
+
only the conservative path-glob + registry-membership + passing-coverage check.
|
|
@@ -508,7 +508,7 @@ After the phase plan passes review:
|
|
|
508
508
|
3. Run local validation:
|
|
509
509
|
- `npm run verify`
|
|
510
510
|
- when a narrower local check is more honest for the touched slice, say so explicitly and run the narrowest justified subset instead of pretending the full verify path was unnecessary
|
|
511
|
-
- for
|
|
511
|
+
- for a *rendered* HTML artifact (a deck `docs/presentations/*.html`, an article `docs/articles/*.html`, or the inspect-run viewer source) UI e2e coverage is **required and auto-scoped** — not opt-in. Touching one obliges you to register it in the shared harness and have a passing UI e2e suite, or the gate fails closed (see [UI e2e scoping step](../docs/ui-e2e-scoping-step.md)); run the matching shared suite locally, e.g. `npm run test:playwright:intro-deck` / `:intro-article` / `:viewer`. For a bespoke local UI surface that is not a registered rendered artifact, the WebKit smoke seam (fixture-backed Playwright WebKit plus screenshot capture) remains available without being mandatory
|
|
512
512
|
- **Commit immediately after validation passes.** Once `npm run verify` (or the narrowest justified validation subset) is green, stage and commit the current slice of work with an atomic commit message. For tracker-backed sessions, also push the commit. In non-interactive subagent sessions, commit authorization is implicit — the subagent was dispatched to implement and must commit before exiting. In interactive sessions, wait for coordination agent authorization. Do not defer commits — uncommitted changes when the session terminates are a workflow defect.
|
|
513
513
|
4. Review the implementation against the merged phase plan.
|
|
514
514
|
5. Run the default pre-approval gate as a full review / fix loop on the branch before calling it review-complete, approval-ready, merge-ready, or ready for final handoff:
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.6.0 - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`/start-spike` — first-class command to start a spike from a question (#988, P2; closes #988).** `/dev-loops:start-spike <question>` (and the Pi `/dev-loops start-spike` subcommand) starts a time-boxed dev-loop spike, a thin wrapper over the already-shipped `--spike` intake (#964/#965/#966). Inline free-text scaffolds a startable findings artifact and `--file <path>` uses a pre-authored one; both then run `resolve-dev-loop-startup.mjs --spike <path>` and hand the bundle to the `dev-loop` skill (gates.spike, exit via `exit-spike.mjs` — discard/graduate per `skills/docs/spike-mode-contract.md`). Because the argument is free text/a path rather than a number, `start-spike` is parsed on a **separate path** from the numeric verbs (start/auto/continue/info), keeping their numeric-validation invariant intact. The one new piece is `scripts/refine/scaffold-spike-file.mjs --question <text> --out <path>`, a pure section builder + file writer that fills `## Question` from the arg and stubs `## Approach`/`## Findings` (leaving `## Recommendation` for the spike) so the result passes `validateSpikeExplorationSections` and is immediately startable (JSON `{ ok, path, question }` + `--jq`/`--silent` via the shared `scripts/lib/jq-output.mjs`). No new spike behavior, gate, or format. `node:test` coverage for the scaffold (startable/in-progress, empty-question fail-closed, CLI write + end-to-end startability), the separate free-text/`--file` parse path (numeric verbs unaffected), and the generated-command presence + no-strategy-leak guard. Also widened the Pi command-palette `description` string's `continue <pr>` to `continue [issue|pr]` for consistency with the P1 surface widening.
|
|
12
|
+
|
|
13
|
+
- **Path-triggered UI e2e auto-scoping gate criterion (#976, P2; replaces opt-in-by-annotation).** A PR that adds or modifies a *rendered* HTML artifact now MUST run the shared UI e2e assertions (mobile + desktop) AND register that artifact in the suite — inclusion is triggered by the changed-file set, never by a human annotating the PR/phase doc. The deterministic core is `packages/core/src/loop/ui-e2e-scoping.mjs` (`evaluateUiE2eScoping`): explicit, conservative path globs (`docs/articles/*.html`, `docs/presentations/*.html`, single-segment, plus the viewer source `scripts/loop/inspect-run-viewer.mjs`) classify changed paths into rendered-artifact descriptors and check each against the registry-membership list keyed by full repo-relative path (`REGISTERED_ARTIFACT_PATHS` mirrors `DECK_REGISTRY` at `docs/presentations/<deck>` + `ARTICLE_REGISTRY` at `docs/articles/<file>`; `VIEWER_ARTIFACT_ID` mirrors `VIEWER_REGISTRY`, kept in sync by hand — the deck/article sync test asserts only that `REGISTERED_ARTIFACT_PATHS` matches `DECK_REGISTRY` + `ARTICLE_REGISTRY`, since the viewer harness pulls `@playwright/test` into core). Full-path keying (not basename) means a deck and an article that share a basename are distinct artifacts. Each artifact family has a stable, path-conditioned CI job (`viewer-smoke`/`deck-smoke`/`article-smoke`) named to match `UI_E2E_CHECK_NAMES`, so a deck- or article-only PR has a satisfiable signal. It is wired as a gate precondition in `evaluatePrGateCoordination` (`packages/core/src/loop/pr-gate-coordination.mjs`), at a seam distinct from the mergeability (#980) and retrospective (#982) preconditions, and **fails closed**: a rendered-artifact change that is unregistered, or registered but whose UI e2e suite has not passed for this head, blocks with `nextAction: run_ui_e2e_suite` and a reason naming the artifact (and that it needs registration / the suite must pass). Non-UI changes pass through (`required: false`). The detect layer reads the changed-file set from `gh pr view --json files` (`extractChangedFiles`) and derives `uiE2ePassed` from the `statusCheckRollup` UI e2e check(s) (`deriveUiE2ePassed` / `UI_E2E_CHECK_NAMES`); a UI e2e check that is absent reads as unknown and fails closed. Standard-step doc: [ui-e2e-scoping-step](skills/docs/ui-e2e-scoping-step.md). `node:test` coverage: helper trigger/negative/fail-closed (unregistered + not-passed) cases + registry sync, gate-level integration (required-pass, non-UI, both fail-closed paths), and detect-helper unit tests.
|
|
14
|
+
|
|
15
|
+
- **`/continue` dual-routing — bare resumes the current in-progress board item (#988, P1).** `/dev-loops:continue` (and the Pi `/dev-loops continue` subcommand) is now dual-routed: with an argument it continues that artifact (the `continue` verb widened from PR-only to `either`, so `123`/`#123`/a GitHub issue-or-PR URL all normalize and the resolver picks the canonical artifact); bare (no argument) it picks up the single in-progress board item. The bare path adds one new thin helper, `scripts/projects/resolve-active-board-item.mjs --repo <owner/name> --project <number|id>`, a pure list→single-target collapse over `list-queue-items.mjs --column "In Progress"` (JSON `{ ok, target: { kind, number } }` + `--jq`/`--silent` via the shared `scripts/lib/jq-output.mjs`): exactly one in-progress item → that target (prefers the linked PR over the issue); **zero or more than one → fails closed** (exit 3) with a reason naming the items and instructing `/continue #N` — it never guesses. No new routing logic: both `/continue` branches hand the resolved target/intent to the existing `dev-loop` skill (`loop startup` → build-envelope → route), still stopping at the human-approval checkpoint. `/dev-loops:dev-loop` remains the catch-all router. `node:test` coverage for the resolver (1/0/multiple), the widened verb + bare/`#`/URL arg normalize, and the no-strategy-leak guard.
|
|
16
|
+
|
|
17
|
+
- Direct dev-loop slash commands as thin wrappers over the public contract (#972): `/dev-loops:start <issue>`, `/dev-loops:auto <issue>`, `/dev-loops:continue [issue|pr]`, `/dev-loops:info <issue|pr>`, and `/dev-loops:status` for the Claude Code plugin (generated under `.claude/commands/` from `commands/*.command.md`), with equivalent `/dev-loops start|auto|continue|info|status` subcommands in the Pi extension (`status` already existed; the start/auto/continue/info entrypoint subcommands are the new parity). `/dev-loops:dev-loop` remains the catch-all router; no new routing logic was added.
|
|
18
|
+
|
|
19
|
+
- **Wrapped the last three agent-level `gh` reads + re-armed the #982 internal-tooling discipline (#993).** Three reads the loop still shelled out for now have thin dev-loops wrappers, following the #981 convention (JSON result + `--jq`/`--silent` via the shared `scripts/lib/jq-output.mjs` helper — `JQ_OUTPUT_PARSE_OPTIONS`/`JQ_OUTPUT_USAGE`/`emitResult`, so the jq-subset filter, fail-closed-on-invalid-filter exit `2`, and exit-code-only `--silent` behavior are reused, not reimplemented): `scripts/github/fetch-ci-logs.mjs --repo --pr [--failed-only] [--tail <n>]` resolves the PR's current head SHA, lists the Actions runs for that commit, and returns each run's log tail (`--log-failed` for a failed run, `--log` otherwise; a per-run log fetch failure records an `<log unavailable: …>` note instead of aborting) — the LOG complement to `probe-ci-status.mjs`, which names the failed checks; `scripts/github/list-issues.mjs --repo [--state open|closed|all] [--label <l>…] [--limit <n>]` returns `{ ok, issues: [{ number, title, state, labels }] }` (state lowercased, labels flattened) for arbitrary issue queries the queue/board tool doesn't cover; `scripts/github/comment-issue.mjs --repo --issue (--body <text> | --body-file <path>)` posts a comment via `gh issue comment` and returns `{ ok, commentUrl }` (sibling to the PR comment/verdict posters). Each is a thin wrapper over `gh` (the script calling gh internally IS the allowed tooling). With these in place, every agent-level raw `gh` read now has a wrapper, so a fully clean internal-tooling record is achievable — the repo-root `.devloops` flips `workflow.requireRetrospectiveInternalTooling` back to `true` (it was set false in #995 pending this), re-arming the hard retro check (#982) on our own retros; the developer-mode rationale comment is restored to the enforce-on posture. The token-economical reading convention in `skills/dev-loop/SKILL.md` (+ generated `.claude/` mirror) names the three wrappers as THE path — never raw `gh run view`/`gh issue list`/`gh issue comment`. `node:test` coverage per wrapper (gh stubbed via the injected `run`): arg parsing/validation, the success path (failing-job log tail for a red PR, filtered issues, comment URL returned), and the shared output flags exercised through `runCli` — `--jq` extraction, invalid-filter fail-closed exit `2`, `--silent` exit-code-only, and gh-failure exit `1`.
|
|
20
|
+
|
|
21
|
+
- **Automated GitHub Release on tag push (#996).** Pushing a `v*` tag now creates the GitHub Release automatically, which fires `npm-publish.yml` — tag → Release → publish is hands-off (closing the gap where v0.5.0 was tagged but never published because no Release was created). New `.github/workflows/release.yml` triggers `on: push: tags: ['v*']`: it mirrors npm-publish's on-main guard (tag commit must be an ancestor of `origin/main`), extracts the release notes from the `## <version>` CHANGELOG.md section, is idempotent (no-op if a Release for the tag already exists), and fails closed if the version has no CHANGELOG section (no empty/auto-generated release for an undocumented version). Notes extraction is factored into `scripts/release/extract-changelog-section.mjs` (`--version <v> [--changelog <path>]`) with `node:test` coverage: present section, fail-closed absent version, the `## Unreleased`-above-latest layout, stop-at-next-`## ` boundary, and version-prefix disambiguation. `npm-publish.yml` is unchanged (still `on: release: published`). Tagging stays the only manual step — see [release-runbook](skills/docs/release-runbook.md).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Reconciled the UI-validation docs to the auto-scoped model and added a worked example (#977, docs-only; UI-e2e epic UE3, follows #975/#976).** The four `docs/ui-*.md` docs no longer describe UI validation as an opt-in-by-annotation convention. `docs/ui-validation-contract.md` now leads with the path-triggered, registry-backed, fail-closed criterion, lists the shared harness assertions (section visibility, CSP-meta lock, 390px mobile fit, wide-element negative control), and carries a worked end-to-end example for the intro deck (`docs/presentations/introducing-dev-loops.html`): `DECK_REGISTRY["intro-deck"]` + thin `defineDeckSuite` spec → `REGISTERED_ARTIFACT_PATHS` membership → `ui_e2e_scoping` gate requirement (fails closed if unregistered) → the satisfiable `deck-smoke` CI signal (`UI_E2E_CHECK_NAMES`). `docs/ui-smoke-harness.md` reframes `webkit-smoke-harness.mjs` as the lower WebKit/config/capture seam the shared deck/article/viewer suites build on and drops the stale "not mandatory CI" claim; `docs/ui-artifact-contract.md` renames CI promotion to auto-scoped CI enforcement; `docs/ui-designer-review-loop.md` notes it is the optional design-review pass, distinct from the required gate. All four now cross-link the canonical owner `skills/docs/ui-e2e-scoping-step.md` and the shared harness. No behavior/harness/gate change. Closes #939 (its slide responsive-fit goal is delivered by the #975 mobile-fit harness).
|
|
26
|
+
|
|
27
|
+
- **Taught the direct subcommand structure across the how-to surfaces (#1001, follows #972).** The intro article (`docs/articles/introducing-dev-loops.{md,html}`) and the intro deck setup slide (`docs/presentations/introducing-dev-loops.html`) now show the direct named commands from #972 (`/dev-loops:start`, `:auto`, `:continue`, `:info`, `:status`), framing `/dev-loops:dev-loop` as the catch-all router for plain-language intent (with the Pi `/dev-loops start|auto|continue|info|status …` parity noted on both surfaces — the article spells out the full set, the deck setup slide flags that Pi drops the colon). The deck slide stays within the 390px mobile-fit harness (#975); section ids unchanged. Conceptual/routing-contract prose (deep-dive article + deck, public-dev-loop-contract, main-agent-contract) was left untouched.
|
|
28
|
+
|
|
29
|
+
- **Per-artifact Playwright deck assertions extracted into a shared harness + registry (#975, generalizes #939).** The two presentation-deck specs (`test/playwright/intro-deck.spec.mjs`, `test/playwright/deep-dive-deck.spec.mjs`) were ~190 lines of near-identical assertions each (same server, same three tests, only the deck path / section ids / mobile-capture id differed). The reusable assertions now live once in `test/playwright/harness/deck-fit-harness.mjs`: the single-file deck server, the `networkidle`+`innerWidth===390`+`document.fonts.ready` layout-settle barrier, the per-element `getBoundingClientRect().right <= innerWidth + 1` mobile-fit check (no overflow-x scroller exemption) with the defensive `scrollingElement.scrollWidth` guard and the per-section vertical-clip check (`clientHeight < scrollHeight` while `overflow-y` is hidden/clip), section-id presence + no-horizontal-scroll, a CSP-`<meta>` guard (`default-src 'none'`), and the guard-the-guard test (a deliberately-wide element MUST fail the fit check). A `DECK_REGISTRY` holds each deck as data (`{ sliceId, deck, sectionIds, mobileCapture }`) and `defineDeckSuite(entry)` generates the full per-deck suite; the two deck specs shrink to thin registrations that resolve the deck path and call `defineDeckSuite`. The inspect-run viewer (an interactive dashboard, not a fit-checked deck) registers via `test/playwright/harness/inspect-run-viewer-harness.mjs` (`startViewer`/`openTab`/`waitForMermaidGraph`/`captureViewerState`/`VIEWER_REGISTRY`), removing the duplicated server-setup + repeated capture blocks from its spec, and now runs the shared `assertSectionIdsAndNoHorizontalScroll` over its registered panel ids. The per-artifact `playwright.*.config.mjs` files and `test:playwright:*` scripts are unchanged (each config's `testMatch` still scopes which registration runs). All existing assertions still pass — `intro-deck` 3/3, `deep-dive-deck` 3/3, `inspect-run-viewer` 7/7 green.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Robust, bounded `<dev-loops-package-root>` resolution for user-level installs (#1009).** The `dev-loop` agent prompt (`agents/dev-loop.agent.md`) and the `dev-loop` skill (`skills/dev-loop/SKILL.md`) previously told the Pi runtime to resolve the package-local CLI by assuming a single fixed `../..` (agent) / `../../..` (skill) package-relative layout. Under a Pi user-level install the agent file is synced to `~/.agents/` and the package itself lives at `~/.pi/agent/npm/node_modules/dev-loops/` (skills are exposed via `package.json` `pi.skills`, loaded from that package location, not copied into `~/.agents/`), so the fixed relative guess missed the package root, the CLI could not be found, and the agent improvised an unbounded `find / -name dev-loop.agent.md` full-filesystem walk that stalled 60s+ and tripped the async needs-attention timeout. The resolution prose now lists an ordered set of **bounded** candidate roots — a best-effort `require.resolve('dev-loops/cli/index.mjs')` probe (cwd-dependent; reliable only when `dev-loops` is on Node's module search path; wrapped in try/catch so a miss exits non-zero silently instead of printing a stack trace that reads as a hard failure), the Pi user-agent npm root (`~/.pi/agent/npm/node_modules/dev-loops`, the reliable path for user-level installs), the legacy package-relative path, and the global npm root — taking the first whose `cli/index.mjs` exists. The prose now explicitly **forbids** `find /` / any unbounded filesystem walk and instructs the agent to stop and ask the orchestrator/operator for the path when every bounded candidate fails. Pi-only prose, stripped from the generated `.claude/` mirrors (which already pin `npx dev-loops@<version>` and rely on Node resolution); `.claude/` regenerated and in sync; cli-invocation contract test green.
|
|
34
|
+
|
|
35
|
+
- **Restored `PI_SUBAGENT_RUN_ID` as a recognized async-context run-id alias — the Pi async-start gate no longer fails closed (#1008, regression from #905).** The Pi runtime injects only `PI_SUBAGENT_RUN_ID` (never the neutral `DEVLOOPS_RUN_ID`) into each async-subagent child env, but #905 dropped that alias from `RUN_ID_MARKERS` as a "deliberate breaking change", leaving the sole marker absent under Pi. With `workflow.asyncStartMode: "required"` this made `validateAsyncStartContext` return `rejected` at startup step 1, so no dev-loop work could start under Pi. `RUN_ID_MARKERS` in `packages/core/src/loop/run-context.mjs` is now `["DEVLOOPS_RUN_ID", "PI_SUBAGENT_RUN_ID"]` — the neutral var stays the primary (and the only var dev-loops mints/propagates), with the Pi-injected name honored as a precedence-after alias. The alias threads through everywhere the primary does (`resolveRunId`/`ensureRunId` loop the markers; `ASYNC_CONTEXT_MARKERS` re-exports them, so the async-start gate recognizes it), and the rejection message names both again ("Set DEVLOOPS_RUN_ID (or the PI_SUBAGENT_RUN_ID alias) to proceed", matching #830). `PI_SUBAGENT_RUN_ID` is an externally-injected Pi-runtime contract var (not a dev-loops-owned `PI_*` name — #905's rename of owned vars to `DEVLOOPS_*` stands), so the harness-agnostic env guard (`test/contracts/cli-harness-agnostic.test.mjs`) allowlists it as a runtime-injected marker confined to the run-context/async-start modules + their tests (and the generated `.claude` mirror). `node:test`: Pi-only-marker recognition under `required` (the regression reproduction), neutral-primary precedence, and the both-markers rejection message.
|
|
36
|
+
|
|
7
37
|
## 0.5.0 - 2026-06-28
|
|
8
38
|
|
|
9
39
|
### Added
|
package/README.md
CHANGED
|
@@ -30,6 +30,21 @@ Use **`dev-loop`** as the single public workflow entrypoint:
|
|
|
30
30
|
|
|
31
31
|
The `dev-loop` entrypoint resolves authoritative state, picks the correct internal strategy, and routes work deterministically. Users never need to choose internal strategy names. See the canonical shorthand example mapping in the [Public Dev Loop Contract](./skills/docs/public-dev-loop-contract.md).
|
|
32
32
|
|
|
33
|
+
### Direct commands
|
|
34
|
+
|
|
35
|
+
The same entrypoints are also available as direct named commands — thin wrappers over the public contract, no separate routing:
|
|
36
|
+
|
|
37
|
+
| Command | Equivalent intent |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `/dev-loops:start <issue>` | start dev loop on issue `<issue>` |
|
|
40
|
+
| `/dev-loops:auto <issue>` | auto dev loop on issue `<issue>` (autonomous until human approval) |
|
|
41
|
+
| `/dev-loops:continue [issue\|pr]` | continue dev loop on `<issue\|pr>`; bare resumes the single in-progress board item |
|
|
42
|
+
| `/dev-loops:start-spike <question>` | start a time-boxed spike from a question (or `--file <path>` for a pre-authored findings file) |
|
|
43
|
+
| `/dev-loops:info <issue|pr>` | read-only state summary (`loop info`) |
|
|
44
|
+
| `/dev-loops:status` | dev-loop readiness (gh auth, git repo, subagent) |
|
|
45
|
+
|
|
46
|
+
`/dev-loops:dev-loop` remains the catch-all router. Inside Pi the same set is reachable as `/dev-loops start|auto|continue|start-spike|info|status …`.
|
|
47
|
+
|
|
33
48
|
## Install from npm
|
|
34
49
|
|
|
35
50
|
### CLI only
|
|
@@ -231,9 +246,13 @@ subagent({
|
|
|
231
246
|
|
|
232
247
|
Do not call internal routed skills such as `local-implementation`, `copilot-pr-followup`, or `final-approval` directly; `dev-loop` selects them from the current GitHub/repo state.
|
|
233
248
|
|
|
234
|
-
The `/dev-loops` command surface
|
|
249
|
+
The `/dev-loops` command surface covers the direct dev-loop entrypoints plus readiness and configuration UX:
|
|
235
250
|
|
|
236
251
|
```bash
|
|
252
|
+
/dev-loops start <issue> # dispatch: start dev loop on issue <issue>
|
|
253
|
+
/dev-loops auto <issue> # dispatch: auto dev loop on issue <issue>
|
|
254
|
+
/dev-loops continue [issue|pr] # dispatch: continue dev loop on <issue|pr>; bare = current in-progress board item
|
|
255
|
+
/dev-loops info <issue|pr> # read-only state summary
|
|
237
256
|
/dev-loops status
|
|
238
257
|
/dev-loops doctor
|
|
239
258
|
/dev-loops gates
|
package/agents/dev-loop.agent.md
CHANGED
|
@@ -26,7 +26,14 @@ The envelope is the primary handoff artifact — it is derived from resolver out
|
|
|
26
26
|
|
|
27
27
|
**Construction sequence:**
|
|
28
28
|
<!-- pi-only -->
|
|
29
|
-
**CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands are invoked as `node <dev-loops-package-root>/cli/index.mjs <verb...>` using the package-local CLI rather than `npx`, so they resolve unambiguously from the installed package without a global install. Resolve `<dev-loops-package-root>`
|
|
29
|
+
**CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands are invoked as `node <dev-loops-package-root>/cli/index.mjs <verb...>` using the package-local CLI rather than `npx`, so they resolve unambiguously from the installed package without a global install. Resolve `<dev-loops-package-root>` via the first of these **bounded** candidates whose `cli/index.mjs` exists — never assume a single fixed layout (this agent may be installed user-level at `~/.agents/`, where the old `../..` package-relative guess resolves to `~`, not the package):
|
|
30
|
+
|
|
31
|
+
1. **Node module resolution** (best-effort first try): `node -e "try{const p=require('node:path');console.log(p.resolve(p.dirname(require.resolve('dev-loops/cli/index.mjs')),'..'))}catch{process.exit(1)}"` — resolves the package root when `dev-loops` is reachable from Node's module search path (notably under `~/.pi/agent/npm`); this is cwd-dependent and commonly misses from a target-repo cwd, so the probe is wrapped in try/catch (no stack trace, exits non-zero on miss) — treat a non-zero exit as "probe missed, try the next candidate", not a hard failure.
|
|
32
|
+
2. **Pi user-agent npm root** (reliable for user-level installs): `~/.pi/agent/npm/node_modules/dev-loops`.
|
|
33
|
+
3. **Package-relative (legacy):** `../..` from this agent's own directory (the original package-local install layout).
|
|
34
|
+
4. **Global npm root:** `$(npm root -g)/dev-loops`.
|
|
35
|
+
|
|
36
|
+
NEVER fall back to `find /` or any unbounded filesystem walk to locate the CLI — it stalls and trips the needs-attention timeout. If every bounded candidate fails, stop and ask the orchestrator/operator for the dev-loops package root rather than searching. (The `dev-loop` skill resolves it analogously.)
|
|
30
37
|
<!-- /pi-only -->
|
|
31
38
|
|
|
32
39
|
1. Run the deterministic startup resolver to produce the authoritative state bundle: `node <dev-loops-package-root>/cli/index.mjs loop startup --issue <n>` for issues, or `node <dev-loops-package-root>/cli/index.mjs loop startup --pr <n>` for PRs.
|
package/cli/index.mjs
CHANGED
|
@@ -58,6 +58,7 @@ const SUBCOMMAND_ROUTES = {
|
|
|
58
58
|
"archive-done": "scripts/projects/archive-done-items.mjs",
|
|
59
59
|
"sync-status": "scripts/projects/sync-item-status.mjs",
|
|
60
60
|
ensure: "scripts/projects/ensure-queue-board.mjs",
|
|
61
|
+
"resolve-active": "scripts/projects/resolve-active-board-item.mjs",
|
|
61
62
|
},
|
|
62
63
|
queue: {
|
|
63
64
|
run: "scripts/loop/run-queue.mjs",
|
|
@@ -68,6 +69,7 @@ const SUBCOMMAND_ROUTES = {
|
|
|
68
69
|
"archive-done": "scripts/projects/archive-done-items.mjs",
|
|
69
70
|
"sync-status": "scripts/projects/sync-item-status.mjs",
|
|
70
71
|
ensure: "scripts/projects/ensure-queue-board.mjs",
|
|
72
|
+
"resolve-active": "scripts/projects/resolve-active-board-item.mjs",
|
|
71
73
|
},
|
|
72
74
|
inspect: {
|
|
73
75
|
run: "scripts/loop/inspect-run.mjs",
|
package/extension/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { createExtensionCoreRuntime } from './checks.ts';
|
|
|
8
8
|
import { createPostMergeUpdateHook } from './post-merge-update.ts';
|
|
9
9
|
import { createPiExtensionAdapter, type ExtensionAPI } from './pi-extension-adapter.ts';
|
|
10
10
|
import {
|
|
11
|
+
buildEntrypointLines,
|
|
11
12
|
buildHelpLines,
|
|
12
13
|
buildInspectLines,
|
|
13
14
|
buildInspectNotification,
|
|
@@ -72,7 +73,7 @@ export default function (pi: ExtensionAPI, runtimeOverrides: ExtensionRuntimeOve
|
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
adapter.registerCommand('dev-loops', {
|
|
75
|
-
description: '
|
|
76
|
+
description: 'Run a dev-loop entrypoint or manage readiness: /dev-loops [start <issue>|auto <issue>|continue [issue|pr]|start-spike <question>|info <issue|pr>|status|doctor|gates|hide|inspect ...]',
|
|
76
77
|
handler: async (args, ctx) => {
|
|
77
78
|
const result = await executeDevLoopsCommand({
|
|
78
79
|
input: args,
|
|
@@ -89,6 +90,14 @@ export default function (pi: ExtensionAPI, runtimeOverrides: ExtensionRuntimeOve
|
|
|
89
90
|
ctx.ui.setWidget(WIDGET_KEY, buildHelpLines(), { placement: 'belowEditor' });
|
|
90
91
|
ctx.ui.notify('dev-loops help', 'info');
|
|
91
92
|
return;
|
|
93
|
+
case 'entrypoint':
|
|
94
|
+
ctx.ui.setWidget(WIDGET_KEY, buildEntrypointLines(result.action, result.intent), { placement: 'belowEditor' });
|
|
95
|
+
ctx.ui.notify(`dev-loops ${result.action}: ${result.intent}`, 'info');
|
|
96
|
+
return;
|
|
97
|
+
case 'start_spike':
|
|
98
|
+
ctx.ui.setWidget(WIDGET_KEY, buildEntrypointLines('start-spike', result.intent), { placement: 'belowEditor' });
|
|
99
|
+
ctx.ui.notify(`dev-loops start-spike: ${result.intent}`, 'info');
|
|
100
|
+
return;
|
|
92
101
|
case 'checks':
|
|
93
102
|
ctx.ui.setWidget(WIDGET_KEY, buildWidgetLines(result.action as Extract<DevLoopsAction, 'doctor' | 'status'>, result.checks), {
|
|
94
103
|
placement: 'belowEditor',
|
|
@@ -35,11 +35,26 @@ export function orderedSetupSteps(checks: DevLoopCheck[]): string[] {
|
|
|
35
35
|
];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function buildEntrypointLines(action: string, intent: string): string[] {
|
|
39
|
+
return [
|
|
40
|
+
`dev-loops ${action}`,
|
|
41
|
+
`Dispatch this public intent through the dev-loop skill:`,
|
|
42
|
+
`/skill:dev-loop ${intent}`,
|
|
43
|
+
'The dev-loop skill resolves authoritative state and routes deterministically — no strategy choice needed.',
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
export function buildHelpLines(): string[] {
|
|
39
48
|
return [
|
|
40
49
|
'dev-loops help',
|
|
41
50
|
'Workflow entry:',
|
|
42
51
|
'- /skill:dev-loop — single public entrypoint; routing handles the rest',
|
|
52
|
+
'Direct entrypoints:',
|
|
53
|
+
'- /dev-loops start <issue>',
|
|
54
|
+
'- /dev-loops auto <issue>',
|
|
55
|
+
'- /dev-loops continue [issue|pr]',
|
|
56
|
+
'- /dev-loops start-spike <question>',
|
|
57
|
+
'- /dev-loops info <issue|pr>',
|
|
43
58
|
'Commands:',
|
|
44
59
|
'- /dev-loops status',
|
|
45
60
|
'- /dev-loops doctor',
|