Skip to content

Fix #479: enforce single-active-stage and predecessor ordering at the claim gate - #489

Merged
richard-devbot merged 4 commits into
mainfrom
fix-479-stage-ordering-single-claim
Jul 30, 2026
Merged

Fix #479: enforce single-active-stage and predecessor ordering at the claim gate#489
richard-devbot merged 4 commits into
mainfrom
fix-479-stage-ordering-single-claim

Conversation

@richard-devbot

Copy link
Copy Markdown
Owner

Summary

Closes #479 (P0, Wave 0 of the #476 autonomy-hardening epic). sdlc_build_next's claim selector picked the next FAIL/BLOCKED/PENDING/READY task without ever checking whether another task was already IN_PROGRESS, and the Definition-of-Ready gate only checked pending decisions, never predecessor-stage completion. The model-free pipeline runner already refused both cases in its own planning layer (planNextAction validates the active task before claiming more; parallel_groups authorization gates concurrent claims) — but a direct sdlc_build_next call bypassed that layer entirely, since the check lived only in the runner. Repeated calls could claim multiple stages concurrently with no authorization, and a later stage could be claimed while an earlier one had never passed — exactly the audit's reproduction steps.

  • pipeline-state.js: new findUnmetPredecessorTask(tasks, candidate) — the first task whose canonical stages are entirely before the candidate's earliest stage and whose status isn't yet passed. Returns null when the candidate has no canonical stage mapping (adopted/custom tasks aren't subject to this ordering) or every true predecessor has passed.
  • parallel-benchmark.js: new isAuthorizedParallelClaim({activeStageIds, candidateStageIds, parallelGroups}) — reuses the existing checkDataIndependence primitive to decide whether one specific active/candidate pair may run concurrently. Distinct from pipeline-run.js's planParallelGroup (which asks "is the ENTIRE fresh frontier one authorized group", used to batch-claim a whole group at once) — this is the narrower question the claim gate needs for a single pair, backed by the same config + independence check either way.
  • rstack-sdlc.ts: sdlc_build_next's claim critical section now:
    • refuses a second claim while another task is IN_PROGRESS unless the pair is authorized parallel work
    • refuses a fresh (never-claimed) PENDING/READY task if a true predecessor hasn't passed — unless that predecessor is itself authorized to run concurrently with the candidate (so an authorized parallel pair is never blocked by its own sibling)
    • exempts retries (FAIL/BLOCKED reclaims) from the predecessor check — they already passed it on their first claim, and the existing sdlc_build_next prefers PENDING tasks over FAIL/BLOCKED — retry policy and guardrail hard-block never engage until the whole plan is consumed #265 claim-order policy deliberately lets a retry jump the queue
    • both refusals return a structured, distinguishable reason (active_stage_exists / dependency_unmet, never a bare "no pending task") and emit claim_refused_active_stage / claim_refused_dependency events

Test plan

  • npx tsx --test tests/parallel-benchmark.test.js — unit coverage for isAuthorizedParallelClaim (disabled config, authorized pair, partial-membership, non-independent group, bundled multi-stage task)
  • npx tsx --test tests/pipeline-state.test.js — unit coverage for findUnmetPredecessorTask (unmet predecessor, satisfied predecessors, later-stage tasks ignored, bundled tasks, no-canonical-mapping exemptions)
  • npx tsx --test tests/stage-ordering-479.test.js — new end-to-end integration test via the real extension: the exact audit repro (second sdlc_build_next call refused, never a second IN_PROGRESS task), validating unblocks the next claim again, predecessor-ordering refusal + resolution, and an authorized parallel_groups pair claiming concurrently without being blocked by its own sibling
  • npm test — 1640/1640
  • npm run typecheck — clean
  • npm run lint — clean (pre-existing unrelated warnings only)
  • npm run validate — clean
  • node scripts/security-audit.mjs — clean (branch includes the Fix Security baseline gate: override brace-expansion in eslint's dependency tree #473 brace-expansion fix so CI is green regardless of merge order)

Scope notes (read before merge)

  • The predecessor check only applies to genuinely fresh PENDING/READY claims — FAIL/BLOCKED retries are exempt, matching the existing, intentional sdlc_build_next prefers PENDING tasks over FAIL/BLOCKED — retry policy and guardrail hard-block never engage until the whole plan is consumed #265 claim-order policy (a retry jumps the queue to engage the attempt budget at the point of failure, rather than deferring to the tail of the plan).
  • Authorization reuses the run's existing parallel_groups config (.rstack/rstack.config.json) and checkDataIndependence — this PR does not introduce a new scheduler-issued/expiring group-token mechanism (that richer model belongs with [P0][Durability] Add immutable attempt ledger, BUILT state, leases, CAS transitions, and transactional outbox #481's transactional foundation, per the epic's own dependency map). It does close the concrete Wave-0 gap: the SAME authorization source the runner already trusts is now consulted by the claim gate itself, so it can't be bypassed by calling sdlc_build_next directly.
  • Dashboard next-action / pipeline run --dry-run parity with this exact claim-gate logic is not addressed here — they already share planNextAction, which independently implements equivalent (but not code-identical) active-task/parallel-group logic. Unifying them onto one function is #479's own "dashboard next-action and pipeline run --dry-run consume the same claim-planning function" acceptance criterion; flagging as a reasonable follow-up rather than folding a runner refactor into this fix.

🤖 Generated with Claude Code

richardsongunde and others added 3 commits July 26, 2026 17:21
…#472)

npm audit reported 5 high-severity advisories, all tracing to one root:
a DoS/ReDoS bug in brace-expansion (GHSA-3jxr-9vmj-r5cp,
GHSA-mh99-v99m-4gvg), reachable via eslint -> minimatch@3.1.5 ->
brace-expansion@1.1.16. npm's only reported fix was eslint@10
(isSemVerMajor), which pulls in a new no-useless-assignment rule
in @eslint/js's recommended config, flagging 15 pre-existing
dead-store patterns across src/ -- too invasive for a security-only
fix.

Instead: add a root `overrides` entry (this repo already uses the
same mechanism for esbuild/protobufjs/ws) forcing brace-expansion to
the patched 5.0.8 line. Verified this reaches eslint's own tree
(minimatch@3.1.5's brace-expansion now 5.0.8) while, as documented,
leaving @earendil-works/pi-coding-agent's shrinkwrapped copy (5.0.6)
untouched -- npm honors a shrinkwrap absolutely, so overrides can't
reach it regardless.

That shrinkwrapped copy is the only remaining high-severity finding,
and it's exactly the node this gate's BUNDLED_ROOTS/
TOLERATED_BUNDLED_ADVISORIES allowlist already exists to tolerate --
added the second GHSA id to that allowlist (the gate is deliberately
strict per-advisory-id, so a newly surfaced GHSA for an
already-tolerated package still fails until consciously added).

Verified: npm run lint unchanged (0 errors, same 4 pre-existing
warnings -- confirms minimatch@3.1.5 works fine with the newer
brace-expansion at runtime), npm run typecheck clean, gate logic
simulated against real npm audit output now reports 0 blocking.
Qodo flagged four issues on the eslint/brace-expansion security fix:
- no test coverage for the TOLERATED_BUNDLED_ADVISORIES allowlist change
- brace-expansion@5.0.8 declares engines.node "20 || >=22", excluding the
  Node 18 CI lane
- the ">=5.0.8" override is unbounded and can float to a future major
- the script comment mis-attributed the version bump to an eslint 9->10
  bump that never happened (the real mechanism is the root override)

Fix: bound the override to an exact "5.0.8" pin; export the pure
tolerate/block decision logic from security-audit.mjs (evaluateAdvisories,
isFullyBundled, advisoryIds) behind a main-guard so it's unit-testable
without shelling out to npm audit, and add tests/security-audit.test.js
covering the tolerate/block/allowlist-miss/severity-threshold paths, plus
a pinned-allowlist regression test; correct the stale comment to document
the real mechanism and the accepted Node 18 engines trade-off (no
brace-expansion release fixes both GHSAs while keeping Node 18 support;
it's a devDependency-only path and no engine-strict is set, so this
doesn't fail CI in practice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… claim gate

sdlc_build_next's claim selector picked the next FAIL/BLOCKED/PENDING/READY
task without ever checking whether another task was already IN_PROGRESS, and
the Definition-of-Ready gate only checked pending decisions, never
predecessor-stage completion. The model-free pipeline runner already refused
both cases in its own planning layer (planNextAction validates the active
task before claiming more; parallel_groups authorization gates concurrent
claims) — but a direct sdlc_build_next call bypassed that layer entirely,
since the check lived only in the runner, never in the claim gate itself.
Repeated calls could claim multiple stages concurrently with no parallel
authorization, and a later stage could be claimed while an earlier one had
never passed.

- pipeline-state.js: new findUnmetPredecessorTask(tasks, candidate) — the
  first task whose canonical stages are entirely before the candidate's
  earliest stage and whose status isn't yet passed. Returns null when the
  candidate has no canonical stage mapping (adopted/custom tasks aren't
  subject to this ordering) or every true predecessor has passed.
- parallel-benchmark.js: new isAuthorizedParallelClaim({activeStageIds,
  candidateStageIds, parallelGroups}) — reuses the existing
  checkDataIndependence primitive to decide whether one specific
  active/candidate PAIR may run concurrently. Distinct from
  pipeline-run.js's planParallelGroup (which asks "is the ENTIRE fresh
  frontier one authorized group", used to batch-claim a whole group at
  once) — this is the narrower question the claim gate needs for a single
  pair, backed by the same config + independence check either way.
- rstack-sdlc.ts: sdlc_build_next's claim critical section now (a) refuses a
  second claim while another task is IN_PROGRESS unless the pair is
  authorized parallel work, and (b) refuses a fresh (never-claimed)
  PENDING/READY task if a true predecessor hasn't passed — UNLESS that
  predecessor is itself authorized to run concurrently with the candidate
  (the same parallel_groups check, so an authorized pair is never blocked by
  its own sibling). Retries (FAIL/BLOCKED reclaims) are exempt from the
  predecessor check — they already passed it on their first claim, and the
  existing #265 claim-order policy deliberately lets a retry jump the queue.
  Both refusals return a structured, distinguishable reason
  (active_stage_exists / dependency_unmet, never a bare "no pending task")
  and emit claim_refused_active_stage / claim_refused_dependency events.

Test coverage: unit tests for both new pure helpers, plus an end-to-end
integration test reproducing the exact audit repro (two sdlc_build_next
calls without validating the first → refusal, not a second IN_PROGRESS
task), the predecessor-ordering case, and the authorized-parallel-pair case.
1640/1640 tests, typecheck clean, lint clean, validate clean, security-audit
clean.

Fixes #479.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Jul 30, 2026

Copy link
Copy Markdown

Strix Security Review

All previously reported security findings have been resolved.

1 resolved finding
Review summary

Reviewed the security-relevant changes in the claim-gate hardening flow, including the new active/candidate parallel authorization helper, exhaustive predecessor detection, the sdlc_build_next critical section, and the dependency-audit gating refactor. The previously reported NEEDS_CONTEXT predecessor-exemption bypass is no longer present: the exemption now applies only when the predecessor is actively IN_PROGRESS, and the claim gate now also evaluates all active tasks and all unmet predecessors before granting a claim. No new exploitable security issues were identified in the changed code.

Updated for 32e5b6b.


Reviewed by Strix
Re-run review · Configure security review settings

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f2c878c-f2aa-40db-9241-efa69251762e

📥 Commits

Reviewing files that changed from the base of the PR and between cce8808 and 32e5b6b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • package.json
  • scripts/security-audit.mjs
  • src/core/harness/parallel-benchmark.js
  • src/core/harness/pipeline-state.js
  • src/integrations/pi/rstack-sdlc.ts
  • tests/parallel-benchmark.test.js
  • tests/pipeline-state.test.js
  • tests/security-audit.test.js
  • tests/stage-ordering-479.test.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Enforce single-active-stage and canonical ordering in sdlc_build_next

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Block unauthorized concurrent sdlc_build_next claims unless parallel_groups explicitly authorizes
 the pair.
• Refuse claiming a canonical stage when any earlier canonical predecessor has not PASSed.
• Pin brace-expansion to 5.0.8 via overrides and unit-test the npm-audit gating logic.
Diagram

graph TD
  A["Caller / API"] --> B["sdlc_build_next"] --> C["Claim gate checks"] --> D["pipeline-state"] --> E["parallel-benchmark"] --> F["tasks.json + events.jsonl"]
  G["CI security baseline"] --> H["security-audit.mjs"] --> I["evaluateAdvisories()"] --> J["package.json overrides"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Enforce ordering only in the model-free runner (planNextAction)
  • ➕ Keeps claim endpoint simpler; runner remains single source of truth for scheduling.
  • ➕ Avoids loading parallel_groups config inside the claim tool.
  • ➖ Does not protect direct sdlc_build_next callers (the audit repro path).
  • ➖ Leaves security gap where repeated claims can bypass scheduling invariants.
2. Centralize scheduling invariants into a shared library used by runner + claim tool
  • ➕ Eliminates duplicated reasoning about parallel_groups and canonical ordering.
  • ➕ Makes future policy changes less error-prone.
  • ➖ More refactor risk/size; harder to land quickly for a P0 hardening item.
  • ➖ Requires careful API design around task selection and persistence semantics.
3. Explicit dependency graph per task (instead of canonical stage ordering)

Recommendation: The PR’s approach is the right immediate fix: enforce single-active-stage and predecessor ordering at the claim gate so it applies to all callers, while reusing the existing data-independence primitive to keep parallel authorization consistent with runner behavior. A follow-up could extract a shared scheduling/policy module to reduce duplication without delaying the security hardening.

Files changed (10) +609 / -62

Enhancement (2) +92 / -36
security-audit.mjsRefactor security audit gate into testable pure logic + broaden allowlist +62/-36

Refactor security audit gate into testable pure logic + broaden allowlist

• Exports bundled roots, severity thresholds, and helper functions; introduces evaluateAdvisories() to separate decision logic from subprocess/exit behavior. Extends tolerated bundled advisories for brace-expansion to include a second GHSA and documents the Node engine trade-off; adds a proper main() guard for direct execution.

scripts/security-audit.mjs

parallel-benchmark.jsAdd pairwise parallel claim authorization helper +30/-0

Add pairwise parallel claim authorization helper

• Introduces isAuthorizedParallelClaim() to decide whether a specific active/candidate stage pair is allowed to run concurrently under parallel_groups. Reuses checkDataIndependence so authorization requires both config membership and data-independence validation.

src/core/harness/parallel-benchmark.js

Bug fix (2) +112 / -2
pipeline-state.jsAdd canonical predecessor detection for claim ordering +30/-0

Add canonical predecessor detection for claim ordering

• Adds findUnmetPredecessorTask() to locate the first not-yet-passed canonical predecessor task relative to a candidate’s earliest canonical stage. Skips non-canonical tasks (no canonical stage mapping) and correctly handles bundled/multi-stage tasks via full stage-id sets.

src/core/harness/pipeline-state.js

rstack-sdlc.tsHarden sdlc_build_next claim gate: single active stage + predecessor ordering +82/-2

Harden sdlc_build_next claim gate: single active stage + predecessor ordering

• Adds claim-time enforcement to refuse a second IN_PROGRESS claim unless the exact active/candidate pair is authorized by parallel_groups + data-independence. Adds a predecessor-completion gate for fresh PENDING/READY tasks (while exempting retries) and emits structured refusal details plus claim_refused_* events to distinguish 'done' from 'refused'.

src/integrations/pi/rstack-sdlc.ts

Tests (4) +388 / -0
parallel-benchmark.test.jsUnit tests for isAuthorizedParallelClaim() +62/-0

Unit tests for isAuthorizedParallelClaim()

• Adds coverage for disabled configs, missing membership, failed data-independence, and bundled/multi-stage active tasks requiring full group inclusion.

tests/parallel-benchmark.test.js

pipeline-state.test.jsUnit tests for canonical predecessor detection +56/-0

Unit tests for canonical predecessor detection

• Adds cases validating first-unmet predecessor selection, no-op when all predecessors passed, bundled multi-stage handling, and correct behavior for non-canonical tasks.

tests/pipeline-state.test.js

security-audit.test.jsUnit tests for npm audit gate decision logic +112/-0

Unit tests for npm audit gate decision logic

• Introduces synthetic npm audit JSON-shape tests for evaluateAdvisories(), isFullyBundled(), and advisoryIds(), ensuring allowlist behavior is strict per GHSA id and per install path. Pins the expected brace-expansion allowlist ids to prevent silent drift.

tests/security-audit.test.js

stage-ordering-479.test.jsIntegration regression tests for sdlc_build_next stage ordering and parallel claims +158/-0

Integration regression tests for sdlc_build_next stage ordering and parallel claims

• Adds end-to-end tests reproducing the audit bypass: repeated sdlc_build_next calls are refused when an active task exists, dependency ordering blocks later stages when predecessors are not PASS, and authorized parallel_groups pairs can be concurrently claimed. Verifies emitted refusal events and task state invariants.

tests/stage-ordering-479.test.js

Other (2) +17 / -24
package-lock.jsonRefresh lockfile for brace-expansion override and dependency metadata +15/-23

Refresh lockfile for brace-expansion override and dependency metadata

• Updates the resolved brace-expansion version to 5.0.8 (and balanced-match to 4.x) and removes now-unneeded transitive entries (e.g., concat-map). Also includes minor metadata normalization (e.g., peer flags / bin path formatting).

package-lock.json

package.jsonPin brace-expansion to 5.0.8 via npm overrides +2/-1

Pin brace-expansion to 5.0.8 via npm overrides

• Adds a root-level overrides entry to force brace-expansion to the patched 5.0.8 line, alongside existing overrides for esbuild/protobufjs/ws.

package.json

@qodo-code-review

qodo-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (4)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Action required

1. Predecessor check incomplete ✓ Resolved 🐞 Bug ≡ Correctness
Description
sdlc_build_next calls findUnmetPredecessorTask once and may exempt that single returned predecessor
via parallel_groups authorization, but it never evaluates other unmet canonical predecessors. This
can allow claiming a fresh task while a different earlier-stage predecessor remains not-passed and
not authorized to run concurrently.
Code

src/integrations/pi/rstack-sdlc.ts[R2281-2294]

+        if (task.status === "PENDING" || task.status === "READY") {
+          const unmetPredecessor = findUnmetPredecessorTask(taskState.tasks, task);
+          const predecessorExempt = unmetPredecessor && isAuthorizedParallelClaim({
+            activeStageIds: taskStageIds(unmetPredecessor),
+            candidateStageIds: taskStageIds(task),
+            parallelGroups,
+          });
+          if (unmetPredecessor && !predecessorExempt) {
+            return {
+              taskState, task: null,
+              missing: [] as string[], requiredApprovals: [] as string[], readiness: null as any, guardrailCheck: null as any, approvalAuditEvents: [] as any[],
+              dependencyBlock: { candidate_task_id: task.id, predecessor_task_id: unmetPredecessor.id, predecessor_status: unmetPredecessor.status },
+            };
+          }
Relevance

●●● Strong

Predecessor-ordering correctness gap undermines claim-gate guarantees; likely to be fixed to match
intended enforcement.

PR-#152
PR-#230

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The caller logic checks exactly one predecessor returned by findUnmetPredecessorTask, but the
helper returns only a single task and stops scanning, so additional unmet predecessors are never
checked for authorization/exemption.

src/integrations/pi/rstack-sdlc.ts[2271-2295]
src/core/harness/pipeline-state.js[116-144]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The predecessor-ordering gate in `sdlc_build_next` evaluates only one unmet predecessor (the first returned by `findUnmetPredecessorTask`) and may exempt it if parallel-authorized. If there are multiple unmet canonical predecessors, this can incorrectly allow a claim when at least one predecessor is still unmet and **not** parallel-authorized.

### Issue Context
- `findUnmetPredecessorTask(...)` returns the first unmet predecessor it encounters and stops; it has no awareness of parallel authorization and does not return the full set of unmet predecessors.
- The caller logic (`predecessorExempt`) only considers that one task.

### Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[2271-2295]
- src/core/harness/pipeline-state.js[116-144]

### Suggested change
Implement the gate as: “block if **any** canonical predecessor is unmet and not parallel-authorized”, for example by:
1) Adding a helper that returns **all** unmet predecessor tasks (or a helper that returns the first unmet predecessor that fails an injected `isExempt(predecessor)` predicate), and
2) In `sdlc_build_next`, iterate predecessors and stop at the first one that is unmet and not exempt.

Concretely:
- Replace the single `unmetPredecessor` lookup with a loop over all tasks earlier than the candidate’s earliest canonical stage.
- For each unmet predecessor, compute `isAuthorizedParallelClaim(predecessor, candidate)`; only skip it if authorized.
- If any unmet predecessor is not authorized, return `dependencyBlock` for that task.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Only one active checked ✓ Resolved 🐞 Bug ≡ Correctness
Description
In sdlc_build_next, the active-stage gate uses Array.find to select only one IN_PROGRESS task and
authorizes the claim against that single task. If multiple tasks are already active (which can be
created by overlapping parallel_groups like [A,B] and [A,C]), a candidate can be authorized vs the
first active task while still being unauthorized vs another active task, allowing an unauthorized
concurrent set.
Code

src/integrations/pi/rstack-sdlc.ts[R2250-2268]

+        const activeTask = taskState.tasks.find((t: any) => t.status === "IN_PROGRESS" && t.id !== task.id);
+        // Loaded once and reused by both #479 checks below, so an authorized
+        // pair reads the SAME config/independence verdict for both "is a
+        // second active stage allowed" and "is this predecessor exempt".
+        const parallelGroups = (activeTask || task.status === "PENDING" || task.status === "READY")
+          ? await loadProjectParallelGroups(projectRoot)
+          : null;
+        if (activeTask) {
+          const authorized = isAuthorizedParallelClaim({
+            activeStageIds: taskStageIds(activeTask),
+            candidateStageIds: taskStageIds(task),
+            parallelGroups,
+          });
+          if (!authorized) {
+            return {
+              taskState, task: null,
+              missing: [] as string[], requiredApprovals: [] as string[], readiness: null as any, guardrailCheck: null as any, approvalAuditEvents: [] as any[],
+              activeStageBlock: { active_task_id: activeTask.id, candidate_task_id: task.id },
+            };
Relevance

●●● Strong

Correctness issue in claim-gate aligns with PR goal (#479 hardening); team usually accepts such gate
fixes.

PR-#152
PR-#230

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The claim gate selects only one active task with .find() and checks authorization only for that
one pair; meanwhile parallel group config loading is tolerant and does not prevent overlapping
groups, and the authorization helper itself is explicitly pairwise.

src/integrations/pi/rstack-sdlc.ts[2238-2269]
src/commands/pipeline-run.js[51-63]
src/core/harness/parallel-benchmark.js[114-128]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`sdlc_build_next` enforces the “single active stage unless parallel_groups authorizes concurrency” rule by checking only a single `IN_PROGRESS` task (via `.find`). When more than one task is already `IN_PROGRESS`, the candidate is not validated against the full active set, which can permit unauthorized concurrent execution (notably when `parallel_groups.groups` overlap).

### Issue Context
- `isAuthorizedParallelClaim` is a pairwise predicate (one active/candidate pair), so the claim gate must apply it to **every** currently-active task.
- `loadProjectParallelGroups` does not enforce disjoint groups, so overlapping groups are a plausible configuration.

### Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[2238-2270]
- src/commands/pipeline-run.js[51-63]
- src/core/harness/parallel-benchmark.js[114-128]

### Suggested change
- Replace `const activeTask = ...find(...)` with `const activeTasks = ...filter(...)`.
- If `activeTasks.length > 0`, require that **for each** active task `t`, `isAuthorizedParallelClaim({ activeStageIds: taskStageIds(t), candidateStageIds: taskStageIds(task), parallelGroups })` is true.
- If any active task is not authorized with the candidate, refuse the claim (optionally include `active_task_ids: [...]` in the structured refusal details/event).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. New any types in claim gate 📜 Skill insight ✧ Quality
Description
The new claim-gate logic introduces any annotations and as any casts for task objects, weakening
type safety in a critical claim-ordering/concurrency control path and increasing the risk of runtime
errors. This violates the compliance requirement to avoid any in TypeScript MCP server code and
use explicit types instead.
Code

src/integrations/pi/rstack-sdlc.ts[R2250-2268]

+        const activeTask = taskState.tasks.find((t: any) => t.status === "IN_PROGRESS" && t.id !== task.id);
+        // Loaded once and reused by both #479 checks below, so an authorized
+        // pair reads the SAME config/independence verdict for both "is a
+        // second active stage allowed" and "is this predecessor exempt".
+        const parallelGroups = (activeTask || task.status === "PENDING" || task.status === "READY")
+          ? await loadProjectParallelGroups(projectRoot)
+          : null;
+        if (activeTask) {
+          const authorized = isAuthorizedParallelClaim({
+            activeStageIds: taskStageIds(activeTask),
+            candidateStageIds: taskStageIds(task),
+            parallelGroups,
+          });
+          if (!authorized) {
+            return {
+              taskState, task: null,
+              missing: [] as string[], requiredApprovals: [] as string[], readiness: null as any, guardrailCheck: null as any, approvalAuditEvents: [] as any[],
+              activeStageBlock: { active_task_id: activeTask.id, candidate_task_id: task.id },
+            };
Relevance

●● Moderate

Type-safety improvement is plausible, but repo frequently uses any; no clear accept/reject
precedent for removing it here.

PR-#152

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance IDs 1399957 and 1400551 both forbid using any in TypeScript (including MCP server
code). The cited claim-gate changes in src/integrations/pi/rstack-sdlc.ts show an explicitly
any-typed callback parameter (e.g., taskState.tasks.find((t: any) => ...)) and multiple as any
casts in newly added return objects, demonstrating that the new logic relies on any rather than
explicit, type-checked task/state shapes.

src/integrations/pi/rstack-sdlc.ts[2250-2268]
src/integrations/pi/rstack-sdlc.ts[2250-2256]
Skill: code-patterns
Skill: mcp-builder

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New claim-gate code in `src/integrations/pi/rstack-sdlc.ts` introduces TypeScript `any` (e.g., `find((t: any) => ...)` and `as any` casts) for task objects within the `sdlc_build_next` claim gate logic, violating compliance rules that require avoiding `any` in MCP server TypeScript code.

## Issue Context
Compliance explicitly forbids the use of `any` in TypeScript (PR Compliance IDs 1399957 and 1400551). This code path is a critical gate for claim ordering / concurrency enforcement and relies on task fields such as `id` and `status`; using `any` removes compile-time verification that these fields exist and match expected types, increasing the chance of runtime errors.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[2250-2268]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. advisoryIds not verb-noun 📜 Skill insight ⚙ Maintainability
Description
The function name advisoryIds does not follow the required verb-noun naming convention, making
intent less explicit (e.g., extract/get vs. a noun phrase). Renaming improves consistency and
discoverability across the codebase.
Code

scripts/security-audit.mjs[R48-52]

+export function advisoryIds(info) {
  return (info.via ?? [])
    .filter((entry) => typeof entry === 'object' && entry?.url)
    .map((entry) => String(entry.url).split('/').pop());
}
Relevance

●● Moderate

Naming-convention rename is subjective; no close precedent in scripts, though security-audit changes
are commonly accepted.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires functions to follow a verb-noun naming pattern. The modified/exported
function is named advisoryIds, which does not start with a verb describing the action performed
(it extracts IDs).

scripts/security-audit.mjs[48-52]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scripts/security-audit.mjs` exports `advisoryIds`, which is a noun phrase rather than a verb-noun action name.

## Issue Context
Compliance requires JS/TS function names to start with a verb (e.g., `get`, `extract`, `build`, `calculate`). This function extracts GHSA IDs from audit entries, so a name like `extractAdvisoryIds` or `getAdvisoryIds` would comply.

## Fix Focus Areas
- scripts/security-audit.mjs[48-52]
- scripts/security-audit.mjs[87-96] (update call sites)
- tests/security-audit.test.js[15-25] (update imports/usage)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. security-audit.mjs uses console.log 📜 Skill insight ✧ Quality
Description
The modified security audit script writes output using console.log, which is disallowed by the
no-debug-logging compliance rule. This can lead to accidental debug output persisting in committed
source files.
Code

scripts/security-audit.mjs[R108-111]

+  if (tolerated.length) {
+    console.log('Tolerated advisories (vendored/bundled, unpatchable downstream — track upstream):');
+    for (const t of tolerated) console.log(`  - ${t.name} (${t.severity}) [${t.ids.join(', ')}]`);
+  }
Relevance

●● Moderate

Console output in scripts appears tolerated historically; no specific precedent enforcing a
no-console rule here.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400056 requires removing console.log/debug logging statements from changed
source files. The updated main() path prints tolerated advisories using console.log(...).

scripts/security-audit.mjs[108-111]
Skill: testing-qa

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Changed code in `scripts/security-audit.mjs` uses `console.log(...)`, which violates the "no debug logging statements" rule.

## Issue Context
This file is a CI/security gate script; it can still emit user-facing output, but it should do so via the project's logging abstraction (or another approved mechanism) rather than direct `console.log`.

## Fix Focus Areas
- scripts/security-audit.mjs[108-111]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Duplicated refusal return objects ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The claim-gate refusal paths duplicate the same large return-object shape (`taskState, task: null,
missing: [], requiredApprovals: [], readiness: null, guardrailCheck: null, approvalAuditEvents: []`)
in multiple places, increasing the chance of drift when fields change. This is a DRY violation that
should be refactored into a shared helper/constant.
Code

src/integrations/pi/rstack-sdlc.ts[R2263-2294]

+          if (!authorized) {
+            return {
+              taskState, task: null,
+              missing: [] as string[], requiredApprovals: [] as string[], readiness: null as any, guardrailCheck: null as any, approvalAuditEvents: [] as any[],
+              activeStageBlock: { active_task_id: activeTask.id, candidate_task_id: task.id },
+            };
+          }
+        }
+        // #479: a fresh (never-claimed) task cannot start ahead of its
+        // canonical predecessors. Retries (FAIL/BLOCKED reclaims) are exempt
+        // — they already passed this gate on their first claim, and the
+        // existing claim-order policy (#265) deliberately lets a retry jump
+        // the queue to engage the attempt budget at the point of failure
+        // rather than deferring it to the tail of the plan. A predecessor
+        // that is itself authorized to run CONCURRENTLY with this candidate
+        // (the same parallel_groups check as the active-stage gate above) is
+        // not a sequential-ordering violation — that's the entire point of
+        // an authorized parallel group.
+        if (task.status === "PENDING" || task.status === "READY") {
+          const unmetPredecessor = findUnmetPredecessorTask(taskState.tasks, task);
+          const predecessorExempt = unmetPredecessor && isAuthorizedParallelClaim({
+            activeStageIds: taskStageIds(unmetPredecessor),
+            candidateStageIds: taskStageIds(task),
+            parallelGroups,
+          });
+          if (unmetPredecessor && !predecessorExempt) {
+            return {
+              taskState, task: null,
+              missing: [] as string[], requiredApprovals: [] as string[], readiness: null as any, guardrailCheck: null as any, approvalAuditEvents: [] as any[],
+              dependencyBlock: { candidate_task_id: task.id, predecessor_task_id: unmetPredecessor.id, predecessor_status: unmetPredecessor.status },
+            };
+          }
Relevance

●● Moderate

DRY refactor is maintainability-only and invasive; no clear precedent that they require extracting
shared refusal objects.

PR-#201

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires refactoring duplicated logic. The two new refusal branches in
sdlc_build_next repeat the same return object fields verbatim, differing only in the block
payload, which indicates an extractable shared helper.

src/integrations/pi/rstack-sdlc.ts[2263-2294]
Skill: plan-eng-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two refusal branches return the same bulky object structure with only a single differing field (`activeStageBlock` vs `dependencyBlock`). This duplication is prone to inconsistency if the response shape evolves.

## Issue Context
The code is in the claim critical section for `sdlc_build_next`, so keeping refusal response construction centralized reduces maintenance risk.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[2263-2294]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. isAuthorizedParallelClaim JSDoc missing tags 📜 Skill insight ⚙ Maintainability
Description
The exported isAuthorizedParallelClaim JSDoc block lacks @param and @returns tags, so the
function is not documented to the project’s required standard. This reduces readability and
tool-assisted API understanding (IDE hints, docs generation).
Code

src/core/harness/parallel-benchmark.js[R100-115]

+/**
+ * Whether an already-ACTIVE task and a CANDIDATE task about to be claimed may
+ * run concurrently under the run's configured `parallel_groups` (#479).
+ *
+ * Distinct from the model-free runner's "does the ENTIRE fresh frontier form
+ * one authorized group" question (pipeline-run.js's planParallelGroup, used
+ * to batch-claim a whole group in one pass) — this checks one specific
+ * active/candidate PAIR, which is what the claim gate itself needs: is a
+ * second in-flight stage legitimate, scheduler-authorized parallelism, or an
+ * unauthorized double-claim? Both stay backed by the SAME data-independence
+ * check (checkDataIndependence) so "authorized" always means "config says
+ * these stages may share a group AND the group is actually data-independent",
+ * never just "config says so".
+ */
+export function isAuthorizedParallelClaim({ activeStageIds, candidateStageIds, parallelGroups } = {}) {
+  if (!parallelGroups?.enabled) return false;
Relevance

● Weak

Team has rejected adding/enforcing JSDoc/docstring tag/coverage requirements for exported JS
functions.

PR-#317
PR-#469

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires @param and @returns tags for exported functions with JSDoc/TSDoc. The new
JSDoc above export function isAuthorizedParallelClaim(...) contains only narrative text and no
tags.

src/core/harness/parallel-benchmark.js[100-128]
Skill: documentation-writing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isAuthorizedParallelClaim` is exported and has a JSDoc block, but it does not include required `@param` and `@returns` tags.

## Issue Context
Compliance requires that public/exported functions with JSDoc/TSDoc include a description plus `@param` and `@returns` tags.

## Fix Focus Areas
- src/core/harness/parallel-benchmark.js[100-128]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +48 to 52
export function advisoryIds(info) {
return (info.via ?? [])
.filter((entry) => typeof entry === 'object' && entry?.url)
.map((entry) => String(entry.url).split('/').pop());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. advisoryids not verb-noun 📜 Skill insight ⚙ Maintainability

The function name advisoryIds does not follow the required verb-noun naming convention, making
intent less explicit (e.g., extract/get vs. a noun phrase). Renaming improves consistency and
discoverability across the codebase.
Agent Prompt
## Issue description
`scripts/security-audit.mjs` exports `advisoryIds`, which is a noun phrase rather than a verb-noun action name.

## Issue Context
Compliance requires JS/TS function names to start with a verb (e.g., `get`, `extract`, `build`, `calculate`). This function extracts GHSA IDs from audit entries, so a name like `extractAdvisoryIds` or `getAdvisoryIds` would comply.

## Fix Focus Areas
- scripts/security-audit.mjs[48-52]
- scripts/security-audit.mjs[87-96] (update call sites)
- tests/security-audit.test.js[15-25] (update imports/usage)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/integrations/pi/rstack-sdlc.ts Outdated
Comment thread src/integrations/pi/rstack-sdlc.ts Outdated
Comment on lines +2250 to +2268
const activeTask = taskState.tasks.find((t: any) => t.status === "IN_PROGRESS" && t.id !== task.id);
// Loaded once and reused by both #479 checks below, so an authorized
// pair reads the SAME config/independence verdict for both "is a
// second active stage allowed" and "is this predecessor exempt".
const parallelGroups = (activeTask || task.status === "PENDING" || task.status === "READY")
? await loadProjectParallelGroups(projectRoot)
: null;
if (activeTask) {
const authorized = isAuthorizedParallelClaim({
activeStageIds: taskStageIds(activeTask),
candidateStageIds: taskStageIds(task),
parallelGroups,
});
if (!authorized) {
return {
taskState, task: null,
missing: [] as string[], requiredApprovals: [] as string[], readiness: null as any, guardrailCheck: null as any, approvalAuditEvents: [] as any[],
activeStageBlock: { active_task_id: activeTask.id, candidate_task_id: task.id },
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. New any types in claim gate 📜 Skill insight ✧ Quality

The new claim-gate logic introduces any annotations and as any casts for task objects, weakening
type safety in a critical claim-ordering/concurrency control path and increasing the risk of runtime
errors. This violates the compliance requirement to avoid any in TypeScript MCP server code and
use explicit types instead.
Agent Prompt
## Issue description
New claim-gate code in `src/integrations/pi/rstack-sdlc.ts` introduces TypeScript `any` (e.g., `find((t: any) => ...)` and `as any` casts) for task objects within the `sdlc_build_next` claim gate logic, violating compliance rules that require avoiding `any` in MCP server TypeScript code.

## Issue Context
Compliance explicitly forbids the use of `any` in TypeScript (PR Compliance IDs 1399957 and 1400551). This code path is a critical gate for claim ordering / concurrency enforcement and relies on task fields such as `id` and `status`; using `any` removes compile-time verification that these fields exist and match expected types, increasing the chance of runtime errors.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[2250-2268]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +108 to +111
if (tolerated.length) {
console.log('Tolerated advisories (vendored/bundled, unpatchable downstream — track upstream):');
for (const t of tolerated) console.log(` - ${t.name} (${t.severity}) [${t.ids.join(', ')}]`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. security-audit.mjs uses console.log 📜 Skill insight ✧ Quality

The modified security audit script writes output using console.log, which is disallowed by the
no-debug-logging compliance rule. This can lead to accidental debug output persisting in committed
source files.
Agent Prompt
## Issue description
Changed code in `scripts/security-audit.mjs` uses `console.log(...)`, which violates the "no debug logging statements" rule.

## Issue Context
This file is a CI/security gate script; it can still emit user-facing output, but it should do so via the project's logging abstraction (or another approved mechanism) rather than direct `console.log`.

## Fix Focus Areas
- scripts/security-audit.mjs[108-111]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/integrations/pi/rstack-sdlc.ts Outdated
Comment thread src/integrations/pi/rstack-sdlc.ts

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment thread src/integrations/pi/rstack-sdlc.ts Outdated
Comment on lines +2281 to +2287
if (task.status === "PENDING" || task.status === "READY") {
const unmetPredecessor = findUnmetPredecessorTask(taskState.tasks, task);
const predecessorExempt = unmetPredecessor && isAuthorizedParallelClaim({
activeStageIds: taskStageIds(unmetPredecessor),
candidateStageIds: taskStageIds(task),
parallelGroups,
});

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parallel-group predecessor exemption bypasses NEEDS_CONTEXT human gate in sdlc_build_next

Resolved — no longer flagged as of 32e5b6b.

…sors

Two reviewers (qodo, Strix) found real correctness bugs in the claim
gate's #479 checks, all stemming from the same root cause: checking only
ONE item (via .find()) when the invariant actually requires checking
EVERY item.

1. (qodo, Action required) Active-stage check used .find() to grab a
   single IN_PROGRESS task and authorize the candidate against just that
   one. With overlapping parallel_groups (e.g. [A,B] and [A,C]), a
   candidate authorized against A (found first) could be wrongly allowed
   even though a DIFFERENT active task B was not authorized against it.
   Fixed: .filter() to get every active task, refuse if ANY of them is
   unauthorized with the candidate.

2. (qodo, Action required) Predecessor check only ever looked at the
   FIRST unmet predecessor (findUnmetPredecessorTask), and if that one
   happened to be exempt via parallel-group authorization, a second,
   non-exempt unmet predecessor elsewhere in the plan was never even
   checked. Fixed: new findUnmetPredecessorTasks (plural) returns every
   unmet predecessor; the claim gate finds the first one that ISN'T
   exempt, and refuses if any such predecessor exists. The singular
   function is now a thin wrapper (`[0]`) kept for its existing narrow
   callers/tests.

3. (Strix, HIGH, CWE-862) The predecessor exemption only checked
   parallel_groups CONFIG authorization, never whether the predecessor
   was actually running. Strix reproduced this live: validate an early
   stage into NEEDS_CONTEXT (its parallel_groups sibling is still
   configured as authorized), then call sdlc_build_next directly for
   that sibling — it claimed anyway, bypassing the runner's intended
   stop-for-human-context behavior. Fixed: exemption now requires
   `predecessor.status === "IN_PROGRESS"` in addition to authorization —
   a predecessor merely CONFIGURED into a group but not actually running
   (NEEDS_CONTEXT, BLOCKED, FAIL) is never exempt. Authorization only
   ever widens what may run alongside a genuinely active peer; it never
   excuses a predecessor that isn't running at all.

Also (qodo DRY finding): extracted the repeated "no task granted" return
object into one `refusal()` helper shared by the three return sites
(plain no-task, active-stage block, dependency block) instead of
duplicating the same seven-field literal three times.

Bonus fix found while writing the adversarial tests below: the
dependency_unmet tool response's `details` was missing `predecessor_status`
(present in the emitted event, but not in what the caller sees) — added.

Test coverage: findUnmetPredecessorTasks (plural) unit tests, plus three
new end-to-end tests reproducing each finding exactly — overlapping
active-task authorization, multiple unmet predecessors with only one
exempt, and the Strix NEEDS_CONTEXT bypass. 1645/1645 tests (1 known,
unrelated, pre-existing temp-dir-cleanup flake in dashboard-env-write.test.js
that passes in isolation), typecheck clean, lint clean.

Skipped as out of scope for this PR (originate from the already-open
#473 branch merged in for CI): security-audit.mjs advisoryIds naming,
console.log usage. Skipped as not applicable given this file's
pre-existing, pervasive convention: `any` typing in the claim gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — the NEEDS_CONTEXT predecessor-exemption bypass reported in the previous review has been fixed (exemption now requires the predecessor to be actively IN_PROGRESS, not just configured into a parallel group).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0][Architecture] Enforce strict stage ordering, single-active claims, and authorized parallelism

2 participants