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,355 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # codeops-migrate.sh — deterministic flat→nested CodeOps migration engine.
4
+ #
5
+ # This is the single, unit-testable source of truth for the migration ALGORITHM (see
6
+ # plans/codeops-v2-layout/03-02-setup-codeops.md, PF-003). The setup_codeops skill owns the
7
+ # conversational UX (preview, confirmation, reporting) and DELEGATES the path arithmetic and the
8
+ # git mv apply to this script — so there is no prose-vs-script drift. scripts/migration-check.sh
9
+ # asserts this engine's move map, warnings, refusals, and idempotency against a fixture copy.
10
+ #
11
+ # It computes the move map from a flat-layout repo (today: one flat repo == one feature-set), scans
12
+ # for hazards (plans absent from the roadmap; links pointing out of the planning tree), refuses to
13
+ # run on a non-git or dirty tree, rejects/normalizes path-traversal slugs, is idempotent (a marker
14
+ # means "already migrated"), and applies via `git mv` (history preserved), writing the marker and
15
+ # the seeded portfolio roadmap LAST so an interrupted run never leaves a false "migrated" marker.
16
+ #
17
+ # Security: never executes repo data; the feature slug is sanitized so it can never escape
18
+ # codeops/features/. All mutation requires an explicit --yes; absent that, it only previews.
19
+ #
20
+ # Usage:
21
+ # codeops-migrate.sh --dry-run # compute + print the move map + warnings; change nothing
22
+ # codeops-migrate.sh --yes # apply the migration via git mv (no interactive prompt)
23
+ # codeops-migrate.sh # same as --dry-run (safe default; never mutates without --yes)
24
+ # Exit: 0 = success / no-op; 1 = refusal (not a git repo, dirty tree); 2 = bad usage.
25
+
26
+ set -uo pipefail
27
+
28
+ DRY=0
29
+ YES=0
30
+ for arg in "$@"; do
31
+ case "$arg" in
32
+ --dry-run) DRY=1 ;;
33
+ --yes) YES=1 ;;
34
+ -h|--help)
35
+ grep -E '^# ' "$0" | sed 's/^# \{0,1\}//'
36
+ exit 0 ;;
37
+ *)
38
+ printf 'ERROR: unknown argument: %s\n' "$arg" >&2
39
+ exit 2 ;;
40
+ esac
41
+ done
42
+
43
+ # Apply only on an explicit --yes that is not paired with --dry-run (preview always wins).
44
+ APPLY=0
45
+ if [[ "$YES" -eq 1 && "$DRY" -eq 0 ]]; then
46
+ APPLY=1
47
+ fi
48
+
49
+ HAVE_PY3=0
50
+ command -v python3 >/dev/null 2>&1 && HAVE_PY3=1
51
+
52
+ # slugify <text> — lowercase, collapse every non-alphanumeric run to a single '-', trim '-'.
53
+ # This sanitizes the feature slug so it can never contain '/', '..', or be absolute (Security).
54
+ slugify() {
55
+ if [[ "$HAVE_PY3" -eq 1 ]]; then
56
+ python3 - "$1" <<'PY'
57
+ import re, sys
58
+ s = re.sub(r'[^a-z0-9]+', '-', sys.argv[1].lower()).strip('-')
59
+ print(s)
60
+ PY
61
+ else
62
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
63
+ fi
64
+ }
65
+
66
+ # -----------------------------------------------------------------------------
67
+ # Preconditions: must be a git repo; idempotent if already migrated; refuse if dirty.
68
+ # -----------------------------------------------------------------------------
69
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
70
+ printf 'ERROR: not inside a git repository (git is required for git mv).\n' >&2
71
+ exit 1
72
+ }
73
+ cd "$ROOT"
74
+
75
+ if [[ -f codeops/.codeops.yml ]]; then
76
+ printf 'codeops-migrate: already migrated (codeops/.codeops.yml present) — nothing to do (no-op).\n'
77
+ exit 0
78
+ fi
79
+
80
+ # Is there any flat-layout content to migrate at all?
81
+ shopt -s nullglob
82
+ plan_dirs=(plans/*/)
83
+ has_flat=0
84
+ [[ -d requirements ]] && has_flat=1
85
+ [[ -f plans/00-roadmap.md ]] && has_flat=1
86
+ [[ ${#plan_dirs[@]} -gt 0 ]] && has_flat=1
87
+ if [[ "$has_flat" -eq 0 ]]; then
88
+ printf 'codeops-migrate: no flat-layout artifacts found; nothing to migrate.\n'
89
+ printf ' (For a fresh repo use the setup_codeops scaffold to create the codeops/ skeleton.)\n'
90
+ exit 0
91
+ fi
92
+
93
+ # Migration must run on a clean tree so the whole move lands as one reviewable commit.
94
+ if [[ -n "$(git status --porcelain)" ]]; then
95
+ printf 'ERROR: working tree is dirty — migration must run on a clean tree so it lands as a\n' >&2
96
+ printf ' single reviewable commit. Commit or stash your changes first, then re-run.\n' >&2
97
+ exit 1
98
+ fi
99
+
100
+ # A non-directory named `codeops` makes every apply step impossible — refuse up front with a
101
+ # clear message instead of failing step-by-step (AR #18).
102
+ if [[ -e codeops && ! -d codeops ]]; then
103
+ printf 'ERROR: `codeops` exists but is not a directory — the nested layout cannot be created.\n' >&2
104
+ printf ' Move or remove it, then re-run.\n' >&2
105
+ exit 1
106
+ fi
107
+
108
+ # -----------------------------------------------------------------------------
109
+ # Derive the feature slug (roadmap header → else repo dir name), then sanitize.
110
+ # -----------------------------------------------------------------------------
111
+ slug=""
112
+ slug_src=""
113
+ if [[ -f plans/00-roadmap.md ]]; then
114
+ header="$(grep -E '^> \*\*Feature-Set\*\*:' plans/00-roadmap.md | head -1 | sed -E 's/^> \*\*Feature-Set\*\*:[[:space:]]*//')"
115
+ if [[ -n "$header" ]]; then
116
+ slug="$(slugify "$header")"
117
+ slug_src="roadmap-header"
118
+ fi
119
+ fi
120
+ if [[ -z "$slug" ]]; then
121
+ slug="$(slugify "$(basename "$ROOT")")"
122
+ slug_src="dir-name"
123
+ fi
124
+ # Final safety net: a slug must be a single safe path component.
125
+ if [[ -z "$slug" || "$slug" == *"/"* || "$slug" == *".."* ]]; then
126
+ printf 'ERROR: could not derive a safe feature slug (got: %q).\n' "$slug" >&2
127
+ exit 1
128
+ fi
129
+
130
+ # -----------------------------------------------------------------------------
131
+ # Compute the move map (no file changes here).
132
+ # -----------------------------------------------------------------------------
133
+ moves=() # each entry: "src|dst"
134
+ [[ -d requirements ]] && moves+=("requirements/|codeops/features/$slug/requirements/")
135
+ for d in plans/*/; do
136
+ name="$(basename "$d")"
137
+ [[ "$name" == "_archive" ]] && continue
138
+ moves+=("plans/$name/|codeops/features/$slug/plans/$name/")
139
+ done
140
+ [[ -f plans/00-roadmap.md ]] && moves+=("plans/00-roadmap.md|codeops/features/$slug/00-roadmap.md")
141
+ if [[ -d plans/_archive ]]; then
142
+ for a in plans/_archive/*/; do
143
+ aname="$(basename "$a")"
144
+ moves+=("plans/_archive/$aname/|codeops/_archive/$aname/")
145
+ done
146
+ # Loose FILES directly under plans/_archive/ move too — leaving them behind would strand a
147
+ # residual plans/ tree after the marker is written (AR #18).
148
+ for af in plans/_archive/*; do
149
+ [[ -f "$af" ]] || continue
150
+ moves+=("$af|codeops/_archive/$(basename "$af")")
151
+ done
152
+ fi
153
+
154
+ # -----------------------------------------------------------------------------
155
+ # Hazard scan → warnings (never block; the user resolves these by hand after the move).
156
+ # -----------------------------------------------------------------------------
157
+ warnings=()
158
+ # (a) plan folders on disk but not referenced in the roadmap.
159
+ if [[ -f plans/00-roadmap.md ]]; then
160
+ for d in plans/*/; do
161
+ name="$(basename "$d")"
162
+ [[ "$name" == "_archive" ]] && continue
163
+ if ! grep -qF "$name" plans/00-roadmap.md; then
164
+ warnings+=("plan-not-in-roadmap: plans/$name/ is on disk but not referenced in the roadmap")
165
+ fi
166
+ done
167
+ fi
168
+ # (a2) loose files directly under plans/ that are NOT the roadmap and NOT inside a plan dir.
169
+ # The move map only relocates plan dirs + plans/00-roadmap.md, so these would otherwise be
170
+ # left behind in a surviving plans/ after the marker is written — a half-migrated state. We
171
+ # cannot guess a feature target for a stray file, so we surface it (never silently orphan it).
172
+ for f in plans/*; do
173
+ [[ -f "$f" ]] || continue
174
+ [[ "$(basename "$f")" == "00-roadmap.md" ]] && continue
175
+ warnings+=("loose-file-not-migrated: $f is directly under plans/ and is left in place (move it by hand)")
176
+ done
177
+ # (a3) loose files directly under plans/_archive/ ARE migrated (into codeops/_archive/), but get
178
+ # surfaced so the user knows an unusual artifact was found and relocated.
179
+ if [[ -d plans/_archive ]]; then
180
+ for af in plans/_archive/*; do
181
+ [[ -f "$af" ]] || continue
182
+ warnings+=("archive-loose-file: $af is a loose file under plans/_archive/ — it will be moved to codeops/_archive/$(basename "$af")")
183
+ done
184
+ fi
185
+ # (b) relative links in plan docs that point OUT of the planning tree (into source, etc.).
186
+ # A structure-preserving move keeps intra-codeops links valid; only out-of-tree links can
187
+ # break, so we surface them as warnings rather than silently rewriting them (AR #16).
188
+ if [[ "$HAVE_PY3" -eq 1 && -d plans ]]; then
189
+ while IFS= read -r line; do
190
+ [[ -n "$line" ]] && warnings+=("$line")
191
+ done < <(python3 - "$ROOT" <<'PY'
192
+ import os, re, sys
193
+ root = sys.argv[1]
194
+ plans = os.path.join(root, "plans")
195
+ link_re = re.compile(r'\]\(([^)]+)\)')
196
+ inside = ("plans" + os.sep, "requirements" + os.sep)
197
+ for dirpath, _dirs, files in os.walk(plans):
198
+ for fn in files:
199
+ if not fn.endswith(".md"):
200
+ continue
201
+ fp = os.path.join(dirpath, fn)
202
+ rel = os.path.relpath(fp, root)
203
+ try:
204
+ text = open(fp, encoding="utf-8", errors="replace").read()
205
+ except OSError:
206
+ continue
207
+ for m in link_re.finditer(text):
208
+ tgt = m.group(1).strip()
209
+ if tgt.startswith(("http://", "https://", "#", "mailto:")):
210
+ continue
211
+ if not tgt.startswith(".."):
212
+ continue
213
+ norm = os.path.normpath(os.path.join(os.path.dirname(rel), tgt))
214
+ if not (norm.startswith(inside) or norm in ("plans", "requirements")):
215
+ print(f"source-relative-link: {rel} -> {tgt}")
216
+ PY
217
+ )
218
+ fi
219
+
220
+ # -----------------------------------------------------------------------------
221
+ # Print the preview (always — dry-run, default, and as the apply log).
222
+ # -----------------------------------------------------------------------------
223
+ if [[ "$APPLY" -eq 1 ]]; then
224
+ printf 'codeops-migrate: applying flat → nested migration (git mv)\n'
225
+ else
226
+ printf 'codeops-migrate: dry-run preview (no changes will be made)\n'
227
+ fi
228
+ printf 'SLUG: %s (source: %s)\n' "$slug" "$slug_src"
229
+ for m in "${moves[@]}"; do
230
+ printf 'MOVE %s -> %s\n' "${m%%|*}" "${m##*|}"
231
+ done
232
+ printf 'CREATE codeops/.codeops.yml\n'
233
+ printf 'CREATE codeops/codeops.json\n'
234
+ printf 'CREATE codeops/00-roadmap.md\n'
235
+ for w in "${warnings[@]:-}"; do
236
+ [[ -n "$w" ]] && printf 'WARN %s\n' "$w"
237
+ done
238
+
239
+ if [[ "$APPLY" -ne 1 ]]; then
240
+ [[ "$DRY" -eq 0 ]] && printf '\n(preview only; re-run with --yes to apply)\n'
241
+ exit 0
242
+ fi
243
+
244
+ # -----------------------------------------------------------------------------
245
+ # Apply: git mv each mapping, then write the marker + seeded portfolio LAST.
246
+ #
247
+ # Every step is failure-checked (AR #18): the FIRST failed step aborts the apply with a
248
+ # non-zero exit, the marker is NEVER written after a failure (so the idempotency guard can
249
+ # never brick a retry on a half-migrated tree), and the report names the failing step.
250
+ # `set -e` is deliberately not relied on — explicit checks avoid its conditional/subshell
251
+ # pitfalls and give a named failure.
252
+ # -----------------------------------------------------------------------------
253
+ fail_apply() {
254
+ printf '\ncodeops-migrate: FAILED at step: %s\n' "$1" >&2
255
+ printf ' The migration did NOT complete and NO layout marker was written.\n' >&2
256
+ printf ' Inspect with "git status"; undo any partial moves with "git reset --hard",\n' >&2
257
+ printf ' fix the cause, then re-run.\n' >&2
258
+ exit 1
259
+ }
260
+ step() {
261
+ local desc="$1"; shift
262
+ "$@" || fail_apply "$desc"
263
+ }
264
+
265
+ step "mkdir codeops/features/$slug/plans" mkdir -p "codeops/features/$slug/plans"
266
+ [[ -d plans/_archive ]] && step "mkdir codeops/_archive" mkdir -p codeops/_archive
267
+
268
+ [[ -d requirements ]] && step "git mv requirements" git mv requirements "codeops/features/$slug/requirements"
269
+ for d in plans/*/; do
270
+ name="$(basename "$d")"
271
+ [[ "$name" == "_archive" ]] && continue
272
+ step "git mv plans/$name" git mv "plans/$name" "codeops/features/$slug/plans/$name"
273
+ done
274
+ [[ -f plans/00-roadmap.md ]] && step "git mv plans/00-roadmap.md" git mv plans/00-roadmap.md "codeops/features/$slug/00-roadmap.md"
275
+ if [[ -d plans/_archive ]]; then
276
+ for a in plans/_archive/*/; do
277
+ [[ -d "$a" ]] || continue
278
+ aname="$(basename "$a")"
279
+ step "git mv plans/_archive/$aname" git mv "plans/_archive/$aname" "codeops/_archive/$aname"
280
+ done
281
+ for af in plans/_archive/*; do
282
+ [[ -f "$af" ]] || continue
283
+ step "git mv $af" git mv "$af" "codeops/_archive/$(basename "$af")"
284
+ done
285
+ fi
286
+ # Remove the now-empty flat directories left behind by the moves.
287
+ find plans requirements -type d -empty -delete 2>/dev/null || true
288
+
289
+ # Marker — written LAST, and only after every move step succeeded, so neither an interrupted
290
+ # nor a FAILED run can leave a false "already migrated" flag.
291
+ # integrationBranch names where features integrate and derived files (portfolio roadmap, AGENTS.md)
292
+ # regenerate — so parallel feature worktrees don't collide. Resolve the repo default: origin/HEAD,
293
+ # else the current branch, else main. Consumers auto-detect the same default when the key is absent.
294
+ integration_branch="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')"
295
+ [[ -n "$integration_branch" ]] || integration_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null)"
296
+ [[ -n "$integration_branch" ]] || integration_branch="main"
297
+ cat > codeops/codeops.json <<'JSON' || fail_apply "write codeops/codeops.json"
298
+ {
299
+ "schema": 1,
300
+ "mode": "strict",
301
+ "artifacts": {"layout": "nested", "root": "codeops"},
302
+ "quality": {
303
+ "independentReview": true,
304
+ "minimumReviewers": 1,
305
+ "stopOnMajorFinding": true
306
+ },
307
+ "metrics": {"enabled": false}
308
+ }
309
+ JSON
310
+ # Seeded portfolio roadmap — one row for the migrated feature. The roadmap skill refines the
311
+ # stage/progress on the next /update_roadmap; this is a valid starting point.
312
+ today="$(date '+%Y-%m-%d')"
313
+ cat > codeops/00-roadmap.md <<PORTFOLIO || fail_apply "write codeops/00-roadmap.md"
314
+ # Portfolio Roadmap: $(basename "$ROOT")
315
+
316
+ > **Status**: Active
317
+ > **Last Updated**: $today
318
+ > **Features**: 0 / 1 done
319
+ > **CodeOps Artifact Schema**: 1
320
+
321
+ ## Legend
322
+
323
+ ⬜ Backlog · 🔄 In progress · ✅ Done · ⛔ Blocked · ⏸️ Deferred · 📦 Archived
324
+
325
+ ## Features
326
+
327
+ | Feature | Roadmap | Stage Summary | Progress | Status | Last Updated |
328
+ |---------|---------|---------------|----------|--------|--------------|
329
+ | $slug | [→](features/$slug/00-roadmap.md) | migrated from flat layout | — | 🔄 | $today |
330
+
331
+ ## Archived
332
+
333
+ | Feature | Roadmap | Completed | Last Updated |
334
+ |---------|---------|-----------|--------------|
335
+ | — | — | — | — |
336
+ PORTFOLIO
337
+
338
+ # The presence marker is the commit point. Write it only after every required
339
+ # artifact exists, so a failed or interrupted run cannot report completion.
340
+ cat > codeops/.codeops.yml <<YML || fail_apply "write codeops/.codeops.yml"
341
+ # CodeOps layout marker. Presence of this file opts the repo into the nested layout.
342
+ # Sole writer: the setup_codeops skill (via codeops-migrate.sh). Schema: _shared/layout-convention.md
343
+ codeopsLayout: nested
344
+ layoutVersion: "3.0.0"
345
+ integrationBranch: ${integration_branch}
346
+ conventions:
347
+ rdIdScope: per-feature
348
+ taskIdPrefix: "T"
349
+ maintenanceFeature: _maintenance
350
+ archiveDir: codeops/_archive
351
+ YML
352
+
353
+ printf '\ncodeops-migrate: applied. Review with "git status" / "git diff --staged" and commit.\n'
354
+ printf ' %d warning(s) above may need a manual follow-up (e.g. source-relative links).\n' "${#warnings[@]}"
355
+ exit 0
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # codeops-roadmap-compact.sh — deterministic roadmap slimming engine.
4
+ #
5
+ # CodeOps Artifact Schema: 1
6
+ #
7
+ # The roadmap skill's `compact` action owns the JUDGMENT (rewriting a verbose table cell down to a
8
+ # terse phrase); this script owns the MECHANICAL, testable, safety-critical half (the same
9
+ # prose-vs-script division as codeops-roadmap-sync.sh / codeops-migrate.sh). For every roadmap in
10
+ # the repo it:
11
+ # - removes the `## Notes` running-log section (heading-anchored: from the `## Notes` line through
12
+ # the line before the next `## ` heading, or EOF) — the one destructive op;
13
+ # - flags oversized Tracker/Features table cells to stdout as a worklist for the `compact` action.
14
+ # It NEVER rewrites a table cell, never touches a header line or the Tracker/Features tables, and
15
+ # never executes repo data. Because deleting the Notes log is only reversible through git, apply
16
+ # refuses to run on a non-git or dirty tree (mirrors codeops-migrate.sh) so a clean, recoverable
17
+ # commit always precedes any mutation.
18
+ #
19
+ # Usage:
20
+ # codeops-roadmap-compact.sh # apply: strip Notes sections in place; print flagged cells
21
+ # codeops-roadmap-compact.sh --dry-run # preview strips + flags; change nothing
22
+ # codeops-roadmap-compact.sh --check # report only; exit 1 if any roadmap still carries a
23
+ # # `## Notes` section or an oversized cell
24
+ # codeops-roadmap-compact.sh -h|--help # this usage
25
+ # Exit: 0 = success / clean / no-op; 1 = refusal (not a git repo, or dirty tree) OR drift found
26
+ # under --check; 2 = bad usage; 3 = python3 unavailable.
27
+
28
+ set -uo pipefail
29
+
30
+ MODE="apply"
31
+ for arg in "$@"; do
32
+ case "$arg" in
33
+ --check) MODE="check" ;;
34
+ --dry-run) MODE="dry" ;;
35
+ -h|--help) grep -E '^# ' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
36
+ *) printf 'ERROR: unknown argument: %s\n' "$arg" >&2; exit 2 ;;
37
+ esac
38
+ done
39
+
40
+ command -v python3 >/dev/null 2>&1 || {
41
+ printf 'ERROR: python3 is required for roadmap parsing.\n' >&2
42
+ exit 3
43
+ }
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # Git safety. Apply DELETES the Notes log, so it must run on a clean, recoverable tree (mirrors
47
+ # codeops-migrate.sh:67-99). --check / --dry-run write nothing, so they may report on any tree.
48
+ # All modes operate from the git root when there is one, so relative roadmap paths are stable.
49
+ # -----------------------------------------------------------------------------
50
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
51
+ if [[ "$MODE" == "apply" ]]; then
52
+ if [[ -z "$ROOT" ]]; then
53
+ printf 'ERROR: not inside a git repository — compact deletes the Notes log, which is only\n' >&2
54
+ printf ' recoverable via git. Run it inside the repository.\n' >&2
55
+ exit 1
56
+ fi
57
+ cd "$ROOT" || { printf 'ERROR: cannot enter repo root %s\n' "$ROOT" >&2; exit 1; }
58
+ if [[ -n "$(git status --porcelain)" ]]; then
59
+ printf 'ERROR: working tree is dirty — compact must run on a clean tree so the removed notes\n' >&2
60
+ printf ' stay recoverable via git. Commit or stash your changes first, then re-run.\n' >&2
61
+ exit 1
62
+ fi
63
+ else
64
+ [[ -n "$ROOT" ]] && cd "$ROOT"
65
+ fi
66
+
67
+ # Layout detection — the canonical grep from _shared/layout-convention.md.
68
+ layout="flat"
69
+ if [[ -f codeops/.codeops.yml ]] && grep -Eq '^codeopsLayout:[[:space:]]*nested[[:space:]]*$' codeops/.codeops.yml; then
70
+ layout="nested"
71
+ fi
72
+
73
+ MODE="$MODE" LAYOUT="$layout" python3 - <<'PY'
74
+ import glob, os, re, sys
75
+
76
+ mode = os.environ["MODE"] # apply | check | dry
77
+ layout = os.environ["LAYOUT"] # flat | nested
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Roadmap discovery (current repo only; every 00-roadmap.md incl. _archive/).
81
+ # ---------------------------------------------------------------------------
82
+ def discover():
83
+ paths = []
84
+ if layout == "flat":
85
+ if os.path.isfile("plans/00-roadmap.md"):
86
+ paths.append("plans/00-roadmap.md")
87
+ paths += sorted(glob.glob("plans/_archive/*/00-roadmap.md"))
88
+ else:
89
+ if os.path.isfile("codeops/00-roadmap.md"):
90
+ paths.append("codeops/00-roadmap.md")
91
+ paths += sorted(glob.glob("codeops/features/*/00-roadmap.md"))
92
+ paths += sorted(glob.glob("codeops/_archive/*/00-roadmap.md"))
93
+ return paths
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Notes-section removal (heading-anchored). Removes the block from a `## Notes` heading through the
97
+ # line before the next `## ` heading (or EOF). Every other line — headers, `## Tracker`,
98
+ # `## Features`, `## Archived` — is left byte-for-byte unchanged. When something was removed the
99
+ # trailing blank run at EOF is collapsed to a single newline so the file stays a clean POSIX text
100
+ # file. Returns (new_text, removed_bool, removed_block_text).
101
+ # ---------------------------------------------------------------------------
102
+ NOTES_HEADING = re.compile(r'^##[ \t]+Notes[ \t]*$')
103
+ ANY_H2 = re.compile(r'^##[ \t]')
104
+
105
+ def strip_notes(text):
106
+ lines = text.splitlines(keepends=True)
107
+ out, removed_block = [], []
108
+ i, n, removed = 0, len(lines), False
109
+ while i < n:
110
+ bare = lines[i].rstrip('\r\n')
111
+ if NOTES_HEADING.match(bare):
112
+ removed = True
113
+ removed_block.append(lines[i]); i += 1
114
+ while i < n and not ANY_H2.match(lines[i].rstrip('\r\n')):
115
+ removed_block.append(lines[i]); i += 1
116
+ else:
117
+ out.append(lines[i]); i += 1
118
+ result = "".join(out)
119
+ if removed:
120
+ result = result.rstrip('\n') + '\n'
121
+ return result, removed, "".join(removed_block)
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Fat-cell flagging (report only — the engine never rewrites a cell). Parses every pipe table
125
+ # (a header row immediately followed by a `---` separator), then flags any DATA-row cell — skipping
126
+ # the first (id/feature) column — whose trimmed content is oversized: > 200 chars, or an embedded
127
+ # line break, or more than one sentence (>= 2 mid-cell `. ` / `; ` breaks). The column name comes
128
+ # from the table's own header row, so the check is layout- and rename-agnostic.
129
+ # ---------------------------------------------------------------------------
130
+ SEP_CELL = re.compile(r'^:?-{1,}:?$')
131
+ SENTENCE_BREAK = re.compile(r'[.;] ')
132
+
133
+ def split_cells(line):
134
+ return [c.strip() for c in line.strip().strip('|').split('|')]
135
+
136
+ def is_separator(line):
137
+ if not line.lstrip().startswith('|'):
138
+ return False
139
+ cells = split_cells(line)
140
+ return bool(cells) and all(SEP_CELL.match(c) for c in cells if c)
141
+
142
+ def cell_is_fat(cell):
143
+ if len(cell) > 200:
144
+ return True
145
+ if '<br' in cell.lower() or '\n' in cell:
146
+ return True
147
+ if len(SENTENCE_BREAK.findall(cell)) >= 2:
148
+ return True
149
+ return False
150
+
151
+ def flag_cells(text, path):
152
+ flags = []
153
+ lines = text.splitlines()
154
+ i, n = 0, len(lines)
155
+ while i < n:
156
+ line = lines[i]
157
+ if line.lstrip().startswith('|') and i + 1 < n and is_separator(lines[i + 1]):
158
+ header = split_cells(line)
159
+ j = i + 2
160
+ while j < n and lines[j].lstrip().startswith('|'):
161
+ if not is_separator(lines[j]):
162
+ cells = split_cells(lines[j])
163
+ row_id = cells[0] if cells else ''
164
+ for c in range(1, len(cells)):
165
+ if cell_is_fat(cells[c]):
166
+ col = header[c] if c < len(header) else f'col{c}'
167
+ flags.append(f"FLAG {path}:{row_id}:{col} ({len(cells[c])} chars)")
168
+ j += 1
169
+ i = j
170
+ else:
171
+ i += 1
172
+ return flags
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Drive the roadmaps per mode.
176
+ # ---------------------------------------------------------------------------
177
+ roadmaps = discover()
178
+ stripped = 0
179
+ flag_count = 0
180
+ drift = 0
181
+
182
+ for path in roadmaps:
183
+ try:
184
+ text = open(path, encoding='utf-8').read()
185
+ except OSError as e:
186
+ print(f"WARN: cannot read {path}: {e}", file=sys.stderr)
187
+ continue
188
+ new_text, removed, block = strip_notes(text)
189
+ flags = flag_cells(new_text, path)
190
+
191
+ if mode == 'apply':
192
+ if removed:
193
+ try:
194
+ open(path, 'w', encoding='utf-8').write(new_text)
195
+ except OSError as e:
196
+ print(f"ERROR: failed to write {path}: {e}", file=sys.stderr)
197
+ sys.exit(1)
198
+ print(f"STRIP {path}: ## Notes ({len(block.encode('utf-8'))} bytes)")
199
+ stripped += 1
200
+ for f in flags:
201
+ print(f)
202
+ flag_count += 1
203
+ elif mode == 'dry':
204
+ if removed:
205
+ print(f"WOULD-STRIP {path}: ## Notes ({len(block.encode('utf-8'))} bytes)")
206
+ for f in flags:
207
+ print(f)
208
+ else: # check
209
+ if removed:
210
+ print(f"NOTES {path}: ## Notes section present")
211
+ drift += 1
212
+ for f in flags:
213
+ print(f)
214
+ drift += 1
215
+
216
+ if mode == 'apply':
217
+ if stripped:
218
+ tail = f"; {flag_count} cell(s) flagged for trimming (see FLAG lines above)." if flag_count else "."
219
+ print(f"compact: stripped the Notes log from {stripped} roadmap(s){tail}")
220
+ else:
221
+ print("compact: all roadmaps already lean — nothing to do.")
222
+ sys.exit(0)
223
+ elif mode == 'dry':
224
+ print("(dry-run: nothing written)")
225
+ sys.exit(0)
226
+ else: # check
227
+ if drift:
228
+ print(f"compact: {drift} issue(s) found — run the roadmap compact action to slim these.")
229
+ sys.exit(1)
230
+ print("compact: all roadmaps already lean.")
231
+ sys.exit(0)
232
+ PY