feat(workflows): add maintainer-standup workflow for daily PR/issue triage#1428
feat(workflows): add maintainer-standup workflow for daily PR/issue triage#1428
Conversation
…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.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
.archon/scripts/maintainer-standup-git-status.ts (2)
17-24: PreferexecFileSyncwith argv arrays overexecSyncfor git commands.
execSyncinvokes a shell and (at L58/L61) interpolatespriorShadirectly into the command string.priorShais read fromstate.json, which is written by the workflow itself, so the practical risk is low — but usingexecFileSyncwith an argv array eliminates shell parsing entirely and aligns with the project convention of avoidingexecfor 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/gitdoesn't apply here since.archon/scriptsmust stay monorepo-free, but theexecFilerecommendation 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: ValidatepriorShashape before interpolating into git ranges.If
state.jsonends up with alast_dev_shathat isn't a valid object name (e.g., empty after trim, contains whitespace, was hand-edited),git log <bad>..origin/devwill 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}$/icheck onpriorShawould 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
/mflag makes^gh_handle:\s*(\S+)\s*$match anywhere, not just inside the leading---…---block. Ifprofile.mdlater includes an example or quoted snippet withgh_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-cli2flags lines 20, 28, and 36. Usetext(orjsonif appropriate) so docs lint stays clean.Proposed fix
-``` +```text $git-status.outputApply 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.
usetext orjson as appropriate) so each block reads liketext 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--limitsuch 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 atruncated: trueflag 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,-2until 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. usetext orjson as appropriate) so each block
reads liketext 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 -->
| 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 '[]'; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://cli.github.com/manual/gh_pr_list
- 2: gh pr list limitation ? cli/cli#3836
- 3: Including
commitsin--jsonfields ofgh pr listtoo easily exceeds graphql node limits cli/cli#5114 - 4: https://docs.github.com/en/graphql/guides/using-pagination-in-the-graphql-api
- 5: bypass --limit to fetch all issues cli/cli#9522
🏁 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.
| const allOpenPrs = parseJson<unknown[]>( | ||
| run(`gh pr list --state open --limit 100 --json ${prFields}`), | ||
| [], | ||
| ); |
There was a problem hiding this comment.
--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 ...—ghpaginates internally for--jsonqueries. - Or detect saturation and emit a stderr warning + a
truncated: trueflag 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.
| 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`, | ||
| })); |
There was a problem hiding this comment.
Persist node silently overwrites and has no fallback if writes fail.
Two minor reliability gaps in the inline persist script:
- If
briefs/<date>.mdalready exists (re-run on the same day), it's silently overwritten — losing the earlier brief without warning. - 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.
- 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)
Summary
maintainer-standupworkflow + 3 gather scripts + a synthesis command file + a per-maintainer config layout under.archon/maintainer-standup/(direction.md committed, profile/state/briefs gitignored)..archon/user content + one.gitignoreblock + one ESLint global-ignore line.UX Journey
Before
After
Architecture Diagram
Before
After
Connection inventory:
.archon/workflows/maintainer-standup.yaml.archon/scripts/.archon/maintainer-standup/{state.json, profile.md, direction.md, briefs/}.archon/commands/maintainer-standup.md$nodeId.output)output_format.archon/maintainer-standup/{state.json, briefs/<date>.md}.archon/**Label Snapshot
risk: lowsize: Mworkflowsworkflows:maintainer-toolingChange Metadata
featureworkflowsLinked Issue
$nodeId.outputpattern is fragile when output contains backticks #1427 (filed mid-PR —String.rawsubstitution pattern fragility caused the first run's persist failure; documented and fixed in this PR's persist node)Validation Evidence (required)
bun run testskipped — this PR adds only.archon/user content + one.gitignoreblock + 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)
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.ghauth as the user that invoked the workflow..archon/maintainer-standup/(already a user-scoped directory).Compatibility / Migration
Human Verification (required)
What was personally validated beyond CI:
observed_prsfor next-run diffing (~5.4 min Sonnet synthesis on 66 PRs).persistdue to aString.rawtemplate-literal collision with backticks inbrief_markdown; reran cleanly via DAG resume in 15ms after fixing the inline script. Brief content and state file both written correctly.archon validate workflows maintainer-standup) passes after each change.state.json→read-context.tsreturnsprior_state: null; synthesis falls back to first-run baseline.dev→git-status.tslogspull_status: dirtyand skips pull, still reports new commits since prior SHA.gh_handleinprofile.md→gh-data.tswarns to stderr and skips review-requested / authored / assigned queries; the open-PR list still returns.persist(root-cause: filed in docs/examples: String.raw$nodeId.outputpattern is fragile when output contains backticks #1427 to fix the misleading example pattern).gh auth login(would fail atgh-data.tswith a clear stderr message).Side Effects / Blast Radius (required)
.archon/**matches the existing.claude/skills/**precedent and drops typed-lint coverage that was previously broken anyway (notsconfigproject covers.archon/)..archon/scripts/*.ts, that lint coverage is removed. In practice it could not have worked —await-thenableand other typed rules require parser-services that the global ignore now confirms are not configured for.archon/.archon validate workflowsruns 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)
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.archon validate workflows maintainer-standupwould fail; CI catches. Runtime failure of any node surfaces in standard workflow run output and is auto-resumable.Risks and Mitigations
output_formatschema — would fail atpersistnode again.output_formatis 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.output_formatis best-effort on Pi and could silently parse-fail on a deeply nested schema.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.state.jsonis 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