okstra 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 (106) hide show
  1. package/README.md +36 -0
  2. package/bin/okstra +62 -0
  3. package/package.json +30 -0
  4. package/runtime/.gitkeep +0 -0
  5. package/runtime/BUILD.json +5 -0
  6. package/runtime/agents/SKILL.md +243 -0
  7. package/runtime/agents/TODO.md +168 -0
  8. package/runtime/agents/workers/claude-worker.md +106 -0
  9. package/runtime/agents/workers/codex-worker.md +179 -0
  10. package/runtime/agents/workers/gemini-worker.md +179 -0
  11. package/runtime/agents/workers/report-writer-worker.md +116 -0
  12. package/runtime/bin/okstra-central.sh +152 -0
  13. package/runtime/bin/okstra-codex-exec.sh +53 -0
  14. package/runtime/bin/okstra-error-log.py +295 -0
  15. package/runtime/bin/okstra-gemini-exec.sh +55 -0
  16. package/runtime/bin/okstra-token-usage.py +46 -0
  17. package/runtime/bin/okstra.sh +162 -0
  18. package/runtime/prompts/launch.template.md +52 -0
  19. package/runtime/prompts/profiles/error-analysis.md +43 -0
  20. package/runtime/prompts/profiles/final-verification.md +37 -0
  21. package/runtime/prompts/profiles/implementation-planning.md +85 -0
  22. package/runtime/prompts/profiles/implementation.md +71 -0
  23. package/runtime/prompts/profiles/requirements-discovery.md +43 -0
  24. package/runtime/python/lib/okstra/cli.sh +227 -0
  25. package/runtime/python/lib/okstra/globals.sh +157 -0
  26. package/runtime/python/lib/okstra/interactive.sh +411 -0
  27. package/runtime/python/lib/okstra/project-resolver.sh +57 -0
  28. package/runtime/python/lib/okstra/usage.sh +98 -0
  29. package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
  30. package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
  31. package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
  32. package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
  33. package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
  34. package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
  35. package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
  36. package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
  37. package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
  38. package/runtime/python/lib/okstra-ctl/main.sh +41 -0
  39. package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
  40. package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
  41. package/runtime/python/okstra_ctl/__init__.py +125 -0
  42. package/runtime/python/okstra_ctl/backfill.py +253 -0
  43. package/runtime/python/okstra_ctl/batch.py +62 -0
  44. package/runtime/python/okstra_ctl/ids.py +84 -0
  45. package/runtime/python/okstra_ctl/index.py +216 -0
  46. package/runtime/python/okstra_ctl/invocation.py +49 -0
  47. package/runtime/python/okstra_ctl/jsonl.py +84 -0
  48. package/runtime/python/okstra_ctl/listing.py +156 -0
  49. package/runtime/python/okstra_ctl/locks.py +42 -0
  50. package/runtime/python/okstra_ctl/material.py +62 -0
  51. package/runtime/python/okstra_ctl/models.py +63 -0
  52. package/runtime/python/okstra_ctl/path_resolve.py +40 -0
  53. package/runtime/python/okstra_ctl/paths.py +251 -0
  54. package/runtime/python/okstra_ctl/project_meta.py +51 -0
  55. package/runtime/python/okstra_ctl/reconcile.py +166 -0
  56. package/runtime/python/okstra_ctl/render.py +1065 -0
  57. package/runtime/python/okstra_ctl/resolver.py +54 -0
  58. package/runtime/python/okstra_ctl/run.py +674 -0
  59. package/runtime/python/okstra_ctl/run_context.py +166 -0
  60. package/runtime/python/okstra_ctl/seeding.py +97 -0
  61. package/runtime/python/okstra_ctl/sequence.py +53 -0
  62. package/runtime/python/okstra_ctl/session.py +33 -0
  63. package/runtime/python/okstra_ctl/tmux.py +27 -0
  64. package/runtime/python/okstra_ctl/workers.py +64 -0
  65. package/runtime/python/okstra_ctl/workflow.py +182 -0
  66. package/runtime/python/okstra_project/__init__.py +41 -0
  67. package/runtime/python/okstra_project/resolver.py +126 -0
  68. package/runtime/python/okstra_project/state.py +170 -0
  69. package/runtime/python/okstra_token_usage/__init__.py +26 -0
  70. package/runtime/python/okstra_token_usage/blocks.py +62 -0
  71. package/runtime/python/okstra_token_usage/claude.py +97 -0
  72. package/runtime/python/okstra_token_usage/cli.py +84 -0
  73. package/runtime/python/okstra_token_usage/codex.py +80 -0
  74. package/runtime/python/okstra_token_usage/collect.py +161 -0
  75. package/runtime/python/okstra_token_usage/gemini.py +77 -0
  76. package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
  77. package/runtime/python/okstra_token_usage/paths.py +22 -0
  78. package/runtime/python/okstra_token_usage/pricing.py +71 -0
  79. package/runtime/python/okstra_token_usage/report.py +64 -0
  80. package/runtime/templates/prd/brief.template.md +273 -0
  81. package/runtime/templates/project-docs/task-index.template.md +65 -0
  82. package/runtime/templates/reports/error-analysis-input.template.md +80 -0
  83. package/runtime/templates/reports/final-report.template.md +167 -0
  84. package/runtime/templates/reports/final-verification-input.template.md +67 -0
  85. package/runtime/templates/reports/implementation-input.template.md +81 -0
  86. package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
  87. package/runtime/templates/reports/quick-input.template.md +64 -0
  88. package/runtime/templates/reports/schedule.template.md +168 -0
  89. package/runtime/templates/reports/settings.template.json +101 -0
  90. package/runtime/templates/reports/task-brief.template.md +165 -0
  91. package/runtime/validators/lib/common.sh +44 -0
  92. package/runtime/validators/lib/fixtures.sh +322 -0
  93. package/runtime/validators/lib/paths.sh +44 -0
  94. package/runtime/validators/lib/runners.sh +140 -0
  95. package/runtime/validators/lib/summary.sh +15 -0
  96. package/runtime/validators/lib/validate-assets.sh +44 -0
  97. package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
  98. package/runtime/validators/lib/validate-tasks.sh +335 -0
  99. package/runtime/validators/validate-run.py +568 -0
  100. package/runtime/validators/validate-schedule.py +665 -0
  101. package/runtime/validators/validate-workflow.sh +190 -0
  102. package/src/doctor.mjs +127 -0
  103. package/src/install.mjs +355 -0
  104. package/src/paths.mjs +132 -0
  105. package/src/uninstall.mjs +122 -0
  106. package/src/version.mjs +20 -0
