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.
- package/CHANGELOG.md +179 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/_shared/auto-design.md +129 -0
- package/_shared/layout-convention.md +198 -0
- package/_shared/quality-profile.md +134 -0
- package/_shared/recommendation-hardening.md +166 -0
- package/_shared/scope-expansion-control.md +176 -0
- package/_shared/spec-first-ordering.md +79 -0
- package/_shared/zero-ambiguity-gate.md +311 -0
- package/agent-templates/codebase-scout.md +17 -0
- package/agent-templates/concurrency-auditor.md +5 -0
- package/agent-templates/design-challenger.md +26 -0
- package/agent-templates/financial-integrity-auditor.md +5 -0
- package/agent-templates/perf-auditor.md +23 -0
- package/agent-templates/phase-reviewer.md +54 -0
- package/agent-templates/plan-task-executor-opus.md +46 -0
- package/agent-templates/plan-task-executor.md +43 -0
- package/agent-templates/preflight-auditor.md +45 -0
- package/agent-templates/security-auditor.md +42 -0
- package/agent-templates/semantics-reviewer.md +5 -0
- package/agent-templates/spec-test-author.md +29 -0
- package/agents/concurrency-auditor.md +15 -0
- package/agents/correctness-reviewer.md +66 -0
- package/agents/demanding-executor.md +58 -0
- package/agents/design-challenger.md +38 -0
- package/agents/executor.md +55 -0
- package/agents/explorer.md +29 -0
- package/agents/financial-integrity-auditor.md +15 -0
- package/agents/performance-auditor.md +35 -0
- package/agents/preflight-auditor.md +57 -0
- package/agents/security-auditor.md +54 -0
- package/agents/semantics-reviewer.md +15 -0
- package/agents/spec-test-author.md +41 -0
- package/bin/codeops-worktree +244 -0
- package/bin/index.mjs +106 -0
- package/bin/install-agents.mjs +453 -0
- package/bin/install-skills.mjs +466 -0
- package/bin/lib/opencode-install.mjs +185 -0
- package/install.sh +55 -0
- package/package.json +73 -0
- package/plugin/index.ts +181 -0
- package/references/domains/compiler-and-language.md +28 -0
- package/references/domains/data-and-migration.md +22 -0
- package/references/domains/distributed-and-concurrent.md +26 -0
- package/references/domains/financial-system.md +28 -0
- package/references/domains/selection.md +19 -0
- package/references/domains/web-application.md +23 -0
- package/schemas/codeops-config.schema.json +56 -0
- package/scripts/check-version.mjs +163 -0
- package/scripts/codeops-migrate.sh +355 -0
- package/scripts/codeops-roadmap-compact.sh +232 -0
- package/scripts/codeops-roadmap-sync.sh +275 -0
- package/scripts/codeops_outcomes.py +155 -0
- package/scripts/codeops_plan.py +239 -0
- package/scripts/codeops_plan_migrate.py +318 -0
- package/scripts/codeops_worktree_snapshot.py +99 -0
- package/scripts/install_agents.py +288 -0
- package/scripts/release.mjs +533 -0
- package/skills/analyze-project/SKILL.md +28 -0
- package/skills/clean-comments/SKILL.md +22 -0
- package/skills/exec-plan/SKILL.md +267 -0
- package/skills/exec-plan/commit-modes.md +113 -0
- package/skills/exec-plan/execution-protocol.md +471 -0
- package/skills/git-commit/SKILL.md +35 -0
- package/skills/github-issues/SKILL.md +38 -0
- package/skills/grill-me/SKILL.md +342 -0
- package/skills/make-plan/SKILL.md +282 -0
- package/skills/make-plan/quality-checklist.md +96 -0
- package/skills/make-plan/templates.md +535 -0
- package/skills/make-plan/zero-ambiguity-gate.md +19 -0
- package/skills/make-requirements/SKILL.md +268 -0
- package/skills/make-requirements/discovery-phases.md +255 -0
- package/skills/make-requirements/review-and-add.md +73 -0
- package/skills/make-requirements/templates.md +296 -0
- package/skills/make-requirements/zero-ambiguity-gate.md +18 -0
- package/skills/outcome-review/SKILL.md +34 -0
- package/skills/preflight/SKILL.md +310 -0
- package/skills/preflight/dimensions.md +181 -0
- package/skills/preflight/report-format.md +300 -0
- package/skills/retro-requirements/SKILL.md +218 -0
- package/skills/retro-requirements/confidence-classification.md +45 -0
- package/skills/retro-requirements/phases.md +609 -0
- package/skills/retro-requirements/triage-gate.md +135 -0
- package/skills/roadmap/SKILL.md +381 -0
- package/skills/roadmap/stage-hooks.md +80 -0
- package/skills/roadmap/template.md +200 -0
- package/skills/setup-codeops/SKILL.md +94 -0
- package/skills/setup-codeops/migration.md +106 -0
- package/skills/setup-codeops/scaffold.md +99 -0
- package/skills/setup-routing/SKILL.md +102 -0
- package/skills/setup-routing/routing.md +44 -0
- package/skills/techdocs/SKILL.md +199 -0
- package/skills/techdocs/authoring-and-update.md +178 -0
- package/skills/techdocs/templates.md +655 -0
- package/skills/techdocs/vitepress-setup.md +143 -0
- package/skills/upgrade-plan/SKILL.md +75 -0
- package/skills/upgrade-plan/content-quality-gate.md +35 -0
- package/skills/upgrade-plan/upgrade-checklists.md +107 -0
- package/standards/coding-standards-full.md +124 -0
- package/standards/coding-standards.md +64 -0
- 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
|