opencode-codeops 2.4.0 → 2.5.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/CHANGELOG.md +6 -0
- package/README.md +10 -2
- package/_shared/discovery.md +2 -1
- package/package.json +1 -1
- package/scripts/__pycache__/codeops_plan.cpython-312.pyc +0 -0
- package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
- package/scripts/discovery-toggle.mjs +236 -0
- package/skills/analyze-project/SKILL.md +1 -1
- package/skills/setup-codeops/SKILL.md +30 -3
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -191,8 +191,16 @@ The shared header carries a real breadcrumb and a **System / Light / Dark** them
|
|
|
191
191
|
follows the operating-system setting; an explicit choice is remembered in the browser. If storage
|
|
192
192
|
is unavailable, the choice still applies for the current tab.
|
|
193
193
|
|
|
194
|
-
New projects created with `setup-codeops` start with Discovery enabled.
|
|
195
|
-
|
|
194
|
+
New projects created with `setup-codeops` start with Discovery enabled. Existing projects can
|
|
195
|
+
toggle it without re-scaffolding:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
/setup-codeops --enable-discovery
|
|
199
|
+
/setup-codeops --disable-discovery
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`analyze-project --with-discovery` remains available for the same config change. The setting lives
|
|
203
|
+
in `codeops/codeops.json`:
|
|
196
204
|
|
|
197
205
|
```json
|
|
198
206
|
{
|
package/_shared/discovery.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Discovery moves large question rounds out of the chat scroll and into a local
|
|
6
6
|
browser page. New projects created with `setup-codeops` enable it by default;
|
|
7
|
-
existing projects
|
|
7
|
+
existing projects toggle it with `setup-codeops --enable-discovery` or
|
|
8
|
+
`--disable-discovery`. It never replaces the Ambiguity Register. Use it only
|
|
8
9
|
when this file's conditions hold; otherwise ask in chat exactly as before.
|
|
9
10
|
|
|
10
11
|
## When it applies
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Enable or disable Discovery in an existing CodeOps project.
|
|
4
|
+
*
|
|
5
|
+
* The toggle is deliberately small and deterministic: it edits only
|
|
6
|
+
* `codeops/codeops.json` and, when the managed block exists, the Discovery
|
|
7
|
+
* line in `AGENTS.md`. It refuses symlinks and malformed configuration, and
|
|
8
|
+
* preserves every unrelated key.
|
|
9
|
+
*
|
|
10
|
+
* @module discovery-toggle
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs"
|
|
14
|
+
import { join, resolve } from "node:path"
|
|
15
|
+
import { fileURLToPath } from "node:url"
|
|
16
|
+
|
|
17
|
+
/** The managed AGENTS.md line for an enabled Discovery page. */
|
|
18
|
+
export const DISCOVERY_LINE =
|
|
19
|
+
"- Discovery: enabled — question rounds open in the local Discovery page"
|
|
20
|
+
|
|
21
|
+
const START_MARKER = "<!-- CODEOPS-PROJECT:START -->"
|
|
22
|
+
const END_MARKER = "<!-- CODEOPS-PROJECT:END -->"
|
|
23
|
+
|
|
24
|
+
/** A user-facing toggle failure. */
|
|
25
|
+
export class DiscoveryToggleError extends Error {}
|
|
26
|
+
|
|
27
|
+
/** True for a non-null, non-array object. */
|
|
28
|
+
function isPlainObject(value) {
|
|
29
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Refuse a symlink or a missing regular file. */
|
|
33
|
+
function assertRegularFile(path, label) {
|
|
34
|
+
if (!existsSync(path)) {
|
|
35
|
+
throw new DiscoveryToggleError(`${label} does not exist.`)
|
|
36
|
+
}
|
|
37
|
+
const stats = lstatSync(path)
|
|
38
|
+
if (stats.isSymbolicLink()) {
|
|
39
|
+
throw new DiscoveryToggleError(`${label} is a symlink; refusing to write through it.`)
|
|
40
|
+
}
|
|
41
|
+
if (!stats.isFile()) {
|
|
42
|
+
throw new DiscoveryToggleError(`${label} is not a regular file.`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Refuse a symlinked codeops directory. */
|
|
47
|
+
function assertDirectory(path, label) {
|
|
48
|
+
if (!existsSync(path)) {
|
|
49
|
+
throw new DiscoveryToggleError(`${label} does not exist; run setup-codeops first.`)
|
|
50
|
+
}
|
|
51
|
+
const stats = lstatSync(path)
|
|
52
|
+
if (stats.isSymbolicLink()) {
|
|
53
|
+
throw new DiscoveryToggleError(`${label} is a symlink; refusing to write through it.`)
|
|
54
|
+
}
|
|
55
|
+
if (!stats.isDirectory()) {
|
|
56
|
+
throw new DiscoveryToggleError(`${label} is not a directory.`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Normalize a config's discovery object without discarding known keys. */
|
|
61
|
+
function nextDiscovery(existing, enable) {
|
|
62
|
+
if (existing !== undefined && !isPlainObject(existing)) {
|
|
63
|
+
throw new DiscoveryToggleError("codeops/codeops.json has a malformed discovery object.")
|
|
64
|
+
}
|
|
65
|
+
const discovery = { ...(existing ?? {}) }
|
|
66
|
+
discovery.enabled = enable
|
|
67
|
+
if (enable && discovery.chatMaxQuestions === undefined) {
|
|
68
|
+
discovery.chatMaxQuestions = 3
|
|
69
|
+
}
|
|
70
|
+
if (
|
|
71
|
+
discovery.chatMaxQuestions !== undefined &&
|
|
72
|
+
(!Number.isInteger(discovery.chatMaxQuestions) || discovery.chatMaxQuestions < 0)
|
|
73
|
+
) {
|
|
74
|
+
throw new DiscoveryToggleError("discovery.chatMaxQuestions must be a non-negative integer.")
|
|
75
|
+
}
|
|
76
|
+
return discovery
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Add or remove the managed Discovery line inside an AGENTS.md managed block. */
|
|
80
|
+
function updateAgents(text, enable) {
|
|
81
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n"
|
|
82
|
+
const lines = text.split(/\r?\n/)
|
|
83
|
+
const starts = lines
|
|
84
|
+
.map((line, index) => (line.trim() === START_MARKER ? index : -1))
|
|
85
|
+
.filter((index) => index >= 0)
|
|
86
|
+
const ends = lines
|
|
87
|
+
.map((line, index) => (line.trim() === END_MARKER ? index : -1))
|
|
88
|
+
.filter((index) => index >= 0)
|
|
89
|
+
if (starts.length !== 1 || ends.length !== 1 || starts[0] > ends[0]) {
|
|
90
|
+
return { status: "block-missing", changed: false, text }
|
|
91
|
+
}
|
|
92
|
+
const start = starts[0]
|
|
93
|
+
const end = ends[0]
|
|
94
|
+
const block = lines.slice(start, end + 1)
|
|
95
|
+
const existing = block.findIndex((line) => line.trim() === DISCOVERY_LINE)
|
|
96
|
+
if (enable) {
|
|
97
|
+
if (existing >= 0) return { status: "already-enabled", changed: false, text }
|
|
98
|
+
const anchor = block.findIndex((line) => line.trim().startsWith("- Specialist coverage:"))
|
|
99
|
+
if (anchor === -1) {
|
|
100
|
+
return { status: "anchor-missing", changed: false, text }
|
|
101
|
+
}
|
|
102
|
+
block.splice(anchor + 1, 0, DISCOVERY_LINE)
|
|
103
|
+
} else {
|
|
104
|
+
if (existing === -1) return { status: "already-disabled", changed: false, text }
|
|
105
|
+
block.splice(existing, 1)
|
|
106
|
+
}
|
|
107
|
+
lines.splice(start, end - start + 1, ...block)
|
|
108
|
+
return { status: enable ? "enabled" : "disabled", changed: true, text: lines.join(eol) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Toggle Discovery for one project.
|
|
113
|
+
*
|
|
114
|
+
* @param {{ root?: string, enable: boolean, dryRun?: boolean }} options
|
|
115
|
+
* @returns {{ configPath: string, discovery: object, changed: boolean,
|
|
116
|
+
* agents: string, dryRun: boolean }}
|
|
117
|
+
*/
|
|
118
|
+
export function toggleDiscovery(options) {
|
|
119
|
+
if (typeof options.enable !== "boolean") {
|
|
120
|
+
throw new DiscoveryToggleError("enable must be true or false.")
|
|
121
|
+
}
|
|
122
|
+
const root = resolve(options.root ?? process.cwd())
|
|
123
|
+
const dryRun = options.dryRun === true
|
|
124
|
+
const codeopsDir = join(root, "codeops")
|
|
125
|
+
assertDirectory(codeopsDir, "codeops/")
|
|
126
|
+
const configPath = join(codeopsDir, "codeops.json")
|
|
127
|
+
assertRegularFile(configPath, "codeops/codeops.json")
|
|
128
|
+
|
|
129
|
+
const original = readFileSync(configPath, "utf8")
|
|
130
|
+
let config
|
|
131
|
+
try {
|
|
132
|
+
config = JSON.parse(original)
|
|
133
|
+
} catch {
|
|
134
|
+
throw new DiscoveryToggleError("codeops/codeops.json is not valid JSON.")
|
|
135
|
+
}
|
|
136
|
+
if (!isPlainObject(config)) {
|
|
137
|
+
throw new DiscoveryToggleError("codeops/codeops.json must contain a JSON object.")
|
|
138
|
+
}
|
|
139
|
+
const discovery = nextDiscovery(config.discovery, options.enable)
|
|
140
|
+
const nextConfig = { ...config, discovery }
|
|
141
|
+
const nextText = JSON.stringify(nextConfig, null, 2) + "\n"
|
|
142
|
+
const configChanged = nextText !== original
|
|
143
|
+
|
|
144
|
+
const agentsPath = join(root, "AGENTS.md")
|
|
145
|
+
let agents = "missing"
|
|
146
|
+
let agentsText = null
|
|
147
|
+
let agentsChanged = false
|
|
148
|
+
if (existsSync(agentsPath)) {
|
|
149
|
+
const stats = lstatSync(agentsPath)
|
|
150
|
+
if (stats.isSymbolicLink()) {
|
|
151
|
+
agents = "symlink"
|
|
152
|
+
} else if (!stats.isFile()) {
|
|
153
|
+
agents = "not-a-file"
|
|
154
|
+
} else {
|
|
155
|
+
const result = updateAgents(readFileSync(agentsPath, "utf8"), options.enable)
|
|
156
|
+
agents = result.status
|
|
157
|
+
agentsChanged = result.changed
|
|
158
|
+
agentsText = result.text
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!dryRun) {
|
|
163
|
+
if (configChanged) writeFileSync(configPath, nextText)
|
|
164
|
+
if (agentsChanged && agentsText !== null) writeFileSync(agentsPath, agentsText)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
configPath,
|
|
169
|
+
discovery,
|
|
170
|
+
changed: configChanged,
|
|
171
|
+
agents,
|
|
172
|
+
dryRun,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Print command usage. */
|
|
177
|
+
function printUsage() {
|
|
178
|
+
console.log(`Toggle Discovery in an existing CodeOps project.
|
|
179
|
+
|
|
180
|
+
Usage:
|
|
181
|
+
node scripts/discovery-toggle.mjs --root <repo> --enable-discovery [--dry-run]
|
|
182
|
+
node scripts/discovery-toggle.mjs --root <repo> --disable-discovery [--dry-run]
|
|
183
|
+
|
|
184
|
+
Options:
|
|
185
|
+
--root <dir> Repository root (defaults to the current directory)
|
|
186
|
+
--enable-discovery Set discovery.enabled to true
|
|
187
|
+
--disable-discovery Set discovery.enabled to false
|
|
188
|
+
--dry-run Show the result without writing files
|
|
189
|
+
-h, --help Show this help
|
|
190
|
+
`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Run the command line. */
|
|
194
|
+
function main() {
|
|
195
|
+
const args = process.argv.slice(2)
|
|
196
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
197
|
+
printUsage()
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
const rootIndex = args.indexOf("--root")
|
|
201
|
+
const root = rootIndex >= 0 ? args[rootIndex + 1] : process.cwd()
|
|
202
|
+
if (root === undefined) {
|
|
203
|
+
throw new DiscoveryToggleError("--root requires a directory.")
|
|
204
|
+
}
|
|
205
|
+
const enable = args.includes("--enable-discovery")
|
|
206
|
+
const disable = args.includes("--disable-discovery")
|
|
207
|
+
if (enable === disable) {
|
|
208
|
+
throw new DiscoveryToggleError("Use exactly one of --enable-discovery or --disable-discovery.")
|
|
209
|
+
}
|
|
210
|
+
const result = toggleDiscovery({
|
|
211
|
+
root,
|
|
212
|
+
enable,
|
|
213
|
+
dryRun: args.includes("--dry-run"),
|
|
214
|
+
})
|
|
215
|
+
const action = enable ? "enabled" : "disabled"
|
|
216
|
+
console.log(
|
|
217
|
+
`${result.dryRun ? "[dry-run] " : ""}Discovery ${action} in ${result.configPath} ` +
|
|
218
|
+
`(chatMaxQuestions: ${result.discovery.chatMaxQuestions ?? "default 3"}).`,
|
|
219
|
+
)
|
|
220
|
+
console.log(`AGENTS.md: ${result.agents}.`)
|
|
221
|
+
if (result.dryRun && result.changed) {
|
|
222
|
+
console.log("No files were written.")
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const invokedDirectly =
|
|
227
|
+
process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
228
|
+
|
|
229
|
+
if (invokedDirectly) {
|
|
230
|
+
try {
|
|
231
|
+
main()
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
234
|
+
process.exitCode = 1
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -28,7 +28,7 @@ description: Analyze the current repository and create or refresh concise CodeOp
|
|
|
28
28
|
|
|
29
29
|
## Discovery switch
|
|
30
30
|
|
|
31
|
-
`analyze-project` accepts `--with-discovery` and `--without-discovery`; passing both flags is a usage error. Both flags validate `codeops/codeops.json` against `schemas/codeops-config.schema.json` before changing anything: an invalid file stops the run with its schema errors and writes nothing (never discard malformed quality, routing, or metrics policy). Refuse to write when `codeops/` or `codeops/codeops.json` is a symlink or not a regular file; never write through a link.
|
|
31
|
+
`analyze-project` accepts `--with-discovery` and `--without-discovery`; passing both flags is a usage error. Existing projects can also use `setup-codeops --enable-discovery` / `--disable-discovery` for the config-only toggle. Both flags validate `codeops/codeops.json` against `schemas/codeops-config.schema.json` before changing anything: an invalid file stops the run with its schema errors and writes nothing (never discard malformed quality, routing, or metrics policy). Refuse to write when `codeops/` or `codeops/codeops.json` is a symlink or not a regular file; never write through a link.
|
|
32
32
|
|
|
33
33
|
- `--with-discovery`: set `discovery.enabled: true`, keeping every other key. When no file exists, create the minimal AR-35 file with the layout resolved from `_shared/layout-convention.md` (`nested` → `codeops`, `flat` → `.`):
|
|
34
34
|
```json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-codeops
|
|
3
3
|
description: >-
|
|
4
|
-
Sets up or upgrades CodeOps in the current git repo. It scaffolds a fresh nested codeops/ layout, auto-migrates a flat requirements/ + plans/ layout, and detects obsolete traceability.json workflow-state graphs in an existing nested project. Use when the user says "setup-codeops", "/setup-codeops", "set up codeops", "initialize codeops", "upgrade existing codeops", "migrate to the nested layout", or "scaffold the codeops structure". Supports --dry-run
|
|
4
|
+
Sets up or upgrades CodeOps in the current git repo. It scaffolds a fresh nested codeops/ layout, auto-migrates a flat requirements/ + plans/ layout, and detects obsolete traceability.json workflow-state graphs in an existing nested project. Use when the user says "setup-codeops", "/setup-codeops", "set up codeops", "initialize codeops", "upgrade existing codeops", "migrate to the nested layout", or "scaffold the codeops structure". Supports --dry-run, unattended --yes, and --enable-discovery/--disable-discovery for existing projects. Every migration previews deterministically, refuses unsafe or ambiguous apply, requires clean Git state for writes, and is idempotent. setup-codeops is the SOLE writer of the layout marker.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# CodeOps Layout Setup (`setup-codeops`)
|
|
@@ -21,8 +21,9 @@ it is the single source of truth for the layout. Do not re-encode paths here.
|
|
|
21
21
|
- This skill sets up the *structure*. It never authors requirements, plans, or roadmaps — that is
|
|
22
22
|
`make-requirements` / `make-plan` / `roadmap`, which then resolve paths via the convention doc.
|
|
23
23
|
- A fresh scaffold enables Discovery by default (`discovery.enabled: true`,
|
|
24
|
-
`chatMaxQuestions: 3`). Existing projects
|
|
25
|
-
`
|
|
24
|
+
`chatMaxQuestions: 3`). Existing projects can toggle it with
|
|
25
|
+
`setup-codeops --enable-discovery` / `--disable-discovery`; `analyze-project --with-discovery`
|
|
26
|
+
remains available for the same config change with guidance refresh.
|
|
26
27
|
|
|
27
28
|
## Dispatch — detect repo state, then branch
|
|
28
29
|
|
|
@@ -59,6 +60,31 @@ fresh scaffold should live in version control) and suggest `git init` first.
|
|
|
59
60
|
| *(none)* | Interactive: scaffold creates the skeleton; either migration previews then asks for one confirmation before applying. |
|
|
60
61
|
| `--dry-run` | Preview only — compute and show what would happen; change **nothing**. |
|
|
61
62
|
| `--yes` | Apply an unblocked migration without confirmation, then verify it. Safety refusals still apply. |
|
|
63
|
+
| `--enable-discovery` | Enable Discovery in an existing project without re-scaffolding. Preserves every other config key. |
|
|
64
|
+
| `--disable-discovery` | Disable Discovery, keeping `chatMaxQuestions` and every other config key. |
|
|
65
|
+
|
|
66
|
+
`--enable-discovery` and `--disable-discovery` are mutually exclusive. Validate that before the
|
|
67
|
+
layout branch so a usage error never changes the repository.
|
|
68
|
+
|
|
69
|
+
## Discovery toggle
|
|
70
|
+
|
|
71
|
+
After the layout branch (no-op, migration, or fresh scaffold), apply exactly one discovery flag
|
|
72
|
+
when present:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
node "${CODEOPS_PLUGIN_ROOT}/scripts/discovery-toggle.mjs" --root . --enable-discovery
|
|
76
|
+
node "${CODEOPS_PLUGIN_ROOT}/scripts/discovery-toggle.mjs" --root . --disable-discovery
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Add `--dry-run` to preview. The script validates the JSON, refuses symlinked `codeops/` or
|
|
80
|
+
`codeops/codeops.json`, preserves unrelated policy, and adds or removes the managed line
|
|
81
|
+
`- Discovery: enabled — question rounds open in the local Discovery page` when the
|
|
82
|
+
`CODEOPS-PROJECT` block exists. If `AGENTS.md` is absent or has no managed block, the config still
|
|
83
|
+
changes; report that guidance should be refreshed with `analyze-project`.
|
|
84
|
+
|
|
85
|
+
A fresh scaffold starts enabled. Passing `--disable-discovery` during scaffold turns it off; passing
|
|
86
|
+
`--enable-discovery` is idempotent. Without either flag, an existing project keeps its current
|
|
87
|
+
setting and a fresh scaffold stays enabled.
|
|
62
88
|
|
|
63
89
|
## Migration engines (delegation — do not re-implement)
|
|
64
90
|
|
|
@@ -86,6 +112,7 @@ deletion, and idempotency.
|
|
|
86
112
|
|
|
87
113
|
- [scaffold.md](scaffold.md) — the minimal fresh-repo skeleton.
|
|
88
114
|
- [migration.md](migration.md) — flat-layout and legacy workflow-state migration UX.
|
|
115
|
+
- `scripts/discovery-toggle.mjs` — deterministic config and managed-line toggle.
|
|
89
116
|
- [_shared/layout-convention.md](../../_shared/layout-convention.md) — the layout/path/ID/marker source of truth.
|
|
90
117
|
|
|
91
118
|
## Grounded Options & Recommendations
|