SpecRunnerDocs
ドキュメント再開と状態照合
RUNBOOK

再開と状態照合

停止したjobのidentity、journal、runner、worktreeを固定し、全ゲートを通過した場合だけ再開します。

このページの内容

再開可否を同じ順で判断する

job resumeは前回processの継続ではなく、新しいstep attemptを開始します。次の5ゲートを順に確認し、不明点を--forceで越えません。

ゲートPASSSTOP
1. identity対象Job ID・slug・branchが一意同じslugの非終端jobが複数、または対象が曖昧
2. integritydoctorとJob ID指定の確認が正常journal corruption、counter reversal、state読込不能
3. ownershipどのrunnerにも所有されていないprocess生存、別machine・CIの実行状態が不明
4. workspace通常worktree、または記録済み--no-worktree modeを確認mode不一致、worktree消失、detached HEAD、branch不一致
5. cause停止理由を解消し、開始stepを説明できる同じ条件の再実行、原因不明の--force

ページ内のresume commandは、この表がすべてPASSした後にだけ実行します。

slugではなくJob IDで対象を固定する

job show <slug>は同じslugが複数ある場合にupdatedAtが最新のjobを選びます。最初の一覧でJob IDを記録し、以後の確認を同じIDへ固定します。

terminal
npx specrunner job ls --all --json
npx specrunner job show <job-id>
PASSJob ID、slug、Status、Branch、Stepが対象変更と一致
STOP同じslugの非終端jobが複数ある、またはbranchが意図と違う

resumeの公開構文はslug指定です。slugが一意であることを確認してから、最後にjob resume <slug>を実行します。

doctorのintegrity failureを停止条件にする

slug検索は一覧性を優先して破損jobを読み取れる経路があり、persistにもjournalをfoldしないfast pathがあります。そのため「resumeが入口で必ず破損を拒否する」とは扱わず、doctorの結果を明示的なゲートにします。

terminal
npx specrunner doctor
npx specrunner job show <job-id>
PASSstorage診断が正常で、Job ID指定のshowにintegrity bannerがない
STOPどちらかがcorruption・truncation・counter reversalを示す

復元前に現在のjournalを保全する

破損fileを直接上書きしません。repository外のアクセス制限されたincident directoryへ現物とhashを残し、Git historyから候補を比較してから復元します。

terminal
mkdir -p <incident-dir>
cp <change-dir>/events.jsonl <incident-dir>/events.jsonl.corrupt
shasum -a 256 <incident-dir>/events.jsonl.corrupt
git log --oneline -- <repo-relative-events-path>
git show <candidate-ref>:<repo-relative-events-path> > <incident-dir>/events.candidate.jsonl
diff -u <incident-dir>/events.candidate.jsonl <incident-dir>/events.jsonl.corrupt

次のvalidatorは復元前だけに適用する、実装より厳しい運用ポリシーです。全非空行がJSON objectであること、transition・step-attempt数が現在のstate.json_journal counterを下回らないことを確認します。

terminal
node - <incident-dir>/events.candidate.jsonl <change-dir>/state.json <<'NODE'
const fs = require("node:fs");
const [eventsPath, statePath] = process.argv.slice(2);
const lines = fs.readFileSync(eventsPath, "utf8").split(/
?
/).filter((line) => line.trim());
const records = lines.map((line, index) => {
  let record;
  try { record = JSON.parse(line); }
  catch { throw new Error("invalid JSON at line " + (index + 1)); }
  if (record === null || typeof record !== "object" || Array.isArray(record)) {
    throw new Error("non-object record at line " + (index + 1));
  }
  return record;
});
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
const counters = state._journal;
if (!counters || typeof counters.historyCount !== "number" || !counters.stepCounts) {
  throw new Error("state.json has no usable _journal counters");
}
const transitions = records.filter((record) => record.type === "transition");
const stepCounts = {};
for (const record of records.filter((item) => item.type === "step-attempt")) {
  stepCounts[record.step] = (stepCounts[record.step] || 0) + 1;
}
if (transitions.length < counters.historyCount) {
  throw new Error("history counter reversal: " + transitions.length + " < " + counters.historyCount);
}
for (const [step, stored] of Object.entries(counters.stepCounts)) {
  const actual = stepCounts[step] || 0;
  if (actual < stored) throw new Error("step counter reversal for " + step + ": " + actual + " < " + stored);
}
console.log("PASS candidate journal", { records: records.length, lastTransition: transitions.at(-1) || null });
NODE

doctorより厳しく判定します 実装のfoldはinvalidな最終非空行をpartial tailとしてdropし、integrity checkは利用可能な_journal counterがなければreversal判定をskipします。この復元前validatorは、候補にその曖昧さを許可しません。

PASS candidate journalと最後の正常なtransitionを確認できた場合だけ復元します。正常revisionを特定できない、またはvalidatorが停止した場合は復元せず、保全したfileと診断出力をrepository maintainerへ渡します。

terminal
git restore --source=<verified-good-ref> -- <repo-relative-events-path>
npx specrunner doctor
npx specrunner job show <job-id>

stale表示だけでprocess消失と判断しない

local sidecarにはmachine-localなPIDとworktree pathがあります。同じhostではPIDを確認し、別machine・CI・schedulerではrun履歴とlockを確認して、active ownerがいないことを確定します。

