opencode-codeops 2.5.0 → 2.6.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 (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +39 -0
  3. package/_shared/spec-first-ordering.md +6 -2
  4. package/_shared/test-selection.md +130 -0
  5. package/bin/codeops-test.mjs +855 -0
  6. package/bin/lib/test-branch-policy.d.mts +51 -0
  7. package/bin/lib/test-branch-policy.mjs +185 -0
  8. package/bin/lib/test-map-validate.d.mts +42 -0
  9. package/bin/lib/test-map-validate.mjs +618 -0
  10. package/bin/lib/test-map.d.mts +114 -0
  11. package/bin/lib/test-map.mjs +384 -0
  12. package/bin/lib/test-provenance.d.mts +73 -0
  13. package/bin/lib/test-provenance.mjs +188 -0
  14. package/bin/lib/test-runner.d.mts +72 -0
  15. package/bin/lib/test-runner.mjs +350 -0
  16. package/bin/lib/test-selection-changes.d.mts +76 -0
  17. package/bin/lib/test-selection-changes.mjs +272 -0
  18. package/bin/lib/test-selection-explain.mjs +75 -0
  19. package/bin/lib/test-selection-glob.d.mts +32 -0
  20. package/bin/lib/test-selection-glob.mjs +206 -0
  21. package/bin/lib/test-selection.d.mts +80 -0
  22. package/bin/lib/test-selection.mjs +418 -0
  23. package/package.json +4 -2
  24. package/schemas/test-map.schema.json +164 -0
  25. package/scripts/__pycache__/codeops_plan.cpython-314.pyc +0 -0
  26. package/scripts/__pycache__/install_agents.cpython-314.pyc +0 -0
  27. package/skills/analyze-project/SKILL.md +8 -1
  28. package/skills/exec-plan/SKILL.md +16 -8
  29. package/skills/exec-plan/execution-protocol.md +15 -2
  30. package/skills/git-commit/SKILL.md +1 -1
  31. package/skills/make-plan/SKILL.md +10 -3
  32. package/skills/make-plan/quality-checklist.md +4 -0
  33. package/skills/make-plan/templates.md +19 -1
  34. package/skills/organize-tests/SKILL.md +169 -0
  35. package/skills/organize-tests/references/agents-testing-block.md +80 -0
  36. package/skills/organize-tests/references/analysis-protocol.md +94 -0
  37. package/skills/organize-tests/references/ci-github-actions.md +178 -0
  38. package/skills/organize-tests/references/language-adapters.md +54 -0
  39. package/skills/organize-tests/references/map-health.md +47 -0
  40. package/skills/organize-tests/references/project-wrapper.md +72 -0
  41. package/skills/organize-tests/references/shadow-mode.md +60 -0
  42. package/standards/coding-standards-full.md +28 -0
  43. package/standards/coding-standards.md +29 -0
  44. package/scripts/__pycache__/codeops_plan.cpython-312.pyc +0 -0
  45. package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pure test-selection algorithm.
4
+ *
5
+ * Given a loaded map, a change set, and a gate, decide which groups must run.
6
+ * The function is pure: it never reads Git, executes a command, or scans the
7
+ * repository. That keeps the most failure-sensitive logic easy to test and
8
+ * makes the CLI's job a thin I/O wrapper.
9
+ *
10
+ * The safety rule is simple: any uncertainty returns `mode: "full"` with a
11
+ * visible reason. A partial selection is never produced from an incomplete or
12
+ * malformed map.
13
+ *
14
+ * @module lib/test-selection
15
+ */
16
+
17
+ import { isTargetBasedGroup, normalizeRepoPath } from "./test-map.mjs"
18
+ import { matchesAnyGlob } from "./test-selection-glob.mjs"
19
+
20
+ /** Gates that may narrow the run to less than every group. */
21
+ export const PARTIAL_GATES = Object.freeze(["task", "phase", "pr"])
22
+
23
+ /** Every gate the selector accepts. */
24
+ export const ALL_GATES = Object.freeze(["task", "phase", "pr", "release", "acceptance", "nightly"])
25
+
26
+ /** Tiers allowed at each gate; `null` means every tier. */
27
+ const GATE_TIERS = Object.freeze({
28
+ task: Object.freeze(["task"]),
29
+ phase: Object.freeze(["task", "phase"]),
30
+ pr: Object.freeze(["task", "phase"]),
31
+ release: null,
32
+ acceptance: null,
33
+ nightly: null,
34
+ })
35
+
36
+ /** Stable reason order used in output and explanations. */
37
+ const REASON_ORDER = Object.freeze([
38
+ "direct-source",
39
+ "direct-test",
40
+ "included",
41
+ "dependent",
42
+ "always-run",
43
+ "smoke",
44
+ "gate-all",
45
+ ])
46
+
47
+ /**
48
+ * Add one reason to a group's reason set.
49
+ *
50
+ * @param {Map<string, Set<string>>} reasons Reason sets by group ID.
51
+ * @param {string} id Group ID.
52
+ * @param {string} reason Stable reason token.
53
+ */
54
+ function addReason(reasons, id, reason) {
55
+ if (!reasons.has(id)) reasons.set(id, new Set())
56
+ reasons.get(id).add(reason)
57
+ }
58
+
59
+ /**
60
+ * Sort reasons into the documented order.
61
+ *
62
+ * @param {Set<string>} values Reason set.
63
+ * @returns {string[]} Ordered reasons.
64
+ */
65
+ function orderedReasons(values) {
66
+ return [...values].sort((left, right) => REASON_ORDER.indexOf(left) - REASON_ORDER.indexOf(right))
67
+ }
68
+
69
+ /**
70
+ * Build the full-suite result.
71
+ *
72
+ * @param {object} input Result input.
73
+ * @param {string} input.reason Stable fallback reason.
74
+ * @param {object|null} input.policy Policy, when a map exists.
75
+ * @param {string|null} input.mapHash Map hash.
76
+ * @param {string} input.gate Gate.
77
+ * @param {object|null} input.base Resolved base.
78
+ * @param {string[]} input.changed Changed paths.
79
+ * @param {string[]} input.skipped Group IDs that did not run.
80
+ * @param {object[]} input.warnings Accumulated warnings.
81
+ * @param {string[]} [input.unknownPaths] Paths with no owner.
82
+ * @returns {object} Full selection result.
83
+ */
84
+ function fullResult({ reason, policy, mapHash, gate, base, changed, skipped, warnings, unknownPaths = [] }) {
85
+ const commands = expandFullCommands(policy)
86
+ return {
87
+ schema: 1,
88
+ mode: "full",
89
+ gate,
90
+ base: base ?? null,
91
+ mapHash: mapHash ?? null,
92
+ changed,
93
+ selected: [],
94
+ deferred: [],
95
+ skipped,
96
+ commands,
97
+ budget: { estimatedSec: 0, status: "not-applicable" },
98
+ warnings,
99
+ reason,
100
+ fullCommand: policy?.full ?? null,
101
+ unknownPaths,
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Expand a policy's full command into runner-shaped commands.
107
+ *
108
+ * @param {object|null} policy Map policy.
109
+ * @returns {Array<{id: string, argv: string[], cwd: string}>} Commands to run.
110
+ */
111
+ function expandFullCommands(policy) {
112
+ const full = policy?.full
113
+ if (!Array.isArray(full) || full.length === 0) return []
114
+ if (full.every((token) => typeof token === "string")) {
115
+ return [{ id: "full", argv: [...full], cwd: "." }]
116
+ }
117
+ return full.map((argv, index) => ({ id: `full.${index + 1}`, argv: [...argv], cwd: "." }))
118
+ }
119
+
120
+ /**
121
+ * Compute the task-budget status without ever dropping a group.
122
+ *
123
+ * @param {object[]} selected Selected groups.
124
+ * @param {object} policy Map policy.
125
+ * @returns {{estimatedSec: number, status: string, breakdown?: object[]}} Budget summary.
126
+ */
127
+ function budgetFor(selected, policy) {
128
+ const taskGroups = selected.filter((group) => group.tier === "task")
129
+ const estimatedSec = taskGroups.reduce((total, group) => total + (group.durationSec ?? 0), 0)
130
+ const target = policy.budgets?.taskTargetSeconds ?? 120
131
+ const acceptable = policy.budgets?.taskAcceptableSeconds ?? 180
132
+ const status = estimatedSec <= target ? "within-target" : estimatedSec <= acceptable ? "acceptable" : "over-budget"
133
+ if (status !== "over-budget") return { estimatedSec, status }
134
+ return {
135
+ estimatedSec,
136
+ status,
137
+ breakdown: taskGroups.map((group) => ({ id: group.id, durationSec: group.durationSec ?? 0 })),
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Resolve a group's full test set and expand its argv command.
143
+ *
144
+ * `{tests}` always expands to every file the group owns, not only the files in
145
+ * the current change set. A source-only change therefore still runs the whole
146
+ * group, which is what makes a green task result meaningful.
147
+ *
148
+ * @param {object} group Selected group.
149
+ * @param {string[]|null} files Tracked repository files.
150
+ * @param {boolean} caseSensitive Match case sensitivity.
151
+ * @returns {{argv: string[], cwd: string}|null} Expanded command, or null when the test set is empty.
152
+ */
153
+ function expandGroupCommand(group, files, caseSensitive) {
154
+ const tests = Array.isArray(group.tests) ? group.tests : []
155
+ const needsTests = group.run.command.some((token) => token === "{tests}")
156
+ const resolved = needsTests
157
+ ? (files ?? [])
158
+ .map(normalizeRepoPath)
159
+ .filter((file) => matchesAnyGlob(file, tests, { caseSensitive }))
160
+ .sort()
161
+ : []
162
+ if (needsTests && resolved.length === 0) return null
163
+
164
+ const argv = []
165
+ for (const token of group.run.command) {
166
+ if (token === "{tests}") argv.push(...resolved)
167
+ else argv.push(token)
168
+ }
169
+ return { argv, cwd: group.run.cwd ?? "." }
170
+ }
171
+
172
+ /**
173
+ * Select test groups for one gate.
174
+ *
175
+ * @param {object} input Selection input.
176
+ * @param {object|null} input.map Loaded map, or null for the no-map case.
177
+ * @param {string|null} [input.mapHash] Map hash for evidence.
178
+ * @param {string[]} [input.changed=[]] Changed repository paths.
179
+ * @param {string[]|null} [input.files=null] Tracked repository files for command expansion.
180
+ * @param {string} [input.gate="task"] Gate name.
181
+ * @param {string[]} [input.include=[]] Extra paths that must be considered changed.
182
+ * @param {boolean} [input.explicitFull=false] Force the full suite.
183
+ * @param {Record<string, string>} [input.env={}] Environment values.
184
+ * @param {object|null} [input.base=null] Resolved base evidence.
185
+ * @returns {object} Deterministic selection result.
186
+ * @throws {Error} When the gate is unknown.
187
+ * @example
188
+ * const result = selectTests({ map: loaded.map, changed: ["web/billing/Dialog.tsx"], gate: "task" })
189
+ */
190
+ export function selectTests({
191
+ map,
192
+ mapHash = null,
193
+ changed = [],
194
+ files = null,
195
+ gate = "task",
196
+ include = [],
197
+ explicitFull = false,
198
+ env = {},
199
+ base = null,
200
+ }) {
201
+ if (!ALL_GATES.includes(gate)) throw new Error(`unknown gate: ${gate}`)
202
+ const warnings = []
203
+ const policy = map?.policy ?? null
204
+ const groups = map?.groups ?? []
205
+ const allIds = groups.map((group) => group.id).sort()
206
+ const changedPaths = [...new Set([...changed, ...include].map(normalizeRepoPath))].sort()
207
+
208
+ if (!map) {
209
+ return fullResult({ reason: "no-map", policy: null, mapHash: null, gate, base, changed: changedPaths, skipped: [], warnings })
210
+ }
211
+
212
+ if (explicitFull || env.CODEOPS_FULL_TESTS === "1") {
213
+ return fullResult({ reason: "forced", policy, mapHash, gate, base, changed: changedPaths, skipped: allIds, warnings })
214
+ }
215
+
216
+ const caseSensitive = policy.caseSensitive !== false
217
+ if (!caseSensitive) {
218
+ warnings.push({ code: "case-insensitive", message: "path matching is case-insensitive by policy override" })
219
+ }
220
+ const matchOptions = { caseSensitive }
221
+
222
+ const fullOnPaths = (policy.fullOnPaths ?? []).filter((pattern) =>
223
+ changedPaths.some((path) => matchesAnyGlob(path, [pattern], matchOptions)),
224
+ )
225
+ if (fullOnPaths.length > 0) {
226
+ const matching = changedPaths.filter((path) => matchesAnyGlob(path, fullOnPaths, matchOptions))
227
+ return fullResult({
228
+ reason: "full-on-path",
229
+ policy,
230
+ mapHash,
231
+ gate,
232
+ base,
233
+ changed: matching,
234
+ skipped: allIds,
235
+ warnings,
236
+ })
237
+ }
238
+
239
+ const ignoreGlobs = (policy.ignorePaths ?? []).map((entry) => entry.glob)
240
+ const relevant = changedPaths.filter((path) => !matchesAnyGlob(path, ignoreGlobs, matchOptions))
241
+ if (changedPaths.length > 0 && relevant.length === 0) {
242
+ return {
243
+ schema: 1,
244
+ mode: "none",
245
+ gate,
246
+ base: base ?? null,
247
+ mapHash,
248
+ changed: changedPaths,
249
+ selected: [],
250
+ deferred: [],
251
+ skipped: allIds,
252
+ commands: [],
253
+ budget: { estimatedSec: 0, status: "not-applicable" },
254
+ warnings,
255
+ reason: "docs-only",
256
+ fullCommand: policy.full ?? null,
257
+ unknownPaths: [],
258
+ }
259
+ }
260
+
261
+ const includeSet = new Set(include.map(normalizeRepoPath))
262
+ const reasons = new Map()
263
+ const unknownPaths = []
264
+
265
+ for (const path of relevant) {
266
+ let matched = false
267
+ for (const group of groups) {
268
+ const sourceMatch = matchesAnyGlob(path, group.sources ?? [], matchOptions)
269
+ const testMatch = Array.isArray(group.tests) && matchesAnyGlob(path, group.tests, matchOptions)
270
+ if (sourceMatch) addReason(reasons, group.id, "direct-source")
271
+ if (testMatch) addReason(reasons, group.id, "direct-test")
272
+ if (sourceMatch || testMatch) {
273
+ matched = true
274
+ if (includeSet.has(path)) addReason(reasons, group.id, "included")
275
+ }
276
+ }
277
+ if (!matched) unknownPaths.push(path)
278
+ }
279
+
280
+ if (unknownPaths.length > 0) {
281
+ return fullResult({
282
+ reason: "unknown-path",
283
+ policy,
284
+ mapHash,
285
+ gate,
286
+ base,
287
+ changed: changedPaths,
288
+ skipped: allIds,
289
+ warnings,
290
+ unknownPaths,
291
+ })
292
+ }
293
+
294
+ const byId = new Map(groups.map((group) => [group.id, group]))
295
+ const dependents = new Map()
296
+ for (const group of groups) {
297
+ for (const dependency of group.dependsOn ?? []) {
298
+ if (!dependents.has(dependency)) dependents.set(dependency, [])
299
+ dependents.get(dependency).push(group.id)
300
+ }
301
+ }
302
+
303
+ const queue = [...reasons.keys()]
304
+ while (queue.length > 0) {
305
+ const current = queue.shift()
306
+ for (const dependent of dependents.get(current) ?? []) {
307
+ if (!reasons.has(dependent)) {
308
+ addReason(reasons, dependent, "dependent")
309
+ queue.push(dependent)
310
+ }
311
+ }
312
+ }
313
+
314
+ for (const group of groups) {
315
+ if (group.alwaysRun === true) addReason(reasons, group.id, "always-run")
316
+ }
317
+
318
+ const allowedTiers = GATE_TIERS[gate]
319
+ if (allowedTiers === null) {
320
+ for (const group of groups) {
321
+ if (!reasons.has(group.id)) addReason(reasons, group.id, "gate-all")
322
+ }
323
+ }
324
+
325
+ const deferred = []
326
+ for (const id of [...reasons.keys()]) {
327
+ const group = byId.get(id)
328
+ if (!group) continue
329
+ if (allowedTiers !== null && !allowedTiers.includes(group.tier)) {
330
+ reasons.delete(id)
331
+ deferred.push({ id: group.id, reason: "deferred-tier" })
332
+ }
333
+ }
334
+
335
+ if (gate === "pr") {
336
+ for (const id of policy.prSmoke ?? []) {
337
+ if (byId.has(id)) addReason(reasons, id, "smoke")
338
+ }
339
+ }
340
+
341
+ if (allowedTiers !== null) {
342
+ const selectedCount = reasons.size
343
+ const threshold = policy.fullThreshold ?? 0.6
344
+ if (groups.length > 0 && selectedCount / groups.length >= threshold) {
345
+ return fullResult({ reason: "threshold", policy, mapHash, gate, base, changed: changedPaths, skipped: allIds, warnings })
346
+ }
347
+ }
348
+
349
+ const selectedGroups = [...reasons.keys()]
350
+ .map((id) => byId.get(id))
351
+ .filter(Boolean)
352
+ .sort((left, right) => left.slice.localeCompare(right.slice) || left.id.localeCompare(right.id))
353
+
354
+ const commands = []
355
+ for (const group of selectedGroups) {
356
+ const command = expandGroupCommand(group, files, caseSensitive)
357
+ if (!command) {
358
+ return fullResult({
359
+ reason: "map.empty-group",
360
+ policy,
361
+ mapHash,
362
+ gate,
363
+ base,
364
+ changed: changedPaths,
365
+ skipped: allIds,
366
+ warnings,
367
+ })
368
+ }
369
+ commands.push({ id: group.id, argv: command.argv, cwd: command.cwd })
370
+ }
371
+
372
+ const budget = budgetFor(selectedGroups, policy)
373
+ if (budget.status === "over-budget") {
374
+ warnings.push({
375
+ code: "budget.over-budget",
376
+ message: `selected task groups estimate ${budget.estimatedSec}s; all groups are retained`,
377
+ breakdown: budget.breakdown,
378
+ })
379
+ }
380
+
381
+ const selectedIds = new Set(selectedGroups.map((group) => group.id))
382
+ const deferredIds = new Set(deferred.map((entry) => entry.id))
383
+ const skipped = allIds.filter((id) => !selectedIds.has(id) && !deferredIds.has(id))
384
+
385
+ return {
386
+ schema: 1,
387
+ mode: "groups",
388
+ gate,
389
+ base: base ?? null,
390
+ mapHash,
391
+ changed: changedPaths,
392
+ selected: selectedGroups.map((group) => ({
393
+ id: group.id,
394
+ slice: group.slice,
395
+ reasons: orderedReasons(reasons.get(group.id)),
396
+ })),
397
+ deferred: deferred.sort((left, right) => left.id.localeCompare(right.id)),
398
+ skipped,
399
+ commands,
400
+ budget: { estimatedSec: budget.estimatedSec, status: budget.status },
401
+ warnings,
402
+ reason: null,
403
+ fullCommand: policy.full ?? null,
404
+ unknownPaths: [],
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Check whether a group is target-based and therefore exempt from `{tests}`.
410
+ *
411
+ * Re-exported so the CLI and templates do not need a second import path.
412
+ *
413
+ * @param {object} group Map group.
414
+ * @returns {boolean} `true` when the command has no `{tests}` placeholder.
415
+ */
416
+ export function groupUsesTargetDiscovery(group) {
417
+ return isTargetBasedGroup(group)
418
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,7 +14,8 @@
14
14
  "./tui": "./plugin/tui.js"
15
15
  },
16
16
  "bin": {
17
- "opencode-codeops": "bin/index.mjs"
17
+ "opencode-codeops": "bin/index.mjs",
18
+ "codeops-test": "bin/codeops-test.mjs"
18
19
  },
19
20
  "scripts": {
20
21
  "test": "node --test",
@@ -39,6 +40,7 @@
39
40
  "!scripts/*.test.mjs",
40
41
  "!scripts/*.spec.test.mjs",
41
42
  "bin/",
43
+ "!bin/fixtures/",
42
44
  "!bin/*.test.mjs",
43
45
  "!bin/*.spec.test.mjs",
44
46
  "references/",
@@ -0,0 +1,164 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/blendsdk/opencode-codeops/schemas/test-map.schema.json",
4
+ "title": "CodeOps test-impact map",
5
+ "description": "A single-file map, a policy file, or one group fragment. The loader combines a policy file with fragments.",
6
+ "oneOf": [
7
+ { "$ref": "#/$defs/singleMap" },
8
+ { "$ref": "#/$defs/policyFile" },
9
+ { "$ref": "#/$defs/fragment" }
10
+ ],
11
+ "$defs": {
12
+ "path": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\u0000).*$"
16
+ },
17
+ "argv": {
18
+ "type": "array",
19
+ "minItems": 1,
20
+ "items": { "type": "string", "minLength": 1 }
21
+ },
22
+ "full": {
23
+ "oneOf": [
24
+ { "$ref": "#/$defs/argv" },
25
+ {
26
+ "type": "array",
27
+ "minItems": 1,
28
+ "items": { "$ref": "#/$defs/argv" }
29
+ }
30
+ ]
31
+ },
32
+ "ignoreEntry": {
33
+ "type": "object",
34
+ "required": ["glob", "reason"],
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "glob": { "$ref": "#/$defs/path" },
38
+ "reason": { "type": "string", "minLength": 1 }
39
+ }
40
+ },
41
+ "branches": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "integration": { "type": "array", "items": { "type": "string" } },
46
+ "feature": { "type": "array", "items": { "type": "string" } },
47
+ "release": { "type": "array", "items": { "type": "string" } },
48
+ "acceptance": { "type": "array", "items": { "type": "string" } },
49
+ "releaseTags": { "type": "array", "items": { "type": "string" } },
50
+ "unknown": { "enum": ["integration", "feature", "release", "acceptance"] }
51
+ }
52
+ },
53
+ "policy": {
54
+ "type": "object",
55
+ "required": ["schema", "full"],
56
+ "additionalProperties": true,
57
+ "properties": {
58
+ "schema": { "const": 1 },
59
+ "full": { "$ref": "#/$defs/full" },
60
+ "budgets": {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "taskTargetSeconds": { "type": "number", "minimum": 0 },
65
+ "taskAcceptableSeconds": { "type": "number", "minimum": 0 }
66
+ }
67
+ },
68
+ "branches": { "$ref": "#/$defs/branches" },
69
+ "ignorePaths": { "type": "array", "items": { "$ref": "#/$defs/ignoreEntry" } },
70
+ "fullOnPaths": { "type": "array", "items": { "$ref": "#/$defs/path" } },
71
+ "acceptance": {
72
+ "type": "object",
73
+ "required": ["mode"],
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "mode": { "enum": ["automated", "manual", "external"] },
77
+ "groups": { "type": "array", "items": { "type": "string" } },
78
+ "checklist": { "type": ["string", "null"] }
79
+ }
80
+ },
81
+ "testRoots": {
82
+ "oneOf": [
83
+ { "type": "null" },
84
+ { "type": "array", "items": { "$ref": "#/$defs/path" } }
85
+ ]
86
+ },
87
+ "fullThreshold": { "type": "number", "minimum": 0, "maximum": 1 },
88
+ "prSmoke": { "type": "array", "items": { "type": "string" } },
89
+ "requirements": {
90
+ "type": "object",
91
+ "additionalProperties": {
92
+ "type": "object",
93
+ "required": ["check"],
94
+ "additionalProperties": false,
95
+ "properties": { "check": { "$ref": "#/$defs/argv" } }
96
+ }
97
+ },
98
+ "caseSensitive": { "type": "boolean" }
99
+ }
100
+ },
101
+ "sliceAcceptance": {
102
+ "type": "object",
103
+ "required": ["mode", "reason"],
104
+ "additionalProperties": false,
105
+ "properties": {
106
+ "mode": { "enum": ["manual", "external", "na"] },
107
+ "reason": { "type": "string", "minLength": 1 }
108
+ }
109
+ },
110
+ "group": {
111
+ "type": "object",
112
+ "required": ["id", "slice", "tier", "sources", "run"],
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "id": { "type": "string", "minLength": 1 },
116
+ "slice": { "type": "string", "minLength": 1 },
117
+ "tier": { "enum": ["task", "phase", "release", "acceptance"] },
118
+ "sources": { "type": "array", "items": { "$ref": "#/$defs/path" } },
119
+ "tests": { "type": "array", "items": { "$ref": "#/$defs/path" } },
120
+ "dependsOn": { "type": "array", "items": { "type": "string" } },
121
+ "alwaysRun": { "type": "boolean" },
122
+ "durationSec": { "type": "number", "minimum": 0 },
123
+ "parallelSafe": { "type": "boolean" },
124
+ "requires": { "type": "array", "items": { "type": "string" } },
125
+ "owner": { "type": "string" },
126
+ "description": { "type": "string" },
127
+ "acceptance": { "$ref": "#/$defs/sliceAcceptance" },
128
+ "run": {
129
+ "type": "object",
130
+ "required": ["command"],
131
+ "additionalProperties": false,
132
+ "properties": {
133
+ "command": { "$ref": "#/$defs/argv" },
134
+ "cwd": { "$ref": "#/$defs/path" }
135
+ }
136
+ }
137
+ }
138
+ },
139
+ "singleMap": {
140
+ "type": "object",
141
+ "required": ["schema", "policy", "groups"],
142
+ "additionalProperties": false,
143
+ "properties": {
144
+ "schema": { "const": 1 },
145
+ "policy": { "$ref": "#/$defs/policy" },
146
+ "groups": { "type": "array", "items": { "$ref": "#/$defs/group" } }
147
+ }
148
+ },
149
+ "policyFile": {
150
+ "$ref": "#/$defs/policy"
151
+ },
152
+ "fragment": {
153
+ "type": "object",
154
+ "required": ["schema", "groups"],
155
+ "additionalProperties": false,
156
+ "properties": {
157
+ "schema": { "const": 1 },
158
+ "domain": { "type": "string" },
159
+ "owner": { "type": "string" },
160
+ "groups": { "type": "array", "items": { "$ref": "#/$defs/group" } }
161
+ }
162
+ }
163
+ }
164
+ }
@@ -20,7 +20,14 @@ description: Analyze the current repository and create or refresh concise CodeOp
20
20
  ```
21
21
 
22
22
  5. Include project type, principal languages/frameworks, authoritative build/test/verify commands, high-level structure, generated-file warnings, and CodeOps artifact/config locations.
23
- 6. If the managed section exists, replace only its contents. Preserve all other text byte-for-byte.
23
+ 6. If the managed section exists, replace only its generated contents outside the `CODEOPS-TESTING` block. Preserve all other text byte-for-byte.
24
+ 6b. **Testing block.** Detect `codeops/test-map.json` or `codeops/test-map/` and the project's
25
+ `full` command. The `<!-- CODEOPS-TESTING:START -->` / `<!-- CODEOPS-TESTING:END -->` block
26
+ inside the managed section is owned by the `organize-tests` skill: preserve it byte-for-byte,
27
+ never render or overwrite it, and warn when it is missing or stale. With no map and no block,
28
+ report that no map is present and do not create a block; an existing no-map block is preserved
29
+ as-is. Warn (advisory only) when CI has no full gate on a release/acceptance branch. The block
30
+ contract is in [_shared/test-selection.md](../../_shared/test-selection.md).
24
31
  7. On a non-integration branch, preview changes to repository-wide guidance and ask before writing unless repository policy explicitly permits branch-local updates.
25
32
  8. In compact mode, remove duplication and stale generated detail from the managed section only. Flag suspected hand-authored bloat; never silently rewrite it.
26
33
  9. Validate every recorded command or mark it explicitly unverified.
@@ -145,7 +145,9 @@ summary template. The essentials:
145
145
 
146
146
  ### Step 1 — Load the plan
147
147
 
148
- 1. Read `plans/$ARGUMENTS/99-execution-plan.md`.
148
+ 1. Read `plans/$ARGUMENTS/99-execution-plan.md`. An old plan with a single `**Verify**` line
149
+ uses that line as the gate command; with a map, derive the task command, otherwise it serves
150
+ both.
149
151
  2. Find incomplete tasks — both `[ ]` and implemented-but-unverified `[~]`; read supporting specs
150
152
  in `plans/$ARGUMENTS/`.
151
153
  3. Determine the starting point: a `[~]` task is resumed first (re-verify, then promote or keep
@@ -177,12 +179,17 @@ For each task, in order:
177
179
  points in [execution-protocol.md](execution-protocol.md) §Progress Reporting; a failed or
178
180
  missing report never blocks any step.
179
181
 
180
- 3. **Verify** — run your project's verify command (from the project's AGENTS.md, or detected
181
- project conventions), output captured per the protocol's **Verify-output capture rule**
182
- (PASS one-liner; on failure the last 50 log lines + log path). Pass → promote `[~]` → `[x]`;
183
- fail → fix and re-verify (mark stays `[~]`). Immediately after every `[x]` promotion, run the
184
- protocol's read-only progress-bar command for the selected plan and show its exact output in the
185
- next commentary update. Only `[x]` contributes to the displayed completion count.
182
+ 3. **Verify** — run the task command: the plan's `**Task verify**` line, or the map's `task`
183
+ gate with `--baseline <phase-tree>` and `--include <spec-file>` for the plan's declared spec
184
+ file(s). With neither, run the plan's `**Verify (gate)**` command, or the project's full verify
185
+ (from AGENTS.md or detected conventions), output captured per the protocol's
186
+ **Verify-output capture rule** (PASS one-liner; on failure
187
+ the last 50 log lines + log path). A changed path outside the phase's expected modification
188
+ set, or a shared/config/test-infrastructure change, forces the full gate. Pass → promote `[~]`
189
+ → `[x]`; fail → fix and re-verify (mark stays `[~]`). Immediately after every `[x]` promotion,
190
+ run the protocol's read-only progress-bar command for the selected plan and show its exact
191
+ output in the next commentary update. Only `[x]` contributes to the displayed completion
192
+ count.
186
193
  4. **Commit** per the active commit mode (see [commit-modes.md](commit-modes.md)) — the commit
187
194
  gate keys off `[x]`.
188
195
  5. **Techdocs check (after each phase):** if the phase introduced architectural changes and
@@ -215,7 +222,8 @@ For each task, in order:
215
222
 
216
223
  1. Finish the current task before stopping.
217
224
  2. **🚨 First, update `99-execution-plan.md`** with all completed tasks (before anything else).
218
- 3. Run the verify command.
225
+ 3. Run the task command when the plan is not complete and the session stops inside a phase;
226
+ run the full gate at plan completion or a phase boundary.
219
227
  4. Handle the commit per the active commit mode.
220
228
  5. Report a session summary (must state `Execution Plan Updated: ✅`). Template in
221
229
  [execution-protocol.md](execution-protocol.md).