Skip to content
Open
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
373 changes: 373 additions & 0 deletions .claude/scripts/idd-edit-helper.sh

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .claude/scripts/tests/idd-edit/fixtures/01-scope-eq/args.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
comment:123
--replace
--scope=whole-comment
--body=hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SCOPE_FLAG=whole-comment
MODE=--replace
BODY_INPUT=hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
comment:123
--replace
--scope
whole-comment
--body=hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SCOPE_FLAG=whole-comment
MODE=--replace
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
comment:123
--replace
--scope
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--scope requires value
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
comment:123
--replace
--scope
--body
hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--scope value cannot start with '--'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
comment:123
--replace
--scope=whole-comment
--body-file=/tmp/idd-edit-nonexistent-xyz-99999.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--body-file not readable
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
comment:123
--replace
--scope=whole-comment
--body-file=__FIXTURE_PATH__/multiline_input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
line 1
line 2
backtick
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
line 1
line 2
line 3 with `backtick`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
comment:123
--append
--body=hello world
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
BODY_INPUT=hello\ world
MODE=--append
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__FIXTURE_PATH__/input.md
## Foo
__FIXTURE_PATH__/replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Foo
REPLACED FOO
new line 2
## Qux
qux content (should NOT be replaced)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Doc

## Foo
foo content

### Bar
bar content under foo

### Baz
baz content under foo

## Qux
qux content (should NOT be replaced)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
REPLACED FOO
new line 2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
section-replace
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__FIXTURE_PATH__/input.md
## Foo
__FIXTURE_PATH__/replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## Foo
REPLACED TO EOF
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Doc

## Foo
some content
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REPLACED TO EOF
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
section-replace
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
comment:123
--replace
--body=hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--replace requires --scope whole-comment OR --section
Requirement 4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
comment:123
--append
--body=test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OVERRIDE_USER_CONTENT=false
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
comment:123
--append
--body=test
--override-user-content
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--override-user-content requires --reason
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
comment:123
--prepend-note
--reason=errata clarification per IDD discipline
--override-user-content
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OVERRIDE_USER_CONTENT=true
REASON=errata\ clarification\ per\ IDD\ discipline
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parse-args
130 changes: 130 additions & 0 deletions .claude/scripts/tests/idd-edit/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# Test runner for .claude/scripts/idd-edit-helper.sh
#
# Each fixture in fixtures/<NN-name>/ provides:
# - subcmd.txt — subcommand to run (parse-args / validate-target / section-replace)
# - args.txt — args to pass (one per line; supports __FIXTURE_PATH__ placeholder)
# - expected_exit.txt — expected exit code
# - expected_stderr_contains.txt (optional) — substrings that MUST appear in stderr
# - expected_stdout.txt (optional) — exact stdout match (only for section-replace)
# - expected_stdout_contains.txt (optional) — substrings that MUST appear in stdout
#
# Exit 0 if all tests pass; 1 otherwise.

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
TARGET_SCRIPT="$REPO_ROOT/.claude/scripts/idd-edit-helper.sh"
FIXTURES_DIR="$SCRIPT_DIR/fixtures"

if [ ! -x "$TARGET_SCRIPT" ]; then
echo "ERROR: target script not executable: $TARGET_SCRIPT" >&2
exit 1
fi

PASS=0
FAIL=0
FAILED_TESTS=()

