Jobs Lifecycle Refactor Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Rename and reshape the current process/run lifecycle into a coherent jobs subsystem while preserving old local .almanac/runs/ history through an explicit compatibility path.
Architecture: Operations define semantic work (build, absorb, garden); jobs own one queued/executed lifecycle instance of that work; harness providers execute the provider runtime; OS child-process control lives near the harness, not in jobs. The final vocabulary is JobRecord, JobSpec, JobStatus, jobId, .almanac/jobs/, and src/jobs/; old run_* records and .almanac/runs/ are read/migrated as compatibility only.
Tech Stack: TypeScript, Vitest, Node filesystem APIs, Commander CLI, local .almanac/ filesystem state, existing harness providers.
Non-Negotiables#
- Start from a clean
devworktree. Do not use the dirty main checkout. - Do not delete or rewrite
.almanac/wiki pages or architecture-audit docs. - Keep
almanac jobsas the user-facing command. - Keep operation names
build,absorb, andgarden. - Use
git mvfor file moves where possible. - Do not introduce a workflow engine, command bus, class hierarchy, or state-machine library.
- Preserve old local job history:
- New jobs write to
.almanac/jobs/. - Old
.almanac/runs/records are migrated or read by fallback. - Old
run_*ids remain valid for lookup/cancel/show/logs. - New ids use
job_*.
- New jobs write to
- Keep
__run-workeras a hidden compatibility alias for one release; new background starts should spawn__job-worker.
Read Before Coding#
MANUAL.md.almanac/README.mdalmanac show process-manager-runsalmanac show lifecycle-architecturealmanac show provider-lifecycle-boundarysrc/process/manager.tssrc/process/background.tssrc/process/records.tssrc/process/spec.tssrc/process/logs.tssrc/cli/commands/jobs.tssrc/viewer/jobs.tssrc/viewer/job-projections.tssrc/harness/types.ts
Target Shape#
src/operations/
spec.ts # OperationKind, OperationSpec, ProviderSessionPersistence
output.ts # operation-specific output summarization
build.ts
absorb.ts
garden.ts
run.ts
src/jobs/
index.ts
ids.ts
start.ts # foreground/background public entrypoints
worker.ts # queued worker drain
executor.ts # execute one claimed job
events.ts # log event sink + observer fanout + providerSessionId persistence
finalization.ts # done/failed/cancelled terminal writes
wiki-effects.ts # snapshots, page deltas, summary counts, reindex-on-success
output.ts # neutral final output -> JobOperationOutput
records.ts
logs.ts
spec.ts
queue.ts
snapshots.ts
projections/
log-events.ts
agent-traces.ts
warnings.ts
types.ts
src/harness/process/
managed-child.ts # actual OS child-process group managementCall flow:
const spec = await createGardenOperationSpec(...);
const result = await startForegroundJob({ repoRoot, spec });Execution flow:
startForegroundJob(...)
-> claim foreground job under per-wiki worker lock
-> executeClaimedJob(...)
startBackgroundJob(...)
-> write JobSpec
-> write queued JobRecord
-> initialize Job log
-> spawn __job-worker
runJobWorker(...)
-> acquire worker lock
-> oldestQueuedJob(...)
-> readJobSpec(...)
-> startQueuedJob(...)
executeClaimedJob(...)
-> snapshotWikiBeforeJob(...)
-> run harness provider with createJobEventSink(...)
-> computeJobWikiEffect(...)
-> projectJobOutput(...)
-> summarizeOperationOutput(...)
-> finalizeJobUnlessCancelled(...)Task 1: Move Provider Child-Process Control To Harness#
Files:
- Move:
src/process/managed-child.ts->src/harness/process/managed-child.ts - Modify:
src/harness/providers/claude.ts - Modify:
src/harness/providers/codex/app-server.ts - Modify:
src/process/index.ts - Modify:
test/managed-child.test.ts - Modify:
test/claude-harness-provider.test.ts - Modify:
test/codex-harness-provider.test.ts
Step 1: Write/adjust import expectations
Update test/managed-child.test.ts to import:
import { spawnManagedChildProcess } from "../src/harness/process/managed-child.js";Step 2: Run the focused test to verify current imports fail
Run:
npx vitest run test/managed-child.test.tsExpected: fail until the file move/import changes are done.
Step 3: Move the file and update imports
Run:
mkdir -p src/harness/process
git mv src/process/managed-child.ts src/harness/process/managed-child.tsUpdate production imports:
// src/harness/providers/claude.ts
import { spawnManagedChildProcess } from "../process/managed-child.js";
// src/harness/providers/codex/app-server.ts
import { spawnManagedChildProcess } from "../../process/managed-child.js";Remove the managed-child export from src/process/index.ts.
Step 4: Verify
Run:
npm run lint
npx vitest run test/managed-child.test.ts test/claude-harness-provider.test.ts test/codex-harness-provider.test.tsExpected: pass.
Step 5: Commit
git add src/harness/process src/harness/providers src/process/index.ts test
git commit -m "refactor: move provider process control into harness"Task 2: Move Lifecycle Module From process To jobs#
This task is a location-only move. Do not rename types/functions yet.
Files:
- Move:
src/process/->src/jobs/ - Modify imports across
src/andtest/ - Modify:
src/cli.ts
Step 1: Move the directory
Run:
git mv src/process src/jobsStep 2: Update imports
Use rg first:
rg -n "../process|../../process|src/process|from \"\\.\\/process|from \"\\.\\.\\/process" src testUpdate imports to jobs.
Examples:
// Before
import { listRunRecords } from "../process/index.js";
// After
import { listRunRecords } from "../jobs/index.js";Update src/cli.ts hidden worker import path if it imports from process.
Step 3: Verify
Run:
npm run lint
npx vitest run test/process-records.test.ts test/process-manager.test.ts test/process-background.test.ts test/process-queue.test.ts test/jobs-command.test.ts test/viewer-api.test.ts test/sync.test.tsExpected: pass. Test filenames may still contain process at this point.
Step 4: Commit
git add -A src test
git commit -m "refactor: move lifecycle code under jobs"Task 3: Rename Run Vocabulary To Job Vocabulary#
Files:
- Modify:
src/jobs/types.ts - Modify:
src/jobs/ids.ts - Modify:
src/jobs/records.ts - Modify:
src/jobs/logs.ts - Modify:
src/jobs/spec.ts - Modify:
src/jobs/queue.ts - Modify:
src/jobs/start.tsor current manager/background files - Modify:
src/jobs/index.ts - Rename tests:
test/process-records.test.ts->test/jobs-records.test.tstest/process-manager.test.ts->test/jobs-executor.test.tstest/process-background.test.ts->test/jobs-worker.test.tstest/process-queue.test.ts->test/jobs-queue.test.tstest/process-logs.test.ts->test/jobs-logs.test.tstest/process-snapshots.test.ts->test/jobs-snapshots.test.ts
Step 1: Rename types
Use these final names:
RunStatus -> JobStatus
DisplayRunStatus -> DisplayJobStatus
RunSummary -> JobSummary
RunPageChanges -> JobPageChanges
RunOperationOutput -> JobOperationOutput
RunRecord -> JobRecord
RunView -> JobView
RunLogEntry -> JobLogEntry
RunLogEntryV1 -> JobLogEntryV1
RunLogEntryV2 -> JobLogEntryV2Step 2: Rename functions
Use these final names:
createRunId -> createJobId
runsDir -> jobsDir
runRecordPath -> jobRecordPath
runLogPath -> jobLogPath
runCancelPath -> jobCancelPath
markRunCancelled -> markJobCancelled
isRunCancellationRequested -> isJobCancellationRequested
writeRunRecord -> writeJobRecord
readRunRecord -> readJobRecord
listRunRecords -> listJobRecords
buildStartedRunRecord -> buildStartedJobRecord
buildQueuedRunRecord -> buildQueuedJobRecord
finishRunRecord -> finishJobRecord
isRunRecord -> isJobRecord
toRunView -> toJobView
runSpecPath -> jobSpecPath
writeRunSpec -> writeJobSpec
readRunSpec -> readJobSpec
startForegroundProcess -> startForegroundJob
startQueuedProcess -> startQueuedJob
startBackgroundProcess -> startBackgroundJob
runBackgroundWorker -> runJobWorker
oldestQueuedRun -> oldestQueuedJob
runWorkerLockPath -> jobWorkerLockPath
acquireRunWorkerLock -> acquireJobWorkerLockjobsDir() may temporarily continue pointing at .almanac/runs/ until Task 4. If so, add a short comment saying storage migration happens in Task 4; do not leave that comment after Task 4.
Return fields should become jobId, not runId:
export interface StartJobResult {
jobId: string;
record: JobRecord;
result: HarnessResult;
}Step 3: Keep operation field unchanged
Do not rename:
record.operation
OperationKind = "build" | "absorb" | "garden"Step 4: Verify no old source vocabulary remains
Run:
rg -n "RunRecord|RunStatus|RunView|RunSummary|RunPageChanges|RunOperationOutput|startForegroundProcess|startQueuedProcess|startBackgroundProcess|runRecordPath|runLogPath|runsDir|listRunRecords|readRunRecord|writeRunRecord|process/index" src testExpected: no matches except explicit legacy compatibility comments added in later tasks. At this stage, there should be no matches.
Step 5: Verify
Run:
npm run lint
npx vitest run test/jobs-records.test.ts test/jobs-executor.test.ts test/jobs-worker.test.ts test/jobs-queue.test.ts test/jobs-command.test.ts test/viewer-api.test.ts test/sync.test.tsExpected: pass.
Step 6: Commit
git add -A src test
git commit -m "refactor: rename run lifecycle to jobs"Task 4: Introduce .almanac/jobs/ Storage With Legacy .almanac/runs/ Compatibility#
Files:
- Modify:
src/jobs/ids.ts - Modify:
src/jobs/records.ts - Modify:
src/jobs/spec.ts - Modify:
src/jobs/queue.ts - Modify:
src/jobs/logs.ts - Modify:
src/cli/commands/jobs.ts - Modify:
src/viewer/jobs.ts - Modify:
test/jobs-records.test.ts - Modify:
test/jobs-command.test.ts - Modify:
test/viewer-api.test.ts - Modify:
test/sync.test.ts
Step 1: Add failing tests
Add tests covering:
it("writes new job records to .almanac/jobs with job_ ids", async () => {
const id = createJobId(new Date("2026-06-08T12:00:00.000Z"));
expect(id).toMatch(/^job_20260608120000_[0-9a-f]{8}$/);
expect(jobRecordPath(repo, id)).toContain(".almanac/jobs/");
});it("lists legacy .almanac/runs records as jobs", async () => {
// Write run_*.json under .almanac/runs.
// listJobRecords(repo) should include it.
});it("resolves old run ids for jobs show/logs/cancel compatibility", async () => {
// Write run_*.json and run_*.jsonl under .almanac/runs.
// run `almanac jobs show run_...` through command helper.
// Expected: old record is found.
});Step 2: Implement primary and legacy paths
In src/jobs/records.ts, change jobsDir() to the primary new storage path and add legacyRunsDir():
export function jobsDir(repoRoot: string): string {
return join(getRepoAlmanacDir(repoRoot), "jobs");
}
export function legacyRunsDir(repoRoot: string): string {
return join(getRepoAlmanacDir(repoRoot), "runs");
}New path helpers must return .almanac/jobs/:
export function jobRecordPath(repoRoot: string, jobId: string): string {
return join(jobsDir(repoRoot), `${jobId}.json`);
}Add resolver helpers for user-supplied ids:
export async function resolveJobRecordPath(
repoRoot: string,
jobId: string,
): Promise<string> {
const primary = jobRecordPath(repoRoot, jobId);
if (existsSync(primary)) return primary;
const legacy = join(legacyRunsDir(repoRoot), `${jobId}.json`);
if (existsSync(legacy)) return legacy;
return primary;
}Do the same for logs, specs, and cancel marker paths.
Step 3: Implement migration
Add:
export async function migrateLegacyRunsStorage(repoRoot: string): Promise<void>Rules:
- If
.almanac/jobs/does not exist and.almanac/runs/exists, rename.almanac/runs/to.almanac/jobs/. - If both exist, do not merge automatically. Read both for compatibility.
- Preserve old
run_*ids and record contents. Do not rewrite JSONL event envelopes in this task.
Call migration from:
listJobRecords(repoRoot)
startBackgroundJob(...)
startForegroundJob(...)
runJobWorker(...)Step 4: Accept both id prefixes
Update record validation:
function isJobId(value: string): boolean {
return value.startsWith("job_") || value.startsWith("run_");
}Update viewer/CLI safe-id validation to accept both:
/^(job|run)_[A-Za-z0-9_-]+$/New jobs must use job_*; old run_* is compatibility only.
Step 5: Verify
Run:
npm run lint
npx vitest run test/jobs-records.test.ts test/jobs-command.test.ts test/viewer-api.test.ts test/sync.test.tsExpected: pass.
Step 6: Commit
git add -A src test
git commit -m "refactor: store lifecycle jobs under almanac jobs"Task 5: Rename Hidden Worker Command With Compatibility Alias#
Files:
- Modify:
src/cli.ts - Modify:
src/jobs/worker.tsor current background module - Modify:
test/jobs-worker.test.ts - Modify:
test/cli.test.tsif it covers hidden worker routing
Step 1: Add failing test
Update existing background spawn test:
expect(spawned.args).toContain("__job-worker");Add compatibility test:
// invoke CLI hidden dispatch with ["__run-worker"]
// Expected: routes to runJobWorker for compatibility.Step 2: Implement
New background jobs spawn:
args: [entrypoint, "__job-worker"]CLI dispatch accepts both:
if (args[0] === "__job-worker" || args[0] === "__run-worker") {
await runJobWorker(...);
}Add a comment:
// Compatibility: old background workers may still invoke __run-worker.Step 3: Verify
Run:
npm run lint
npx vitest run test/jobs-worker.test.ts test/cli.test.tsExpected: pass.
Step 4: Commit
git add src/cli.ts src/jobs test
git commit -m "refactor: rename hidden worker command to job worker"Task 6: Move Operation Spec Ownership Out Of Harness#
Files:
- Create:
src/operations/spec.ts - Modify:
src/harness/types.ts - Modify:
src/harness/index.ts - Modify:
src/jobs/spec.ts - Modify:
src/jobs/types.ts - Modify:
src/operations/run.ts - Modify:
src/operations/build.ts - Modify:
src/operations/absorb.ts - Modify:
src/operations/garden.ts - Modify provider tests importing
AgentRunSpec
Step 1: Add new spec module
Create:
// src/operations/spec.ts
import type { FinalOutputSpec } from "../harness/final-output.js";
import type { ToolRequest } from "../harness/tools.js";
import type { AgentProviderId } from "../agent/provider-id.js";
export type OperationKind = "build" | "absorb" | "garden";
export type ProviderSessionPersistence = "ephemeral" | "persistent";
export interface OperationAgentSpec {
description: string;
prompt: string;
tools?: ToolRequest[];
model?: string;
effort?: string;
skills?: string[];
mcpServers?: Record<string, unknown>;
maxTurns?: number;
}
export interface OperationSpec {
provider: {
id: AgentProviderId;
model?: string;
effort?: string;
};
cwd: string;
systemPrompt?: string;
prompt: string;
tools?: ToolRequest[];
agents?: Record<string, OperationAgentSpec>;
skills?: string[];
mcpServers?: Record<string, unknown>;
networkAccess?: boolean;
limits?: {
maxTurns?: number;
maxCostUsd?: number;
};
providerSession?: {
persistence?: ProviderSessionPersistence;
};
output?: FinalOutputSpec;
metadata?: {
operation: OperationKind;
targetKind?: string;
targetPaths?: string[];
};
}Step 2: Update harness to consume operation specs
In src/harness/types.ts, remove AgentRunSpec, AgentSpec, OperationKind, and ProviderSessionPersistence.
Import:
import type { OperationSpec } from "../operations/spec.js";Update:
run(spec: OperationSpec, hooks?: HarnessRunHooks): Promise<HarnessResult>;Step 3: Update all imports
Rename:
AgentRunSpec -> OperationSpec
AgentSpec -> OperationAgentSpecDo not keep a type alias unless TypeScript import churn becomes too large for this task. If a temporary alias is necessary, mark it with a deletion comment and remove it before final verification.
Step 4: Verify no old spec name remains
Run:
rg -n "AgentRunSpec|AgentSpec|ProviderSessionPersistence|OperationKind" src testExpected:
OperationKindmay exist only insrc/operations/spec.tsand imports from it.ProviderSessionPersistencemay exist only insrc/operations/spec.tsand imports from it.- No
AgentRunSpec. - No old generic
AgentSpecfrom harness.
Step 5: Verify
Run:
npm run lint
npx vitest run test/harness-types.test.ts test/build-operation.test.ts test/absorb-operation.test.ts test/garden-operation.test.ts test/claude-harness-provider.test.ts test/codex-harness-provider.test.ts test/jobs-records.test.tsExpected: pass.
Step 6: Commit
git add -A src test
git commit -m "refactor: move operation specs out of harness"Task 7: Split Job Execution Lifecycle Into Named Modules#
Files:
- Create:
src/jobs/start.ts - Create:
src/jobs/worker.ts - Create:
src/jobs/executor.ts - Create:
src/jobs/events.ts - Create:
src/jobs/finalization.ts - Create:
src/jobs/wiki-effects.ts - Create:
src/jobs/output.ts - Modify:
src/jobs/index.ts - Delete or shrink: old
src/jobs/manager.ts - Delete or shrink: old
src/jobs/background.ts - Modify tests as needed
Step 1: Extract event sink
Move current event logger behavior into:
// src/jobs/events.ts
export function createJobEventSink(args: {
logPath: string;
jobId: string;
now: () => Date;
recordPath: string;
fallbackRecord: JobRecord;
observer?: (event: HarnessEvent) => void | Promise<void>;
}): {
hooks: HarnessRunHooks;
flush(): Promise<void>;
}It must own:
appendJobEvent- observer fanout
- providerSessionId persistence
- sequence numbering
Step 2: Extract finalization
Move cancellation-aware terminal record writes into:
// src/jobs/finalization.ts
export async function finishJobUnlessCancelled(...)
export async function cancelledJobRecordIfRequested(...)Step 3: Extract wiki effects
Move snapshot/delta/reindex logic into:
// src/jobs/wiki-effects.ts
export interface JobWikiEffect {
summary: Pick<JobSummary, "created" | "updated" | "archived" | "deleted">;
pageChanges: Omit<JobPageChanges, "summary">;
}
export async function captureJobWikiEffect(args: {
repoRoot: string;
before: PageSnapshot[];
jobId: string;
result: HarnessResult;
}): Promise<JobWikiEffect>
export async function reindexAfterSuccessfulJob(repoRoot: string): Promise<void>Step 4: Extract neutral output conversion
Move only generic final-output storage into:
// src/jobs/output.ts
export function toJobOperationOutput(
output: FinalOutputResult | undefined,
): JobOperationOutput | undefined
export function fallbackJobSummary(result: string): string | undefinedDo not import src/operations/reports.ts here.
Step 5: Create executor
src/jobs/executor.ts should be the readable workflow:
export async function executeClaimedJob(args: {
repoRoot: string;
spec: OperationSpec;
record: JobRecord;
now: () => Date;
onEvent?: (event: HarnessEvent) => void | Promise<void>;
harnessRun?: HarnessRun;
}): Promise<StartJobResult> {
const before = await snapshotPages(pagesDir);
const events = createJobEventSink(...);
const result = await runHarnessSafely(...);
await events.flush();
const effect = await captureJobWikiEffect(...);
const output = toJobOperationOutput(result.output);
const summaryText = summarizeOperationOutput(output) ?? fallbackJobSummary(result.result);
return finalize...
}Step 6: Split start and worker
src/jobs/start.ts owns:
startForegroundJobstartQueuedJobstartBackgroundJob
src/jobs/worker.ts owns:
runJobWorkermarkQueuedJobFailed
Step 7: Verify
Run:
npm run lint
npx vitest run test/jobs-executor.test.ts test/jobs-worker.test.ts test/jobs-records.test.ts test/jobs-logs.test.ts test/jobs-snapshots.test.ts test/jobs-command.test.ts test/viewer-api.test.ts test/sync.test.tsExpected: pass.
Step 8: Commit
git add -A src/jobs test
git commit -m "refactor: split job lifecycle into named modules"Task 8: Move Operation Output Semantics To Operations#
Files:
- Create:
src/operations/output.ts - Modify:
src/jobs/executor.ts - Modify:
src/operations/index.ts - Modify:
test/jobs-executor.test.ts - Modify:
test/operation-commands.test.tsif needed
Step 1: Add operations output projector
Create:
// src/operations/output.ts
import type { JobOperationOutput } from "../jobs/types.js";
import {
ALMANAC_OPERATION_REPORT_NAME,
parseAlmanacOperationReport,
} from "./reports.js";
export function summarizeOperationOutput(
output: JobOperationOutput | undefined,
): string | undefined {
if (output?.contract !== ALMANAC_OPERATION_REPORT_NAME) return undefined;
return parseAlmanacOperationReport(output.value).summary;
}Step 2: Remove operation-report imports from jobs
Run:
rg -n "ALMANAC_OPERATION_REPORT_NAME|parseAlmanacOperationReport|operations/reports" src/jobsExpected: no matches.
src/jobs/executor.ts may import only:
import { summarizeOperationOutput } from "../operations/output.js";That import is acceptable because it calls a neutral operation-level projection and does not know the concrete report contract.
Step 3: Verify
Run:
npm run lint
npx vitest run test/jobs-executor.test.ts test/process-records.test.ts test/operation-commands.test.tsIf test/process-records.test.ts has been renamed, use test/jobs-records.test.ts.
Expected: pass.
Step 4: Commit
git add src/operations src/jobs test
git commit -m "refactor: move operation output semantics out of jobs"Task 9: Move Job Projections Out Of Viewer#
Files:
- Create:
src/jobs/projections/log-events.ts - Create:
src/jobs/projections/agent-traces.ts - Create:
src/jobs/projections/warnings.ts - Create:
src/jobs/projections/view.ts - Modify:
src/viewer/jobs.ts - Modify:
src/viewer/job-types.ts - Modify:
src/cli/commands/jobs.ts - Modify:
test/viewer-api.test.ts - Modify:
test/jobs-command.test.ts - Add:
test/jobs-projections.test.ts
Step 1: Move log-event parsing
Move readJobLogEvents, wrapped-event parsing, actor parsing, and event sorting from src/viewer/jobs.ts into:
// src/jobs/projections/log-events.ts
export async function readJobLogEvents(path: string): Promise<JobLogEvent[]>Use job-owned types or neutral projection types. Do not import viewer types from jobs.
Step 2: Move agent trace derivation
Move deriveAgentTraces from src/viewer/job-projections.ts into:
// src/jobs/projections/agent-traces.ts
export function deriveJobAgentTraces(events: JobLogEvent[]): JobAgentTrace[]Step 3: Move warning derivation
Move deriveRunWarnings to:
// src/jobs/projections/warnings.ts
export function deriveJobWarnings(
operation: OperationKind,
job: JobView,
events: JobLogEvent[],
): JobWarning[]Step 4: Move view enrichment
Move enrichRunView, display title/subtitle, and transcript-source inference into:
// src/jobs/projections/view.ts
export function enrichJobView(...)Step 5: Let viewer adapt names only
src/viewer/jobs.ts should become an API wrapper:
const records = await listJobRecords(repoRoot);
const view = toJobView(...);
const events = await readJobLogEvents(jobLogPath(repoRoot, record.id));
return enrichJobView(view, events, specPrompt);Viewer-specific response names may stay ViewerJobRun, but the derivation should be from job projections.
Step 6: Let CLI reuse projections where useful
Update src/cli/commands/jobs.ts to reuse:
toJobViewreadJobLogEventsfor logs/attach if it currently reparses JSONL differently- warning helpers only if CLI output already has equivalent warnings
Do not overbuild CLI rendering in this task. The minimum goal is no duplicate event parsing/projection logic.
Step 7: Verify
Run:
npm run lint
npx vitest run test/jobs-projections.test.ts test/viewer-api.test.ts test/jobs-command.test.tsExpected: pass.
Step 8: Commit
git add -A src/jobs src/viewer src/cli test
git commit -m "refactor: share job projections across viewer and cli"Task 10: Update Sync, Ledger, Jobs CLI, Viewer, And Serve References#
Files:
- Modify:
src/sync/sweep.ts - Modify:
src/sync/ledger.ts - Modify:
src/cli/commands/jobs.ts - Modify:
src/viewer/jobs.ts - Modify:
src/viewer/job-types.ts - Modify:
src/viewer/job-projections.tsif still present; ideally delete it - Modify tests touching these modules
Step 1: Remove stale run naming from production source
Run:
rg -n "\\brun\\b|\\bruns\\b|RunRecord|RunStatus|RunView|runId|runLogPath|runRecordPath|readRun|writeRun|listRun|process-manager|src/process" src testReview matches carefully. Some may be legitimate compatibility comments or user-facing historical docs in tests. Production source should prefer job vocabulary except:
__run-workercompatibility alias comment- legacy
.almanac/runs/compatibility helpers - old
run_*id compatibility validators
Step 2: Update sync ledger
Current ledger names may have fields such as pendingRunId. Rename code-level names to pendingJobId if they are source fields under current control.
Compatibility rule:
- If persisted ledger JSON already contains
pendingRunId, either keep the persisted key and map topendingJobIdinternally, or migrate it explicitly. - Do not silently drop pending capture reconciliation data.
Step 3: Update tests
Tests should assert new vocabulary:
expect(result.stdout).toContain("job");
expect(record.id).toMatch(/^job_/);Legacy tests should explicitly say legacy:
it("accepts legacy run ids for job lookup", ...)Step 4: Verify
Run:
npm run lint
npx vitest run test/sync.test.ts test/jobs-command.test.ts test/viewer-api.test.ts test/serve-command.test.tsExpected: pass.
Step 5: Commit
git add -A src test
git commit -m "refactor: align job references across sync and viewer"Task 11: Update Docs And User-Facing References#
Files:
- Modify:
guides/reference.mdor equivalent generated/static CLI reference if present - Modify:
AGENTS.md - Modify:
docs/plans/2026-06-08-jobs-lifecycle-refactor.mdonly if implementation decisions changed - Modify README/reference docs that mention
.almanac/runs/
Step 1: Find references
Run:
rg -n "process manager|src/process|\\.almanac/runs|RunRecord|RunSpec|runId|run record|runs/" AGENTS.md README.md docs guides src testDo not edit .almanac/pages/* in this task unless the user explicitly asks for wiki maintenance.
Step 2: Update docs
Preferred wording:
Jobs are user-visible lifecycle executions for write-capable operations.
New job records live under .almanac/jobs/.
Old .almanac/runs/ records are migrated or read as compatibility.Step 3: Verify docs do not contradict CLI
Run:
npm run lint
npm testExpected: pass.
Step 4: Commit
git add AGENTS.md README.md docs guides
git commit -m "docs: document jobs lifecycle vocabulary"Task 12: Full Verification And Compatibility Audit#
Step 1: Full source vocabulary audit
Run:
rg -n "src/process|process manager|RunRecord|RunStatus|RunView|RunSummary|RunPageChanges|RunOperationOutput|AgentRunSpec|startForegroundProcess|startBackgroundProcess|runRecordPath|runLogPath|runsDir" src test docs AGENTS.md README.mdExpected: no matches except:
- Explicit legacy compatibility comments.
- Historical docs intentionally left alone.
- External issue text or archived plans if not edited.
Step 2: Storage compatibility test manually
Create a temp repo through tests or a small manual script:
.almanac/runs/run_20260509195001_deadbeef.json
.almanac/runs/run_20260509195001_deadbeef.jsonl
.almanac/runs/run_20260509195001_deadbeef.spec.jsonVerify:
almanac jobs
almanac jobs show run_20260509195001_deadbeef
almanac jobs logs run_20260509195001_deadbeefExpected: old job is visible.
Step 3: New job storage test
Run a foreground operation in a temp wiki using test helpers or command tests.
Expected:
.almanac/jobs/job_*.json
.almanac/jobs/job_*.jsonl
.almanac/jobs/job_*.spec.jsonNo new .almanac/runs/ directory is created.
Step 4: Full verification
Run:
npm run lint
npm testExpected: pass.
Step 5: Final review
Use the repo’s code-review process. Review focus:
- No hidden second vocabulary remains.
- Jobs do not know provider-specific process details.
- Harness does not own operation semantics.
- Operation report parsing is not inside jobs.
- Legacy
.almanac/runs/handling is isolated and commented. - New jobs write only to
.almanac/jobs/.
Step 6: Squash merge
After review fixes and full verification:
git checkout dev
git merge --squash codex/jobs-lifecycle-refactor
npm run lint
npm test
git commit -m "refactor: standardize lifecycle jobs"Final Acceptance Criteria#
src/process/no longer exists.src/jobs/owns job lifecycle, records, logs, specs, queue, snapshots, execution, finalization, and job projections.src/harness/process/managed-child.tsowns provider child-process control.src/operations/spec.tsownsOperationSpecandOperationKind.src/operations/output.tsowns operation-specific output summaries.- New persisted records use
.almanac/jobs/job_*.json. - Legacy
.almanac/runs/run_*records are still visible throughalmanac jobs. almanac jobsremains the command surface.__job-workeris the new hidden worker command.__run-workerstill works as a temporary compatibility alias.npm run lintpasses.npm testpasses.