Skip to content

[Bug]: summary-confirmation guard is unsatisfiable on zero-Unit per-unit stages — summaryQuestionFiles scans only construction/<unit>/<stage>/, ignoring the stage-level path the engine itself resolves (refactor, infra, security-patch) #1020

Description

@leeyoseph-dev

Description

On a zero-Unit run (a scope where Units Generation is SKIP), a per-unit stage that declares summary_confirmation: required can never be completed. The engine writes the stage's artifacts — including its <stage>-questions.md — to the stage-level path, but the summary-confirmation guard looks for that questions file only under per-unit paths. The file exists, the human answered it, the receipt was minted; the guard still refuses with "its question flow has no <stage>-questions.md file".

The zero-Unit escape hatch already exists in the codebase (usesStageLevelPerUnitArtifacts) and is applied by the artifact resolver — it is simply not applied to the questions-file scanner.

Mechanism

Everything below is on a277af218f0d (current main).

1. The writer resolves the zero-Unit path. resolveArtifactInstances (core/tools/aidlc-artifact-resolution.ts:219-251): for a per-unit stage, when usesStageLevelPerUnitArtifacts(scope, stateContent) is true it returns a single instance at join(record, owner.phase, owner.slug, filename) — i.e. <record>/construction/<stage>/…, with no Unit segment. code-generation.md documents this shape explicitly ("with no synthetic Unit segment").

usesStageLevelPerUnitArtifacts (core/tools/aidlc-lib.ts:21910-21915) is exactly the zero-Unit predicate:

export function usesStageLevelPerUnitArtifacts(scope, stateContent): boolean {
  return effectivePlanAction("units-generation", scope, stateContent) !== "EXECUTE";
}

2. The reader does not. summaryQuestionFiles (core/tools/aidlc-lib.ts:6971-6997) branches only on isPerUnitStage(stage) (:113-115, i.e. for_each: unit-of-work):

function summaryQuestionFiles(projectDir, stage): SummaryQuestionFile[] {
  const rec = recordDir(projectDir);
  if (rec === null) return [];
  if (!isPerUnitStage(stage)) {
    return questionFilesInDir(join(rec, stage.phase, stage.slug), null);   // stage-level
  }
  const constructionDir = join(rec, "construction");
  ...
  for (const unit of readdirSync(constructionDir).sort()) {
    files.push(...questionFilesInDir(join(constructionDir, unit, stage.slug), unit));
  }
  ...
}

It takes no scope/stateContent parameter, so it cannot consult usesStageLevelPerUnitArtifacts at all. On a zero-Unit run the artifacts sit at construction/<stage>/, which is itself a child of construction/ — so readdirSync returns the stage slug as a pseudo-unit and the scanner probes construction/<stage>/<stage>/, which never exists.

3. The guard refuses before the escape hatch is ever consulted. In checkSummaryConfirmationEvidence (core/tools/aidlc-lib.ts:7023):

  • :7049 — let questions = summaryQuestionFiles(projectDir, stage); → []
  • :7056-7066 — questions.length === 0 and summary_confirmation === "required" → returns {ok:false, message: "Refusing to complete \"<stage>\": its question flow has no <stage>-questions.md file. …"}
  • :7067-7079 — the only call to usesStageLevelPerUnitArtifacts in this function, reached only after the refusal above, and it guards a different concern (the bolt-DAG unit-coverage check).

4. Writer/reader asymmetry. summaryQuestionEvidence (core/tools/aidlc-log.ts:160-200) accepts any *-questions.md inside the active record via --questions-file, validates its [Answer]: line, hashes it, and mints SUMMARY_CONFIRMATION_RECORDED with Questions File / Questions SHA-256. So the receipt is recorded successfully, pointing at the stage-level path, and only the verifier is unable to find it.

5. Three blocking call sites, all with options.unit === undefined (there is no unit in a zero-Unit run):

Site Command Message prefix
core/tools/aidlc-log.ts:1702 aidlc-log.ts review --stage <s> --reviewer <a> --iteration 1 Cannot start review for "<s>": …
core/tools/aidlc-state.ts:3348-3368 (verifySummaryConfirmationPrecondition) aidlc-state.ts gate-start <s> Refusing to complete "<s>": …
core/tools/aidlc-orchestrate.ts (report) aidlc-orchestrate.ts report --stage <s> --result … Refusing to complete "<s>": …

Review cannot start, the gate cannot open, and completion is refused — so there is no ordering of the sanctioned steps that gets through.

Affected scopes

Derived from the compiled stage-graph.json + scope-grid.json. Per-unit stages (for_each: unit-of-work) that declare summary_confirmation: required: functional-design, nfr-requirements, nfr-design, infrastructure-design. (code-generation declares no summary_confirmation, so it returns early at aidlc-lib.ts:7045 and is unaffected.)

Shipped scopes with units-generation: SKIP and at least one of those four EXECUTE:

Scope Affected stage(s)
refactor functional-design
infra nfr-requirements, nfr-design, infrastructure-design
security-patch nfr-requirements

3 of the 11 shipped scopes, plus any composed scope with the same shape. poc, bugfix and express are zero-Unit too but skip all four stages, so they do not hit this.

Steps to Reproduce

