Files
msd-core/get-shit-done/workflows/secure-phase.md
Tom Boucher 2b9fa009d3 fix(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher (#379)
* fix(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher

Workflow bash blocks resolved the runtime as GSD_SDK="node $GSD_TOOLS" and
invoked it unquoted ($GSD_SDK query ...). On install paths containing spaces
(e.g. /Volumes/Mini Me/...) the unquoted expansion word-split into
`node /Volumes/Mini gsd-tools.cjs ...`, failing with "Cannot find module
'/Volumes/Mini'" and getting masked by `2>/dev/null || echo "{}"` into a
silent empty state.

Replace the string variable with a single-line shell launcher that defines a
gsd_run function, invokes the runtime with a fully-quoted path and "$@", and
preserves the local-cjs / installed-gsd-tools-on-PATH fallback (#3668) plus
the loud not-found error and install hint. The launcher uses _GSD_SHIM_NAME
indirection so no workflow emits the /gsd-tools substring that the do.md
dispatcher-parity scanner would misread, and is single-line to stay within
the per-file progressive-disclosure line budgets (#2551).

The canonical launcher lives in
get-shit-done/workflows/_runtime-launcher.snippet.sh, is propagated once per
file by scripts/sync-runtime-launcher.cjs, and is locked by
tests/runtime-launcher-parity.test.cjs (fails CI on drift, on a reappearing
$GSD_SDK token, or on a /gsd-tools substring). Dependent workflow-assertion
tests are updated from $GSD_SDK to gsd_run, and the runtime launcher is
registered in CONTEXT.md.

Fixes #373

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(#373): add changeset fragment and allow-test-rule for parity guard

The runtime-launcher parity test is a structural drift guard that reads
workflow markdown to assert the canonical launcher is present and the
retired $GSD_SDK / /gsd-tools tokens are absent; annotate it with
allow-test-rule per the no-source-grep lint escape hatch. Add the required
.changeset fragment for this user-facing fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(#373): make parity PATH-fallback assertion cross-platform (Windows)

Subtest (E) compared GSD_TOOLS against the Node-side absolute temp path,
but the value originates from git-bash which reports the POSIX form, so the
prefix comparison failed on windows-latest while the launcher itself worked
(the installed stub was invoked). Assert the resolved binary by normalized
suffix (/bin/gsd-tools, not .cjs) instead of the absolute prefix; the
behavioral stub-invocation assertion is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:21:05 -04:00

7.9 KiB

Verify threat mitigations for a completed phase. Confirm PLAN.md threat register dispositions are resolved. Update SECURITY.md.

<required_reading> @~/.claude/get-shit-done/references/ui-brand.md </required_reading>

<available_agent_types> Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):

  • gsd-security-auditor — Verifies threat mitigation coverage </available_agent_types>

0. Initialize

_GSD_SHIM_NAME="gsd-tools.cjs"; GSD_TOOLS="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/get-shit-done/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/get-shit-done-redux@latest --claude --local" >&2; exit 1; fi
INIT=$(gsd_run query init.phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_AUDITOR=$(gsd_run query agent-skills gsd-security-auditor)

Parse: phase_dir, phase_number, phase_name, phase_slug, padded_phase.

AUDITOR_MODEL=$(gsd_run query resolve-model gsd-security-auditor --raw)
SECURITY_CFG=$(gsd_run query config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")

If SECURITY_CFG is false: exit with "Security enforcement disabled. Enable via /gsd:settings."

Display banner: GSD > SECURE PHASE {N}: {name}

1. Detect Input State

SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
PLAN_FILES=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null)
SUMMARY_FILES=$(ls "${PHASE_DIR}"/*-SUMMARY.md 2>/dev/null)
  • State A (SECURITY_FILE non-empty): Audit existing
  • State B (SECURITY_FILE empty, PLAN_FILES and SUMMARY_FILES non-empty): Run from artifacts
  • State C (SUMMARY_FILES empty): Exit — "Phase {N} not executed. Run /gsd:execute-phase {N} first."

2. Discovery

2a. Read Phase Artifacts

Read PLAN.md — extract <threat_model> block: trust boundaries, STRIDE register (threat_id, category, component, disposition, mitigation_plan).

2b. Read Summary Threat Flags

Read SUMMARY.md — extract ## Threat Flags entries.

2c. Build Threat Register

Per threat: { threat_id, category, component, disposition, mitigation_pattern, files_to_check }

Also set register_authored_at_plan_time: true if at least one PLAN file contained a parseable <threat_model> block; false if no PLAN files had any <threat_model> block (legacy phase authored before formal threat modelling was standard).

3. Threat Classification

Classify each threat:

Status Criteria
CLOSED mitigation found OR accepted risk documented in SECURITY.md OR transfer documented
OPEN none of the above

Build: { threat_id, category, component, disposition, status, evidence }

Short-circuit rule:

  • If threats_open: 0 AND register_authored_at_plan_time: true → skip to Step 6 directly. All plan-time threats are verified CLOSED.
  • If threats_open: 0 AND register_authored_at_plan_time: false → do NOT skip. Empty-by-no-planning must not rubber-stamp a clean SECURITY.md. Proceed to Step 5 in retroactive-STRIDE mode — the auditor builds a register from implementation files first, then verifies mitigations.
  • If threats_open > 0 → proceed to Step 4 (present threat plan to user).

4. Present Threat Plan

Text mode (workflow.text_mode: true in config or --text flag): Set TEXT_MODE=true if --text is present in $ARGUMENTS OR text_mode from init JSON is true. When TEXT_MODE is active, replace every AskUserQuestion call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where AskUserQuestion is not available. Call AskUserQuestion with threat table and options:

  1. "Verify all open threats" → Step 5
  2. "Accept all open — document in accepted risks log" → add to SECURITY.md accepted risks, set all CLOSED, Step 6
  3. "Cancel" → exit

5. Spawn gsd-security-auditor

Auditor constraint — varies by register origin:

  • register_authored_at_plan_time: true — Verify mitigations exist — do not scan for new threats. The register is complete; verify each threat's mitigation is present in the implementation.
  • register_authored_at_plan_time: false (retroactive-STRIDE mode) — Retroactive-STRIDE: build a STRIDE register from implementation files first, then verify mitigations. The phase was authored before formal threat modelling; the auditor must construct the register from scratch before verifying.
Agent(
  prompt="Read ~/.claude/agents/gsd-security-auditor.md for instructions.\n\n" +
    "<files_to_read>{PLAN, SUMMARY, impl files, SECURITY.md}</files_to_read>" +
    "<threat_register>{threat register}</threat_register>" +
    "<config>asvs_level: {SECURITY_ASVS}, block_on: {SECURITY_BLOCK_ON}</config>" +
    "<constraints>Never modify implementation files. Verify mitigations exist — do not scan for new threats. Escalate implementation gaps.</constraints>" +
    "${AGENT_SKILLS_AUDITOR}",
  subagent_type="gsd-security-auditor",
  model="{AUDITOR_MODEL}",
  description="Verify threat mitigations for Phase {N}"
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

Handle return:

  • ## SECURED → record closures → Step 6
  • ## OPEN_THREATS → record closed + open, present user with accept/block choice → Step 6
  • ## ESCALATE → present to user → Step 6

6. Write/Update SECURITY.md

State B (create):

  1. Read template from ~/.claude/get-shit-done/templates/SECURITY.md
  2. Fill: frontmatter, threat register, accepted risks, audit trail
  3. Write to ${PHASE_DIR}/${PADDED_PHASE}-SECURITY.md

State A (update):

  1. Update threat register statuses, append to audit trail:
## Security Audit {date}
| Metric | Count |
|--------|-------|
| Threats found | {N} |
| Closed | {M} |
| Open | {K} |

ENFORCING GATE: If threats_open > 0 after all options exhausted (user did not accept, not all verified closed):

GSD > PHASE {N} SECURITY BLOCKED
{K} threats open — phase advancement blocked until threats_open: 0
▶ Fix mitigations then re-run: /gsd:secure-phase {N}
▶ Or document accepted risks in SECURITY.md and re-run.

Do NOT emit next-phase routing. Stop here.

7. Commit

gsd_run query commit "docs(phase-${PHASE}): add/update security threat verification"

8. Results + Routing

Secured (threats_open: 0):

GSD > PHASE {N} THREAT-SECURE
threats_open: 0 — all threats have dispositions.
▶ /gsd:validate-phase {N}    validate test coverage
▶ /gsd:verify-work {N}       run UAT

Display /clear reminder.

<success_criteria>

  • Security enforcement checked — exit if false
  • Input state detected (A/B/C) — state C exits cleanly
  • PLAN.md threat model parsed, register built
  • SUMMARY.md threat flags incorporated
  • threats_open: 0 AND register_authored_at_plan_time: true → skip directly to Step 6
  • threats_open: 0 AND register_authored_at_plan_time: false → retroactive-STRIDE mode (Step 5), not skipped
  • User gate with threat table presented
  • Auditor spawned with complete context
  • All three return formats (SECURED/OPEN_THREATS/ESCALATE) handled
  • SECURITY.md created or updated
  • threats_open > 0 BLOCKS advancement (no next-phase routing emitted)
  • Results with routing presented on success </success_criteria>