Skip to content

feat(workflows): add maintainer-standup workflow for daily PR/issue triage#1428

Merged
Wirasm merged 2 commits intodevfrom
feat/maintainer-standup-workflow
Apr 27, 2026
Merged

feat(workflows): add maintainer-standup workflow for daily PR/issue triage#1428
Wirasm merged 2 commits intodevfrom
feat/maintainer-standup-workflow

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Apr 27, 2026

Summary

  • Problem: Maintainers (especially the main maintainer, with ~66 open PRs at time of writing) have no pre-built way to start their day with a prioritized, direction-aligned view of the queue.
  • Why it matters: Manual morning triage is slow, drifts in priority criteria across days, and rarely produces written history. PRs not aligned with project direction sit too long.
  • What changed: New maintainer-standup workflow + 3 gather scripts + a synthesis command file + a per-maintainer config layout under .archon/maintainer-standup/ (direction.md committed, profile/state/briefs gitignored).
  • What did NOT change (scope boundary): No engine, package, or platform-adapter code touched. No existing workflows modified. Only .archon/ user content + one .gitignore block + one ESLint global-ignore line.

UX Journey

Before

Maintainer                                    GitHub
──────────                                    ──────
opens laptop, sips coffee
scrolls `gh pr list`
re-derives priority from memory ◀──────────── 66 open PRs, all equal weight
context-switches to direction.md
mentally cross-references
forgets what was hot yesterday
picks something to work on

After

Maintainer                                       Archon                                    GitHub
──────────                                       ──────                                    ──────
runs `archon workflow run maintainer-standup`──▶ git-status, gh-data, read-context (parallel scripts)
                                                 ──────────────────────────────────────▶  fetches PRs/issues/closed
                                                 reads direction.md + profile.md + last 3 briefs + state.json
                                                 [synthesize node, Sonnet, output_format]
                                                 - classifies P1-P4 against direction.md
                                                 - cites direction clauses for declines
                                                 - diffs against prior run's observed_prs
                                                 - ages carry-over items across runs
                                                 [persist node, inline bun script]
                                                 - writes briefs/YYYY-MM-DD.md
                                                 - writes state.json
reads briefs/<today>.md ◀─────────────────────── (the prose brief)
acts on P1 list

Architecture Diagram

Before

.archon/
├── commands/        (existing)
├── scripts/         (existing — 2 echo demos)
├── workflows/       (existing — 11 workflows)
└── state/           (existing — repo-triage cross-run memory)

After

.archon/
├── commands/
│   └── maintainer-standup.md                  [+]   synthesis prompt
├── maintainer-standup/                        [+]   per-workflow folder
│   ├── direction.md                           [+]   committed, neutral north-star
│   ├── README.md                              [+]   setup docs
│   ├── profile.md.example                     [+]   committed template
│   ├── profile.md                             [+]   gitignored, per-maintainer
│   ├── state.json                             [+]   gitignored, auto-written
│   └── briefs/YYYY-MM-DD.md                   [+]   gitignored, auto-written
├── scripts/
│   ├── maintainer-standup-git-status.ts       [+]   bun, fetches origin/dev
│   ├── maintainer-standup-gh-data.ts          [+]   bun, gh queries
│   └── maintainer-standup-read-context.ts     [+]   bun, reads local state
└── workflows/
    └── maintainer-standup.yaml                [+]   DAG: 3 gathers ── synthesize ── persist