# 1. Install the Claude Code distribution into an empty git repo
mkdir demo && cd demo && git init
cp -r /path/to/aidlc-workflows/dist/claude/.claude/ .claude/
cp -r /path/to/aidlc-workflows/dist/claude/aidlc/   aidlc/

# 2. Create a refactor-scope intent (units-generation SKIP, functional-design EXECUTE)
bun .claude/tools/aidlc-utility.ts intent-create \
  --scope refactor --label "demo" --arguments "tidy up a small module"

# 3. Run through to functional-design. Because Units Generation was skipped, the
#    engine resolves this stage's artifacts to the stage-level path, so the
#    questions file is written to:
#      aidlc/spaces/default/intents/<record>/construction/functional-design/functional-design-questions.md

# 4. Answer the questions and record the consolidated-summary checkpoint.
#    This SUCCEEDS — the receipt binds to the stage-level path above:
bun .claude/tools/aidlc-log.ts decision \
  --stage functional-design --checkpoint summary-confirmation \
  --questions-file aidlc/spaces/default/intents/<record>/construction/functional-design/functional-design-questions.md \
  --decision "Does this all look correct before I generate the artifact?" \
  --options "Looks correct,Request changes" --details "Looks correct"

# 5. Generate the artifacts, then try any of the three sanctioned next steps:
bun .claude/tools/aidlc-log.ts review --stage functional-design \
  --reviewer aidlc-architecture-reviewer-agent --iteration 1
bun .claude/tools/aidlc-state.ts gate-start functional-design

Expected Behavior

summaryQuestionFiles finds the questions file the engine itself told the stage to write. On a zero-Unit run the guard should verify the single stage-level questions file at <record>/<phase>/<stage>/<stage>-questions.md — the same path resolveArtifactInstances returns — and the freshness/hash/artifact-ordering checks should proceed against it normally.

Actual Behavior

Step 5 fails, repeatedly and identically, regardless of what the questions file contains:

Cannot start review for "functional-design": its question flow has no
functional-design-questions.md file. Create and answer the stage questions, then
record the consolidated summary checkpoint before generating artifacts.
Refusing to complete "functional-design": its question flow has no
functional-design-questions.md file. Create and answer the stage questions, then
record the consolidated summary checkpoint before generating artifacts.

…while <record>/construction/functional-design/functional-design-questions.md exists, carries exactly one [Answer]: Looks correct under ## Consolidated Summary Confirmation, and is named verbatim in the SUMMARY_CONFIRMATION_RECORDED row the engine itself wrote minutes earlier:

## Summary Confirmation Recorded
**Event**: SUMMARY_CONFIRMATION_RECORDED
**Stage**: functional-design
**Details**: Looks correct
**Checkpoint**: Consolidated Summary Confirmation
**Questions File**: aidlc/spaces/default/intents/<record>/construction/functional-design/functional-design-questions.md
**Questions SHA-256**: 58bc5409…
**Hash Scope**: confirmed-content-v1

The message's own remedy ("create and answer the stage questions, then record the consolidated summary checkpoint") is not actionable — all three of those steps had already succeeded.

What we observed (separate from the code reading above): on a real zero-Unit run we recorded five of these refusals across aidlc-log review and aidlc-state gate-start. The only thing that got us past it was AIDLC_SKIP_SUMMARY_CONFIRMATION_GUARD=1 (aidlc-lib.ts:6917-6919), which is a blunt instrument: it also disables the receipt-freshness check, the questions-file hash binding, and the post-confirmation artifact-write check for the rest of the workflow. Jumping past the stage is worse — it marks the stage [S] (skipped) in aidlc-state.md, which misrepresents what happened.

AI-DLC Version

v2

Release / Commit

a277af218f0d (current main at time of filing).

AI-DLC Phase

Construction

Harness

Claude Code

AI Model

Claude Opus 5

Environment

  • OS: macOS (Darwin 25.5.0)
  • Scope: composed scope with units-generation — SKIP, functional-design — EXECUTE (reproducible on the stock refactor scope)
  • Zero-Unit: no unit-of-work.md, no Unit DAG

Possible fix directions

  1. Thread the escape hatch into the scanner (smallest change). Give summaryQuestionFiles the stateContent that checkSummaryConfirmationEvidence already holds and short-circuit the per-unit branch:

    if (!isPerUnitStage(stage) ||
        usesStageLevelPerUnitArtifacts(getField(stateContent ?? "", "Scope"), stateContent)) {
      return questionFilesInDir(join(rec, stage.phase, stage.slug), null);
    }

    This mirrors resolveArtifactResolution's branch exactly, so writer and reader agree by construction. Callers already pass stateContent at all three blocking sites (aidlc-log.ts:1703, aidlc-state.ts:3364, and the orchestrate report path).

  2. Resolve the questions file through resolveArtifactInstances instead of by directory walk, so there is one code path deciding where a per-unit stage's files live. More invasive, but removes the whole class of drift — [Bug]: traceability sensor can never pass on zero-Unit code-generation (poc/bugfix/refactor/security-patch/express) #1011 is the same class of defect in a different component (aidlc-sensor-traceability.ts extractUnitName deriving a Unit from a path that has none).

  3. Whichever route, a regression test at the checkSummaryConfirmationEvidence level covering units-generation: SKIP + a summary_confirmation: required per-unit stage would pin it.

Relationship to existing issues

Happy to open a PR for direction 1 with a regression test if that is the preferred shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions