Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Consolidation Runtime Environment Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Allow the standalone duplicate-draft consolidation CLI to compose its production adapters without weakening explicit environment validation.

**Architecture:** Preserve whether `environment` was explicitly supplied to the CLI. Route all three CLI modes through one adapter-option builder that omits an implicit environment, allowing the adapter's existing runtime snapshot to own `process.env`.

**Tech Stack:** Node.js 24, ESM, `node:test`, pnpm, Biome

---

### Task 1: Preserve the runtime environment boundary

**Files:**
- Modify: `scripts/release/duplicate-draft-consolidation-cli.mjs`
- Test: `scripts/release/test/duplicate-draft-consolidation-cli.test.mjs`

- [x] **Step 1: Write the failing three-mode environment regressions**

Add table-driven CLI cases for `inspect`, `perform`, and `verify`. For each mode:

1. omit the top-level `environment` option, capture every options object passed
to `createAdapters`, and assert none has an own `environment` property;
2. pass one frozen plain environment explicitly and assert the identical object
reaches every `createAdapters` call; and
3. pass `environment: undefined` explicitly and assert it remains an own field
in the composition options so the adapter's strict explicit-input parser,
rather than the runtime path, owns rejection.

For `perform`, make the successful dependency invoke `createAdapters()` once
without a budget and once with a frozen request budget. Assert both calls obey
the environment rule and the second preserves the exact budget object. Keep
mode-specific operation dependencies successful so the tests observe only the
composition boundary.

- [x] **Step 2: Prove the regression is red**

Run:

```bash
PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test \
--test-name-pattern='preserves the environment boundary across every mode' \
scripts/release/test/duplicate-draft-consolidation-cli.test.mjs
```

Expected: FAIL because the current CLI explicitly forwards `process.env`.

- [x] **Step 3: Add the minimal shared composition helper**

Keep `environment` absent from normalized CLI options unless it was supplied by
the caller. Add one helper that returns:

```js
{
cwd: invocation.cwd,
...(Object.hasOwn(invocation, "environment")
? { environment: invocation.environment }
: {}),
dependencies: { now },
...(requestBudget === undefined ? {} : { requestBudget }),
}
```

Use it for `inspect`, `perform`, and `verify`. Do not change adapter validation.

- [x] **Step 4: Prove all environment and budget cases are green**

Run the table-driven regression and require all default, explicit-frozen, and
explicit-undefined cases to pass for all three modes. Require both perform
adapter calls to preserve the exact request-budget behavior.

- [x] **Step 5: Run focused verification**

Run the CLI test, the complete duplicate-draft consolidation suite, scoped
repository-configured Biome, `node scripts/check-docs.mjs`, and
`git diff --check`. Expected: all pass.

- [x] **Step 6: Run full validation and commit**

Run:

```bash
PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \
DAWN_REQUIRE_DOCKER=1 pnpm ci:validate
```

Expected: all Definition of Done gates pass. Commit the design, plan, test, and
implementation with a factual message, push a focused PR, and require exact-head
CI before merge.

### Task 2: Resume live read-only inspection

**Files:**
- Live private output: `.dawn/release/duplicate-draft-consolidation.proposed.json`

- [ ] **Step 1: Refresh exact merged-main authority**

Require clean symbolic `main` and identical local HEAD, `origin/main`, and
GitHub default-branch SHAs. Confirm Release remains disabled and no nonterminal
Release run exists.

- [ ] **Step 2: Retry the exact production inspect command**

Run the incident-scoped `pnpm release:consolidate-drafts inspect` command from
the merged main checkout. Expected: a canonical private proposal and bounded
safe summary; zero writer calls.

- [ ] **Step 3: Independently validate the proposal**

Confirm mode `0600`, canonical envelope parsing, exact survivor and ordered
duplicates, and the printed record digest before entering the separate live
mutation freeze.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Consolidation Runtime Environment Design

**Status:** Implemented and locally verified; merge and live retry pending

**Date:** 2026-09-02

**Scope:** Production composition for the duplicate-draft consolidation CLI

## Problem

