---ci--- project: orca phase: 3 milestone: v0.3 status: complete requirements: covered: [REQ-022, REQ-030, REQ-032] partial: [] ---/ci--- v0.3 milestone merged to main. Includes all v0.2 work (P08-P10) that was previously on the milestone branch but not yet merged to main, plus the v0.3 completion work (iter.Seq streaming + doctor network/db). v0.2 phases included: P08 (mTLS), P09 (scheduling), P10 (security scan). v0.3 phases: P0 (pre-execution), P1 (iter.Seq streaming), P2 (doctor), P3 (final review+ship). Total: 40 requirements, all complete. No new go.mod dependencies. Full test suite passes under -race. gofmt + go vet clean.
28 KiB
Plan: Orca v0.3 — scheduling-streaming
Milestone v0.3 (scheduling-streaming) — completion milestone closing the two work items deferred from v0.2 (iter.Seq streaming + doctor network/db). Two execution phases (P01, P02) followed by one final phase (P03 review + ship).
Branch: phase/00-pre-execution (cut from milestone/v0.3-scheduling-streaming).
Go toolchain: go1.25.0 (iter package + range-over-func are stable stdlib).
No new go.mod dependencies (D-041). Source of implementation guidance:
.ciagent/RESEARCH_v0.3.md (D-025..D-042).
Phase P01: iter.Seq streaming for --watch flags
Goal: Add pull-based iter.Seq streaming to orca job list and orca node list behind a --watch flag, with table refresh (default) or streaming one-line JSON per event (--watch --json).
Requirements: REQ-022 (iter.Seq for streaming job lists, Go 1.25+), REQ-030 (--watch output format: table default vs streaming one-line JSON per event)
Milestone: v0.3
Phase tag: v0.3.1
Key decisions: D-019 (1s poll ticker), D-025 (iter.Seq on store repos), D-026 (*model.Job/*model.Node element type), D-028 (poll re-runs List, yields full snapshot), D-030 (per-event JSON streaming), D-031 (signal.NotifyContext replaces 5s timeout on watch path), D-032 (inline pull loop, no goroutine).
Wave 1: Store layer iter.Seq + unit tests (vertical slice)
Wave 1 is independently testable: the two Watch methods + the migration-version-less store layer compile and run in isolation. No CLI or doctor code is touched. Running go test ./internal/store/... after this wave passes and exercises the iter.Seq contracts (yield, ctx cancellation, consumer break, no goroutine leak).
| Task ID | Description | Persona | Files | Must-have | Deps |
|---|---|---|---|---|---|
| 01-01-01 | Add JobRepo.Watch(ctx) iter.Seq[[]*model.Job] — pull-based inline polling loop on a 1s ticker; re-runs the List SELECT ... FROM jobs ORDER BY created_at DESC each tick, collects ALL rows into a []*model.Job slice via scanJob, then yields the full snapshot as a single slice (yield(snapshot)). Immediate first yield before the first ticker wait (G-002): the loop queries+yields on the first iteration, then selects on the ticker for subsequent ticks. defer ticker.Stop() + rows.Close() on every exit path (ctx.Done, yield==false, scan error). No goroutine spawned (D-032). Transient query errors are logged via slog.Default().Warn and the loop continues to the next tick (D-034 lite). Imports: add "iter" ("time" already present). |
data-engineer | internal/store/job_task_repo.go |
go build ./internal/store/... succeeds; Watch method exists with signature func (r *JobRepo) Watch(ctx context.Context) iter.Seq[[]*model.Job]; code path closes rows on ctx.Done and on yield==false; first yield is immediate (no watchInterval delay before first snapshot — G-002). |
- |
| 01-01-02 | Add NodeRepo.Watch(ctx) iter.Seq[[]*model.Node] — analogous to 01-01-01 but against the nodes query SELECT id, name, address, state, joined_at, last_seen, metadata FROM nodes ORDER BY joined_at ASC, reusing scanNode. Yields the full snapshot as a []*model.Node slice per tick. Same inline-pull / no-goroutine / rows-close-on-all-paths / immediate-first-yield contract (G-001, G-002). |
data-engineer | internal/store/node_repo.go |
go build ./internal/store/... succeeds; Watch method exists with signature func (r *NodeRepo) Watch(ctx context.Context) iter.Seq[[]*model.Node]; immediate first yield. |
- |
| 01-01-03 | Add an unexported test hook for the poll interval so unit tests are deterministic (D-035). Preferred shape: an unexported package var watchInterval = 1 * time.Second in internal/store that Watch reads instead of a literal, overridable from _test.go via watchInterval = 10 * time.Millisecond. Both JobRepo.Watch and NodeRepo.Watch reference this var. |
data-engineer | internal/store/job_task_repo.go, internal/store/node_repo.go (optionally a tiny internal/store/watch_test_helper_test.go if a shared helper reads cleaner) |
Watch uses the watchInterval var, not a literal 1 * time.Second; tests can set it to a small value. |
01-01-01, 01-01-02 |
| 01-01-04 | Write store-layer unit tests for Watch. New/append: internal/store/job_task_repo_test.go and internal/store/node_repo_test.go (mirror). Tests: (a) TestJobRepoWatch_YieldsSnapshots — insert 1 job, set watchInterval=10ms, range over seq collecting []*model.Job snapshots into a slice, insert a 2nd job from a goroutine after ~30ms, cancel ctx after ~80ms, assert at least one snapshot contains both jobs and the first snapshot contains only the first job (G-001: each yield is a complete tick snapshot). (b) TestJobRepoWatch_ImmediateFirstYield (G-002) — assert the first snapshot appears within <50ms even with watchInterval=10ms (proving first yield is not tick-gated). (c) TestJobRepoWatch_StopsOnConsumerBreak — range and break after first yield; assert the range returns (no hang) within a short deadline. (d) TestJobRepoWatch_StopsOnCtxCancel — cancel ctx; assert the range loop exits within ~50ms. (e) Mirror all four for NodeRepo.Watch. Run go test -race ./internal/store/.... |
data-engineer | internal/store/job_task_repo_test.go, internal/store/node_repo_test.go |
go test -race ./internal/store/... passes; all 8 Watch tests pass; -race reports no leaks/data races; immediate-first-yield assertion holds (G-002). |
01-01-03 |
Wave 2: CLI --watch flag + integration
Wave 2 depends on Wave 1's Watch methods. It wires the --watch flag into both list commands, implements the two output modes, and adds CLI-level smoke tests. After this wave orca job list --watch and orca node list --watch are runnable end-to-end.
| Task ID | Description | Persona | Files | Must-have | Deps |
|---|---|---|---|---|---|
| 01-02-01 | Add --watch flag to jobListCmd in internal/cli/job.go. Register jobListCmd.Flags().BoolVar(&jobWatch, "watch", false, "stream jobs until Ctrl-C") (package var jobWatch bool). In RunE, branch: if !jobWatch keep the existing 5s-timeout List path unchanged; if jobWatch, build ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM); defer cancel() (drop the 5s timeout — D-031), then seq := store.NewJobRepo(db).Watch(ctx). Imports: os/signal, syscall, iter. The branch structure is added in this task; the actual rendering (table vs JSON) is filled by 01-02-02 + 01-02-03. |
cli-engineer | internal/cli/job.go |
go build ./internal/cli/... succeeds; orca job list --help shows --watch flag; non-watch path behavior unchanged (existing tests pass); watch path compiles (rendering may be a placeholder for range seq {} at this step). |
01-01-01 |
| 01-02-02 | Implement the table watch render in jobListCmd (D-029, G-001). Each yielded value is a complete []*model.Job snapshot. Maintain the previous snapshot's rendered-table string (or a hash of it). On each tick, if the new rendered table differs from the previous, emit "\033[2J\033[H" (clear screen + home) then the table header + rows (reuse the existing table-rendering code path). Unchanged snapshots produce no output (avoids flicker). |
cli-engineer | internal/cli/job.go |
orca job list --watch on a temp DB: inserting a job causes a cleared-screen re-render showing the new job; no output when the snapshot is unchanged. |
01-02-01 |
| 01-02-03 | Implement the --watch --json render in jobListCmd (D-030, G-001). Each yielded value is a complete []*model.Job snapshot. Maintain map[string][]byte of last-seen compact-JSON bytes per job ID. Per tick: diff the current snapshot against the map — for each job in the snapshot, marshal compact JSON; if it differs from stored bytes (or ID unseen), print {"event":"init","job":{...}}\n (first sighting) or {"event":"update","job":{...}}\n (subsequent change). For IDs in the map but NOT in the current snapshot, print {"event":"delete","job":{...}}\n (G-006 DEFER: delete event now natural with snapshot-per-tick). One line per changed element per tick — matches REQ-030. |
cli-engineer | internal/cli/job.go |
orca job list --watch --json on a temp DB: inserting/changing a job prints one JSON line per changed job; first tick prints "init" lines for existing jobs; deleting a job prints "delete"; unchanged jobs on a tick produce no line. |
01-02-01 |
| 01-02-04 | Add --watch flag + both render modes to nodeListCmd in internal/cli/node.go, mirroring 01-02-01..01-02-03. Package var nodeWatch bool; flag --watch. For the watch path bypass engine.NodeRegistry and call store.NewNodeRepo(db).Watch(ctx) directly (D-025 — keeps iter boundary in store; registry adds no value for a read-only stream). Same signal.NotifyContext cancellation. Table + JSON renders analogous to job (element type []*model.Node snapshot per tick, event wrapper {"event":"...","node":{...}}). Note (G-003): This task modifies internal/cli/node.go, which P02 task 02-01-01 also modifies (removing old dbPath). Task 02-01-01 MUST complete first to avoid merge conflicts. |
cli-engineer | internal/cli/node.go |
orca node list --watch and orca node list --watch --json behave as specified; non-watch path unchanged. |
01-01-02, 01-02-03, 02-01-01 |
| 01-02-05 | Add CLI-level watch tests. New files (or append if present): internal/cli/job_test.go, internal/cli/node_test.go. Smoke-level (store layer is the thorough test home): TestJobListWatch_JSONStreaming — temp DB, insert a job, run jobListCmd.RunE with --watch --json in a goroutine under a cancellable ctx, insert a 2nd job, capture stdout for ~200ms, assert ≥2 JSON lines appear, then cancel ctx and assert the command returns promptly. TestJobListWatch_TableRefresh — assert the clear-screen escape \033[2J\033[H appears in output on change. Mirror for nodes. Keep deterministic: small watchInterval via the test hook, short timeouts. Run go test -race ./internal/cli/.... |
cli-engineer | internal/cli/job_test.go, internal/cli/node_test.go |
go test -race ./... passes (whole repo); the 4 CLI watch smoke tests pass; no goroutine leaks under -race. |
01-02-02, 01-02-03, 01-02-04 |
Test strategy (P01)
- Store layer (thorough, deterministic):
internal/store/job_task_repo_test.go+internal/store/node_repo_test.go— three tests per repo (snapshots over time, consumer-break stops, ctx-cancel stops). Uses thewatchIntervaltest hook (10ms) for speed. Runs undergo test -race ./internal/store/.... This is where theiter.Seqcontract is verified rigorously. - CLI layer (smoke):
internal/cli/job_test.go+internal/cli/node_test.go— verify the flag is wired, JSON streaming emits one line per changed element, table mode emits the clear-screen escape on change, and the command exits promptly on ctx cancellation. Kept intentionally lightweight; deterministic via the sharedwatchIntervalhook + short timeouts. - Non-watch regression: existing
job list/node listtests must still pass unchanged (the 5s-timeout path is untouched). - Race:
go test -race ./...is the gate (REQ-031 already enforces-racein CI).
Vertical slice integrity (P01)
- Wave 1 produces a runnable, testable artifact:
go build ./internal/store/...+go test -race ./internal/store/.... No CLI or doctor code is modified. Theiter.Seqcontract (pull, cancel, no-leak) is fully verified at this layer. - Wave 2 builds on Wave 1's
Watchmethods to deliver the user-facing--watchflag end-to-end. After Wave 2 an operator can demoorca job list --watchandorca node list --watch --json.
Phase P02: orca doctor network + db full implementation
Goal: Replace the NetworkStub and DBStub placeholders with real diagnostics — peer reachability via mTLS /healthz probe and SQLite PRAGMA integrity_check + migration version — completing REQ-032.
Requirements: REQ-032 (completion: network reachability + db integrity)
Milestone: v0.3
Phase tag: v0.3.2
Key decisions: D-027 (closure-capture handles in check constructors), D-033 (store.MigrationVersion), D-034 (db check opens its own *sql.DB), D-035 (PRAGMA integrity_check + migration version), D-036 (peers from nodes table, not in-memory registry), D-037 (ServerName = node.Name), D-038 (zero peers → WARN, any fail → FAIL, 3s per-probe timeout), D-039 (dbPath → certpaths.DBPath()), D-040 (delete stubs, no shims).
Wave 1: Shared infra + store migration-version query (vertical slice)
Wave 1 breaks the would-be doctor → cli import cycle (D-039) and adds the public store.MigrationVersion query. Both are independently testable: go test ./internal/store/... ./internal/certpaths/... passes after this wave, and the foundation for both the db and network checks is in place.
| Task ID | Description | Persona | Files | Must-have | Deps |
|---|---|---|---|---|---|
| 02-01-01 | Move dbPath() (the ORCA_DB-env-honoring path resolver) from internal/cli to internal/certpaths as DBPath() (D-039). certpaths already owns the ORCA_HOME-honoring Dir(). Add func DBPath() string to internal/certpaths/certpaths.go: honors ORCA_DB env override, else filepath.Join(Dir(), "orca.db"). Update internal/cli/node.go (and any other internal/cli caller of the old unexported dbPath) to call certpaths.DBPath(); remove the old dbPath from internal/cli. Territory note: this crosses cli-engineer territory — lead-developer adjudicates (D-039). |
lead-developer | internal/certpaths/certpaths.go, internal/cli/node.go (remove old dbPath), any other internal/cli/* caller |
go build ./... succeeds (no import cycle); certpaths.DBPath() exists and honors ORCA_DB/ORCA_HOME; internal/cli no longer defines dbPath; existing CLI tests pass. |
- |
| 02-01-02 | Add store.MigrationVersion(ctx, db) (string, error) to internal/store/migrate.go (D-033). SQL: SELECT name FROM schema_migrations ORDER BY name DESC LIMIT 1. Returns ("", nil) on sql.ErrNoRows (empty/fresh db). Wraps other errors with fmt.Errorf("query migration version: %w", err). Add "context" import if missing (likely already imported). |
data-engineer | internal/store/migrate.go |
go build ./internal/store/... succeeds; function is exported; sql.ErrNoRows maps to ("", nil). |
- |
| 02-01-03 | Test MigrationVersion. New/append internal/store/migrate_test.go: TestMigrationVersion — open a fresh test db via store.Open (which runs migrate), call MigrationVersion, assert it returns 0005_node_capacity.sql (the highest current migration). Then manually db.Exec("DELETE FROM schema_migrations"), call again, assert ("", nil). Run go test -race ./internal/store/.... |
data-engineer | internal/store/migrate_test.go |
go test -race ./internal/store/... passes; both assertions (highest version, empty → "") hold. |
02-01-02 |
Wave 2: doctor DB check + network check + CLI wiring + tests
Wave 2 depends on Wave 1 (certpaths.DBPath + store.MigrationVersion). It replaces both stubs with real checks, updates All() and the CLI subcommands, rewrites the broken stub test, and adds per-check tests. After this wave orca doctor, orca doctor network, and orca doctor db are fully functional.
| Task ID | Description | Persona | Files | Must-have | Deps |
|---|---|---|---|---|---|
| 02-02-01 | Replace DBStub() with DB() in internal/doctor/doctor.go (D-027, D-034, D-035). The Check.Run closure: path := certpaths.DBPath(); db, err := store.Open(path) (defer db.Close()); PRAGMA integrity_check via db.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&integrity); FAIL if not "ok" (first line of message); then store.MigrationVersion(ctx, db) — WARN if "" (fresh/never-migrated), else PASS with "... migrations up to <ver>". New imports: strings, internal/store, internal/certpaths. |
data-engineer | internal/doctor/doctor.go |
go build ./internal/doctor/... succeeds; doctor.DB() returns a Check with Name=="db"; DBStub still present (removed in 02-02-04 lockstep). |
02-01-01, 02-01-02 |
| 02-02-02 | Add probeHealthz(ctx, caPath, certPath, keyPath, serverName, addr) error helper in internal/doctor/doctor.go (network-engineer territory — connection lifecycle). Builds an mTLS client via transport.NewMTLSClient(caPath, serverName, certPath, keyPath) (D-037: serverName = node.Name), http.NewRequestWithContext(ctx, GET, "https://"+addr+"/healthz", nil), client.Do(req), defer resp.Body.Close(), FAIL if status != 200. New imports: net/http, time, internal/transport. |
network-engineer | internal/doctor/doctor.go |
go build ./internal/doctor/... succeeds; probeHealthz exists with the specified signature; reuses transport.NewMTLSClient (no new TLS code — security-engineer territory respected). |
02-01-01 |
| 02-02-03 | Replace NetworkStub() with Network() in internal/doctor/doctor.go (D-036, D-037, D-038). The Check.Run closure: path := certpaths.DBPath(); db, err := store.Open(path) (defer close); nodes, err := store.NewNodeRepo(db).List(ctx); filter state != model.NodeStateLeft into live; if len(live)==0 → ResultWarn ("no peers registered; network check skipped (single-node?)"). Else per-peer: probeCtx, cancel := context.WithTimeout(ctx, 3*time.Second); probeHealthz(...); collect PASS/FAIL lines; aggregate — FAIL if any peer failed, PASS if all OK. serverName = n.Name, caPath = certpaths.CACertPath(), certPath/keyPath = certpaths.ServerCertPath()/ServerKeyPath(). New imports: internal/model. |
network-engineer | internal/doctor/doctor.go |
go build ./internal/doctor/... succeeds; doctor.Network() returns a Check with Name=="network"; zero-peer → WARN; per-probe 3s timeout enforced. |
02-02-02 |
| 02-02-04 | Update All() in internal/doctor/doctor.go to use Network() and DB() instead of the stubs (D-040). Delete NetworkStub and DBStub (no backward-compat shims — internal only). Update internal/cli/doctor.go: doctorNetworkCmd.RunE calls doctor.Network() (was NetworkStub()); doctorDBCmd.RunE calls doctor.DB() (was DBStub()). Ensure per-subcommand render honors jsonOutput (minor enhancement, in scope). |
cli-engineer | internal/doctor/doctor.go, internal/cli/doctor.go |
go build ./... succeeds; grep -r "NetworkStub|DBStub" internal/ returns nothing; orca doctor, orca doctor network, orca doctor db run without referencing stubs. |
02-02-01, 02-02-03 |
| 02-02-05 | Rewrite + add doctor tests in internal/doctor/doctor_test.go. (a) Rewrite TestRunAllChecksWithNoCA — set ORCA_HOME to a temp dir with no CA. Assert per-check by name: cert.ca FAIL, cert.server FAIL, cert.expiry FAIL, cert.fingerprint FAIL, db PASS (store.Open runs migrations → version 0005), network WARN (no peers). Remove the stale "expects WARN (stubs)" comment. (b) TestDBCheck_IntegrityOK — fresh db via store.Open in temp, run doctor.DB().Run(ctx), expect PASS, message contains "0005". (c) TestNetworkCheck_NoPeers — fresh db, no nodes, run doctor.Network().Run(ctx), expect WARN. (d) TestNetworkCheck_PeerUnreachable — insert a node with Address = "127.0.0.1:1" (nothing listening), run, expect FAIL with the peer name in the message. (e) TestNetworkCheck_PeerReachable (integration) — bootstrap a CA via security.CAInit-equivalent, generate+sign a server cert with SAN localhost, start an httptest.NewUnstartedServer with TLS + ClientAuth=RequireAndVerifyClientCert (mirror security/integration_test.go pattern), insert a node row with Address = ts.Listener.Addr().String() and Name = "localhost", set ORCA_HOME, run doctor.Network().Run(ctx), expect PASS. Run go test -race ./internal/doctor/.... |
network-engineer (network tests), data-engineer (db test), cli-engineer (All() rewrite test) | internal/doctor/doctor_test.go |
go test -race ./internal/doctor/... passes; all 5 test cases pass; the stale stub assertion is gone. |
02-02-04 |
Test strategy (P02)
- Store layer:
internal/store/migrate_test.go—MigrationVersionreturns highest applied migration (0005_node_capacity.sql) and""on empty. (-race.) - Doctor db check:
TestDBCheck_IntegrityOK— fresh db → PASS with version in message. (Optional brittleTestDBCheck_Corruptmay be added if a reliable corruption method is found; otherwise rely on the integrity-string parsing logic via the PASS/FAIL branch coverage.) - Doctor network check:
TestNetworkCheck_NoPeers(WARN),TestNetworkCheck_PeerUnreachable(FAIL, peer name in message),TestNetworkCheck_PeerReachable(integration: real mTLShttptestserver → PASS). The reachable test reuses the provenTestEndToEndMTLSpattern fromsecurity/integration_test.go. - Doctor
All()regression: rewrittenTestRunAllChecksWithNoCAasserts per-check results (certs FAIL, db PASS, network WARN) — no more global "hasWarn" stub assertion. - Race:
go test -race ./...is the gate.
Vertical slice integrity (P02)
- Wave 1 produces a runnable, testable artifact:
certpaths.DBPath()(cycle broken) +store.MigrationVersion(tested).go test -race ./internal/store/... ./internal/certpaths/...passes. No doctor code depends on the stubs being changed yet. - Wave 2 builds on Wave 1 to deliver the real
DB()andNetwork()checks, wires the CLI, and replaces the stub tests. After Wave 2 an operator can demoorca doctorshowing real PASS/WARN/FAIL for db and network.
Phase P03 (final): review + ship + audit
Goal: Review the v0.3 milestone for completeness against REQ-022/030/032, audit the codebase for leftover stubs/dead code, run the full CI gate (go test -race ./..., gosec, govulncheck, gitleaks), tag the milestone release, and ship.
Requirements: REQ-022 (verify complete), REQ-030 (verify complete), REQ-032 (verify complete)
Milestone: v0.3
Phase tag: v0.3.3 (= milestone release; target milestone tag v0.4.0 per ROADMAP next-minor rule)
Wave 1: Review + audit + ship (single wave)
| Task ID | Description | Persona | Files | Must-have | Deps |
|---|---|---|---|---|---|
| 03-01-01 | Verify REQ coverage: confirm REQ-022 (iter.Seq streaming job lists), REQ-030 (--watch table/JSON modes), REQ-032 (doctor network + db) are fully implemented. Update REQUIREMENTS.md status for REQ-022/030/032 from Pending/Partial → Complete. Cross-check against the plan's must-have criteria. |
lead-developer | .ciagent/REQUIREMENTS.md |
All three REQs marked Complete with phase references; no remaining "stub" or "Pending" status for v0.3 scope. | P01, P02 complete |
| 03-01-02 | Codebase audit: grep -r "NetworkStub|DBStub" internal/ returns nothing; grep -r "TODO|FIXME" internal/ reviewed (no v0.3 leftovers); confirm no dbPath duplication remains in internal/cli; confirm iter import is used (no unused imports); run go vet ./.... |
lead-developer | (read-only audit; edits only if cleanup needed) | go vet ./... clean; no stub references; no leftover TODOs for v0.3 scope. |
03-01-01 |
| 03-01-03 | Full CI gate: go build ./..., go test -race ./..., gosec (vs baseline JSON), govulncheck ./... (offline mode per REQ-027), gitleaks (vs baseline per REQ-029). Fix any new findings. |
lead-developer | (fixes if needed) | All gates green; no new gosec findings beyond baseline; govulncheck exit 0; gitleaks clean vs baseline. | 03-01-02 |
| 03-01-04 | Tag + ship: per .ciagent/RELEASE_POLICY.md, tag v0.3.3 (phase tag) and the milestone tag (next-minor per ROADMAP). Produce Gitea release. Update ROADMAP.md v0.3 section to mark P01/P02/P03 complete. |
lead-developer | .ciagent/ROADMAP.md |
v0.3.3 tag exists; Gitea release published; ROADMAP v0.3 checkboxes updated. |
03-01-03 |
Test strategy (P03)
- No new tests; this phase is review + audit + release.
- The gate is the existing test suite + security scans all passing under CI.
Cross-phase notes
- Phase ordering / parallelism (D-042, revised by G-003): P01 Wave 1 and P02 Wave 1 may run in parallel (file-disjoint: store repos vs certpaths+migrate). P02 Wave 1 (02-01-01) MUST complete before P01 Wave 2 (01-02-04) because both modify
internal/cli/node.go(P01 adds--watch, P02 removes olddbPath). P01 Wave 2 tasks 01-02-01..01-02-03 (job.go only) are not blocked by P02. Recommended order: P01 W1 + P02 W1 in parallel → P02 W1 02-01-01 completes → P01 W2 (job.go tasks) + P02 W2 in parallel → P01 W2 01-02-04 (node.go) after 02-01-01. P03 is strictly sequential after both phases complete. - No new dependencies (D-041):
iter(P01) and the mTLS health probe (P02) use stdlib + existing internal packages only. The 4 directgo.moddeps (cobra, hcl/v2, modernc/sqlite, uuid) are unchanged. - Decisions logged during planning (new, this plan):
- D-043 —
watchIntervaltest hook: an unexported package var ininternal/store(default1 * time.Second) referenced by bothWatchmethods, overridable from_test.go. Avoids a publicWatchWithIntervalconstructor that would leak test-only API into production. Confidence 0.90. - D-044 — P01 Wave 1 / Wave 2 split: Wave 1 = store-layer
Watchmethods + tests (data-engineer only, fully isolated); Wave 2 = CLI--watchflag + renders + CLI tests (cli-engineer). This keeps theiter.Seqcontract verifiable without the CLI and matches persona territories. Confidence 0.93. - D-045 — P02 Wave 1 / Wave 2 split: Wave 1 =
certpaths.DBPath()relocation +store.MigrationVersion+ tests (breaks the import cycle, data-engineer + lead-developer); Wave 2 = realDB()/Network()checks + CLI wiring + doctor tests. Wave 1 is the unblock for both checks. Confidence 0.91. - D-046 —
--watch --jsonevent wrapper shape:{"event":"update","job":{...}}/{"event":"init","job":{...}}(andnodeanalog)."init"on first sighting of an ID,"update"on subsequent change. Unchanged IDs on a tick emit nothing. Confidence 0.80 (matches D-030's per-event interpretation of REQ-030).
- D-043 —
Grill amendments (binding ACCEPT verdicts applied)
Three binding changes from .ciagent/GRILL_v0.3.md have been applied to this plan:
- G-001 [CRITICAL] —
Watchelement type changed fromiter.Seq[*model.Job](per-row) toiter.Seq[[]*model.Job](full snapshot per tick). Each tick yields the complete snapshot as a single slice. This makes table render mode correct (clear-screen + re-render needs full snapshot) and enables natural"delete"events in JSON mode. Applied to tasks 01-01-01, 01-01-02, 01-02-02, 01-02-03, 01-02-04, 01-01-04. - G-002 [CRITICAL] —
Watchmust yield immediately on the first iteration, thenselecton the ticker for subsequent ticks. Prevents a 1s blank-screen UX bug in production (tests with 10ms interval missed this). Applied to tasks 01-01-01, 01-01-02; new testTestWatch_ImmediateFirstYieldadded to 01-01-04. - G-003 [HIGH] — D-042's "file-disjoint" claim corrected: both P01 (01-02-04) and P02 (02-01-01) modify
internal/cli/node.go. P02 Wave 1 task 02-01-01 is now a dependency of P01 Wave 2 task 01-02-04. Cross-phase ordering updated.
DEFER items (G-004, G-006, G-007, G-009, G-012) are noted in the grill report and will be addressed during execution.
Summary
- Phases: 3 (P01, P02 execution; P03 final review/ship)
- Waves: P01 = 2 waves (4 + 5 tasks); P02 = 2 waves (3 + 5 tasks); P03 = 1 wave (4 tasks). Total = 5 waves.
- Tasks: P01 = 9, P02 = 8, P03 = 4. Total = 21 tasks.
- Grill amendments: G-001 (snapshot-per-tick), G-002 (immediate first yield), G-003 (serialize P02 W1 → P01 W2 on node.go). 3 ACCEPT verdicts applied.
- New planning decisions: D-043 (watchInterval test hook), D-044 (P01 wave split), D-045 (P02 wave split), D-046 (JSON event wrapper shape).
- Requirements closed: REQ-022, REQ-030 (P01); REQ-032 (P02, completion).
- No new go.mod dependencies. No source code written in this plan — implementation begins at P01 Wave 1.