import fs from 'fs'; const p = 'C:/Users/decid/Documents/projects/claude_skill_owl/.planning/phases/19-sendkeys-foundation-capsule-launch/19-02-PLAN.md'; let s = fs.readFileSync(p, 'utf8'); const original = s; // 1. Replace collision truth const truthOld = ' - "agent_id collision with an existing non-capsule perch emits `COLLISION:` and exits 1 (Pitfall 7)"'; const truthNew = ' - "agent_id collision (existing live non-capsule perch OR existing live capsule perch when capsule.bat did not route through reattach) auto-suffixes -1, -2, ..., up to -99 per D18/CLNCH-09; resolved id is echoed to stdout as `RESOLVED_AGENT_ID: `; only if all 100 candidates (base + 99 suffixes) are live does it emit `COLLISION:` and exit 1"\n - "`--resume` path is NOT subject to suffixing — it resolves to the literal agent_id passed in"'; if (!s.includes(truthOld)) throw new Error('truthOld not found'); s = s.replace(truthOld, truthNew); // 2. Replace Test 3 const t3Old = ' - Test 3: `owl capsule alpha` where owlery/alpha/info.json already exists with `state: listener` and a LIVE pid emits `COLLISION:alpha` to stderr and exits 1 (Pitfall 7).'; const t3New = ' - Test 3 (CLNCH-09 / D18 auto-suffix): `owl capsule alpha` where owlery/alpha/info.json already exists with `state: listener` and a LIVE pid does NOT fail — it tries `alpha-1`, finds it free, writes the skeleton perch at owlery/alpha-1/info.json, and prints `RESOLVED_AGENT_ID: alpha-1` on stdout. The resolved id is what capsule.bat uses for subsequent psmux commands.\n - Test 3b (suffix chaining): With owlery/alpha plus owlery/alpha-1 and owlery/alpha-2 all occupied by live perches, `owl capsule alpha` resolves to `alpha-3` and writes that perch.\n - Test 3c (exhaustion): With owlery/alpha and owlery/alpha-1..alpha-99 all live, `owl capsule alpha` emits `COLLISION:alpha` to stderr and exits 1.'; if (!s.includes(t3Old)) throw new Error('t3Old not found'); s = s.replace(t3Old, t3New); // 3. Replace the collision check block const step3Old = ` // 3. Collision check — existing non-capsule perch alive let perch = owlery::perch_dir(agent_id); let info_path = owlery::info_file(agent_id); if info_path.exists() { if let Ok(content) = fs::read_to_string(&info_path) { if let Ok(info) = serde_json::from_str::(&content) { let alive = match info.pid { PidValue::Numeric(p) => types::is_process_alive(p), PidValue::Busy(_) => true, }; if alive && info.state != PerchState::Capsule { output::owl_err(&format!("COLLISION:{}", agent_id)); std::process::exit(1); } // If state == Capsule and alive: reattach path — capsule.bat // handles this via has-session before calling owl capsule, but // defensively: just skip re-writing and exit 0 (idempotent). if alive && info.state == PerchState::Capsule { println!("Reactivating spacetime capsule for agent '{}'", agent_id); return; } } } }`; const step3New = ` // 3. Collision check + auto-suffix (CLNCH-09 / D18). // Probe agent_id, then agent_id-1 through agent_id-99. // A slot is FREE if info.json is absent/corrupt OR the recorded pid is // dead (or 0 — the Plan 01 skeleton pid-pending value). A slot is // OCCUPIED if alive, regardless of state — reattach to an already-live // capsule is capsule.bat's responsibility via psmux has-session (D10), // so owl capsule called fresh means capsule.bat already confirmed no // existing psmux session. Resolved id is echoed to stdout as // "RESOLVED_AGENT_ID: " so capsule.bat uses it for psmux naming. let resolved_id = match resolve_agent_id(agent_id) { Some(r) => r, None => { output::owl_err(&format!("COLLISION:{}", agent_id)); std::process::exit(1); } }; // Always emit the resolved id so capsule.bat's parser is uniform. println!("RESOLVED_AGENT_ID: {}", resolved_id); let agent_id_owned = resolved_id; let agent_id: &str = &agent_id_owned; // shadow: downstream uses resolved id let perch = owlery::perch_dir(agent_id); let info_path = owlery::info_file(agent_id);`; if (!s.includes(step3Old)) throw new Error('step3Old not found'); s = s.replace(step3Old, step3New); // 4. Insert resolve_agent_id helper before spawn_capsule_listen const helperMarker = ` #[cfg(windows)] fn spawn_capsule_listen(agent_id: &str) {`; const helperInsert = ` /// CLNCH-09 / D18: try base, then base-1 through base-99. Return the first /// id whose perch is absent or whose recorded pid is dead (0 counts as dead /// — Plan 01 skeleton placeholder). Return None if all 100 candidates are /// occupied by live perches. fn resolve_agent_id(base: &str) -> Option { for n in 0..=99u32 { let candidate = if n == 0 { base.to_string() } else { format!("{}-{}", base, n) }; let info_path = owlery::info_file(&candidate); if !info_path.exists() { return Some(candidate); } let Ok(content) = fs::read_to_string(&info_path) else { return Some(candidate); }; let Ok(info) = serde_json::from_str::(&content) else { return Some(candidate); }; let alive = match info.pid { PidValue::Numeric(0) => false, PidValue::Numeric(p) => types::is_process_alive(p), PidValue::Busy(_) => true, }; if !alive { return Some(candidate); } } None } #[cfg(windows)] fn spawn_capsule_listen(agent_id: &str) {`; if (!s.includes(helperMarker)) throw new Error('helperMarker not found'); s = s.replace(helperMarker, helperInsert); // 5. Update acceptance criteria const accOld = " - `grep -n 'COLLISION:' src/owl/capsule.rs` returns a match (Pitfall 7 mitigation present)"; const accNew = " - `grep -n 'COLLISION:' src/owl/capsule.rs` returns a match (exhaustion-only fallback after suffix loop)\n - `grep -n 'fn resolve_agent_id' src/owl/capsule.rs` returns a match (CLNCH-09 / D18 auto-suffix helper)\n - `grep -n 'RESOLVED_AGENT_ID' src/owl/capsule.rs` returns a match (resolved id echoed to stdout for capsule.bat)"; if (!s.includes(accOld)) throw new Error('accOld not found'); s = s.replace(accOld, accNew); // 6. Update test count in acceptance s = s.replace('`cargo test --test capsule_integration` exits 0 with all 6 tests passing', '`cargo test --test capsule_integration` exits 0 with all 8 tests passing (1, 2, 3, 3b, 3c, 4, 5, 6)'); // 7. Update test creation instruction s = s.replace('Create `tests/capsule_integration.rs` with the six test cases from ``.', 'Create `tests/capsule_integration.rs` with the eight test cases from `` (Tests 1, 2, 3, 3b, 3c, 4, 5, 6 — 3b and 3c exercise the CLNCH-09 suffix loop and its exhaustion branch).'); // 8. Update threat T-19-07 const t07Old = "| T-19-07 | Tampering | Capsule listener overwrites a non-capsule perch | mitigate | `owl capsule` checks `state != PerchState::Capsule && alive` → COLLISION exit (Task 1, Test 3). Listener itself (Task 2) reuses poll.rs DUPLICATE guard against same-state active listeners. |"; const t07New = "| T-19-07 | Tampering | Capsule listener overwrites a non-capsule perch | mitigate | `owl capsule::resolve_agent_id` skips any slot whose info.json shows a live perch (Task 1, Tests 3/3b). Auto-suffix lands the new capsule on the next free id per D18/CLNCH-09. Only if all 100 candidates are live does it exit COLLISION. Listener itself (Task 2) reuses poll.rs DUPLICATE guard against same-state active listeners. |"; if (!s.includes(t07Old)) throw new Error('t07Old not found'); s = s.replace(t07Old, t07New); // 9. Total test count references s = s.replace('all 13 tests from Tasks 1+2 pass', 'all 15 tests from Tasks 1+2 pass (8 from Task 1 including CLNCH-09 suffix cases, 7 from Task 2)'); s = s.replace('Release build green; all 13 integration tests pass.', 'Release build green; all 15 integration tests pass.'); if (s === original) throw new Error('No changes applied'); fs.writeFileSync(p, s); console.log('Plan 02 updated: ' + (s.length - original.length) + ' char delta');