Skip to content

feat(maintainer): Pi/Minimax variant of maintainer-standup + dual-format persist#1480

Merged
Wirasm merged 1 commit intodevfrom
feat/maintainer-standup-pi-format
Apr 29, 2026
Merged

feat(maintainer): Pi/Minimax variant of maintainer-standup + dual-format persist#1480
Wirasm merged 1 commit intodevfrom
feat/maintainer-standup-pi-format

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Apr 29, 2026

Summary

  • Problem: The original maintainer-standup workflow uses Claude SDK-enforced output_format (one big JSON wrapping a ~12KB markdown brief plus state). It works on Claude Sonnet but hangs in nested Claude Code sessions (2 issues: v0.3.5: CLI workflow run silently hangs — dotenv loads .env from CWD instead of ~/.archon/.env,, + rchon serve hardcodes skipPlatformAdapters:true — Telegram/Discord/Slack adapters are unreachable #1067), and Pi/Minimax can't reliably emit a 30KB JSON wrapper around markdown content (parser failures, prose preamble interference).
  • Why it matters: Maintainer wants a fallback path when the Claude variant is unavailable or when burning Claude tokens on a daily ~5-min synth isn't the right trade-off. Without a Pi-compatible standup, the daily brief silently fails on nested-session days.
  • What changed:
    • Drop output_format from the synthesize node.
    • New output contract: brief markdown emitted plain, followed by an ARCHON_STATE_JSON_BEGIN / ARCHON_STATE_JSON_END delimited state JSON block.
    • Replace the script: persist node with a bash: node that pipes raw synth output into a new .archon/scripts/maintainer-standup-persist.ts. The script tries the delimiter format first; falls back to extracting {brief_markdown, next_state} from a JSON wrapper (the format Pi/Minimax stubbornly emits regardless of prompt instructions). Either format yields the same files on disk.
    • Add .archon/workflows/maintainer/maintainer-standup-minimax.yaml — a 1:1 copy of the Claude variant but with provider: pi, model: minimax/MiniMax-M2.7.
    • Strengthen synthesizer-prompt format directive (top-of-file, with negative example "do not wrap in JSON object").
  • What did NOT change (scope boundary): No engine changes. No bundled-default changes. No user-facing surface — these are maintainer-side tooling files under .archon/ that don't ship with Archon binaries.

Label Snapshot

  • Risk: risk: low
  • Size: size: M
  • Scope: core
  • Module: tools:maintainer-standup

Change Metadata

  • Change type: feature
  • Primary scope: core

Linked Issue

Validation Evidence (required)

$ bun run validate
(passes — no engine changes, no test files modified)

End-to-end behavioral test: ran archon workflow run maintainer-standup-minimax against today's payload (60+ open PRs, 19 open issues). Synthesize node emitted prose-preamble + JSON-wrapper format despite prompt instructions; persist script recovered via the fallback path and wrote .archon/maintainer-standup/briefs/2026-04-29.md (17KB) + state.json. Logged Synth output used JSON-wrapper format (delimiter contract not followed); recovered via fallback. to stderr as designed.

  • Evidence provided: end-to-end run on the maintainer's live payload.
  • If any command is intentionally skipped: bun run generate:bundled not needed (no bundled-default files touched).

Security Impact (required)

  • New permissions/capabilities? No — same surface as existing standup workflow.
  • New external network calls? No — same gh api calls and Pi/Claude provider calls as today.
  • Secrets/tokens handling changed? No.
  • File system access scope changed? No — same .archon/maintainer-standup/ writes.

Compatibility / Migration

  • Backward compatible? Yes — the Claude variant still works (the synthesizer prompt change is a strict improvement; the dual-format persist accepts both old and new shapes).
  • Config/env changes? No.
  • Database migration needed? No.

Human Verification (required)

  • Verified scenarios:
    • Pi/Minimax variant: archon workflow run maintainer-standup-minimax end-to-end on macOS arm64; brief + state both written; verdict logging shows source: 'json-wrapper' (fallback path).
    • Resume behavior: a prior failed synth was re-used by a later run, only persist re-executed (36ms) and produced the brief.
  • Edge cases checked:
    • Synth output containing markdown code fences (backticks): handled because persist no longer uses String.raw template substitution; raw text is shell-quoted by the framework's bash substitution.
    • Synth output with prose preamble before the first heading: stripped by the script's "keep from first # heading onward" pass.
    • Synth output that uses the delimiter format correctly (Claude path): handled by Tier 1 of the dual-format parser.
  • What was not verified:

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: maintainer-standup and maintainer-standup-minimax only. No other workflow consumes the synthesizer command file. No engine changes.
  • Potential unintended effects: A future Claude run will see the new prompt directive and may emit the delimiter format instead of the JSON wrapper; persist handles either, so this is a no-op semantically.
  • Guardrails/monitoring for early detection: persist logs which path it took (source: 'delimiter' vs 'json-wrapper') to stdout, and stderr-logs the fallback case. If the fallback ever stops working, the maintainer sees a PERSIST FAILED error with the raw output dumped for log recovery.

Rollback Plan (required)

  • Fast rollback: revert the single commit. Files are scoped to .archon/; no engine impact.
  • Feature flags or config toggles: none — keep both yaml variants if you want to be able to A/B between providers.
  • Observable failure symptoms: PERSIST FAILED: could not extract brief and state from synth output in stderr if both extraction paths fail.

Risks and Mitigations

  • Risk: Future Pi/Minimax SDK or model updates change the output format such that neither delimiter nor JSON-wrapper matches.
    • Mitigation: persist dumps the raw output to stderr on failure; the brief is then recoverable from logs. Adding a Tier 3 path is a one-line addition to the script.
  • Risk: A real-world synth output happens to contain ARCHON_STATE_JSON_BEGIN literally inside the brief markdown.
    • Mitigation: extremely unlikely (specific marker name), and Tier 1 only matches when both BEGIN and END markers appear with valid JSON between — a false positive would fail JSON.parse and fall through to Tier 2.

Summary by CodeRabbit

  • New Features

    • Added maintainer-standup-minimax workflow with MiniMax AI model support
  • Refactor

    • Restructured maintainer standup output format to use delimited JSON blocks for improved clarity
    • Enhanced output persistence with robust multi-format parsing and fallback handling

…mat persist

The original maintainer-standup workflow relies on Claude SDK-enforced
output_format (one big JSON wrapping the brief markdown + state). It works on
Claude but hangs in nested Claude Code sessions (#1067), and Pi/Minimax can't
reliably emit a 30KB JSON wrapper around markdown content.

Changes:
- Drop output_format from synthesize node. Synthesis now emits brief
  markdown plain, followed by a delimited ARCHON_STATE_JSON_BEGIN/END
  block.
- Replace persist (script:) with bash: that pipes synth output into a new
  .archon/scripts/maintainer-standup-persist.ts. The script tries the
  delimiter format first and falls back to the legacy
  {brief_markdown, next_state} JSON wrapper Pi/Minimax tends to emit
  regardless of prompt instructions. Either format yields the same brief
  + state.json on disk.
- Add maintainer-standup-minimax.yaml — a 1:1 copy with provider: pi,
  model: minimax/MiniMax-M2.7 — for use when the Claude variant hangs or
  when the maintainer wants to spend Pi tokens instead.
- Update the maintainer-standup synthesizer prompt: top-of-file output
  format directive, explicit "do not wrap in JSON object" guidance,
  delimiter-based example.

Tested end-to-end on the Minimax variant against today's PR/issue payload;
brief was extracted via the JSON-wrapper fallback path and written to
.archon/maintainer-standup/briefs/2026-04-29.md.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

This change refactors the maintainer standup workflow by extracting persistence logic into a dedicated script and updating the output contract format. It introduces a new persistence script that handles two parsing strategies, modifies the main workflow to use external script-based persistence, adds a new minimax workflow variant, and updates command documentation to reflect the new delimiter-based JSON state format.

Changes

Cohort / File(s) Summary
Command Documentation
.archon/commands/maintainer-standup.md
Rewrites the workflow output contract to require a plain markdown brief followed by a standalone delimited JSON state block using ARCHON_STATE_JSON_BEGIN/ARCHON_STATE_JSON_END markers. Renames next_state.* fields to state.* and updates Phase 3/4 instructions to enforce delimiter placement and JSON validity.
Persistence Script
.archon/scripts/maintainer-standup-persist.ts
Adds new stdin-driven script that parses standup output using two fallback strategies: first attempting to extract delimited JSON state blocks, then falling back to parsing JSON objects with brief_markdown and next_state fields. Writes parsed briefs to dated markdown files and state to a central state.json file.
Workflow Configuration
.archon/workflows/maintainer/maintainer-standup.yaml, .archon/workflows/maintainer/maintainer-standup-minimax.yaml
Refactors the main workflow to remove inline persistence logic and pipe synthesis output to the external persist script via bash. Adds new minimax workflow variant that runs multi-node parallel data gathering followed by synthesis and external persistence.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #1428: Directly related—introduced the initial synth output format with brief_markdown and next_state fields; this PR extends that work by restructuring the output contract, renaming fields to state.*, requiring delimiter-based JSON blocks, and providing robust dual-format parsing in the new persistence script.
  • PR #1430: Related through workflow surface—modifies maintainer workflows in the .archon/workflows/maintainer/ directory alongside this change.

Poem

🐇 A rabbit's ode to scripts so fine,
Where briefs and states now intertwine,
No more inline, but pipes that flow,
Through delimiters—watch 'em go!
Persist with grace, and dates precise,
Our standup stands up—oh, how nice! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Pi/Minimax variant of maintainer-standup with dual-format persistence support.
Description check ✅ Passed The PR description follows the template structure with comprehensive coverage of problem, rationale, changes, scope boundaries, validation evidence, security impact, compatibility, verification, side effects, rollback, and risk mitigation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/maintainer-standup-pi-format

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
.archon/commands/maintainer-standup.md (1)

30-42: Add language identifiers to fenced examples to satisfy markdownlint.

Line 30 and Line 196 use fenced blocks without a language, which triggers MD040 warnings.

Suggested lint-only doc fix
-```
+```text
 # Maintainer Standup — 2026-04-29
@@
 ARCHON_STATE_JSON_END

@@
- +text
ARCHON_STATE_JSON_BEGIN
{
@@
ARCHON_STATE_JSON_END

Also applies to: 196-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/commands/maintainer-standup.md around lines 30 - 42, The markdown
contains fenced code blocks around the maintainer standup and the embedded
ARCHON_STATE_JSON sections that lack a language identifier (triggering MD040);
update the opening fences from ``` to ```text for the blocks delimited by
ARCHON_STATE_JSON_BEGIN and ARCHON_STATE_JSON_END (both the top example and the
later occurrence) so the fenced examples include the "text" language identifier
and satisfy markdownlint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.archon/scripts/maintainer-standup-persist.ts:
- Around line 28-31: The parsed JSON assigned to the module-level variables
(brief, state, source) is not schema-validated before being used or persisted;
add a runtime validation step (either a lightweight type-guard function or a
schema using zod/io-ts) that checks required keys and types for the expected
state shape, ensures brief is a non-empty string and source is one of
'delimiter'|'json-wrapper', and call this validator wherever parsed data is
assigned to state (the places that currently set state from JSON) and
immediately before the persist/write routine that saves state; if validation
fails, log a clear error via the existing logger and skip persisting (or recover
to a safe default) so malformed data is never written back.
- Around line 34-39: The end-marker lookup currently uses raw.indexOf(END) which
can find an earlier END before the real BEGIN; change it to search for END
starting after the found BEGIN (use the variant of indexOf that accepts a start
position) so endIdx = raw.indexOf(END, beginIdx + BEGIN.length) and keep the
existing checks (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) before
slicing stateText from raw.slice(beginIdx + BEGIN.length, endIdx). This ensures
the END anchor is anchored to the found BEGIN and prevents mistaking earlier
marker-like text.

---

Nitpick comments:
In @.archon/commands/maintainer-standup.md:
- Around line 30-42: The markdown contains fenced code blocks around the
maintainer standup and the embedded ARCHON_STATE_JSON sections that lack a
language identifier (triggering MD040); update the opening fences from ``` to
```text for the blocks delimited by ARCHON_STATE_JSON_BEGIN and
ARCHON_STATE_JSON_END (both the top example and the later occurrence) so the
fenced examples include the "text" language identifier and satisfy markdownlint.
🪄 Autofix (Beta)

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

Run ID: f01079fe-b116-4856-8601-66d45e8784e3

📥 Commits

Reviewing files that changed from the base of the PR and between 7d06773 and 9674c90.

📒 Files selected for processing (4)
  • .archon/commands/maintainer-standup.md
  • .archon/scripts/maintainer-standup-persist.ts
  • .archon/workflows/maintainer/maintainer-standup-minimax.yaml
  • .archon/workflows/maintainer/maintainer-standup.yaml

Comment on lines +28 to +31
type State = Record<string, unknown>;
let brief: string | null = null;
let state: State | null = null;
let source: 'delimiter' | 'json-wrapper' | null = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate required state schema before persisting.

Line 41 and Line 64 accept any parsed JSON as state, and Line 104 writes it directly. If required fields are missing or wrong types, next runs can silently degrade or fail during progress detection.

Proposed guardrail for state shape validation
-type State = Record<string, unknown>;
+type CarryOverItem = { kind: string; id: string; note: string; first_seen: string };
+type ObservedItem = { number: number; title: string };
+type State = {
+  last_run_at: string;
+  last_dev_sha: string;
+  carry_over: CarryOverItem[];
+  observed_prs: ObservedItem[];
+  observed_issues: ObservedItem[];
+  direction_questions: string[];
+};
+
+const isObjectRecord = (v: unknown): v is Record<string, unknown> =>
+  typeof v === 'object' && v !== null && !Array.isArray(v);
+
+function isState(v: unknown): v is State {
+  if (!isObjectRecord(v)) return false;
+  return (
+    typeof v.last_run_at === 'string' &&
+    typeof v.last_dev_sha === 'string' &&
+    Array.isArray(v.carry_over) &&
+    Array.isArray(v.observed_prs) &&
+    Array.isArray(v.observed_issues) &&
+    Array.isArray(v.direction_questions)
+  );
+}
@@
-    state = JSON.parse(stateText) as State;
+    const parsedState = JSON.parse(stateText);
+    if (!isState(parsedState)) {
+      throw new Error('Delimiter state JSON does not match required schema');
+    }
+    state = parsedState;
@@
-      const parsed = JSON.parse(candidate) as Record<string, unknown>;
+      const parsed = JSON.parse(candidate) as Record<string, unknown>;
       if (
         typeof parsed.brief_markdown === 'string' &&
-        typeof parsed.next_state === 'object' &&
-        parsed.next_state !== null
+        isState(parsed.next_state)
       ) {
         brief = parsed.brief_markdown;
-        state = parsed.next_state as State;
+        state = parsed.next_state;

Also applies to: 38-42, 57-65, 104-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/scripts/maintainer-standup-persist.ts around lines 28 - 31, The
parsed JSON assigned to the module-level variables (brief, state, source) is not
schema-validated before being used or persisted; add a runtime validation step
(either a lightweight type-guard function or a schema using zod/io-ts) that
checks required keys and types for the expected state shape, ensures brief is a
non-empty string and source is one of 'delimiter'|'json-wrapper', and call this
validator wherever parsed data is assigned to state (the places that currently
set state from JSON) and immediately before the persist/write routine that saves
state; if validation fails, log a clear error via the existing logger and skip
persisting (or recover to a safe default) so malformed data is never written
back.

Comment on lines +34 to +39
const BEGIN = 'ARCHON_STATE_JSON_BEGIN';
const END = 'ARCHON_STATE_JSON_END';
const beginIdx = raw.indexOf(BEGIN);
const endIdx = raw.indexOf(END);
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
const stateText = raw.slice(beginIdx + BEGIN.length, endIdx).trim();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Anchor closing-marker lookup to the opening marker.

Line 37 searches ARCHON_STATE_JSON_END from the start of the payload. If marker-like text appears before the real block, valid delimiter output can be misread.

Suggested delimiter extraction hardening
 const BEGIN = 'ARCHON_STATE_JSON_BEGIN';
 const END = 'ARCHON_STATE_JSON_END';
 const beginIdx = raw.indexOf(BEGIN);
-const endIdx = raw.indexOf(END);
+const endIdx = beginIdx === -1 ? -1 : raw.indexOf(END, beginIdx + BEGIN.length);
 if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/scripts/maintainer-standup-persist.ts around lines 34 - 39, The
end-marker lookup currently uses raw.indexOf(END) which can find an earlier END
before the real BEGIN; change it to search for END starting after the found
BEGIN (use the variant of indexOf that accepts a start position) so endIdx =
raw.indexOf(END, beginIdx + BEGIN.length) and keep the existing checks (beginIdx
!== -1 && endIdx !== -1 && endIdx > beginIdx) before slicing stateText from
raw.slice(beginIdx + BEGIN.length, endIdx). This ensures the END anchor is
anchored to the found BEGIN and prevents mistaking earlier marker-like text.

@Wirasm Wirasm merged commit ff924e8 into dev Apr 29, 2026
4 checks passed
@Wirasm Wirasm deleted the feat/maintainer-standup-pi-format branch April 29, 2026 09:10
@Wirasm Wirasm mentioned this pull request Apr 29, 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.

1 participant