28 KiB
Workstream 1 Implementation Plan
Purpose And Status
This document is the executable implementation plan for
refactoring.md, Workstream 1: Go baseline, process
supervision, and transcription concurrency. The refactoring roadmap owns the
approved scope, rationale, and target state; the
Audit Decision Register records the controlling audit
dispositions. This plan translates that settled direction into six bounded
stages suitable for one gpt-5.6-terra coding prompt apiece.
All stages are pending and must be implemented in numeric order. Workstreams 2
and later are out of scope. In particular, this plan routes adapter-owned file
replacement through the existing internal/fileops owner but does not perform
the broader FileOps simplification reserved for Workstream 2.
Settled Implementation Decisions
The following decisions are final for this implementation:
- The maintained language baseline is Go 1.26. The module directive is
go 1.26.0; ordinary verification and scheduled CI use the officialgolang:1.26image, while tag CI uses the current explicitly pinned Go 1.26 patch image,golang:1.26.7. The release-workflow documentation check must assert that same pinned tag. Do not retain a Go 1.25 execution path. - Go 1.26 is adopted before code relies on
os.Process.WithHandle. Existing release semantics, supported build targets, and the rule that tag push—not asynchronous CI completion—is the synchronous release boundary do not change. golang.org/x/sync/errgroupis the only new dependency. Addgolang.org/x/sync v0.22.0as a direct dependency when transcription starts using it, then rungo mod tidy. Do not add an external subprocess, worker-pool, filesystem, or error framework.subprocess.Run,subprocess.RunRequest, andsubprocess.RunResultremain the adapter-facing contract. Their retained behavior includes environment allowlisting and overrides, sensitive-value redaction, separate or shared bounded diagnostics, timeouts and cancellation, process-tree termination, reaping, exit information, error wrapping, and tool-specific diagnostic hints.- Split the existing subprocess implementation inside the current
internal/adapters/subprocesspackage. Do not create a public API, a new repository package, or an external module. A private execution mechanism owns process-tree creation, start, wait, interruption, bounded termination, reaping, disposal, timestamps, exit state, and typed termination outcomes. The existingRunboundary remains the Narratio policy wrapper and owns request validation, timeout construction, environment selection, command arguments and working directory, diagnostic paths and modes, bounded and redacted sinks, public result mapping, diagnostic wording, and hints. - Represent the mechanism result with a private outcome type and private
reason enum that distinguish: not started/start failure, natural exit,
parent cancellation, deadline expiry, and external abort (currently a
diagnostic-size limit). Preserve the underlying start, wait, context, abort,
and cleanup errors as separate fields so the policy layer can retain
errors.Is/errors.Asbehavior and join secondary cleanup failures without replacing the primary classification. - The mechanism accepts an already policy-configured
*exec.Cmdand a generic<-chan errorabort signal. It must not importinternal/fileops, inspect environment variables or secrets, open diagnostic paths, know diagnostic byte limits or owners, marshal configuration, or construct Narratio-facing error text. The policy wrapper must not directly start, wait for, kill, or reap the command. - On Windows, continue creating the process suspended, configure a
kill-on-close Job Object, assign the child before resuming its initial
thread, and fail closed on every setup error. Perform assignment inside
cmd.Process.WithHandle, converting the callback'suintptrtowindows.Handleonly forwindows.AssignProcessToJobObject. Capture the callback's assignment error separately from the error returned byWithHandle. Remove the PID-basedwindows.OpenProcessand its associated process-handle close; do not change the thread-discovery/resumption or job disposal behavior. - Remove
WriteYAMLAtomicandWriteFileAtomicfrom the subprocess package. Each ofaudita,scriptorium, andseriatimowns a small privatewriteGeneratedYAMLhelper that marshals its generated configuration withgopkg.in/yaml.v3and installs the bytes throughfileops.WriteFileAtomicusingfileops.WorkspaceFileMode. Fake-adapter byte placeholders callfileops.WriteFileAtomicdirectly. Do not create a new generic adapter-I/O package merely to preserve the old wrapper. - Generated configuration must continue to omit API-key values. Preserve the current schemas and semantic content; incidental YAML key order and exact wrapper punctuation are not contracts.
- Replace the transcription channel/worker/first-error implementation with
errgroup.WithContextandSetLimit. Submit one closure per prepared audio job, use the errgroup context for every WhisperX call, and return speaker-qualified adapter, authoritative-output, and JSON-validation errors directly from that closure. Errgroup cancellation replaces the manual cancel-on-first-error path. - The transcription mutex protects successful shared aggregation only. It must not coordinate errors or adapter execution. Canonical materialization still begins only after every job succeeds and still occurs in sorted speaker order. A parent cancellation returns an error wrapping the parent context error and reports completed versus planned work; it does not need to retain the current incidental “dispatched” count.
- Tests protect observable outcomes and cross-platform compilation, not the exact internal helper graph, exact full diagnostic strings, or the literal syscall sequence. Native Windows behavior must not be claimed where only a Windows cross-compile is available.
- The original audits and decision records are historical inputs. Do not
rewrite them during implementation. Mark Workstream 1
Completeinrefactoring.mdonly after all six stages and final validation succeed.
Instructions For Every Stage
Before changing files in each stage:
- Read
docs/development.md, every document underdocs/policy/, this plan, and Workstream 1 plus the dependency and target-state sections ofrefactoring.md. - Consult the relevant entries H11, H19, H22, H24, H33, and L01 in
audit-decisions.md. Uselibrary-audit.mdonly for its subprocess-incubation rationale; its “extract later” possibility is not current scope. - Inspect the current implementation and focused tests named by the stage. Prefer the codebase knowledge graph for Go discovery and use text search for YAML, shell, Markdown, literals, and generated configuration.
- Confirm the worktree state and preserve unrelated changes. Do not begin a later workstream merely because adjacent code would also benefit from it.
During every stage:
- Keep package ownership and dependency direction consistent with
docs/policy/architecture.md. Prefer private types and focused files to a new architectural layer. - Keep API keys out of arguments where the adapter contract already uses the environment, and out of generated configuration, logs, errors, fixtures, and test output. Ordinary workspace files retain group-owned modes.
- Preserve supported Linux, macOS, and Windows behavior. Use build constraints for platform-specific process code and do not add a permissive unsupported- platform fallback.
- Keep tests deterministic, offline, and behavior-focused. Exercise configured
and observed concurrency, retain
errors.Is/errors.Asassertions where meaningful, and avoid freezing complete error wording or private helper choreography. - Update canonical current-behavior documentation in the same stage that makes a behavior current. Roadmap documents may describe future work; canonical docs may not.
- Run
gofmton changed Go files,go mod tidywhen module state changes, focused tests while iterating, andgit diff --checkbefore completing the stage. Mark a stageCompletedonly after all of its exit criteria pass.
Stage 1 — Establish The Go 1.26 Baseline
Status: Completed
Goal
Move every maintained build and release surface to Go 1.26 before introducing Go 1.26-only process APIs.
Required Work
- Change the module directive in
go.modfromgo 1.25.0togo 1.26.0. RunGOWORK=off go mod tidywith Go 1.26 and commit any resultinggo.mod/go.sumchanges. Do not addx/syncin this stage; it becomes a used direct dependency in Stage 5. - Update every Go step in
.woodpecker/verify.ymland.woodpecker/shuffle.ymltogolang:1.26. Update both Go steps in.woodpecker/release.ymltogolang:1.26.7. Preserve workflow triggers, dependency ordering, commands, release-plugin configuration, and the asynchronous release responsibility boundary. - Update
internal/doccheck/doccheck_test.goso the release workflow contract expectsgolang:1.26.7for validation and asset construction. Keep this check semantic: it should still validate the shared candidate checker, asset-builder delegation, step dependencies, and release-plugin settings. - Update Go-module fixture text under
internal/releasecheckfrom 1.25 to 1.26 where it represents an otherwise valid synthetic module. Do not change unrelated decimal values such as model/configuration examples. - Add a concise Go 1.26 prerequisite to
docs/development.mdanddocs/release.md. In the release document, distinguish the maintained Go 1.26 baseline from the currently pinned tag-CI patch image without changing the release procedure or promising that automation waits for CI. - Search maintained source, workflows, scripts, canonical docs, and semantic test fixtures for stale Go 1.25 baseline claims. Historical audit prose may remain historical; controlling roadmap text already records the approved Go 1.26 decision.
Exit Criteria
go env GOVERSIONreports a Go 1.26 toolchain andGOWORK=off go mod tidy -diffsucceeds.go test ./...,go test -race ./...,go vet ./..., andgo build ./...succeed.go test ./internal/doccheckandgo test ./internal/config -run '^TestExamplesLoadAndValidate$'succeed.- The six CGO-disabled release targets build through
scripts/build-release-assets.shinto a new temporary directory and the host version smoke check succeeds. - All maintained Go execution surfaces resolve to Go 1.26, and
git diff --checksucceeds.
Stage 2 — Separate Subprocess Lifecycle Mechanics From Policy
Status: Completed
Goal
Make the process-tree lifecycle an internally reusable, typed mechanism while keeping Narratio-specific environment, diagnostics, redaction, paths, and error presentation at the existing adapter-facing boundary.
Required Work
- Add
internal/adapters/subprocess/execution.go. Move the complete command lifecycle behind one private function that receives a context, an already configured*exec.Cmd, and a generic<-chan errorabort signal. The function must:- record UTC start/completion times and initialize an unknown exit code;
- reject an already-done context without starting a child;
- create the platform-owned process tree;
- apply the bounded stream
WaitDelaybefore start; - start exactly once through the tree owner;
- wait once in a buffered result channel;
- on cancellation, deadline, or abort, perform the existing bounded graceful/forceful termination and reaping sequence;
- dispose the tree on every path after creation; and
- return a private typed outcome containing its reason, timing, exit code, and separate start, wait, context, abort, and cleanup errors.
- Keep platform-specific process ownership in
process_tree_*.go. Reduceprocess_tree.goto the owned-tree interface, shared termination intervals, and narrowly reusable termination/reaping helpers needed byexecution.go. The lifecycle mechanism, rather thanRun, must be the only caller ofStart,cmd.Wait, tree termination, andDispose. - Change the diagnostic-limit notification channel in
diagnostics.goto the generic error channel accepted by the mechanism. Continue retaining the first concrete*captureLimitErrorseparately so policy formatting can name the stream, diagnostic owner, and configured limit after the command has been reaped. Preserve the exact byte caps, redaction, tail bounds, shared-log locking, flush, and close behavior. - Refactor
Runinrun.gointo the policy/composition layer. It must still:- validate the executable and normalize a nil parent context;
- create the optional timeout context;
- build the allowlisted, overridden child environment;
- select sensitive values and open bounded redacting log sinks;
- construct and configure
exec.Cmdarguments, working directory, environment, stdout, and stderr; - call the private execution mechanism exactly once;
- flush diagnostics, map the mechanism outcome to
RunResult, and format the existing useful start/exit/timeout/cancellation/capture/cleanup categories; and - preserve stderr-tail redaction, log paths, bad-descriptor hints, and error chains.
- Keep
RunRequestandRunResultsource-compatible for every adapter. Exact punctuation is not required, but existing callers and behavior tests must continue to receive useful executable/argument/path/exit context anderrors.Is/errors.Asaccess to cancellation, deadline,exec.ExitError, capture, and cleanup causes. - Reorganize
run_test.goandprocess_tree_supported_test.goonly where the new boundary makes ownership clearer. Retain publicRuncoverage for success, environment filtering/override, secrets, separate/shared logs, size limits, timeouts, cancellation, nonzero exit, hinting, and descendant cleanup. Add focused private-mechanism tests only for a typed distinction not already protected throughRun; do not duplicate the whole suite or assert private function names. - Update
internal/adapters/subprocess/doc.goanddocs/internal/adapters.mdto describe the implemented internal mechanism/policy split. Keep the package internal and do not describe it as a released reusable library.
Exit Criteria
- The lifecycle mechanism has no dependency on
fileops, YAML, environment selection, secret catalogs/redaction policy, diagnostic paths or modes, or Narratio-facing error wording. Rundoes not directly start, wait, terminate, reap, or dispose a process, and its public request/result surface is unchanged.go test ./internal/adapters/subprocessandgo test -race ./internal/adapters/subprocesspass on the native supported host.- Existing adapter subprocess tests for Audita, Scriptorium, Seriatim, and Notarius pass without loosening credential or diagnostic assertions.
go test ./...,go vet ./...,go build ./..., andgit diff --checksucceed.
Stage 3 — Use The Stable Windows Process Handle
Status: Completed
Goal
Eliminate PID reopening during Windows Job Object assignment while preserving the fail-closed suspended-child lifecycle.
Required Work
- In
process_tree_windows.go, keep Job Object creation andJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEconfiguration before child start. Keepwindows.CREATE_SUSPENDEDon the command. - After
cmd.Start, callcmd.Process.WithHandle. Inside its callback, callwindows.AssignProcessToJobObject(job, windows.Handle(handle))and store that assignment error in a callback-local variable visible afterWithHandlereturns. Treat aWithHandleerror and an assignment error as distinct setup failures with accurate context. - Delete the
windows.OpenProcesscall, the requested process access-mask constants, and the matchingwindows.CloseHandlefor the reopened process. Never retain or close the handle supplied to theWithHandlecallback. - Resume the initial thread only after successful assignment. Preserve
resumeInitialThread, the suspended process's PID-based thread enumeration, and the existing thread-handle lifetime. - On handle acquisition, assignment, or resume failure, kill and wait for the suspended child, close the not-yet-owned Job Object, and return an error that preserves the primary setup cause plus cleanup causes. On success, retain the Job Object in the tree and preserve its kill-on-close disposal behavior.
- Do not add a test that scans source text for
OpenProcessor mocks the standard library. Rely on Go 1.26 Windows compilation for API/type safety and the existing supported-host process-tree behavior tests for lifecycle semantics. If a native Windows runner is unavailable, state that limitation in the implementation-stage handoff rather than claiming runtime coverage.
Exit Criteria
process_tree_windows.gousesos.Process.WithHandlefor Job Object assignment and no longer reopens the child PID as a process handle.- CGO-disabled Windows
amd64andarm64builds succeed. - A Windows test binary for
./internal/adapters/subprocesscross-compiles withgo test -c; it is not executed on a non-Windows host. - Native-host subprocess tests, race tests, repository tests, vet, build, and
git diff --checksucceed. - The stage handoff accurately distinguishes cross-compilation from native Windows runtime evidence.
Stage 4 — Remove File Writing From The Subprocess Package
Status: Completed
Goal
Make internal/adapters/subprocess exclusively about command execution and
move generated configuration and fake-output persistence to the adapters that
own those files.
Required Work
- Add one unexported
writeGeneratedYAML(path string, payload any) errorhelper within each of theaudita,scriptorium, andseriatimadapter packages. A focusedgenerated_config.gofile is preferred in each package so production and fake runners share the same tool-owned serialization. - In each helper, marshal with
yaml.Marshal, wrap marshal failures with the destination path, and install the bytes throughfileops.WriteFileAtomic(path, data, fileops.WorkspaceFileMode). Preserve a path-bearing write error and the existing group-owned mode. Do not put API key values into the payload while moving code. - Replace every production and fake use of
subprocess.WriteYAMLAtomicwith its package-local helper. Replace every fake use ofsubprocess.WriteFileAtomicwith a directfileops.WriteFileAtomiccall, keeping the existing operation-specific caller context for outputs, reports, stdout, and stderr placeholders. - Remove
WriteYAMLAtomicandWriteFileAtomicfrominternal/adapters/subprocess/run.go, along with now-unused YAML, filesystem, mode, and fileops imports. Update the package comment so it claims only process execution/supervision and Narratio subprocess policy. - Delete the two generic YAML-writer tests from subprocess coverage. Do not recreate tests for a trivial private wrapper in all three packages. Instead, retain the existing Audita, Scriptorium, and Seriatim production/fake adapter tests that read generated configurations, check placeholder files, and assert that credentials are absent. Add a focused adapter test only if one migrated output is not already covered at its stable adapter boundary.
- Search all Go source to prove no caller or reference to the removed
subprocess writers remains. Do not move or simplify
fileopsitself; that package is the temporary focused replacement owner until Workstream 2.
Exit Criteria
- The subprocess package imports neither
gopkg.in/yaml.v3norinternal/fileopsand exports no file-writing helper. - All generated YAML and fake adapter files retain their schemas/content, workspace mode, overwrite behavior, and contextual failures.
- Audita, Scriptorium, and Seriatim production and fake adapter tests pass, including credential-absence checks.
- Subprocess tests pass after removing only writer-specific coverage.
go test ./...,go test -race ./internal/adapters/...,go vet ./...,go build ./..., andgit diff --checksucceed.
Stage 5 — Replace The Transcription Worker Pool With Errgroup
Status: Pending
Goal
Use the maintained bounded-concurrency primitive for per-speaker WhisperX work while preserving cancellation, all-or-nothing canonical materialization, and deterministic results.
Required Work
- Add
golang.org/x/sync v0.22.0as a direct dependency and runGOWORK=off go mod tidy. Importgolang.org/x/sync/errgroupininternal/stage/transcribe.go. - Preserve all pre-concurrency behavior: environment validation, session and run-layout resolution, prepared-audio discovery, positive/default concurrency calculation, duplicate speaker rejection, and deterministic job construction.
- Replace the explicit
context.WithCancel, worker count, jobs channel,sync.WaitGroup, dispatch loop, and mutex-protectedfirstErrwith:group, groupCtx := errgroup.WithContext(ctx);group.SetLimit(concurrency); and- one
group.Goclosure per prepared job, capturing a per-iteration job value and checkinggroupCtxbefore beginning expensive work.
- Each closure calls
env.WhisperX.TranscribewithgroupCtx, validates the authoritative output path, validates the run-local transcript JSON, and returns immediately on failure. Every returned failure must identify the speaker; adapter failures should also retain the audio basename, and output failures should retain the relevant output path. Do not store a shared first error or call a manual cancel function. - On success, build that speaker's metadata locally, then lock only long
enough to add it to
perFileand incrementcompleted. Do not keep a shared map of output refs: each job's planned run-local path is already in the immutable job list and can be used aftergroup.Waitsucceeds. - After submissions, call
group.Waitexactly once. Readcompletedunder the same mutex before constructing any diagnostic. Apply this result order:- if the parent
ctx.Err()is non-nil, return a transcribe cancellation error that reports completed and total jobs and wraps that parent error; - otherwise, if
group.Waitreturned an error, return its speaker-qualified failure; and - otherwise, require
completed == len(jobs)as an internal completeness guard. A closure admitted after errgroup cancellation may return the group context error without invoking WhisperX; the first originating errgroup error remains authoritative.
- if the parent
- Preserve the post-success transaction boundary. Sort speaker IDs, then materialize each planned run-local output to the canonical transcript path serially in that order. Preserve ordered outputs, run/canonical path metadata, per-file metadata, retry/language/timeout metadata, and the rule that no canonical partial output is installed after a job failure or parent cancellation.
- Update
internal/stage/transcribe_test.goto protect the refactored outcomes:- configured concurrency remains the observed maximum and never exceeds the configured limit;
- a speaker failure is returned with speaker context and cancels/blockades remaining work without deadlock;
- invalid JSON and authoritative-output mismatch remain speaker-qualified;
- parent cancellation wraps
context.Canceled, reports completed/planned context, and installs no partial canonical output; - an already-canceled context makes no WhisperX request; and
- successful results and metadata remain deterministic by speaker. Use synchronization channels rather than timing alone where a new failure- cancellation test needs ordering.
- Update
docs/internal/stage-transcribe.mdto describe bounded per-job errgroup execution and all-or-nothing canonical materialization. Keepdocs/integrations/whisperx.mdimplementation-neutral unless a retained integration contract actually changed.
Exit Criteria
transcribe.gocontains no manual worker channel, worker goroutine loop, shared first-error slot, or manual stage-cancel function.go.modrecordsgolang.org/x/sync v0.22.0directly andGOWORK=off go mod tidy -diffsucceeds.go test ./internal/stage -run '^TestTranscribeStage'andgo test -race ./internal/stage -run '^TestTranscribeStage'pass repeatedly, including with-shuffle=on -count=10.go test ./...,go test -race ./...,go vet ./...,go build ./..., andgit diff --checksucceed.
Stage 6 — Integrated Validation And Workstream Closure
Status: Pending
Goal
Verify the combined baseline, process, adapter-file, and transcription changes at every maintained repository boundary and close only this workstream.
Required Work
- Review the complete Workstream 1 diff against this plan and
refactoring.md. Remove obsolete imports, helpers, comments, tests, and documentation claims introduced solely by the replaced mechanisms. Do not opportunistically implement Workstream 2 filesystem simplification or external subprocess-library extraction. - Confirm canonical documentation is current:
docs/development.mdanddocs/release.mdidentify Go 1.26;docs/internal/adapters.mddescribes the internal mechanism/policy split and retained process-tree semantics;docs/internal/stage-transcribe.mddescribes bounded per-job execution, cancellation, deterministic ordering, and all-or-nothing canonical materialization; and- integration documents retain accurate external contracts without exposing unnecessary private implementation detail.
- Search maintained code and configuration for stale Go 1.25 build-image or baseline assertions, PID-based Windows process reopening, removed subprocess writer calls, and the old transcription worker machinery. Exclude historical audit descriptions and unrelated numeric values from mechanical rewrites.
- Run the complete release-candidate-equivalent local validation with Go 1.26:
GOWORK=off go mod tidy -diff;gofmt -lacross tracked Go files, requiring no output;git diff --checkandgit diff --cached --check;go test -count=1 ./...;go test -race -count=1 ./...;go vet ./...;go build ./...;go test -count=1 ./internal/doccheck;go test -count=1 ./internal/config -run '^TestExamplesLoadAndValidate$'; andscripts/build-release-assets.shwith a valid synthetic version and a fresh absolute temporary output directory, proving all six official CGO-disabled targets compile and the host smoke binary reports that version.
- Run the scheduled-CI stress command
go test -race -shuffle=on -count=3 ./.... If a failure is genuinely flaky, diagnose and correct the retained concurrency contract; do not weaken or skip the test to close the stage. - After every check succeeds, change only Workstream 1's status in
docs/roadmap/refactoring.mdfromPendingtoComplete. Leave all later workstreams pending and leave historical audit decisions intact. Update the status of all six stages in this document toCompletedas implementation history.
Exit Criteria
- All commands above succeed from a clean, Go 1.26 repository checkout.
- Linux/macOS native-host evidence and Windows/macOS cross-build evidence are reported accurately; no unrun native platform test is claimed.
- No API key value appears in generated files, diagnostics, errors, fixtures, or the diff.
- Workstream 1's consolidated target state is fully implemented, its status is
Complete, every later workstream remainsPending, and the worktree is clean after the implementation-stage commit.
Open Questions
None. The audits, refactoring roadmap, approved Go 1.26 revision, and decisions above resolve the product, compatibility, dependency, ownership, testing, and platform choices required to implement this workstream.