agent-templates 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADOPTING.md +57 -0
- package/CLAUDE.md +125 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/package.json +32 -0
- package/patterns/three-agent-architect-builder-reviewer/README.md +153 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/architect.md +31 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/builder.md +25 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/reviewer.md +33 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/triage.md +31 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/breakdown-prd.md +18 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/build-ticket.md +12 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/nightly-issues.md +14 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/plan-ticket.md +10 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/review-ticket.md +14 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-milestone.md +15 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/verify-delivery.md +20 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/hooks/guard-main-session-writes.mjs +53 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/publish-tickets.mjs +263 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/settings.json +15 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/nightly-issues.js +139 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/run-milestone.js +223 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/INSTALL.md +65 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/claude-md-snippet.md +37 -0
- package/scripts/adopt.mjs +177 -0
- package/scripts/build-site.mjs +264 -0
- package/scripts/cli.mjs +52 -0
- package/templates/pattern-README.template.md +65 -0
- package/templates/ticket.template.md +104 -0
- package/templates/tracker/github/ISSUE_TEMPLATE/bug-report.md +28 -0
- package/templates/tracker/github/ISSUE_TEMPLATE/decision-record.md +25 -0
- package/templates/tracker/github/ISSUE_TEMPLATE/task.md +36 -0
- package/templates/tracker/github/PULL_REQUEST_TEMPLATE.md +54 -0
- package/templates/tracker/gitlab/issue_templates/bug-report.md +23 -0
- package/templates/tracker/gitlab/issue_templates/decision-record.md +20 -0
- package/templates/tracker/gitlab/issue_templates/task.md +31 -0
- package/templates/tracker/gitlab/merge_request_templates/default.md +55 -0
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/build-ticket.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the Builder stage on a planned ticket (three-agent pattern)
|
|
3
|
+
argument-hint: <ticket-id>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Launch the **builder** subagent for ticket $ARGUMENTS, pointing it at the ticket file and its plan at `docs/plans/$ARGUMENTS.md` (adjust the path if the plan lives elsewhere).
|
|
7
|
+
|
|
8
|
+
Refuse to start if the plan file does not exist — `/plan-ticket` runs first.
|
|
9
|
+
|
|
10
|
+
When the builder returns, show its diff summary, actual test output, and Deviations note, then STOP. Do not merge; clearance requires `/review-ticket` in a fresh context.
|
|
11
|
+
|
|
12
|
+
Hard rule: this stage runs in the **builder** subagent, never inline in this session — no matter how small the change looks. If the subagent cannot be launched or fails, report that and stop — do not absorb its role.
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/nightly-issues.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Nightly issue sweep — triage open issues, auto-fix the fixable ones through the three-agent pipeline, post the morning report (designed for headless `claude -p "/nightly-issues"`)
|
|
3
|
+
argument-hint: [max-issues]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Arguments: `$ARGUMENTS` — optional first argument caps how many issues to process tonight (default 5).
|
|
7
|
+
|
|
8
|
+
Execute in order:
|
|
9
|
+
|
|
10
|
+
1. **Collect.** List open issues with the platform CLI (`gh issue list --state open --json number,title,body,labels,createdAt,url` / glab equivalent). Exclude issues labeled `nightly:escalated`, `triage:invalid`, or `needs-human`, and any issue titled `Nightly report ...`. For each remaining issue set `isNew` = created within the last 24 hours. Compute `reportDate` = today's local date (YYYY-MM-DD).
|
|
11
|
+
2. **Launch the sweep.** Call the **Workflow** tool with `name: "nightly-issues"` and `args: { issues, maxIssues, defaultBranch, platform, reportDate }`. This command's instruction is your authorization to use the Workflow tool. The workflow triages (read-only), runs fixable issues through the run-milestone pipeline autonomously, and posts all tracker writes in one report step.
|
|
12
|
+
3. **Final output** (this is the headless run's stdout): the report issue URL, then one line per processed issue — `#N · <classification or pipeline status> · <one-line reason>` — plus how many eligible issues were left for the next night.
|
|
13
|
+
|
|
14
|
+
Operator notes: scheduling, permissions, and the morning-email mechanics are documented in the scaffold's INSTALL.md § Nightly sweep. You are an observer while the workflow runs — no stage work, no file edits, no tracker writes outside the workflow.
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/plan-ticket.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the Architect stage on a ticket (three-agent pattern)
|
|
3
|
+
argument-hint: <ticket-id or path to ticket file>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Launch the **architect** subagent on ticket: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
Pass it only the ticket reference — no prior conversation content. When it returns, show the plan path and its summary, then STOP. Implementation is the Builder's stage (`/build-ticket`), not this session's next step.
|
|
9
|
+
|
|
10
|
+
Hard rule: this stage runs in the **architect** subagent, never inline in this session. If the subagent cannot be launched or fails, report that and stop — do not absorb its role.
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/review-ticket.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the Reviewer stage on a built ticket (three-agent pattern) — requires a fresh context
|
|
3
|
+
argument-hint: <ticket-id> [branch-or-PR-ref]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Arguments: `$ARGUMENTS` — the first is the ticket id (TICKET below); the optional second is the diff reference (branch or PR; default: the ticket's `ticket/<id>` branch).
|
|
7
|
+
|
|
8
|
+
Contamination check first: if this session contains the Builder's implementation conversation for TICKET, STOP and tell the user to run `/review-ticket` from a fresh session. (A reviewer subagent launched from a session that only orchestrated — passing artifacts, never transcripts — is acceptable: Mode A in the scaffold's INSTALL.md.)
|
|
9
|
+
|
|
10
|
+
Launch the **reviewer** subagent for TICKET. Give it ONLY: the ticket path, the plan path (`docs/plans/<ticket-id>.md`), and the diff reference. Never include the Builder's transcript or self-assessment.
|
|
11
|
+
|
|
12
|
+
Relay the verdict verbatim — CLEAR, or BOUNCE with its numbered findings. On BOUNCE, findings go back to the Builder; after 2 bounce cycles, escalate to a human instead of looping.
|
|
13
|
+
|
|
14
|
+
Hard rule: this stage runs in the **reviewer** subagent, never inline in this session. If the subagent cannot be launched or fails, report that and stop — do not absorb its role. After a merge, `/verify-delivery` still runs — a CLEAR verdict does not close the ticket by itself.
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-milestone.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Gate 1 start signal — publish the module's tickets as tracker issues, then run the milestone pipeline (three-agent pattern)
|
|
3
|
+
argument-hint: <module dir, e.g. docs/prd/01-foo> [supervised|autonomous]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Arguments: `$ARGUMENTS` — the first is the module directory (MODULE below); the optional second overrides the repo's declared Operating mode (CLAUDE.md).
|
|
7
|
+
|
|
8
|
+
Typing this command **is** the human Gate 1 sign-off: the sub-PRD and tickets are final, and tracker issue **creation** is authorized by this sign-off. Execute in order:
|
|
9
|
+
|
|
10
|
+
1. **Verify Gate 1 inputs.** `MODULE/README.md` (the sub-PRD) exists and `MODULE/tickets/*.md` is non-empty; every ticket has the required frontmatter (see `templates/ticket.template.md`). Anything missing → STOP and list exactly what is missing. Do not fix it yourself — that is Architect-stage work.
|
|
11
|
+
2. **Publish tickets as tracker issues.** Run `node .claude/scripts/publish-tickets.mjs MODULE` (dry-run) and show the mapping. If the summary contains `error` entries → STOP and report them. Otherwise re-run with `--create` (idempotent — the `[<id>]` title prefix dedupes, so re-running is safe), and again STOP on any `error` entries.
|
|
12
|
+
3. **Launch the pipeline.** Parse the final `PUBLISH-SUMMARY-JSON` line into `tickets: [{id, path, issue}]`. **Filter out tickets whose issue is already closed** (check via `gh`/`glab` — closed means delivered by an earlier run; this filter is what makes re-runs after supervised pauses or crashes safe). Determine the Operating mode, then call the **Workflow** tool with `name: "run-milestone"` and `args: { tickets, mode, defaultBranch, platform }`. This command's instruction is your authorization to use the Workflow tool.
|
|
13
|
+
4. **Relay the final report verbatim** — per ticket: `delivered` · `awaiting-human-merge` · `escalated` (stage `review`, `reviewer-failed`, or `bounce-fix-build`) · `failed` · `delivery-incomplete`, plus `notStarted` when the run stopped early. Escalated and failed items go to the human; do not fix them inline (the write guard will hold you to that). In `supervised` mode the run stops after each CLEAR verdict — tell the human to merge, then re-run this command to continue.
|
|
14
|
+
|
|
15
|
+
While the workflow runs you are an observer. Do not do stage work in parallel, do not "help" a slow stage, do not edit files.
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/verify-delivery.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Post-merge Definition-of-Done check for a ticket (three-agent pattern) — verifies delivery instead of assuming it
|
|
3
|
+
argument-hint: <ticket-id>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Verify — do not assume — that ticket $ARGUMENTS is actually delivered. Check every item yourself this session and print a pass/fail table:
|
|
7
|
+
|
|
8
|
+
1. **Plan** exists at `docs/plans/$ARGUMENTS.md`.
|
|
9
|
+
2. **Reviewer verdict** is CLEAR, attached to the MR/PR.
|
|
10
|
+
3. **Tests green on the merged default branch** — run the suite yourself this session; never accept reported results.
|
|
11
|
+
4. **MR/PR merged** into the **default branch** (not a side branch).
|
|
12
|
+
5. **Tracker issue closed** — check via `glab issue view` / `gh issue view`. Auto-close via `Closes #N` fires only under specific conditions (GitLab: default-branch merge with the closing pattern in the MR description) — never trust it blindly; this exact gap has shipped before (MRs merged, issues left open).
|
|
13
|
+
6. **Writeback** — any Deviations noted by the Builder are reflected back into the ticket/sub-PRD.
|
|
14
|
+
|
|
15
|
+
Report every failed item plainly. If the MR is merged but the issue is still open, the repair is the exact close command (`glab issue close <N>` / `gh issue close <N>`) — behavior depends on the repo's declared operating mode (CLAUDE.md, "Operating mode"):
|
|
16
|
+
|
|
17
|
+
- `supervised`: show the command and run it only after explicit human OK.
|
|
18
|
+
- `autonomous`: run the repair, report what was repaired, and escalate to a human only if the repair fails or the gap is not mechanically repairable.
|
|
19
|
+
|
|
20
|
+
Never mark an item passed that you did not check this session.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Three-agent pattern guard: the ORCHESTRATOR (main session) never writes.
|
|
3
|
+
//
|
|
4
|
+
// PreToolUse hook on Edit|Write|MultiEdit|NotebookEdit (wired in .claude/settings.json).
|
|
5
|
+
// Per https://code.claude.com/docs/en/hooks.md (verified 2026-07-17): hooks fire for
|
|
6
|
+
// subagent tool calls too, and the input carries `agent_id`/`agent_type` ONLY when the
|
|
7
|
+
// call comes from a subagent — their absence identifies the main session.
|
|
8
|
+
//
|
|
9
|
+
// Behavior:
|
|
10
|
+
// - subagent call (architect writes the plan, builder writes code) -> no objection
|
|
11
|
+
// - main-session call -> deny, with the dispatch instruction fed back to the model
|
|
12
|
+
// - override switch for a human-approved out-of-pipeline edit: create the file
|
|
13
|
+
// .claude/allow-main-writes (and delete it afterwards to re-arm the guard)
|
|
14
|
+
//
|
|
15
|
+
// Only the main session is constrained here; each subagent is governed by its own
|
|
16
|
+
// tools/disallowedTools frontmatter (e.g. the reviewer has no Write/Edit at all).
|
|
17
|
+
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
22
|
+
|
|
23
|
+
let input = {};
|
|
24
|
+
try {
|
|
25
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
26
|
+
} catch {
|
|
27
|
+
process.exit(0); // unparsable input: stay out of the way rather than block blindly
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isSubagent = typeof input.agent_id === "string" && input.agent_id.length > 0;
|
|
31
|
+
const overrideSwitch = new URL("../allow-main-writes", import.meta.url);
|
|
32
|
+
|
|
33
|
+
if (isSubagent || existsSync(overrideSwitch)) {
|
|
34
|
+
process.exit(0); // no objection; normal permission flow applies
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const target =
|
|
38
|
+
input.tool_input?.file_path ?? input.tool_input?.notebook_path ?? "(unknown target)";
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
hookSpecificOutput: {
|
|
43
|
+
hookEventName: "PreToolUse",
|
|
44
|
+
permissionDecision: "deny",
|
|
45
|
+
permissionDecisionReason:
|
|
46
|
+
`Three-agent pattern: the main session orchestrates only and never writes ` +
|
|
47
|
+
`(blocked ${input.tool_name} on ${target}). Dispatch the work to its stage instead: ` +
|
|
48
|
+
`/plan-ticket (architect) or /build-ticket (builder). For a human-approved ` +
|
|
49
|
+
`out-of-pipeline edit, create .claude/allow-main-writes and retry — then delete it.`,
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
process.exit(0);
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/publish-tickets.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// publish-tickets.mjs — the ONLY sanctioned issue-creation path for the three-agent
|
|
3
|
+
// pattern. Agents never hand-create issues (fabrication risk); this script is
|
|
4
|
+
// deterministic and idempotent. Adapted from fx-eye-tracking scripts/create-issues.mjs
|
|
5
|
+
// (read 2026-07-17), extended with gh support and a machine-readable summary.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node .claude/scripts/publish-tickets.mjs <module-dir> [--create] [--platform gh|glab]
|
|
9
|
+
//
|
|
10
|
+
// <module-dir> e.g. docs/prd/01-foo — scans <module-dir>/tickets/*.md
|
|
11
|
+
// --create actually create issues (default: dry-run preview)
|
|
12
|
+
// --platform tracker CLI; default: autodetect from the origin remote host
|
|
13
|
+
//
|
|
14
|
+
// Mapping (one issue per ticket file):
|
|
15
|
+
// title = "[<id>] <title>" <- the [<id>] prefix is the idempotency key
|
|
16
|
+
// body = file content minus frontmatter (the ticket FILE stays the content source of truth)
|
|
17
|
+
// labels = module:<module>, size:<size>, agent:<agent> (each only if present)
|
|
18
|
+
//
|
|
19
|
+
// Idempotency: the existing-issue list is fetched ONCE per run (list endpoints are
|
|
20
|
+
// strongly consistent, unlike per-ticket search) and matched client-side by the
|
|
21
|
+
// "[<id>]" title prefix. In --create mode a failed fetch aborts BEFORE creating
|
|
22
|
+
// anything. Note: the gh path lists up to 1000 issues; beyond that, split modules.
|
|
23
|
+
//
|
|
24
|
+
// Last line of stdout is machine-readable for /start-milestone:
|
|
25
|
+
// PUBLISH-SUMMARY-JSON: [{"id","path","title","issue","error"?}]
|
|
26
|
+
// Exit codes: 0 = ok (invalid tickets are reported in the summary, not fatal);
|
|
27
|
+
// 1 = bad invocation, missing CLI in --create mode, fetch failure in
|
|
28
|
+
// --create mode, or any create failure (summary still printed).
|
|
29
|
+
|
|
30
|
+
import { execFileSync } from 'node:child_process'
|
|
31
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
32
|
+
import { join } from 'node:path'
|
|
33
|
+
|
|
34
|
+
const argv = process.argv.slice(2)
|
|
35
|
+
const CREATE = argv.includes('--create')
|
|
36
|
+
|
|
37
|
+
const platformIx = argv.indexOf('--platform')
|
|
38
|
+
let PLATFORM = ''
|
|
39
|
+
if (platformIx !== -1) {
|
|
40
|
+
PLATFORM = argv[platformIx + 1] || ''
|
|
41
|
+
if (!PLATFORM || PLATFORM.startsWith('--')) {
|
|
42
|
+
console.error('missing or invalid --platform value (expected gh or glab)')
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const moduleDir = argv.find((a, i) => !a.startsWith('--') && (platformIx === -1 || i !== platformIx + 1))
|
|
47
|
+
|
|
48
|
+
if (!moduleDir) {
|
|
49
|
+
console.error('usage: node publish-tickets.mjs <module-dir> [--create] [--platform gh|glab]')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
const ticketsDir = join(moduleDir, 'tickets')
|
|
53
|
+
let ticketsDirOk = false
|
|
54
|
+
try { ticketsDirOk = statSync(ticketsDir).isDirectory() } catch {}
|
|
55
|
+
if (!ticketsDirOk) {
|
|
56
|
+
console.error(`no tickets directory: ${ticketsDir}`)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const run = (bin, args, opts = {}) => execFileSync(bin, args, { encoding: 'utf8', ...opts })
|
|
61
|
+
|
|
62
|
+
// GH_BIN / GLAB_BIN env overrides (precedent: fx-eye-tracking's GLAB_BIN) for
|
|
63
|
+
// non-PATH binaries and test doubles. The value may include leading args, e.g.
|
|
64
|
+
// GH_BIN="node tools/fake-gh.mjs" (no spaces in the path itself).
|
|
65
|
+
const cli = (platform, args, opts = {}) => {
|
|
66
|
+
const raw = platform === 'gh' ? process.env.GH_BIN || 'gh' : process.env.GLAB_BIN || 'glab'
|
|
67
|
+
const parts = raw.split(' ')
|
|
68
|
+
return run(parts[0], [...parts.slice(1), ...args], opts)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let detectedFrom = ''
|
|
72
|
+
if (!PLATFORM) {
|
|
73
|
+
try {
|
|
74
|
+
const origin = run('git', ['remote', 'get-url', 'origin'], { stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
75
|
+
const host = (origin.match(/(?:@|:\/\/)([^/:]+)[/:]/) || [])[1] || ''
|
|
76
|
+
PLATFORM = /(^|\.)gitlab\./.test(host) || host.includes('gitlab') ? 'glab' : 'gh'
|
|
77
|
+
detectedFrom = host || origin
|
|
78
|
+
} catch {
|
|
79
|
+
PLATFORM = 'gh'
|
|
80
|
+
detectedFrom = 'no origin remote'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (PLATFORM !== 'gh' && PLATFORM !== 'glab') {
|
|
84
|
+
console.error(`unknown platform: ${PLATFORM} (expected gh or glab)`)
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
console.log(`platform: ${PLATFORM}${detectedFrom ? ` (autodetected from ${detectedFrom}; override with --platform)` : ''}`)
|
|
88
|
+
|
|
89
|
+
let cliOk = false
|
|
90
|
+
try {
|
|
91
|
+
cli(PLATFORM, ['auth', 'status'], { stdio: ['ignore', 'ignore', 'ignore'] })
|
|
92
|
+
cliOk = true
|
|
93
|
+
} catch {}
|
|
94
|
+
if (CREATE && !cliOk) {
|
|
95
|
+
console.error(`x ${PLATFORM} not found or not authenticated — install it and run \`${PLATFORM} auth login\`.`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
if (!cliOk) {
|
|
99
|
+
console.log(`(note) ${PLATFORM} unavailable — dry-run previews without checking which issues already exist.`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fetch the existing-issue list ONCE; match "[<id>]" prefixes client-side.
|
|
103
|
+
// Returns [{number, title}] or null when unavailable.
|
|
104
|
+
const fetchExistingIssues = () => {
|
|
105
|
+
if (!cliOk) return null
|
|
106
|
+
try {
|
|
107
|
+
if (PLATFORM === 'gh') {
|
|
108
|
+
const out = cli('gh', ['issue', 'list', '--state', 'all', '--limit', '1000', '--json', 'number,title'])
|
|
109
|
+
return JSON.parse(out).map((i) => ({ number: i.number, title: i.title }))
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const out = cli('glab', ['issue', 'list', '--all', '--output', 'json'])
|
|
113
|
+
return JSON.parse(out).map((i) => ({ number: i.iid ?? i.id, title: i.title }))
|
|
114
|
+
} catch {
|
|
115
|
+
// older glab without --output json: parse per LINE so the number always
|
|
116
|
+
// belongs to the line whose title matches (never "first #N in the blob")
|
|
117
|
+
const out = cli('glab', ['issue', 'list', '--all'])
|
|
118
|
+
return out
|
|
119
|
+
.split('\n')
|
|
120
|
+
.map((l) => l.match(/^#(\d+)\s+(.*)$/))
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.map((m) => ({ number: Number(m[1]), title: m[2] }))
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const existingIssues = fetchExistingIssues()
|
|
130
|
+
if (CREATE && existingIssues === null) {
|
|
131
|
+
console.error('x could not fetch the existing-issue list — refusing to create without a reliable existence check.')
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
// Returns an issue number, null (not found), or 'ambiguous' (mentions of "[<id>]"
|
|
135
|
+
// exist but none is a clean title prefix — creating would risk a duplicate, guessing
|
|
136
|
+
// would risk closing the wrong issue later, so the ticket is skipped with an error).
|
|
137
|
+
const findExisting = (id) => {
|
|
138
|
+
if (!existingIssues) return null
|
|
139
|
+
const marker = `[${id}]`
|
|
140
|
+
const hits = existingIssues.filter((i) => String(i.title).includes(marker))
|
|
141
|
+
const exact = hits.find((i) => String(i.title).trim().startsWith(marker))
|
|
142
|
+
if (exact) return exact.number
|
|
143
|
+
if (hits.length > 0) return 'ambiguous'
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const field = (fm, name) => {
|
|
148
|
+
const m = fm.match(new RegExp(`^${name}\\s*:\\s*(.+)$`, 'm'))
|
|
149
|
+
if (!m) return ''
|
|
150
|
+
// strip one pair of surrounding YAML quotes (and unescape \" inside them) so
|
|
151
|
+
// quoted titles don't leak quote characters into issue titles
|
|
152
|
+
const v = m[1].trim()
|
|
153
|
+
const stripped = v.replace(/^(['"])(.*)\1$/s, '$2')
|
|
154
|
+
return stripped === v ? v : stripped.replace(/\\"/g, '"')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const createIssue = (issueTitle, body, labels) => {
|
|
158
|
+
const attempt = (withLabels) => {
|
|
159
|
+
if (PLATFORM === 'gh') {
|
|
160
|
+
const args = ['issue', 'create', '--title', issueTitle, '--body-file', '-']
|
|
161
|
+
if (withLabels) for (const l of labels) args.push('--label', l)
|
|
162
|
+
return cli('gh', args, { input: body }).trim()
|
|
163
|
+
}
|
|
164
|
+
const args = ['issue', 'create', '--title', issueTitle, '--description', body]
|
|
165
|
+
if (withLabels && labels.length) args.push('--label', labels.join(','))
|
|
166
|
+
return cli('glab', args).trim()
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return attempt(true)
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (!labels.length) throw e
|
|
172
|
+
console.error(` (warn) create with labels failed — retrying without labels (create them in the tracker to keep labeling)`)
|
|
173
|
+
return attempt(false)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const summary = []
|
|
178
|
+
const seenIds = new Set()
|
|
179
|
+
let created = 0
|
|
180
|
+
let skipped = 0
|
|
181
|
+
let planned = 0
|
|
182
|
+
let invalid = 0
|
|
183
|
+
let createFailed = 0
|
|
184
|
+
|
|
185
|
+
for (const f of readdirSync(ticketsDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
186
|
+
const path = join(ticketsDir, f).replaceAll('\\', '/')
|
|
187
|
+
const text = readFileSync(path, 'utf8').replace(/^/, '') // strip BOM (PowerShell 5.1 utf8 writes one)
|
|
188
|
+
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
189
|
+
if (!fmMatch) {
|
|
190
|
+
console.log(` skip (no frontmatter): ${path}`)
|
|
191
|
+
summary.push({ id: null, path, title: null, issue: null, error: 'no-frontmatter' })
|
|
192
|
+
invalid++
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
const fm = fmMatch[1]
|
|
196
|
+
const id = field(fm, 'id')
|
|
197
|
+
const title = field(fm, 'title')
|
|
198
|
+
if (!id || !title) {
|
|
199
|
+
console.log(` skip (missing id/title): ${path}`)
|
|
200
|
+
summary.push({ id: id || null, path, title: null, issue: null, error: 'missing-id-title' })
|
|
201
|
+
invalid++
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
if (seenIds.has(id)) {
|
|
205
|
+
console.log(` skip (duplicate id ${id}): ${path}`)
|
|
206
|
+
summary.push({ id, path, title: null, issue: null, error: 'duplicate-id' })
|
|
207
|
+
invalid++
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
seenIds.add(id)
|
|
211
|
+
|
|
212
|
+
const issueTitle = `[${id}] ${title}`
|
|
213
|
+
const body = text.slice(fmMatch[0].length).trimStart()
|
|
214
|
+
const labels = [
|
|
215
|
+
field(fm, 'module') && `module:${field(fm, 'module')}`,
|
|
216
|
+
field(fm, 'size') && `size:${field(fm, 'size')}`,
|
|
217
|
+
field(fm, 'agent') && `agent:${field(fm, 'agent')}`,
|
|
218
|
+
].filter(Boolean)
|
|
219
|
+
|
|
220
|
+
const existing = findExisting(id)
|
|
221
|
+
if (existing === 'ambiguous') {
|
|
222
|
+
console.error(`x skip ${id}: issues mention "[${id}]" but none has it as a clean title prefix — resolve by hand`)
|
|
223
|
+
summary.push({ id, path, title: issueTitle, issue: null, error: 'ambiguous-existing' })
|
|
224
|
+
invalid++
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (existing) {
|
|
228
|
+
console.log(`= skip ${id}: issue #${existing} already exists`)
|
|
229
|
+
summary.push({ id, path, title: issueTitle, issue: existing })
|
|
230
|
+
skipped++
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!CREATE) {
|
|
235
|
+
console.log(`+ would create ${id}: "${issueTitle}" labels=[${labels.join(',')}]`)
|
|
236
|
+
summary.push({ id, path, title: issueTitle, issue: null })
|
|
237
|
+
planned++
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const out = createIssue(issueTitle, body, labels)
|
|
243
|
+
const lastLine = out.split('\n').filter(Boolean).pop() || ''
|
|
244
|
+
const num = (lastLine.match(/\/issues\/(\d+)\s*$/) || out.match(/#(\d+)/) || [])[1]
|
|
245
|
+
console.log(`+ created ${id}: ${lastLine}`)
|
|
246
|
+
summary.push({ id, path, title: issueTitle, issue: num ? Number(num) : null })
|
|
247
|
+
created++
|
|
248
|
+
} catch (e) {
|
|
249
|
+
const msg = String(e && e.message ? e.message : e).split('\n')[0]
|
|
250
|
+
console.error(`x create failed for ${id}: ${msg}`)
|
|
251
|
+
summary.push({ id, path, title: issueTitle, issue: null, error: `create-failed: ${msg}` })
|
|
252
|
+
createFailed++
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const invalidNote = invalid ? `, invalid: ${invalid}` : ''
|
|
257
|
+
console.log(
|
|
258
|
+
CREATE
|
|
259
|
+
? `CREATED: ${created}, already existed: ${skipped}, failed: ${createFailed}${invalidNote}.`
|
|
260
|
+
: `DRY-RUN: ${planned} would be created, ${skipped} already exist${invalidNote}. Re-run with --create after Gate 1 sign-off.`
|
|
261
|
+
)
|
|
262
|
+
console.log('PUBLISH-SUMMARY-JSON: ' + JSON.stringify(summary))
|
|
263
|
+
process.exit(createFailed ? 1 : 0)
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/nightly-issues.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'nightly-issues',
|
|
3
|
+
description: 'Nightly sweep: triage open issues, run fixable ones through the three-agent pipeline, post the morning report',
|
|
4
|
+
phases: [{ title: 'Triage' }, { title: 'Fix' }, { title: 'Report' }],
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Deterministic nightly orchestrator. The command (/nightly-issues) collects the
|
|
8
|
+
// issue list and date (Date is unavailable in workflow scripts) and passes:
|
|
9
|
+
//
|
|
10
|
+
// args:
|
|
11
|
+
// {
|
|
12
|
+
// issues: [{ number, title, body, labels: [..], isNew: bool, url }],
|
|
13
|
+
// maxIssues: 5, // optional cost cap per night
|
|
14
|
+
// defaultBranch: 'main', // optional
|
|
15
|
+
// platform: 'gh' | 'glab', // default 'gh'
|
|
16
|
+
// reportDate: 'YYYY-MM-DD', // required — stamped by the caller
|
|
17
|
+
// }
|
|
18
|
+
//
|
|
19
|
+
// Design: triage never touches the tracker; the pipeline (child run-milestone
|
|
20
|
+
// workflow) does the fixing with continueOnFailure=true (issues are independent,
|
|
21
|
+
// unlike milestone tickets); a single report step does ALL tracker writes at the
|
|
22
|
+
// end — comments, labels, and the "Nightly report <date>" issue the human reads
|
|
23
|
+
// over morning email (tracker notifications).
|
|
24
|
+
|
|
25
|
+
const cfg = Object.assign({ maxIssues: 5, defaultBranch: 'main', platform: 'gh' }, args)
|
|
26
|
+
if (!Array.isArray(cfg.issues)) throw new Error('args.issues must be an array (may be empty)')
|
|
27
|
+
if (typeof cfg.reportDate !== 'string' || !cfg.reportDate) throw new Error('args.reportDate is required (the caller stamps the date)')
|
|
28
|
+
if (!Number.isInteger(cfg.maxIssues) || cfg.maxIssues < 1) throw new Error('args.maxIssues must be an integer >= 1')
|
|
29
|
+
if (cfg.platform !== 'gh' && cfg.platform !== 'glab') throw new Error("args.platform must be 'gh' or 'glab'")
|
|
30
|
+
|
|
31
|
+
const SKIP_LABELS = ['nightly:escalated', 'triage:invalid', 'needs-human']
|
|
32
|
+
const eligible = cfg.issues.filter(function (i) {
|
|
33
|
+
return i && Number.isInteger(i.number) && !(i.labels || []).some(function (l) { return SKIP_LABELS.includes(l) })
|
|
34
|
+
})
|
|
35
|
+
const candidates = eligible.slice(0, cfg.maxIssues)
|
|
36
|
+
if (eligible.length > candidates.length) {
|
|
37
|
+
log('cap: processing ' + candidates.length + '/' + eligible.length + ' eligible issues (maxIssues=' + cfg.maxIssues + '); the rest wait for the next night')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TRIAGE = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
classification: { enum: ['fixable', 'invalid', 'needs-human'] },
|
|
44
|
+
reason: { type: 'string' },
|
|
45
|
+
ticketPath: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
required: ['classification', 'reason'],
|
|
48
|
+
}
|
|
49
|
+
const REPORT = {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { reportUrl: { type: 'string' }, notes: { type: 'string' } },
|
|
52
|
+
required: ['reportUrl'],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- Phase 1: triage (parallel, read-only + ticket synthesis) ----
|
|
56
|
+
const triaged = await parallel(candidates.map(function (issue) {
|
|
57
|
+
return function () {
|
|
58
|
+
return agent(
|
|
59
|
+
'Triage this tracker issue per your role definition.\n' +
|
|
60
|
+
'Issue #' + issue.number + ': ' + issue.title + '\n' +
|
|
61
|
+
'Labels: ' + JSON.stringify(issue.labels || []) + '\n' +
|
|
62
|
+
'Body:\n' + (issue.body || '(empty)') + '\n\n' +
|
|
63
|
+
'If fixable, the ticket file MUST be docs/prd/99-nightly/tickets/ISS-' + issue.number + '-<slug>.md with id ISS-' + issue.number + '.',
|
|
64
|
+
{ agentType: 'triage', label: 'triage:#' + issue.number, phase: 'Triage', schema: TRIAGE }
|
|
65
|
+
).then(function (t) {
|
|
66
|
+
return { issue: issue, triage: t || { classification: 'needs-human', reason: 'triage agent returned nothing' } }
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}))
|
|
70
|
+
const outcomes = triaged.filter(Boolean)
|
|
71
|
+
|
|
72
|
+
const fixable = outcomes.filter(function (o) { return o.triage.classification === 'fixable' && o.triage.ticketPath })
|
|
73
|
+
for (const o of outcomes) {
|
|
74
|
+
if (o.triage.classification === 'fixable' && !o.triage.ticketPath) {
|
|
75
|
+
o.triage.classification = 'needs-human'
|
|
76
|
+
o.triage.reason = 'classified fixable but returned no ticket path — ' + o.triage.reason
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
log('triage: ' + fixable.length + ' fixable, ' +
|
|
80
|
+
outcomes.filter(function (o) { return o.triage.classification === 'invalid' }).length + ' invalid, ' +
|
|
81
|
+
outcomes.filter(function (o) { return o.triage.classification === 'needs-human' }).length + ' needs-human')
|
|
82
|
+
|
|
83
|
+
// ---- Phase 2: fix via the milestone runner (child workflow, one nesting level) ----
|
|
84
|
+
let pipeline_results = []
|
|
85
|
+
if (fixable.length > 0) {
|
|
86
|
+
const tickets = fixable.map(function (o) {
|
|
87
|
+
return { id: 'ISS-' + o.issue.number, path: o.triage.ticketPath, issue: o.issue.number }
|
|
88
|
+
})
|
|
89
|
+
try {
|
|
90
|
+
const child = await workflow('run-milestone', {
|
|
91
|
+
tickets: tickets,
|
|
92
|
+
mode: 'autonomous',
|
|
93
|
+
defaultBranch: cfg.defaultBranch,
|
|
94
|
+
platform: cfg.platform,
|
|
95
|
+
continueOnFailure: true, // nightly issues are independent; one failure must not strand the rest
|
|
96
|
+
})
|
|
97
|
+
pipeline_results = (child && child.results) || []
|
|
98
|
+
} catch (e) {
|
|
99
|
+
log('pipeline failed to run: ' + (e && e.message ? e.message : String(e)))
|
|
100
|
+
pipeline_results = tickets.map(function (t) { return { id: t.id, status: 'failed', stage: 'pipeline', detail: 'milestone runner did not run' } })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const resultFor = function (issueNumber) {
|
|
104
|
+
return pipeline_results.find(function (r) { return r.id === 'ISS-' + issueNumber }) || null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- Phase 3: report (the ONLY tracker-writing step) ----
|
|
108
|
+
const digest = outcomes.map(function (o) {
|
|
109
|
+
const r = resultFor(o.issue.number)
|
|
110
|
+
return {
|
|
111
|
+
number: o.issue.number,
|
|
112
|
+
title: o.issue.title,
|
|
113
|
+
isNew: Boolean(o.issue.isNew),
|
|
114
|
+
classification: o.triage.classification,
|
|
115
|
+
reason: o.triage.reason,
|
|
116
|
+
pipeline: r ? { status: r.status, stage: r.stage || null, bounces: r.bounces || 0, detail: r.detail || null } : null,
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const report = await agent(
|
|
121
|
+
'You are the nightly report step — the only step allowed to write to the tracker (' + cfg.platform + ' CLI via Bash). ' +
|
|
122
|
+
'Process this digest of tonight\'s sweep (JSON): ' + JSON.stringify({ date: cfg.reportDate, digest: digest, notProcessed: eligible.length - candidates.length }) + '\n' +
|
|
123
|
+
'Do, in order:\n' +
|
|
124
|
+
'1. Per issue: post ONE comment stating the outcome — fixed (link the merge) / attempted-but-not-solved (why, findings) / needs-human (why) / invalid (why). Base every statement on the digest; fabricate nothing.\n' +
|
|
125
|
+
'2. Labels: add `triage:invalid` to invalid issues (do NOT close them — the human decides in the morning); add `nightly:escalated` to attempted-but-not-solved and pipeline-failed issues; add `needs-human` to needs-human issues.\n' +
|
|
126
|
+
'3. For issues whose pipeline status is `delivered`: verify the issue is actually closed; close it if the deliver step missed it.\n' +
|
|
127
|
+
'4. Create a tracker issue titled exactly "Nightly report ' + cfg.reportDate + '" (search first; if it already exists, comment on it instead of duplicating) with sections: New overnight · Fixed & closed · Attempted, not solved · Needs human · Invalid (ignore) · Not processed (cap). Use issue references (#N) so the tracker links them.\n' +
|
|
128
|
+
'Return reportUrl and notes (anything that went wrong).',
|
|
129
|
+
{ label: 'report:' + cfg.reportDate, phase: 'Report', schema: REPORT }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
date: cfg.reportDate,
|
|
134
|
+
processed: digest,
|
|
135
|
+
notProcessed: eligible.length - candidates.length,
|
|
136
|
+
fixed: digest.filter(function (d) { return d.pipeline && d.pipeline.status === 'delivered' }).length,
|
|
137
|
+
reportUrl: report ? report.reportUrl : null,
|
|
138
|
+
reportNotes: report ? report.notes : 'report step returned nothing',
|
|
139
|
+
}
|