Skip to content

feat: add load-time inputs for include nodes - #2467

Merged
Wirasm merged 3 commits into
devfrom
archon/task-archon-fix-github-issue-experimental-1785918317060
Aug 5, 2026
Merged

feat: add load-time inputs for include nodes#2467
Wirasm merged 3 commits into
devfrom
archon/task-archon-fix-github-issue-experimental-1785918317060

Conversation

@Wirasm

@Wirasm Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Problem: included workflow blocks could not accept caller-specific values; include.with was rejected at load time.
  • Why it matters: authors had to duplicate inline workflow blocks instead of reusing a static sub-DAG with literals or prior node-output references.
  • What changed: include.with now accepts an identifier-keyed string map and expands $INPUTS.<name> in included inline node fields while flattening; unsupported $INPUTS. inside command files fails clearly.
  • What did not change (scope boundary): no workflow.with, input/return declarations, runtime $INPUTS delivery, command-file parameterization, database/API/UI, or dependency changes.

UX Journey

Before

Workflow author             Workflow loader              Included block
───────────────             ───────────────              ──────────────
writes include.with ─────▶ rejects `with:`
duplicates an inline block to vary values

After

Workflow author             Workflow loader              Included block
───────────────             ───────────────              ──────────────
writes include.with ─────▶ [validates named strings]
                            [namespaces child references]
                            [replaces $INPUTS.<name>] ──▶ receives literal or
                                                          preserved $node.output
                            validates flattened DAG

Architecture Diagram

Before

workflow YAML --> dag-node schema --> include-expander --> flattened workflow
                                      |
                                      +--> command content scan (sibling refs)
flattened workflow --> loader DAG validation --> executor

After

workflow YAML --> [~ dag-node schema: include.with] --> [~ include-expander]
                                                         |=== applies $INPUTS macros
                                                         |=== rejects command-file macros
                                                         +--> flattened workflow
flattened workflow --> loader DAG validation --> executor
docs --> workflow authors

Connection inventory (list every module-to-module edge, mark changes):

From To Status Notes
Workflow YAML dag-node schema modified Validates and retains include.with; reserves INPUTS as a node ID.
dag-node schema include-expander modified Typed include mappings are consumed during flattening.
include-expander flattened workflow modified Rewrites $INPUTS.<key> after child-reference namespacing.
include-expander resolved command content modified Rejects $INPUTS. because command bodies load at execution time.
flattened workflow loader DAG validation unchanged Existing validation catches injected dangling output references.
documentation workflow authors modified Describes supported include-only mappings and command boundary.

Label Snapshot

  • Risk: risk: medium
  • Size: size: M
  • Scope: workflows, docs, tests
  • Module: workflows:include-expander

Change Metadata

  • Change type: feature
  • Primary scope: workflows

Linked Issue

Validation Evidence (required)

Commands and result summary:

bun run type-check        # pass
bun run lint --max-warnings 0  # pass, 0 warnings
bun run format:check      # pass
bun run test              # pass, 0 failures
bun run build             # pass
bun run validate          # pass
  • Evidence provided (test/log/trace/screenshot): focused schema/expander tests (150 passing), loader tests (198 passing), docs build (85 pages), a manual CLI positive case plus three negative cases, and an end-to-end workflow execution producing scope=THE-PLAN.
  • If any command is intentionally skipped, explain why: none.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No
  • If any Yes, describe risk and mitigation: not applicable.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Database migration needed? No
  • If yes, exact upgrade steps: not applicable.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios: literal include inputs and caller $node.output references expand correctly; the isolated end-to-end workflow printed scope=THE-PLAN.
  • Edge cases checked: missing input names, reserved INPUTS node ID, invalid mapping values/keys, namespace-safe child references, dangling injected output references, and $INPUTS. inside resolved command files.
  • What was not verified: later workflow-signature phases (declarations, sub-run inputs, runtime delivery, and command-body parameterization) are intentionally out of scope.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: @archon/workflows schema validation, include flattening, workflow discovery command checks, and workflow authoring documentation.
  • Potential unintended effects: malformed include mappings now fail at load time; macro replacement could alter included inline strings that deliberately contain matching $INPUTS.<name> tokens.
  • Guardrails/monitoring for early detection: contextual schema errors, explicit unresolved-macro errors, existing flattened-DAG validation, and explicit command-body rejection prevent silent fallback.

Rollback Plan (required)

  • Fast rollback command/path: revert commit b091f6fb from dev if the feature must be removed.
  • Feature flags or config toggles (if any): none.
  • Observable failure symptoms: workflow loading reports an include mapping, unresolved macro, command-file macro, or dangling output-reference error.

Risks and Mitigations

  • Risk: Include namespacing could accidentally bind an input output-reference to a renamed child node.
    • Mitigation: inputs are applied after internal child-reference rewriting and before child IDs are assigned; tests cover parent and child reference behavior.
  • Risk: $INPUTS. in command files could reach an agent unexpanded.
    • Mitigation: resolved command content is scanned and rejected with remediation guidance.

Summary by CodeRabbit

  • New Features
    • Include workflows now support with: string mappings to pass $INPUTS.<name> into inline prompts, including code fences and inline code.
    • Substitution applies across supported node fields, while runtime output references continue to resolve correctly.
  • Bug Fixes
    • Missing or invalid $INPUTS values now fail workflow loading with clear error details.
    • $INPUTS references in command files, including loop.command, are rejected; unresolved command files produce warnings.
  • Documentation
    • Updated workflow documentation to reflect supported include-input behavior and limitations.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ca760c6-1575-4e35-95df-4174afe34ec3

📥 Commits

Reviewing files that changed from the base of the PR and between c7b96d9 and d0fd551.

📒 Files selected for processing (11)
  • CLAUDE.md
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/docs-web/src/content/docs/reference/workflow-language-constitution.md
  • packages/workflows/src/command-file.ts
  • packages/workflows/src/include-expander.test.ts
  • packages/workflows/src/include-expander.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/schemas.test.ts
  • packages/workflows/src/schemas/dag-node.ts
  • packages/workflows/src/schemas/index.ts
  • packages/workflows/src/workflow-discovery.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • CLAUDE.md
  • packages/workflows/src/command-file.ts
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/workflows/src/workflow-discovery.ts
  • packages/workflows/src/schemas/index.ts
  • packages/docs-web/src/content/docs/reference/workflow-language-constitution.md
  • packages/workflows/src/schemas.test.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/include-expander.test.ts

📝 Walkthrough

Walkthrough

This change adds validated with: mappings for include nodes. The loader substitutes $INPUTS.<name> during expansion, preserves runtime output references, rejects missing inputs and unsafe command-file usage, and updates tests and documentation.

Changes

Include Input Substitution

