---
phase: 24.1
plan: 01
subsystem: foundation-primitives
tags: [schema, types, git, time, foundation]
one_liner: "Phase 24.1 foundation primitives — `now_iso_utc()`, `hostname()` pub(crate), `head_branch_or_empty()`, `TrackedAgentInfo` + `ProjectHistoryEntry` + `MachineHistoryEntry` schema, `InfoJson.project_history` upgraded to `Vec<ProjectHistoryEntry>`"
requires:
  - phase: 24
    plan: "*"
    note: "tracked worktree primitives (ensure_agent_worktree, agent_worktree_path, atomic_write_string, commit pipeline) — pre-existing"
  - phase: 32
    plan: "*"
    note: "Phase 32 D-08 project_history Vec<String> shape — upgraded in place"
provides:
  - "`pub(crate) fn now_iso_utc() -> String` at src/common/time.rs:32 — crate-wide ISO-8601 UTC seconds-precision formatter (`YYYY-MM-DDTHH:MM:SSZ`)"
  - "`pub(crate) fn hostname() -> String` at src/common/git.rs:326 — promoted from private; cross-module callable from owlery/tracked"
  - "`pub(crate) fn head_branch_or_empty(cwd: &Path) -> String` at src/common/git.rs:379 — sole call-site abstraction for project_history branch capture; soft-fails to \"\" on detached HEAD / non-repo / missing git / nonexistent path"
  - "`pub struct MachineHistoryEntry { name, first_seen, last_seen }` at src/common/types.rs:39"
  - "`pub struct ProjectHistoryEntry { name, branch, first_seen, last_seen }` at src/common/types.rs:56 — shared element type for BOTH perch InfoJson.project_history AND TrackedAgentInfo.project_history"
  - "`pub struct TrackedAgentInfo` at src/common/types.rs:77 — field-order-locked on-disk shape for tracked info.json"
  - "`InfoJson.project_history: Vec<ProjectHistoryEntry>` at src/common/types.rs:130 — upgraded from Vec<String>; legacy payloads tolerated via serde_json::Value round-trip (Plan 02 wires the normalize helper)"
affects:
  - "src/common/tracked.rs:979 — append_session_entry now routes timestamp through `crate::common::time::now_iso_utc` (sessions.log byte-format unchanged)"
tech_stack:
  added: []
  patterns:
    - "preserve_order + declaration-order field locking on the new TrackedAgentInfo struct"
    - "#[serde(default)] + skip_serializing_if for legacy-tolerant Option and Vec fields"
    - "soft-fail silent-on-error posture on every git shell-out (returns \"\" or default)"
    - "hide_window guard on every git Command invocation (Windows CREATE_NO_WINDOW)"
key_files:
  created: []
  modified:
    - "src/common/time.rs (+now_iso_utc, +2 unit tests)"
    - "src/common/tracked.rs (-private now_iso, +use crate::common::time::now_iso_utc)"
    - "src/common/git.rs (hostname → pub(crate); +head_branch_or_empty, +5 unit tests)"
    - "src/common/types.rs (+MachineHistoryEntry, +ProjectHistoryEntry, +TrackedAgentInfo; InfoJson.project_history type upgrade; +6 unit tests)"
decisions:
  - "Add #[allow(dead_code)] to head_branch_or_empty until Plan 02 wires the first caller — matches Phase 23 _at-variant precedent for foundation symbols introduced ahead of their first call site"
  - "Pin Task 1 wire format with parse-back via chrono::DateTime::parse_from_rfc3339 + length/suffix/no-dot/no-plus invariants, not a regex dep — keeps Cargo.toml clean"
  - "Task 2 init_repo_with_main test helper uses git -B main to force a deterministic branch regardless of host's init.defaultBranch config"
  - "Task 3 typed-InfoJson round-trip will REJECT legacy Vec<String> payloads cleanly — acceptable because every production read of legacy data goes through the Value-fallback path (Plan 02's normalize_legacy_strings_to_objects)"
metrics:
  duration_minutes: 12
  tasks_completed: 3
  files_modified: 4
  files_created: 0
  test_count_delta: 13   # 2 (Task 1) + 5 (Task 2) + 6 (Task 3)
  lib_test_count_after: 666
completed: 2026-05-21
---

# Phase 24.1 Plan 01: Foundation Primitives Summary

## One-liner

Phase 24.1 foundation primitives — `now_iso_utc()`, `hostname()` pub(crate), `head_branch_or_empty()`, `TrackedAgentInfo` + `ProjectHistoryEntry` + `MachineHistoryEntry` schema, `InfoJson.project_history` upgraded to `Vec<ProjectHistoryEntry>`.

## What Was Built

Three foundation primitives every downstream Phase 24.1 plan depends on:

### Task 1 — Shared ISO-8601 UTC timestamp (commit `52f2218`)

- **`pub(crate) fn now_iso_utc()`** at `src/common/time.rs:32` returns `"YYYY-MM-DDTHH:MM:SSZ"` (20 chars, seconds-precision, literal `Z` suffix, no fractional seconds, no `+00:00` offset).
- Body lifted verbatim from the private `tracked::now_iso()` (deleted) so the byte-shape used by Phase 24 `sessions.log` is identical to Phase 24.1 tracked `info.json`. RESEARCH Pitfall 2 + Pitfall 6 satisfied.
- `format_timestamp` (local-time + PDT/PST) is preserved as-is for the Phase 23 perch `info.json::started` field carryover contract.
- 2 unit tests pin the wire format via byte-length + suffix + no-dot + parse-back, plus a monotonic-prefix invariant.

