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.
Files changed (35) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -1
  3. package/.claude/commands/auto.md +7 -0
  4. package/.claude/commands/continue.md +15 -0
  5. package/.claude/commands/info.md +7 -0
  6. package/.claude/commands/start-spike.md +16 -0
  7. package/.claude/commands/start.md +7 -0
  8. package/.claude/commands/status.md +6 -0
  9. package/.claude/hooks/_run-context.mjs +11 -4
  10. package/.claude/skills/dev-loop/SKILL.md +11 -6
  11. package/.claude/skills/docs/release-runbook.md +45 -0
  12. package/.claude/skills/docs/ui-e2e-scoping-step.md +102 -0
  13. package/.claude/skills/local-implementation/SKILL.md +1 -1
  14. package/CHANGELOG.md +30 -0
  15. package/README.md +20 -1
  16. package/agents/dev-loop.agent.md +8 -1
  17. package/cli/index.mjs +2 -0
  18. package/extension/index.ts +10 -1
  19. package/extension/presentation.ts +15 -0
  20. package/lib/dev-loops-core.mjs +141 -0
  21. package/package.json +5 -2
  22. package/scripts/claude/generate-claude-assets.mjs +15 -1
  23. package/scripts/github/comment-issue.mjs +181 -0
  24. package/scripts/github/fetch-ci-logs.mjs +215 -0
  25. package/scripts/github/list-issues.mjs +191 -0
  26. package/scripts/loop/_handoff-contract.mjs +1 -0
  27. package/scripts/loop/detect-pr-gate-coordination-state.mjs +31 -1
  28. package/scripts/loop/run-conductor-cycle.mjs +5 -0
  29. package/scripts/projects/resolve-active-board-item.mjs +193 -0
  30. package/scripts/refine/scaffold-spike-file.mjs +183 -0
  31. package/scripts/release/extract-changelog-section.mjs +111 -0
  32. package/skills/dev-loop/SKILL.md +14 -2
  33. package/skills/docs/release-runbook.md +45 -0
  34. package/skills/docs/ui-e2e-scoping-step.md +102 -0
  35. package/skills/local-implementation/SKILL.md +1 -1
@@ -0,0 +1,111 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { isDirectCliRun } from "../_core-helpers.mjs";
4
+
5
+ const USAGE = `Usage: extract-changelog-section.mjs --version <v> [--changelog <path>]
6
+
7
+ Prints the CHANGELOG.md section for <version> (the block from "## <version>"
8
+ up to the next "## " heading). Exits 1 if no such section exists or the section
9
+ is empty, so a release is never created for an undocumented version. Exits 2 on
10
+ usage/argument errors (missing --version, missing flag value, unreadable file).`;
11
+
12
+ /**
13
+ * Extract the changelog block for a single version.
14
+ *
15
+ * Matches a heading line of the form "## <version>" optionally followed by
16
+ * " - <date>" (or any trailing text), and returns everything up to but not
17
+ * including the next "## " heading. Returns null when no matching section
18
+ * exists (fail closed — caller must not synthesize notes).
19
+ *
20
+ * @param {string} changelog - Full CHANGELOG.md contents.
21
+ * @param {string} version - Version without a leading "v" (e.g. "0.5.0").
22
+ * @returns {string|null} Trimmed section body (without the heading), or null.
23
+ */
24
+ export function extractChangelogSection(changelog, version) {
25
+ const lines = changelog.split("\n");
26
+ // A heading is "## " followed by the exact version token, then either end of
27
+ // line or a non-version-character (space / "-"). This avoids "0.5.0" matching
28
+ // "0.5.0-rc1" or "0.5.01".
29
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ const headingRe = new RegExp(`^##\\s+v?${escaped}(?=\\s|$)`);
31
+
32
+ let start = -1;
33
+ for (let i = 0; i < lines.length; i += 1) {
34
+ if (headingRe.test(lines[i])) {
35
+ start = i;
36
+ break;
37
+ }
38
+ }
39
+
40
+ if (start === -1) {
41
+ return null;
42
+ }
43
+
44
+ const body = [];
45
+ for (let i = start + 1; i < lines.length; i += 1) {
46
+ if (/^##\s/.test(lines[i])) {
47
+ break;
48
+ }
49
+ body.push(lines[i]);
50
+ }
51
+
52
+ return body.join("\n").trim();
53
+ }
54
+
55
+ export async function main(argv = process.argv.slice(2)) {
56
+ if (argv.includes("--help") || argv.includes("-h")) {
57
+ process.stdout.write(`${USAGE}\n`);
58
+ return 0;
59
+ }
60
+
61
+ let version;
62
+ let changelogPath = "CHANGELOG.md";
63
+ for (let i = 0; i < argv.length; i += 1) {
64
+ if (argv[i] === "--version" || argv[i] === "--changelog") {
65
+ const flag = argv[i];
66
+ const value = argv[i + 1];
67
+ if (value === undefined) {
68
+ process.stderr.write(`error: ${flag} requires a value\n\n${USAGE}\n`);
69
+ return 2;
70
+ }
71
+ if (flag === "--version") {
72
+ version = value;
73
+ } else {
74
+ changelogPath = value;
75
+ }
76
+ i += 1;
77
+ }
78
+ }
79
+
80
+ if (!version) {
81
+ process.stderr.write(`error: --version is required\n\n${USAGE}\n`);
82
+ return 2;
83
+ }
84
+
85
+ const normalized = version.replace(/^v/, "");
86
+
87
+ let changelog;
88
+ try {
89
+ changelog = await readFile(changelogPath, "utf8");
90
+ } catch (error) {
91
+ process.stderr.write(`error: cannot read ${changelogPath}: ${error.message}\n`);
92
+ return 2;
93
+ }
94
+
95
+ const section = extractChangelogSection(changelog, normalized);
96
+ if (section === null || section === "") {
97
+ process.stderr.write(
98
+ `error: no section found for version ${normalized} in ${changelogPath}. ` +
99
+ `Refusing to create a release for an undocumented version.\n`,
100
+ );
101
+ return 1;
102
+ }
103
+
104
+ process.stdout.write(`${section}\n`);
105
+ return 0;
106
+ }
107
+
108
+ if (isDirectCliRun(import.meta.url)) {
109
+ const exitCode = await main();
110
+ process.exitCode = exitCode;
111
+ }
@@ -35,7 +35,14 @@ For async-required routes (config `workflow.asyncStartMode`, default `required`)
35
35
  > 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).
