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
8 changes: 4 additions & 4 deletions .specgit.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
version: 1
delivery: event-idempotency-gate
delivery: specgit-bootstrap-wrapper
context:
kind: branch
branch: fix/523-event-idempotency-gate
branch: feat/521-specgit-bootstrap-wrapper
issues:
- 523
pr: 527
- 521
pr: 532
11 changes: 10 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ feat/**, fix/** ──PR(Typecheck + Unit Tests 门禁)──▶ dev ──push
新功能开发、Debug 等一切交付范畴恒定走此循环;后续所有工作必须遵守该方案,不得另起流程:

1. **确立条目**:明确条目的内容、范围、类型(`feat`/`fix`/…)。一个 issue = 一个可独立验证的 WHY,无法独立验证的先拆分再立项。
2. **SpecGit 立项**:`specgit issue <title-or-number>` 创建/复用 issues 批次,确立交付分支与草稿 PR 脚手架(`.specgit.yaml` 绑定);立项前先查重,避免同一 WHY 双开。
2. **SpecGit 立项**:`script/specgit-bootstrap.sh <title-or-number>` 创建/复用 issues 批次,确立交付分支与草稿 PR 脚手架(`.specgit.yaml` 绑定);立项前先查重,避免同一 WHY 双开。wrapper 是 canonical 入口(见 "SpecGit harness local specializations");直跑裸 `specgit issue` 预期被 harness currency gate 以 `harness_stale` (exit 2) 拒绝
3. **超流执行**:安排 DAG workflow(超流)承载实现——并行开发 + 多角度 Review + 复合(synthesize),其产出作为交付证据基线。
4. **PR 过门禁**:SpecGit 发起/推进 PR,过 TDD 与 CI 门禁(Typecheck、Unit Tests、DAG gate;`specgit finish` exit 0 是唯一 "done")。
5. **修复门禁问题**:门禁失败在交付分支修代码/测试,永远不削弱门禁本身。
Expand Down Expand Up @@ -273,6 +273,15 @@ Kept OUTSIDE the managed block so `specgit init`/`--force` never rewrites them;
- The wait script hand-parses `spec_git/policy.yaml` (minimal line-based parse) instead of importing the `yaml` package: no root-reachable `yaml` exists under workspace catalog isolation, so `import { parse } from 'yaml'` would fail to resolve on the runner.
- `spec_git/policy.yaml` `required_checks` uses the template's canonical check IDs (`unit-tests`, `e2e-tests`), not display names.

#### specgit-bootstrap wrapper (canonical `specgit issue` entry, #521)

`script/specgit-bootstrap.sh <specgit issue args...>` is THE canonical way to run `specgit issue` in this repository. Bare `specgit issue` is expected to fail with `harness_stale` (exit 2) whenever the pinned CLI's harness template moves — the wrapper satisfies that gate safely: it snapshots the full init write surface to a temp dir outside the repo, runs `specgit init --force --no-protect` (hardcoded, offline), then `specgit issue "$@"` with arguments, exit status, and diagnostics passed through verbatim, and restores the specialized bytes above on success and every failure path (EXIT/INT/TERM/HUP), verifying each file byte-for-byte via `git hash-object`.

- Never run bare `specgit init --force` here: it overwrites the six specialized bytes; the wrapper exists to make that refresh transient.
- Fail-closed rejections: dirty write-surface paths (tracked/staged/untracked) → exit 2 with the offending paths listed; no SpecGit binding (`.specgit.yaml` or `spec_git/policy.yaml` missing) → exit 3; restore hash mismatch → exit 3 with the snapshot kept for forensics. Rejection paths print plain `specgit-bootstrap:` stderr lines and NEVER produce a `--json` envelope.
- The inner `.specgit.yaml` written by `specgit issue` is a legitimate binding artifact and is never rolled back.
- Managed-block guidance referencing bare `specgit issue` commands is superseded by this section for this repository. Behavior tests: `bash script/specgit-bootstrap.test.sh` (stubbed CLI, zero network; not CI-wired).

<!-- specgit:block:start -->
## SpecGit delivery harness

Expand Down
150 changes: 150 additions & 0 deletions script/specgit-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/bin/sh
# specgit-bootstrap — repository-local fail-safe wrapper around `specgit issue` (#521).
#
# Why: `specgit issue` (1.10.1) runs an unconditional harness-currency gate that
# exits 2 (`harness_stale`) unless the managed harness was refreshed by
# `specgit init --force`. But `init --force` overwrites this repository's six
# hand-applied specializations (see AGENTS.md, "SpecGit harness local
# specializations"). This wrapper makes the refresh safe:
#
# 1. refuses to run when any init write-surface path has uncommitted changes
# (tracked, staged, or untracked), or when the repo has no SpecGit binding;
# 2. snapshots every existing write-surface path to a temp directory OUTSIDE
# the repository, recording each file's `git hash-object` content hash;
# 3. runs `specgit init --force --no-protect` (hardcoded, offline; init's
# stdout prose is routed to stderr so a wrapped `--json` call's stdout
# stays exactly one JSON document) then `specgit issue "$@"` with all
# arguments preserved verbatim and stdin/stdout/stderr inherited;
# 4. restores the snapshots on every exit path (EXIT/INT/TERM/HUP) and
# verifies each restored file byte-for-byte against the recorded hash;
# any mismatch is reported loudly and exits 3.
#
# The `.specgit.yaml` binding written/updated by the inner `specgit issue` is a
# legitimate delivery artifact and is never rolled back. Wrapper rejections
# print plain stderr lines prefixed `specgit-bootstrap:` — never a `--json`
# envelope; only the inner CLI receives the wrapped arguments.
#
# Usage: script/specgit-bootstrap.sh <specgit issue args...>
#
# Write surface below mirrors specgit 1.10.1 harness-placement; it is
# version-coupled to the pinned CLI in .github/workflows/specgit-accept.yml.

set -u

SURFACE='
.github/workflows/specgit-accept.yml
AGENTS.md
CLAUDE.md
.opencode/hooks.json
.opencode/hooks/specgit-merge-guard.sh
.git/hooks/pre-push
.husky/_/pre-push
'

say() {
printf 'specgit-bootstrap: %s\n' "$1" >&2
}

REPO=$(git rev-parse --show-toplevel 2>/dev/null) || {
say "not inside a git repository"
exit 3
}
cd "$REPO" || exit 3

# Fail-closed: serve only bound delivery repositories; a fresh repo has no
# specializations to protect, so bare `specgit issue` is fine there.
if [ ! -f .specgit.yaml ] || [ ! -f spec_git/policy.yaml ]; then
say "no SpecGit binding (.specgit.yaml / spec_git/policy.yaml missing) - run bare 'specgit issue' instead"
exit 3
fi

# Fail-closed: ambiguous pre-existing changes on the write surface could be
# clobbered by init and could not be told apart from init's own writes.
# shellcheck disable=SC2086
dirty=$(git status --porcelain -- $SURFACE)
if [ -n "$dirty" ]; then
say "refusing to run - init write-surface paths have uncommitted changes (inner CLI NOT executed):"
printf '%s\n' "$dirty" | sed 's/^/ /' >&2
say "commit or stash those changes first, then retry"
exit 2
fi

SNAP=$(mktemp -d "${TMPDIR:-/tmp}/specgit-bootstrap.XXXXXX") || {
say "cannot create snapshot directory under \${TMPDIR:-/tmp}"
exit 3
}
mkdir "$SNAP/tree" "$SNAP/hashes" || {
rm -rf "$SNAP"
say "cannot prepare snapshot directory layout"
exit 3
}

# shellcheck disable=SC2086
for rel in $SURFACE; do
[ -f "$rel" ] || continue
mkdir -p "$SNAP/tree/$(dirname "$rel")" "$SNAP/hashes/$(dirname "$rel")" || {
rm -rf "$SNAP"
say "cannot stage snapshot for $rel"
exit 3
}
cp "$rel" "$SNAP/tree/$rel" || {
rm -rf "$SNAP"
say "snapshot copy failed for $rel"
exit 3
}
git hash-object -- "$rel" > "$SNAP/hashes/$rel" || {
rm -rf "$SNAP"
say "content hash failed for $rel"
exit 3
}
done

RESTORED=0

# Idempotent restore + byte verification. On mismatch the snapshot directory
# is KEPT for forensics and the wrapper exits 3 (fail-closed, aligning with
# the CLI's exit contract for "cannot proceed").
restore_all() {
[ "$RESTORED" -eq 1 ] && return 0
RESTORED=1
mismatched=0
# shellcheck disable=SC2086
for rel in $SURFACE; do
[ -f "$SNAP/tree/$rel" ] || continue
cp "$SNAP/tree/$rel" "$rel"
now=$(git hash-object -- "$rel" 2>/dev/null)
want=$(cat "$SNAP/hashes/$rel" 2>/dev/null)
if [ "$now" != "$want" ]; then
printf 'specgit-bootstrap: RESTORE MISMATCH for %s (got %s, expected %s)\n' \
"$rel" "${now:-<none>}" "${want:-<none>}" >&2
mismatched=1
fi
done
if [ "$mismatched" -eq 1 ]; then
say "restored bytes differ from pre-run snapshots - specialized harness bytes may be corrupted."
say "snapshot kept for forensics at $SNAP; inspect 'git diff' before continuing."
trap - EXIT
exit 3
fi
rm -rf "$SNAP"
}

trap 'restore_all' EXIT
trap 'restore_all; exit 129' HUP
trap 'restore_all; exit 130' INT
trap 'restore_all; exit 143' TERM

# init's stdout prose must not pollute the wrapped --json parse surface.
specgit init --force --no-protect >&2
init_status=$?
if [ "$init_status" -ne 0 ]; then
say "specgit init --force --no-protect failed (exit $init_status); restoring harness bytes"
restore_all
exit "$init_status"
fi

# All arguments pass through verbatim; exit status and diagnostics inherit.
specgit issue "$@"
issue_status=$?
restore_all
exit "$issue_status"
Loading
Loading