Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 119 additions & 2 deletions .agents/skills/spectra-archive/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,27 @@ metadata:

Archive a completed change.

**Input**: Optionally specify a change name after `$spectra-archive` (e.g., `$spectra-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Input**: Optionally specify a change name after `/spectra-archive` (e.g., `/spectra-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.

**Prerequisites**: This skill requires the `spectra` CLI. If any `spectra` command fails with "command not found" or similar, report the error and STOP.

**Step 0: Bootstrap Stage Task List** (required)

Before doing anything else, call `TaskCreate` to build a harness-level todo list for this archive run — one entry per step below. Mark each `TaskUpdate → completed` as you finish it; silent completion is a violation. This mirrors the Step 0 Bootstrap discipline every `idd-*` skill enforces, and gives per-step accountability when `/idd-close` cascades into `/spectra-archive`.

```
TaskCreate(name="prompt_change_name", description="Step 1: resolve change name — prompt via AskUserQuestion if not provided / inferable")
TaskCreate(name="check_artifacts", description="Step 2: spectra status --json — warn + confirm if any artifact not done")
TaskCreate(name="check_tasks", description="Step 3: scan tasks.md — warn + confirm if incomplete - [ ] tasks")
TaskCreate(name="assess_delta_sync", description="Step 4: compare delta specs vs main specs; prompt sync now / archive without sync")
TaskCreate(name="cleanup_tracking", description="Step 5: rm -f .spectra/touched/<name>.json")
TaskCreate(name="run_archive_cli", description="Step 6: spectra archive <name>")
TaskCreate(name="post_implementation_complete", description="Step 7: invoke spectra-archive-post-ic.sh to post ## Implementation Complete to the linked GitHub issue (#56)")
TaskCreate(name="display_summary", description="Step 8: read Step 7 outcome + show archive completion summary")
```

Complete each step → `TaskUpdate → completed` immediately.

**Steps**

1. **If no change name provided, prompt for selection**
Expand Down Expand Up @@ -93,13 +110,111 @@ Archive a completed change.

**If archive fails** with "already exists" error, suggest renaming existing archive.

7. **Display summary**
7. **Post `## Implementation Complete` to linked GitHub issue (v1.3+, PsychQuant/issue-driven-development#56)**

**Purpose**: ensures `/idd-close` Step 0 supersession gate triggers for Spectra-path issues, removing the need for manual retroactive Implementation Complete synthesis.

**Delegated to executable helper script** `.claude/scripts/spectra-archive-post-ic.sh` (with unit tests at `.claude/scripts/tests/spectra-archive-post-ic/`). The script is the source of truth — this skill calls it and reads the outcome. Behavior contract (detection / idempotent guard / safe body composition / multi-candidate handling) lives in the script + its tests, not in skill prose. This separation was introduced after R2 verify found that prose-with-illustrative-bash had structural defects (variable persistence across Bash tool calls, Python3 RCE via shell-string interpolation, etc.) — see PsychQuant/issue-driven-development#56 R2 verify report.

**Required inputs from caller (agent)**: before invoking Step 7, the agent MUST have these in scope (from earlier skill steps):
- `$CHANGE_NAME` — the Spectra change name (slug; same value passed to `spectra archive`). **This exact value must also be reused, byte-identical, in Step 8** — the outcome file path is derived from it, so any drift (typo / whitespace / case) makes Step 8 read the wrong path.
- `$SPEC_DELTAS` (optional) — comma-separated capability names from `spectra archive` stdout in Step 6 (e.g., `"idd-all-chain, idd-spawn-manifest"`); defaults to placeholder if absent

**Invocation**:

```bash
# Resolve repo root to make the script path cwd-independent — the skill may be
# invoked from a subdirectory (cd openspec && /spectra-archive ...). Relative
# path .claude/scripts/... breaks; absolute path via git-root prefix doesn't.
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)

ARCHIVE_DIR="${REPO_ROOT}/openspec/changes/archive/$(date +%Y-%m-%d)-${CHANGE_NAME}"
SPEC_DELTAS="${SPEC_DELTAS:-(see archived change directory)}"

# The helper script DERIVES the outcome file path internally from
# --change-name: /tmp/spectra-archive-ic-outcome-<change-name>.txt
# (--change-name is allowlist-validated inside the script before being used
# in the path, so the derived path is always traversal-safe). The formula
# has a single source of truth — the script — so this skill does NOT pass
# --outcome-file. Step 8 recomputes the same path from the same $CHANGE_NAME
# to read the outcome across separate Bash tool calls.
# Why deterministic, not $$-$(date +%s): a random suffix cannot be recomputed
# in a second shell, which breaks the Step 7 → Step 8 handoff (R4 verify
# R4-S1 finding). The change name is the skill's primary input — always known
# to Step 8 — so the derived path IS recoverable.
bash "${REPO_ROOT}/.claude/scripts/spectra-archive-post-ic.sh" \
--change-name "$CHANGE_NAME" \
--archive-dir "$ARCHIVE_DIR" \
--spec-deltas "$SPEC_DELTAS"
POST_IC_EXIT=$?
```

**Exit codes**:

| Exit | Meaning | Agent action |
|------|---------|--------------|
| `0` | Success or normal skip (posted / none / idempotent / unsafe-name / generic failure) | Read outcome from the derived outcome file (see Step 8), proceed to Step 8 |
| `2` | Usage error (missing args, or unsafe `--outcome-file` path) | Skill bug — fix invocation |
| `64` | Dependency missing (python3) | Surface to user; archive itself succeeded; manual retry after install |
| `75` | Multi-candidate detected | **AskUserQuestion required** — read `/tmp/spectra-archive-candidates.txt` for the candidate list, prompt user to pick canonical issue (show `#N + gh issue title` for each), then re-invoke script with `--linked-issue <chosen>` |

**Stdout**: a single line that is either the IC comment URL, or one of the documented status messages (`(none — ...)`, `(skipped — ...)`, `(pending — ...)`, `(failed — ...)`). The same line is also written to the derived outcome file `/tmp/spectra-archive-ic-outcome-${CHANGE_NAME}.txt` for cross-Bash-call persistence (Step 8 reads from there).

**Multi-candidate flow (agent responsibility)**:

```bash
if [ "$POST_IC_EXIT" = "75" ]; then
# Read candidates + AskUserQuestion + re-invoke (script re-derives the outcome path from --change-name)
CANDIDATES=$(cat /tmp/spectra-archive-candidates.txt)
# For each candidate, fetch title via `gh issue view <N> --json title -q .title`
# Then AskUserQuestion: "Multi-candidate detected: which is canonical?"
# User picks → CHOSEN_ISSUE=<N>
bash "${REPO_ROOT}/.claude/scripts/spectra-archive-post-ic.sh" \
--change-name "$CHANGE_NAME" \
--archive-dir "$ARCHIVE_DIR" \
--spec-deltas "$SPEC_DELTAS" \
--linked-issue "$CHOSEN_ISSUE"
fi
```

The agent (LLM-driven) handles the AskUserQuestion step — bash cannot prompt. The script validates `$CHOSEN_ISSUE` against the original candidate set on re-invoke.

**Failure semantics**: any failure in Step 7 (gh auth lost, network, body too large, etc.) is recorded in the outcome file but does NOT abort the overall archive operation — the archive itself (Step 6) has already succeeded, and the archived change directory + main spec deltas are the canonical record. The GitHub comment is the convenience anchor for `/idd-close` supersession.

**Testing**: run `.claude/scripts/tests/spectra-archive-post-ic/test.sh` to validate the script against fixture archive directories (covers explicit-marker / Refs-fallback / no-marker / multi-candidate / malicious-tasks.md / missing-tasks.md / unsafe-change-name / linked-issue-resolved / linked-issue-invalid / outcome-path-derivation / unsafe-outcome-file). All 11 fixtures pass.

8. **Display summary**

Read the outcome from Step 7. Recompute the outcome file path the same way the
script derived it — from the **identical** `$CHANGE_NAME` slug passed to
`spectra archive` in Step 6. This works whether Step 7 + Step 8 ran in the same
Bash invocation (var still in scope) OR in separate Bash calls (the formula is
deterministic). **The `$CHANGE_NAME` here MUST be byte-identical to Step 7's —
no typo, trailing whitespace, or case difference** — otherwise Step 8 reads a
different path:

```bash
# Recompute the same path the script derived in Step 7.
# MUST use the identical $CHANGE_NAME slug — see contract note in Step 7.
OUTCOME_FILE="/tmp/spectra-archive-ic-outcome-${CHANGE_NAME}.txt"

if [ -f "$OUTCOME_FILE" ]; then
IMPLEMENTATION_COMPLETE_POSTED=$(cat "$OUTCOME_FILE")
else
# Loud failure — NOT a quiet "(unknown)". A missing outcome file means
# either Step 7 never ran, or $CHANGE_NAME drifted between Step 7 and
# Step 8. Both are real errors the user must see.
IMPLEMENTATION_COMPLETE_POSTED="⚠️ ERROR — outcome file not found: $OUTCOME_FILE (Step 7 did not run, or \$CHANGE_NAME drifted between Step 7 and Step 8)"
echo "WARNING: spectra-archive Step 8 — outcome file missing: $OUTCOME_FILE" >&2
fi
```

Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Spec sync status (synced / sync skipped / no delta specs)
- **Implementation Complete posted to:** `$IMPLEMENTATION_COMPLETE_POSTED`
- Note about any warnings (incomplete artifacts/tasks)

**Output On Success**
Expand Down Expand Up @@ -172,3 +287,5 @@ Target archive directory already exists.
- If sync is requested, use the Skill tool to invoke `spectra-sync-specs` (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
- If **AskUserQuestion tool** is not available, ask the same questions as plain text and wait for the user's response
- **Step 8 multi-candidate disambiguation**: if linked-issue detection (Fallback 1 explicit marker) returns multiple distinct `#N` values, MUST prompt user via AskUserQuestion to pick the canonical one — never auto-pick to avoid posting to the wrong issue
- **Step 8 silent skip on no linked issue**: do not warn or prompt; not all archives have GitHub trackers (legacy archives, design-only changes). Reflect skip reason in Step 7 summary line
2 changes: 1 addition & 1 deletion plugins/issue-driven-dev/.claude-plugin/plugin.json

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions plugins/issue-driven-dev/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.70.0] - 2026-05-20

### Fixed

- **`idd-issue` Step 1 pasted-image immediate-persistence** ([#112](https://github.com/PsychQuant/issue-driven-development/issues/112)): Claude Code's `~/.claude/image-cache/<session-id>/` is per-session + cleared by context compaction / session lifecycle / session-id rollover. Step 1 → Step 4 separation (read annotation in Step 1, upload in Step 4) spans `AskUserQuestion` + Step 2.5/2.6 + Step 3 `gh issue create` + Step 4 upload — easily long enough for cache eviction. 2026-05-20 downstream incident (`kiki830621/ai_martech_global_scripts#788`) hit exactly this failure mode. NEW immediate-persistence rule: when Step 1 encounters `[Image: source: <path>]` annotation, `cp` to `/tmp/idd-issue-attachments/issue_pending_<ts>_<rand>.png` in the **same tool turn** that reads the annotation; Step 4 references the staged path, not the original cache path. Anonymous `/tmp` staging (POSIX-safe, system-cleanup-friendly, no repo pollution) per `feedback_lead_minimal`. Fallback for already-evicted source: warn + continue without that attachment.

### Refactored

- **`spectra-archive` skill `.agents/` ↔ `.claude/` sync** ([#93](https://github.com/PsychQuant/issue-driven-development/issues/93)): #93 surfaced 3-copy divergence between `.claude/skills/`, `.agents/skills/`, and `plugins/.../references/spectra-skills/`. Investigation refuted the diagnose-time recommendation to delete `.agents/` — 4 openspec specs reference `.agents/skills/spectra-*/SKILL.md` as Spectra-tier dependencies (the path is LIVE, not legacy). Revised disposition: sync `.agents/skills/spectra-archive/SKILL.md` from `.claude/` so both LIVE load paths carry the v1.3+ Implementation Complete auto-post feature (#56). `plugins/.../references/spectra-skills/spectra-archive/` left as historical snapshot (no markdown cross-refs found; low cleanup ROI per `lead-minimal`). **Sister-skill divergences out of scope**: 7 other spectra-* skills (audit / discuss / propose / apply / ingest / debug / commit) also diverge between `.claude/` and `.agents/` — audit comment on #93 documents the drift matrix. **Sister issues NOT auto-filed in this PR** per `feedback_lead_minimal` — drift documented as observation, separate issues will be filed if specific divergence causes user-visible friction. (Original wording "filed for separate follow-up as needed" was misleading per #115 DA finding DA-1 — no issues actually filed.) Drift-prevention CI hook deferred until drift recurs naturally.

- **`idd-implement` cluster detection glob hardening + Option A-final doc** ([#100](https://github.com/PsychQuant/issue-driven-development/issues/100)): two non-blocking findings from PR #99 (#96) verify rounds.
- **Finding 1 (design)** — Option A (cluster mode unconditionally forces PR regardless of branch context) confirmed final. NEW `### Feature-branch + cluster + direct-commit — rejected case` subsection in `references/pr-flow.md` § Cluster mode override documenting the rejected Option B (branch-context-gated cluster direct-commit) with comparison table + rationale. Contract simplicity wins; feature-branch direct-commit workflow remains viable for single-issue `--no-pr` invocations.
- **Finding 2 (refactor)** — `idd-implement` Step 0.5 cluster detection bash hardened. Previous glob `\#[0-9]*` over-matched (`#42abc` counted, `#34 #34` over-counted as 2). Replaced with strict integer check (`[[ "$arg_num" =~ ^[0-9]+$ ]]`) + associative-array dedup matching the documented `^#\d+$` form in `batch-and-cluster.md`. 0 behavior change for well-formed distinct invocations. **Quiet behavior change for malformed tokens** (per #115 DA finding DA-2): pre-v2.70.0, `#42abc` was counted as a cluster member (causing later failures when used as issue number); post-v2.70.0 it's silently skipped from the count. Users invoking with typo'd tokens get cluster-mode evaluation based on well-formed tokens only — failure modes shifted from "fail mid-loop on bad number" to "treat as if token not present".

### Notes

- Plugin v2.70.0 is a **minor** bump (over v2.69.0) covering 3 issues across `idd-issue` + `idd-implement` + `pr-flow.md` + `.agents/skills/spectra-archive/SKILL.md`. All changes additive (#112 immediate-persistence + #93 sync + #100 glob hardening + Option A-final documentation). Cluster PR for review surface — verify ensemble runs over the cumulative diff.
- Marketplace.json sync deferred to `/idd-close` Step 6.5 chain (per repo precedent).

## [2.69.0] - 2026-05-20

### Fixed
Expand Down
17 changes: 17 additions & 0 deletions plugins/issue-driven-dev/references/pr-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ Then proceeds as PR path. **No abort, no silent ignore** — the flag is acknowl

**Single-issue invocation behavior is unchanged** — the cluster carve-out only fires on ≥2 `#N`. Backward compatibility preserved.

#### Feature-branch + cluster + direct-commit — rejected case (v2.70.0+, #100 Finding 1)

PR #99 (#96 implementation, Option A) chose to unconditionally force PR for cluster mode, regardless of the starting branch. Devil's Advocate during verify flagged that the "force PR" rationale (stacked half-isolated changes on default branch) **only holds when the user starts from the default branch**. On a non-default feature branch, cluster direct-commit just stacks N `Refs #N` commits on that feature branch — a legitimate workflow (one local feature tracking N issues, shipped as one PR later).

The alternative was **Option B** (branch-context-gated cluster direct-commit): if current branch != default branch, honor `--no-pr` / `pr_policy=never`; otherwise force PR as today. This issue confirms **Option A is final**:

| Aspect | Option A (current) | Option B (rejected) |
|--------|--------------------|--------------------|
| Contract simplicity | Cluster → PR. Uniform regardless of branch context. | Cluster → PR if default branch, else direct-commit. Two paths. |
| Override notice | One wording, mirrors fork detection | Two wordings depending on branch context |
| `git symbolic-ref` dependency | None | Required (detached HEAD / merge-state edge cases) |
| Cluster-on-feature-branch frequency | Rare (most cluster invocations are explicit `--pr`) | Same rare frequency, but now requires branch-context check overhead |

**Recommendation**: keep Option A. The feature-branch direct-commit workflow remains viable for **single-issue** invocations (which honor `--no-pr` / `pr_policy=never`). Users who want cluster-on-feature-branch direct-commit pattern can: (a) run cluster as PR + cherry-pick or rebase to feature branch post-merge, or (b) run N atomic single-issue `--no-pr` invocations on the feature branch.

If cluster-on-feature-branch direct-commit becomes a common pattern (not anticipated based on current usage), revisit Option B in a future issue. Until then, contract simplicity wins.

Cross-reference: full cluster semantics in [batch-and-cluster.md](batch-and-cluster.md).

### `pr_policy` config field
Expand Down
20 changes: 18 additions & 2 deletions plugins/issue-driven-dev/skills/idd-implement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,30 @@ TaskCreate(name="sister_bug_sweep", description="Step 5.7: review session log +
```

```bash
# 1. Parse flag + count issue args (cluster mode = ≥2 #N)
# 1. Parse flag + count DISTINCT well-formed issue args (cluster mode = ≥2 #N)
#
# v2.70.0+ #100 Finding 2 — glob hardening:
# - Previous glob `\#[0-9]*` matched any `#<digit><anything>` including
# malformed tokens like `#42abc` (the `*` absorbed the trailing letters)
# - Duplicate args like `#34 #34` over-counted as 2, tripping CLUSTER_MODE
# on a single distinct issue
# - Strict regex `^#[0-9]+$` (matching batch-and-cluster.md documented form)
# + associative-array dedup closes both issues
PR_FLAG="" # "pr" / "no-pr" / ""
declare -A SEEN_ISSUES=()
ISSUE_COUNT=0
for arg in "$@"; do
case "$arg" in
--pr) PR_FLAG="pr" ;;
--no-pr) PR_FLAG="no-pr" ;;
\#[0-9]*) ISSUE_COUNT=$((ISSUE_COUNT + 1)) ;;
\#*)
# Strict integer check + dedup (v2.70.0+ #100)
arg_num="${arg#\#}"
if [[ "$arg_num" =~ ^[0-9]+$ ]] && [ -z "${SEEN_ISSUES[$arg_num]:-}" ]; then
SEEN_ISSUES[$arg_num]=1
ISSUE_COUNT=$((ISSUE_COUNT + 1))
fi
;;
esac
done
CLUSTER_MODE="false"
Expand Down
Loading