for fixture in "$FIXTURES_DIR"/*/; do
name=$(basename "$fixture")
fixture_abs=$(cd "$fixture" && pwd)

if [ ! -f "$fixture/subcmd.txt" ] || [ ! -f "$fixture/args.txt" ] || [ ! -f "$fixture/expected_exit.txt" ]; then
echo "SKIP $name (missing subcmd.txt/args.txt/expected_exit.txt)"
continue
fi

subcmd=$(cat "$fixture/subcmd.txt")
expected_exit=$(cat "$fixture/expected_exit.txt" | tr -d '[:space:]')

# Build args array — read one line per arg, substitute placeholder
args=()
while IFS= read -r line || [ -n "$line" ]; do
line="${line//__FIXTURE_PATH__/$fixture_abs}"
args+=("$line")
done < "$fixture/args.txt"

# Run target script, capture stdout + stderr + exit
set +e
actual_stdout=$("$TARGET_SCRIPT" "$subcmd" "${args[@]+"${args[@]}"}" 2>"/tmp/idd-edit-test-stderr-$$")
actual_exit=$?
actual_stderr=$(cat "/tmp/idd-edit-test-stderr-$$")
rm -f "/tmp/idd-edit-test-stderr-$$"
set -e

# Assertions
local_pass=true
failure_reasons=()

# Exit code
if [ "$actual_exit" != "$expected_exit" ]; then
local_pass=false
failure_reasons+=("exit code: expected=$expected_exit actual=$actual_exit")
fi

# Stderr contains (use -- to handle needles starting with --)
if [ -f "$fixture/expected_stderr_contains.txt" ]; then
while IFS= read -r needle || [ -n "$needle" ]; do
[ -z "$needle" ] && continue
if ! echo "$actual_stderr" | grep -qF -- "$needle"; then
local_pass=false
failure_reasons+=("stderr missing: $needle")
fi
done < "$fixture/expected_stderr_contains.txt"
fi

# Stdout exact
if [ -f "$fixture/expected_stdout.txt" ]; then
expected_stdout=$(cat "$fixture/expected_stdout.txt")
if [ "$actual_stdout" != "$expected_stdout" ]; then
local_pass=false
failure_reasons+=("stdout mismatch (see diff below)")
fi
fi

# Stdout contains (use -- to handle needles starting with --)
if [ -f "$fixture/expected_stdout_contains.txt" ]; then
while IFS= read -r needle || [ -n "$needle" ]; do
[ -z "$needle" ] && continue
if ! echo "$actual_stdout" | grep -qF -- "$needle"; then
local_pass=false
failure_reasons+=("stdout missing: $needle")
fi
done < "$fixture/expected_stdout_contains.txt"
fi

if [ "$local_pass" = "true" ]; then
echo "PASS $name"
PASS=$((PASS + 1))
else
echo "FAIL $name"
for r in "${failure_reasons[@]}"; do
echo " → $r"
done
# Diff for visibility on stdout mismatch
if [ -f "$fixture/expected_stdout.txt" ] && [ "$actual_stdout" != "$(cat "$fixture/expected_stdout.txt")" ]; then
echo " expected stdout:"
sed 's/^/ /' "$fixture/expected_stdout.txt"
echo " actual stdout:"
echo "$actual_stdout" | sed 's/^/ /'
fi
echo " stderr:"
echo "$actual_stderr" | sed 's/^/ /'
FAIL=$((FAIL + 1))
FAILED_TESTS+=("$name")
fi
done

echo ""
echo "================================"
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo "Failed tests:"
for t in "${FAILED_TESTS[@]}"; do
echo " - $t"
done
exit 1
fi
exit 0
6 changes: 3 additions & 3 deletions openspec/specs/append-vs-modify-discipline/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Defines the action-scoped modify discipline that governs every IDD plugin action that modifies existing artifacts (issue bodies, comments, files, state fields). Every modify-action SHALL declare exactly one scope category from a canonical 7-category enumeration (`state-field-update`, `bounded-section-replace`, `audit-block-append`, `inline-replace-before-publish`, `verbatim-preserve`, `append-only`, `free-rewrite`). Undeclared modify-actions SHALL be refused at runtime regardless of actor identity (default-refuse), and `/idd-edit --replace` SHALL require an explicit `--scope` or `--section` flag. The discipline also defines authoritative-source resolution for checklist gates (Implementation Complete > Current Status > top-level Todo/Tasks/Checklist) with a legacy-scan fallback for pre-discipline issues, plus retroactive category labeling of existing IDD skills. Sourced from change `add-action-scoped-modify-discipline`.

**Implementation status (v2.74.0)**: discipline declared at spec level. Runtime enforcement landed for `bounded-section-replace` (`/idd-update` REPLACE), `state-field-update` (`/idd-clarify` status mutation, `spectra task done`), `audit-block-append` (5 IC_R011 PATCH sites), `inline-replace-before-publish` (`/idd-close` Step 3.5), Path C authoritative-source gate logic across 4 gate sites. **`/idd-edit` runtime enforcement of `--scope` / `--section` (Requirement 4) + user-authored verbatim-preserve guard (Requirement 5) deferred to follow-up issue [#154](https://github.com/PsychQuant/issue-driven-development/issues/154)** after 3 verify iterations (R1/R2/R3 on PR #153) showed bash-incremental impl introduces new bugs each pass. AI/user invocation SHALL apply Requirements 4-5 as discipline (read spec + apply manually) until #154 ships runtime gate. Requirements 4-5 SHALL be re-verified for runtime conformance when #154 closes.
**Implementation status (v2.75.0)**: discipline declared at spec level + runtime enforcement landed for ALL categories. Runtime gates for `bounded-section-replace` (`/idd-update` REPLACE + `/idd-edit --replace --scope`/`--section` per R4), `state-field-update` (`/idd-clarify` status mutation, `spectra task done`), `audit-block-append` (5 IC_R011 PATCH sites), `inline-replace-before-publish` (`/idd-close` Step 3.5), `verbatim-preserve` (`/idd-edit` R5 author-check + override pathway), Path C authoritative-source gate logic across 4 gate sites. **`/idd-edit` Requirements 4 + 5 runtime gates landed via [#154](https://github.com/PsychQuant/issue-driven-development/issues/154)** through extracted helper `.claude/scripts/idd-edit-helper.sh` (parse-args / validate-target / section-replace subcommands) with 13 unit-test fixtures at `.claude/scripts/tests/idd-edit/` — closes R1/R2/R3 bash-inline parser bug class observed on PR #153.

## Requirements

Expand Down Expand Up @@ -113,7 +113,7 @@ code:
---
### Requirement: /idd-edit --replace SHALL require scope flag

**Implementation status (v2.74.0)**: discipline declared; runtime enforcement deferred to [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). AI / user invocations SHALL apply this requirement as discipline (include `--scope` / `--section` in invocation patterns) until #154 ships bash runtime gate. Scenarios below describe intended runtime behavior post-#154 — they are NOT currently enforced by `idd-edit/SKILL.md` (which is at pre-#150 baseline). Re-verify for runtime conformance when #154 closes.
**Implementation status (v2.75.0)**: landed via [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). `/idd-edit` Step 1 invokes `.claude/scripts/idd-edit-helper.sh parse-args` which enforces R4 gate (exit code 3 with actionable error message). Tested by fixture `10-replace-no-scope`. SKILL.md `## Runtime gates` section documents the gate matrix.

The `/idd-edit` skill SHALL require explicit scope when invoked with `--replace` mode. The skill SHALL accept either `--scope whole-comment` (explicit acknowledgment of full-comment overwrite) OR `--section <heading-within-comment>` (limit replacement to a named subsection within the comment). Invocations of `--replace` without either flag SHALL be refused. The `--append` and `--prepend-note` modes SHALL NOT require scope flags because their scope is inherently bounded (trailing block / leading errata marker respectively).

Expand Down Expand Up @@ -159,7 +159,7 @@ code:
---
### Requirement: /idd-edit SHALL refuse modifications to user-authored comments

**Implementation status (v2.74.0)**: discipline declared; runtime enforcement deferred to [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). AI / user invocations SHALL apply this requirement as discipline (include `--override-user-content --reason="..."` when intentionally editing non-OWNER non-bot comments) until #154 ships bash runtime gate + `/idd-comment` errata flow integration. Scenarios below describe intended runtime behavior post-#154 — NOT currently enforced by `idd-edit/SKILL.md` (pre-#150 baseline). Re-verify for runtime conformance when #154 closes.
**Implementation status (v2.75.0)**: landed via [#154](https://github.com/PsychQuant/issue-driven-development/issues/154). `/idd-edit` Step 1.5 invokes `.claude/scripts/idd-edit-helper.sh validate-target` which enforces R5 gate (exit code 4 with actionable error message + bot allowlist via `*[bot]` glob + OWNER passthrough + override pathway). `/idd-comment` errata Template auto-call handles exit 4 with helpful manual-invocation hint (D2 decision: refuse-with-message > auto-override, aligns with IC_R007 user-authored-intent spirit). Tested by fixtures `11-non-owner-no-override` (default OVERRIDE=false) + `12-non-owner-with-override` (override-pair guard) + `13-errata-refuse-message` (override+reason succeeds).

The `/idd-edit` skill SHALL refuse modifications targeting comments authored by users whose `author_association` is not `OWNER` and who are not in the known-bot allowlist (`github-actions[bot]`, `dependabot[bot]`, and other repo-configured bots). This protection SHALL apply to all three modes (`--append`, `--prepend-note`, `--replace`). Invocations targeting user-authored comments SHALL be refused unless the caller provides `--override-user-content` flag together with `--reason="<rationale>"` documenting the explicit decision to modify user content. This requirement is the comment-level instance of the `verbatim-preserve` category, aligned with IC_R007 (verbatim source preservation).

Expand Down
4 changes: 2 additions & 2 deletions plugins/issue-driven-dev/.claude-plugin/plugin.json

Large diffs are not rendered by default.

Loading