opencode-codeops 1.4.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/LICENSE +21 -0
  3. package/README.md +171 -0
  4. package/_shared/auto-design.md +129 -0
  5. package/_shared/layout-convention.md +198 -0
  6. package/_shared/quality-profile.md +134 -0
  7. package/_shared/recommendation-hardening.md +166 -0
  8. package/_shared/scope-expansion-control.md +176 -0
  9. package/_shared/spec-first-ordering.md +79 -0
  10. package/_shared/zero-ambiguity-gate.md +311 -0
  11. package/agent-templates/codebase-scout.md +17 -0
  12. package/agent-templates/concurrency-auditor.md +5 -0
  13. package/agent-templates/design-challenger.md +26 -0
  14. package/agent-templates/financial-integrity-auditor.md +5 -0
  15. package/agent-templates/perf-auditor.md +23 -0
  16. package/agent-templates/phase-reviewer.md +54 -0
  17. package/agent-templates/plan-task-executor-opus.md +46 -0
  18. package/agent-templates/plan-task-executor.md +43 -0
  19. package/agent-templates/preflight-auditor.md +45 -0
  20. package/agent-templates/security-auditor.md +42 -0
  21. package/agent-templates/semantics-reviewer.md +5 -0
  22. package/agent-templates/spec-test-author.md +29 -0
  23. package/agents/concurrency-auditor.md +15 -0
  24. package/agents/correctness-reviewer.md +66 -0
  25. package/agents/demanding-executor.md +58 -0
  26. package/agents/design-challenger.md +38 -0
  27. package/agents/executor.md +55 -0
  28. package/agents/explorer.md +29 -0
  29. package/agents/financial-integrity-auditor.md +15 -0
  30. package/agents/performance-auditor.md +35 -0
  31. package/agents/preflight-auditor.md +57 -0
  32. package/agents/security-auditor.md +54 -0
  33. package/agents/semantics-reviewer.md +15 -0
  34. package/agents/spec-test-author.md +41 -0
  35. package/bin/codeops-worktree +244 -0
  36. package/bin/index.mjs +106 -0
  37. package/bin/install-agents.mjs +453 -0
  38. package/bin/install-skills.mjs +466 -0
  39. package/bin/lib/opencode-install.mjs +185 -0
  40. package/install.sh +55 -0
  41. package/package.json +73 -0
  42. package/plugin/index.ts +181 -0
  43. package/references/domains/compiler-and-language.md +28 -0
  44. package/references/domains/data-and-migration.md +22 -0
  45. package/references/domains/distributed-and-concurrent.md +26 -0
  46. package/references/domains/financial-system.md +28 -0
  47. package/references/domains/selection.md +19 -0
  48. package/references/domains/web-application.md +23 -0
  49. package/schemas/codeops-config.schema.json +56 -0
  50. package/scripts/check-version.mjs +163 -0
  51. package/scripts/codeops-migrate.sh +355 -0
  52. package/scripts/codeops-roadmap-compact.sh +232 -0
  53. package/scripts/codeops-roadmap-sync.sh +275 -0
  54. package/scripts/codeops_outcomes.py +155 -0
  55. package/scripts/codeops_plan.py +239 -0
  56. package/scripts/codeops_plan_migrate.py +318 -0
  57. package/scripts/codeops_worktree_snapshot.py +99 -0
  58. package/scripts/install_agents.py +288 -0
  59. package/scripts/release.mjs +533 -0
  60. package/skills/analyze-project/SKILL.md +28 -0
  61. package/skills/clean-comments/SKILL.md +22 -0
  62. package/skills/exec-plan/SKILL.md +267 -0
  63. package/skills/exec-plan/commit-modes.md +113 -0
  64. package/skills/exec-plan/execution-protocol.md +471 -0
  65. package/skills/git-commit/SKILL.md +35 -0
  66. package/skills/github-issues/SKILL.md +38 -0
  67. package/skills/grill-me/SKILL.md +342 -0
  68. package/skills/make-plan/SKILL.md +282 -0
  69. package/skills/make-plan/quality-checklist.md +96 -0
  70. package/skills/make-plan/templates.md +535 -0
  71. package/skills/make-plan/zero-ambiguity-gate.md +19 -0
  72. package/skills/make-requirements/SKILL.md +268 -0
  73. package/skills/make-requirements/discovery-phases.md +255 -0
  74. package/skills/make-requirements/review-and-add.md +73 -0
  75. package/skills/make-requirements/templates.md +296 -0
  76. package/skills/make-requirements/zero-ambiguity-gate.md +18 -0
  77. package/skills/outcome-review/SKILL.md +34 -0
  78. package/skills/preflight/SKILL.md +310 -0
  79. package/skills/preflight/dimensions.md +181 -0
  80. package/skills/preflight/report-format.md +300 -0
  81. package/skills/retro-requirements/SKILL.md +218 -0
  82. package/skills/retro-requirements/confidence-classification.md +45 -0
  83. package/skills/retro-requirements/phases.md +609 -0
  84. package/skills/retro-requirements/triage-gate.md +135 -0
  85. package/skills/roadmap/SKILL.md +381 -0
  86. package/skills/roadmap/stage-hooks.md +80 -0
  87. package/skills/roadmap/template.md +200 -0
  88. package/skills/setup-codeops/SKILL.md +94 -0
  89. package/skills/setup-codeops/migration.md +106 -0
  90. package/skills/setup-codeops/scaffold.md +99 -0
  91. package/skills/setup-routing/SKILL.md +102 -0
  92. package/skills/setup-routing/routing.md +44 -0
  93. package/skills/techdocs/SKILL.md +199 -0
  94. package/skills/techdocs/authoring-and-update.md +178 -0
  95. package/skills/techdocs/templates.md +655 -0
  96. package/skills/techdocs/vitepress-setup.md +143 -0
  97. package/skills/upgrade-plan/SKILL.md +75 -0
  98. package/skills/upgrade-plan/content-quality-gate.md +35 -0
  99. package/skills/upgrade-plan/upgrade-checklists.md +107 -0
  100. package/standards/coding-standards-full.md +124 -0
  101. package/standards/coding-standards.md +64 -0
  102. package/standards/output-style.md +17 -0
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install, inspect, and remove the CodeOps subagents for OpenCode.
4
+ *
5
+ * OpenCode discovers subagents from the filesystem: `.opencode/agents/` in a
6
+ * project, or `~/.config/opencode/agents/` globally. This script copies the
7
+ * packaged agent definitions (one `.md` file per role) into one of those
8
+ * locations.
9
+ *
10
+ * The shipped agent files are generated from `agent-templates/` by
11
+ * `scripts/install_agents.py`, which can also apply per-role model and
12
+ * permission overrides from `codeops/codeops.json`. This installer copies the
13
+ * packaged defaults; running the Python generator afterward re-applies any
14
+ * routing overrides a project has configured.
15
+ *
16
+ * Ownership and atomic-replace rules match the skills installer: only files the
17
+ * marker records are replaced, so hand-authored agents survive, and a re-run
18
+ * upgrades in place.
19
+ *
20
+ * Usage:
21
+ * opencode-codeops install-agents [options] Install or upgrade (default)
22
+ * opencode-codeops agents-status [options] Show the installed version
23
+ * opencode-codeops agents-uninstall [options] Remove the managed agents
24
+ *
25
+ * @module install-agents
26
+ */
27
+
28
+ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs"
29
+ import { dirname, join, resolve } from "node:path"
30
+ import { fileURLToPath } from "node:url"
31
+
32
+ import {
33
+ MARKER_FILE,
34
+ atomicReplace,
35
+ cleanStaleArtifacts,
36
+ ensureDir,
37
+ entryExists,
38
+ isSymlink,
39
+ linkEntry,
40
+ readMarker,
41
+ resolveTarget,
42
+ writeMarkerFile,
43
+ } from "./lib/opencode-install.mjs"
44
+
45
+ // This file lives in <package root>/bin/, so the parent of its directory is the
46
+ // package root that contains agents/ and package.json.
47
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
48
+
49
+ export { MARKER_FILE, readMarker }
50
+
51
+ /**
52
+ * Reads the marker only when it is an agents marker.
53
+ *
54
+ * Both installers use the same marker file name, so a marker written by the
55
+ * skills installer must not be mistaken for one that lists agents. Treating it
56
+ * as absent is safe: it makes the install behave like a first run instead of
57
+ * silently skipping every packaged agent.
58
+ *
59
+ * @param targetDir - Agents directory that may hold a marker
60
+ * @returns The parsed agents marker, or `undefined`
61
+ */
62
+ function readAgentsMarker(targetDir) {
63
+ const marker = readMarker(targetDir)
64
+ return marker && Array.isArray(marker.agents) ? marker : undefined
65
+ }
66
+
67
+ /**
68
+ * Reads this package's own version, recorded in the install marker.
69
+ *
70
+ * @returns The package version, or `"0.0.0"` when package.json cannot be read
71
+ */
72
+ export function readPackageVersion() {
73
+ try {
74
+ const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"))
75
+ return typeof manifest.version === "string" ? manifest.version : "0.0.0"
76
+ } catch {
77
+ return "0.0.0"
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Resolves the directory that holds the packaged agent definitions.
83
+ *
84
+ * The `CODEOPS_PLUGIN_ROOT` environment variable overrides the default, which
85
+ * lets a checkout run the installer before the package is published.
86
+ *
87
+ * @param override - Optional explicit agents directory
88
+ * @returns Absolute path to the agents directory
89
+ */
90
+ export function resolveSourceDir(override) {
91
+ if (override) return resolve(override)
92
+
93
+ const envRoot = process.env.CODEOPS_PLUGIN_ROOT
94
+ if (envRoot && existsSync(join(envRoot, "agents"))) {
95
+ return join(envRoot, "agents")
96
+ }
97
+
98
+ return join(PACKAGE_ROOT, "agents")
99
+ }
100
+
101
+ /**
102
+ * Lists the agent definition files shipped in a directory.
103
+ *
104
+ * @param sourceDir - Directory to scan
105
+ * @returns Sorted file names ending in `.md`
106
+ */
107
+ export function listAgents(sourceDir) {
108
+ if (!existsSync(sourceDir)) return []
109
+
110
+ return readdirSync(sourceDir, { withFileTypes: true })
111
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
112
+ .map((entry) => entry.name)
113
+ .sort()
114
+ }
115
+
116
+ /**
117
+ * Writes the install marker that records ownership and the installed version.
118
+ *
119
+ * @param targetDir - Agents directory to mark
120
+ * @param details - Marker contents
121
+ * @param details.version - Installed package version
122
+ * @param details.agents - Agent file names the package owns
123
+ */
124
+ export function writeMarker(targetDir, { version, agents }) {
125
+ writeMarkerFile(targetDir, {
126
+ schema: 1,
127
+ source: "opencode-codeops",
128
+ version,
129
+ installedAt: new Date().toISOString(),
130
+ agents,
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Installs one agent file by replacing it atomically, or by linking to it when
136
+ * `link` is set (used by development checkouts).
137
+ *
138
+ * @param details - Install inputs
139
+ * @param details.sourceDir - Agents directory holding the packaged file
140
+ * @param details.targetDir - Agents directory to install into
141
+ * @param details.name - Agent file name
142
+ * @param details.dryRun - Report only, write nothing
143
+ * @param details.link - Symlink to the source instead of copying
144
+ * @returns Whether a previous file existed
145
+ */
146
+ function installOneAgent({ sourceDir, targetDir, name, dryRun, link }) {
147
+ const from = join(sourceDir, name)
148
+ const dest = join(targetDir, name)
149
+ const existed = entryExists(dest)
150
+
151
+ if (dryRun) return { existed }
152
+
153
+ if (link) {
154
+ ensureDir(targetDir)
155
+ linkEntry({ from, dest, type: "file" })
156
+ return { existed }
157
+ }
158
+
159
+ return atomicReplace({ from, targetDir, name, recursive: false })
160
+ }
161
+
162
+ /**
163
+ * Installs or upgrades every packaged agent into a target directory.
164
+ *
165
+ * Agent files named by an existing marker, plus every packaged file on a first
166
+ * run, are replaced. A same-named file the marker does not own is skipped
167
+ * unless `force` is set, so a hand-authored or routing-generated agent is never
168
+ * overwritten by accident.
169
+ *
170
+ * @param details - Install inputs
171
+ * @param details.sourceDir - Agents directory holding the packaged files
172
+ * @param details.targetDir - Agents directory to install into
173
+ * @param details.version - Version stored in the marker
174
+ * @param details.force - Replace same-named files the marker does not own
175
+ * @param details.dryRun - Report only, write nothing
176
+ * @param details.link - Symlink to the source instead of copying
177
+ * @returns Counts plus the agent names recorded in the marker
178
+ */
179
+ export function installAgents({
180
+ sourceDir,
181
+ targetDir,
182
+ version,
183
+ force = false,
184
+ dryRun = false,
185
+ link = false,
186
+ }) {
187
+ const names = listAgents(sourceDir)
188
+ const marker = readAgentsMarker(targetDir)
189
+ const managed = marker ? new Set(marker.agents ?? []) : null
190
+ const counts = { agents: names.length, installed: 0, replaced: 0, skipped: 0 }
191
+ const owned = []
192
+ const prefix = dryRun ? "[dry-run] would " : ""
193
+
194
+ if (!dryRun) {
195
+ ensureDir(targetDir)
196
+ cleanStaleArtifacts(targetDir)
197
+ }
198
+
199
+ for (const name of names) {
200
+ const dest = join(targetDir, name)
201
+
202
+ if (entryExists(dest) && managed && !managed.has(name) && !force) {
203
+ console.log(`conflict, skipped (not managed by opencode-codeops; use --force): ${dest}`)
204
+ counts.skipped += 1
205
+ continue
206
+ }
207
+
208
+ const { existed } = installOneAgent({ sourceDir, targetDir, name, dryRun, link })
209
+ owned.push(name)
210
+ if (existed) counts.replaced += 1
211
+ else counts.installed += 1
212
+ console.log(`${prefix}${existed ? "replace" : "install"}: ${dest}`)
213
+ }
214
+
215
+ if (!dryRun) writeMarker(targetDir, { version, agents: owned })
216
+
217
+ return { ...counts, owned }
218
+ }
219
+
220
+ /**
221
+ * Removes only the agent files recorded as owned by this package.
222
+ *
223
+ * @param details - Uninstall inputs
224
+ * @param details.targetDir - Agents directory to clean
225
+ * @param details.dryRun - Report only, write nothing
226
+ * @returns Removed file names plus the marker outcome, or a reason it refused
227
+ */
228
+ export function uninstallAgents({ targetDir, dryRun = false }) {
229
+ const marker = readAgentsMarker(targetDir)
230
+
231
+ if (!marker) {
232
+ return { removed: [], markerRemoved: false, error: "no opencode-codeops marker found" }
233
+ }
234
+
235
+ const removed = []
236
+
237
+ for (const name of marker.agents ?? []) {
238
+ const dest = join(targetDir, name)
239
+ if (!entryExists(dest)) continue
240
+
241
+ if (!dryRun) rmSync(dest, { force: true })
242
+ removed.push(name)
243
+ console.log(`${dryRun ? "would remove" : "removed"}: ${dest}`)
244
+ }
245
+
246
+ if (!dryRun) rmSync(join(targetDir, MARKER_FILE), { force: true })
247
+
248
+ return { removed, markerRemoved: true }
249
+ }
250
+
251
+ /** Prints command usage. */
252
+ function printUsage() {
253
+ console.log(`Install the CodeOps subagents so OpenCode can discover them.
254
+
255
+ Usage:
256
+ opencode-codeops install-agents [options] Install or upgrade (default)
257
+ opencode-codeops agents-status [options] Show the installed version
258
+ opencode-codeops agents-uninstall [options] Remove the managed agents
259
+
260
+ Options:
261
+ --global Use ~/.config/opencode/agents (default)
262
+ --project Use ./.opencode/agents
263
+ --target <dir> Use a custom agents directory
264
+ --source <dir> Override the packaged agents directory
265
+ --link Symlink to the source instead of copying (development)
266
+ --dry-run Show what would happen without writing files
267
+ --force Replace same-named files this package does not own
268
+ -h, --help Show this help`)
269
+ }
270
+
271
+ /**
272
+ * Parses argv into a subcommand and options.
273
+ *
274
+ * The subcommand is optional and defaults to `install`. The dispatcher passes
275
+ * `agents-status` and `agents-uninstall`; the short forms are accepted too.
276
+ *
277
+ * @param argv - Arguments after the executable
278
+ * @returns The command, parsed options, and an error message when invalid
279
+ */
280
+ export function parseArgs(argv) {
281
+ const options = {
282
+ project: false,
283
+ target: null,
284
+ source: null,
285
+ dryRun: false,
286
+ force: false,
287
+ link: false,
288
+ help: false,
289
+ }
290
+ let command = "install"
291
+ let index = 0
292
+
293
+ const first = argv[0]
294
+ if (first && !first.startsWith("-")) {
295
+ if (first === "install" || first === "install-agents") command = "install"
296
+ else if (first === "status" || first === "agents-status") command = "status"
297
+ else if (first === "uninstall" || first === "agents-uninstall") command = "uninstall"
298
+ else if (first === "help") command = "help"
299
+ else return { command, options, error: `unknown command '${first}'` }
300
+ index = 1
301
+ }
302
+
303
+ for (; index < argv.length; index += 1) {
304
+ const arg = argv[index]
305
+
306
+ if (arg === "--global") options.project = false
307
+ else if (arg === "--project") options.project = true
308
+ else if (arg === "--dry-run") options.dryRun = true
309
+ else if (arg === "--force") options.force = true
310
+ else if (arg === "--link") options.link = true
311
+ else if (arg === "--help" || arg === "-h") options.help = true
312
+ else if (arg === "--target" || arg === "--source") {
313
+ const value = argv[index + 1]
314
+ if (!value || value.startsWith("--")) {
315
+ return { command, options, error: `${arg} requires a directory argument` }
316
+ }
317
+ if (arg === "--target") options.target = value
318
+ else options.source = value
319
+ index += 1
320
+ } else {
321
+ return { command, options, error: `unknown argument '${arg}'` }
322
+ }
323
+ }
324
+
325
+ return { command, options }
326
+ }
327
+
328
+ /** Prints the installed version and health for one target directory. */
329
+ function printStatus({ targetDir, sourceVersion }) {
330
+ const marker = readAgentsMarker(targetDir)
331
+
332
+ if (marker) {
333
+ const owned = marker.agents ?? []
334
+ console.log(`agents: installed: v${marker.version} (${owned.length} agents)`)
335
+ if (sourceVersion && marker.version !== sourceVersion) {
336
+ console.log(`agents: update available: v${sourceVersion} — run install-agents to upgrade`)
337
+ }
338
+ const missing = owned.filter((name) => !entryExists(join(targetDir, name)))
339
+ if (missing.length > 0) {
340
+ console.log(`agents: warning: missing managed agents: ${missing.join(", ")}`)
341
+ }
342
+ return
343
+ }
344
+
345
+ console.log("agents: not installed")
346
+ }
347
+
348
+ /**
349
+ * Runs the installer CLI.
350
+ *
351
+ * @param argv - Arguments after the executable
352
+ * @param io - Injectable environment for tests
353
+ * @param io.cwd - Project root used for `--project`
354
+ * @param io.source - Override the packaged agents directory
355
+ * @param io.version - Override the version recorded in the marker
356
+ * @returns Process exit code
357
+ */
358
+ export function main(argv, io = {}) {
359
+ if (argv.length === 0) {
360
+ printUsage()
361
+ return 0
362
+ }
363
+
364
+ const { command, options, error } = parseArgs(argv)
365
+ if (error) {
366
+ console.error(`error: ${error}`)
367
+ return 2
368
+ }
369
+
370
+ if (options.help || command === "help") {
371
+ printUsage()
372
+ return 0
373
+ }
374
+
375
+ const cwd = io.cwd ?? process.cwd()
376
+ const version = io.version ?? readPackageVersion()
377
+ const sourceDir = io.source ?? resolveSourceDir(options.source)
378
+ const targetDir = resolveTarget(options, cwd, "agents")
379
+
380
+ if (!existsSync(sourceDir)) {
381
+ console.error(`error: agents directory not found: ${sourceDir}`)
382
+ return 1
383
+ }
384
+
385
+ console.log(`Source: ${sourceDir}`)
386
+ console.log(`Target: ${targetDir}`)
387
+
388
+ // Writing through a symlinked target would mutate whatever the link points
389
+ // at. Refuse unless the user explicitly overrides with --force. Status is
390
+ // read-only and always allowed.
391
+ if (command !== "status" && !options.force && isSymlink(targetDir)) {
392
+ console.error(
393
+ `error: target agents directory is a symlink to ${realpathSync(targetDir)}; ` +
394
+ "install/uninstall would write through it. Pass --force to proceed."
395
+ )
396
+ return 2
397
+ }
398
+
399
+ if (command === "status") {
400
+ printStatus({ targetDir, sourceVersion: version })
401
+ return 0
402
+ }
403
+
404
+ if (command === "uninstall") {
405
+ const result = uninstallAgents({ targetDir, dryRun: options.dryRun })
406
+ if (result.error) {
407
+ console.error(`error: ${result.error} at ${targetDir}`)
408
+ return 1
409
+ }
410
+ console.log(
411
+ `${options.dryRun ? "would remove" : "removed"} ${result.removed.length} agent(s); ` +
412
+ `marker ${options.dryRun ? "would be removed" : "removed"}`
413
+ )
414
+ return 0
415
+ }
416
+
417
+ const counts = installAgents({
418
+ sourceDir,
419
+ targetDir,
420
+ version,
421
+ force: options.force,
422
+ dryRun: options.dryRun,
423
+ link: options.link,
424
+ })
425
+ const mode = options.dryRun ? " (dry-run, nothing written)" : ""
426
+ console.log(
427
+ `Done${mode}: ${counts.agents} agents | ` +
428
+ `installed ${counts.installed}, replaced ${counts.replaced}, ${counts.skipped} skipped`
429
+ )
430
+ if (counts.skipped > 0) {
431
+ console.log("Re-run with --force to replace skipped files.")
432
+ }
433
+ return 0
434
+ }
435
+
436
+ /**
437
+ * True when this module is the process entry point.
438
+ *
439
+ * @returns True when this file is the entry point
440
+ */
441
+ function isMainModule() {
442
+ if (!process.argv[1]) return false
443
+
444
+ try {
445
+ return fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
446
+ } catch {
447
+ return false
448
+ }
449
+ }
450
+
451
+ if (isMainModule()) {
452
+ process.exitCode = main(process.argv.slice(2))
453
+ }