.gitignore                                     [~]   3 new patterns under .archon/maintainer-standup/
eslint.config.mjs                              [~]   1 new line: ignore .archon/**

Connection inventory:

From To Status Notes
.archon/workflows/maintainer-standup.yaml 3 gather scripts in .archon/scripts/ new parallel layer 0
3 gather scripts .archon/maintainer-standup/{state.json, profile.md, direction.md, briefs/} new scripts read local state
.archon/commands/maintainer-standup.md gather node outputs ($nodeId.output) new Sonnet synthesis with output_format
persist node .archon/maintainer-standup/{state.json, briefs/<date>.md} new inline bun script, JSON-as-JS-literal pattern
ESLint typed-rule pipeline .archon/** modified ignored — not in any tsconfig project

Label Snapshot

  • Risk: risk: low
  • Size: size: M
  • Scope: workflows
  • Module: workflows:maintainer-tooling

Change Metadata

  • Change type: feature
  • Primary scope: workflows

Linked Issue

Validation Evidence (required)

archon validate workflows maintainer-standup    # → ok (1 valid, 0 errors)
bun run lint                                    # → clean
bun run type-check                              # → clean across all 10 packages
bun run format:check                            # → "All matched files use Prettier code style!"
  • Evidence provided: Workflow validator passes; lint/type/format checks pass. End-to-end run completed successfully on the second invocation (DAG resume skipped the four already-completed nodes and persisted state cleanly in 15ms after the YAML fix).
  • Skipped commands and why: bun run test skipped — this PR adds only .archon/ user content + one .gitignore block + one ESLint ignore line. No source code modified, so no test coverage is at risk. Run on a follow-up if reviewer prefers.

Security Impact (required)

  • New permissions/capabilities? No.
  • New external network calls? Yes — gh pr list/view, gh issue list/view, git fetch origin. All already used by other bundled Archon tooling (e.g. repo-triage). No new auth, no new endpoints.
  • Secrets/tokens handling changed? No — uses existing gh auth as the user that invoked the workflow.
  • File system access scope changed? No — writes confined to .archon/maintainer-standup/ (already a user-scoped directory).

Compatibility / Migration

  • Backward compatible? Yes — additive.
  • Config/env changes? No.
  • Database migration needed? No.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios:
    • First run with no prior state — synthesizes baseline brief, snapshots observed_prs for next-run diffing (~5.4 min Sonnet synthesis on 66 PRs).
    • Failed-and-resumed flow — initial run failed in persist due to a String.raw template-literal collision with backticks in brief_markdown; reran cleanly via DAG resume in 15ms after fixing the inline script. Brief content and state file both written correctly.
    • Validator (archon validate workflows maintainer-standup) passes after each change.
  • Edge cases checked:
    • Missing state.jsonread-context.ts returns prior_state: null; synthesis falls back to first-run baseline.
    • Dirty working tree on devgit-status.ts logs pull_status: dirty and skips pull, still reports new commits since prior SHA.
    • Missing gh_handle in profile.mdgh-data.ts warns to stderr and skips review-requested / authored / assigned queries; the open-PR list still returns.
    • Backtick-bearing markdown in synthesized output → no longer breaks persist (root-cause: filed in docs/examples: String.raw $nodeId.output pattern is fragile when output contains backticks #1427 to fix the misleading example pattern).
  • What was not verified: invocation from a non-git directory (workflow is git-required by design and the CLI rejects this); behavior on a fresh clone with no gh auth login (would fail at gh-data.ts with a clear stderr message).

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: none — net-new files. The ESLint ignore line for .archon/** matches the existing .claude/skills/** precedent and drops typed-lint coverage that was previously broken anyway (no tsconfig project covers .archon/).
  • Potential unintended effects: if any contributor was relying on ESLint covering .archon/scripts/*.ts, that lint coverage is removed. In practice it could not have worked — await-thenable and other typed rules require parser-services that the global ignore now confirms are not configured for .archon/.
  • Guardrails / monitoring: archon validate workflows runs in this PR's CI and on every workflow invocation. The failed-and-resumed run exercised the standard DAG resume path and persisted cleanly.

Rollback Plan (required)

  • Fast rollback command/path: git revert <merge-commit-sha> — single commit, 10 files, all additive (except 2 small modified files). All gitignored runtime artifacts under .archon/maintainer-standup/ (profile/state/briefs) are local-only and unaffected.
  • Feature flags or config toggles: none — the workflow is opt-in by name; not running it changes nothing.
  • Observable failure symptoms: archon validate workflows maintainer-standup would fail; CI catches. Runtime failure of any node surfaces in standard workflow run output and is auto-resumable.

Risks and Mitigations

  • Risk: synthesis prompt may produce malformed JSON if Claude drifts from output_format schema — would fail at persist node again.
    • Mitigation: output_format is SDK-enforced on Claude; the failed-and-resumed flow this PR went through proves the persist node can be fixed and re-run without losing the (expensive) synthesis work via DAG resume.
  • Risk: Pi-provider users expecting to run this workflow on a cheaper model — output_format is best-effort on Pi and could silently parse-fail on a deeply nested schema.
    • Mitigation: workflow pins provider: claude + model: sonnet. README does not advertise Pi compatibility. Cross-provider work can come as a follow-up if Pi/Minimax demonstrates schema reliability.
  • Risk: per-maintainer profile/state coupling — each contributor's local state divergence could surprise them after a long absence.
    • Mitigation: state.json is overwritten each run; briefs/ is purely additive history. Worst case is "the first run of the day reads an old state and reports lots of carry-over deltas," which is informational, not destructive.

Summary by CodeRabbit

  • New Features
    • Added a daily Maintainer Standup that synthesizes git/GitHub activity into a single brief with prioritized P1–P4 triage, carry‑over handling, and surfaced direction questions.
  • Documentation
    • New README, direction guide, and profile template explaining workflow usage and how to tune triage behavior.
  • Chores
    • Persisted briefs/state, updated ignore and lint rules to exclude per‑maintainer standup files.

…riage

Daily morning briefing that pulls origin/dev, triages all open PRs and assigned
issues against direction.md, and surfaces progress vs. the previous run. Designed
for live-checkout use (worktree.enabled: false) so it can read its own state.

Layout under .archon/maintainer-standup/:
  - direction.md (committed) — project north-star: what Archon IS / IS NOT.
    Drives PR P4 polite-decline classification with cited clauses.
  - README.md / profile.md.example — setup docs and template for new maintainers.
  - profile.md, state.json, briefs/YYYY-MM-DD.md — gitignored, per-maintainer.

Engine:
  - 3 parallel gather scripts in .archon/scripts/maintainer-standup-*.ts
    (git-status, gh-data, read-context) — bun runtime, JSON stdout.
  - Synthesis node: command file with output_format schema for
    { brief_markdown, next_state }.
  - Persist node: tiny inline bun script writes both to disk.

Run-to-run continuity: state.json carries observed_prs/issues snapshots, so the
next run can detect what merged, what closed, what the maintainer shipped, and
which carry-over items aged past N days.

Also adds .archon/** to the ESLint global ignore list (matches the existing
.claude/skills/** pattern) since .archon/ is user content and not part of any
tsconfig project.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b7895d8-59ce-4df6-9c08-a779fe342303

📥 Commits

Reviewing files that changed from the base of the PR and between b492400 and 4db48b0.

📒 Files selected for processing (4)
  • .archon/commands/maintainer-standup.md
  • .archon/scripts/maintainer-standup-gh-data.ts
  • .archon/scripts/maintainer-standup-git-status.ts
  • .archon/workflows/maintainer-standup.yaml

📝 Walkthrough

Walkthrough

Adds a maintainer standup system: three data-gathering Bun/TypeScript scripts (git status, GitHub data, read context), a workflow that runs them in parallel and invokes a synthesis step, templates and docs for triage (P1–P4) and carry-over state, and persistence of brief markdown and next_state JSON.

Changes

Cohort / File(s) Summary
Workflow & Command
.archon/workflows/maintainer-standup.yaml, .archon/commands/maintainer-standup.md
New workflow that triggers daily/morning briefs, runs three collection scripts in parallel, enforces synthesis output contract (brief_markdown + next_state), and persists state and dated brief files.
Data Collection Scripts
.archon/scripts/maintainer-standup-git-status.ts, .archon/scripts/maintainer-standup-gh-data.ts, .archon/scripts/maintainer-standup-read-context.ts
Adds three Bun/TypeScript scripts: git-status (fetch/pull, dev SHA, dirty state, new commits/diff), gh-data (queries gh for PRs/issues, authored commits, recent closed items), read-context (loads direction.md, profile.md, prior state, recent briefs).
Documentation & Config
.archon/maintainer-standup/README.md, .archon/maintainer-standup/direction.md, .archon/maintainer-standup/profile.md.example
Introduces README, direction guidance for triage and citation rules, and a profile example for per-maintainer settings.
Persistence & Ignoring
.gitignore, eslint.config.mjs, .archon/maintainer-standup/*
Ignores per-maintainer profile, state, and briefs in git; adds .archon/** to ESLint ignore so new scripts/docs are excluded from linting.

Sequence Diagram

sequenceDiagram
    participant Trigger as Workflow Trigger
    participant GS as GitStatus (script)
    participant GH as GH-Data (script)
    participant RC as ReadContext (script)
    participant Synth as Synthesis (command)
    participant FS as FileSystem

    Trigger->>GS: start (parallel)
    Trigger->>GH: start (parallel)
    Trigger->>RC: start (parallel)

    GS->>FS: read prior state.json
    GS->>FS: git fetch/pull, compute SHA, dirty, commits/diff
    GS-->>Synth: emit git-status.output (JSON)

    GH->>FS: read profile.md, prior state.json
    GH->>GH: run gh queries (PRs, issues), git log for author
    GH-->>Synth: emit gh-data.output (JSON)

    RC->>FS: read direction.md, profile.md, prior state, recent briefs
    RC-->>Synth: emit read-context.output (JSON)

    Synth->>Synth: compare prior vs current snapshots
    Synth->>Synth: compute resolved/carry-over, assign P1–P4, surface direction questions
    Synth-->>FS: write next_state (state.json)
    Synth-->>FS: write brief markdown (briefs/YYYY-MM-DD.md)
    Synth-->>Trigger: complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Poem

🐇 I hopped through commits at break of day,

Gathered PRs and issues in tidy array,
Tagged them P1 through P4 with care,
Carried the slow ones like carrots to bear,
A brief for the maintainer — fresh as spring air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(workflows): add maintainer-standup workflow for daily PR/issue triage' directly and clearly summarizes the main change—the addition of a new maintainer-standup workflow for daily PR/issue triage.
Description check ✅ Passed The description is comprehensive and well-structured, covering all key template sections: Summary (problem, impact, changes, scope), UX Journey (before/after flows), Architecture Diagram (module layout and connections), Change Metadata (type, scope), Validation Evidence (with test results and reasoning for skips), Security Impact (network calls and auth), Compatibility (backward-compatible), Human Verification (tested scenarios and edge cases), Side Effects, Rollback Plan, and Risks/Mitigations.
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-workflow

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 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: 5

🧹 Nitpick comments (4)
.archon/scripts/maintainer-standup-git-status.ts (2)

17-24: Prefer execFileSync with argv arrays over execSync for git commands.

execSync invokes a shell and (at L58/L61) interpolates priorSha directly into the command string. priorSha is read from state.json, which is written by the workflow itself, so the practical risk is low — but using execFileSync with an argv array eliminates shell parsing entirely and aligns with the project convention of avoiding exec for git calls. It also means dropping the shell quotes around --format=....

🛠 Proposed refactor
-import { execSync } from 'node:child_process';
+import { execFileSync } from 'node:child_process';
@@
-function run(cmd: string): { stdout: string; ok: boolean } {
+function run(args: string[]): { stdout: string; ok: boolean } {
   try {
-    const out = execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
+    const out = execFileSync('git', args, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
     return { stdout: out, ok: true };
   } catch {
     return { stdout: '', ok: false };
   }
 }
@@
-run('git fetch origin dev');
+run(['fetch', 'origin', 'dev']);
@@
-const currentBranch = run('git rev-parse --abbrev-ref HEAD').stdout.trim();
-const isDirty = run('git status --porcelain').stdout.trim().length > 0;
+const currentBranch = run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
+const isDirty = run(['status', '--porcelain']).stdout.trim().length > 0;
@@
-  const result = run('git pull --ff-only origin dev');
+  const result = run(['pull', '--ff-only', 'origin', 'dev']);
@@
-const currentDevSha = run('git rev-parse origin/dev').stdout.trim();
+const currentDevSha = run(['rev-parse', 'origin/dev']).stdout.trim();
@@
-  const log = run(`git log ${priorSha}..origin/dev --no-decorate --format="%h %an: %s"`);
+  const log = run(['log', `${priorSha}..origin/dev`, '--no-decorate', '--format=%h %an: %s']);
   if (log.ok) {
     newCommits = log.stdout;
-    diffStat = run(`git diff --stat ${priorSha}..origin/dev`).stdout;
+    diffStat = run(['diff', '--stat', `${priorSha}..origin/dev`]).stdout;

Based on learnings: Use archon/git functions for git operations; use execFileAsync (not exec) when calling git directly. (archon/git doesn't apply here since .archon/scripts must stay monorepo-free, but the execFile recommendation still holds.)

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

In @.archon/scripts/maintainer-standup-git-status.ts around lines 17 - 24, The
run function currently uses execSync with a shell string; change it to use
child_process.execFileSync (or execFileSync) and pass git and its arguments as
an argv array instead of a shell command to avoid shell interpolation; update
the implementation referenced by the run function so calls that previously
interpolated priorSha or used quoted format flags become execFileSync('git',
['log', '--format=...'] , { encoding: 'utf8', stdio: ['ignore','pipe','pipe']
})-style calls (i.e., drop shell quoting and pass each flag/arg as array
elements) and preserve the returned shape { stdout: string; ok: boolean } and
error handling.

56-65: Validate priorSha shape before interpolating into git ranges.

If state.json ends up with a last_dev_sha that isn't a valid object name (e.g., empty after trim, contains whitespace, was hand-edited), git log <bad>..origin/dev will fail and the script falls back to "(prior SHA not found locally — full diff unavailable)" — which is the correct fallback, but it would be cleaner to short-circuit before invoking git. A simple /^[0-9a-f]{7,40}$/i check on priorSha would also harden the path against any future upstream change to how state is populated.

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

In @.archon/scripts/maintainer-standup-git-status.ts around lines 56 - 65,
Validate the priorSha value before using it in git range commands: in the block
that compares priorSha and currentDevSha, add a check using a regex like
/^[0-9a-f]{7,40}$/i against priorSha and if it fails, skip calling run(`git log
...`) and run(`git diff ...`) and set newCommits to the existing fallback string
and leave diffStat empty (or the current fallback behavior); update the code
around the priorSha/currentDevSha check and use the run(...) calls, newCommits,
and diffStat variables referenced in the snippet so invalid or empty priorSha
values never get interpolated into git commands.
.archon/scripts/maintainer-standup-gh-data.ts (1)

33-39: Frontmatter regex is unscoped — picks up any line in the file.

The /m flag makes ^gh_handle:\s*(\S+)\s*$ match anywhere, not just inside the leading ------ block. If profile.md later includes an example or quoted snippet with gh_handle: someone, that line will win. Cheap fix is to slice the frontmatter first:

Proposed fix
-  const profile = readFileSync(profilePath, 'utf8');
-  const match = profile.match(/^gh_handle:\s*(\S+)\s*$/m);
-  if (match) ghHandle = match[1];
+  const profile = readFileSync(profilePath, 'utf8');
+  const fm = profile.match(/^---\r?\n([\s\S]*?)\r?\n---/);
+  const match = (fm?.[1] ?? profile).match(/^gh_handle:\s*(\S+)\s*$/m);
+  if (match) ghHandle = match[1];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/scripts/maintainer-standup-gh-data.ts around lines 33 - 39, The
frontmatter regex is unscoped and currently searches the whole file via
profile.match; instead, read the file into profile (already done) but first
extract the YAML frontmatter bounded by leading '---' and the next '---' (or
EOF) and run the gh_handle regex against that slice only; update the code around
profilePath/profile/readFileSync and replace the profile.match(...) usage with a
match on the sliced frontmatter so ghHandle is set only from the top-level
frontmatter block.
.archon/commands/maintainer-standup.md (1)

20-38: Add a language to the upstream-output fenced blocks (MD040).

markdownlint-cli2 flags lines 20, 28, and 36. Use text (or json if appropriate) so docs lint stays clean.

Proposed fix
-```
+```text
 $git-status.output

Apply the same change to the `$gh-data.output` and `$read-context.output` fences.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @.archon/commands/maintainer-standup.md around lines 20 - 38, The fenced code
blocks for the upstream outputs are missing a language tag and failing
markdownlint (MD040); update the three fences that wrap $git-status.output,
$gh-data.output, and $read-context.output to include a language identifier (e.g.
use text or json as appropriate) so each block reads like text followed by the existing block contents and a closing , ensuring lint passes for
MD040.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.archon/commands/maintainer-standup.md:

  • Line 71: The P1 rule references CI status that isn’t being fetched: update the
    data gatherer or the doc to stay consistent. Either add the statusCheckRollup
    GraphQL field to the prFields array in maintainer-standup-gh-data.ts so
    gh-data.output.all_open_prs includes CI/status info (so the model can apply
    "green CI" without extra gh calls), or soften the P1 wording in
    .archon/commands/maintainer-standup.md to require “CI green per gh pr checks if
    you drill in” (or similar) so prompts match current inputs; pick one approach
    and apply it consistently (modify prFields to include statusCheckRollup if you
    want automated checks, otherwise adjust the P1 sentence).

In @.archon/scripts/maintainer-standup-gh-data.ts:

  • Around line 76-79: The current creation of allOpenPrs via parseJson(run(gh pr list --state open --limit 100 --json ${prFields}), []) can silently truncate
    results; change the gh invocation to fetch the full set (e.g. use --paginate
    and a high --limit such as 1000: run(gh pr list --state open --limit 1000 --paginate --json ${prFields})) or implement truncation detection: after
    running the query check the returned count against the requested limit and, if
    equal, emit a stderr warning and include a truncated: true flag alongside
    observed_prs so synthesis can handle incomplete snapshots; update the code paths
    that populate observed_prs (the allOpenPrs parseJson call and similar
    recently_closed_prs/recently_closed_issues invocations) accordingly.
  • Around line 15-22: The run function currently shells out via execSync(cmd)
    which allows shell injection; change it to use child_process.execFileSync(file,
    args, options) and update all call sites that build shell strings (the places
    that currently interpolate ghHandle and lastRunAt) to pass the executable (e.g.,
    "gh") and an args array with ghHandle and lastRunAt as distinct elements instead
    of interpolating them into one cmd string; validate ghHandle right after parsing
    (variable ghHandle) with the GitHub username regex
    /^a-zA-Z0-9?$/ and reject or sanitize invalid values,
    and keep equivalent stdio/error handling and the same fallback return ('[]') in
    run (or a renamed helper) while including the caught error message in the error
    output.

In @.archon/workflows/maintainer-standup.yaml:

  • Around line 117-144: The script currently overwrites briefs/.md and
    doesn't handle write failures; update the logic around briefsDir/briefPath to
    (1) detect if briefPath exists and, on collision, append an incrementing numeric
    suffix like -1, -2 until a non-existent filepath is found before calling
    writeFileSync, and (2) wrap the writeFileSync calls (for both state.json and the
    brief file) in a try/catch that on error prints the full data.brief_markdown to
    stdout (console.log) and exits non-zero so the synthesized output is not lost;
    reference the existing symbols writeFileSync, briefPath, briefsDir,
    data.brief_markdown, existsSync, and mkdirSync when making the changes.
  • Around line 136-138: The filename uses UTC via new
    Date().toISOString().slice(0, 10), which can produce the wrong local calendar
    day; change the date generation to use the local date instead (e.g., compute
    local year/month/day from new Date() or a helper like
    toLocaleDateString('sv-SE')) so the variable date, used when building briefPath
    (resolve(briefsDir, ${date}.md)) reflects the maintainer's local "today"
    before calling writeFileSync; update the date assignment in that block to use
    the local-date helper.

Nitpick comments:
In @.archon/commands/maintainer-standup.md:

  • Around line 20-38: The fenced code blocks for the upstream outputs are missing
    a language tag and failing markdownlint (MD040); update the three fences that
    wrap $git-status.output, $gh-data.output, and $read-context.output to include a
    language identifier (e.g. use text or json as appropriate) so each block
    reads like text followed by the existing block contents and a closing ,
    ensuring lint passes for MD040.

In @.archon/scripts/maintainer-standup-gh-data.ts:

  • Around line 33-39: The frontmatter regex is unscoped and currently searches
    the whole file via profile.match; instead, read the file into profile (already
    done) but first extract the YAML frontmatter bounded by leading '---' and the
    next '---' (or EOF) and run the gh_handle regex against that slice only; update
    the code around profilePath/profile/readFileSync and replace the
    profile.match(...) usage with a match on the sliced frontmatter so ghHandle is
    set only from the top-level frontmatter block.

In @.archon/scripts/maintainer-standup-git-status.ts:

  • Around line 17-24: The run function currently uses execSync with a shell
    string; change it to use child_process.execFileSync (or execFileSync) and pass
    git and its arguments as an argv array instead of a shell command to avoid shell
    interpolation; update the implementation referenced by the run function so calls
    that previously interpolated priorSha or used quoted format flags become
    execFileSync('git', ['log', '--format=...'] , { encoding: 'utf8', stdio:
    ['ignore','pipe','pipe'] })-style calls (i.e., drop shell quoting and pass each
    flag/arg as array elements) and preserve the returned shape { stdout: string;
    ok: boolean } and error handling.
  • Around line 56-65: Validate the priorSha value before using it in git range
    commands: in the block that compares priorSha and currentDevSha, add a check
    using a regex like /^[0-9a-f]{7,40}$/i against priorSha and if it fails, skip
    calling run(git log ...) and run(git diff ...) and set newCommits to the
    existing fallback string and leave diffStat empty (or the current fallback
    behavior); update the code around the priorSha/currentDevSha check and use the
    run(...) calls, newCommits, and diffStat variables referenced in the snippet so
    invalid or empty priorSha values never get interpolated into git commands.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `9e7688c4-5595-4fc8-94cb-c905623122ef`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b286ad97d88b74c96e7526371c6cc0ec16c80efb and b492400c5703803c119de9838d187bacdefc8a5a.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `.archon/commands/maintainer-standup.md`
* `.archon/maintainer-standup/README.md`
* `.archon/maintainer-standup/direction.md`
* `.archon/maintainer-standup/profile.md.example`
* `.archon/scripts/maintainer-standup-gh-data.ts`
* `.archon/scripts/maintainer-standup-git-status.ts`
* `.archon/scripts/maintainer-standup-read-context.ts`
* `.archon/workflows/maintainer-standup.yaml`
* `.gitignore`
* `eslint.config.mjs`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread .archon/commands/maintainer-standup.md Outdated
Comment on lines +15 to +22
function run(cmd: string): string {
try {
return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
} catch (e) {
process.stderr.write(`gh command failed: ${cmd}\n${(e as Error).message}\n`);
return '[]';
}
}
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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

gh CLI: does gh pr list --json paginate beyond --limit, and what is the maximum --limit accepted?

💡 Result:

No, gh pr list --json does not automatically paginate beyond the value specified by --limit (default 30). It respects the --limit as the maximum number of items to fetch and does not fetch additional pages automatically. Users must use gh api graphql --paginate for full pagination if needed. There is no documented maximum for --limit, but practical limits apply due to GraphQL constraints (e.g., node limits when including fields like commits) and GitHub Search API caps (1000 results max when using --label or --search). Examples show --limit=50000 or --limit=1000 being attempted, but effectiveness depends on filters and fields requested. For large limits without search filters, it uses paginated GraphQL queries (first:100 per page), but stops at the specified --limit.

Citations:


🏁 Script executed:

cat -n .archon/scripts/maintainer-standup-gh-data.ts | sed -n '1,160p'

Repository: coleam00/Archon

Length of output: 5801


Switch execSync to execFileSync with argument arrays to eliminate shell injection.

The code interpolates ghHandle (parsed as \S+ at line 37) and lastRunAt (from state.json at line 50) directly into shell commands at lines 88, 94, 100, 140. Quoted variables in shell still permit command substitution ($(), backticks, ${...}), and line 88 is unquoted. If profile.md or state.json ever contain shell metacharacters, the script executes arbitrary commands.

Replace execSync(cmd, ...) with execFileSync(file, args, ...) and pass ghHandle and lastRunAt as separate array elements. Example at lines 76–79:

Proposed fix
-import { execSync } from 'node:child_process';
+import { execFileSync } from 'node:child_process';
@@
-function run(cmd: string): string {
+function run(file: string, args: string[]): string {
   try {
-    return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
+    return execFileSync(file, args, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
   } catch (e) {
-    process.stderr.write(`gh command failed: ${cmd}\n${(e as Error).message}\n`);
+    process.stderr.write(`command failed: ${file} ${args.join(' ')}\n${(e as Error).message}\n`);
     return '[]';
   }
 }
@@
-const allOpenPrs = parseJson<unknown[]>(
-  run(`gh pr list --state open --limit 100 --json ${prFields}`),
-  [],
-);
+const allOpenPrs = parseJson<unknown[]>(
+  run('gh', ['pr', 'list', '--state', 'open', '--limit', '100', '--json', prFields]),
+  [],
+);

Apply similarly to lines 88, 94, 100, 136–140.

At minimum, validate ghHandle against GitHub's username rule (/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38})$/) immediately after parsing (line 38).

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

In @.archon/scripts/maintainer-standup-gh-data.ts around lines 15 - 22, The run
function currently shells out via execSync(cmd) which allows shell injection;
change it to use child_process.execFileSync(file, args, options) and update all
call sites that build shell strings (the places that currently interpolate
ghHandle and lastRunAt) to pass the executable (e.g., "gh") and an args array
with ghHandle and lastRunAt as distinct elements instead of interpolating them
into one cmd string; validate ghHandle right after parsing (variable ghHandle)
with the GitHub username regex /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38})?$/ and reject
or sanitize invalid values, and keep equivalent stdio/error handling and the
same fallback return ('[]') in run (or a renamed helper) while including the
caught error message in the error output.

Comment on lines +76 to +79
const allOpenPrs = parseJson<unknown[]>(
run(`gh pr list --state open --limit 100 --json ${prFields}`),
[],
);
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

--limit 100 can silently truncate all_open_prs and break the observed_prs invariant.

The synthesis command (.archon/commands/maintainer-standup.md Phase 3) requires next_state.observed_prs to include every entry in all_open_prs; that property is what makes "resolved since last run" detection correct on subsequent runs. With --limit 100, once the open-PR count exceeds 100 the snapshot is silently incomplete and PRs that fall off the tail will be misclassified as "resolved" the next day.

Suggested options:

  • Use --paginate (and a more permissive --limit) to fetch the full set, e.g. gh pr list --state open --limit 1000 --json ...gh paginates internally for --json queries.
  • Or detect saturation and emit a stderr warning + a truncated: true flag in the output so the synthesis prompt can disclose it instead of silently dropping items.

The same concern applies, more mildly, to --limit 50 on recently_closed_prs/recently_closed_issues (lines 121, 127) when since_date is far in the past after a long break.

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

In @.archon/scripts/maintainer-standup-gh-data.ts around lines 76 - 79, The
current creation of allOpenPrs via parseJson(run(`gh pr list --state open
--limit 100 --json ${prFields}`), []) can silently truncate results; change the
gh invocation to fetch the full set (e.g. use `--paginate` and a high `--limit`
such as 1000: run(`gh pr list --state open --limit 1000 --paginate --json
${prFields}`)) or implement truncation detection: after running the query check
the returned count against the requested limit and, if equal, emit a stderr
warning and include a `truncated: true` flag alongside observed_prs so synthesis
can handle incomplete snapshots; update the code paths that populate
observed_prs (the allOpenPrs parseJson call and similar
recently_closed_prs/recently_closed_issues invocations) accordingly.

Comment on lines +117 to +144
script: |
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';

// JSON is valid JS expression syntax — substitute directly without a
// template literal. Wrapping in String.raw breaks if the output contains
// backticks (e.g. markdown code spans inside brief_markdown).
const data = $synthesize.output;

const baseDir = resolve(process.cwd(), '.archon/maintainer-standup');
if (!existsSync(baseDir)) mkdirSync(baseDir, { recursive: true });

writeFileSync(
resolve(baseDir, 'state.json'),
JSON.stringify(data.next_state, null, 2) + '\n',
);

const briefsDir = resolve(baseDir, 'briefs');
if (!existsSync(briefsDir)) mkdirSync(briefsDir, { recursive: true });
const date = new Date().toISOString().slice(0, 10);
const briefPath = resolve(briefsDir, `${date}.md`);
writeFileSync(briefPath, data.brief_markdown);

console.log(JSON.stringify({
date,
state_path: '.archon/maintainer-standup/state.json',
brief_path: `.archon/maintainer-standup/briefs/${date}.md`,
}));
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

Persist node silently overwrites and has no fallback if writes fail.

Two minor reliability gaps in the inline persist script:

  1. If briefs/<date>.md already exists (re-run on the same day), it's silently overwritten — losing the earlier brief without warning.
  2. There's no try/catch around writeFileSync; a transient I/O failure after the LLM synthesis discards the (expensive) brief output.

Consider appending a numeric suffix on collision and printing the full brief_markdown to stdout on write failure so the run isn't a total loss.

🛠 Suggested approach
       writeFileSync(
         resolve(baseDir, 'state.json'),
         JSON.stringify(data.next_state, null, 2) + '\n',
       );

       const briefsDir = resolve(baseDir, 'briefs');
       if (!existsSync(briefsDir)) mkdirSync(briefsDir, { recursive: true });
-      const date = new Date().toISOString().slice(0, 10);
-      const briefPath = resolve(briefsDir, `${date}.md`);
-      writeFileSync(briefPath, data.brief_markdown);
+      const date = /* local YYYY-MM-DD */;
+      let briefPath = resolve(briefsDir, `${date}.md`);
+      let n = 2;
+      while (existsSync(briefPath)) {
+        briefPath = resolve(briefsDir, `${date}-${n}.md`);
+        n++;
+      }
+      try {
+        writeFileSync(briefPath, data.brief_markdown.endsWith('\n') ? data.brief_markdown : data.brief_markdown + '\n');
+      } catch (err) {
+        process.stderr.write(`Failed to write brief (${String(err)}); dumping to stdout:\n`);
+        process.stdout.write(data.brief_markdown);
+        throw err;
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/workflows/maintainer-standup.yaml around lines 117 - 144, The script
currently overwrites briefs/<date>.md and doesn't handle write failures; update
the logic around briefsDir/briefPath to (1) detect if briefPath exists and, on
collision, append an incrementing numeric suffix like `-1`, `-2` until a
non-existent filepath is found before calling writeFileSync, and (2) wrap the
writeFileSync calls (for both state.json and the brief file) in a try/catch that
on error prints the full data.brief_markdown to stdout (console.log) and exits
non-zero so the synthesized output is not lost; reference the existing symbols
writeFileSync, briefPath, briefsDir, data.brief_markdown, existsSync, and
mkdirSync when making the changes.

Comment thread .archon/workflows/maintainer-standup.yaml Outdated
- gh-data: bump --limit 100 → 1000 on all_open_prs and warn loudly when
  the cap is hit; preserves the observed_prs invariant the next-run
  "resolved since last run" diff depends on. (CodeRabbit critical)
- maintainer-standup.md: clarify P1 CI signal — the gathered payload only
  carries mergeStateStatus, not statusCheckRollup; for borderline P1s,
  drill in via `gh pr checks <n>`. (CodeRabbit minor)
- workflow.yaml persist: write briefs under local YYYY-MM-DD (sv-SE
  locale) instead of UTC ISO date, so an evening run doesn't file
  tomorrow's brief and break recent_briefs lookups. (CodeRabbit minor)
- workflow.yaml persist: wrap state/brief writes in try/catch; on
  failure dump brief_markdown and next_state to stderr so a 5-minute
  Sonnet synthesis isn't lost to a transient disk error. (CodeRabbit minor)
- gh-data + git-status: switch from execSync (shell-string) to
  execFileSync (argv array) for git/gh invocations. Defense-in-depth
  against shell metacharacters in values that pass through (esp. the
  gh_handle from profile.md). (CodeRabbit nitpick)
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