@dev-loops/core 1.0.2-pre.0 → 1.0.2

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure decision logic for the Claude Code dev-loop hooks (#773).
2
+ * Pure decision logic for the Claude Code dev-loop hooks.
3
3
  *
4
4
  * The hook *scripts* are thin: they read the PreToolUse/PostToolUse stdin payload, gather facts
5
5
  * (git tracked/ignored status, gate-evidence result), and call these pure deciders. Keeping the
@@ -46,7 +46,7 @@ const ALLOW = Object.freeze({ decision: "allow" });
46
46
 
47
47
  /**
48
48
  * Whether the command string also invokes an evidence-writing script (findings-log ledger or
49
- * checkpoint-verdict upsert). Used only to enrich the merge-block message (#1172) — a compound
49
+ * checkpoint-verdict upsert). Used only to enrich the merge-block message — a compound
50
50
  * command combining an evidence write with `gh pr merge` is blocked pre-execution, so the write
51
51
  * never runs; this substring check has no false-negative cost (worst case: the plain message).
52
52
  */
@@ -64,22 +64,15 @@ export const DEV_LOOP_AGENT_TYPE = "dev-loop";
64
64
  /**
65
65
  * Decide whether a PreToolUse Bash command must be blocked by a dev-loop gate boundary.
66
66
  *
67
- * Three gated commands on the target repo:
67
+ * Gated commands on the target repo (each rationale sits inline at its check):
68
68
  * - `gh pr create` — blocked outright; PR creation must flow through the canonical wrapper
69
- * (`scripts/github/create-pr.mjs` / `dev-loops pr create`), which always drafts and
70
- * self-assigns. Closes the hole where raw `gh pr create` opens a ready PR, bypassing draft-first.
71
- * - `gh pr ready` — blocked without clean draft_gate evidence (`pre-pr-ready-gate`).
72
- * - `gh pr merge` — blocked without the full pre-merge gate evidence (`detect-checkpoint-evidence`:
73
- * clean current-head draft_gate + pre_approval_gate). The loop runs this check before merging;
74
- * gating it here closes the hole where a hand-run `gh pr merge` skips the pre-approval gate
75
- * entirely. Everything else passes through.
76
- * - raw `gh issue create` / `gh issue comment` / `gh issue edit` / `gh pr comment` — blocked ONLY when the call
77
- * originates from a SUBAGENT context (`agentType` is a non-null string) and targets the repo.
78
- * Sanctioned external writes flow through node wrappers (gate-verdict comments via
79
- * `upsert-checkpoint-verdict.mjs`, review replies via `reply-resolve*.mjs`, board sync,
80
- * `comment-issue.mjs`), whose Bash command string is `node scripts/…` and never matches these
81
- * raw-`gh` matchers. The MAIN AGENT / operator (agentType null) retains direct `gh issue
82
- * create` — that path is authorized (#1051).
69
+ * (`scripts/github/create-pr.mjs` / `dev-loops pr create`), which always drafts and self-assigns.
70
+ * - `gh pr ready` blocked without clean draft_gate evidence.
71
+ * - `gh pr merge` — blocked without full pre-merge gate evidence (clean current-head draft_gate +
72
+ * pre_approval_gate).
73
+ * - raw `gh issue create` / `gh issue comment` / `gh issue edit` / `gh pr comment` — blocked ONLY
74
+ * from a SUBAGENT context (`agentType` non-null) on the target repo. Sanctioned external writes
75
+ * flow through node wrappers; the MAIN AGENT / operator (agentType null) retains direct access.
83
76
  *
84
77
  * The hook computes `gatePassed`/`gateError` from the gate script appropriate to the command kind.
85
78
  *
@@ -101,10 +94,10 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
101
94
  return ALLOW;
102
95
  }
103
96
  // Normalize (trim + case-fold) so a divergent slug (surrounding whitespace, casing) does not
104
- // silently fail OPEN and disable every guard that depends on inTargetRepo (#1622).
97
+ // silently fail OPEN and disable every guard that depends on inTargetRepo.
105
98
  const inTargetRepo = (repoSlug ?? "").trim().toLowerCase() === TARGET_REPO_SLUG.trim().toLowerCase();
106
99
 
107
- // OPS-NO-INLINE-INTERPRETER (#1622): inline interpreters (`node -e`/`--eval`/`-p`, `python3 -c`,
100
+ // OPS-NO-INLINE-INTERPRETER: inline interpreters (`node -e`/`--eval`/`-p`, `python3 -c`,
108
101
  // heredocs fed to node/python) are barred actor-independently on the target repo — the rule bars
109
102
  // "Coordinator and agent flows"; sanctioned output parsing uses `--jq`/`--silent`, never an
110
103
  // inline interpreter.
@@ -118,10 +111,10 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
118
111
  };
119
112
  }
120
113
 
121
- // SUBISSUE-NO-ADHOC-BYPASS (#1622): ad-hoc `gh api` writes to the target repo's sub-issue endpoints.
114
+ // SUBISSUE-NO-ADHOC-BYPASS: ad-hoc `gh api` writes to the target repo's sub-issue endpoints.
122
115
  // Actor-independent (no reserved direct path). Gated on the target repo: the absolute slug-embedded
123
116
  // form identifies the target repo; the bare relative form (`gh api issues/5/sub_issues`) resolves
124
- // against the cwd repo, so it is in scope only when running in the target repo (mirrors the #1047
117
+ // against the cwd repo, so it is in scope only when running in the target repo (mirrors the
125
118
  // explicit-`--repo`/cwd-target posture).
126
119
  if (inTargetRepo && commandContainsSubIssueAdHocBypass(command)) {
127
120
  return {
@@ -132,7 +125,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
132
125
  };
133
126
  }
134
127
 
135
- // COPILOT-FOLLOWUP-REPLY-RESOLVE-HELPER (#1622): ad-hoc thread-resolution writes — raw `gh api` POST
128
+ // COPILOT-FOLLOWUP-REPLY-RESOLVE-HELPER: ad-hoc thread-resolution writes — raw `gh api` POST
136
129
  // to pulls/<n>/comments/<m>/replies, or a `gh api graphql` resolveReviewThread mutation (the Rest
137
130
  // path names the target repo; the graphql form has no path-host repo, so it is scoped to the cwd
138
131
  // repo). Actor-independent: reply through reply-resolve-review-thread(s).mjs.
@@ -146,7 +139,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
146
139
  };
147
140
  }
148
141
 
149
- // COPILOT-FOLLOWUP-REQUEST-HELPER-ONLY (#1622): ad-hoc Copilot review requests — raw `gh api` writes
142
+ // COPILOT-FOLLOWUP-REQUEST-HELPER-ONLY: ad-hoc Copilot review requests — raw `gh api` writes
150
143
  // to pulls/<n>/requested_reviewers, or a bare `/copilot` / `/copilot re-review` comment summon on the
151
144
  // target repo. Actor-independent: request Copilot via scripts/github/request-copilot-review.mjs.
152
145
  if (inTargetRepo && (commandContainsCopilotRequestBypass(command) || commandContainsCopilotSummonComment(command))) {
@@ -174,7 +167,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
174
167
  }
175
168
  // Subagent-scoped external-write guard: block ad-hoc `gh issue create`/`gh issue comment`/
176
169
  // `gh issue edit`/`gh pr comment` on the target repo from a subagent, so external writes flow through the
177
- // sanctioned node wrappers. The main-agent/operator path (agentType null) is unaffected (#1051).
170
+ // sanctioned node wrappers. The main-agent/operator path (agentType null) is unaffected.
178
171
  if (typeof agentType === "string" && commandContainsRawExternalWrite(command)) {
179
172
  const cwdTargets = (repoSlug ?? "").toLowerCase() === TARGET_REPO_SLUG.toLowerCase();
180
173
  // Scope PER segment, mirroring the `gh pr create` block: in scope when no explicit --repo and
@@ -205,7 +198,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
205
198
  const isMerge = commandContainsGhPrMerge(command);
206
199
  const isCreate = commandContainsGhPrCreate(command);
207
200
 
208
- // STOP-HUMAN-MERGE-001 (#1622): when the repo resolves `autonomy.humanMergeOnly`, `gh pr merge` is
201
+ // STOP-HUMAN-MERGE-001: when the repo resolves `autonomy.humanMergeOnly`, `gh pr merge` is
209
202
  // refused actor-independently — the main agent is the actor that performs GitHub writes, so only an
210
203
  // actor-independent deny enforces the human-merge invariant (an agent-scoped deny would enforce
211
204
  // nothing on the main-agent write path).
@@ -220,7 +213,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
220
213
  }
221
214
 
222
215
  if (!isReady && !isMerge && !isCreate) {
223
- // COPILOT-FOLLOWUP-WAIT-TOOLS (#1622): banned detached/polling wait wrappers. Subagent-only — the
216
+ // COPILOT-FOLLOWUP-WAIT-TOOLS: banned detached/polling wait wrappers. Subagent-only — the
224
217
  // rule is classified `agent` (behavioral guidance for the dev-loop driving agent); the main
225
218
  // agent/operator retains manual wait tooling. The main agent's own sanctioned wait path is still
226
219
  // the deterministic tools.
@@ -244,7 +237,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
244
237
  const cwdTargets = (repoSlug ?? "").toLowerCase() === TARGET_REPO_SLUG.toLowerCase();
245
238
  // Evaluate scope PER create segment, not just the first: a create is in scope when it
246
239
  // explicitly targets the repo, or (with no explicit --repo) the cwd is the repo. An explicit
247
- // `--repo <target>` is denied regardless of cwd (#1047). DENY if ANY create segment is in
240
+ // `--repo <target>` is denied regardless of cwd. DENY if ANY create segment is in
248
241
  // scope — otherwise a leading out-of-scope create (`gh pr create --repo other/repo`) would
249
242
  // short-circuit and shield a later in-scope raw create (`&& gh pr create --fill`).
250
243
  const anyCreateInScope = extractRepoFlagsFromGhPrCreateSegments(command).some((seg) =>
@@ -307,7 +300,7 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
307
300
  // This hook evaluates PreToolUse — BEFORE the Bash tool call runs. A compound command that
308
301
  // writes gate evidence (findings-log ledger, checkpoint verdict) and merges in the same call
309
302
  // is blocked here with the write never having executed, which looks like the evidence
310
- // "vanished" (#1172). Hint the split when the command carries an evidence-writing invocation
303
+ // "vanished". Hint the split when the command carries an evidence-writing invocation
311
304
  // alongside the merge, so the failure is self-explaining instead of looking like data loss.
312
305
  const alsoWritesEvidence = commandContainsEvidenceWrite(command);
313
306
  return {
@@ -464,7 +457,7 @@ export function decideWorktreeCheckoutGuard({
464
457
 
465
458
  /**
466
459
  * Env var that exempts an interactive session awaiting commit authorization from the
467
- * SubagentStop uncommitted-work guard (#1619).
460
+ * SubagentStop uncommitted-work guard.
468
461
  *
469
462
  * An opt-in signal set by the operator or the interactive coordination path
470
463
  * (`DEVLOOPS_COMMIT_AUTH_PENDING=1`) when intentionally holding uncommitted work pending
@@ -477,7 +470,7 @@ export function decideWorktreeCheckoutGuard({
477
470
  export const DEVLOOPS_COMMIT_AUTH_PENDING_VAR = "DEVLOOPS_COMMIT_AUTH_PENDING";
478
471
 
479
472
  /**
480
- * Subagent roles whose contract forbids committing to the repository (#1925).
473
+ * Subagent roles whose contract forbids committing to the repository.
481
474
  *
482
475
  * The `judge` and `review` agents are read-only over the repository: the judge writes only its
483
476
  * own verdict artifact (under `tmp/`, gitignored) and the gate reviewer writes only its findings
@@ -489,7 +482,7 @@ export const DEVLOOPS_COMMIT_AUTH_PENDING_VAR = "DEVLOOPS_COMMIT_AUTH_PENDING";
489
482
  * contract (`agents/judge.agent.md`: "The only thing you write is your own verdict artifact").
490
483
  * The data-loss protection is enforced against the OWNER of the edit (the orchestrator) on its own
491
484
  * stop instead. This is intentionally scoped to read-only roles: editing roles (`developer`,
492
- * `fixer`, `docs`, `quality`) and the orchestrator stay enforced (#1925 non-goal).
485
+ * `fixer`, `docs`, `quality`) and the orchestrator stay enforced.
493
486
  */
494
487
  export const READONLY_SUBAGENT_ROLES = Object.freeze(["judge", "review"]);
495
488
 
@@ -500,7 +493,7 @@ export function isReadOnlySubagentRole(agentType) {
500
493
 
501
494
  /**
502
495
  * Decide whether a SubagentStop must be blocked because the subagent's worktree has
503
- * uncommitted changes (#1619).
496
+ * uncommitted changes.
504
497
  *
505
498
  * `scripts/loop/cleanup-worktree.mjs` runs `git worktree remove --force` after a merge, so
506
499
  * uncommitted changes in a worktree are destroyed with no warning. `LOCAL-COMMIT-BEFORE-EXIT`
@@ -512,10 +505,10 @@ export function isReadOnlySubagentRole(agentType) {
512
505
  * Editing roles (`developer`/`fixer`/`docs`/`quality`) stay fully enforced: an editing
513
506
  * sub-delegate commits its own work before exit (`LOCAL-COMMIT-BEFORE-EXIT`), so a dirty exit is
514
507
  * always a real defect, never a sanctioned "orchestrator owns the commit" split. The removed
515
- * `DEVLOOPS_ORCHESTRATOR_OWNS_COMMIT` env-var exemption (#1786) deadlocked such a role under a
508
+ * `DEVLOOPS_ORCHESTRATOR_OWNS_COMMIT` env-var exemption deadlocked such a role under a
516
509
  * task-scoped no-commit instruction whenever the orchestrator could not set a per-dispatch env
517
510
  * var (the Claude harness): the hook demanded a commit the session then denied, then re-blocked
518
- * the exit (#1936). Disallowing the edit-here/commit-there split at the contract level makes the
511
+ * the exit. Disallowing the edit-here/commit-there split at the contract level makes the
519
512
  * guard the enforcer and the deadlock structurally impossible while preserving data-loss
520
513
  * protection. An orchestrator that wants one consolidated commit performs the edits itself.
521
514
  *
@@ -532,7 +525,7 @@ export function isReadOnlySubagentRole(agentType) {
532
525
  * awaiting commit authorization (exempt) — derived by the hook script from the
533
526
  * `DEVLOOPS_COMMIT_AUTH_PENDING=1` opt-in env signal.
534
527
  * @param {string|null} [params.agentType] - Claude `agent_type` from the SubagentStop payload;
535
- * a read-only role (`judge`/`review`, per `READONLY_SUBAGENT_ROLES`) is exempt (#1925) — its
528
+ * a read-only role (`judge`/`review`, per `READONLY_SUBAGENT_ROLES`) is exempt — its
536
529
  * contract forbids commits, so any dirty tracked edit in its worktree is foreign
537
530
  * (orchestrator-owned) and must not be pinned on it.
538
531
  * @returns {HookDecision}
@@ -547,7 +540,7 @@ export function decideSubagentStopGuard({ cwd, porcelain, pendingCommitAuthoriza
547
540
  if (typeof porcelain !== "string" || porcelain.trim() === "") {
548
541
  return ALLOW;
549
542
  }
550
- // Read-only role exemption (#1925): the worktree is dirty, but a `judge`/`review` subagent's
543
+ // Read-only role exemption: the worktree is dirty, but a `judge`/`review` subagent's
551
544
  // contract forbids commits, so this pending tracked edit is foreign — it belongs to the
552
545
  // orchestrator that dispatched this pass. Do not force a verdict-only role to commit it; allow
553
546
  // the stop with an advisory naming the orchestrator as the actor responsible for the edit. The