(ctx: TemplateContext, args?: string[])
| 118 | }; |
| 119 | |
| 120 | export function generateRedactInvocationBlock(ctx: TemplateContext, args?: string[]): string { |
| 121 | const sinkLabel = args?.[0] ?? 'pre-issue'; |
| 122 | const brief = args?.[1] === 'brief'; |
| 123 | const sink = SINKS[sinkLabel] ?? SINKS['pre-issue']; |
| 124 | const bin = `${ctx.paths.binDir}/gstack-redact`; |
| 125 | |
| 126 | // Brief variant: a compact pointer for repeat sinks, so the full ~40-line |
| 127 | // procedure ships once per skill, not once per enforcement point. |
| 128 | if (brief) { |
| 129 | return `#### Redaction scan — ${sinkLabel} (${sink.noun}) |
| 130 | |
| 131 | Run the SAME scan-at-sink procedure shown above (resolve \`$REDACT_VIS\` once and |
| 132 | reuse it; write the exact bytes to \`$REDACT_FILE\`; \`${bin} --from-file "$REDACT_FILE" |
| 133 | --repo-visibility "$REDACT_VIS" --json\`), now on ${sink.noun}. Apply the same |
| 134 | exit-3/2/0 handling. On exit 3, do NOT ${sink.blockVerb}; HIGH has no skip. Pass the |
| 135 | same \`$REDACT_FILE\` downstream so the bytes scanned are the bytes sent.`; |
| 136 | } |
| 137 | |
| 138 | return `#### Redaction scan — ${sinkLabel} (${sink.noun}) |
| 139 | |
| 140 | Scan-at-sink on the EXACT bytes that will be sent: write to a temp file, scan that |
| 141 | file, pass the SAME file downstream. Never scan a string then re-render it. |
| 142 | |
| 143 | \`\`\`bash |
| 144 | command -v bun >/dev/null 2>&1 || echo "redaction scan skipped — bun not on PATH" |
| 145 | # Resolve visibility once; cache + reuse. Order: local config (~/.gstack, never |
| 146 | # committed) → gh → glab → unknown(=public-strict). |
| 147 | REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null) |
| 148 | [ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z') |
| 149 | [ -z "$REDACT_VIS" ] && REDACT_VIS=$(glab repo view -F json 2>/dev/null | grep -o '"visibility":"[^"]*"' | head -1 | sed 's/.*:"//;s/"//' | tr 'A-Z' 'a-z') |
| 150 | REDACT_VIS="\${REDACT_VIS:-unknown}" |
| 151 | REDACT_FILE=$(mktemp) |
| 152 | cat > "$REDACT_FILE" <<'REDACT_BODY_EOF' |
| 153 | <the exact ${sink.noun} goes here> |
| 154 | REDACT_BODY_EOF |
| 155 | REDACT_JSON=$(${bin} --from-file "$REDACT_FILE" --repo-visibility "$REDACT_VIS" --self-email "$(git config user.email 2>/dev/null)" --json) |
| 156 | REDACT_CODE=$? |
| 157 | \`\`\` |
| 158 | |
| 159 | Branch on \`$REDACT_CODE\`: |
| 160 | |
| 161 | 1. **Exit 3 (HIGH)** — print findings; do NOT ${sink.blockVerb}; tell the user to |
| 162 | rotate + redact at source, then re-run. No skip flag for HIGH. Do not persist |
| 163 | ${sink.noun} anywhere. |
| 164 | 2. **Exit 2 (MEDIUM)** — AskUserQuestion per finding (cluster identical ids; PUBLIC |
| 165 | repos get sterner wording, no batch-acknowledge, no silent-proceed). PII subset |
| 166 | (\`pii.email\`/\`pii.phone.e164\`/\`pii.ssn\`/\`pii.cc\`) gets **Auto-redact** (re-run |
| 167 | with \`--auto-redact <ids>\` → use the printed sanitized body) / **Edit** / **Cancel**; |
| 168 | non-PII MEDIUM gets **Proceed (acknowledged)** / **Edit** / **Cancel** (no auto-redact). |
| 169 | 3. **Exit 0 (clean)** — proceed; surface \`WARN\` (tool-fence degrades) + \`LOW\` as a |
| 170 | one-line FYI (never blocks). |
| 171 | |
| 172 | \`\`\`bash |
| 173 | rm -f "$REDACT_FILE" |
| 174 | \`\`\` |
| 175 | |
| 176 | Guardrail, not airtight enforcement — direct \`gh\`/\`git\` bypass it; it catches accidents.`; |
| 177 | } |
no outgoing calls
no test coverage detected