再開と状態照合
停止したjobのidentity、journal、runner、worktreeを固定し、全ゲートを通過した場合だけ再開します。
このページの内容
再開可否を同じ順で判断する
job resumeは前回processの継続ではなく、新しいstep attemptを開始します。次の5ゲートを順に確認し、不明点を--forceで越えません。
| ゲート | PASS | STOP |
|---|---|---|
| 1. identity | 対象Job ID・slug・branchが一意 | 同じslugの非終端jobが複数、または対象が曖昧 |
| 2. integrity | doctorと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へ固定します。
npx specrunner job ls --all --json
npx specrunner job show <job-id>resumeの公開構文はslug指定です。slugが一意であることを確認してから、最後にjob resume <slug>を実行します。
doctorのintegrity failureを停止条件にする
slug検索は一覧性を優先して破損jobを読み取れる経路があり、persistにもjournalをfoldしないfast pathがあります。そのため「resumeが入口で必ず破損を拒否する」とは扱わず、doctorの結果を明示的なゲートにします。
npx specrunner doctor
npx specrunner job show <job-id>復元前に現在のjournalを保全する
破損fileを直接上書きしません。repository外のアクセス制限されたincident directoryへ現物とhashを残し、Git historyから候補を比較してから復元します。
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を下回らないことを確認します。
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 });
NODEdoctorより厳しく判定します。 実装のfoldはinvalidな最終非空行をpartial tailとしてdropし、integrity checkは利用可能な_journal counterがなければreversal判定をskipします。この復元前validatorは、候補にその曖昧さを許可しません。
PASS candidate journalと最後の正常なtransitionを確認できた場合だけ復元します。正常revisionを特定できない、またはvalidatorが停止した場合は復元せず、保全したfileと診断出力をrepository maintainerへ渡します。
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がいないことを確定します。
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を確認します。
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現行revisionでは、stateが消失worktreeだけにあるjobは解決不能です。stateを解決できる場合も、再作成はfetchせずorigin/<base>のdetached worktreeを作り得ます。自動resumeせず、feature branchとstateの復元をrepository maintainerが行います。
--no-worktree mode
元runのstate.jsonでnoWorktree: trueを確認し、stateがmain checkoutのchange folderにあること、現在branchが記録済みbranchと一致すること、working treeがcleanであることを確認します。
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.jsonnoWorktree: true、branch一致、clean、sidecarのworktreePath: null--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は迂回できません。
| 順序 | 評価 | 結果 |
|---|---|---|
| 1 | status・running process | 遷移不可またはprocess生存なら停止 |
| 2 | 記録済みstepの直近3 outcome | escalation / errorが3回連続なら停止 |
| 3 | --from <step> | CLIが受理する登録済みstepなら開始位置に採用 |
| 4 | resumePoint.step | --fromがなければ採用 |
| 5 | state.step | resumePointがない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される可能性があります。
--fromで別step記録済みstep用の自動contextは渡らない--prompt人間の追加noteを自動contextの後ろへ追加--prompt-file長いnoteをfileから読み込む。--promptとは併用不可内容はagent promptへ直接注入されます。Issue comment、chat、外部ticketの未信頼本文をそのまま渡さず、操作者が確認した事実と判断だけを記述します。
全ゲート通過後に再開する
# 記録済み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: true | main checkout gate通過後に--no-worktree付きでresume |
| 開始位置を変える | 影響範囲を確認して--from |
| 3回連続のescalation / error | 原因を変更後、必要時だけ--force |
| journal・ownership・worktreeのいずれか不明 | resumeしない |
archived / canceled | 別slugのrequestとして開始 |
現行revisionで運用上の前提にしない挙動
- slug指定のresumeが、入口で必ずjournal corruptionを拒否すること
job lsとjob resumeが同じstale判定を返すこと- 消失したworktreeが、元のfeature branchへ安全に自動復元されること
- journal由来のresumePointが、常に直近processの停止理由だけを表すこと
これらは自動回復へ委ねず、このrunbookの明示ゲートで補います。実行証跡の読み方は監査と証跡を参照してください。
参照ソース 11件
src/core/command/resume.tsresumeの検証・状態遷移src/core/resume/resolve-step.ts再開stepの解決順src/core/resume/safety.tsstale・連続失敗guardsrc/core/resume/resume-context.ts自動・人間入力contextsrc/store/job-state-projection.tsjournalからresumePointを構成src/store/job-catalog.tsslug・worktree state探索src/store/job-journal.tsjournal persist pathsrc/cli/ps.tsjob lsのstale判定src/core/runtime/local.tsno-worktree setup・clean checksrc/core/runtime/workspace-materializer.tsworktree再利用・再作成src/cli/command-registry.tsresume flags・prompt警告