Layer / File(s) Summary
Include input schema and preservation
packages/workflows/src/schemas/dag-node.ts, packages/workflows/src/schemas/index.ts, packages/workflows/src/schemas.test.ts
Include nodes accept string-valued with mappings with valid keys. The mapping is preserved during transformation. The INPUTS node ID is reserved.
Load-time input expansion
packages/workflows/src/include-expander.ts, packages/workflows/src/include-expander.test.ts, packages/docs-web/src/content/docs/guides/authoring-workflows.md, packages/docs-web/src/content/docs/reference/workflow-language-constitution.md, CLAUDE.md
Expansion substitutes $INPUTS.<name> across supported inline fields and nested groups after output-reference namespacing. Missing parameters produce expansion errors. Documentation describes the supported behavior and remaining restrictions.
Command-file safety validation
packages/workflows/src/command-file.ts, packages/workflows/src/include-expander.ts, packages/workflows/src/workflow-discovery.ts, packages/workflows/src/include-expander.test.ts, packages/workflows/src/loader.test.ts
File-backed command names are resolved for direct and loop commands. Readable command files containing $INPUTS fail expansion. Unresolved command files remain warnings.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IncludeNode
  participant IncludeExpander
  participant RuntimeReferences
  IncludeNode->>IncludeExpander: provide with mapping
  IncludeExpander->>RuntimeReferences: namespace child output references
  IncludeExpander->>IncludeNode: substitute $INPUTS values in cloned fields
Loading

Possibly related issues

Possibly related PRs

  • coleam00/Archon#2129: Provides the related include-expansion system extended by this change.
  • coleam00/Archon#2455: Shares workflow include loading, schema handling, discovery, and loader warning changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The context identifies gaps in fan-out substitution, some agent and approval prompts, and recursive loop-group command scanning. Implement and test the missing substitution and recursive command-scanning cases while preserving warnings for unresolved command files.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: load-time inputs for include nodes.
Description check ✅ Passed The description covers the required sections, scope boundaries, validation evidence, risks, compatibility, and rollback plan.
Out of Scope Changes check ✅ Passed The changes remain within include expansion, schema validation, command scanning, tests, and documentation defined by issue #2466.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch archon/task-archon-fix-github-issue-experimental-1785918317060

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Wirasm

Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Consolidated Review: PR #2467

Date: 2026-08-05T09:30:00Z
Agents: code-review, error-handling, test-coverage, comment-quality, docs-impact
Total Findings: 5


Executive Summary

This PR cleanly introduces load-time include.with expansion without adding executor semantics. The schema, namespace-safe reference ordering, and focused tests are generally strong. However, an included loop.command is not covered by the command-body safety scan, so it can carry a literal $INPUTS.* token to runtime despite the feature's stated fail-fast boundary. There is also a related unresolved-command fallback, incomplete coverage for several supported inline surfaces, and two documentation claims that are narrower than the implemented string-only validation. The documentation-impact review found no broader docs, API, configuration, or migration changes needed.

Overall Verdict: REQUEST_CHANGES

Auto-fix Candidates: 1 HIGH issue can be auto-fixed
Manual Review Needed: 4 MEDIUM issues require decision


Statistics

Agent CRITICAL HIGH MEDIUM LOW Total
Code Review 0 1 0 0 1
Error Handling 0 0 1 0 1
Test Coverage 0 0 1 0 1
Comment Quality 0 0 2 0 2
Docs Impact 0 0 0 0 0
Total 0 1 4 0 5

CRITICAL Issues (Must Fix)

None.


HIGH Issues (Should Fix)

Issue 1: Included loop.command files bypass the $INPUTS fail-fast boundary

Source Agent: code-review
Location: packages/workflows/src/include-expander.ts:395
Category: Bug / command-file validation

Problem:

scanBlockCommandRefs() only scans ordinary command: nodes. A loop's loop.command is also a file-backed prompt loaded at execution time, but it is neither pre-resolved by discovery nor scanned by the expander. An included loop command containing $INPUTS.scope therefore loads successfully and sends the unsupported literal to the agent at runtime.

Recommended Fix:

function getFileBackedCommandName(node: DagNode): string | undefined {
  if ('command' in node && typeof node.command === 'string') return node.command;
  if (isLoopNode(node) && typeof node.loop.command === 'string') return node.loop.command;
  return undefined;
}

Use this narrow helper when discovery builds command contents and when the include expander performs its safety scan. Add a loader/discovery test for an included loop.command file containing $INPUTS.scope.

Why High:

This breaks the explicit Phase 1 boundary that command-file parameterization must fail during loading, rather than silently becoming runtime behavior.


MEDIUM Issues (Options for User)

Issue 1: An unresolved included command file bypasses safety validation

Source Agent: error-handling
Location: packages/workflows/src/include-expander.ts:398

Problem:

When a command file cannot be resolved, the expander logs a warning and continues. It consequently cannot reject $INPUTS.* or unsafe pre-namespacing sibling references in that file. This is related to, but distinct from, the loop.command omission: even ordinary command nodes can bypass the existing scan when their content is unavailable.

Options:

Option Approach Effort Risk if Skipped
Fix Now Throw IncludeExpansionError with include/block/command context when safety scanning cannot resolve the command. LOW Unsupported tokens or stale refs can reach execution silently.
Create Issue Preserve compatibility and add a structured load diagnostic later. LOW Same behavior remains until follow-up ships.
Skip Keep the warning-only fallback. NONE Workflow authors receive no actionable failure.

Recommendation: Fix now. A safety scan that cannot inspect the input is ambiguous rather than safe, and fail-fast aligns with the newly introduced command-body restriction.

throw new IncludeExpansionError(
  `Node '${includeNode.id}': command file '${command}.md' in included block ` +
    `'${child.name}' could not be resolved for include safety validation. ` +
    'Make the command available, or inline the prompt.'
);

Issue 2: Supported substitution surfaces lack behavioral coverage

Source Agent: test-coverage
Location: packages/workflows/src/include-expander.ts:212-231

Problem:

New tests exercise prompt, when, and fenced text, but not the loop, recursive loop_group, approval, bash, script, workflow.input, or cancel branches in applyInputsMacro(). A future change could leave $INPUTS.* unexpanded on one of these executable or structured surfaces without a test failure.

Options:

Option Approach Effort Risk if Skipped
Fix Now Add one table-driven public-expander test covering every remaining supported surface. MEDIUM Regressions can reach scripts, loop controls, cancellation text, or child inputs.
Create Issue Add focused tests in a follow-up. LOW Coverage hole remains temporarily.
Skip Rely on current implementation and prompt-focused tests. NONE Broad feature contract is not guarded.

Recommendation: Fix now. A compact table-driven test is proportionate and keeps the surface-wide contract regression-safe.

Issue 3: Authoring guide promises an unenforced with: value grammar

Source Agent: comment-quality
Location: packages/docs-web/src/content/docs/guides/authoring-workflows.md:892

Problem:

The guide says values are only string literals or $node.output references and that expressions are never allowed. The schema validates identifier keys and that values are strings, then applyInputsMacro() inserts the string verbatim. No grammar check enforces the narrower documentation claim.

Options:

Option Approach Effort Risk if Skipped
Fix Now Document the enforced rule: values must be strings and are substituted verbatim at load time. LOW Authors are promised validation the product does not provide.
Create Issue Add a deliberate restricted-value grammar in a later language change. MEDIUM Documentation remains inaccurate until fixed.
Skip Keep the literal/output-reference claim. NONE Normative guidance misstates behavior.

Recommendation: Fix the documentation now; consider a stricter grammar only as a separately specified feature.

Issue 4: Workflow-language constitution repeats the unsupported restriction

Source Agent: comment-quality
Location: packages/docs-web/src/content/docs/reference/workflow-language-constitution.md:59

Problem:

The normative constitution repeats that only literals or $node.output references are accepted and expressions are rejected, though the shipped validator accepts any string. This finding conflicts with the docs-impact agent's general conclusion that the documentation matches implementation; the concrete schema and macro evidence supports this more specific finding.

Options:

Option Approach Effort Risk if Skipped
Fix Now State that identifier-keyed string values are substituted at load time; retain the enforced workflow.with boundary. LOW The language reference remains misleading.
Create Issue Implement the claimed grammar and tests in a dedicated PR. MEDIUM Current reference remains inaccurate.
Skip Leave the expression-rejection claim. NONE Reviewers and authors infer nonexistent guardrails.

Recommendation: Fix the wording now, consistently with the authoring guide.


LOW Issues (For Consideration)

None.


Positive Observations

  • Child-internal output references are namespaced before caller values are inserted, preserving parent reference scope under node-ID collisions.
  • Mapping validation is strongly typed and keeps Phase 2 extra-key validation intentionally deferred.
  • Existing tests cover empty and repeated values, missing mappings, nested forwarding, fenced text, output-reference collisions, dangling injected references, and ordinary command-file rejection.
  • Error handling is mostly explicit and contextual: missing macros are aggregated, unsafe when: composition is rejected, and unexpected exceptions are rethrown.
  • The implementation comments accurately describe ordering and the load-time/runtime command-file boundary.
  • No API, database, configuration, command, or broader architectural documentation changes are required.

Suggested Follow-up Issues

Issue Title Priority Related Finding
"Define and enforce a restricted include.with value grammar" P2 MEDIUM issues 3-4, if arbitrary string values are not the intended permanent contract.

Next Steps

  1. Auto-fix step should address the 1 HIGH command-surface gap.
  2. Review the four MEDIUM issues and decide whether to fix now or defer the optional grammar enforcement work.
  3. Merge after command-file validation and documentation are aligned with the intended Phase 1 contract.

Agent Artifacts

Agent Artifact Findings
Code Review code-review-findings.md 1
Error Handling error-handling-findings.md 1
Test Coverage test-coverage-findings.md 1
Comment Quality comment-quality-findings.md 2
Docs Impact docs-impact-findings.md 0

Metadata

  • Synthesized: 2026-08-05T09:30:00Z
  • Artifact: /Users/rasmus/.archon/workspaces/coleam00/Archon/artifacts/runs/166bc0584cbae0a19db002d663d28136/review/consolidated-review.md

@Wirasm

Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Fix Report: PR #2467

Date: 2026-08-05T09:19:42Z
Status: COMPLETE
Branch: archon/task-archon-fix-github-issue-experimental-1785918317060
Commit: c87c104
Philosophy: Aggressive fix — lean towards fixing everything


Summary

All five review findings were fixed. Included command-backed loops now undergo the same load-time safety validation as ordinary command nodes, unresolved included command files fail fast, and tests and documentation cover the complete shipped behavior.


Fixes Applied

Severity Finding Location What Was Done
HIGH Included loop.command files bypass $INPUTS validation packages/workflows/src/include-expander.ts Centralized file-backed command extraction and scan both ordinary command nodes and deferred loop commands.
MEDIUM Unresolved command file bypasses safety validation packages/workflows/src/include-expander.ts Converted the warning-only fallback to a contextual include-expansion error.
MEDIUM Supported input surfaces lacked coverage packages/workflows/src/include-expander.test.ts Added coverage for bash, script, loop, loop group recursion, approval, cancel, and workflow input.
MEDIUM Authoring guide overstated with: value grammar packages/docs-web/src/content/docs/guides/authoring-workflows.md Documented the enforced identifier-keyed string-map behavior and command-file restriction accurately.
MEDIUM Constitution repeated unsupported grammar restriction packages/docs-web/src/content/docs/reference/workflow-language-constitution.md Replaced the narrower claim with the actual load-time string substitution behavior.

Tests Added

File Test Cases
packages/workflows/src/include-expander.test.ts All supported inline substitution surfaces; included loop.command containing $INPUTS; unresolved command-file failure.
packages/workflows/src/loader.test.ts Discovery resolves and rejects unsafe included loop.command content.

Docs Updated

File Changes
packages/docs-web/src/content/docs/guides/authoring-workflows.md Corrected the accepted mapping grammar and documented loop.command validation.
packages/docs-web/src/content/docs/reference/workflow-language-constitution.md Corrected the include.with language guarantee.

Skipped Findings

(none)


Blocked (Could Not Fix)

(none)


Suggested Follow-up Issues

(none)


Validation

Check Status
Type check
Lint
Format check
Focused workflow tests ✅ 243 passed
Full test suite
Full validation (bun run validate)

@Wirasm Wirasm left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First review pass — one docs change before this lands. The implementation isn't reviewed yet; this is the example that jumped out on read-through.

depends_on: [plan]
with:
plan: $plan.output
base_branch: main

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to base_branch: $BASE_BRANCH.

Two reasons, and the second is the one that matters:

1. main is the wrong branch to name in this repo. main is the release branch here; everything targets dev. A reader who copies this example inherits the wrong default. (Unrelated but adjacent: a stale local refs/remotes/origin/HEAD on this machine was also pointing at main, so the word showed up twice from two unrelated causes — that one's fixed separately.)

2. It teaches hardcoding a branch name in the same paragraph that introduces the feature. Archon already provides $BASE_BRANCH precisely so authors don't do this. Using it here demonstrates something more interesting than a literal: a workflow variable flowing through with: into a block.

Verified it worksapplyInputsMacro is a pure verbatim text.replace, and this file has zero references to substituteWorkflowVariables or BASE_BRANCH. So $BASE_BRANCH lands as literal text in the block's node body and resolves through normal runtime substitution, by exactly the mechanism the prose above already describes for $node.output. No code change needed — the doc line is the whole fix.

Worth a sentence in the surrounding prose too: a with: value can carry any variable the runtime substitutes, not just $node.output.

@Wirasm
Wirasm marked this pull request as ready for review August 5, 2026 10:02
@Wirasm
Wirasm force-pushed the archon/task-archon-fix-github-issue-experimental-1785918317060 branch from c87c104 to fbe9108 Compare August 5, 2026 10:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/workflows/src/include-expander.ts`:
- Around line 409-424: The $INPUTS validation currently scans stripped command
content, allowing references inside fenced or inline code to bypass detection.
Keep the stripped scan for sibling output references, but run INPUTS_REF against
the original content so all unsupported include-input references are rejected;
add regression cases covering fenced and inline-code $INPUTS references.
- Around line 204-211: Update the substitute callback in the include expansion
logic to require that name is an own property of args using Object.hasOwn before
reading its value. Treat inherited or absent mappings as missing by adding name
to missing and returning the original match; preserve substitution for
explicitly supplied mappings, including values that are undefined if the
existing contract requires them.

In `@packages/workflows/src/schemas/dag-node.ts`:
- Around line 701-703: Update the node validation logic near the raw with field
to reject data.with when neither hasInclude nor hasWorkflow is enabled. Preserve
the existing workflow-specific error and ensure unsupported prompt, command, and
bash modes fail validation instead of silently dropping with.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ebc32889-2968-42e1-9ae6-af0570830b6c

📥 Commits

Reviewing files that changed from the base of the PR and between d78fe64 and fbe9108.

📒 Files selected for processing (9)
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/docs-web/src/content/docs/reference/workflow-language-constitution.md
  • packages/workflows/src/command-file.ts
  • packages/workflows/src/include-expander.test.ts
  • packages/workflows/src/include-expander.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/schemas.test.ts
  • packages/workflows/src/schemas/dag-node.ts
  • packages/workflows/src/workflow-discovery.ts

Comment thread packages/workflows/src/include-expander.ts
Comment thread packages/workflows/src/include-expander.ts
Comment on lines 701 to 703
// Raw here because include/workflow modes validate and retain it differently.
// Other node modes strip it with the rest of their unsupported surface.
with: z.unknown().optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject with on every unsupported node mode.

A prompt, command, or bash node can pass this raw field through validation. The transform then drops it without an error. This makes a workflow configuration silently ineffective.

Add a !hasInclude && !hasWorkflow && data.with !== undefined validation branch. Keep the existing workflow-specific error.

Proposed fix
     if (hasWorkflow && data.with !== undefined) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
         message:
           "'with:' named-parameter mapping is not yet supported on workflow nodes (slice 2). Use 'input:' instead.",
         path: ['with'],
       });
     }
+    if (!hasInclude && !hasWorkflow && data.with !== undefined) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: "'with' is only supported on include nodes.",
+        path: ['with'],
+      });
+    }

As per coding guidelines, “Fail fast with clear errors for unsupported or unsafe states; never silently swallow errors or broaden permissions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workflows/src/schemas/dag-node.ts` around lines 701 - 703, Update
the node validation logic near the raw with field to reject data.with when
neither hasInclude nor hasWorkflow is enabled. Preserve the existing
workflow-specific error and ensure unsupported prompt, command, and bash modes
fail validation instead of silently dropping with.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/workflows/src/include-expander.ts`:
- Around line 232-234: Update the isWorkflowNode branch in the node substitution
logic to also apply substitute to workflow.fan_out.items when that property is
defined, preserving the existing node.input substitution and leaving other node
types unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 516d5467-6fdf-4a53-930e-d6c38ffd2d51

📥 Commits

Reviewing files that changed from the base of the PR and between d78fe64 and fbe9108.

📒 Files selected for processing (9)
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/docs-web/src/content/docs/reference/workflow-language-constitution.md
  • packages/workflows/src/command-file.ts
  • packages/workflows/src/include-expander.test.ts
  • packages/workflows/src/include-expander.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/schemas.test.ts
  • packages/workflows/src/schemas/dag-node.ts
  • packages/workflows/src/workflow-discovery.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/docs-web/src/content/docs/reference/workflow-language-constitution.md
  • packages/workflows/src/command-file.ts
  • packages/workflows/src/include-expander.test.ts
  • packages/workflows/src/schemas/dag-node.ts
  • packages/workflows/src/workflow-discovery.ts
  • packages/workflows/src/loader.test.ts
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/workflows/src/schemas.test.ts

Comment thread packages/workflows/src/include-expander.ts
@Wirasm

Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

PR Review: #2467 — load-time inputs for include: nodes

Recommendation: REQUEST CHANGES — one confirmed silent-failure bug (one-line fix), one deliberate spec deviation that needs your decision.

Reviewed against the spec in #2466, which is unusually prescriptive (8 numbered tasks + explicit ⚠️ constraints + an Acceptance list). Six specialist agents (code, errors, tests, types, comments, docs) plus runnable repros for every correctness claim below. Rebase: not needed — branch is already on top of dev (0 behind / 2 ahead).

Validation

Check Result
Type check PASS
Lint PASS (0 warnings)
Format PASS
Tests PASS — 0 fail in every package; packages/workflows 1641 pass
Docs build PASS (85 pages)

Critical

C1 · $INPUTS in workflow.fan_out.items is silently dropped

packages/workflows/src/include-expander.tsapplyInputsMacro's isWorkflowNode branch substitutes node.input but never node.fan_out.items. rewriteNodeOutputRefs, the function it is required to mirror, walks both:

// rewriteNodeOutputRefs — both surfaces
if (node.input !== undefined) node.input = code(node.input);
if (node.fan_out !== undefined) node.fan_out.items = code(node.fan_out.items);

// applyInputsMacro — fan_out.items missing
if (node.input !== undefined) node.input = substitute(node.input);

This violates Task 4 verbatim: "It must walk exactly the field set rewriteNodeOutputRefs walks... copy that branch structure field-for-field."

Confirmed by repro, not inference:

fan_out.items ACTUAL = "$INPUTS.itemsref"   errors: []
workflow.input ACTUAL = "scope=$gather.output"   (control — correct)

Both failure halves fire at once. The literal $INPUTS.<name> reaches the executor, and missingInputs never sees the name — so a caller who forgot to pass that input gets no load error either. That directly contradicts the PR's own Acceptance criterion: "An unsupplied $INPUTS.<name> fails the load, naming the include node, the block and the parameter."

There is no safety net. validateDagStructure does scan fan_out.items (loader.ts:265) but only for the \$id\.output grammar, so $INPUTS.tasks matches nothing. The guard fires only when the input is coincidentally named output:

input named 'tasks':  errors=0  (SILENT)
input named 'scope':  errors=0  (SILENT)
input named 'output': errors=1  Node 'inc__fan' references unknown node '$INPUTS.output'

Fix — one line in applyInputsMacro's workflow branch:

if (node.fan_out !== undefined) node.fan_out.items = substitute(node.fan_out.items);

Plus a case in substitutes inputs across every other supported inline node surface — that test enumerates every surface except this one, so its title currently asserts a completeness it doesn't have.


Important

I1 · WARN → hard error on unresolvable command files (explicitly forbidden; needs your call)

include-expander.ts scanBlockCommandRefs now throws where it previously warned and continued. Task 6 says: "Do not upgrade that warn to an error — it would break blocks whose commands live outside the scanned set." It also breaks the must-survive constraint "Undeclared includes keep working byte-for-byte."

This was deliberate, not an oversight — commit fbe9108f lists "fail fast when included command files cannot be validated", and the loader test was renamed should warn (not fail) …should fail … with its mockLogger.warn assertion deleted.

Confirmed by repro — a workflow with no with: and no $INPUTS anywhere is dropped entirely from discovery:

errors: [{ error: "…could not be resolved for include safety validation…" }]
workflow 'p' still discoverable? NO -- DROPPED

Blast radius (resolveCommandContentForScan returns null): defaults.loadDefaultCommands: false; a command name containing / (rejected by isValidCommandName before resolution); cwd === null global/non-repo discovery. Under loadDefaultCommands: false this drops archon-issue-review-full, archon-plan-to-pr and archon-idea-to-pr — all three include archon-review-block, whose 9 command nodes are bundled defaults. A stock install is unaffected today.

This is a judgment call, not a clear-cut bug. Fail-fast is a stated CLAUDE.md principle and the author defended it in-code; the issue author explicitly forbade it. Recommended split: keep the hard fail for a resolved file containing $INPUTS.<name> (that half is correct and valuable), revert the unresolved-file branch to WARN + continue. If you'd rather keep the strict behavior, it should be owned explicitly in the PR description and #2466 amended — it shouldn't land silently against a written constraint.

I2 · loop_group body command files escape the scan on both sides

A command:/loop.command node nested in a loop_group body inside an included block is invisible to the safety scan — resolveIncludeBlockCommandContents (workflow-discovery.ts) iterates workflow.nodes top-level only, so its content never enters the map, and scanBlockCommandRefs iterates child.nodes top-level only. Confirmed by repro: errors: [] where the identical command at top level is correctly rejected.

applyInputsMacro does recurse into loop_group.nodes; the scan does not. The top-level loop structure is pre-existing (not a regression from this PR), but the new $INPUTS check inherits the blind spot, and it makes a docs claim untrue — see S1.

I3 · The "KEEP IN SYNC" comment now undercounts, and it already drifted

rewriteNodeOutputRefs's block comment still reads "KEEP IN SYNC (three ref-surface enumerations must agree)". applyInputsMacro is a fourth enumeration of the identical surface, and it has already drifted — that drift is exactly C1. Suggested: "four ref-surface enumerations must agree: this rewrite, applyInputsMacro below, the loader's validateDagStructure scan, and the substituteNodeOutputRefs call sites in dag-executor.ts."

I4 · Misleading comment on the flat dagNodeSchema.with field

Current: "Raw here because include/workflow modes validate and retain it differently." Workflow mode never retains with in any form — it unconditionally rejects it as unsupported (phase 2). A maintainer could read this as "both branches keep it somehow." Suggested: "Raw here because include mode validates its shape and retains it on the parsed node; workflow mode still rejects it outright as unsupported (phase 2). Other node modes strip it with the rest of their unsupported surface."


Suggestions

  • S1 · Docs overclaim. authoring-workflows.md states unconditionally that loading fails if a command file "cannot be resolved for safety validation or contains $INPUTS.<name>". Only true for command-backed nodes at the block's top level (see I2). Scope the sentence or close the recursion gap.
  • S2 · Reserved INPUTS node id is undocumented anywhere — a user who names a node INPUTS gets a load error with no doc explaining why.
  • S3 · CLAUDE.md:453 — the include: node-type description wasn't updated with with:/$INPUTS. The same line already documents workflow:'s "with: and retry: rejected" at comparable density.
  • S4 · Task 6's "best-effort" caveat was required next to the message and isn't there. One line above the new $INPUTS throw would satisfy it.
  • S5 · Docs never say "never expressions" explicitly, which Task 8 asked for. One clause closes it.
  • S6 · Duplicated identifier grammar/^[a-zA-Z_][a-zA-Z0-9_-]*$/ in dag-node.ts and the capture group in INPUTS_REF. They agree today; a shared constant would prevent a key that validates but never substitutes.
  • S7 · No test combines the unresolvable-command throw with loadDefaultCommands: false (low value — the branch itself is pre-existing).
  • S8 · command-file.ts is scope creep beyond feat(workflows): with: parameters on include: nodes — $INPUTS as a load-time macro (signature phase 1) #2466 (adds loop.command). Correct on its own and closes a real gap, but it widens I1's blast radius to files that previously weren't scanned at all.

Strengths

  • The load-bearing ordering is right and genuinely protected. rewriteNodeOutputRefsapplyInputsMacro → id rename, with a comment explaining why. Hand-traced: reversing it would rename an injected $gather.output into $review__gather.output, and the collision test's exact-match assertion would fail. That test earns its keep.
  • Test quality is high. Every Task 7 case and Acceptance item is covered with regression-catching assertions, not placeholders. The old "with: is rejected" test was genuinely replaced per the ⚠️, not deleted. Loader tests exercise the real discovery path with files on disk, not just unit-level calls.
  • Nested-include forwarding is correct by design — memoized single expansion means an intermediate block's $INPUTS passthrough survives to be resolved by the outer caller.
  • superRefinetransform is safe. Verified against zod 4.4.3: a non-fatal ctx.addIssue() short-circuits safeParse before the transform runs, so the as Record<string, string> cast is never observably reached.
  • INPUTS_REF /g statefulness is handledString.replace resets lastIndex per spec, and the .exec() site resets defensively.
  • Constitution treatment is careful and honest — data-only mapping, present tense for the shipped include: half, explicit that workflow.with has not shipped. No overclaiming of parity with the unbuilt phase-2 half.
  • CLAUDE.md zod conventions respected — two-arg z.record, no hand-rolled parallel interface, correct schema naming.

Bottom line

C1 is a one-line fix plus a test case and should land before merge — it's a silent failure in exactly the mode the feature exists to prevent, and it's untested in both directions. I1 is a real decision for the maintainer rather than a defect to fix on autopilot: fail-fast is defensible, but it contradicts a written constraint and changes load behavior for include: users who never opted into this feature. I2–I4 are cheap. Everything else is polish.

The implementation is otherwise careful, well-tested, and faithful to a demanding spec.

@Wirasm

Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Addendum — type-design pass + independent confirmation

The type-design and error-handling passes landed after the main review. Net effect: no change to the recommendation or to C1/I1/I2 — all three were independently re-confirmed with live repros. Below is what's genuinely new, plus one correction to my own report.

Correction to S6 (identifier-regex duplication) — downgrade

I flagged the duplicated /^[a-zA-Z_][a-zA-Z0-9_-]*$/ between dag-node.ts and INPUTS_REF as worth a shared constant. That was framed misleadingly: the same character-class literal already appears independently at loader.ts:109, dag-executor.ts:752/875, condition-evaluator.ts:133 and validator.ts:687 with no shared constant anywhere. Hand-duplicating this pattern per call site is the codebase's prevailing convention, so this PR is consistent with existing practice rather than introducing drift risk. Not a finding against this PR — at most a separate codebase-wide cleanup, and not one I'd push for.

New — S9 · The schema that types with and the schema that enforces it are different objects

includeNodeSchema is never .parse()'d anywhere in production — it exists solely so IncludeNode = z.infer<typeof includeNodeSchema> derives the right TS type (correctly following the z.infer-only rule). All runtime enforcement is the 35-line hand-rolled superRefine block. The two agree today by coincidence of authorship, not by construction.

Verified that zod v4 expresses the entire invariant natively in one line, reproducing every accept/reject case including the null-prototype allowance:

z.record(z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_-]*$/), z.string())

Calling that inside superRefine and forwarding its issues would collapse type-derivation and runtime-check into one object while keeping the flat field z.unknown(). Worth noting this is exactly what #2466 Task 1 directed ("validate the flat one in superRefine", with hand-written messages), so the implementer followed instructions faithfully — this is a tightening opportunity, not a defect.

New — S10 · isCommandNode is missing from the guard catalog

'command' in node && typeof node.command === 'string' inside command-file.ts is now the third independent copy of that idiom (dag-executor.ts:3967, validator.ts:379, command-file.ts). Every other node mode has an exported is*Node guard in schemas/dag-node.ts; isCommandNode is conspicuously the one absent. This PR completed the rule of three, so the bar is now met. Small, contained fix: add the guard and have command-file.ts call it.

Placement of getFileBackedCommandName in a new command-file.ts (rather than in schemas/dag-node.ts) is the right call — it derives a value spanning two node types for two consumers, so keeping it out of the foundational widely-imported schema module avoids coupling that module to command-resolution logic.

Extends I4 · The comment's stated rationale is wrong on a second axis

Beyond the "validate and retain it differently" problem already filed, the claim that raw z.unknown() is needed so "other node modes strip it" doesn't hold either: isolation and fan_out in the same file are typed directly at the flat-schema level and rejected via ctx.addIssue on the wrong node type. So "typed at flat level + reject on wrong mode" is both achievable and the established local pattern. The real (unstated) justification is sound — workflow.with's future shape is unknown, so committing the shared flat field to Record<string, string> now risks a breaking change in phase 2. Worth stating that reason instead, since it's the one that actually survives scrutiny.

Checked clean — nothing to change

  • Prototype check (prototype !== Object.prototype && prototype !== null), tested against Bun.YAML.parse (the actual parser, per loader.ts:85): bare timestamps parse as strings not Date; mappings never produce Map; arrays and null are already caught by the preceding clauses. So the clause is defensive redundancy against a future parser swap, not live logic. No prototype-pollution vector__proto__ arrives as an ordinary own key with Object.prototype intact, exactly as JSON.parse behaves. One harmless quirk: an input literally named __proto__ validates and substitutes correctly. Nothing worth changing.
  • Zod transform/cast safety — an agent disagreement I settled by execution. One pass claimed the .transform() runs on invalid data (safety coming only from callers gating on .success); another claimed it short-circuits. Ran it against the project's actual zod: safeParse success: false, transform ran: false. The transform does not execute after a non-fatal ctx.addIssue(), so the as Record<string, string> cast is genuinely unreachable — the stronger of the two guarantees, as originally reported.

Independent re-confirmation of the main findings

C1 (fan_out.items), I1 (WARN→throw), and I2 (loop_group body scan hole) were each reproduced independently on a second pass with live discoverWorkflows() runs rather than code reading alone. On I1 specifically: all 9 of archon-review-block.yaml's command files exist only under .archon/commands/defaults/ — the non-default .archon/commands/ set is entirely disjoint — so the loadDefaultCommands: false blast radius on the three including workflows is confirmed as stated.

Type-design ratings

Dimension Rating Driver
Encapsulation 6/10 Type and enforcement live in two objects that agree only by authorship
Invariant expression 7/10 Fully correct with good messages, but hand-rolled where zod expresses it natively
Usefulness 8/10 Both types solve real narrow problems; correct absence signal on the helper
Enforcement 6/10 Solid tests, no exploitable gap found against the real parser; docked for the hand-written/structural split

No blocking issues from this pass. C1 and I1 remain the only items I'd hold the merge for.

@Wirasm

Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Addendum 2 — silent-failure pass (final)

The error-handling pass landed last and is the most consequential of the six. It raises one new finding at Critical severity and closes the two questions I'd flagged as unverified. Recommendation is unchanged (REQUEST CHANGES) but the fix list grows.

New — C2 · $INPUTS reaches the model verbatim on three more surfaces, permanently

Neither applyInputsMacro nor rewriteNodeOutputRefs walks these three text surfaces:

Surface Where Runtime behavior
agents[].prompt dag-node.ts:123 Passed straight to the Claude SDK (dag-executor.ts:1122claude/provider.ts:567-571). No substitution, ever.
systemPrompt dag-node.ts:219 Wired directly to baseOptions.systemPrompt (dag-executor.ts:1108). No substitution, ever.
approval.on_reject.prompt dag-node.ts:395 Runs through prompt substitution on a rejection retry (dag-executor.ts:5202-5231) — but that path only understands $id.output, not $INPUTS.

Confirmed by repro — caller supplies detail, control substitutes, the other three don't, and errors: 0:

errors: 0
prompt        (control): "main CLEAN-TEMP-FILES"
agents.helper.prompt   : "Sub-task: $INPUTS.detail"
systemPrompt           : "You handle $INPUTS.detail"
on_reject.prompt       : "Retry with $INPUTS.detail"

Why this is worse than the equivalent $node.output gap, and why I'm rating it Critical rather than "pre-existing." These three are pre-existing blind spots in rewriteNodeOutputRefs, so applyInputsMacro technically did match it field-for-field here — no spec violation. But the two macros have different fallbacks. A surface rewriteNodeOutputRefs misses is only a namespacing miss; the executor's runtime substituteNodeOutputRefs pass still resolves the ref. $INPUTS has no such second chance — I verified there is no $INPUTS handling anywhere outside include-expander.ts and the schemas, so load-time expansion is the sole resolution path. A missed surface is therefore permanent literal $INPUTS.<name> text delivered to the model, and because the field is never visited it is never recorded in missingInputs either — no load error whether the caller supplies the value or forgets it entirely.

That is precisely the failure #2466's "Read this first" section says must never happen: "an author should learn this from the PR, not from $INPUTS.scope silently reaching the model as literal text."

Recommended: wire all three into applyInputsMacro (cheap — they're plain string fields). If you'd rather scope this PR tightly, the minimum acceptable alternative is an explicit documented reserved-surface list plus a tracked follow-up, so it's a stated boundary rather than an undocumented hole. Silently shipping it is the one option I'd argue against.

Partial restore of S6 — my downgrade was too broad

In Addendum 1 I downgraded the duplicated identifier regex to "not a finding" on the grounds that the same char class is already unshared across five other files. That reasoning holds for the family, but I over-corrected on the specific pair. The drift is one-directional, and the dangerous direction is silent:

  • Loosen the with:-key validator (dag-node.ts) without INPUTS_REF → e.g. with: {my.key: "v"} validates, but $INPUTS.my.key matches only $INPUTS.my, leaving .key as trailing literal text. Silent wrong substitution, no error.
  • Loosen INPUTS_REF without the key validator → the matching with: key fails schema validation and the node fails to load. Loud and safe.

Unlike the other five sites (which encode node-id output refs — a related but distinct concept), these two were introduced by the same PR, same author, same feature, and encode the identical semantic concept "a valid include-input name." Narrow recommendation: one shared INPUT_NAME_PATTERN constant for this pair only. Explicitly not the broader five-site refactor — that's older, cross-cutting, and out of scope here.

Closed — the two questions I'd flagged as unverified

  • Expansion errors are surfaced correctly, not silently dropped. expandWorkflowIncludes catches IncludeExpansionError per top-level workflow, pushes {filename, error, errorType} into errors[], and excludes that workflow from the returned map. Two confirmed surfacing points: /workflow list renders a "N workflow(s) failed to load:" section (command-handler.ts:612-653, capped at 10), and GET /api/workflows returns it in its documented errors? field. The resilient drop-one-report-it-keep-the-rest mechanism is sound — which is exactly what makes I1 user-visible as a load failure rather than a background warning.
  • The prototype check is correct, with no false accepts or rejects. Tested against Bun.YAML.parse (the real parser, loader.ts:85): bare timestamps parse as strings not Date; mappings never yield Map; null/arrays are caught by earlier clauses. So the clause is defensive redundancy, not live logic. No prototype-pollution vector__proto__ arrives as an ordinary own key with Object.prototype intact. Nothing to change.

Also confirmed clean

  • missingInputs throw placement — .map() runs its callback unconditionally, and the loop_group recursion passes the same Set by reference, so nested-body misses land before the post-map throw. No interaction bug with the entry-node when: combining logic.
  • The dangling-ref safety net genuinely works for $node.outputvalidateDagStructure really does re-validate the flattened DAG in the right order (after macro substitution), so a caller's dangling $typo.output is caught. It provides zero protection for $INPUTS, per C1/C2.
  • The zod .transform() question that split two passes is settled by execution: transformCalls: 0, success: false. The transform does not run after a non-fatal ctx.addIssue(), so the cast is unreachable — as originally reported.

Revised fix list

Before merge: C1 (fan_out.items — one line), C2 (three surfaces, or an explicit documented boundary + follow-up), I1 (revert the unresolved-file branch to WARN, or sign off on the deviation explicitly).
Cheap follow-ups: I2, I3, I4, S6-narrowed, S10 (isCommandNode).

The underlying implementation remains careful and well-tested; the gaps are all in surface coverage, and C1/C2 share one root cause — the ref-surface enumerations were never mechanically tied together, which is exactly what I3's stale "KEEP IN SYNC" comment was supposed to prevent.

Wirasm added a commit that referenced this pull request Aug 5, 2026
…t command scanning

Addresses the blocking findings from the review of #2467.

C1 — $INPUTS in workflow.fan_out.items was dropped. applyInputsMacro walked
node.input but not node.fan_out.items, so the literal reached the executor,
which JSON.parses it and spawns one child per unsubstituted placeholder. The
name never reached missingInputs either, so a caller who forgot the value got
no load error. Added to the workflow branch and to the "every other supported
inline node surface" test, whose title claimed a completeness it lacked.

C2 — three AI-turn surfaces were never walked: systemPrompt, agents.*.prompt
(plus .description, same delivery path) and approval.on_reject.prompt. These
are also blind spots in rewriteNodeOutputRefs, so the macro did mirror it
field-for-field — but the two have different fallbacks. A surface the rewrite
misses is only a namespacing miss; the executor's runtime pass still resolves
the ref. $INPUTS has no runtime pass at all, so a missed surface is permanent
literal text delivered to the model, and silent in both directions. Walked
here only; the rewrite half is a pre-existing gap left to its own issue.

F1 — args[name] read inherited Object.prototype members, so an unsupplied
$INPUTS.toString spliced a native function body into the prompt instead of
failing the load. Now Object.hasOwn, with anything not supplied as an own key
treated as missing.

F2 — the $INPUTS command-body guard tested fence-stripped content, so a fenced
or inline-code $INPUTS.scope passed validation and shipped unsubstituted. It
now tests raw content. This is the behaviour the macro's own docblock and the
shipped guide already claimed. The sibling-ref scan keeps stripping — it is
wrong there too, but that is pre-existing and goes with the rewrite gap.

I1 — reverted the unresolvable-command-file throw to WARN + continue. An
unreadable file is an incomplete-information state, not an unsafe one, and
failing it dropped workflows with no with: and no $INPUTS anywhere from
discovery entirely, breaking "undeclared includes keep working byte-for-byte".
The hard fail is kept for a file that was actually read and contains $INPUTS.
Documented the scan as best-effort by construction next to the message (S4).

Also folded in: the KEEP IN SYNC comment now counts four enumerations and
records that the drift it was meant to prevent had already happened (I3); the
flat dagNodeSchema.with comment now states the rationale that survives
scrutiny — workflow.with's phase-2 shape is undecided, so committing the
shared field to Record<string, string> risks a breaking change (I4); one
shared INPUT_NAME_SOURCE for the with:-key validator and INPUTS_REF, whose
drift is silent in one direction only (S6, narrowed to that pair); isCommandNode
added to the guard catalog now the rule of three is met (S10); docs scoped to
what the scan actually covers and given the explicit "never expressions"
clause (S1, S5); CLAUDE.md include: line updated (S3).

Deferred deliberately, each filed rather than fixed here: the loop_group body
command scan hole (I2 — pre-existing on both sides, wants its own coverage);
the rewriteNodeOutputRefs half of the three unwalked surfaces; with: accepted
and silently stripped on six non-include node modes; and the schema/enforcement
split on includeNodeSchema (S9 — the implementer followed Task 1 faithfully).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/workflows/src/include-expander.ts`:
- Around line 440-442: The command-reference scan currently ignores commands
nested in loop_group bodies, allowing unresolved $INPUTS macros through. Update
command-content discovery and scanBlockCommandRefs to recursively traverse each
loop_group.nodes collection alongside top-level child.nodes, and add a
regression test covering a nested command file containing $INPUTS.scope.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cdbea9b2-8a4a-447f-9efc-c36a32d08854

📥 Commits

Reviewing files that changed from the base of the PR and between fbe9108 and eb95862.

📒 Files selected for processing (8)
  • CLAUDE.md
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/workflows/src/command-file.ts
  • packages/workflows/src/include-expander.test.ts
  • packages/workflows/src/include-expander.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/schemas/dag-node.ts
  • packages/workflows/src/schemas/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/workflows/src/command-file.ts
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md

Comment on lines +440 to +442
* BEST-EFFORT BY CONSTRUCTION. This scan sees only what discovery could resolve, and only
* the block's TOP-LEVEL command nodes — a command nested in a `loop_group` body is not
* reached. So a clean scan is "nothing found in what we could read", never a proof of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scan command files inside loop_group bodies.

A loop_group body can contain command nodes. This scan only visits child.nodes, so it does not reject $INPUTS.<name> in a nested command file. The command runs after include expansion and receives the unresolved macro.

Recursively walk loop_group.nodes in both command-content discovery and scanBlockCommandRefs. Add a regression test for a nested command file containing $INPUTS.scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workflows/src/include-expander.ts` around lines 440 - 442, The
command-reference scan currently ignores commands nested in loop_group bodies,
allowing unresolved $INPUTS macros through. Update command-content discovery and
scanBlockCommandRefs to recursively traverse each loop_group.nodes collection
alongside top-level child.nodes, and add a regression test covering a nested
command file containing $INPUTS.scope.

@Wirasm

Wirasm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-ups from this PR are now filed, so the deferrals are tracked rather than implied:

One thing worth folding in only if you are already pushing: the two KEEP IN SYNC comments now disagree. include-expander.ts:161 says FOUR enumerations and counts applyInputsMacro; loader.ts:237 still says three and omits it. That is I3 fixed on one side. Not worth restarting CI on its own — #2475 picks it up.

Wirasm added 3 commits August 5, 2026 14:33
Included blocks had no safe parameter surface, forcing authors to duplicate reusable inline DAGs.

Changes:
- validate and retain include with mappings and reserve INPUTS
- substitute inline INPUTS macros without capturing parent refs
- reject unsupported command-body parameters with actionable errors
- add schema, expansion, discovery, and documentation coverage

Fixes #2466
Fixed:
- validate command-backed loop prompts during include expansion
- fail fast when included command files cannot be validated
- align include input documentation with the accepted string mapping

Tests added:
- cover every inline input substitution surface and loop command discovery
…t command scanning

Addresses the blocking findings from the review of #2467.

C1 — $INPUTS in workflow.fan_out.items was dropped. applyInputsMacro walked
node.input but not node.fan_out.items, so the literal reached the executor,
which JSON.parses it and spawns one child per unsubstituted placeholder. The
name never reached missingInputs either, so a caller who forgot the value got
no load error. Added to the workflow branch and to the "every other supported
inline node surface" test, whose title claimed a completeness it lacked.

C2 — three AI-turn surfaces were never walked: systemPrompt, agents.*.prompt
(plus .description, same delivery path) and approval.on_reject.prompt. These
are also blind spots in rewriteNodeOutputRefs, so the macro did mirror it
field-for-field — but the two have different fallbacks. A surface the rewrite
misses is only a namespacing miss; the executor's runtime pass still resolves
the ref. $INPUTS has no runtime pass at all, so a missed surface is permanent
literal text delivered to the model, and silent in both directions. Walked
here only; the rewrite half is a pre-existing gap left to its own issue.

F1 — args[name] read inherited Object.prototype members, so an unsupplied
$INPUTS.toString spliced a native function body into the prompt instead of
failing the load. Now Object.hasOwn, with anything not supplied as an own key
treated as missing.

F2 — the $INPUTS command-body guard tested fence-stripped content, so a fenced
or inline-code $INPUTS.scope passed validation and shipped unsubstituted. It
now tests raw content. This is the behaviour the macro's own docblock and the
shipped guide already claimed. The sibling-ref scan keeps stripping — it is
wrong there too, but that is pre-existing and goes with the rewrite gap.

I1 — reverted the unresolvable-command-file throw to WARN + continue. An
unreadable file is an incomplete-information state, not an unsafe one, and
failing it dropped workflows with no with: and no $INPUTS anywhere from
discovery entirely, breaking "undeclared includes keep working byte-for-byte".
The hard fail is kept for a file that was actually read and contains $INPUTS.
Documented the scan as best-effort by construction next to the message (S4).

Also folded in: the KEEP IN SYNC comment now counts four enumerations and
records that the drift it was meant to prevent had already happened (I3); the
flat dagNodeSchema.with comment now states the rationale that survives
scrutiny — workflow.with's phase-2 shape is undecided, so committing the
shared field to Record<string, string> risks a breaking change (I4); one
shared INPUT_NAME_SOURCE for the with:-key validator and INPUTS_REF, whose
drift is silent in one direction only (S6, narrowed to that pair); isCommandNode
added to the guard catalog now the rule of three is met (S10); docs scoped to
what the scan actually covers and given the explicit "never expressions"
clause (S1, S5); CLAUDE.md include: line updated (S3).

Deferred deliberately, each filed rather than fixed here: the loop_group body
command scan hole (I2 — pre-existing on both sides, wants its own coverage);
the rewriteNodeOutputRefs half of the three unwalked surfaces; with: accepted
and silently stripped on six non-include node modes; and the schema/enforcement
split on includeNodeSchema (S9 — the implementer followed Task 1 faithfully).
@Wirasm
Wirasm force-pushed the archon/task-archon-fix-github-issue-experimental-1785918317060 branch from eb95862 to d0fd551 Compare August 5, 2026 11:36
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@Wirasm
Wirasm merged commit ed4f6ac into dev Aug 5, 2026
5 checks passed
Wirasm added a commit that referenced this pull request Aug 6, 2026
The deterministic tier covered bash/script/bun/uv/timeout and two trigger rules. It did not cover join semantics against a skipped upstream, until_bash termination, output_type, or fan-out at all — so the composition primitives merged in #2223/#2224/#2467 had no unattended regression test.

Four workflows, no AI nodes, all assertions in bash:

  e2e-joins              trigger_rule all_done + none_failed_min_one_success against a
                         SKIPPED upstream, output_type, and a loop_group terminating
                         on until_bash
  e2e-echo-child         one bash node; the cheapest possible fan-out child
  e2e-fanout-alldone     fan_out over a literal list with one failing child; all_done
                         must aggregate it as {error,status}
  e2e-fanout-allsuccess  same list, join: all_success — must FAIL the node. Wired as a
                         negative test, so a zero exit is the regression and CI inverts
                         the assertion

Each runs in seconds and costs nothing, which is what makes them viable on every push
rather than as a deliberate exercise. Verified with --no-worktree, the form CI uses:
the first two exit 0, the third exits 1.

Also adds rasmus-tests/ for the AI-driven probes these were derived from. Those need a
funded provider and minutes per run, so they stay out of CI — t8-cascade additionally
needs a concurrent abandon to observe cascade-cancel and cannot run unattended without
a driver script. The split is now readable from either side: unattended work lives in
test-workflows/, subscription work in rasmus-tests/.

Two engine findings came out of running them: #2494 and #2495.
@Wirasm Wirasm mentioned this pull request Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(workflows): with: parameters on include: nodes — $INPUTS as a load-time macro (signature phase 1)

1 participant