36
36
 
37
37
  <!-- pi-only -->
38
- **CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands below 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>` from this skill's own installed path: this skill is installed at `<package-root>/.pi/skills/dev-loop/SKILL.md`, so the package root is `../../..` from this skill's directory. (The `dev-loop` agent resolves it analogously from its own installed path.)
38
+ **CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands below 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 (under a Pi user-level install the package lives at `~/.pi/agent/npm/node_modules/dev-loops/`, so the old `../../..` package-relative guess from `skills/dev-loop/SKILL.md` overshoots the package root):
39
+
40
+ 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.
41
+ 2. **Pi user-agent npm root** (reliable for user-level installs): `~/.pi/agent/npm/node_modules/dev-loops`.
42
+ 3. **Package-relative (legacy):** `../../..` from this skill's own directory (the original package-local install layout).
43
+ 4. **Global npm root:** `$(npm root -g)/dev-loops`.
44
+
45
+ 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` agent resolves it analogously.)
39
46
  <!-- /pi-only -->
40
47
 
41
48
  Resolve authoritative state via the startup resolver (`node <dev-loops-package-root>/cli/index.mjs loop startup --issue <n>` for issues, `node <dev-loops-package-root>/cli/index.mjs loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `node <dev-loops-package-root>/cli/index.mjs 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.
@@ -125,7 +132,12 @@ When you need a fact from a dev-loops JSON-emitting script, climb this ladder an
125
132
  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.
126
133
  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`.
127
134
  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`).
128
- 4. **`gh --jq` on a raw `gh` call** when no dev-loops script covers it.
135
+ 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:
136
+ - 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.)
137
+ - 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.)
138
+ - 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 }`.
139
+
140
+ All three accept the same `--jq`/`--silent` output flags as the other JSON-emitting scripts.
129
141
  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.
130
142
 
131
143
  ## 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.
@@ -512,7 +512,7 @@ After the phase plan passes review:
512
512
  3. Run local validation:
513
513
  - `npm run verify`
514
514
  - 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
515
- - for user-facing HTML/UI/component slices when the user opts in, add a bounded deterministic browser smoke harness (prefer fixture-backed Playwright WebKit plus screenshot capture); use `npm run test:playwright:viewer` when that viewer/browser surface is part of the slice, and wire it into CI once it becomes required validation for that slice
515
+ - 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
516
516
  - **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.
517
517
  4. Review the implementation against the merged phase plan.
518
518
  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: