opencode-agile-agent 1.0.4 → 1.2.1

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.
Files changed (70) hide show
  1. package/README.md +78 -14
  2. package/bin/cli.js +180 -254
  3. package/bin/sync-templates.js +1 -7
  4. package/bin/validate-templates.js +17 -19
  5. package/package.json +1 -1
  6. package/templates/.opencode/ARCHITECTURE.md +94 -64
  7. package/templates/.opencode/README.md +115 -63
  8. package/templates/.opencode/agents/archiver.md +45 -0
  9. package/templates/.opencode/agents/backend-specialist.md +43 -46
  10. package/templates/.opencode/agents/context-gatherer.md +26 -26
  11. package/templates/.opencode/agents/debugger.md +45 -45
  12. package/templates/.opencode/agents/developer.md +54 -45
  13. package/templates/.opencode/agents/devops-engineer.md +42 -45
  14. package/templates/.opencode/agents/feature-lead.md +81 -50
  15. package/templates/.opencode/agents/frontend-specialist.md +44 -46
  16. package/templates/.opencode/agents/performance-optimizer.md +45 -45
  17. package/templates/.opencode/agents/pr-reviewer.md +46 -45
  18. package/templates/.opencode/agents/project-planner.md +41 -45
  19. package/templates/.opencode/agents/retrospective-writer.md +48 -0
  20. package/templates/.opencode/agents/security-auditor.md +39 -45
  21. package/templates/.opencode/agents/system-analyst.md +43 -43
  22. package/templates/.opencode/agents/test-engineer.md +44 -44
  23. package/templates/.opencode/bun.lock +18 -0
  24. package/templates/.opencode/commands/archive.md +15 -0
  25. package/templates/.opencode/commands/assign-models.md +39 -0
  26. package/templates/.opencode/commands/brainstorm.md +5 -2
  27. package/templates/.opencode/commands/check-progress.md +21 -0
  28. package/templates/.opencode/commands/create.md +8 -3
  29. package/templates/.opencode/commands/plan.md +7 -2
  30. package/templates/.opencode/commands/reframe.md +17 -0
  31. package/templates/.opencode/commands/review.md +9 -3
  32. package/templates/.opencode/commands/rubber-duck.md +14 -0
  33. package/templates/.opencode/commands/status.md +3 -0
  34. package/templates/.opencode/commands/test.md +8 -3
  35. package/templates/.opencode/config.template.json +160 -20
  36. package/templates/.opencode/package-lock.json +115 -0
  37. package/templates/.opencode/package.json +6 -0
  38. package/templates/.opencode/plugins/session-artifacts.ts +611 -0
  39. package/templates/.opencode/skills/archive-writing/SKILL.md +36 -0
  40. package/templates/.opencode/skills/artifact-discipline/SKILL.md +30 -0
  41. package/templates/.opencode/skills/clarify-first/SKILL.md +34 -0
  42. package/templates/.opencode/skills/context-archive/SKILL.md +10 -26
  43. package/templates/.opencode/skills/context-gathering/SKILL.md +2 -0
  44. package/templates/.opencode/skills/intelligent-routing/SKILL.md +10 -2
  45. package/templates/.opencode/skills/plan-writing/SKILL.md +5 -5
  46. package/templates/.opencode/templates/brief.template.md +25 -0
  47. package/templates/.opencode/templates/notes.template.md +13 -0
  48. package/templates/.opencode/templates/review-summary.template.md +6 -0
  49. package/templates/.opencode/templates/session-summary.template.md +7 -0
  50. package/templates/.opencode/templates/spec.template.md +17 -0
  51. package/templates/.opencode/templates/status.template.yaml +14 -0
  52. package/templates/.opencode/templates/task.template.md +5 -0
  53. package/templates/opencode.json +12 -0
  54. package/templates/.opencode/agents/api-designer.md +0 -45
  55. package/templates/.opencode/agents/code-archaeologist.md +0 -45
  56. package/templates/.opencode/agents/database-architect.md +0 -45
  57. package/templates/.opencode/agents/documentation-writer.md +0 -45
  58. package/templates/.opencode/agents/explorer-agent.md +0 -55
  59. package/templates/.opencode/agents/game-developer.md +0 -45
  60. package/templates/.opencode/agents/mobile-developer.md +0 -45
  61. package/templates/.opencode/agents/orchestrator.md +0 -48
  62. package/templates/.opencode/agents/penetration-tester.md +0 -46
  63. package/templates/.opencode/agents/product-manager.md +0 -46
  64. package/templates/.opencode/agents/qa-automation-engineer.md +0 -46
  65. package/templates/.opencode/agents/seo-specialist.md +0 -45
  66. package/templates/.opencode/archive/README.md +0 -24
  67. package/templates/.opencode/commands/debug.md +0 -10
  68. package/templates/.opencode/skills/parallel-agents/SKILL.md +0 -38
  69. package/templates/.opencode/skills/redteam-validation/SKILL.md +0 -33
  70. package/templates/AGENTS.template.md +0 -300
@@ -0,0 +1,611 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { execFileSync } from "child_process"
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs"
4
+ import { dirname, join } from "path"
5
+ import YAML from "yaml"
6
+
7
+ type StatusState = "brainstorm" | "planning" | "implementation" | "verification" | "review" | "done" | "blocked"
8
+
9
+ type StatusDocument = {
10
+ feature: string
11
+ owner: string
12
+ handoff_to: string
13
+ stage: StatusState
14
+ status: StatusState
15
+ summary: string
16
+ next_step: string
17
+ updated_at: string
18
+ approved_scope: string[]
19
+ open_questions: string[]
20
+ risks: string[]
21
+ blockers: string[]
22
+ files_of_interest: string[]
23
+ changed_files: string[]
24
+ }
25
+
26
+ type PluginContext = {
27
+ directory: string
28
+ client: any
29
+ }
30
+
31
+ const FEATURE_ROOT = ["artifacts", "features"]
32
+ const ACTIVE_FEATURE_FILE = ["artifacts", "active-feature.txt"]
33
+ const ARCHIVE_ROOT = ["archive"]
34
+ const TEMPLATE_ROOT = ["templates"]
35
+
36
+ const DEFAULT_STATUS = (feature: string): StatusDocument => ({
37
+ feature,
38
+ owner: "feature-lead",
39
+ handoff_to: "context-gatherer",
40
+ stage: "brainstorm",
41
+ status: "brainstorm",
42
+ summary: "New feature conversation started.",
43
+ next_step: "Clarify the request and decide whether to promote it into planning.",
44
+ updated_at: new Date().toISOString(),
45
+ approved_scope: [],
46
+ open_questions: [],
47
+ risks: [],
48
+ blockers: [],
49
+ files_of_interest: [],
50
+ changed_files: [],
51
+ })
52
+
53
+ const slugify = (value: string) =>
54
+ value
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9]+/g, "-")
57
+ .replace(/^-+|-+$/g, "") || "active-feature"
58
+
59
+ const ensureDir = (path: string) => {
60
+ if (!existsSync(path)) mkdirSync(path, { recursive: true })
61
+ }
62
+
63
+ const readText = (path: string) => (existsSync(path) ? readFileSync(path, "utf8") : "")
64
+
65
+ const writeText = (path: string, content: string) => {
66
+ ensureDir(dirname(path))
67
+ writeFileSync(path, content)
68
+ }
69
+
70
+ const appendJsonLine = (path: string, record: Record<string, unknown>) => {
71
+ const current = readText(path)
72
+ writeText(path, `${current}${JSON.stringify(record)}\n`)
73
+ }
74
+
75
+ const templatePath = (ctx: PluginContext, file: string) => join(ctx.directory, ...TEMPLATE_ROOT, file)
76
+ const featureRoot = (ctx: PluginContext) => join(ctx.directory, ...FEATURE_ROOT)
77
+ const featurePath = (ctx: PluginContext, feature: string) => join(featureRoot(ctx), feature)
78
+ const archiveRoot = (ctx: PluginContext) => join(ctx.directory, ...ARCHIVE_ROOT)
79
+ const activeFeatureFile = (ctx: PluginContext) => join(ctx.directory, ...ACTIVE_FEATURE_FILE)
80
+ const statusPath = (ctx: PluginContext, feature: string) => join(featurePath(ctx, feature), "status.yaml")
81
+ const handoffPath = (ctx: PluginContext, feature: string) => join(featurePath(ctx, feature), "handoff", "latest.md")
82
+ const notesPath = (ctx: PluginContext, feature: string) => join(featurePath(ctx, feature), "notes.md")
83
+ const archiveSummaryPath = (ctx: PluginContext, feature: string) => join(archiveRoot(ctx), `${feature}.md`)
84
+
85
+ const resolveCurrentFeature = (ctx: PluginContext) => {
86
+ const current = readText(activeFeatureFile(ctx)).trim()
87
+ if (current) return current
88
+
89
+ const root = featureRoot(ctx)
90
+ if (!existsSync(root)) return ""
91
+ const dirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory())
92
+ return dirs[0]?.name ?? ""
93
+ }
94
+
95
+ const saveCurrentFeature = (ctx: PluginContext, feature: string) => {
96
+ writeText(activeFeatureFile(ctx), `${feature}\n`)
97
+ }
98
+
99
+ const ensureFeatureScaffold = (ctx: PluginContext, feature: string) => {
100
+ const root = featurePath(ctx, feature)
101
+ ensureDir(root)
102
+ ensureDir(join(root, "handoff"))
103
+ ensureDir(join(root, "sessions"))
104
+ ensureDir(join(root, "evidence"))
105
+
106
+ const templateFiles = [
107
+ ["brief.template.md", "brief.md"],
108
+ ["spec.template.md", "spec.md"],
109
+ ["task.template.md", "task.md"],
110
+ ["notes.template.md", "notes.md"],
111
+ ] as const
112
+
113
+ for (const [template, target] of templateFiles) {
114
+ const nextPath = join(root, target)
115
+ if (!existsSync(nextPath)) writeText(nextPath, readText(templatePath(ctx, template)))
116
+ }
117
+
118
+ if (!existsSync(statusPath(ctx, feature))) {
119
+ saveStatus(ctx, feature, DEFAULT_STATUS(feature))
120
+ }
121
+ }
122
+
123
+ const loadStatus = (ctx: PluginContext, feature: string): StatusDocument => {
124
+ ensureFeatureScaffold(ctx, feature)
125
+ const parsed = YAML.parse(readText(statusPath(ctx, feature))) as Partial<StatusDocument> | null
126
+ return {
127
+ ...DEFAULT_STATUS(feature),
128
+ ...(parsed ?? {}),
129
+ feature,
130
+ }
131
+ }
132
+
133
+ const saveStatus = (ctx: PluginContext, feature: string, status: StatusDocument) => {
134
+ writeText(statusPath(ctx, feature), YAML.stringify(status))
135
+ }
136
+
137
+ const unique = (values: string[]) => [...new Set(values.filter(Boolean))]
138
+
139
+ const sectionList = (items: string[]) => (items.length ? items.map((item) => `- ${item}`).join("\n") : "- None")
140
+
141
+ const buildHandoff = (status: StatusDocument, target: string) => {
142
+ const lines = [
143
+ "# Handoff",
144
+ "",
145
+ `- Feature: ${status.feature}`,
146
+ `- Stage: ${status.stage}`,
147
+ `- Requested owner: ${target}`,
148
+ `- Current owner: ${status.owner}`,
149
+ `- Summary: ${status.summary}`,
150
+ `- Next step: ${status.next_step}`,
151
+ "",
152
+ "## Approved Scope",
153
+ sectionList(status.approved_scope),
154
+ "",
155
+ "## Open Questions",
156
+ sectionList(status.open_questions),
157
+ "",
158
+ "## Risks",
159
+ sectionList(status.risks),
160
+ "",
161
+ "## Blockers",
162
+ sectionList(status.blockers),
163
+ "",
164
+ "## Files Of Interest",
165
+ sectionList(status.files_of_interest),
166
+ "",
167
+ "## Changed Files",
168
+ sectionList(status.changed_files),
169
+ ]
170
+ return lines.join("\n")
171
+ }
172
+
173
+ const refreshHandoff = (ctx: PluginContext, feature: string, target: string, status: StatusDocument) => {
174
+ writeText(handoffPath(ctx, feature), `${buildHandoff(status, target)}\n`)
175
+ }
176
+
177
+ const updateNotes = (ctx: PluginContext, feature: string, label: string, items: string[]) => {
178
+ if (!items.length) return
179
+ const path = notesPath(ctx, feature)
180
+ const current = readText(path)
181
+ const block = [``, `## ${label}`, ...items.map((item) => `- ${item}`)].join("\n")
182
+ writeText(path, `${current.trimEnd()}\n${block}\n`)
183
+ }
184
+
185
+ const fileContents = (ctx: PluginContext, feature: string, name: string) => {
186
+ const path = join(featurePath(ctx, feature), name)
187
+ return readText(path).trim()
188
+ }
189
+
190
+ const specAcceptanceCriteria = (ctx: PluginContext, feature: string) => {
191
+ const spec = fileContents(ctx, feature, "spec.md")
192
+ if (!spec) return "Acceptance criteria not written yet."
193
+ const match = spec.match(/## Acceptance Criteria\s*([\s\S]*?)(\n## |$)/)
194
+ return match?.[1]?.trim() || "Acceptance criteria not written yet."
195
+ }
196
+
197
+ const reviewPacket = (ctx: PluginContext, feature: string) => {
198
+ const status = loadStatus(ctx, feature)
199
+ const lines = [
200
+ "# Review Packet",
201
+ "",
202
+ `- Feature: ${status.feature}`,
203
+ `- Stage: ${status.stage}`,
204
+ `- Summary: ${status.summary}`,
205
+ `- Next step: ${status.next_step}`,
206
+ "",
207
+ "## Approved Scope",
208
+ sectionList(status.approved_scope),
209
+ "",
210
+ "## Acceptance Criteria",
211
+ specAcceptanceCriteria(ctx, feature),
212
+ "",
213
+ "## Risks",
214
+ sectionList(status.risks),
215
+ "",
216
+ "## Changed Files",
217
+ sectionList(status.changed_files),
218
+ ]
219
+ return lines.join("\n")
220
+ }
221
+
222
+ const archiveSummary = (ctx: PluginContext, feature: string, finalSummary: string) => {
223
+ const status = loadStatus(ctx, feature)
224
+ const lines = [
225
+ `# ${feature}`,
226
+ "",
227
+ `- Archived: ${new Date().toISOString()}`,
228
+ `- Final stage: ${status.stage}`,
229
+ `- Owner: ${status.owner}`,
230
+ `- Summary: ${status.summary}`,
231
+ "",
232
+ "## What Was Done",
233
+ finalSummary.trim(),
234
+ "",
235
+ "## Approved Scope",
236
+ sectionList(status.approved_scope),
237
+ "",
238
+ "## Changed Files",
239
+ sectionList(status.changed_files),
240
+ "",
241
+ "## Risks Or Follow-Up",
242
+ sectionList(unique([...status.risks, ...status.blockers, ...status.open_questions])),
243
+ ]
244
+ return lines.join("\n") + "\n"
245
+ }
246
+
247
+ const runGit = (ctx: PluginContext, args: string[]) => {
248
+ try {
249
+ return execFileSync("git", ["-C", ctx.directory, ...args], { encoding: "utf8" }).trim()
250
+ } catch {
251
+ return ""
252
+ }
253
+ }
254
+
255
+ const repoDelta = (ctx: PluginContext, feature: string) => {
256
+ const status = loadStatus(ctx, feature)
257
+ const porcelain = runGit(ctx, ["status", "--porcelain"])
258
+ const trackedArtifact = unique([...status.changed_files, ...status.files_of_interest])
259
+
260
+ const gitChanged = unique(
261
+ porcelain
262
+ .split(/\r?\n/)
263
+ .map((line) => line.trimEnd())
264
+ .filter(Boolean)
265
+ .map((line) => line.slice(3).replace(/\\/g, "/")),
266
+ )
267
+
268
+ const artifactOnly = trackedArtifact.filter((file) => !gitChanged.includes(file.replace(/\\/g, "/")))
269
+ const gitOnly = gitChanged.filter((file) => !trackedArtifact.includes(file))
270
+ const clean = artifactOnly.length === 0 && gitOnly.length === 0
271
+
272
+ const lines = [
273
+ "# Repo Delta",
274
+ "",
275
+ `- Feature: ${feature}`,
276
+ `- Clean match: ${clean ? "yes" : "no"}`,
277
+ "",
278
+ "## Artifact Files",
279
+ sectionList(trackedArtifact),
280
+ "",
281
+ "## Git Changed Files",
282
+ sectionList(gitChanged),
283
+ "",
284
+ "## Artifact Only",
285
+ sectionList(artifactOnly),
286
+ "",
287
+ "## Git Only",
288
+ sectionList(gitOnly),
289
+ ]
290
+
291
+ return lines.join("\n")
292
+ }
293
+
294
+ const archiveReadiness = (ctx: PluginContext, feature: string) => {
295
+ ensureFeatureScaffold(ctx, feature)
296
+ const status = loadStatus(ctx, feature)
297
+ const required = ["brief.md", "spec.md", "task.md", "notes.md", "status.yaml"]
298
+ const missing = required.filter((file) => !existsSync(join(featurePath(ctx, feature), file)))
299
+ const issues = [
300
+ ...(missing.length ? [`Missing files: ${missing.join(", ")}`] : []),
301
+ ...(status.status !== "done" ? [`status.yaml.status must be done, got ${status.status}`] : []),
302
+ ...(!status.summary ? ["summary is empty"] : []),
303
+ ...(!status.next_step ? ["next_step is empty"] : []),
304
+ ]
305
+ return issues.length ? `NOT READY\n${issues.map((issue) => `- ${issue}`).join("\n")}` : `READY\n- ${feature} can be archived.`
306
+ }
307
+
308
+ const createCurrentTool = (ctx: PluginContext) =>
309
+ tool({
310
+ description: "Read the current active feature artifact and return the live execution state.",
311
+ args: {
312
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
313
+ },
314
+ async execute(args) {
315
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
316
+ ensureFeatureScaffold(ctx, feature)
317
+ saveCurrentFeature(ctx, feature)
318
+ const status = loadStatus(ctx, feature)
319
+ return [
320
+ `Feature: ${status.feature}`,
321
+ `Stage: ${status.stage}`,
322
+ `Status: ${status.status}`,
323
+ `Owner: ${status.owner}`,
324
+ `Next: ${status.next_step}`,
325
+ `Summary: ${status.summary}`,
326
+ `Files of interest: ${status.files_of_interest.join(", ") || "none"}`,
327
+ `Changed files: ${status.changed_files.join(", ") || "none"}`,
328
+ ].join("\n")
329
+ },
330
+ })
331
+
332
+ const createHandoffTool = (ctx: PluginContext) =>
333
+ tool({
334
+ description: "Generate the canonical handoff packet for a target subagent.",
335
+ args: {
336
+ target: tool.schema.string().describe("Target subagent name."),
337
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
338
+ },
339
+ async execute(args) {
340
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
341
+ ensureFeatureScaffold(ctx, feature)
342
+ saveCurrentFeature(ctx, feature)
343
+ const status = loadStatus(ctx, feature)
344
+ refreshHandoff(ctx, feature, args.target, status)
345
+ appendJsonLine(join(featurePath(ctx, feature), "evidence", "handoffs.jsonl"), {
346
+ timestamp: new Date().toISOString(),
347
+ feature,
348
+ target: args.target,
349
+ stage: status.stage,
350
+ })
351
+ return readText(handoffPath(ctx, feature))
352
+ },
353
+ })
354
+
355
+ const createSectionTool = (ctx: PluginContext) =>
356
+ tool({
357
+ description: "Read one planning artifact section without loading the full feature bundle.",
358
+ args: {
359
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
360
+ section: tool.schema.enum(["brief", "spec", "task", "notes", "status"]).describe("Artifact section to read."),
361
+ },
362
+ async execute(args) {
363
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
364
+ ensureFeatureScaffold(ctx, feature)
365
+ const map: Record<string, string> = {
366
+ brief: "brief.md",
367
+ spec: "spec.md",
368
+ task: "task.md",
369
+ notes: "notes.md",
370
+ status: "status.yaml",
371
+ }
372
+ const content = fileContents(ctx, feature, map[args.section])
373
+ return content || `${args.section} is empty.`
374
+ },
375
+ })
376
+
377
+ const createChangedFilesTool = (ctx: PluginContext) =>
378
+ tool({
379
+ description: "Read the changed files tracked for the active feature.",
380
+ args: {
381
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
382
+ },
383
+ async execute(args) {
384
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
385
+ const status = loadStatus(ctx, feature)
386
+ return status.changed_files.length ? status.changed_files.join("\n") : "No changed files recorded yet."
387
+ },
388
+ })
389
+
390
+ const createAcceptanceCriteriaTool = (ctx: PluginContext) =>
391
+ tool({
392
+ description: "Read only the acceptance criteria from the active feature spec.",
393
+ args: {
394
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
395
+ },
396
+ async execute(args) {
397
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
398
+ ensureFeatureScaffold(ctx, feature)
399
+ return specAcceptanceCriteria(ctx, feature)
400
+ },
401
+ })
402
+
403
+ const createReviewPacketTool = (ctx: PluginContext) =>
404
+ tool({
405
+ description: "Read a review-focused packet with scope, risks, acceptance criteria, and changed files.",
406
+ args: {
407
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
408
+ },
409
+ async execute(args) {
410
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
411
+ ensureFeatureScaffold(ctx, feature)
412
+ return reviewPacket(ctx, feature)
413
+ },
414
+ })
415
+
416
+ const createRepoDeltaTool = (ctx: PluginContext) =>
417
+ tool({
418
+ description: "Compare the live artifact file lists against the actual git working tree changes.",
419
+ args: {
420
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
421
+ },
422
+ async execute(args) {
423
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
424
+ ensureFeatureScaffold(ctx, feature)
425
+ return repoDelta(ctx, feature)
426
+ },
427
+ })
428
+
429
+ const createUpdateTool = (ctx: PluginContext) =>
430
+ tool({
431
+ description: "Update the live artifact state, record checkpoints, and keep the latest handoff packet fresh.",
432
+ args: {
433
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
434
+ summary: tool.schema.string().optional().describe("Short live summary."),
435
+ next_step: tool.schema.string().optional().describe("Next best action."),
436
+ owner: tool.schema.string().optional().describe("Current owner."),
437
+ handoff_to: tool.schema.string().optional().describe("Next owner."),
438
+ stage: tool.schema.enum(["brainstorm", "planning", "implementation", "verification", "review", "done", "blocked"]).optional().describe("Stage transition."),
439
+ approved_scope: tool.schema.array(tool.schema.string()).optional().describe("Approved scope bullets."),
440
+ open_questions: tool.schema.array(tool.schema.string()).optional().describe("Open questions."),
441
+ risks: tool.schema.array(tool.schema.string()).optional().describe("Risks."),
442
+ blockers: tool.schema.array(tool.schema.string()).optional().describe("Blockers."),
443
+ files_of_interest: tool.schema.array(tool.schema.string()).optional().describe("Key files for the next agent."),
444
+ changed_files: tool.schema.array(tool.schema.string()).optional().describe("Files changed in the current stage."),
445
+ facts: tool.schema.array(tool.schema.string()).optional().describe("Facts to append into notes."),
446
+ decisions: tool.schema.array(tool.schema.string()).optional().describe("Decisions to append into notes."),
447
+ links: tool.schema.array(tool.schema.string()).optional().describe("Links to append into notes."),
448
+ },
449
+ async execute(args, toolCtx) {
450
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
451
+ ensureFeatureScaffold(ctx, feature)
452
+ saveCurrentFeature(ctx, feature)
453
+ const current = loadStatus(ctx, feature)
454
+ const next: StatusDocument = {
455
+ ...current,
456
+ summary: args.summary ?? current.summary,
457
+ next_step: args.next_step ?? current.next_step,
458
+ owner: args.owner ?? current.owner,
459
+ handoff_to: args.handoff_to ?? current.handoff_to,
460
+ stage: args.stage ?? current.stage,
461
+ status: args.stage ?? current.status,
462
+ updated_at: new Date().toISOString(),
463
+ approved_scope: args.approved_scope ? unique([...current.approved_scope, ...args.approved_scope]) : current.approved_scope,
464
+ open_questions: args.open_questions ? unique([...current.open_questions, ...args.open_questions]) : current.open_questions,
465
+ risks: args.risks ? unique([...current.risks, ...args.risks]) : current.risks,
466
+ blockers: args.blockers ? unique([...current.blockers, ...args.blockers]) : current.blockers,
467
+ files_of_interest: args.files_of_interest ? unique([...current.files_of_interest, ...args.files_of_interest]) : current.files_of_interest,
468
+ changed_files: args.changed_files ? unique([...current.changed_files, ...args.changed_files]) : current.changed_files,
469
+ }
470
+ saveStatus(ctx, feature, next)
471
+ updateNotes(ctx, feature, "Facts", args.facts ?? [])
472
+ updateNotes(ctx, feature, "Decisions", args.decisions ?? [])
473
+ updateNotes(ctx, feature, "Links", args.links ?? [])
474
+ refreshHandoff(ctx, feature, next.handoff_to || toolCtx.agent || "feature-lead", next)
475
+ appendJsonLine(join(featurePath(ctx, feature), "evidence", "checkpoints.jsonl"), {
476
+ timestamp: new Date().toISOString(),
477
+ feature,
478
+ stage: next.stage,
479
+ owner: next.owner,
480
+ next_step: next.next_step,
481
+ })
482
+ return `Updated ${feature} at stage ${next.stage}. Next: ${next.next_step}`
483
+ },
484
+ })
485
+
486
+ const createArchiveCheckTool = (ctx: PluginContext) =>
487
+ tool({
488
+ description: "Check whether the active feature is ready for /archive.",
489
+ args: {
490
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
491
+ },
492
+ async execute(args) {
493
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
494
+ return archiveReadiness(ctx, feature)
495
+ },
496
+ })
497
+
498
+ const createFinalizeTool = (ctx: PluginContext) =>
499
+ tool({
500
+ description: "Finalize the active feature artifact into archive after validation passes.",
501
+ args: {
502
+ feature: tool.schema.string().optional().describe("Feature slug. Uses the active feature when omitted."),
503
+ final_summary: tool.schema.string().describe("Final shipped summary for archive."),
504
+ },
505
+ async execute(args) {
506
+ const feature = slugify(args.feature || resolveCurrentFeature(ctx) || "active-feature")
507
+ ensureFeatureScaffold(ctx, feature)
508
+ const source = featurePath(ctx, feature)
509
+ const readiness = archiveReadiness(ctx, feature)
510
+ if (!String(readiness).startsWith("READY")) return readiness
511
+
512
+ ensureDir(archiveRoot(ctx))
513
+ const target = archiveSummaryPath(ctx, feature)
514
+ writeText(target, archiveSummary(ctx, feature, args.final_summary))
515
+ appendJsonLine(join(source, "evidence", "checkpoints.jsonl"), {
516
+ timestamp: new Date().toISOString(),
517
+ feature,
518
+ stage: "done",
519
+ archived: true,
520
+ })
521
+ return `Archived ${feature} to ${target}`
522
+ },
523
+ })
524
+
525
+ const createEventHandlers = (ctx: PluginContext) => ({
526
+ "session.created": async (event: any) => {
527
+ const feature = resolveCurrentFeature(ctx)
528
+ if (!feature) return
529
+ const sessionID = event?.sessionID || event?.session?.id || "unknown"
530
+ const sessionFile = join(featurePath(ctx, feature), "sessions", `${sessionID}.md`)
531
+ if (!existsSync(sessionFile)) {
532
+ writeText(sessionFile, `# Session ${sessionID}\n\n- Feature: ${feature}\n- Started: ${new Date().toISOString()}\n`)
533
+ }
534
+ },
535
+ "command.executed": async (event: any) => {
536
+ const feature = resolveCurrentFeature(ctx)
537
+ if (!feature) return
538
+ appendJsonLine(join(featurePath(ctx, feature), "evidence", "commands.jsonl"), {
539
+ timestamp: new Date().toISOString(),
540
+ feature,
541
+ command: event?.command ?? event?.name ?? "unknown",
542
+ })
543
+ },
544
+ "session.idle": async (event: any) => {
545
+ const feature = resolveCurrentFeature(ctx)
546
+ if (!feature) return
547
+ const sessionID = event?.sessionID || event?.session?.id || "unknown"
548
+ const sessionFile = join(featurePath(ctx, feature), "sessions", `${sessionID}.md`)
549
+ const current = readText(sessionFile)
550
+ writeText(sessionFile, `${current.trimEnd()}\n- Idle at: ${new Date().toISOString()}\n`)
551
+ },
552
+ })
553
+
554
+ const createToolHooks = (ctx: PluginContext) => ({
555
+ before: async (input: any) => {
556
+ const feature = resolveCurrentFeature(ctx)
557
+ if (!feature) return
558
+ appendJsonLine(join(featurePath(ctx, feature), "evidence", "tools.jsonl"), {
559
+ timestamp: new Date().toISOString(),
560
+ phase: "before",
561
+ tool: input?.tool ?? "unknown",
562
+ })
563
+ },
564
+ after: async (input: any, output: any) => {
565
+ const feature = resolveCurrentFeature(ctx)
566
+ if (!feature) return
567
+ const status = loadStatus(ctx, feature)
568
+ const args = output?.args ?? input?.args ?? {}
569
+ const changed = typeof args.filePath === "string"
570
+ ? [args.filePath]
571
+ : Array.isArray(args.filePaths)
572
+ ? args.filePaths.filter((value: unknown) => typeof value === "string")
573
+ : []
574
+ if (changed.length) {
575
+ status.changed_files = unique([...status.changed_files, ...changed])
576
+ status.files_of_interest = unique([...status.files_of_interest, ...changed])
577
+ status.updated_at = new Date().toISOString()
578
+ saveStatus(ctx, feature, status)
579
+ refreshHandoff(ctx, feature, status.handoff_to, status)
580
+ }
581
+ appendJsonLine(join(featurePath(ctx, feature), "evidence", "tools.jsonl"), {
582
+ timestamp: new Date().toISOString(),
583
+ phase: "after",
584
+ tool: input?.tool ?? "unknown",
585
+ changed_files: changed,
586
+ })
587
+ },
588
+ })
589
+
590
+ export default async ({ client, directory }: PluginContext) => {
591
+ const ctx: PluginContext = { client, directory }
592
+ const hooks = createToolHooks(ctx)
593
+
594
+ return {
595
+ tool: {
596
+ session_artifact_acceptance_criteria: createAcceptanceCriteriaTool(ctx),
597
+ session_artifact_changed_files: createChangedFilesTool(ctx),
598
+ session_artifact_current: createCurrentTool(ctx),
599
+ session_artifact_handoff: createHandoffTool(ctx),
600
+ session_artifact_repo_delta: createRepoDeltaTool(ctx),
601
+ session_artifact_review_packet: createReviewPacketTool(ctx),
602
+ session_artifact_section: createSectionTool(ctx),
603
+ session_artifact_update: createUpdateTool(ctx),
604
+ session_artifact_archive_check: createArchiveCheckTool(ctx),
605
+ session_artifact_finalize: createFinalizeTool(ctx),
606
+ },
607
+ event: createEventHandlers(ctx),
608
+ "tool.execute.before": hooks.before,
609
+ "tool.execute.after": hooks.after,
610
+ }
611
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: archive-writing
3
+ description: Write compact archive summaries that explain what shipped, what changed, what was verified, and what follow-up remains.
4
+ ---
5
+
6
+ # Archive Writing
7
+
8
+ ## Philosophy
9
+ An archive entry should explain the outcome faster than reading the full feature bundle. Preserve signal, not process exhaust.
10
+
11
+ ## Use When
12
+ - A slice of work is approved and worth preserving.
13
+ - A feature is complete enough to summarize for future reference.
14
+ - You need a searchable outcome record instead of a draft bundle.
15
+
16
+ ## Core Moves
17
+ - Lead with what shipped.
18
+ - Name the changed surfaces explicitly.
19
+ - Include the checks that mattered.
20
+ - Leave unresolved follow-up visible.
21
+ - Keep the record short enough to scan in under a minute.
22
+
23
+ ## Recommended Shape
24
+ - What was done
25
+ - Changed files or surfaces
26
+ - Verification signal
27
+ - Risks or follow-up
28
+
29
+ ## Anti-Patterns
30
+ - Replaying the full implementation history.
31
+ - Copying planning docs into archive.
32
+ - Claiming certainty when verification was partial.
33
+ - Hiding open questions.
34
+
35
+ ## Remember
36
+ The archive is for retrieval later, not for proving how hard the work was.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: artifact-discipline
3
+ description: Keep runtime state in artifacts, retrieve documents through tools, and pass compact intent-only handoffs.
4
+ ---
5
+
6
+ # Artifact Discipline
7
+
8
+ ## Philosophy
9
+ Agents should pass intent, not document dumps. Runtime state belongs in artifacts, and artifacts should be retrieved through tools.
10
+
11
+ ## Use When
12
+ - Work spans more than one stage or more than one subagent.
13
+ - You need durable context through compaction or session breaks.
14
+ - You want deterministic handoff instead of full prompt replay.
15
+
16
+ ## Core Moves
17
+ - Start from `session_artifact_current` or `session_artifact_handoff`.
18
+ - Carry only goal, stage, constraints, and exit criteria in the prompt.
19
+ - Pull spec, notes, and evidence on demand.
20
+ - Write back structured updates instead of freeform memory.
21
+ - Refresh the handoff packet before delegating.
22
+
23
+ ## Anti-Patterns
24
+ - Re-explaining the full feature in every handoff.
25
+ - Treating prompt memory as the source of truth.
26
+ - Updating `status.yaml` manually when an artifact tool can do it.
27
+ - Mixing archived history with active execution state.
28
+
29
+ ## Remember
30
+ If a subagent can resume safely after compaction, the harness is doing its job.