The standalone consolidation CLI defaults `environment` to Node's `process.env`
and then passes that value explicitly to
`createDuplicateDraftConsolidationAdapters`. The adapter treats explicit
environment overrides as untrusted caller data and therefore requires a plain
data object. Node's host-owned `process.env` object is not a plain object, so
production `inspect` stops during adapter composition before any live read.

The adapter already owns a separate runtime path: when the `environment` option
is absent, it snapshots `process.env` with runtime-specific validation and
allowlisting. Tests that explicitly inject an environment must continue through
the stricter caller-data path.

## Considered approaches

1. **Omit an implicit environment at the CLI-to-adapter boundary.** Preserve
whether the caller supplied `environment`; pass it only when explicit. The
adapter then uses its existing runtime snapshot for standalone execution.
2. Copy `process.env` into a plain object in the CLI. This duplicates environment
ownership and allowlisting across layers.
3. Let the adapter's explicit-override parser accept `process.env`. This weakens
the distinction between host-owned runtime state and injected caller data.

## Decision

Use approach 1. The CLI will preserve option presence and build one exact
adapter-composition object for `inspect`, `perform`, and `verify`. It includes
`environment` only when the caller explicitly supplied it, and continues to add
the request budget only when present. No new flag, override, fallback, or
compatibility path is introduced.

## Safety and verification

