dev-loops 0.6.0 → 0.7.1
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 +2 -1
- package/.claude/agents/review.md +2 -1
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
- package/.claude/skills/dev-loop/SKILL.md +15 -11
- package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
- package/.claude/skills/docs/anti-patterns.md +2 -1
- package/.claude/skills/docs/artifact-authority-contract.md +22 -4
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/issue-intake-procedure.md +1 -0
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +17 -1
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +25 -14
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +95 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +3 -2
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +1 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +1 -1
- package/agents/review.agent.md +3 -2
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +9 -10
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +14 -2
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +8 -2
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
- package/scripts/github/verify-fresh-review-context.mjs +150 -31
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/write-gate-context.mjs +248 -61
- package/scripts/github/write-gate-findings-log.mjs +75 -1
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +135 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +36 -7
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +104 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +207 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +441 -0
- package/scripts/projects/_resolve-project.mjs +148 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +116 -65
- package/scripts/projects/move-queue-item.mjs +28 -49
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +41 -47
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +10 -6
- package/skills/docs/acceptance-criteria-verification.md +6 -2
- package/skills/docs/anti-patterns.md +2 -1
- package/skills/docs/artifact-authority-contract.md +22 -4
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/issue-intake-procedure.md +1 -0
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +17 -1
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +1 -1
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +25 -14
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- package/scripts/loop/pre-push-main-guard.mjs +0 -117
|
@@ -19,10 +19,11 @@ The envelope is the primary handoff artifact — it is derived from resolver out
|
|
|
19
19
|
- `nextAction` — the bounded task to execute
|
|
20
20
|
- `stopRules` — stop boundaries that must not be crossed without authorization
|
|
21
21
|
- `acceptance` — self-validation criteria for declaring completion
|
|
22
|
+
- `sanctionedCommands` — the operation → wrapper command map (reads/edits/lifecycle), plus the forbidden and orchestrator-owned lists. Carried by DEFAULT on every build so you never re-derive which wrapper performs a GitHub/loop operation. Do NOT restate the map here — the single source of truth is `scripts/loop/sanctioned-commands.mjs`, surfaced verbatim in the envelope.
|
|
22
23
|
|
|
23
24
|
**Construction sequence:**
|
|
24
25
|
|
|
25
|
-
1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops@0.
|
|
26
|
+
1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops@0.7.1 loop startup --issue <n>` for issues, or `npx dev-loops@0.7.1 loop startup --pr <n>` for PRs.
|
|
26
27
|
2. Pass the resolver output, resolved settings (merged from `.devloops` and `.pi/dev-loop/defaults.yaml`), and current gate state to `buildDevLoopHandoffEnvelope()`.
|
|
27
28
|
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
29
|
4. Read the envelope as the first artifact.
|
package/.claude/agents/review.md
CHANGED
|
@@ -30,7 +30,8 @@ This agent has two modes. The default mode is the full-PR review described in th
|
|
|
30
30
|
|
|
31
31
|
You are in scoped angle-review mode when the invocation supplies a single review `<angle>` plus a gate-context artifact path (`tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json`, written by `scripts/github/write-gate-context.mjs`). Review like an external code reviewer hunting real bugs, not a process auditor: your job is to find concrete defects in the changed code and its call paths, not to remark that "there is no test" or that a doc section is thin. In that mode:
|
|
32
32
|
|
|
33
|
-
- **
|
|
33
|
+
- **No worktree isolation (mandatory):** you review in the PR's actual worktree/head — the same checkout the gate-context preamble ran in — never a fresh isolated worktree. You are read-only and never mutate files, so isolation buys nothing; a fresh worktree would also be checked out from `main` (not the PR head) and would lack the gitignored, worktree-local gate-context bundle below, so you'd silently review the wrong stale tree (#1135).
|
|
34
|
+
- **Fresh-context guard (mandatory):** run `node scripts/github/verify-fresh-review-context.mjs --scope <angle> --context-path <gate-context-artifact-path>` at startup. If it reports `fresh: false` (exit 1) or any error — including a missing gate-context artifact (`gateContextPresent: false`), which is the fail-closed signal that you're not in the right worktree/head — refuse to proceed and report the failure; do not review on inherited context or without seeded context. "Fresh" here means your context is the **neutral builder artifact + your angle**, and explicitly NOT the main agent's conversation/state or a prior reviewer session's state. The injected neutral bundle is the INTENDED seed (allowed); main-agent or cross-session state bleed still fails closed.
|
|
34
35
|
- **Use the PROVIDED neutral bundle as your base; do NOT re-derive it from scratch:** the gate-context artifact carries the diff (`scope.diffPath`) and a deterministic, neutral adjacent-code bundle in `adjacentCode` (each changed file plus its 1-hop import in/out-edges, with a `stripped`/`truncated`/`missing` manifest). Start from this bundle — it is the build-once, work-deduped seed shared verbatim across all reviewers. Only widen (load more files) per-angle when the bundle genuinely lacks something your angle needs; do not re-build the whole diff/adjacent-code graph yourself.
|
|
35
36
|
- **Single-angle scope:** review ONLY the supplied angle's concern. Read the gate-context artifact for the resolved angle set, change scope (branch, head SHA, touched files), acceptance-criteria pointer, and validation posture. Do not review other angles; the fan-in pass consolidates across angles.
|
|
36
37
|
- **Read the FULL diff, not just hunks:** the gate-context artifact records `scope.diffPath` (a repo-relative path to the complete captured diff) and `scope.changedFiles` (full repo-relative paths). Read the entire diff from `scope.diffPath`; if it is `null` or missing, reconstruct it yourself with `git diff` against the change's base (e.g. `git diff origin/main...HEAD` or the branch/head SHA in `scope`). Do not review off a partial or summarized hunk list — you must see every changed line for your angle.
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
description: "Run an autonomous dev loop on a GitHub issue until the human approval checkpoint."
|
|
3
3
|
argument-hint: "<issue>"
|
|
4
4
|
---
|
|
5
|
-
<!-- GENERATED from commands/auto.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
5
|
+
<!-- GENERATED from commands/loop-auto.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
6
|
|
|
7
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/loop-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`): resolve the single continue target from the board. 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>`. It continues the single **In Progress** item; if there is none, it picks the **HEAD of Next Up by position**. It fails closed (idle) when Next Up is empty, and still fails closed on multiple In-Progress items. It never pulls from Backlog.
|
|
12
|
+
- It returns `{ ok: true, target: { kind, number }, source }` (`source` is `"in-progress"` or `"next-up"`) → 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 multiple in-progress items, or when there is nothing in progress **and** Next Up is empty → FAIL CLOSED: print the reason verbatim and instruct the user to run `/loop-continue #N` explicitly (or prioritize a Backlog item into Next Up). Do NOT guess, and never pick from Backlog.
|
|
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,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Add an issue/PR to the auto queue, or quick-capture a freeform idea as a grilled issue and queue it."
|
|
3
|
+
argument-hint: "<issue-or-pr-number | freeform text>"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/loop-enqueue.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Add work to the dev-loop queue. Parse `$ARGUMENTS` before acting. The project is auto-resolved from `.devloops` — never pass `--project` and never invent a project number. `<owner/repo>` resolves from the git remote at invocation, same as the other loop commands.
|
|
8
|
+
|
|
9
|
+
1. **Argument parsing (fail-closed):**
|
|
10
|
+
- If `$ARGUMENTS` is empty, print `Error: missing argument — provide an issue/PR number or freeform text.` and stop without mutating anything.
|
|
11
|
+
- If `$ARGUMENTS` is a single integer token, take the **numeric path**. Otherwise take the **freeform path**.
|
|
12
|
+
|
|
13
|
+
2. **Numeric path (existing issue/PR):**
|
|
14
|
+
- Run `node scripts/projects/add-queue-item.mjs --repo <owner/repo> --item <n>`.
|
|
15
|
+
- The script is idempotent and returns `{ ok, item: { status, alreadyPresent } }`. Report the resulting Status column and whether it was already present, e.g. `#<n> is in "Backlog" (already on the board)` or `#<n> added to "Backlog"`.
|
|
16
|
+
- Exit `3` means the issue/PR was not found (`ITEM_NOT_FOUND` / `CONTENT_NOT_FOUND`): print `Error: issue/PR #<n> not found.` and stop. Do not retry with a guessed project.
|
|
17
|
+
|
|
18
|
+
3. **Freeform path (quick-capture a new idea):** run these steps in order.
|
|
19
|
+
1. **Confirm first — no mutation before approval.** Print a one-line preview of the issue to be created (`About to create issue: "<concise title>"`) and STOP for approval. Do not create the issue, grill, or enqueue anything until the human approves.
|
|
20
|
+
2. **Create the issue.** After approval, create a minimal GitHub issue from the text. The repo has no dedicated create-issue wrapper under `scripts/github/`; the sanctioned create path in this codebase is `gh issue create --repo <owner/repo> --assignee @me --title "<concise title>" --body "<freeform text>"` (the same path used by `skills/docs/issue-intake-procedure.md` and permitted by `skills/docs/main-agent-contract.md`). Title = a concise summary of the text; body = the verbatim freeform text.
|
|
21
|
+
3. **Grill it.** Run `/loop-grill <n> --auto` (delegates to the `loop-grill` skill). The skill writes a `## Grill findings` section back into the issue body and flags any unresolved items there, so they are visible before pickup. Do not re-implement grilling.
|
|
22
|
+
4. **Enqueue.** Run `node scripts/projects/add-queue-item.mjs --repo <owner/repo> --item <n>` to add the grill-annotated issue to the board, then report its Status column as in the numeric path. The `strategy: local-first` default is set repo-wide in `.devloops` (`strategy.default: local-first`), so the new issue is already local-first — this repo has no per-issue local-first label or body tag to apply, and you must not invent one.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Run a structured Socratic Q&A grill session against an issue or plan file before the dev loop starts."
|
|
3
|
+
argument-hint: "<issue-number> | <path/to/plan.md> [--auto]"
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/loop-grill.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Invoke the `loop-grill` skill with `$ARGUMENTS`. Parse the argument list before acting:
|
|
8
|
+
|
|
9
|
+
1. **Argument parsing (fail-closed):**
|
|
10
|
+
- If `$ARGUMENTS` is empty, print `Error: missing argument — provide an issue number or a path to a plan file.` and stop without mutating anything.
|
|
11
|
+
- Split `$ARGUMENTS` into tokens. The first token is the target (`<issue-number>` or `<path/to/plan.md>`). Remaining tokens may include `--auto`. Any other flag produces `Error: unrecognized flag '<flag>' — only --auto is supported.` and stops.
|
|
12
|
+
- If the first token looks like an integer, treat it as a tracker-first issue number. Verify the issue exists via `gh issue view <n> --repo <owner/repo> --json number` (exit non-zero = not found); if not found, print `Error: issue #<n> not found.` and stop.
|
|
13
|
+
- If the first token is a path, resolve it relative to the repo root (or as absolute). If the file does not exist, print `Error: plan file not found: <path>` and stop.
|
|
14
|
+
|
|
15
|
+
2. **Route to the `loop-grill` skill**, passing the resolved target and the `--auto` flag (if present). The skill owns all grilling logic: gap detection, interactive Q&A or auto-answer, write-back, and verdict emission.
|
|
16
|
+
|
|
17
|
+
> **Note:** Interactive mode uses `AskUserQuestion` for bounded-choice gaps and plain text turns for open-ended gaps. `AskUserQuestion` is a Claude Code–native construct; use `--auto` as the portable fallback when running outside Claude Code.
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
description: "Show the read-only dev-loop state summary for an issue or PR."
|
|
3
3
|
argument-hint: "<issue|pr>"
|
|
4
4
|
---
|
|
5
|
-
<!-- GENERATED from commands/info.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
5
|
+
<!-- GENERATED from commands/loop-info.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
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.
|
|
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.7.1 loop info --issue $ARGUMENTS`; if it is a PR number, run `npx dev-loops@0.7.1 loop info --pr $ARGUMENTS`. Report the strategy, route, linked PR / branch, CI, and next action. Do not start implementation.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Show the dev-loop queue board grouped by column."
|
|
3
|
+
argument-hint: ""
|
|
4
|
+
---
|
|
5
|
+
<!-- GENERATED from commands/loop-queue-status.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
|
+
|
|
7
|
+
Show the dev-loop queue board. Takes no arguments; if `$ARGUMENTS` is non-empty, ignore it. `<owner/repo>` resolves from the git remote at invocation, same as the other loop commands, and the project is auto-resolved from `.devloops` — never pass `--project`.
|
|
8
|
+
|
|
9
|
+
1. Run `node scripts/projects/list-queue-items.mjs --repo <owner/repo> --summary`. This is the sanctioned grouped-by-column path: it emits `{ ok, groups: { "<Status>": { count, items } } }` in board column order. Do not re-implement client-side grouping and do not pipe flat output through inline parsers.
|
|
10
|
+
|
|
11
|
+
2. Render the result human-readably (not raw JSON). Walk `groups` in the order they are returned (board column order — Backlog, Next Up, In Progress, Done). For each column, print the column name with its count, then one line per item as `#<number> <title>`. Example:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Backlog (2)
|
|
15
|
+
#1035 Add /loop-enqueue and /loop-queue-status commands
|
|
16
|
+
#1042 Fix article layout
|
|
17
|
+
Next Up (0)
|
|
18
|
+
In Progress (1)
|
|
19
|
+
#1041 Align nav/wrap/prose widths
|
|
20
|
+
Done (3)
|
|
21
|
+
...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. If every column is empty (all counts `0`), print `Queue is empty.` instead of a list.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: "Start a time-boxed dev-loop spike from a question (or a pre-authored findings file)."
|
|
3
3
|
argument-hint: "<question> | --file <path>"
|
|
4
4
|
---
|
|
5
|
-
<!-- GENERATED from commands/start-spike.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
5
|
+
<!-- GENERATED from commands/loop-start-spike.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
6
|
|
|
7
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
8
|
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
description: "Start a dev loop on a GitHub issue."
|
|
3
3
|
argument-hint: "<issue>"
|
|
4
4
|
---
|
|
5
|
-
<!-- GENERATED from commands/start.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
5
|
+
<!-- GENERATED from commands/loop-start.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
6
6
|
|
|
7
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Report dev-loop readiness (gh auth, git repo, subagent availability)."
|
|
3
3
|
---
|
|
4
|
-
<!-- GENERATED from commands/status.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
4
|
+
<!-- GENERATED from commands/loop-status.command.md by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->
|
|
5
5
|
|
|
6
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.
|
|
@@ -15,6 +15,53 @@ export const TARGET_REPO_SLUG = "mfittko/dev-loops";
|
|
|
15
15
|
/** Flags known to take a value argument for `gh pr ready` (not boolean flags). */
|
|
16
16
|
export const FLAGS_THAT_TAKE_VALUE = new Set(["-r", "--repo"]);
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Shell command separators that terminate one segment and begin the next.
|
|
20
|
+
* Newline (`\n`) and carriage return (`\r`) are full command terminators in bash
|
|
21
|
+
* (equivalent to `;`), and the Claude Code Bash tool accepts multi-line command
|
|
22
|
+
* strings — so a segment must break on them too, else `echo hi\ngh pr create` evades
|
|
23
|
+
* the gate. Used by all segment-splitting sites (DRY).
|
|
24
|
+
*/
|
|
25
|
+
const SHELL_SEGMENT_SEPARATOR = /\s*(?:&&|\|\||;|\||\n|\r)\s*/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Strip a single balanced surrounding quote pair (`'…'` or `"…"`) from a shell arg value.
|
|
29
|
+
* A repo flag value may reach us quoted (`--repo 'owner/name'`); the scope check compares against
|
|
30
|
+
* the bare slug, so quotes must be normalized or a quoted on-target repo evades the guard (#1074).
|
|
31
|
+
* ponytail: single balanced pair only — no full shell tokenization (mismatched/partial quotes stay).
|
|
32
|
+
* @param {string|null} value @returns {string|null}
|
|
33
|
+
*/
|
|
34
|
+
function stripSurroundingQuotes(value) {
|
|
35
|
+
if (value == null || value.length < 2) return value;
|
|
36
|
+
const first = value[0];
|
|
37
|
+
if ((first === "'" || first === '"') && value[value.length - 1] === first) {
|
|
38
|
+
return value.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read an inline `GH_REPO=<value>` env-assignment prefix on a single command segment.
|
|
45
|
+
* `gh` resolves its target repo from the `GH_REPO` env var, and a segment may set it inline
|
|
46
|
+
* (`GH_REPO=owner/name gh issue create …`) — same targeting intent as `--repo owner/name`, so the
|
|
47
|
+
* scope check must treat it the same or an off-cwd redirect evades the guard (#1074). Only the
|
|
48
|
+
* FIRST leading env assignment matching `GH_REPO=` is read (env assignments precede the executable);
|
|
49
|
+
* the value is quote-normalized. Ambient `process.env.GH_REPO` is out of scope — this is a static
|
|
50
|
+
* command-string classifier, so only the inline assignment in the string is considered.
|
|
51
|
+
* @param {string} segment @returns {string|null}
|
|
52
|
+
*/
|
|
53
|
+
function extractGhRepoEnvAssignment(segment) {
|
|
54
|
+
if (!segment) return null;
|
|
55
|
+
for (const token of segment.trim().split(/\s+/)) {
|
|
56
|
+
const assign = token.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
57
|
+
if (!assign) break; // first non-assignment token ends the env-assignment prefix
|
|
58
|
+
if (assign[1] === "GH_REPO") {
|
|
59
|
+
return trimToNull(stripSurroundingQuotes(assign[2]));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
18
65
|
/** @param {string|null|undefined} value @returns {string|null} */
|
|
19
66
|
export function trimToNull(value) {
|
|
20
67
|
const trimmed = `${value ?? ""}`.trim();
|
|
@@ -51,7 +98,7 @@ export function normalizeGitHubRepoSlug(remoteUrl) {
|
|
|
51
98
|
return null;
|
|
52
99
|
}
|
|
53
100
|
|
|
54
|
-
function
|
|
101
|
+
function segmentIsGhPrMerge(segment) {
|
|
55
102
|
if (!/^gh\s+pr\s+merge(?:\s|$)/i.test(segment)) {
|
|
56
103
|
return false;
|
|
57
104
|
}
|
|
@@ -82,36 +129,178 @@ export function isMergeCapableCommand(command) {
|
|
|
82
129
|
return false;
|
|
83
130
|
}
|
|
84
131
|
return normalized
|
|
85
|
-
.split(
|
|
86
|
-
.some((segment) =>
|
|
132
|
+
.split(SHELL_SEGMENT_SEPARATOR)
|
|
133
|
+
.some((segment) => segmentIsGhPrMerge(segment) || isGitMergeCompletionCommand(segment));
|
|
87
134
|
}
|
|
88
135
|
|
|
89
136
|
/** @param {string} command @returns {string} */
|
|
90
137
|
export function firstShellSegment(command) {
|
|
91
|
-
return command.trim().split(
|
|
138
|
+
return command.trim().split(SHELL_SEGMENT_SEPARATOR)[0]?.trim() ?? "";
|
|
92
139
|
}
|
|
93
140
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
141
|
+
/** Split a compound shell command into its individual segments. */
|
|
142
|
+
function shellSegments(command) {
|
|
143
|
+
return command.trim().split(SHELL_SEGMENT_SEPARATOR).map((s) => s.trim()).filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Leading prefix a `gh pr <verb>` segment may carry before the `gh` executable:
|
|
148
|
+
* a run of `NAME=value` env assignments, optional `command`/`env`/`exec` wrapper
|
|
149
|
+
* words, and an absolute/relative path on the gh binary (`/usr/bin/gh`).
|
|
150
|
+
*
|
|
151
|
+
* Note: this is a pragmatic normalizer, not a full shell tokenizer. Subshell
|
|
152
|
+
* `(gh pr create)`, `{ …; }` group, `-R=value` short-flag, and backslash-escaped
|
|
153
|
+
* `\gh` forms are deliberately out of scope.
|
|
154
|
+
*/
|
|
155
|
+
const GH_PR_VERB_PREFIX = "(?:[A-Za-z_][A-Za-z0-9_]*=\\S*\\s+)*(?:(?:command|env|exec)\\s+)*(?:\\S*/)?";
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the `gh <subcmd> <verb>` prefix matcher (subcmd = "pr" | "issue").
|
|
159
|
+
* Tolerates a leading env-assignment/wrapper/path prefix so `GH_TOKEN=x gh pr create`,
|
|
160
|
+
* `command gh issue create`, and `/usr/bin/gh pr create` are all matched. The same regex
|
|
161
|
+
* is reused to strip the matched prefix (`segment.replace(re, "")`), so remainder
|
|
162
|
+
* extraction stays consistent across all matcher/extractor call sites. The `gh` prefix
|
|
163
|
+
* requirement means node-wrapper commands (`node scripts/github/comment-issue.mjs …`) never
|
|
164
|
+
* match — their first token is `node`, not `gh`.
|
|
165
|
+
*/
|
|
166
|
+
function ghSubcmdVerbRegex(subcmd, verb) {
|
|
167
|
+
return new RegExp(`^${GH_PR_VERB_PREFIX}gh\\s+${subcmd}\\s+${verb}(?:\\s|$)`, "i");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Build the `gh pr <verb>` prefix matcher — delegates to the generic subcmd matcher (DRY). */
|
|
171
|
+
function ghPrVerbRegex(verb) {
|
|
172
|
+
return ghSubcmdVerbRegex("pr", verb);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Return the first segment in the command that is a `gh <subcmd> <verb>` call (ignoring
|
|
177
|
+
* --help/-h), or null. Scans ALL segments so compound commands are caught.
|
|
178
|
+
*/
|
|
179
|
+
function findGhSubcmdVerbSegment(command, subcmd, verb) {
|
|
180
|
+
const re = ghSubcmdVerbRegex(subcmd, verb);
|
|
181
|
+
for (const segment of shellSegments(command)) {
|
|
182
|
+
if (!re.test(segment)) continue;
|
|
183
|
+
const remainder = segment.replace(re, "").trim();
|
|
184
|
+
if (!remainder) return segment;
|
|
185
|
+
const args = remainder.split(/\s+/).map((a) => a.toLowerCase());
|
|
186
|
+
if (!args.includes("--help") && !args.includes("-h")) return segment;
|
|
99
187
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Extract the `--repo`/`-R` flag value from an already-isolated `gh <subcmd> <verb>` segment.
|
|
193
|
+
* @param {string} segment @param {string} subcmd @param {string} verb @returns {string|null}
|
|
194
|
+
*/
|
|
195
|
+
function extractRepoFlagFromSubcmdSegment(segment, subcmd, verb) {
|
|
196
|
+
const re = ghSubcmdVerbRegex(subcmd, verb);
|
|
197
|
+
if (!segment || !re.test(segment)) return null;
|
|
198
|
+
const remainder = segment.replace(re, "").trim();
|
|
199
|
+
if (!remainder) return null;
|
|
200
|
+
const tokens = remainder.split(/\s+/);
|
|
201
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
202
|
+
const token = tokens[i];
|
|
203
|
+
const lower = token.toLowerCase();
|
|
204
|
+
if (lower === "-r" || lower === "--repo") {
|
|
205
|
+
if (i + 1 < tokens.length && !tokens[i + 1].startsWith("-")) return stripSurroundingQuotes(tokens[i + 1]);
|
|
206
|
+
}
|
|
207
|
+
const repoEqMatch = token.match(/^(?:--repo|-R)=(.+)$/i);
|
|
208
|
+
if (repoEqMatch) return stripSurroundingQuotes(repoEqMatch[1]);
|
|
209
|
+
}
|
|
210
|
+
// No explicit --repo/-R flag: fall back to an inline GH_REPO= env assignment (flag wins,
|
|
211
|
+
// mirroring gh's own precedence). This closes the GH_REPO repo-targeting bypass (#1074).
|
|
212
|
+
return extractGhRepoEnvAssignment(segment);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Return one `{ segment, explicitRepo }` entry for EVERY `gh <subcmd> <verb>` segment (ignoring
|
|
217
|
+
* --help/-h). Mirrors `extractRepoFlagsFromGhPrCreateSegments`.
|
|
218
|
+
* @param {string} command @param {string} subcmd @param {string} verb
|
|
219
|
+
* @returns {{ segment: string, explicitRepo: string|null }[]}
|
|
220
|
+
*/
|
|
221
|
+
function extractRepoFlagsFromGhSubcmdVerbSegments(command, subcmd, verb) {
|
|
222
|
+
const re = ghSubcmdVerbRegex(subcmd, verb);
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const segment of shellSegments(command)) {
|
|
225
|
+
if (!re.test(segment)) continue;
|
|
226
|
+
const remainder = segment.replace(re, "").trim();
|
|
227
|
+
if (remainder) {
|
|
228
|
+
const args = remainder.split(/\s+/).map((a) => a.toLowerCase());
|
|
229
|
+
if (args.includes("--help") || args.includes("-h")) continue;
|
|
230
|
+
}
|
|
231
|
+
out.push({ segment, explicitRepo: extractRepoFlagFromSubcmdSegment(segment, subcmd, verb) });
|
|
103
232
|
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The raw external-write verb forms that must be blocked when originating from a subagent:
|
|
238
|
+
* ad-hoc GitHub issue/PR creation and comments run directly via `gh` (not the sanctioned node
|
|
239
|
+
* wrappers). Each entry is `[subcmd, verb]`.
|
|
240
|
+
*/
|
|
241
|
+
const EXTERNAL_WRITE_VERB_FORMS = Object.freeze([
|
|
242
|
+
["issue", "create"],
|
|
243
|
+
["issue", "comment"],
|
|
244
|
+
["pr", "comment"],
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether `command` contains a raw `gh issue create`, `gh issue comment`, or `gh pr comment`
|
|
249
|
+
* invocation in ANY shell segment (ignoring --help/-h). PreToolUse gate use only — the gate
|
|
250
|
+
* blocks these when they originate from a subagent context. Node-wrapper commands
|
|
251
|
+
* (`node scripts/github/comment-issue.mjs …`) never match (first token is `node`, not `gh`).
|
|
252
|
+
* @param {string} command @returns {boolean}
|
|
253
|
+
*/
|
|
254
|
+
export function commandContainsRawExternalWrite(command) {
|
|
255
|
+
return EXTERNAL_WRITE_VERB_FORMS.some(([subcmd, verb]) => findGhSubcmdVerbSegment(command, subcmd, verb) !== null);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Return `{ segment, explicitRepo }` for every raw external-write segment across all three verb
|
|
260
|
+
* forms (`gh issue create` / `gh issue comment` / `gh pr comment`). PreToolUse gate use only —
|
|
261
|
+
* lets the gate decide in-scope-ness per segment so a leading out-of-scope write can't shield a
|
|
262
|
+
* later in-scope one. `explicitRepo` is the segment's `--repo`/`-R` value or null.
|
|
263
|
+
* @param {string} command @returns {{ segment: string, explicitRepo: string|null }[]}
|
|
264
|
+
*/
|
|
265
|
+
export function extractRepoFlagsFromExternalWriteSegments(command) {
|
|
266
|
+
return EXTERNAL_WRITE_VERB_FORMS.flatMap(([subcmd, verb]) =>
|
|
267
|
+
extractRepoFlagsFromGhSubcmdVerbSegments(command, subcmd, verb),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Return the first segment in the command that is a `gh pr <verb>` call (ignoring --help/-h),
|
|
273
|
+
* or null. Scans ALL segments so compound commands (`echo ok && gh pr merge 1`) are caught.
|
|
274
|
+
*/
|
|
275
|
+
function findGhPrVerbSegment(command, verb) {
|
|
276
|
+
return findGhSubcmdVerbSegment(command, "pr", verb);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generic `gh pr <verb>` detector — checks the FIRST shell segment only.
|
|
281
|
+
*
|
|
282
|
+
* Used by the Pi extension's post-execute handler (`onUserBash`) to record that `gh pr ready`
|
|
283
|
+
* actually ran. First-segment-only is correct for that use: `false && gh pr ready 42` short-
|
|
284
|
+
* circuits so ready never executes, and the extension should not record a spurious invocation.
|
|
285
|
+
*
|
|
286
|
+
* For the Claude Code PreToolUse gate (block before execution), use
|
|
287
|
+
* `commandContainsGhPrReady`/`commandContainsGhPrMerge` instead — those scan ALL segments.
|
|
288
|
+
*/
|
|
289
|
+
function isGhPrVerbCommand(command, verb) {
|
|
290
|
+
const re = ghPrVerbRegex(verb);
|
|
291
|
+
const segment = firstShellSegment(command);
|
|
292
|
+
if (!segment || !re.test(segment)) return false;
|
|
293
|
+
const remainder = segment.replace(re, "").trim();
|
|
294
|
+
if (!remainder) return true;
|
|
104
295
|
const args = remainder.split(/\s+/).map((a) => a.toLowerCase());
|
|
105
296
|
return !args.includes("--help") && !args.includes("-h");
|
|
106
297
|
}
|
|
107
298
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
const remainder = segment.replace(/^gh\s+pr\s+ready(?:\s|$)/i, "").trim();
|
|
299
|
+
/** Extract PR number from a single already-isolated segment (shared by both first- and all-segment paths). */
|
|
300
|
+
function extractPrNumberFromSegment(segment, verb) {
|
|
301
|
+
const re = ghPrVerbRegex(verb);
|
|
302
|
+
if (!segment || !re.test(segment)) return null;
|
|
303
|
+
const remainder = segment.replace(re, "").trim();
|
|
115
304
|
if (!remainder) {
|
|
116
305
|
return null;
|
|
117
306
|
}
|
|
@@ -136,13 +325,11 @@ export function extractPrNumberFromGhPrReady(command) {
|
|
|
136
325
|
return null;
|
|
137
326
|
}
|
|
138
327
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
const remainder = segment.replace(/^gh\s+pr\s+ready(?:\s|$)/i, "").trim();
|
|
328
|
+
/** Extract repo flag from a single already-isolated segment. */
|
|
329
|
+
function extractRepoFlagFromSegment(segment, verb) {
|
|
330
|
+
const re = ghPrVerbRegex(verb);
|
|
331
|
+
if (!segment || !re.test(segment)) return null;
|
|
332
|
+
const remainder = segment.replace(re, "").trim();
|
|
146
333
|
if (!remainder) {
|
|
147
334
|
return null;
|
|
148
335
|
}
|
|
@@ -152,13 +339,130 @@ export function extractRepoFlagFromGhPrReady(command) {
|
|
|
152
339
|
const lower = token.toLowerCase();
|
|
153
340
|
if (lower === "-r" || lower === "--repo") {
|
|
154
341
|
if (i + 1 < tokens.length && !tokens[i + 1].startsWith("-")) {
|
|
155
|
-
return tokens[i + 1];
|
|
342
|
+
return stripSurroundingQuotes(tokens[i + 1]);
|
|
156
343
|
}
|
|
157
344
|
}
|
|
158
345
|
const repoEqMatch = token.match(/^(?:--repo|-R)=(.+)$/i);
|
|
159
346
|
if (repoEqMatch) {
|
|
160
|
-
return repoEqMatch[1];
|
|
347
|
+
return stripSurroundingQuotes(repoEqMatch[1]);
|
|
161
348
|
}
|
|
162
349
|
}
|
|
163
|
-
|
|
350
|
+
// No explicit --repo/-R flag: fall back to an inline GH_REPO= env assignment (flag wins,
|
|
351
|
+
// mirroring gh's own precedence). Applied here too so gh pr ready/merge/create scope checks get
|
|
352
|
+
// consistent GH_REPO handling — the root-cause fix, not just the external-write path (#1074).
|
|
353
|
+
return extractGhRepoEnvAssignment(segment);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** First-segment extractor for `gh pr <verb>` PR number — Pi extension public API. */
|
|
357
|
+
function extractPrNumberFromGhPrVerb(command, verb) {
|
|
358
|
+
return extractPrNumberFromSegment(firstShellSegment(command), verb);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** First-segment extractor for `gh pr <verb>` --repo flag — Pi extension public API. */
|
|
362
|
+
function extractRepoFlagFromGhPrVerb(command, verb) {
|
|
363
|
+
return extractRepoFlagFromSegment(firstShellSegment(command), verb);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** @param {string} command @returns {boolean} */
|
|
367
|
+
export function isGhPrReadyCommand(command) {
|
|
368
|
+
return isGhPrVerbCommand(command, "ready");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** @param {string} command @returns {number|null} */
|
|
372
|
+
export function extractPrNumberFromGhPrReady(command) {
|
|
373
|
+
return extractPrNumberFromGhPrVerb(command, "ready");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** @param {string} command @returns {string|null} */
|
|
377
|
+
export function extractRepoFlagFromGhPrReady(command) {
|
|
378
|
+
return extractRepoFlagFromGhPrVerb(command, "ready");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Whether `command` contains a `gh pr merge` invocation in the FIRST shell segment,
|
|
383
|
+
* ignoring `--help`/`-h`. Used by the Pi extension's post-execute handler.
|
|
384
|
+
* For the Claude Code PreToolUse gate, use `commandContainsGhPrMerge` instead.
|
|
385
|
+
* @param {string} command @returns {boolean}
|
|
386
|
+
*/
|
|
387
|
+
export function isGhPrMergeCommand(command) {
|
|
388
|
+
return isGhPrVerbCommand(command, "merge");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Whether `command` contains a `gh pr ready` invocation in ANY shell segment.
|
|
393
|
+
* For use in the Claude Code PreToolUse gate only — blocks the whole command pre-emptively
|
|
394
|
+
* regardless of shell short-circuit semantics (`false && gh pr ready 42` is still blocked).
|
|
395
|
+
* @param {string} command @returns {boolean}
|
|
396
|
+
*/
|
|
397
|
+
export function commandContainsGhPrReady(command) {
|
|
398
|
+
return findGhPrVerbSegment(command, "ready") !== null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Whether `command` contains a `gh pr merge` invocation in ANY shell segment.
|
|
403
|
+
* For use in the Claude Code PreToolUse gate only — blocks the whole command pre-emptively.
|
|
404
|
+
* @param {string} command @returns {boolean}
|
|
405
|
+
*/
|
|
406
|
+
export function commandContainsGhPrMerge(command) {
|
|
407
|
+
return findGhPrVerbSegment(command, "merge") !== null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Extract PR number from `gh pr ready` in any shell segment — PreToolUse gate use only. */
|
|
411
|
+
export function extractPrNumberFromGhPrReadyAnywhere(command) {
|
|
412
|
+
return extractPrNumberFromSegment(findGhPrVerbSegment(command, "ready"), "ready");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** @param {string} command @returns {string|null} */
|
|
416
|
+
export function extractRepoFlagFromGhPrReadyAnywhere(command) {
|
|
417
|
+
return extractRepoFlagFromSegment(findGhPrVerbSegment(command, "ready"), "ready");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Extract PR number from `gh pr merge` in any shell segment — PreToolUse gate use only. */
|
|
421
|
+
export function extractPrNumberFromGhPrMergeAnywhere(command) {
|
|
422
|
+
return extractPrNumberFromSegment(findGhPrVerbSegment(command, "merge"), "merge");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** @param {string} command @returns {string|null} */
|
|
426
|
+
export function extractRepoFlagFromGhPrMergeAnywhere(command) {
|
|
427
|
+
return extractRepoFlagFromSegment(findGhPrVerbSegment(command, "merge"), "merge");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Whether `command` contains a raw `gh pr create` invocation in ANY shell segment.
|
|
432
|
+
* PreToolUse gate use only — blocks raw `gh pr create` so PR creation flows through the
|
|
433
|
+
* canonical wrapper (`scripts/github/create-pr.mjs` / `dev-loops pr create`), which always
|
|
434
|
+
* creates a draft and self-assigns. The wrapper runs `gh pr create` inside a node child
|
|
435
|
+
* process, so its Bash command string (`node …/create-pr.mjs …`) never matches this — only a
|
|
436
|
+
* literal `gh pr create` in the agent's shell command does.
|
|
437
|
+
* @param {string} command @returns {boolean}
|
|
438
|
+
*/
|
|
439
|
+
export function commandContainsGhPrCreate(command) {
|
|
440
|
+
return findGhPrVerbSegment(command, "create") !== null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Extract repo flag from `gh pr create` in any shell segment — PreToolUse gate use only. */
|
|
444
|
+
export function extractRepoFlagFromGhPrCreateAnywhere(command) {
|
|
445
|
+
return extractRepoFlagFromSegment(findGhPrVerbSegment(command, "create"), "create");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Return one `{ segment, explicitRepo }` entry for EVERY `gh pr create` segment (ignoring
|
|
450
|
+
* --help/-h), not just the first. PreToolUse gate use only: the create-scope decision must
|
|
451
|
+
* consider every create segment, so a leading out-of-scope create can't shield a later
|
|
452
|
+
* in-scope raw create (`gh pr create --repo other/repo && gh pr create --fill`).
|
|
453
|
+
* `explicitRepo` is the segment's `--repo`/`-R` value or null when none is present.
|
|
454
|
+
* @param {string} command @returns {{ segment: string, explicitRepo: string|null }[]}
|
|
455
|
+
*/
|
|
456
|
+
export function extractRepoFlagsFromGhPrCreateSegments(command) {
|
|
457
|
+
return extractRepoFlagsFromGhSubcmdVerbSegments(command, "pr", "create");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** @param {string} command @returns {number|null} */
|
|
461
|
+
export function extractPrNumberFromGhPrMerge(command) {
|
|
462
|
+
return extractPrNumberFromGhPrVerb(command, "merge");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @param {string} command @returns {string|null} */
|
|
466
|
+
export function extractRepoFlagFromGhPrMerge(command) {
|
|
467
|
+
return extractRepoFlagFromGhPrVerb(command, "merge");
|
|
164
468
|
}
|