terminal
cat .specrunner/local/<slug>/liveness.json
ps -p <pid> -o pid=,ppid=,etime=,command=
観測判断
PIDが生存STOP。runningの二重実行は--forceでも許可されない
PIDが不在他host・CI・schedulerにもactive runがないことを確認
sidecarがないSTOP。所有者不明としてrunner側を確認

job lsとjob resumeはsidecar不在時の判定が同じではありません job lsはsidecarがなければ15分のupdatedAt fallbackを使います。一方、resumeは期待sidecar pathを渡すため、missing・unreadableを即staleとして回復し得ます。fresh checkoutでの表示を安全根拠にしません。

実行modeごとにworkspaceを確認する

通常のworktree mode

sidecarのworktreePathを使い、directory、branch、未commit変更、change folderのstateを確認します。

terminal
git worktree list
git -C <recorded-worktree-path> status --short --branch
git -C <recorded-worktree-path> rev-parse --abbrev-ref HEAD
test -f <recorded-worktree-path>/specrunner/changes/<slug>/state.json
PASSpathが存在し、期待feature branchへ接続され、stateを読める
STOPpath消失、detached HEAD、branch不一致、state欠損
worktree消失時の自動再作成を運用手順にしません

現行revisionでは、stateが消失worktreeだけにあるjobは解決不能です。stateを解決できる場合も、再作成はfetchせずorigin/<base>のdetached worktreeを作り得ます。自動resumeせず、feature branchとstateの復元をrepository maintainerが行います。

--no-worktree mode

元runのstate.jsonnoWorktree: trueを確認し、stateがmain checkoutのchange folderにあること、現在branchが記録済みbranchと一致すること、working treeがcleanであることを確認します。

terminal
node -e 'const s=require("./specrunner/changes/<slug>/state.json"); console.log({jobId:s.jobId,noWorktree:s.noWorktree,branch:s.branch})'
git branch --show-current
git status --short
cat .specrunner/local/<slug>/liveness.json
PASSJob IDが一致し、noWorktree: true、branch一致、clean、sidecarのworktreePath: null
STOP通常worktreeのjobへflagを後付け、branch不一致、dirty、state・sidecar欠損

--no-worktreeは消失worktreeの回避flagではありません 元runが同じmodeの場合だけ、resumeにも--no-worktreeを付けます。通常worktree modeのjobをmain checkout上で継続する用途には使いません。

main checkout driftで停止した場合も自動revertされません。表示されたcreated / modified / deleted pathの所有者を決め、main checkoutを期待状態へ戻してから再度このゲートを確認します。

status・safety gateの後に開始stepを決める

--fromの優先順位は、statusと連続失敗guardを通過した後に適用されます。--fromを付けても、記録済みstepのguardは迂回できません。

順序評価結果
1status・running process遷移不可またはprocess生存なら停止
2記録済みstepの直近3 outcomeescalation / errorが3回連続なら停止
3--from <step>CLIが受理する登録済みstepなら開始位置に採用
4resumePoint.step--fromがなければ採用
5state.stepresumePointがないhard crashのfallback

Custom reviewer memberは、記録済みresumePointからの自動再開時にcoordinatorへmapされます。member名は公開CLIの--from候補ではないため、明示指定しません。

自動contextを証跡として再確認する

記録済みstepから戻る場合、前回attempt、verdict、findings path、停止理由、iteration情報が自動contextになります。ただしjournalのinterruption recordは消費されないため、後続のhard crashでは過去の停止理由が再materializeされる可能性があります。

通常resumestate.step、journal時刻、result fileと自動contextが一致するか確認
--fromで別step記録済みstep用の自動contextは渡らない
--prompt人間の追加noteを自動contextの後ろへ追加
--prompt-file長いnoteをfileから読み込む。--promptとは併用不可
追加promptもtrusted inputです

内容はagent promptへ直接注入されます。Issue comment、chat、外部ticketの未信頼本文をそのまま渡さず、操作者が確認した事実と判断だけを記述します。

全ゲート通過後に再開する

terminal
# 記録済みstepから再開
npx specrunner job resume <slug>

# 元runがnoWorktree: trueの場合だけ
npx specrunner job resume <slug> --no-worktree

# 開始stepを意図的に変更
npx specrunner job resume <slug> --from code-review

# 確認済みの追加noteを渡す
npx specrunner job resume <slug> --prompt-file ./resume-note.md
ケース操作
awaiting-resume・全ゲートPASS通常resume
dead processのrunning・ownership確認済み通常resumeでstale回復
hard crash・resumePointなしstate.stepとjournalを照合して通常resume
元runがnoWorktree: truemain checkout gate通過後に--no-worktree付きでresume
開始位置を変える影響範囲を確認して--from
3回連続のescalation / error原因を変更後、必要時だけ--force
journal・ownership・worktreeのいずれか不明resumeしない
archived / canceled別slugのrequestとして開始

現行revisionで運用上の前提にしない挙動

  • slug指定のresumeが、入口で必ずjournal corruptionを拒否すること
  • job lsjob resumeが同じstale判定を返すこと
  • 消失したworktreeが、元のfeature branchへ安全に自動復元されること
  • journal由来のresumePointが、常に直近processの停止理由だけを表すこと

これらは自動回復へ委ねず、このrunbookの明示ゲートで補います。実行証跡の読み方は監査と証跡を参照してください。

参照ソース 11