tmux-agent 0.1.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 (161) hide show
  1. package/.codex/skills/speckit/SKILL.md +173 -0
  2. package/.codex/skills/speckit/assets/templates/checklist-template.md +49 -0
  3. package/.codex/skills/speckit/assets/templates/notes-entrypoints-template.md +11 -0
  4. package/.codex/skills/speckit/assets/templates/notes-questions-template.md +7 -0
  5. package/.codex/skills/speckit/assets/templates/notes-readme-template.md +36 -0
  6. package/.codex/skills/speckit/assets/templates/notes-session-template.md +21 -0
  7. package/.codex/skills/speckit/assets/templates/plan-template.md +126 -0
  8. package/.codex/skills/speckit/assets/templates/spec-template.md +135 -0
  9. package/.codex/skills/speckit/assets/templates/tasks-template.md +269 -0
  10. package/.codex/skills/speckit/references/acceptance.md +183 -0
  11. package/.codex/skills/speckit/references/analyze.md +186 -0
  12. package/.codex/skills/speckit/references/checklist.md +302 -0
  13. package/.codex/skills/speckit/references/clarify-auto.md +69 -0
  14. package/.codex/skills/speckit/references/clarify-detailed.md +78 -0
  15. package/.codex/skills/speckit/references/clarify.md +189 -0
  16. package/.codex/skills/speckit/references/constitution.md +90 -0
  17. package/.codex/skills/speckit/references/group.md +89 -0
  18. package/.codex/skills/speckit/references/implement-task.md +115 -0
  19. package/.codex/skills/speckit/references/implement.md +129 -0
  20. package/.codex/skills/speckit/references/notes.md +82 -0
  21. package/.codex/skills/speckit/references/plan-deep.md +87 -0
  22. package/.codex/skills/speckit/references/plan-from-questions.md +115 -0
  23. package/.codex/skills/speckit/references/plan-from-review.md +89 -0
  24. package/.codex/skills/speckit/references/plan.md +97 -0
  25. package/.codex/skills/speckit/references/review-plan.md +156 -0
  26. package/.codex/skills/speckit/references/specify.md +246 -0
  27. package/.codex/skills/speckit/references/tasks.md +155 -0
  28. package/.codex/skills/speckit/references/taskstoissues.md +33 -0
  29. package/.codex/skills/speckit/scripts/bash/check-prerequisites.sh +206 -0
  30. package/.codex/skills/speckit/scripts/bash/common.sh +191 -0
  31. package/.codex/skills/speckit/scripts/bash/create-new-feature.sh +259 -0
  32. package/.codex/skills/speckit/scripts/bash/extract-coded-points.sh +322 -0
  33. package/.codex/skills/speckit/scripts/bash/extract-spec-ids.sh +238 -0
  34. package/.codex/skills/speckit/scripts/bash/extract-tasks.sh +295 -0
  35. package/.codex/skills/speckit/scripts/bash/extract-user-stories.sh +312 -0
  36. package/.codex/skills/speckit/scripts/bash/setup-notes.sh +182 -0
  37. package/.codex/skills/speckit/scripts/bash/setup-plan.sh +110 -0
  38. package/.codex/skills/speckit/scripts/bash/show-todo-tasks.sh +257 -0
  39. package/.codex/skills/speckit/scripts/bash/spec-group-checklist.sh +402 -0
  40. package/.codex/skills/speckit/scripts/bash/spec-group-members.sh +215 -0
  41. package/.codex/skills/speckit/scripts/bash/spec-registry-graph.sh +399 -0
  42. package/.specify/memory/constitution.md +67 -0
  43. package/.specify/templates/agent-file-template.md +28 -0
  44. package/.specify/templates/checklist-template.md +49 -0
  45. package/.specify/templates/plan-template.md +126 -0
  46. package/.specify/templates/spec-template.md +135 -0
  47. package/.specify/templates/tasks-template.md +269 -0
  48. package/README.md +128 -0
  49. package/README.zh-CN.md +127 -0
  50. package/bun.lock +269 -0
  51. package/dist/cli/commands/codex/forkHome.js +88 -0
  52. package/dist/cli/commands/codex/send.js +55 -0
  53. package/dist/cli/commands/codex/sessionInfo.js +42 -0
  54. package/dist/cli/commands/codex/spawn.js +68 -0
  55. package/dist/cli/commands/find.js +26 -0
  56. package/dist/cli/commands/paneKill.js +33 -0
  57. package/dist/cli/commands/paneSpawn.js +40 -0
  58. package/dist/cli/commands/paneTitle.js +33 -0
  59. package/dist/cli/commands/read.js +34 -0
  60. package/dist/cli/commands/send.js +51 -0
  61. package/dist/cli/commands/snapshot.js +19 -0
  62. package/dist/cli/commands/ui/select.js +41 -0
  63. package/dist/cli/commands/windowKill.js +25 -0
  64. package/dist/cli/commands/windowLs.js +15 -0
  65. package/dist/cli/commands/windowNew.js +28 -0
  66. package/dist/cli/commands/windowRename.js +25 -0
  67. package/dist/cli/index.js +365 -0
  68. package/dist/cli/parse.js +39 -0
  69. package/dist/lib/codex/forkHome.js +101 -0
  70. package/dist/lib/codex/isCodexPane.js +55 -0
  71. package/dist/lib/codex/send.js +58 -0
  72. package/dist/lib/codex/sessionInfo.js +449 -0
  73. package/dist/lib/codex/spawn.js +246 -0
  74. package/dist/lib/contracts/types.js +2 -0
  75. package/dist/lib/fs/safeRm.js +32 -0
  76. package/dist/lib/io/readStdin.js +14 -0
  77. package/dist/lib/os/process.js +55 -0
  78. package/dist/lib/output/format.js +95 -0
  79. package/dist/lib/proc/lsof.js +42 -0
  80. package/dist/lib/proc/ps.js +60 -0
  81. package/dist/lib/targeting/errors.js +13 -0
  82. package/dist/lib/targeting/resolvePaneTarget.js +91 -0
  83. package/dist/lib/targeting/resolveWindowTarget.js +40 -0
  84. package/dist/lib/targeting/scope.js +58 -0
  85. package/dist/lib/tmux/capturePane.js +20 -0
  86. package/dist/lib/tmux/exec.js +66 -0
  87. package/dist/lib/tmux/paneOps.js +29 -0
  88. package/dist/lib/tmux/paste.js +23 -0
  89. package/dist/lib/tmux/sendKeys.js +47 -0
  90. package/dist/lib/tmux/session.js +29 -0
  91. package/dist/lib/tmux/snapshotPanes.js +46 -0
  92. package/dist/lib/tmux/snapshotWindows.js +24 -0
  93. package/dist/lib/tmux/windowOps.js +32 -0
  94. package/dist/lib/ui/popupSelect.js +432 -0
  95. package/dist/lib/ui/popupSupport.js +76 -0
  96. package/package.json +23 -0
  97. package/src/cli/commands/codex/forkHome.ts +141 -0
  98. package/src/cli/commands/codex/send.ts +83 -0
  99. package/src/cli/commands/codex/sessionInfo.ts +59 -0
  100. package/src/cli/commands/codex/spawn.ts +90 -0
  101. package/src/cli/commands/find.ts +40 -0
  102. package/src/cli/commands/paneKill.ts +49 -0
  103. package/src/cli/commands/paneSpawn.ts +53 -0
  104. package/src/cli/commands/paneTitle.ts +50 -0
  105. package/src/cli/commands/read.ts +48 -0
  106. package/src/cli/commands/send.ts +71 -0
  107. package/src/cli/commands/snapshot.ts +28 -0
  108. package/src/cli/commands/ui/select.ts +49 -0
  109. package/src/cli/commands/windowKill.ts +35 -0
  110. package/src/cli/commands/windowLs.ts +20 -0
  111. package/src/cli/commands/windowNew.ts +40 -0
  112. package/src/cli/commands/windowRename.ts +36 -0
  113. package/src/cli/index.ts +430 -0
  114. package/src/lib/codex/forkHome.ts +148 -0
  115. package/src/lib/codex/isCodexPane.ts +56 -0
  116. package/src/lib/codex/send.ts +84 -0
  117. package/src/lib/codex/sessionInfo.ts +521 -0
  118. package/src/lib/codex/spawn.ts +305 -0
  119. package/src/lib/contracts/types.ts +30 -0
  120. package/src/lib/fs/safeRm.ts +32 -0
  121. package/src/lib/io/readStdin.ts +11 -0
  122. package/src/lib/output/format.ts +105 -0
  123. package/src/lib/proc/lsof.ts +44 -0
  124. package/src/lib/proc/ps.ts +70 -0
  125. package/src/lib/targeting/errors.ts +25 -0
  126. package/src/lib/targeting/resolvePaneTarget.ts +106 -0
  127. package/src/lib/targeting/resolveWindowTarget.ts +45 -0
  128. package/src/lib/targeting/scope.ts +76 -0
  129. package/src/lib/tmux/capturePane.ts +21 -0
  130. package/src/lib/tmux/exec.ts +90 -0
  131. package/src/lib/tmux/paneOps.ts +35 -0
  132. package/src/lib/tmux/paste.ts +20 -0
  133. package/src/lib/tmux/sendKeys.ts +72 -0
  134. package/src/lib/tmux/session.ts +27 -0
  135. package/src/lib/tmux/snapshotPanes.ts +52 -0
  136. package/src/lib/tmux/snapshotWindows.ts +23 -0
  137. package/src/lib/tmux/windowOps.ts +43 -0
  138. package/src/lib/ui/popupSelect.ts +561 -0
  139. package/src/lib/ui/popupSupport.ts +84 -0
  140. package/tests/e2e/codexForkHome.test.ts +146 -0
  141. package/tests/e2e/codexSessionInfo.test.ts +112 -0
  142. package/tests/e2e/codexTuiSend.test.ts +68 -0
  143. package/tests/integration/codexSpawn.test.ts +113 -0
  144. package/tests/integration/paneOps.test.ts +60 -0
  145. package/tests/integration/sendRead.test.ts +52 -0
  146. package/tests/integration/snapshot.test.ts +39 -0
  147. package/tests/integration/tmuxHarness.ts +39 -0
  148. package/tests/integration/windowOps.test.ts +60 -0
  149. package/tests/unit/codexSend.test.ts +105 -0
  150. package/tests/unit/codexSessionInfo.test.ts +88 -0
  151. package/tests/unit/codexSpawn.test.ts +34 -0
  152. package/tests/unit/keys.test.ts +30 -0
  153. package/tests/unit/outputFormat.test.ts +52 -0
  154. package/tests/unit/popupSelect.test.ts +77 -0
  155. package/tests/unit/popupSupport.test.ts +109 -0
  156. package/tests/unit/resolvePaneTarget.test.ts +43 -0
  157. package/tests/unit/resolveWindowTarget.test.ts +36 -0
  158. package/tests/unit/safeRm.test.ts +41 -0
  159. package/tests/unit/scope.test.ts +57 -0
  160. package/tsconfig.json +14 -0
  161. package/vitest.config.ts +16 -0
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Create/refresh a "spec group" execution checklist under specs/<group>/checklists/.
4
+ #
5
+ # This script is intentionally light-weight:
6
+ # - It does NOT copy member tasks into the group spec.
7
+ # - It only creates an index-style checklist with links to member specs.
8
+ #
9
+ # Usage:
10
+ # ./spec-group-checklist.sh <group> <member...> [--name <name>] [--title <title>] [--force] [--json]
11
+ #
12
+ # Examples:
13
+ # ./spec-group-checklist.sh 046 045 039 --name m0-m1 --title "M0→M1"
14
+ # ./spec-group-checklist.sh 046-core-ng-roadmap 045-dual-kernel-contract 039-trait-converge-int-exec-evidence
15
+
16
+ set -euo pipefail
17
+
18
+ JSON_MODE=false
19
+ FORCE=false
20
+ DRY_RUN=false
21
+ FROM=""
22
+ GROUP=""
23
+ NAME=""
24
+ TITLE=""
25
+ MEMBERS=()
26
+
27
+ usage() {
28
+ cat << 'EOF'
29
+ Usage:
30
+ spec-group-checklist.sh <group> [<member...>] [--from registry] [--name <name>] [--title <title>] [--force] [--dry-run] [--json]
31
+
32
+ Args:
33
+ <group> Group spec id (e.g., 046 or 046-core-ng-roadmap)
34
+ <member...> Member spec ids (optional; if omitted, try deriving from the group spec)
35
+
36
+ Options:
37
+ --from <source> How to derive members when <member...> is omitted (default: registry)
38
+ Supported: registry
39
+ --name <name> Checklist filename (default: group.<members>.md)
40
+ --title <title> Checklist title (default: Spec Group Checklist: <group> ...)
41
+ --force Overwrite existing checklist file
42
+ --dry-run Resolve paths and print output, but do not write any file
43
+ --json Output JSON payload (also prints nothing else)
44
+
45
+ Examples:
46
+ spec-group-checklist.sh 046 045 039 --name m0-m1 --title "M0→M1"
47
+ EOF
48
+ }
49
+
50
+ while [[ $# -gt 0 ]]; do
51
+ case "$1" in
52
+ --json)
53
+ JSON_MODE=true
54
+ shift
55
+ ;;
56
+ --force)
57
+ FORCE=true
58
+ shift
59
+ ;;
60
+ --dry-run)
61
+ DRY_RUN=true
62
+ shift
63
+ ;;
64
+ --from)
65
+ if [[ $# -lt 2 || "${2:-}" == --* ]]; then
66
+ echo "ERROR: --from requires a value." >&2
67
+ exit 1
68
+ fi
69
+ FROM="$2"
70
+ shift 2
71
+ ;;
72
+ --name)
73
+ if [[ $# -lt 2 || "${2:-}" == --* ]]; then
74
+ echo "ERROR: --name requires a value." >&2
75
+ exit 1
76
+ fi
77
+ NAME="$2"
78
+ shift 2
79
+ ;;
80
+ --title)
81
+ if [[ $# -lt 2 || "${2:-}" == --* ]]; then
82
+ echo "ERROR: --title requires a value." >&2
83
+ exit 1
84
+ fi
85
+ TITLE="$2"
86
+ shift 2
87
+ ;;
88
+ --help|-h)
89
+ usage
90
+ exit 0
91
+ ;;
92
+ --*)
93
+ echo "ERROR: Unknown option '$1'. Use --help for usage information." >&2
94
+ exit 1
95
+ ;;
96
+ *)
97
+ if [[ -z "$GROUP" ]]; then
98
+ GROUP="$1"
99
+ else
100
+ MEMBERS+=("$1")
101
+ fi
102
+ shift
103
+ ;;
104
+ esac
105
+ done
106
+
107
+ if [[ -z "$GROUP" ]]; then
108
+ echo "ERROR: Missing <group>." >&2
109
+ usage >&2
110
+ exit 1
111
+ fi
112
+
113
+ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
114
+ CHECK="$SCRIPT_DIR/check-prerequisites.sh"
115
+
116
+ group_paths_json="$("$CHECK" --json --paths-only --feature "$GROUP")"
117
+ GROUP_DIR="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["FEATURE_DIR"])' <<<"$group_paths_json")"
118
+ GROUP_BRANCH="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["BRANCH"])' <<<"$group_paths_json")"
119
+
120
+ if [[ ! -d "$GROUP_DIR" ]]; then
121
+ echo "ERROR: Group feature directory not found: $GROUP_DIR" >&2
122
+ exit 1
123
+ fi
124
+
125
+ DERIVED_FROM="explicit"
126
+ if [[ ${#MEMBERS[@]} -eq 0 ]]; then
127
+ source="${FROM:-registry}"
128
+ case "$source" in
129
+ registry)
130
+ registry_json="$GROUP_DIR/spec-registry.json"
131
+ registry_md="$GROUP_DIR/spec-registry.md"
132
+ if [[ -f "$registry_json" ]]; then
133
+ registry_file="$registry_json"
134
+ elif [[ -f "$registry_md" ]]; then
135
+ registry_file="$registry_md"
136
+ else
137
+ echo "ERROR: Missing member specs and no spec-registry.json/spec-registry.md found at $GROUP_DIR" >&2
138
+ exit 1
139
+ fi
140
+ mapfile -t MEMBERS < <(python3 - "$registry_file" << 'PY'
141
+ from __future__ import annotations
142
+
143
+ import json
144
+ import re
145
+ import sys
146
+ from pathlib import Path
147
+
148
+ registry = Path(sys.argv[1])
149
+ group_dir = registry.parent
150
+ repo_root = group_dir.parent.parent # .../specs/<group>
151
+ specs_dir = repo_root / "specs"
152
+
153
+ ordered: list[str] = []
154
+ seen: set[str] = set()
155
+
156
+ group_prefix_match = re.match(r"^(?P<prefix>\d{3})-", group_dir.name)
157
+ group_prefix = group_prefix_match.group("prefix") if group_prefix_match else None
158
+
159
+ def push_candidate(raw: str) -> None:
160
+ raw = raw.strip()
161
+ if raw.startswith("specs/"):
162
+ raw = raw[len("specs/") :]
163
+ raw = raw.strip().strip("/")
164
+
165
+ m = re.match(r"^(?P<prefix>\d{3})(?:-|$)", raw)
166
+ if not m:
167
+ return
168
+
169
+ prefix = m.group("prefix")
170
+ if group_prefix and prefix == group_prefix:
171
+ return
172
+ if prefix in seen:
173
+ return
174
+
175
+ matches = list(specs_dir.glob(f"{prefix}-*"))
176
+ if not matches:
177
+ return
178
+
179
+ seen.add(prefix)
180
+ ordered.append(prefix)
181
+
182
+
183
+ if registry.suffix == ".json":
184
+ data = json.loads(registry.read_text(encoding="utf-8", errors="replace"))
185
+ entries = data.get("entries", [])
186
+ for entry in entries:
187
+ if isinstance(entry, str):
188
+ push_candidate(entry)
189
+ continue
190
+ if isinstance(entry, dict):
191
+ candidate = entry.get("dir") or entry.get("id") or ""
192
+ push_candidate(str(candidate))
193
+ continue
194
+ else:
195
+ text = registry.read_text(encoding="utf-8", errors="replace")
196
+ lines = text.splitlines()
197
+ for line in lines:
198
+ if not line.lstrip().startswith("|"):
199
+ continue
200
+
201
+ parts = line.split("|")
202
+ if len(parts) < 3:
203
+ continue
204
+
205
+ first_cell = parts[1].strip()
206
+ if not first_cell.startswith("`") or "`" not in first_cell[1:]:
207
+ continue
208
+
209
+ inner = first_cell.strip().strip("`").strip()
210
+ push_candidate(inner)
211
+
212
+ for prefix in ordered:
213
+ print(prefix)
214
+ PY
215
+ )
216
+ DERIVED_FROM="registry"
217
+ ;;
218
+ *)
219
+ echo "ERROR: Unsupported --from value: $source (supported: registry)" >&2
220
+ exit 1
221
+ ;;
222
+ esac
223
+ fi
224
+
225
+ if [[ ${#MEMBERS[@]} -eq 0 ]]; then
226
+ echo "ERROR: No members resolved for group $GROUP_BRANCH (derivedFrom=$DERIVED_FROM)." >&2
227
+ exit 1
228
+ fi
229
+
230
+ if [[ -z "$NAME" ]]; then
231
+ if [[ "$DERIVED_FROM" != "explicit" ]]; then
232
+ NAME="group.${DERIVED_FROM}.md"
233
+ else
234
+ prefixes=()
235
+ for m in "${MEMBERS[@]}"; do
236
+ if [[ "$m" =~ ^([0-9]{3}) ]]; then
237
+ prefixes+=("${BASH_REMATCH[1]}")
238
+ else
239
+ prefixes+=("$m")
240
+ fi
241
+ done
242
+ joined="$(IFS=-; echo "${prefixes[*]}")"
243
+ NAME="group.${joined}.md"
244
+ fi
245
+ fi
246
+
247
+ if [[ "$NAME" != *.md ]]; then
248
+ NAME="${NAME}.md"
249
+ fi
250
+
251
+ CHECKLIST_DIR="$GROUP_DIR/checklists"
252
+ mkdir -p "$CHECKLIST_DIR"
253
+
254
+ OUT_FILE="$CHECKLIST_DIR/$NAME"
255
+
256
+ if [[ "$DRY_RUN" == true ]]; then
257
+ if $JSON_MODE; then
258
+ python3 - "$OUT_FILE" "$GROUP_BRANCH" "$GROUP_DIR" "$DERIVED_FROM" "${MEMBERS[@]}" << 'PY'
259
+ from __future__ import annotations
260
+
261
+ import json
262
+ import sys
263
+
264
+ out_file = sys.argv[1]
265
+ group_branch = sys.argv[2]
266
+ group_dir = sys.argv[3]
267
+ derived_from = sys.argv[4]
268
+ members = sys.argv[5:]
269
+
270
+ print(
271
+ json.dumps(
272
+ {
273
+ "dryRun": True,
274
+ "group": group_branch,
275
+ "groupDir": group_dir,
276
+ "derivedFrom": derived_from,
277
+ "members": members,
278
+ "checklistFile": out_file,
279
+ },
280
+ ensure_ascii=False,
281
+ )
282
+ )
283
+ PY
284
+ else
285
+ echo "dry-run: would write $OUT_FILE"
286
+ fi
287
+ exit 0
288
+ fi
289
+
290
+ if [[ -f "$OUT_FILE" && "$FORCE" != true ]]; then
291
+ echo "ERROR: Checklist already exists: $OUT_FILE (use --force to overwrite or choose another --name)" >&2
292
+ exit 1
293
+ fi
294
+
295
+ python3 - "$CHECK" "$GROUP_BRANCH" "$GROUP_DIR" "$OUT_FILE" "$TITLE" "$DERIVED_FROM" "${MEMBERS[@]}" << 'PY'
296
+ from __future__ import annotations
297
+
298
+ import json
299
+ import subprocess
300
+ import sys
301
+ from datetime import datetime
302
+ from pathlib import Path
303
+
304
+
305
+ CHECK = sys.argv[1]
306
+ GROUP_BRANCH = sys.argv[2]
307
+ GROUP_DIR = Path(sys.argv[3])
308
+ OUT_FILE = Path(sys.argv[4])
309
+ TITLE = sys.argv[5].strip()
310
+ DERIVED_FROM = sys.argv[6].strip()
311
+ MEMBERS = sys.argv[7:]
312
+
313
+
314
+ def run_paths(feature: str) -> dict:
315
+ out = subprocess.check_output([CHECK, "--json", "--paths-only", "--feature", feature], text=True)
316
+ return json.loads(out)
317
+
318
+
319
+ def rel(p: Path) -> str:
320
+ try:
321
+ return str(p.relative_to(Path.cwd()))
322
+ except Exception:
323
+ return str(p)
324
+
325
+
326
+ members_info: list[dict] = []
327
+ for member in MEMBERS:
328
+ paths = run_paths(member)
329
+ feature_dir = Path(paths["FEATURE_DIR"])
330
+ members_info.append(
331
+ {
332
+ "input": member,
333
+ "id": paths["BRANCH"],
334
+ "dir": feature_dir,
335
+ "spec": feature_dir / "spec.md",
336
+ "plan": feature_dir / "plan.md",
337
+ "tasks": feature_dir / "tasks.md",
338
+ "quickstart": feature_dir / "quickstart.md",
339
+ }
340
+ )
341
+
342
+
343
+ auto_title = f"Spec Group Checklist: {GROUP_BRANCH} · " + " + ".join([m["id"] for m in members_info])
344
+ final_title = TITLE or auto_title
345
+
346
+
347
+ lines: list[str] = []
348
+ lines.append(f"# {final_title}")
349
+ lines.append("")
350
+ lines.append(f"**Group**: `{rel(GROUP_DIR)}`")
351
+ lines.append(f"**Derived From**: `{DERIVED_FROM}`")
352
+ lines.append(f"**Members**: " + ", ".join([f"`{rel(m['dir'])}`" for m in members_info]))
353
+ lines.append(f"**Created**: {datetime.now().strftime('%Y-%m-%d')}")
354
+ lines.append("")
355
+ lines.append("> This file is an **execution index checklist**: it only provides links and gate summaries, and does not copy member implementation tasks (avoid parallel truth sources).")
356
+ lines.append("")
357
+ lines.append("## Members")
358
+ lines.append("")
359
+
360
+ for m in members_info:
361
+ lines.append(
362
+ f"- [ ] `{m['id']}` meets its tasks/quickstart criteria (entry points: `{rel(m['tasks'])}`, `{rel(m['quickstart'])}`)"
363
+ )
364
+
365
+ lines.append("")
366
+ lines.append("## Notes")
367
+ lines.append("")
368
+ lines.append("- For cross-spec acceptance, prefer: `$speckit acceptance <member...>` (multi-spec mode).")
369
+ lines.append("- To view a summary of member task progress, use: `extract-tasks.sh --json --feature ...`.")
370
+ lines.append("")
371
+
372
+ OUT_FILE.parent.mkdir(parents=True, exist_ok=True)
373
+ OUT_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
374
+ PY
375
+
376
+ if $JSON_MODE; then
377
+ python3 - "$OUT_FILE" "$GROUP_BRANCH" "$GROUP_DIR" "${MEMBERS[@]}" << 'PY'
378
+ from __future__ import annotations
379
+
380
+ import json
381
+ import sys
382
+
383
+ out_file = sys.argv[1]
384
+ group_branch = sys.argv[2]
385
+ group_dir = sys.argv[3]
386
+ members = sys.argv[4:]
387
+
388
+ print(
389
+ json.dumps(
390
+ {
391
+ "group": group_branch,
392
+ "groupDir": group_dir,
393
+ "members": members,
394
+ "checklistFile": out_file,
395
+ },
396
+ ensure_ascii=False,
397
+ )
398
+ )
399
+ PY
400
+ else
401
+ echo "wrote $OUT_FILE"
402
+ fi
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Resolve "spec group" members from specs/<group>/spec-registry.md.
4
+ #
5
+ # Usage:
6
+ # spec-group-members.sh <group> [--from registry] [--json]
7
+ #
8
+ # Examples:
9
+ # spec-group-members.sh 046 --json
10
+
11
+ set -euo pipefail
12
+
13
+ JSON_MODE=false
14
+ FROM=""
15
+ GROUP=""
16
+
17
+ usage() {
18
+ cat << 'EOF'
19
+ Usage:
20
+ spec-group-members.sh <group> [--from registry] [--json]
21
+
22
+ Args:
23
+ <group> Group spec id (e.g., 046 or 046-core-ng-roadmap)
24
+
25
+ Options:
26
+ --from <source> How to derive members (default: registry)
27
+ Supported: registry
28
+ --json Output JSON payload (default: plain lines)
29
+ --help, -h Show help
30
+
31
+ Output:
32
+ Plain mode: one member prefix per line (e.g., 045)
33
+ JSON mode: {"group":"046-core-ng-roadmap","groupDir":"...","derivedFrom":"registry","registryFile":"...","members":[...]}
34
+ EOF
35
+ }
36
+
37
+ while [[ $# -gt 0 ]]; do
38
+ case "$1" in
39
+ --json)
40
+ JSON_MODE=true
41
+ shift
42
+ ;;
43
+ --from)
44
+ if [[ $# -lt 2 || "${2:-}" == --* ]]; then
45
+ echo "ERROR: --from requires a value." >&2
46
+ exit 1
47
+ fi
48
+ FROM="$2"
49
+ shift 2
50
+ ;;
51
+ --help|-h)
52
+ usage
53
+ exit 0
54
+ ;;
55
+ --*)
56
+ echo "ERROR: Unknown option '$1'. Use --help for usage information." >&2
57
+ exit 1
58
+ ;;
59
+ *)
60
+ if [[ -z "$GROUP" ]]; then
61
+ GROUP="$1"
62
+ else
63
+ echo "ERROR: Unexpected extra argument '$1'. Use --help for usage information." >&2
64
+ exit 1
65
+ fi
66
+ shift
67
+ ;;
68
+ esac
69
+ done
70
+
71
+ if [[ -z "$GROUP" ]]; then
72
+ echo "ERROR: Missing <group>." >&2
73
+ usage >&2
74
+ exit 1
75
+ fi
76
+
77
+ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
78
+ CHECK="$SCRIPT_DIR/check-prerequisites.sh"
79
+
80
+ group_paths_json="$("$CHECK" --json --paths-only --feature "$GROUP")"
81
+ GROUP_DIR="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["FEATURE_DIR"])' <<<"$group_paths_json")"
82
+ GROUP_BRANCH="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["BRANCH"])' <<<"$group_paths_json")"
83
+
84
+ source="${FROM:-registry}"
85
+ case "$source" in
86
+ registry)
87
+ registry_json="$GROUP_DIR/spec-registry.json"
88
+ registry_md="$GROUP_DIR/spec-registry.md"
89
+ if [[ -f "$registry_json" ]]; then
90
+ registry_file="$registry_json"
91
+ elif [[ -f "$registry_md" ]]; then
92
+ registry_file="$registry_md"
93
+ else
94
+ echo "ERROR: Missing spec-registry.json/spec-registry.md at $GROUP_DIR" >&2
95
+ exit 1
96
+ fi
97
+ mapfile -t MEMBERS < <(python3 - "$registry_file" << 'PY'
98
+ from __future__ import annotations
99
+
100
+ import json
101
+ import re
102
+ import sys
103
+ from pathlib import Path
104
+
105
+ registry = Path(sys.argv[1])
106
+ group_dir = registry.parent
107
+ repo_root = group_dir.parent.parent # .../specs/<group>
108
+ specs_dir = repo_root / "specs"
109
+
110
+ ordered: list[str] = []
111
+ seen: set[str] = set()
112
+
113
+ group_prefix_match = re.match(r"^(?P<prefix>\d{3})-", group_dir.name)
114
+ group_prefix = group_prefix_match.group("prefix") if group_prefix_match else None
115
+
116
+ def push_candidate(raw: str) -> None:
117
+ raw = raw.strip()
118
+ if raw.startswith("specs/"):
119
+ raw = raw[len("specs/") :]
120
+ raw = raw.strip().strip("/")
121
+
122
+ m = re.match(r"^(?P<prefix>\d{3})(?:-|$)", raw)
123
+ if not m:
124
+ return
125
+
126
+ prefix = m.group("prefix")
127
+ if group_prefix and prefix == group_prefix:
128
+ return
129
+ if prefix in seen:
130
+ return
131
+
132
+ matches = list(specs_dir.glob(f"{prefix}-*"))
133
+ if not matches:
134
+ return
135
+
136
+ seen.add(prefix)
137
+ ordered.append(prefix)
138
+
139
+
140
+ if registry.suffix == ".json":
141
+ data = json.loads(registry.read_text(encoding="utf-8", errors="replace"))
142
+ entries = data.get("entries", [])
143
+ for entry in entries:
144
+ if isinstance(entry, str):
145
+ push_candidate(entry)
146
+ continue
147
+ if isinstance(entry, dict):
148
+ candidate = entry.get("dir") or entry.get("id") or ""
149
+ push_candidate(str(candidate))
150
+ continue
151
+ else:
152
+ text = registry.read_text(encoding="utf-8", errors="replace")
153
+ lines = text.splitlines()
154
+ for line in lines:
155
+ if not line.lstrip().startswith("|"):
156
+ continue
157
+
158
+ parts = line.split("|")
159
+ if len(parts) < 3:
160
+ continue
161
+
162
+ first_cell = parts[1].strip()
163
+ if not first_cell.startswith("`") or "`" not in first_cell[1:]:
164
+ continue
165
+
166
+ inner = first_cell.strip().strip("`").strip()
167
+ push_candidate(inner)
168
+
169
+ for prefix in ordered:
170
+ print(prefix)
171
+ PY
172
+ )
173
+ ;;
174
+ *)
175
+ echo "ERROR: Unsupported --from value: $source (supported: registry)" >&2
176
+ exit 1
177
+ ;;
178
+ esac
179
+
180
+ if [[ ${#MEMBERS[@]} -eq 0 ]]; then
181
+ echo "ERROR: No members resolved for group $GROUP_BRANCH (derivedFrom=$source)." >&2
182
+ exit 1
183
+ fi
184
+
185
+ if $JSON_MODE; then
186
+ python3 - "$GROUP_BRANCH" "$GROUP_DIR" "$source" "$registry_file" "${MEMBERS[@]}" << 'PY'
187
+ from __future__ import annotations
188
+
189
+ import json
190
+ import sys
191
+
192
+ group = sys.argv[1]
193
+ group_dir = sys.argv[2]
194
+ derived_from = sys.argv[3]
195
+ registry_file = sys.argv[4]
196
+ members = sys.argv[5:]
197
+
198
+ print(
199
+ json.dumps(
200
+ {
201
+ "group": group,
202
+ "groupDir": group_dir,
203
+ "derivedFrom": derived_from,
204
+ "registryFile": registry_file,
205
+ "members": members,
206
+ },
207
+ ensure_ascii=False,
208
+ )
209
+ )
210
+ PY
211
+ else
212
+ for m in "${MEMBERS[@]}"; do
213
+ echo "$m"
214
+ done
215
+ fi