@@ -0,0 +1,411 @@
1
+ # shellcheck shell=bash
2
+
3
+ trim_whitespace() {
4
+ local value="${1-}"
5
+ value="${value#"${value%%[![:space:]]*}"}"
6
+ value="${value%"${value##*[![:space:]]}"}"
7
+ printf '%s' "$value"
8
+ }
9
+
10
+ is_interactive_session() {
11
+ [[ -t 0 && -t 1 ]]
12
+ }
13
+
14
+ split_task_key() {
15
+ local raw_key=""
16
+ raw_key="$(trim_whitespace "${TASK_KEY_INPUT-}")"
17
+
18
+ if [[ -z "$raw_key" ]]; then
19
+ return 0
20
+ fi
21
+
22
+ if [[ "$raw_key" != *:*:* ]]; then
23
+ printf 'invalid --task-key value: %s (expected <project-id>:<task-group>:<task-id>)\n' "$raw_key" >&2
24
+ exit 1
25
+ fi
26
+
27
+ local key_proj="${raw_key%%:*}"
28
+ local key_rest="${raw_key#*:}"
29
+ local key_group="${key_rest%%:*}"
30
+ local key_id="${key_rest#*:}"
31
+
32
+ if [[ -z "$key_proj" || -z "$key_group" || -z "$key_id" || "$key_id" == *:* ]]; then
33
+ printf 'invalid --task-key value: %s (expected <project-id>:<task-group>:<task-id>)\n' "$raw_key" >&2
34
+ exit 1
35
+ fi
36
+
37
+ if [[ -n "$PROJECT_ID" && "$PROJECT_ID" != "$key_proj" ]]; then
38
+ printf '%s\n' "--task-key project-id ($key_proj) conflicts with --project-id ($PROJECT_ID)" >&2
39
+ exit 1
40
+ fi
41
+ if [[ -n "$TASK_GROUP" && "$TASK_GROUP" != "$key_group" ]]; then
42
+ printf '%s\n' "--task-key task-group ($key_group) conflicts with --task-group ($TASK_GROUP)" >&2
43
+ exit 1
44
+ fi
45
+ if [[ -n "$TASK_ID" && "$TASK_ID" != "$key_id" ]]; then
46
+ printf '%s\n' "--task-key task-id ($key_id) conflicts with --task-id ($TASK_ID)" >&2
47
+ exit 1
48
+ fi
49
+
50
+ PROJECT_ID="$key_proj"
51
+ TASK_GROUP="$key_group"
52
+ TASK_ID="$key_id"
53
+ }
54
+
55
+ resolve_task_root_for_shortcut() {
56
+ local project_root="$1"
57
+ local project_id="$2"
58
+ local task_group="$3"
59
+ local task_id="$4"
60
+
61
+ local resolved=""
62
+ resolved="$(python3 - "$project_root" "$project_id" "$task_group" "$task_id" <<'PY'
63
+ import json, os, re, sys
64
+ from pathlib import Path
65
+
66
+ project_root = Path(sys.argv[1])
67
+ project_id = sys.argv[2]
68
+ task_group = sys.argv[3]
69
+ task_id = sys.argv[4]
70
+
71
+ requested_key = f"{project_id}:{task_group}:{task_id}"
72
+ requested_key_ci = requested_key.lower()
73
+
74
+ def slugify(value: str) -> str:
75
+ value = value.lower()
76
+ value = re.sub(r"[^a-z0-9]+", "-", value).strip("-")
77
+ return value
78
+
79
+ candidates = []
80
+
81
+ catalog_path = project_root / ".project-docs" / "okstra" / "discovery" / "task-catalog.json"
82
+ catalog_keys = []
83
+ if catalog_path.is_file():
84
+ try:
85
+ catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
86
+ except Exception as exc:
87
+ sys.stderr.write(f"resolve_task_root: failed to parse {catalog_path}: {exc}\n")
88
+ catalog = None
89
+ if isinstance(catalog, dict):
90
+ catalog_project_id = catalog.get("projectId")
91
+ tasks = catalog.get("tasks") or []
92
+ for entry in tasks:
93
+ if not isinstance(entry, dict):
94
+ continue
95
+ entry_key = entry.get("taskKey") or ""
96
+ if not isinstance(entry_key, str) or not entry_key:
97
+ continue
98
+ entry_belongs_to_project = (
99
+ catalog_project_id == project_id
100
+ or entry.get("projectId") == project_id
101
+ or entry_key.startswith(f"{project_id}:")
102
+ )
103
+ if entry_belongs_to_project:
104
+ catalog_keys.append(entry_key)
105
+ if entry_key.lower() == requested_key_ci:
106
+ rel = entry.get("taskRootPath") or entry.get("taskRoot")
107
+ if isinstance(rel, str) and rel:
108
+ abs_path = (project_root / rel).resolve() if not os.path.isabs(rel) else Path(rel)
109
+ if abs_path.is_dir():
110
+ print(f"OK\t{abs_path}")
111
+ sys.exit(0)
112
+ candidates.append(str(abs_path))
113
+
114
+ slug_path = project_root / ".project-docs" / "okstra" / "tasks" / slugify(task_group) / slugify(task_id)
115
+ if slug_path.is_dir():
116
+ print(f"OK\t{slug_path}")
117
+ sys.exit(0)
118
+ candidates.append(str(slug_path))
119
+
120
+ # Failure path: emit diagnostics so the wrapper can show them.
121
+ print("MISSING")
122
+ print(f"REQUESTED\t{requested_key}")
123
+ for cand in candidates:
124
+ print(f"TRIED\t{cand}")
125
+ for key in sorted(set(catalog_keys)):
126
+ print(f"AVAILABLE\t{key}")
127
+ PY
128
+ )"
129
+
130
+ local status=""
131
+ status="$(printf '%s' "$resolved" | head -n 1 | awk -F '\t' '{print $1}')"
132
+
133
+ if [[ "$status" == "OK" ]]; then
134
+ printf '%s' "$resolved" | head -n 1 | awk -F '\t' '{print $2}'
135
+ return 0
136
+ fi
137
+
138
+ printf 'task root not found for %s:%s:%s\n' "$project_id" "$task_group" "$task_id" >&2
139
+ printf '%s\n' "$resolved" | awk -F '\t' '
140
+ $1 == "TRIED" { printf " tried: %s\n", $2 }
141
+ $1 == "AVAILABLE"{ avail = avail " " $2 "\n" }
142
+ END {
143
+ if (avail != "") {
144
+ printf " available task-keys for this project (case- and slug-sensitive on disk):\n%s", avail
145
+ }
146
+ }' >&2
147
+ printf ' hint: task-key lookup is case-insensitive against task-catalog.json. Group / id segments on disk are slugified (lowercased, non-alphanumeric runs collapsed to "-").\n' >&2
148
+ return 1
149
+ }
150
+
151
+ autofill_from_manifest() {
152
+ if [[ -z "$PROJECT_ID" ]]; then
153
+ return 0
154
+ fi
155
+ if [[ -n "$BRIEF_PATH" && -n "$ANALYSIS_TYPE" ]]; then
156
+ return 0
157
+ fi
158
+ if [[ -z "$TASK_GROUP" || -z "$TASK_ID" ]]; then
159
+ return 0
160
+ fi
161
+
162
+ # 신규 모델: PROJECT_ROOT 는 okstra.sh 가 시작 단계에서 best-effort 해석해 둔다.
163
+ # 미해석(빈 문자열) 상태이거나 실제 디렉토리가 아니면 autofill 을 skip 한다 — 과거
164
+ # "conf 파일 부재" 분기와 동일한 의미.
165
+ if [[ -z "${PROJECT_ROOT:-}" || ! -d "${PROJECT_ROOT:-}" ]]; then
166
+ return 0
167
+ fi
168
+ local manifest_project_root="$PROJECT_ROOT"
169
+
170
+ local manifest_task_root=""
171
+ manifest_task_root="$(resolve_task_root_for_shortcut "$manifest_project_root" "$PROJECT_ID" "$TASK_GROUP" "$TASK_ID" 2>/dev/null)" || return 0
172
+ local manifest_path="$manifest_task_root/task-manifest.json"
173
+ if [[ ! -f "$manifest_path" ]]; then
174
+ return 0
175
+ fi
176
+
177
+ local need_brief_val need_type_val
178
+ need_brief_val="$([[ -z "$BRIEF_PATH" ]] && printf '1' || printf '0')"
179
+ need_type_val="$([[ -z "$ANALYSIS_TYPE" ]] && printf '1' || printf '0')"
180
+ local autofill_output=""
181
+ autofill_output="$(python3 - "$manifest_path" "$need_brief_val" "$need_type_val" <<'PY'
182
+ import json, sys
183
+ path = sys.argv[1]
184
+ need_brief = sys.argv[2] == "1"
185
+ need_type = sys.argv[3] == "1"
186
+ try:
187
+ data = json.loads(open(path, "r", encoding="utf-8").read())
188
+ except Exception as exc:
189
+ sys.stderr.write(f"autofill_from_manifest: failed to parse {path}: {exc}\n")
190
+ sys.exit(0)
191
+
192
+ brief = ""
193
+ task_type = ""
194
+ if need_brief:
195
+ brief = (data.get("taskBriefPath") or "").strip()
196
+ if need_type:
197
+ workflow = data.get("workflow") or {}
198
+ task_type = (workflow.get("nextRecommendedPhase") or "").strip()
199
+ if task_type in ("", "pending-routing-decision", "done-or-follow-up"):
200
+ task_type = ""
201
+
202
+ print(f"BRIEF={brief}")
203
+ print(f"TYPE={task_type}")
204
+ PY
205
+ )"
206
+
207
+ local manifest_brief=""
208
+ local manifest_type=""
209
+ while IFS= read -r line; do
210
+ case "$line" in
211
+ BRIEF=*) manifest_brief="${line#BRIEF=}" ;;
212
+ TYPE=*) manifest_type="${line#TYPE=}" ;;
213
+ esac
214
+ done <<<"$autofill_output"
215
+
216
+ if [[ -z "$BRIEF_PATH" && -n "$manifest_brief" ]]; then
217
+ BRIEF_PATH="$manifest_brief"
218
+ printf 'autofill: brief-path from task-manifest.json: %s\n' "$BRIEF_PATH" >&2
219
+ fi
220
+ if [[ -z "$ANALYSIS_TYPE" && -n "$manifest_type" ]]; then
221
+ ANALYSIS_TYPE="$manifest_type"
222
+ printf 'autofill: task-type from manifest workflow.nextRecommendedPhase: %s\n' "$ANALYSIS_TYPE" >&2
223
+ fi
224
+ }
225
+
226
+ missing_required_arguments_summary() {
227
+ local missing=()
228
+
229
+ [[ -z "$PROJECT_ID" ]] && missing+=("<project-id>")
230
+ [[ -z "$TASK_GROUP" ]] && missing+=("<task-group>")
231
+ [[ -z "$TASK_ID" ]] && missing+=("<task-id>")
232
+ [[ -z "$ANALYSIS_TYPE" ]] && missing+=("<task-type>")
233
+ [[ -z "$BRIEF_PATH" ]] && missing+=("<brief-path>")
234
+
235
+ if (( ${#missing[@]} == 0 )); then
236
+ printf '%s' ""
237
+ return 0
238
+ fi
239
+
240
+ printf '%s' "${missing[*]}"
241
+ }
242
+
243
+ prompt_for_required_argument() {
244
+ local variable_name="$1"
245
+ local prompt_label="$2"
246
+ local current_value="${!variable_name}"
247
+
248
+ current_value="$(trim_whitespace "$current_value")"
249
+
250
+ while [[ -z "$current_value" ]]; do
251
+ printf '%s: ' "$prompt_label" >&2
252
+ if ! IFS= read -r current_value; then
253
+ printf 'input cancelled while reading %s\n' "$prompt_label" >&2
254
+ exit 1
255
+ fi
256
+ current_value="$(trim_whitespace "$current_value")"
257
+ done
258
+
259
+ printf -v "$variable_name" '%s' "$current_value"
260
+ }
261
+
262
+ find_latest_final_report() {
263
+ # Args: project_root task_group task_id task_type_filter(optional)
264
+ # Echoes: <abs path to latest final-report-*.md>\t<task-type>
265
+ # Returns: 1 if not found.
266
+ local project_root="$1"
267
+ local task_group="$2"
268
+ local task_id="$3"
269
+ local task_type_filter="$4"
270
+
271
+ local task_root=""
272
+ task_root="$(resolve_task_root_for_shortcut "$project_root" "$PROJECT_ID" "$task_group" "$task_id")" || {
273
+ printf 'resume-clarification: unable to resolve task root for the given task-key. See diagnostics above.\n' >&2
274
+ return 1
275
+ }
276
+
277
+ local runs_root="$task_root/runs"
278
+ if [[ ! -d "$runs_root" ]]; then
279
+ printf 'resume-clarification: no runs/ directory under %s\n' "$task_root" >&2
280
+ return 1
281
+ fi
282
+
283
+ local search_dirs=()
284
+ if [[ -n "$task_type_filter" ]]; then
285
+ search_dirs+=("$runs_root/$task_type_filter/reports")
286
+ else
287
+ [[ -d "$runs_root/error-analysis/reports" ]] && search_dirs+=("$runs_root/error-analysis/reports")
288
+ [[ -d "$runs_root/requirements-discovery/reports" ]] && search_dirs+=("$runs_root/requirements-discovery/reports")
289
+ fi
290
+
291
+ if (( ${#search_dirs[@]} == 0 )); then
292
+ printf 'resume-clarification: no requirements-discovery or error-analysis reports/ directory under %s/runs/. Run that phase first before invoking --resume-clarification.\n' "$task_root" >&2
293
+ return 1
294
+ fi
295
+
296
+ local best_path=""
297
+ local best_basename=""
298
+ local candidate=""
299
+ local candidate_base=""
300
+ local d=""
301
+ for d in "${search_dirs[@]}"; do
302
+ [[ -d "$d" ]] || continue
303
+ while IFS= read -r candidate; do
304
+ candidate_base="$(basename "$candidate")"
305
+ if [[ -z "$best_path" || "$candidate_base" > "$best_basename" ]]; then
306
+ best_path="$candidate"
307
+ best_basename="$candidate_base"
308
+ fi
309
+ done < <(find "$d" -maxdepth 1 -type f -name 'final-report-*.md' 2>/dev/null)
310
+ done
311
+
312
+ if [[ -z "$best_path" ]]; then
313
+ printf 'resume-clarification: no final-report-*.md found for task %s:%s under %s\n' \
314
+ "$task_group" "$task_id" "$runs_root" >&2
315
+ return 1
316
+ fi
317
+
318
+ local resolved_type=""
319
+ resolved_type="$(basename "$(dirname "$(dirname "$best_path")")")"
320
+ printf '%s\t%s\n' "$best_path" "$resolved_type"
321
+ }
322
+
323
+ run_resume_clarification() {
324
+ if [[ "$RENDER_ONLY" == "true" ]]; then
325
+ printf '%s\n' '--resume-clarification is incompatible with --render-only' >&2
326
+ exit 1
327
+ fi
328
+ if [[ -n "${CLARIFICATION_RESPONSE_PATH-}" ]]; then
329
+ printf '%s\n' '--resume-clarification is incompatible with --clarification-response (the resume mode supplies it automatically)' >&2
330
+ exit 1
331
+ fi
332
+ if [[ -n "${APPROVED_PLAN_PATH-}" ]]; then
333
+ printf '%s\n' '--resume-clarification is incompatible with --approved-plan' >&2
334
+ exit 1
335
+ fi
336
+
337
+ if [[ -z "$PROJECT_ID" || -z "$TASK_GROUP" || -z "$TASK_ID" ]]; then
338
+ printf 'resume-clarification: --project-id, --task-group, --task-id (or --task-key) are required\n' >&2
339
+ exit 1
340
+ fi
341
+
342
+ # 신규 모델: PROJECT_ROOT 는 okstra.sh 진입 시점에 resolve_project_root_strict
343
+ # 로 이미 확정되어 export 되어 있다.
344
+ if [[ -z "${PROJECT_ROOT:-}" || ! -d "${PROJECT_ROOT:-}" ]]; then
345
+ printf 'resume-clarification: PROJECT_ROOT not resolved\n' >&2
346
+ exit 1
347
+ fi
348
+ local resume_project_root="$PROJECT_ROOT"
349
+
350
+ local lookup_result=""
351
+ if ! lookup_result="$(find_latest_final_report \
352
+ "$resume_project_root" "$TASK_GROUP" "$TASK_ID" "${ANALYSIS_TYPE-}")"; then
353
+ exit 1
354
+ fi
355
+ local report_path="${lookup_result%$'\t'*}"
356
+ local resolved_type="${lookup_result##*$'\t'}"
357
+
358
+ case "$resolved_type" in
359
+ requirements-discovery|error-analysis) ;;
360
+ *)
361
+ printf '%s (resolved task-type: %s)\n' '--resume-clarification only applies to requirements-discovery or error-analysis runs' "$resolved_type" >&2
362
+ exit 1
363
+ ;;
364
+ esac
365
+
366
+ local resume_task_root=""
367
+ resume_task_root="$(resolve_task_root_for_shortcut "$resume_project_root" "$PROJECT_ID" "$TASK_GROUP" "$TASK_ID")" || {
368
+ printf 'resume-clarification: unable to resolve task root for the given task-key. See diagnostics above.\n' >&2
369
+ exit 1
370
+ }
371
+ local manifest_file="$resume_task_root/task-manifest.json"
372
+ if [[ ! -f "$manifest_file" ]]; then
373
+ printf 'resume-clarification: task-manifest.json not found: %s\n' "$manifest_file" >&2
374
+ exit 1
375
+ fi
376
+ local resume_brief_rel=""
377
+ resume_brief_rel="$(python3 - "$manifest_file" <<'PY'
378
+ import json, sys
379
+ data = json.loads(open(sys.argv[1], "r", encoding="utf-8").read())
380
+ print((data.get("taskBriefPath") or "").strip())
381
+ PY
382
+ )"
383
+ if [[ -z "$resume_brief_rel" ]]; then
384
+ printf 'resume-clarification: manifest missing taskBriefPath: %s\n' "$manifest_file" >&2
385
+ exit 1
386
+ fi
387
+
388
+ if [[ "${OKSTRA_RESUME_CLARIFICATION_DRY_RUN:-false}" == "true" ]]; then
389
+ printf 'resume-clarification (dry-run):\n' >&2
390
+ printf ' report: %s\n' "$report_path" >&2
391
+ printf ' re-exec: bash %s --task-type %s --project-id %s --task-group %s --task-id %s --task-brief %s --clarification-response %s\n' \
392
+ "$0" "$resolved_type" "$PROJECT_ID" "$TASK_GROUP" "$TASK_ID" "$resume_brief_rel" "$report_path" >&2
393
+ return 0
394
+ fi
395
+
396
+ local editor_cmd="${EDITOR:-vi}"
397
+ printf 'resume-clarification: opening %s with %s\n' "$report_path" "$editor_cmd" >&2
398
+ if ! "$editor_cmd" "$report_path"; then
399
+ printf 'resume-clarification: editor exited non-zero; aborting\n' >&2
400
+ exit 1
401
+ fi
402
+
403
+ printf 'resume-clarification: re-running okstra with --clarification-response\n' >&2
404
+ exec bash "$0" \
405
+ --task-type "$resolved_type" \
406
+ --project-id "$PROJECT_ID" \
407
+ --task-group "$TASK_GROUP" \
408
+ --task-id "$TASK_ID" \
409
+ --task-brief "$resume_brief_rel" \
410
+ --clarification-response "$report_path"
411
+ }
@@ -0,0 +1,57 @@
1
+ # shellcheck shell=bash
2
+
3
+ # bash wrappers around scripts/okstra_project resolver. okstra.sh 가 시작 시
4
+ # PROJECT_ROOT 를 확정하고 <PROJECT_ROOT>/.project-docs/okstra/project.json 을
5
+ # upsert 한다. 과거 conf 파일 모델은 폐기되었다.
6
+
7
+ # 절대경로(또는 빈 문자열) 를 stdout 으로 출력한다. 실패 시 stderr 에 메시지를
8
+ # 남기고 비-0 종료. autofill 처럼 best-effort 호출자가 에러를 무시할 수 있도록
9
+ # strict/safe 두 종류를 분리한다.
10
+ resolve_project_root_strict() {
11
+ local explicit="${1-}"
12
+ python3 - "$WORKSPACE_ROOT/scripts" "$explicit" <<'PY'
13
+ import os, sys
14
+ sys.path.insert(0, sys.argv[1])
15
+ from okstra_project import resolve_project_root, ResolverError
16
+ explicit = sys.argv[2] if len(sys.argv) > 2 else ""
17
+ try:
18
+ p = resolve_project_root(explicit_root=explicit, cwd=os.getcwd())
19
+ except ResolverError as exc:
20
+ sys.stderr.write(f"resolve_project_root: {exc}\n")
21
+ sys.exit(1)
22
+ print(p)
23
+ PY
24
+ }
25
+
26
+ # 실패해도 0 으로 종료하고 stdout 은 빈 문자열(autofill 등 best-effort 경로용).
27
+ resolve_project_root_safe() {
28
+ local explicit="${1-}"
29
+ python3 - "$WORKSPACE_ROOT/scripts" "$explicit" <<'PY'
30
+ import os, sys
31
+ sys.path.insert(0, sys.argv[1])
32
+ from okstra_project import resolve_project_root, ResolverError
33
+ explicit = sys.argv[2] if len(sys.argv) > 2 else ""
34
+ try:
35
+ p = resolve_project_root(explicit_root=explicit, cwd=os.getcwd())
36
+ except ResolverError:
37
+ sys.exit(0)
38
+ print(p)
39
+ PY
40
+ }
41
+
42
+ # project.json upsert. 충돌(projectId 불일치) 시 비-0 종료 + stderr 메시지.
43
+ upsert_project_json() {
44
+ local project_root="$1"
45
+ local project_id="$2"
46
+ python3 - "$WORKSPACE_ROOT/scripts" "$project_root" "$project_id" <<'PY'
47
+ import sys
48
+ sys.path.insert(0, sys.argv[1])
49
+ from pathlib import Path
50
+ from okstra_project import upsert_project_json, ResolverError
51
+ try:
52
+ upsert_project_json(Path(sys.argv[2]), sys.argv[3])
53
+ except ResolverError as exc:
54
+ sys.stderr.write(f"project.json upsert failed: {exc}\n")
55
+ sys.exit(1)
56
+ PY
57
+ }
@@ -0,0 +1,98 @@
1
+ # shellcheck shell=bash
2
+
3
+ usage() {
4
+ cat >&2 <<USAGE_EOF
5
+ usage:
6
+ $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--refresh-assets] --task-type <task-type> [--workers worker1,worker2] [--lead-model <model>] [--claude-model <model>] [--codex-model <model>] [--gemini-model <model>] [--report-writer-model <model>] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>]
7
+
8
+ summary:
9
+ $DISPLAY_TOOL_NAME prepares a task-keyed instruction bundle for Claude Code and launches an interactive Claude session by default.
10
+ The stable task identifier is composed of project-id + task-group + task-id.
11
+
12
+ Skills, worker agents, and the codex wrapper are installed once per user under
13
+ ~/.claude and ~/.okstra by scripts/okstra-install.sh. The user's settings.json
14
+ and project .claude/settings.local.json files are never modified; per-session
15
+ permissions are injected via 'claude --settings' at launch time.
16
+
17
+ required arguments:
18
+ --project-id Globally unique project ID. Example: fontradar-v2-api.
19
+ Each project is registered at <project-root>/.project-docs/okstra/project.json
20
+ on first run; subsequent runs verify the projectId there matches.
21
+ --task-group Logical task group. Example: backend-api, bugfix, linear-8858
22
+ --task-id Stable task identifier inside the task group. Example: login-error-analysis
23
+ --task-type Task type. Also selects prompts/profiles/<task-type>.md
24
+ --task-brief Task brief file path. Relative paths are resolved against the target project root.
25
+
26
+ optional arguments:
27
+ --project-root Absolute path to the target project root. Resolution order when omitted:
28
+ (1) ancestor of cwd that contains .project-docs/okstra/project.json,
29
+ (2) `git rev-parse --show-toplevel` from cwd. Errors out if neither resolves.
30
+ --directive Free-form user-supplied directive carried into the run as a "## Directive" section
31
+ inside instruction-set/analysis-material.md. Lead, workers, and skills (e.g. okstra-schedule)
32
+ may treat this as a hard hint that overrides default heuristics. Use to express intent
33
+ like "render a Gantt even with single XL task" or "emphasize rollout risk".
34
+ --clarification-response
35
+ Low-level path argument. Carries an edited final-report.md from a prior
36
+ requirements-discovery or error-analysis run into this run as Section 0
37
+ input so the lead can reconcile each prior Q*. Use this for scripted or
38
+ CI runs where the answer file is already prepared. Interactive users
39
+ should prefer --resume-clarification, which wraps this flag.
40
+ --approved-plan Path to the approved final-report.md from a prior implementation-planning run.
41
+ Required when --task-type=implementation; the file MUST contain a recorded user approval marker.
42
+ --task-key <project-id:task-group:task-id>
43
+ Shorthand for --project-id/--task-group/--task-id. When the matching task-manifest.json
44
+ exists, brief-path and task-type are auto-filled from it (taskBriefPath and
45
+ workflow.nextRecommendedPhase). Explicit flags always win.
46
+
47
+ options:
48
+ --render-only Render the Claude handoff prompt only. Do not launch Claude.
49
+ --resume-clarification
50
+ Interactive convenience mode that wraps --clarification-response.
51
+ Locates the latest requirements-discovery or error-analysis
52
+ final-report-*.md for the given task-key, opens it in \$EDITOR so
53
+ you can fill Section 5, then re-execs okstra with
54
+ --clarification-response set to the edited file. Use this for
55
+ hand-driven turn-arounds. Requires task identity flags
56
+ (--project-id/--task-group/--task-id or --task-key). Mutually
57
+ exclusive with --clarification-response and --approved-plan.
58
+ --yes Skip interactive prompting and confirmation. Requires all required arguments.
59
+ --refresh-assets Deprecated. okstra now installs skills/agents into ~/.claude and the codex
60
+ wrapper into ~/.okstra/bin via scripts/okstra-install.sh. Re-run that
61
+ installer with --refresh to update installed assets.
62
+ --workers Comma-separated worker list for this run. Default: claude,codex,gemini,report-writer
63
+ --lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus
64
+ --claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or sonnet
65
+ --codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.5
66
+ --gemini-model Model for Gemini worker. Default: OKSTRA_DEFAULT_GEMINI_MODEL or auto
67
+ --report-writer-model
68
+ Model for report writer worker. Default: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or lead model default
69
+ --related-tasks Optional comma-separated related task identifiers. Example: auth-token-refresh,frontend-login-ui
70
+ --task-type Set the task purpose for this run and select the matching profile file.
71
+ -h, --help Show this help.
72
+
73
+ model defaults:
74
+ Claude lead: OKSTRA_DEFAULT_LEAD_MODEL or opus
75
+ Report writer worker: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or Claude lead default
76
+ Claude worker: OKSTRA_DEFAULT_CLAUDE_MODEL or sonnet
77
+ Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.5
78
+ Gemini worker: OKSTRA_DEFAULT_GEMINI_MODEL or auto
79
+
80
+ output:
81
+ Stable task bundles are stored under:
82
+ <target-project>/.project-docs/okstra/tasks/<task-group>/<task-id>/
83
+ Per-run history is stored under:
84
+ <target-project>/.project-docs/okstra/tasks/<task-group>/<task-id>/runs/
85
+ Inside each run date folder, artifacts are grouped by type under:
86
+ manifests/, state/, prompts/, reports/, status/, sessions/, worker-results/
87
+
88
+ project-level discovery:
89
+ Latest $DISPLAY_TOOL_NAME task pointer:
90
+ <target-project>/.project-docs/okstra/discovery/latest-task.json
91
+
92
+ interactive behavior:
93
+ If required arguments are missing and stdin is interactive, $DISPLAY_TOOL_NAME prompts for them.
94
+ Before execution, interactive runs show the collected input summary and continue only when you enter y or yes.
95
+ If --yes is provided, $DISPLAY_TOOL_NAME skips prompting and confirmation and requires all required arguments up front.
96
+ USAGE_EOF
97
+ }
98
+
@@ -0,0 +1,59 @@
1
+ # shellcheck shell=bash
2
+
3
+ _okstra_ctl_batch() {
4
+ if [[ $# -lt 1 ]]; then
5
+ printf 'batch: missing subcommand (list|status)\n' >&2; exit 2
6
+ fi
7
+ local sub="$1"; shift
8
+ case "$sub" in
9
+ list)
10
+ OKSTRA_HOME_RESOLVED="$(okstra_central_home)" python3 - <<'PY'
11
+ import json, os
12
+ from pathlib import Path
13
+ home = Path(os.environ["OKSTRA_HOME_RESOLVED"])
14
+ batches = sorted((home / "batches").glob("*.json"))
15
+ print(f"{'BATCH-ID':<24} {'CREATED':<22} {'SPAWNED':>7} {'SKIPPED':>7}")
16
+ for b in batches:
17
+ m = json.loads(b.read_text())
18
+ s = m.get("summary", {})
19
+ print(f"{m['batchId']:<24} {m['createdAt']:<22} "
20
+ f"{s.get('spawned', 0):>7} {s.get('skipped', 0):>7}")
21
+ PY
22
+ ;;
23
+ status)
24
+ if [[ $# -lt 1 ]]; then
25
+ printf 'batch status: missing <batch-id>\n' >&2; exit 2
26
+ fi
27
+ OKSTRA_HOME_RESOLVED="$(okstra_central_home)" \
28
+ OKSTRA_CTL_LIB_DIR="$SCRIPT_DIR" \
29
+ OK_BID="$1" python3 - <<'PY'
30
+ import json, os, sys
31
+ sys.path.insert(0, os.environ["OKSTRA_CTL_LIB_DIR"])
32
+ from pathlib import Path
33
+ from okstra_ctl import find_row_by_run_id
34
+ home = Path(os.environ["OKSTRA_HOME_RESOLVED"])
35
+ target = home / "batches" / f"{os.environ['OK_BID']}.json"
36
+ if not target.is_file():
37
+ print(f"batch: not found: {os.environ['OK_BID']}", file=sys.stderr); sys.exit(2)
38
+ m = json.loads(target.read_text())
39
+ print(f"batchId : {m['batchId']}")
40
+ print(f"createdAt : {m['createdAt']}")
41
+ print(f"summary : {m['summary']}")
42
+ print()
43
+ print(f"{'SPAWN':<10} {'LIVE':<11} {'NEW-RUN-ID':<55} {'SESSION-NAME'}")
44
+ for it in m["items"]:
45
+ spawn_status = it["status"]
46
+ new_run_id = it.get("newRunId") or "-"
47
+ # 현재 인덱스(active+recent+archive)에서 live 상태를 조회한다.
48
+ live = "-"
49
+ if it.get("newRunId"):
50
+ live_row = find_row_by_run_id(home, it["newRunId"])
51
+ if live_row is not None:
52
+ live = live_row.get("status", "-")
53
+ print(f"{spawn_status:<10} {live:<11} "
54
+ f"{new_run_id:<55} {it.get('sessionName') or '-'}")
55
+ PY
56
+ ;;
57
+ *) printf 'batch: unknown subcommand: %s\n' "$sub" >&2; exit 2 ;;
58
+ esac
59
+ }
@@ -0,0 +1,35 @@
1
+ # shellcheck shell=bash
2
+
3
+ _okstra_ctl_list() {
4
+ local project="all" task_group="all" status="all" since="" limit="0"
5
+ local include_archive="false"
6
+ while [[ $# -gt 0 ]]; do
7
+ case "$1" in
8
+ --project) project="$2"; shift 2 ;;
9
+ --task-group) task_group="$2"; shift 2 ;;
10
+ --status) status="$2"; shift 2 ;;
11
+ --since) since="$2"; shift 2 ;;
12
+ --limit) limit="$2"; shift 2 ;;
13
+ --include-archive) include_archive="true"; shift ;;
14
+ *) printf 'list: unknown option: %s\n' "$1" >&2; exit 2 ;;
15
+ esac
16
+ done
17
+ OKSTRA_HOME_RESOLVED="$(okstra_central_home)" \
18
+ OKSTRA_CTL_LIB_DIR="$SCRIPT_DIR" \
19
+ OK_PROJECT="$project" OK_TG="$task_group" OK_STATUS="$status" \
20
+ OK_SINCE="$since" OK_LIMIT="$limit" OK_ARC="$include_archive" \
21
+ python3 - <<'PY'
22
+ import os, sys
23
+ sys.path.insert(0, os.environ["OKSTRA_CTL_LIB_DIR"])
24
+ from pathlib import Path
25
+ from okstra_ctl import list_runs, format_runs_table
26
+ rows = list_runs(
27
+ Path(os.environ["OKSTRA_HOME_RESOLVED"]),
28
+ project=os.environ["OK_PROJECT"], task_group=os.environ["OK_TG"],
29
+ status=os.environ["OK_STATUS"], since=os.environ["OK_SINCE"],
30
+ limit=int(os.environ["OK_LIMIT"]),
31
+ include_archive=os.environ["OK_ARC"] == "true",
32
+ )
33
+ print(format_runs_table(rows), end="")
34
+ PY
35
+ }