-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworkflow.ts
More file actions
357 lines (306 loc) · 13.6 KB
/
workflow.ts
File metadata and controls
357 lines (306 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/**
* maintain-agent-rules
*
* Continuous repo hygiene: audit and update agent instruction files against
* the actual state of the codebase.
*
* Scope (only these three file types are touched):
* - AGENTS.md (any directory)
* - AGENTS.md.override (any directory)
* - .claude/rules/*.md (any directory under .claude/rules/)
*
* How it works (DAG):
* 1. discover-scope — deterministic: list changed dirs + existing rule files
* 2. snapshot-rules — deterministic: cat all rule files into a single blob
* 3. audit-drift — analyst: produces .trail/rules-drift.md
* 4. verify-drift-exists — deterministic: fail fast if drift report is empty
* 5. team-lead — lead (interactive Claude): coordinates edits on channel
* 6. team-writer — writer (interactive Codex): applies per-file edits
* 7. verify-writes — deterministic: git diff --stat must be non-empty
* 8. commit-and-pr — deterministic: branch, commit, push, gh pr create
* 9. pr-self-review — reviewer: reads the PR diff, flags over-reach
*
* Inputs (via env vars):
* TARGET_REPO_DIR absolute path to the repo to audit (default: process.cwd())
* SINCE_REF git ref to diff against for "recently touched" (default: HEAD~50)
* DRY_RUN "true" to skip commit/PR (default: "false")
* BASE_BRANCH PR base branch (default: "main")
*
* Run locally:
* TARGET_REPO_DIR=/path/to/repo agent-relay run --dry-run workflow.ts
* TARGET_REPO_DIR=/path/to/repo agent-relay run workflow.ts
*
* Run via cloud (when AgentWorkforce/cloud is wired up):
* See ../../cookbooks/01-maintain-agent-rules/README.md
*/
import { workflow } from '@agent-relay/sdk/workflows'
import { ClaudeModels, CodexModels } from '@agent-relay/config'
const TARGET_REPO_DIR = process.env.TARGET_REPO_DIR ?? process.cwd()
const SINCE_REF = process.env.SINCE_REF ?? 'HEAD~50'
const DRY_RUN = process.env.DRY_RUN === 'true'
const BASE_BRANCH = process.env.BASE_BRANCH ?? 'main'
const BRANCH_NAME = `chore/maintain-agent-rules-${Date.now()}`
async function runWorkflow() {
const result = await workflow('maintain-agent-rules')
.description('Audit and update AGENTS.md, AGENTS.md.override, and .claude/rules/ against current code')
.pattern('dag')
.channel('wf-maintain-agent-rules')
.maxConcurrency(4)
.timeout(3_600_000)
.agent('analyst', {
cli: 'codex',
model: CodexModels.GPT_5_4,
preset: 'worker',
role: 'Drift analyst — reads rule files + current code, writes structured drift report',
retries: 2,
})
.agent('lead', {
cli: 'claude',
model: ClaudeModels.OPUS,
role: 'Rules editor lead — reviews drift, assigns edits on channel, verifies writer output',
retries: 1,
})
.agent('writer', {
cli: 'codex',
model: CodexModels.GPT_5_4,
role: 'Rules editor — applies one-file-at-a-time edits to AGENTS.md / override / .claude/rules files',
retries: 2,
})
.agent('reviewer', {
cli: 'claude',
model: ClaudeModels.SONNET,
preset: 'reviewer',
role: 'PR reviewer — flags hallucinated rules and over-reach',
retries: 1,
})
// ─── 1. Discover scope ────────────────────────────────────────────────
.step('discover-scope', {
type: 'deterministic',
command: `set -e
mkdir -p .trail
cd "${TARGET_REPO_DIR}"
echo "=== target: ${TARGET_REPO_DIR} ===" > .trail/rules-scope.txt
echo "=== since: ${SINCE_REF} ===" >> .trail/rules-scope.txt
echo "" >> .trail/rules-scope.txt
echo "=== touched directories ===" >> .trail/rules-scope.txt
git diff --name-only "${SINCE_REF}...HEAD" 2>/dev/null \
| xargs -n1 dirname 2>/dev/null \
| sort -u >> .trail/rules-scope.txt || true
echo "" >> .trail/rules-scope.txt
echo "=== existing AGENTS.md files ===" >> .trail/rules-scope.txt
find . -type f -name "AGENTS.md" -not -path "*/node_modules/*" -not -path "*/.git/*" >> .trail/rules-scope.txt || true
echo "" >> .trail/rules-scope.txt
echo "=== existing AGENTS.md.override files ===" >> .trail/rules-scope.txt
find . -type f -name "AGENTS.md.override" -not -path "*/node_modules/*" -not -path "*/.git/*" >> .trail/rules-scope.txt || true
echo "" >> .trail/rules-scope.txt
echo "=== existing .claude/rules/ files ===" >> .trail/rules-scope.txt
find . -type f -path "*/.claude/rules/*.md" -not -path "*/node_modules/*" -not -path "*/.git/*" >> .trail/rules-scope.txt || true
cat .trail/rules-scope.txt`,
captureOutput: true,
failOnError: true,
})
// ─── 2. Snapshot rule file contents ────────────────────────────────────
.step('snapshot-rules', {
type: 'deterministic',
dependsOn: ['discover-scope'],
command: `set -e
cd "${TARGET_REPO_DIR}"
{
echo "# Rule file snapshot"
echo ""
for f in $(find . -type f \\( -name "AGENTS.md" -o -name "AGENTS.md.override" -o -path "*/.claude/rules/*.md" \\) -not -path "*/node_modules/*" -not -path "*/.git/*"); do
echo "## FILE: $f"
echo ""
echo '-----BEGIN FILE-----'
cat "$f"
echo '-----END FILE-----'
echo ""
done
} > .trail/rules-snapshot.md
wc -l .trail/rules-snapshot.md`,
captureOutput: true,
failOnError: true,
})
// ─── 3. Audit drift ────────────────────────────────────────────────────
.step('audit-drift', {
agent: 'analyst',
dependsOn: ['snapshot-rules'],
task: `You are auditing agent instruction files against the current state of the code.
Inputs:
- Scope summary:
{{steps.discover-scope.output}}
- Rule file snapshot:
{{steps.snapshot-rules.output}}
Process each rule file listed in the snapshot and identify drift:
1. Does it reference files, directories, functions, or conventions that no longer exist?
2. Are there new patterns in the code (based on touched directories) that are NOT yet documented?
3. Are any rules contradicted by current code (e.g. rule says "NEVER use X" but code uses X)?
4. Are there broken internal links or stale build commands?
Write the drift report to .trail/rules-drift.md with this exact structure:
# Rules Drift Report
## Summary
- files_audited: N
- files_with_drift: N
- new_rules_to_add: N
## Drift per file
### <file path>
- **Status:** clean | drift | stale
- **Issues:** (bulleted, concrete — reference line numbers or rule headings)
- **Proposed edits:** (bulleted, imperative — "Replace X with Y", "Delete section Z", "Add rule about W")
## New rules to add
- **File:** <where it should go>
- **Rule:** (imperative — "Use X, never Y because Z")
- **Evidence:** (file path + brief quote from current code)
If nothing is drifting AND no new rules are needed, write exactly "NO_DRIFT_DETECTED" on its own line and nothing else.
IMPORTANT: Write the file to disk at ${TARGET_REPO_DIR}/.trail/rules-drift.md. Do not output to stdout.`,
verification: { type: 'file_exists', value: '.trail/rules-drift.md' },
})
// ─── 4. Verify drift report is meaningful ──────────────────────────────
.step('verify-drift-exists', {
type: 'deterministic',
dependsOn: ['audit-drift'],
command: `set -e
cd "${TARGET_REPO_DIR}"
if [ ! -s .trail/rules-drift.md ]; then
echo "drift report is empty"
exit 1
fi
if grep -q "^NO_DRIFT_DETECTED$" .trail/rules-drift.md; then
echo "NO_DRIFT"
exit 0
fi
echo "DRIFT_PRESENT"
wc -l .trail/rules-drift.md`,
captureOutput: true,
failOnError: true,
})
// ─── 5. Lead coordinates edits on channel ──────────────────────────────
.step('team-lead', {
agent: 'lead',
dependsOn: ['verify-drift-exists'],
task: `You are the lead on #wf-maintain-agent-rules. Your writer is named "writer".
If the prior step output contains "NO_DRIFT", post "NO_DRIFT_EXITING" to the channel and exit cleanly.
Otherwise:
1. Read ${TARGET_REPO_DIR}/.trail/rules-drift.md
2. For each file needing edits, post a clear assignment to the channel:
"writer: edit <absolute path>. Changes: <bulleted list from drift report>. Only edit this one file. Reply DONE when written."
3. After writer says DONE, read the file and verify the changes match the drift report.
4. If correct, post next assignment. If wrong, post corrections.
5. When all files are updated, post "ALL_RULES_UPDATED" and exit.
Constraints you must enforce on the writer:
- ONLY edit AGENTS.md, AGENTS.md.override, or .claude/rules/*.md
- Never delete files unless the drift report says to
- Never add speculative rules — only what the drift report prescribes
- Preserve existing structure and tone of each file
Work directory: ${TARGET_REPO_DIR}`,
})
.step('team-writer', {
agent: 'writer',
dependsOn: ['verify-drift-exists'],
task: `You are "writer" on #wf-maintain-agent-rules. The lead will assign rule file edits one at a time.
For each assignment:
1. Read the file at the absolute path given
2. Apply exactly the changes listed
3. Save the file to disk
4. Post "DONE: <path>" to the channel
5. Wait for the next assignment
Rules:
- ONLY edit the file the lead assigns
- ONLY edit AGENTS.md, AGENTS.md.override, or .claude/rules/*.md
- Never touch source code
- Preserve YAML/markdown frontmatter structure
- If you disagree with an edit, still apply it but post a WARNING after DONE
Exit when the lead posts "ALL_RULES_UPDATED" or "NO_DRIFT_EXITING".
Work directory: ${TARGET_REPO_DIR}`,
})
// ─── 6. Verify writes landed ───────────────────────────────────────────
.step('verify-writes', {
type: 'deterministic',
dependsOn: ['team-lead'],
command: `set -e
cd "${TARGET_REPO_DIR}"
modified=$(git diff --name-only -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md')
untracked=$(git ls-files --others --exclude-standard -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md')
changed=$({ printf '%s\\n' "$modified"; printf '%s\\n' "$untracked"; } | sed '/^$/d' | sort -u | wc -l | tr -d ' ')
echo "changed rule files: $changed"
git diff --stat -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md' || true
if [ -n "$untracked" ]; then
echo "=== new untracked rule files ==="
printf '%s\\n' "$untracked"
fi
if grep -q "^NO_DRIFT$" ../../.trail/rules-scope.txt 2>/dev/null; then
echo "no-drift path, exiting 0"
exit 0
fi
if [ "$changed" = "0" ]; then
if grep -q "NO_DRIFT_EXITING\\|NO_DRIFT_DETECTED" .trail/rules-drift.md 2>/dev/null; then
echo "no drift to apply — exiting clean"
exit 0
fi
echo "drift report had work but no files were changed"
exit 1
fi`,
captureOutput: true,
failOnError: true,
})
// ─── 7. Commit + PR (skipped on DRY_RUN) ───────────────────────────────
.step('commit-and-pr', {
type: 'deterministic',
dependsOn: ['verify-writes'],
command: `set -e
cd "${TARGET_REPO_DIR}"
if [ "${DRY_RUN ? 'true' : 'false'}" = "true" ]; then
echo "DRY_RUN — skipping commit and PR"
git diff --stat -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md' || true
exit 0
fi
modified=$(git diff --name-only -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md')
untracked=$(git ls-files --others --exclude-standard -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md')
changed=$({ printf '%s\\n' "$modified"; printf '%s\\n' "$untracked"; } | sed '/^$/d' | sort -u | wc -l | tr -d ' ')
if [ "$changed" = "0" ]; then
echo "nothing to commit"
exit 0
fi
git checkout -b "${BRANCH_NAME}"
{
git diff --name-only -z -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md'
git ls-files --others --exclude-standard -z -- '*AGENTS.md' '*AGENTS.md.override' '*/.claude/rules/*.md'
} | git add --pathspec-from-file=- --pathspec-file-nul
git commit -m "chore: maintain-agent-rules automated drift update
Generated by AgentWorkforce/workflows repeatable/maintain-agent-rules.
Drift report: .trail/rules-drift.md"
git push -u origin "${BRANCH_NAME}"
gh pr create \\
--base "${BASE_BRANCH}" \\
--head "${BRANCH_NAME}" \\
--title "chore: automated agent rules drift update" \\
--body-file .trail/rules-drift.md \\
> .trail/pr-url.txt
echo "PR: $(cat .trail/pr-url.txt)"`,
captureOutput: true,
failOnError: true,
})
// ─── 8. PR self-review ─────────────────────────────────────────────────
.step('pr-self-review', {
agent: 'reviewer',
dependsOn: ['commit-and-pr'],
task: `Review the automated rules-drift PR.
Commit diff (from the previous deterministic step):
{{steps.commit-and-pr.output}}
Drift report the changes were based on:
{{steps.audit-drift.output}}
Evaluate:
1. Do the edits match what the drift report prescribed? (no extra changes, nothing missed)
2. Are any added rules speculative or not grounded in real code?
3. Do any edits contradict still-valid rules elsewhere?
4. Was any source code accidentally touched? (it should not have been)
Output a short verdict: PASS or FAIL with bulleted reasons. One paragraph max.`,
})
.onError('fail-fast')
.run({ cwd: TARGET_REPO_DIR })
console.log('Workflow status:', result.status)
}
runWorkflow().catch((error) => {
console.error(error)
process.exit(1)
})