- Add a table-driven regression proving the standalone/default CLI omits
`environment` for `inspect`, `perform`, and `verify`.
- For all three modes, prove an explicitly injected frozen plain environment is
still forwarded by identity, while explicit `environment: undefined` remains
explicit and reaches strict adapter rejection.
- For `perform`, cover both initial composition and the request-budget path.
- Run the focused CLI and consolidation suites, repository-configured static
checks, documentation checks, and the full validation lane before merge.
- After merge, retry the exact read-only live `inspect` command. Any later live
authority drift still stops through the existing fail-closed workflow.
31 changes: 13 additions & 18 deletions scripts/release/duplicate-draft-consolidation-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,7 @@ export async function runDuplicateDraftConsolidationCli(options = {}) {
if (input.mode === "inspect") {
result = await inspect(input.value, {
repositoryRoot: invocation.cwd,
adapters: await createAdapters({
cwd: invocation.cwd,
environment: invocation.environment,
dependencies: { now },
}),
adapters: await createAdapters(adapterCompositionOptions(invocation, now)),
now,
wait,
repositoryRootIdentity,
Expand All @@ -83,24 +79,14 @@ export async function runDuplicateDraftConsolidationCli(options = {}) {
result = await perform(input.value, {
repositoryRoot: invocation.cwd,
createAdapters: (requestBudget) =>
createAdapters({
cwd: invocation.cwd,
environment: invocation.environment,
dependencies: { now },
...(requestBudget === undefined ? {} : { requestBudget }),
}),
createAdapters(adapterCompositionOptions(invocation, now, requestBudget)),
now,
wait,
})
} else {
result = await verify(input.value, {
repositoryRoot: invocation.cwd,
createAdapters: () =>
createAdapters({
cwd: invocation.cwd,
environment: invocation.environment,
dependencies: { now },
}),
createAdapters: () => createAdapters(adapterCompositionOptions(invocation, now)),
})
}
const summary =
Expand Down Expand Up @@ -260,7 +246,7 @@ function normalizeOptions(options) {
const result = {
argv: values.argv ?? process.argv.slice(2),
cwd,
environment: values.environment ?? process.env,
...(Object.hasOwn(values, "environment") ? { environment: values.environment } : {}),
stdout,
stderr,
dependencies,
Expand All @@ -274,6 +260,15 @@ function normalizeOptions(options) {
return result
}

function adapterCompositionOptions(invocation, now, requestBudget) {
return {
cwd: invocation.cwd,
...(Object.hasOwn(invocation, "environment") ? { environment: invocation.environment } : {}),
dependencies: { now },
...(requestBudget === undefined ? {} : { requestBudget }),
}
}

function bindSink(value) {
if (value === null || typeof value !== "object" || utilTypes.isProxy(value)) {
throw new InvocationError()
Expand Down
107 changes: 107 additions & 0 deletions scripts/release/test/duplicate-draft-consolidation-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,67 @@ test("CLI threads each exact convergence request budget into production adapter
assert.equal(adapterOptions.requestBudget, requestBudget)
})

test("CLI preserves the environment boundary across every mode", async (t) => {
const explicitEnvironment = Object.freeze({ HOME: "/fixture/home", PATH: "/fixture/bin" })
const controller = new AbortController()
const requestBudget = Object.freeze({
operation: "release",
timeoutMs: 12_345,
signal: controller.signal,
})
const modes = Object.freeze([
Object.freeze({ name: "inspect", argv: COMMAND }),
Object.freeze({ name: "perform", argv: PERFORM_COMMAND }),
Object.freeze({ name: "verify", argv: VERIFY_COMMAND }),
])
const environmentCases = Object.freeze([
Object.freeze({ name: "omitted", expected: null }),
Object.freeze({
name: "explicit frozen",
value: explicitEnvironment,
expected: explicitEnvironment,
}),
Object.freeze({ name: "explicit undefined", value: undefined, expected: undefined }),
])

for (const mode of modes) {
for (const environmentCase of environmentCases) {
await t.test(`${mode.name} ${environmentCase.name}`, async () => {
const adapterCalls = []
const options = {
argv: mode.argv,
cwd: process.cwd(),
stdout: sink(),
stderr: sink(),
dependencies: environmentBoundaryDependencies({
mode: mode.name,
requestBudget,
adapterCalls,
}),
}
if (environmentCase.name !== "omitted") {
options.environment = environmentCase.value
}

assert.equal(await runDuplicateDraftConsolidationCli(options), 0)
assert.equal(adapterCalls.length, mode.name === "perform" ? 2 : 1)
for (const adapterOptions of adapterCalls) {
if (environmentCase.name === "omitted") {
assert.equal(Object.hasOwn(adapterOptions, "environment"), false)
} else {
assert.equal(Object.hasOwn(adapterOptions, "environment"), true)
assert.equal(adapterOptions.environment, environmentCase.expected)
}
}
if (mode.name === "perform") {
assert.equal(Object.hasOwn(adapterCalls[0], "requestBudget"), false)
assert.equal(adapterCalls[1].requestBudget, requestBudget)
}
})
}
}
})

test("CLI perform rejects digest, confirmation, path, force, survivor, and reordered-ID variants", async () => {
for (const argv of [
PERFORM_COMMAND.with(8, CONFIRMATION.replace(PROPOSAL_SHA256, "A".repeat(64))),
Expand Down Expand Up @@ -580,6 +641,52 @@ function successfulDependencies() {
}
}

function environmentBoundaryDependencies({ mode, requestBudget, adapterCalls }) {
const dependencies = {
async createAdapters(options) {
adapterCalls.push(options)
return Object.freeze({})
},
}
if (mode === "inspect") {
dependencies.inspect = async (input) =>
Object.freeze({
proposalSha256: "a".repeat(64),
version: input.version,
commitSha: input.commitSha,
survivor: input.survivor,
duplicates: Object.freeze([...input.duplicates]),
output: input.output,
})
} else if (mode === "perform") {
dependencies.perform = async (_input, operations) => {
await operations.createAdapters()
await operations.createAdapters(requestBudget)
return Object.freeze({
status: "complete",
survivor: "379991871",
deleted: Object.freeze(["379982100", "379986168"]),
receipt: "scripts/release/duplicate-draft-consolidation.json",
receiptSha256: "b".repeat(64),
})
}
} else {
dependencies.verify = async (_input, operations) => {
await operations.createAdapters()
return Object.freeze({
status: "verified",
survivor: "379991871",
deleted: Object.freeze(["379982100", "379986168"]),
receipt: "scripts/release/duplicate-draft-consolidation.json",
receiptSha256: "c".repeat(64),
historicalParity:
"Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.",
})
}
}
return dependencies
}

function failingWritable(message) {
return new Writable({
write(_chunk, _encoding, callback) {
Expand Down
Loading