### Task 2 — `git.rs` helpers (commit `a9874cf`)

- **`pub(crate) fn hostname()`** at `src/common/git.rs:326` — visibility promotion (was `fn hostname()`). Body unchanged. RESEARCH Pitfall 3 (cross-module callable from owlery/tracked).
- **`pub(crate) fn head_branch_or_empty(cwd: &Path) -> String`** at `src/common/git.rs:379` — sole call-site abstraction for `project_history.branch` capture. Uses `git symbolic-ref --short -q HEAD` (NOT `rev-parse --abbrev-ref` — that returns literal `"HEAD"` on detached HEAD per Pitfall 1). Soft-fails to `String::new()` on non-zero exit / spawn failure / non-UTF-8 stdout / no-git / no-repo / nonexistent path. `hide_window` guard for Windows CREATE_NO_WINDOW.
- 5 unit tests: main-branch repo, detached HEAD via `--detach <sha>`, non-repo tempdir, nonexistent path, hostname callability.
- `#[allow(dead_code)]` on `head_branch_or_empty` until Plan 02 wires the first caller (matches Phase 23 `_at`-variant precedent).

### Task 3 — Schema types + `InfoJson.project_history` upgrade (commit `b2c5597`)

- **`MachineHistoryEntry`** at `src/common/types.rs:39` — `{name, first_seen, last_seen}`. Element of `TrackedAgentInfo.machine_history` only.
- **`ProjectHistoryEntry`** at `src/common/types.rs:56` — `{name, branch, first_seen, last_seen}`. Shared element type for BOTH perch `InfoJson.project_history` AND `TrackedAgentInfo.project_history` — shape-equality satisfies ROADMAP SC5 by construction (D-05).
- **`TrackedAgentInfo`** at `src/common/types.rs:77` — field-declaration order locked via serde_json's `preserve_order`: `agent_id`, `last_started`, `last_machine_name`, `last_project_name` (Option, `skip_serializing_if`), `machine_history`, `project_history` (both `#[serde(default)]`). `#[derive(..., Default)]` for migration synth paths.
- **`InfoJson.project_history`** type upgraded from `Vec<String>` to `Vec<ProjectHistoryEntry>` at `src/common/types.rs:130`. `#[serde(default, skip_serializing_if = "Vec::is_empty")]` preserved. Constructor's `Vec::new()` is type-inferred to the new element type — no constructor change. Legacy `Vec<String>` payloads are tolerated on READ via `serde_json::Value` round-trip (Plan 02's `normalize_legacy_strings_to_objects` helper does the in-place migration).
- 6 unit tests: default-construct invariant, `last_project_name == None` JSON-key omission, field-order substring positions, legacy `Vec<String>` Value-round-trip OK, new-shape typed round-trip OK, missing-key defaults to empty.

## Verification

- `cargo build --release`: green, 3 pre-existing warnings unchanged (none introduced by this plan).
- `cargo test --lib -- --test-threads=1`: **666 passed, 0 failed, 3 ignored** (baseline + 13 new tests).
- `cargo test --lib common::tracked::tests::compose_session_line_locked_field_order`: PASS — proves the `now_iso_utc` formatter swap preserved the Phase 24 `sessions.log` byte-level contract.

## Test Counts

| Task | Tests added | Cumulative |
|------|-------------|------------|
| 1    | 2           | 658        |
| 2    | 5           | 663        |
| 3    | 6           | 666        |

(Baseline at plan start: 653 — confirmed by SC numerical delta in the file-test count progression. Plan-introduced surface: 13.)

## Cargo Warnings

| Warning | Resolution |
|---------|-----------|
| `head_branch_or_empty` is never used | Added `#[allow(dead_code)]` to the helper with a doc-comment noting Plan 02 will wire the first caller. Matches Phase 23 `_at`-variant precedent. |

No other warnings introduced. The 3 pre-existing warnings (`check_alive`, `source`, `should_fire`) are out-of-scope per the executor's SCOPE BOUNDARY rule.

## Deviations from Plan

None — plan executed exactly as written.

The `#[allow(dead_code)]` annotation on `head_branch_or_empty` is anticipated by the plan's `done` criterion ("no warnings about unused imports") and is a single-symbol allow consistent with Phase 23 precedent — not a deviation.

## Authentication Gates

None encountered.

## Known Stubs

None. All three primitives are fully implemented with unit-test coverage. `head_branch_or_empty` is callable end-to-end; Plan 02 will add the first call site.

## Threat Flags

None — no new network endpoints, no new auth paths, no new schema surface at a trust boundary. The new `git symbolic-ref` shell-out matches the existing `hostname()` posture and is bounded by the same trust assumptions documented in `<threat_model>` (T-24.1-01 through T-24.1-SC).

## Self-Check: PASSED

- `src/common/time.rs` exists; `now_iso_utc` defined at line 32. ✓
- `src/common/git.rs` exists; `hostname` is `pub(crate)` at line 326; `head_branch_or_empty` at line 379. ✓
- `src/common/types.rs` exists; `MachineHistoryEntry` at 39, `ProjectHistoryEntry` at 56, `TrackedAgentInfo` at 77; `InfoJson.project_history: Vec<ProjectHistoryEntry>` at 130. ✓
- Commits exist in git log: 52f2218, a9874cf, b2c5597. ✓
- `cargo build --release` green. ✓
- 666 lib tests pass. ✓
