diff --git a/.archon/commands/maintainer-standup.md b/.archon/commands/maintainer-standup.md new file mode 100644 index 0000000000..2e549fb9a1 --- /dev/null +++ b/.archon/commands/maintainer-standup.md @@ -0,0 +1,161 @@ +--- +description: Synthesize the maintainer's morning standup brief from gathered git/PR/issue/state data +argument-hint: (no arguments — all context provided via upstream nodes) +--- + +# Maintainer Standup Synthesis + +You are producing a daily maintainer briefing for the Archon project. The user is the maintainer running this workflow. Your job is to read the gathered facts, cross-reference against the project's direction document and the maintainer's profile, and produce a prioritized brief plus state to persist for tomorrow's run. + +**Workflow ID**: $WORKFLOW_ID + +--- + +## Phase 1: LOAD INPUTS + +You have three sources of upstream context, all already gathered. Each is a JSON string that you should parse. + +### Git status (origin/dev movement since last run) + +``` +$git-status.output +``` + +Fields: `current_dev_sha`, `prior_dev_sha`, `current_branch`, `is_dirty`, `pull_status`, `new_commits`, `diff_stat`. + +### GitHub data (PRs, issues, review requests, recently closed) + +``` +$gh-data.output +``` + +Fields: `gh_handle`, `since_date`, `all_open_prs`, `review_requested`, `authored_by_me`, `issues_assigned`, `recent_unlabeled_issues`, `recently_closed_prs`, `recently_closed_issues`, `my_recent_commits`. + +### Local context (direction doc, maintainer profile, prior state, recent briefs) + +``` +$read-context.output +``` + +Fields: `direction` (markdown string), `profile` (markdown string), `prior_state` (object or null), `recent_briefs` (array of `{date, content}`). + +--- + +## Phase 2: ANALYZE + +### 2a. Detect first-run vs ongoing + +If `prior_state` is `null` and `recent_briefs` is empty, this is a **first run**. Skip "Since last run" comparisons; produce a baseline triage and state snapshot the next run can diff against. + +### 2b. Compare prior state to current reality (progress detection) + +When `prior_state` exists: + +- **Resolved since last run**: PRs in `prior_state.observed_prs` whose numbers do NOT appear in current `gh-data.output.all_open_prs` — they were closed or merged. Cross-reference against `gh-data.output.recently_closed_prs` to know whether they merged or were closed without merging. Same for issues. +- **Carry-over revisited**: each item in `prior_state.carry_over` — is it still open? Did its status change? If resolved, mention briefly under "Resolved since last run" and DROP from `next_state.carry_over`. If still pending, keep with original `first_seen` date (so age is preserved). +- **What you shipped**: `gh-data.output.my_recent_commits` lists the maintainer's commits since the last run. Summarize meaningfully — group by area, highlight notable ones. Don't just list shas. +- **New since last run**: PRs in current `all_open_prs` whose numbers are NOT in `prior_state.observed_prs` are new this run. Same for issues. + +### 2c. Read the direction doc and profile + +The `direction` markdown defines what Archon IS / IS NOT. The `profile` markdown describes the maintainer's role, scope, and current focus. Both inform the triage: + +- **Profile scope** drives breadth of coverage. `scope: everything` (main maintainer) means classify all open PRs, not just ones touching the maintainer's focus areas. +- **Direction clauses** drive the polite-decline classification. PRs adding multi-tenancy, hosted-service features, or anything contradicting the IS-NOT list go to P4 with a citation. +- **Profile focus areas** weight prioritization within P1-P3 — items aligned with current focus rank higher. + +### 2d. Triage all open PRs into P1-P4 + +For each PR in `all_open_prs`: + +- **P1 (Do today)**: ready-to-merge PRs awaiting your review (`reviewDecision: APPROVED` or null AND `mergeStateStatus: clean`), security fixes, items breaking dev, blockers for an in-flight release. **Note**: `mergeStateStatus` is the only CI/merge signal in the gathered payload (values: `clean`, `unstable`, `dirty`, `blocked`, `behind`, `unknown`). For ambiguous cases run `gh pr checks ` to verify CI before classifying as P1. +- **P2 (This week)**: in-flight PRs needing review or maintainer feedback, PRs with merge conflicts that can be unblocked, PRs from the maintainer's current focus areas that are progressing. +- **P3 (Whenever)**: low-urgency items, drafts you authored, exploratory PRs, items outside current focus that aren't time-sensitive. +- **P4 (Polite-decline candidates)**: PRs that conflict with `direction.md`. Each P4 entry MUST cite a specific clause (e.g., `direction.md §single-developer-tool`). + +You may use `gh pr view `, `gh pr diff `, or `gh pr checks ` to drill into PRs whose triage classification cannot be determined from the metadata alone. Be selective — drilling into all 60+ PRs is wasteful. Drill into 5-10 of the most ambiguous or interesting cases. + +### 2e. Triage issues + +Issues in `issues_assigned` and `recent_unlabeled_issues` follow the same P1-P4 classification. Use `gh issue view ` to drill into ambiguous ones. Recently-filed unlabeled issues are likely candidates for first-pass labeling. + +### 2f. Surface direction questions + +If any PR raises a "we don't have a stance on this" question that `direction.md` doesn't answer, surface it under **Direction questions raised**. These go into `next_state.direction_questions` so the maintainer can absorb them into `direction.md` over time. + +### 2g. Carry-over aging + +Items that have been in `prior_state.carry_over` for multiple runs (check `first_seen` dates) are higher priority — surface them prominently and consider escalating their P-level. + +--- + +## Phase 3: GENERATE OUTPUT + +Return a JSON object matching the workflow's `output_format` schema. Do not write any files yourself — the workflow's `persist` node handles disk writes from your structured response. + +### `brief_markdown` (string) + +A maintainer-ready markdown brief. Adapt sections — omit empty ones, add others if useful. Keep entries to one line each. The brief should be readable on a single screen. + +```markdown +# Maintainer Standup — YYYY-MM-DD + +## Since last run +- (Summary of new commits on dev with notable highlights, or "first run — baseline snapshot") +- (Mention pull_status if not 'pulled': dirty/not_on_dev/pull_failed) + +## What you shipped +- (One-line summary grouped by area, derived from `my_recent_commits`. Omit if empty.) + +## Resolved since last run +- **PR #N** — [title] — merged ✓ / closed +- **Issue #N** — [title] — closed +- (Omit section if nothing resolved.) + +## P1 — Do today +- **PR #N** — [title] ([+X/-Y]) — [why P1, e.g. "ready to merge, awaiting your review"] +- **Issue #N** — [title] — [why P1] + +## P2 — This week +- (Same format) + +## P3 — Whenever +- (Same format) + +## P4 — Polite-decline candidates +- **PR #N** — [title] by @[author] — Conflicts with `direction.md §[clause]`. [One-line reason.] + +## Direction questions raised +- (PR #N raises: should Archon support [Y]? Add a stance to direction.md.) +- (Or omit if none.) + +## Carry-over still pending +- **PR #N** — [title] — first seen YYYY-MM-DD ([N] runs ago) — [current status] +- (Omit section if nothing carried over.) +``` + +### `next_state` (object) + +Carry-over state for tomorrow's run. Schema: + +- `last_run_at`: current ISO-8601 timestamp (use the actual timestamp at synthesis time). +- `last_dev_sha`: value from `git-status.output.current_dev_sha`. +- `carry_over`: items the next run should remember as "still pending." For items already in `prior_state.carry_over` that are still pending, **preserve the original `first_seen` date** so age is tracked correctly. +- `observed_prs`: snapshot of ALL currently-open PRs (number + title only) — used to detect new PRs and resolved PRs next run. This must include every entry in `all_open_prs`, not just ones you classified. +- `observed_issues`: same for assigned + unlabeled issues. +- `direction_questions`: new direction questions surfaced this run (string array). + +### PHASE_3_CHECKPOINT + +- [ ] Every PR in `all_open_prs` is either classified into P1-P4 OR included in `observed_prs` (no PR silently dropped). +- [ ] All P4 entries cite a specific `direction.md §clause`. +- [ ] Carry-over items still pending have their original `first_seen` preserved. +- [ ] Resolved-since-last-run items are surfaced in the brief AND removed from `next_state.carry_over`. +- [ ] `next_state.last_dev_sha` is set from `git-status.output.current_dev_sha`. +- [ ] `next_state.observed_prs` includes ALL currently-open PRs. + +--- + +## Phase 4: REPORT + +Return the JSON object only. The workflow's `persist` node writes `brief_markdown` to `.archon/maintainer-standup/briefs/.md` and `next_state` to `.archon/maintainer-standup/state.json`. Do not write files yourself. diff --git a/.archon/maintainer-standup/README.md b/.archon/maintainer-standup/README.md new file mode 100644 index 0000000000..3395682999 --- /dev/null +++ b/.archon/maintainer-standup/README.md @@ -0,0 +1,53 @@ +# Maintainer Standup + +Daily morning briefing for Archon maintainers. Pulls latest `dev`, fetches all open PRs and assigned issues, classifies them **P1–P4** against `direction.md`, and surfaces progress versus the previous run (merged, closed, what you shipped). + +## Files in this folder + +| File | Committed? | Purpose | +|------|:---:|---------| +| `direction.md` | ✓ | Project north-star — what Archon IS / IS NOT. **Shared by all maintainers.** Drives PR triage and polite-decline classification. | +| `README.md` | ✓ | This file. | +| `profile.md.example` | ✓ | Template for new maintainers to copy. | +| `profile.md` | gitignored | Your personal config (gh handle, role, focus areas). | +| `state.json` | gitignored | Auto-written carry-over for the next run. | +| `briefs/YYYY-MM-DD.md` | gitignored | Daily prose briefs. Last 3 are read into the next run. | + +`direction.md` is committed because triage decisions should be consistent across maintainers and across runs. `profile.md`, `state.json`, and `briefs/` are personal — your focus, your daily notes, your reading material — so each maintainer manages their own. + +## Setup for a new maintainer + +1. Copy the template: + ```bash + cp .archon/maintainer-standup/profile.md.example .archon/maintainer-standup/profile.md + ``` +2. Edit `profile.md`: + - Set `gh_handle` to your GitHub login. + - Set `role` and `scope` to match your maintainer focus (`main_maintainer` / `everything` for full coverage; narrower for sub-maintainers). + - Optionally fill in **Currently focused on** — the synthesizer weights items toward what you list there. +3. Run it: + ```bash + archon workflow run maintainer-standup "" + ``` +4. The first run is a baseline (no prior state to diff). Subsequent runs compare against `state.json` and surface "Resolved since last run" / "What you shipped" / aged carry-over items. + +## How it works (engine view) + +1. **Three gather scripts** run in parallel (`bun`, no AI): + - `maintainer-standup-git-status.ts` — fetches `origin/dev`, fast-forwards if safe, captures new commits + diff stat since the last recorded SHA. + - `maintainer-standup-gh-data.ts` — pulls open PRs (full metadata), review-requested PRs, authored-by-me PRs, assigned issues, recently-filed unlabeled issues, and recently-closed PRs/issues since the last run. + - `maintainer-standup-read-context.ts` — reads `direction.md`, `profile.md`, `state.json`, and the last 3 briefs. +2. **Synthesis node** (`command: maintainer-standup`, Claude Sonnet, structured output) reads everything, optionally drills into specific PRs/issues with `gh pr view` / `gh issue view`, classifies P1–P4 against `direction.md`, and returns `{ brief_markdown, next_state }`. +3. **Persist node** writes `brief_markdown` to `briefs/YYYY-MM-DD.md` and `next_state` to `state.json`. + +The workflow runs **in the live checkout** (`worktree.enabled: false`) — it has to read this folder and pull `dev`. `--branch` and `--no-worktree` flags are rejected. + +## Editing direction.md + +`direction.md` is the source of truth for "what Archon is / isn't" during PR triage. Add a clause when a triage decision needs justification (so the next maintainer can reach the same conclusion). When declining a PR, cite the clause inline (e.g., `direction.md §single-developer-tool`). + +The synthesizer also surfaces **Direction questions raised** — PRs that touch areas where `direction.md` has no stance yet. Use those to evolve the doc deliberately rather than deciding case-by-case. + +## Customizing the brief format + +The output structure is defined in `.archon/commands/maintainer-standup.md`. Adjust the Phase 3 template if you want different sections or a different P-tier scheme. The synthesizer's `output_format` schema lives in `.archon/workflows/maintainer-standup.yaml`. diff --git a/.archon/maintainer-standup/direction.md b/.archon/maintainer-standup/direction.md new file mode 100644 index 0000000000..07cd83ab79 --- /dev/null +++ b/.archon/maintainer-standup/direction.md @@ -0,0 +1,41 @@ +# Archon Direction + +The maintainer-standup workflow consults this document when triaging PRs and issues to suggest which contributions align with the project and which are likely polite-decline candidates. + +This file is **committed and shared by all maintainers**. Edit deliberately — direction calls live here so that PR triage stays consistent across runs and across maintainers. When declining a PR, cite the specific clause (e.g., `direction.md §single-developer-tool`). + +--- + +## What Archon IS + +- **A remote agentic coding platform.** Control AI coding assistants (Claude Code SDK, Codex SDK, Pi community provider) remotely from Slack, Telegram, GitHub, Discord, CLI, and Web UI. +- **A single-developer tool.** No multi-tenant complexity. Built for one practitioner running their own instance. +- **Platform-agnostic at the conversation layer.** Unified interface across adapters via `IPlatformAdapter`. Stream/batch AI responses in real time. +- **Workflow-driven.** Reproducible AI execution chains defined as YAML DAGs in `.archon/workflows/`. Workflows run in isolated git worktrees by default. +- **Type-safe.** Strict TypeScript everywhere. No `any` without justification. +- **Composable.** Scripts in `.archon/scripts/`, commands in `.archon/commands/`, workflows compose them. +- **Self-hostable.** Bun + TypeScript runtime. SQLite by default; PostgreSQL optional. Zero external service dependencies for core operation. + +## What Archon is NOT + +- **Not multi-tenant.** No user accounts, role management, billing, or SaaS scaffolding. PRs adding these conflict with the single-developer thesis. +- **Not a hosted service.** No proprietary backend dependencies. Self-hosted by design. +- **Not a general-purpose chat UI.** Adapters are conversation surfaces for *workflow execution*, not standalone chat experiences. +- **Not a replacement for the AI coding agent itself.** Archon orchestrates Claude Code / Codex / Pi — it doesn't reimplement them. +- **Not opinionated about the dev environment.** No mandatory editor integrations, framework lock-in, or Docker requirement beyond what users opt into. +- **Not a workflow marketplace.** Bundled workflows are reference patterns; Archon is not aiming to be a hub for third-party workflow distribution. + +## Open questions (no stance yet) + +These are direction calls we haven't made. PRs that touch these areas should surface the question for explicit decision rather than be silently accepted or rejected. The workflow may add to this list as new questions appear. + +- (No open questions yet — populated over time.) + +--- + +## How to evolve this doc + +- Add a "What Archon IS" or "is NOT" line when a PR triage forces a direction call. +- Move "Open questions" entries to the IS / IS NOT sections once decided. +- Reference the relevant clause in PR comments when declining: `direction.md §single-developer-tool`. +- Keep entries short — one or two lines each. The point is fast lookup during triage, not a manifesto. diff --git a/.archon/maintainer-standup/profile.md.example b/.archon/maintainer-standup/profile.md.example new file mode 100644 index 0000000000..220f7a26c6 --- /dev/null +++ b/.archon/maintainer-standup/profile.md.example @@ -0,0 +1,28 @@ +--- +# Required: your GitHub login (used by gh queries for review-requested / assigned filters). +gh_handle: your-github-login + +# Suggested: drives how broadly the synthesizer classifies the queue. +# - main_maintainer / everything → triage all open PRs, not just yours +# - reviewer / focus-area → narrower coverage +role: main_maintainer +scope: everything +--- + +# Maintainer Profile — Your Name + +One paragraph on how you want the brief tuned. The synthesizer reads this verbatim, so write what you actually want it to do. + +Example: + +> I'm a sub-maintainer focused on the workflow engine. Show me PRs that touch packages/workflows/ first; deprioritize adapter-only PRs unless they're P1. + +## What I want from the brief + +- (Whatever level of full-repo coverage you want) +- (How aggressively to flag polite-decline candidates) +- (Whether to surface drafts, third-party PRs, etc.) + +## Currently focused on + +- (Update as priorities shift. Items here rank higher within their P-tier.) diff --git a/.archon/scripts/maintainer-standup-gh-data.ts b/.archon/scripts/maintainer-standup-gh-data.ts new file mode 100644 index 0000000000..eb0d03964b --- /dev/null +++ b/.archon/scripts/maintainer-standup-gh-data.ts @@ -0,0 +1,195 @@ +#!/usr/bin/env bun +/** + * Fetches GitHub data for the maintainer-standup synthesis: all open PRs + * (light metadata), review-requested PRs, authored-by-me PRs, assigned issues, + * recent unlabeled issues, and recently-closed PRs/issues since the last run. + * + * Reads gh_handle from .archon/maintainer-standup/profile.md frontmatter. + * + * Output: JSON to stdout. + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// execFileSync with argv arrays — avoids shell-string interpolation and the +// associated quoting hazards (esp. for handles loaded from profile.md). +function exec(file: string, args: string[]): string { + try { + return execFileSync(file, args, { stdio: ['ignore', 'pipe', 'pipe'] }).toString(); + } catch (e) { + process.stderr.write(`${file} command failed: ${file} ${args.join(' ')}\n${(e as Error).message}\n`); + return '[]'; + } +} + +function parseJson(s: string, fallback: T): T { + try { + return JSON.parse(s) as T; + } catch { + return fallback; + } +} + +// ── Load gh_handle from profile.md frontmatter ── +let ghHandle = ''; +const profilePath = resolve(process.cwd(), '.archon/maintainer-standup/profile.md'); +if (existsSync(profilePath)) { + const profile = readFileSync(profilePath, 'utf8'); + const match = profile.match(/^gh_handle:\s*(\S+)\s*$/m); + if (match) ghHandle = match[1]; +} +if (!ghHandle) { + process.stderr.write('Warning: no gh_handle found in profile.md frontmatter\n'); +} + +// ── Load prior state to scope "recently closed" lookups ── +let lastRunAt = ''; +const statePath = resolve(process.cwd(), '.archon/maintainer-standup/state.json'); +if (existsSync(statePath)) { + try { + const state = JSON.parse(readFileSync(statePath, 'utf8')) as { last_run_at?: string }; + lastRunAt = state.last_run_at ?? ''; + } catch { + // ignore corrupt state + } +} + +// ── Open PRs (full metadata for triage) ── +const prFields = [ + 'number', + 'title', + 'author', + 'labels', + 'createdAt', + 'updatedAt', + 'isDraft', + 'mergeable', + 'mergeStateStatus', + 'reviewDecision', + 'headRefName', + 'baseRefName', + 'additions', + 'deletions', + 'changedFiles', + 'reviewRequests', +].join(','); + +// `gh pr list --json` does NOT auto-paginate beyond `--limit`. 1000 is the +// practical ceiling for a single GraphQL call and gives ~15× headroom over +// today's open-PR count. The next-run-diff invariant in the synthesis +// command (observed_prs must include every entry in all_open_prs) requires +// completeness here, so we warn loudly if we ever hit the cap. +const PR_LIMIT = 1000; +const allOpenPrs = parseJson( + exec('gh', ['pr', 'list', '--state', 'open', '--limit', String(PR_LIMIT), '--json', prFields]), + [], +); +if (allOpenPrs.length === PR_LIMIT) { + process.stderr.write( + `Warning: hit --limit ${PR_LIMIT} on all_open_prs. Some PRs may be silently truncated; ` + + `next-run "resolved since last run" detection will misclassify the dropped tail. ` + + `Switch to gh api graphql --paginate when this becomes a persistent issue.\n`, + ); +} + +let reviewRequested: unknown[] = []; +let authoredByMe: unknown[] = []; +let issuesAssigned: unknown[] = []; + +if (ghHandle) { + reviewRequested = parseJson( + exec('gh', [ + 'pr', 'list', + '--search', `is:open is:pr review-requested:${ghHandle}`, + '--json', 'number,title,author,createdAt,updatedAt', + ]), + [], + ); + authoredByMe = parseJson( + exec('gh', [ + 'pr', 'list', + '--author', ghHandle, + '--state', 'open', + '--json', 'number,title,createdAt,updatedAt,reviewDecision,mergeStateStatus', + ]), + [], + ); + issuesAssigned = parseJson( + exec('gh', [ + 'issue', 'list', + '--assignee', ghHandle, + '--state', 'open', + '--json', 'number,title,labels,createdAt,updatedAt,author', + ]), + [], + ); +} + +// ── Recent unlabeled issues (last 7 days) ── +const sevenDaysAgo = new Date(); +sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); +const sevenDaysAgoStr = sevenDaysAgo.toISOString().slice(0, 10); +const recentUnlabeledIssues = parseJson( + exec('gh', [ + 'issue', 'list', + '--state', 'open', + '--search', `no:label created:>${sevenDaysAgoStr}`, + '--json', 'number,title,createdAt,author', + '--limit', '30', + ]), + [], +); + +// ── Recently closed/merged since last run (or last 7 days as fallback) ── +const sinceDate = lastRunAt ? lastRunAt.slice(0, 10) : sevenDaysAgoStr; +const recentlyClosedPrs = parseJson( + exec('gh', [ + 'pr', 'list', + '--state', 'closed', + '--search', `closed:>${sinceDate}`, + '--json', 'number,title,author,closedAt,mergedAt,state', + '--limit', '50', + ]), + [], +); +const recentlyClosedIssues = parseJson( + exec('gh', [ + 'issue', 'list', + '--state', 'closed', + '--search', `closed:>${sinceDate}`, + '--json', 'number,title,author,closedAt,state', + '--limit', '50', + ]), + [], +); + +// ── Maintainer's recent commits on dev (what you shipped) ── +let myRecentCommits = ''; +if (ghHandle) { + const since = lastRunAt || '7 days ago'; + try { + myRecentCommits = execFileSync( + 'git', + ['log', 'origin/dev', `--since=${since}`, `--author=${ghHandle}`, '--no-decorate', '--format=%h %s'], + { stdio: ['ignore', 'pipe', 'pipe'] }, + ).toString(); + } catch { + myRecentCommits = ''; + } +} + +console.log( + JSON.stringify({ + gh_handle: ghHandle, + since_date: sinceDate, + all_open_prs: allOpenPrs, + review_requested: reviewRequested, + authored_by_me: authoredByMe, + issues_assigned: issuesAssigned, + recent_unlabeled_issues: recentUnlabeledIssues, + recently_closed_prs: recentlyClosedPrs, + recently_closed_issues: recentlyClosedIssues, + my_recent_commits: myRecentCommits, + }), +); diff --git a/.archon/scripts/maintainer-standup-git-status.ts b/.archon/scripts/maintainer-standup-git-status.ts new file mode 100644 index 0000000000..9076c0eb0a --- /dev/null +++ b/.archon/scripts/maintainer-standup-git-status.ts @@ -0,0 +1,80 @@ +#!/usr/bin/env bun +/** + * Fetches origin/dev, optionally fast-forwards local dev, and reports new + * commits + diff stat since the last run's recorded SHA. + * + * Output: JSON to stdout with shape: + * { + * current_dev_sha, prior_dev_sha, current_branch, is_dirty, + * pull_status: 'pulled' | 'fetch_only' | 'pull_failed' | 'not_on_dev' | 'dirty', + * new_commits, diff_stat + * } + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// execFileSync (argv array, no shell) — defense-in-depth for git invocations. +// All args are hardcoded literals or values from `git` output (SHAs); using +// execFileSync removes any need to reason about shell metacharacters. +function git(args: string[]): { stdout: string; ok: boolean } { + try { + const out = execFileSync('git', args, { stdio: ['ignore', 'pipe', 'pipe'] }).toString(); + return { stdout: out, ok: true }; + } catch { + return { stdout: '', ok: false }; + } +} + +let priorSha = ''; +const stateFile = resolve(process.cwd(), '.archon/maintainer-standup/state.json'); +if (existsSync(stateFile)) { + try { + const state = JSON.parse(readFileSync(stateFile, 'utf8')) as { last_dev_sha?: string }; + priorSha = state.last_dev_sha ?? ''; + } catch { + // ignore corrupt state — first-run-like behavior + } +} + +git(['fetch', 'origin', 'dev']); + +const currentBranch = git(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim(); +const isDirty = git(['status', '--porcelain']).stdout.trim().length > 0; + +let pullStatus: 'pulled' | 'fetch_only' | 'pull_failed' | 'not_on_dev' | 'dirty'; +if (currentBranch !== 'dev') { + pullStatus = 'not_on_dev'; +} else if (isDirty) { + pullStatus = 'dirty'; +} else { + const result = git(['pull', '--ff-only', 'origin', 'dev']); + pullStatus = result.ok ? 'pulled' : 'pull_failed'; +} + +const currentDevSha = git(['rev-parse', 'origin/dev']).stdout.trim(); + +let newCommits = ''; +let diffStat = ''; +if (priorSha && priorSha !== currentDevSha) { + // %h short SHA, %an author name, %s subject + const log = git(['log', `${priorSha}..origin/dev`, '--no-decorate', '--format=%h %an: %s']); + if (log.ok) { + newCommits = log.stdout; + diffStat = git(['diff', '--stat', `${priorSha}..origin/dev`]).stdout; + } else { + newCommits = '(prior SHA not found locally — full diff unavailable)'; + } +} + +console.log( + JSON.stringify({ + current_dev_sha: currentDevSha, + prior_dev_sha: priorSha, + current_branch: currentBranch, + is_dirty: isDirty, + pull_status: pullStatus, + new_commits: newCommits, + diff_stat: diffStat, + }), +); diff --git a/.archon/scripts/maintainer-standup-read-context.ts b/.archon/scripts/maintainer-standup-read-context.ts new file mode 100644 index 0000000000..02b8054701 --- /dev/null +++ b/.archon/scripts/maintainer-standup-read-context.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env bun +/** + * Loads local context for the maintainer-standup synthesis: direction.md + * (committed), profile.md (per-maintainer), prior state.json, and the most + * recent N briefs. + * + * Output: JSON to stdout. + */ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const RECENT_BRIEFS_LIMIT = 3; + +const baseDir = resolve(process.cwd(), '.archon/maintainer-standup'); + +const directionPath = resolve(baseDir, 'direction.md'); +const direction = existsSync(directionPath) ? readFileSync(directionPath, 'utf8') : ''; + +const profilePath = resolve(baseDir, 'profile.md'); +const profile = existsSync(profilePath) ? readFileSync(profilePath, 'utf8') : ''; + +const statePath = resolve(baseDir, 'state.json'); +let priorState: unknown = null; +if (existsSync(statePath)) { + try { + priorState = JSON.parse(readFileSync(statePath, 'utf8')); + } catch { + priorState = null; + } +} + +const briefsDir = resolve(baseDir, 'briefs'); +const recentBriefs: { date: string; content: string }[] = []; +if (existsSync(briefsDir)) { + const files = readdirSync(briefsDir) + .filter((f) => f.endsWith('.md')) + .sort() + .reverse() + .slice(0, RECENT_BRIEFS_LIMIT); + for (const f of files) { + recentBriefs.push({ + date: f.replace(/\.md$/, ''), + content: readFileSync(resolve(briefsDir, f), 'utf8'), + }); + } +} + +console.log( + JSON.stringify({ + direction, + profile, + prior_state: priorState, + recent_briefs: recentBriefs, + }), +); diff --git a/.archon/workflows/maintainer-standup.yaml b/.archon/workflows/maintainer-standup.yaml new file mode 100644 index 0000000000..9382ce0887 --- /dev/null +++ b/.archon/workflows/maintainer-standup.yaml @@ -0,0 +1,162 @@ +name: maintainer-standup +description: | + Use when: Maintainer wants their morning briefing — what changed on dev, + what's in the review queue, what to focus on today across PRs and issues. + Triggers: "morning standup", "maintainer standup", "what's new today", + "daily brief", "morning brief", "what should i work on today", + "start my day". + Does: Pulls latest dev, fetches all open PRs and assigned issues, cross- + references against direction.md to flag polite-decline candidates, + compares against prior run state to surface progress (merged, closed, + what you shipped), produces a prioritized P1-P4 brief. Saves dated + brief + state for next-run continuity. + NOT for: Fixing issues (use archon-fix-github-issue), reviewing a specific + PR (use archon-comprehensive-pr-review), repo-wide triage automation + (use repo-triage). + +provider: claude +model: sonnet + +worktree: + enabled: false # Live checkout — needs to git pull and read .archon/maintainer-standup/ + +nodes: + # ── Layer 0: gather facts in parallel ── + + - id: git-status + script: maintainer-standup-git-status + runtime: bun + timeout: 60000 + + - id: gh-data + script: maintainer-standup-gh-data + runtime: bun + timeout: 180000 + + - id: read-context + script: maintainer-standup-read-context + runtime: bun + timeout: 10000 + + # ── Layer 1: synthesize the brief ── + + - id: synthesize + command: maintainer-standup + depends_on: [git-status, gh-data, read-context] + output_format: + type: object + properties: + brief_markdown: + type: string + description: Human-readable maintainer brief in markdown, with P1-P4 sections. + next_state: + type: object + description: Carry-over state for tomorrow's run. + properties: + last_run_at: + type: string + description: ISO-8601 timestamp of this run. + last_dev_sha: + type: string + description: origin/dev SHA at the end of this run. + carry_over: + type: array + description: Items still pending from previous runs (or surfaced this run). + items: + type: object + properties: + kind: + type: string + enum: [pr, issue, task, direction_question] + id: + type: string + description: PR/issue number as string, or task identifier. + note: + type: string + description: Why this is being carried over. + first_seen: + type: string + description: ISO-8601 date when this item first appeared in carry_over (preserved across runs). + required: [kind, id, note, first_seen] + observed_prs: + type: array + description: Snapshot of ALL currently-open PRs, used to detect resolved/new PRs next run. + items: + type: object + properties: + number: + type: number + title: + type: string + required: [number, title] + observed_issues: + type: array + description: Snapshot of currently-tracked issues (assigned + recent unlabeled). + items: + type: object + properties: + number: + type: number + title: + type: string + required: [number, title] + direction_questions: + type: array + description: New "we don't have a stance on this" questions surfaced this run. + items: + type: string + required: [last_run_at, last_dev_sha, carry_over, observed_prs, observed_issues, direction_questions] + required: [brief_markdown, next_state] + + # ── Layer 2: persist state and dated brief ── + + - id: persist + depends_on: [synthesize] + runtime: bun + timeout: 15000 + 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; + + // Local YYYY-MM-DD (sv-SE locale gives ISO format in local time) so a + // late-night run doesn't write tomorrow's UTC date and confuse next-run + // recent_briefs lookups. + const date = new Date().toLocaleDateString('sv-SE'); + + try { + 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 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`, + })); + } catch (err) { + // Synthesis (Sonnet, ~5 min) is the expensive part. If persist fails + // (disk full, read-only fs, permission), dump the brief + state to + // stderr so the run isn't a total loss — they're recoverable from logs. + process.stderr.write(`PERSIST FAILED: ${err.message}\n`); + process.stderr.write('--- BEGIN brief_markdown (recoverable from logs) ---\n'); + process.stderr.write(data.brief_markdown + '\n'); + process.stderr.write('--- END brief_markdown ---\n'); + process.stderr.write('--- BEGIN next_state (recoverable from logs) ---\n'); + process.stderr.write(JSON.stringify(data.next_state, null, 2) + '\n'); + process.stderr.write('--- END next_state ---\n'); + process.exit(1); + } diff --git a/.gitignore b/.gitignore index 4b225843ea..1f8415a4f8 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,11 @@ e2e-screenshots/ # Cross-run workflow state (e.g. issue-triage memory) .archon/state/ +# Maintainer standup — per-maintainer state and briefs (direction.md is committed) +.archon/maintainer-standup/profile.md +.archon/maintainer-standup/state.json +.archon/maintainer-standup/briefs/ + # Agent artifacts (generated, local only) .agents/ .agents/rca-reports/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 152c4245dd..6e926f7bc0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,6 +17,7 @@ export default tseslint.config( 'worktrees/**', '.claude/worktrees/**', '.claude/skills/**', + '.archon/**', // User workflow/script/command content — not in any tsconfig project '**/*.generated.ts', // Auto-generated source files (content inlined via JSON.stringify) '**/*.js', '*.mjs',