← back to Cli Printing Press
fix(cli): stamp run_id into manifest at generate time (#606)
37ee7e38b6b3b9f6b989fa8a52172cee8cb104c8 · 2026-05-05 00:26:44 -0700 · Trevin Chow
* fix(cli): stamp run_id into manifest at generate time
Belt-and-suspenders fix for the missing run_id failure mode that's been
hitting ~12% of v3.x library CLIs (allrecipes, yahoo-finance, trigger-dev).
Without run_id in .printing-press.json, printing-press dogfood --live
--write-acceptance refuses to write the gate marker and lock promote
refuses to promote — the canonical pipeline halts.
Two halves, one PR (different review surfaces but same root cause):
1. Generator (internal/pipeline/climanifest.go, internal/cli/root.go).
GenerateManifestParams gains a RunID field; WriteManifestForGenerate
stamps it into the emitted manifest. Both root.go call sites
(:161, :321) derive the value via DeriveRunIDFromResearchDir, which
extracts the basename of --research-dir when it matches ^\d{8}-\d{6}$
(the canonical pipeline run_id shape). When derivation yields empty,
the call site emits a one-line stderr warning that phase5 acceptance
will refuse to write without it — deliberate signal, not a blocker.
The empty fallback preserves the legacy --output-only path; the
warning surfaces on every generate without --research-dir, which is
why the four generate-* golden cases need re-baselining (separate
commit).
2. Skill template (skills/printing-press/SKILL.md). The state.json schema
now includes "run_id": "$RUN_ID", consistent with RUN_ID="$(date
+%Y%m%d-%H%M%S)" already computed earlier in Run Initialization.
Persisting run_id in state.json keeps /printing-press-score and any
future state-loading consumer in sync with the value the manifest
now carries — without depending on the --research-dir naming
convention.
Tests cover the positive case (RunID stamped through), the omitempty
case (empty RunID produces no run_id field in the JSON), and the
DeriveRunIDFromResearchDir derivation across canonical, edge, and
non-matching shapes.
Closes #591 (which consolidated #588).
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U2 + U3)
* test(cli): re-baseline generate goldens for run_id warning
The generate command now emits a stderr warning when --research-dir is
absent or its basename does not match ^\d{8}-\d{6}$ (the canonical
run_id shape). All four generate-* golden cases run without
--research-dir, so they pick up the new warning line. Each golden
gains exactly one prefix line:
warning: could not derive run_id from --research-dir; phase5 dogfood
acceptance will refuse to write without it
Verified via scripts/golden.sh verify after update — all 11 cases pass.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U2)
Files touched
M internal/cli/root.goM internal/pipeline/climanifest.goM internal/pipeline/climanifest_test.goM skills/printing-press/SKILL.mdM testdata/golden/expected/generate-golden-api-oauth2-cc/stderr.txtM testdata/golden/expected/generate-golden-api-unicode/stderr.txtM testdata/golden/expected/generate-golden-api/stderr.txtM testdata/golden/expected/generate-tier-routing-api/stderr.txt
Diff
commit 37ee7e38b6b3b9f6b989fa8a52172cee8cb104c8
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 5 00:26:44 2026 -0700
fix(cli): stamp run_id into manifest at generate time (#606)
* fix(cli): stamp run_id into manifest at generate time
Belt-and-suspenders fix for the missing run_id failure mode that's been
hitting ~12% of v3.x library CLIs (allrecipes, yahoo-finance, trigger-dev).
Without run_id in .printing-press.json, printing-press dogfood --live
--write-acceptance refuses to write the gate marker and lock promote
refuses to promote — the canonical pipeline halts.
Two halves, one PR (different review surfaces but same root cause):
1. Generator (internal/pipeline/climanifest.go, internal/cli/root.go).
GenerateManifestParams gains a RunID field; WriteManifestForGenerate
stamps it into the emitted manifest. Both root.go call sites
(:161, :321) derive the value via DeriveRunIDFromResearchDir, which
extracts the basename of --research-dir when it matches ^\d{8}-\d{6}$
(the canonical pipeline run_id shape). When derivation yields empty,
the call site emits a one-line stderr warning that phase5 acceptance
will refuse to write without it — deliberate signal, not a blocker.
The empty fallback preserves the legacy --output-only path; the
warning surfaces on every generate without --research-dir, which is
why the four generate-* golden cases need re-baselining (separate
commit).
2. Skill template (skills/printing-press/SKILL.md). The state.json schema
now includes "run_id": "$RUN_ID", consistent with RUN_ID="$(date
+%Y%m%d-%H%M%S)" already computed earlier in Run Initialization.
Persisting run_id in state.json keeps /printing-press-score and any
future state-loading consumer in sync with the value the manifest
now carries — without depending on the --research-dir naming
convention.
Tests cover the positive case (RunID stamped through), the omitempty
case (empty RunID produces no run_id field in the JSON), and the
DeriveRunIDFromResearchDir derivation across canonical, edge, and
non-matching shapes.
Closes #591 (which consolidated #588).
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U2 + U3)
* test(cli): re-baseline generate goldens for run_id warning
The generate command now emits a stderr warning when --research-dir is
absent or its basename does not match ^\d{8}-\d{6}$ (the canonical
run_id shape). All four generate-* golden cases run without
--research-dir, so they pick up the new warning line. Each golden
gains exactly one prefix line:
warning: could not derive run_id from --research-dir; phase5 dogfood
acceptance will refuse to write without it
Verified via scripts/golden.sh verify after update — all 11 cases pass.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U2)
---
internal/cli/root.go | 10 ++++
internal/pipeline/climanifest.go | 24 ++++++++++
internal/pipeline/climanifest_test.go | 55 ++++++++++++++++++++++
skills/printing-press/SKILL.md | 3 ++
.../generate-golden-api-oauth2-cc/stderr.txt | 1 +
.../generate-golden-api-unicode/stderr.txt | 1 +
.../golden/expected/generate-golden-api/stderr.txt | 1 +
.../expected/generate-tier-routing-api/stderr.txt | 1 +
8 files changed, 96 insertions(+)
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 50d66bd8..8e634157 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -158,11 +158,16 @@ func newGenerateCmd() *cobra.Command {
return err
}
+ runID := pipeline.DeriveRunIDFromResearchDir(researchDir)
+ if runID == "" {
+ fmt.Fprintln(os.Stderr, "warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it")
+ }
if err := pipeline.WriteManifestForGenerate(pipeline.GenerateManifestParams{
APIName: parsed.Name,
DocsURL: docsURL,
OutputDir: absOut,
Owner: parsed.Owner,
+ RunID: runID,
Spec: parsed,
NovelFeatures: novelFeatures,
}); err != nil {
@@ -318,12 +323,17 @@ func newGenerateCmd() *cobra.Command {
}
}
+ runID := pipeline.DeriveRunIDFromResearchDir(researchDir)
+ if runID == "" {
+ fmt.Fprintln(os.Stderr, "warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it")
+ }
if err := pipeline.WriteManifestForGenerate(pipeline.GenerateManifestParams{
APIName: apiSpec.Name,
SpecSrcs: specFiles,
SpecURL: specURL,
OutputDir: absOut,
Owner: apiSpec.Owner,
+ RunID: runID,
Spec: apiSpec,
NovelFeatures: novelFeatures,
}); err != nil {
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 264a49f3..dc20f28a 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"reflect"
+ "regexp"
"sort"
"strings"
"time"
@@ -330,10 +331,32 @@ type GenerateManifestParams struct {
DocsURL string // --docs URL, if used
OutputDir string
Owner string // resolved owner attribution (manifest preserve > copyright parse > git config)
+ RunID string // YYYYMMDD-HHMMSS, derived from --research-dir basename when empty
Spec *spec.APISpec // parsed spec for MCP metadata (nil if unavailable)
NovelFeatures []NovelFeatureManifest // transcendence features from research (nil if unavailable)
}
+// runIDPattern matches the canonical pipeline run_id shape: YYYYMMDD-HHMMSS.
+// When an arbitrary path basename happens to match this pattern, treat it as
+// a real run_id; otherwise fall back to empty (and warn at the call site).
+var runIDPattern = regexp.MustCompile(`^\d{8}-\d{6}$`)
+
+// DeriveRunIDFromResearchDir extracts a canonical run_id from a research-dir
+// path, or returns "" when no valid run_id can be derived. The standalone
+// generate command does not load a PipelineState, so it cannot reach
+// state.RunID directly; the basename of --research-dir is the only structured
+// signal available without a state-loading refactor.
+func DeriveRunIDFromResearchDir(researchDir string) string {
+ if researchDir == "" {
+ return ""
+ }
+ base := filepath.Base(researchDir)
+ if runIDPattern.MatchString(base) {
+ return base
+ }
+ return ""
+}
+
// WriteManifestForGenerate writes a .printing-press.json manifest into the
// generated CLI directory. This is the generate-command counterpart of
// writeCLIManifestForPublish (which operates on PipelineState).
@@ -344,6 +367,7 @@ func WriteManifestForGenerate(p GenerateManifestParams) error {
PrintingPressVersion: version.Version,
APIName: p.APIName,
CLIName: naming.CLI(p.APIName),
+ RunID: p.RunID,
Owner: p.Owner,
}
diff --git a/internal/pipeline/climanifest_test.go b/internal/pipeline/climanifest_test.go
index 199b68d2..60f09fa3 100644
--- a/internal/pipeline/climanifest_test.go
+++ b/internal/pipeline/climanifest_test.go
@@ -474,6 +474,61 @@ func TestWriteManifestForGenerateNoSpec(t *testing.T) {
assert.Empty(t, got.SpecChecksum)
}
+func TestWriteManifestForGenerateStampsRunID(t *testing.T) {
+ dir := t.TempDir()
+
+ err := WriteManifestForGenerate(GenerateManifestParams{
+ APIName: "runid-test",
+ RunID: "20260504-190931",
+ OutputDir: dir,
+ })
+ require.NoError(t, err)
+
+ data, err := os.ReadFile(filepath.Join(dir, CLIManifestFilename))
+ require.NoError(t, err)
+
+ var got CLIManifest
+ require.NoError(t, json.Unmarshal(data, &got))
+ assert.Equal(t, "20260504-190931", got.RunID)
+}
+
+func TestWriteManifestForGenerateOmitsEmptyRunID(t *testing.T) {
+ dir := t.TempDir()
+
+ err := WriteManifestForGenerate(GenerateManifestParams{
+ APIName: "norunid-test",
+ OutputDir: dir,
+ })
+ require.NoError(t, err)
+
+ data, err := os.ReadFile(filepath.Join(dir, CLIManifestFilename))
+ require.NoError(t, err)
+ // run_id has the omitempty tag; empty value must not appear in serialized JSON.
+ assert.NotContains(t, string(data), `"run_id"`)
+}
+
+func TestDeriveRunIDFromResearchDir(t *testing.T) {
+ cases := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {"canonical run_id basename", "/tmp/runs/20260504-190931", "20260504-190931"},
+ {"trailing slash", "/tmp/runs/20260504-190931/", "20260504-190931"},
+ {"basename only", "20260101-000000", "20260101-000000"},
+ {"empty input", "", ""},
+ {"non-matching basename", "/tmp/runs/research", ""},
+ {"partial match (date only)", "/tmp/runs/20260504", ""},
+ {"longer suffix", "/tmp/runs/20260504-190931-x", ""},
+ {"wrong shape (T separator)", "/tmp/runs/20260504T190931", ""},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, DeriveRunIDFromResearchDir(tc.input))
+ })
+ }
+}
+
func TestArchiveRunArtifactsCopiesDiscovery(t *testing.T) {
home := setPressTestEnv(t)
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index e3053976..d484faff 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -406,12 +406,15 @@ Maintain a lightweight state file at `$STATE_FILE` so `/printing-press-score` ca
```json
{
"api_name": "<api>",
+ "run_id": "$RUN_ID",
"working_dir": "$CLI_WORK_DIR",
"output_dir": "$CLI_WORK_DIR",
"spec_path": "<absolute spec path if known>"
}
```
+`run_id` is the same `YYYYMMDD-HHMMSS` value computed earlier as `RUN_ID="$(date +%Y%m%d-%H%M%S)"`. The generator's manifest writer derives the same value from the `--research-dir` basename when generate is invoked through the canonical `$API_RUN_DIR` (whose basename equals `$RUN_ID`); persisting it in `state.json` here keeps `/printing-press-score` and any future state-loading consumer in sync. Without `run_id` in either path, `printing-press dogfood --live --write-acceptance` refuses to write the gate marker.
+
Do not create a `go.work` file in `$CLI_WORK_DIR`. Generated modules must build and test as standalone modules; a mismatched workspace `go` directive can break Go 1.25+ toolchains and lefthook checks. Editor/gopls workspace noise is cosmetic and must not be traded for broken `go build` or `go test`.
There are exactly three durable writable locations. Every generated artifact this
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/stderr.txt b/testdata/golden/expected/generate-golden-api-oauth2-cc/stderr.txt
index 7bcea578..29ff4136 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/stderr.txt
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/stderr.txt
@@ -1 +1,2 @@
+warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it
Generated printing-press-oauth2 at <ARTIFACT_DIR>/generate-golden-api-oauth2-cc/printing-press-oauth2-cc
diff --git a/testdata/golden/expected/generate-golden-api-unicode/stderr.txt b/testdata/golden/expected/generate-golden-api-unicode/stderr.txt
index 45c603f2..fd2178ca 100644
--- a/testdata/golden/expected/generate-golden-api-unicode/stderr.txt
+++ b/testdata/golden/expected/generate-golden-api-unicode/stderr.txt
@@ -1 +1,2 @@
+warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it
Generated cafe-bistro at <ARTIFACT_DIR>/generate-golden-api-unicode/cafe-bistro
diff --git a/testdata/golden/expected/generate-golden-api/stderr.txt b/testdata/golden/expected/generate-golden-api/stderr.txt
index 5c2fa18c..60a9fb00 100644
--- a/testdata/golden/expected/generate-golden-api/stderr.txt
+++ b/testdata/golden/expected/generate-golden-api/stderr.txt
@@ -1 +1,2 @@
+warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it
Generated printing-press-golden at <ARTIFACT_DIR>/generate-golden-api/printing-press-golden
diff --git a/testdata/golden/expected/generate-tier-routing-api/stderr.txt b/testdata/golden/expected/generate-tier-routing-api/stderr.txt
index 1d686931..0b18592d 100644
--- a/testdata/golden/expected/generate-tier-routing-api/stderr.txt
+++ b/testdata/golden/expected/generate-tier-routing-api/stderr.txt
@@ -1 +1,2 @@
+warning: could not derive run_id from --research-dir; phase5 dogfood acceptance will refuse to write without it
Generated tier-routing-golden at <ARTIFACT_DIR>/generate-tier-routing-api/tier-routing-golden
← 7359a18f fix(cli): align phase5 quick gate with runner verdict (#605)
·
back to Cli Printing Press
·
fix(cli): route sync --json events to stdout (#608) 6281f2be →