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.
- package/README.md +36 -0
- package/bin/okstra +62 -0
- package/package.json +30 -0
- package/runtime/.gitkeep +0 -0
- package/runtime/BUILD.json +5 -0
- package/runtime/agents/SKILL.md +243 -0
- package/runtime/agents/TODO.md +168 -0
- package/runtime/agents/workers/claude-worker.md +106 -0
- package/runtime/agents/workers/codex-worker.md +179 -0
- package/runtime/agents/workers/gemini-worker.md +179 -0
- package/runtime/agents/workers/report-writer-worker.md +116 -0
- package/runtime/bin/okstra-central.sh +152 -0
- package/runtime/bin/okstra-codex-exec.sh +53 -0
- package/runtime/bin/okstra-error-log.py +295 -0
- package/runtime/bin/okstra-gemini-exec.sh +55 -0
- package/runtime/bin/okstra-token-usage.py +46 -0
- package/runtime/bin/okstra.sh +162 -0
- package/runtime/prompts/launch.template.md +52 -0
- package/runtime/prompts/profiles/error-analysis.md +43 -0
- package/runtime/prompts/profiles/final-verification.md +37 -0
- package/runtime/prompts/profiles/implementation-planning.md +85 -0
- package/runtime/prompts/profiles/implementation.md +71 -0
- package/runtime/prompts/profiles/requirements-discovery.md +43 -0
- package/runtime/python/lib/okstra/cli.sh +227 -0
- package/runtime/python/lib/okstra/globals.sh +157 -0
- package/runtime/python/lib/okstra/interactive.sh +411 -0
- package/runtime/python/lib/okstra/project-resolver.sh +57 -0
- package/runtime/python/lib/okstra/usage.sh +98 -0
- package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
- package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
- package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
- package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
- package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
- package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
- package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
- package/runtime/python/lib/okstra-ctl/main.sh +41 -0
- package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
- package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
- package/runtime/python/okstra_ctl/__init__.py +125 -0
- package/runtime/python/okstra_ctl/backfill.py +253 -0
- package/runtime/python/okstra_ctl/batch.py +62 -0
- package/runtime/python/okstra_ctl/ids.py +84 -0
- package/runtime/python/okstra_ctl/index.py +216 -0
- package/runtime/python/okstra_ctl/invocation.py +49 -0
- package/runtime/python/okstra_ctl/jsonl.py +84 -0
- package/runtime/python/okstra_ctl/listing.py +156 -0
- package/runtime/python/okstra_ctl/locks.py +42 -0
- package/runtime/python/okstra_ctl/material.py +62 -0
- package/runtime/python/okstra_ctl/models.py +63 -0
- package/runtime/python/okstra_ctl/path_resolve.py +40 -0
- package/runtime/python/okstra_ctl/paths.py +251 -0
- package/runtime/python/okstra_ctl/project_meta.py +51 -0
- package/runtime/python/okstra_ctl/reconcile.py +166 -0
- package/runtime/python/okstra_ctl/render.py +1065 -0
- package/runtime/python/okstra_ctl/resolver.py +54 -0
- package/runtime/python/okstra_ctl/run.py +674 -0
- package/runtime/python/okstra_ctl/run_context.py +166 -0
- package/runtime/python/okstra_ctl/seeding.py +97 -0
- package/runtime/python/okstra_ctl/sequence.py +53 -0
- package/runtime/python/okstra_ctl/session.py +33 -0
- package/runtime/python/okstra_ctl/tmux.py +27 -0
- package/runtime/python/okstra_ctl/workers.py +64 -0
- package/runtime/python/okstra_ctl/workflow.py +182 -0
- package/runtime/python/okstra_project/__init__.py +41 -0
- package/runtime/python/okstra_project/resolver.py +126 -0
- package/runtime/python/okstra_project/state.py +170 -0
- package/runtime/python/okstra_token_usage/__init__.py +26 -0
- package/runtime/python/okstra_token_usage/blocks.py +62 -0
- package/runtime/python/okstra_token_usage/claude.py +97 -0
- package/runtime/python/okstra_token_usage/cli.py +84 -0
- package/runtime/python/okstra_token_usage/codex.py +80 -0
- package/runtime/python/okstra_token_usage/collect.py +161 -0
- package/runtime/python/okstra_token_usage/gemini.py +77 -0
- package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
- package/runtime/python/okstra_token_usage/paths.py +22 -0
- package/runtime/python/okstra_token_usage/pricing.py +71 -0
- package/runtime/python/okstra_token_usage/report.py +64 -0
- package/runtime/templates/prd/brief.template.md +273 -0
- package/runtime/templates/project-docs/task-index.template.md +65 -0
- package/runtime/templates/reports/error-analysis-input.template.md +80 -0
- package/runtime/templates/reports/final-report.template.md +167 -0
- package/runtime/templates/reports/final-verification-input.template.md +67 -0
- package/runtime/templates/reports/implementation-input.template.md +81 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
- package/runtime/templates/reports/quick-input.template.md +64 -0
- package/runtime/templates/reports/schedule.template.md +168 -0
- package/runtime/templates/reports/settings.template.json +101 -0
- package/runtime/templates/reports/task-brief.template.md +165 -0
- package/runtime/validators/lib/common.sh +44 -0
- package/runtime/validators/lib/fixtures.sh +322 -0
- package/runtime/validators/lib/paths.sh +44 -0
- package/runtime/validators/lib/runners.sh +140 -0
- package/runtime/validators/lib/summary.sh +15 -0
- package/runtime/validators/lib/validate-assets.sh +44 -0
- package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
- package/runtime/validators/lib/validate-tasks.sh +335 -0
- package/runtime/validators/validate-run.py +568 -0
- package/runtime/validators/validate-schedule.py +665 -0
- package/runtime/validators/validate-workflow.sh +190 -0
- package/src/doctor.mjs +127 -0
- package/src/install.mjs +355 -0
- package/src/paths.mjs +132 -0
- package/src/uninstall.mjs +122 -0
- 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
|
+
}
|