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,322 @@
1
+ # shellcheck shell=bash
2
+
3
+ write_validation_brief() {
4
+ local brief_path="$1"
5
+ local brief_title="$2"
6
+ local task_group="$3"
7
+ local task_id="$4"
8
+ local validation_focus="$5"
9
+ local task_key_value=""
10
+
11
+ task_key_value="$(task_key "$task_group" "$task_id")"
12
+
13
+ cat >"$brief_path" <<EOF
14
+ # Cross Verify Task Brief
15
+
16
+ ## Brief Identity
17
+
18
+ - Brief Title: $brief_title
19
+ - Project ID: \`okstra-validation\`
20
+ - Task Group: \`$task_group\`
21
+ - Task ID: \`$task_id\`
22
+ - Task Key: \`$task_key_value\`
23
+ - Related Tasks:
24
+ - Task Type: \`$TASK_TYPE\`
25
+ - Requested Outcome: validate asset seeding, refresh behavior, and discovery catalog integrity
26
+ - Owner: \`Oz\`
27
+
28
+ ## Problem or Request Summary
29
+
30
+ - Verify that okstra seeds all Claude Markdown assets into the target project on first run.
31
+ - Verify that refresh is optional and only forced with the refresh option.
32
+ - Verify that config and deployment expected states are discoverable by task bundle artifacts.
33
+ - Validation focus: $validation_focus
34
+
35
+ ## Source Materials
36
+
37
+ - Primary problem statement: \`validation\`
38
+ - Existing analysis or notes:
39
+ - Related raw samples or logs:
40
+ - Related code paths: \`scripts/okstra.sh\`
41
+ - Related tickets or docs: \`OKSTRA_USAGE_MANUAL.md\`
42
+ - Previous reports in the same task history:
43
+
44
+ ## Constraints and Assumptions
45
+
46
+ - Known constraints: validation must stay inside the temporary project root
47
+ - Known assumptions: task bundle artifacts are the canonical source for skill loading
48
+ - Things that are still uncertain: none
49
+
50
+ ## Configuration References and Expected Values
51
+
52
+ - Config file: \`.claude/settings.json\`
53
+ - Expected values:
54
+ - project-local okstra Claude assets must remain discoverable under \`.claude/skills/\` and \`.claude/agents/\`
55
+ - refresh should occur only when \`--refresh-assets\` is used
56
+ - Config file: \`.project-docs/okstra/discovery/latest-task.json\`
57
+ - Expected values:
58
+ - latest prepared task pointer must include the current task key
59
+ - task catalog path must be present
60
+ - Config file: \`.project-docs/okstra/discovery/task-catalog.json\`
61
+ - Expected values:
62
+ - task catalog must preserve prepared task bundles by task key
63
+ - task catalog must allow task-group and task-id level distinction
64
+
65
+ ## Deployment Manifests and Expected Values
66
+
67
+ - Manifest file: \`deploy/values.yaml\`
68
+ - Expected values:
69
+ - image tag should match the validated release candidate for this task
70
+ - rollout-specific values must be verified against the task brief before final approval
71
+ - Manifest file: \`k8s/deployment.yaml\`
72
+ - Expected values:
73
+ - env and image settings must match the task brief requirements
74
+ - missing deployment expectations must be reported as missing information
75
+
76
+ ## Questions for Workers
77
+
78
+ 1. Are all required Claude assets seeded into the temporary target project?
79
+ 2. Do the generated task artifacts preserve config and deployment expected states?
80
+ 3. Does refresh remain optional rather than automatic?
81
+ 4. Does the discovery catalog preserve distinct task entries?
82
+
83
+ ## Expected Outputs
84
+
85
+ - Asset seed verification:
86
+ - Missing information:
87
+ - Risks:
88
+ - Recommended next actions:
89
+ EOF
90
+ }
91
+
92
+ write_worker_prompt_history_fixture() {
93
+ local task_group="$1"
94
+ local task_id="$2"
95
+ local worker_id="$3"
96
+ local expected_task_manifest_relative_path=""
97
+
98
+ expected_task_manifest_relative_path="$(task_manifest_relative_path "$task_group" "$task_id")"
99
+
100
+ python3 - "$PROJECT_ROOT" "$expected_task_manifest_relative_path" "$worker_id" <<'PY'
101
+ from pathlib import Path
102
+ import json
103
+ import sys
104
+
105
+ project_root = Path(sys.argv[1])
106
+ task_manifest_path = project_root / sys.argv[2]
107
+ target_worker_id = sys.argv[3]
108
+ task_manifest = json.loads(task_manifest_path.read_text())
109
+ team_state_path = project_root / task_manifest["teamStatePath"]
110
+ team_state = json.loads(team_state_path.read_text())
111
+
112
+ target_worker = None
113
+ for worker in team_state.get("workers", []):
114
+ if isinstance(worker, dict) and worker.get("workerId") == target_worker_id:
115
+ target_worker = worker
116
+ break
117
+
118
+ if target_worker is None:
119
+ raise SystemExit(f"worker not found in team-state: {target_worker_id}")
120
+
121
+ prompt_relative = str(target_worker.get("promptPath", "")).strip()
122
+ if not prompt_relative:
123
+ raise SystemExit(f"worker promptPath is missing: {target_worker_id}")
124
+
125
+ prompt_path = project_root / prompt_relative
126
+ prompt_path.parent.mkdir(parents=True, exist_ok=True)
127
+ prompt_path.write_text(
128
+ "\n".join(
129
+ [
130
+ f"# {target_worker.get('role', target_worker_id)} Prompt Snapshot",
131
+ "",
132
+ f"Assigned worker prompt history path: {prompt_relative}",
133
+ f"Task Key: {task_manifest.get('taskKey', '')}",
134
+ "Validation fixture prompt body.",
135
+ ]
136
+ )
137
+ + "\n"
138
+ )
139
+ PY
140
+ }
141
+
142
+ prepare_run_validator_fixture() {
143
+ local task_group="$1"
144
+ local task_id="$2"
145
+ local omitted_worker_id="$3"
146
+ local expected_task_manifest_relative_path=""
147
+
148
+ expected_task_manifest_relative_path="$(task_manifest_relative_path "$task_group" "$task_id")"
149
+
150
+ python3 - "$PROJECT_ROOT" "$expected_task_manifest_relative_path" "$omitted_worker_id" <<'PY'
151
+ from pathlib import Path
152
+ import json
153
+ import sys
154
+
155
+ project_root = Path(sys.argv[1])
156
+ task_manifest_path = project_root / sys.argv[2]
157
+ omitted_worker_id = sys.argv[3]
158
+
159
+
160
+ def load_json(path: Path) -> dict:
161
+ return json.loads(path.read_text())
162
+
163
+
164
+ def write_json(path: Path, payload: dict) -> None:
165
+ path.parent.mkdir(parents=True, exist_ok=True)
166
+ path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
167
+
168
+
169
+ task_manifest = load_json(task_manifest_path)
170
+ timeline_path = project_root / task_manifest["historyTimelinePath"]
171
+ timeline = load_json(timeline_path)
172
+ runs = timeline.get("runs", [])
173
+ latest_run = None
174
+ if isinstance(runs, list):
175
+ for item in reversed(runs):
176
+ if isinstance(item, dict):
177
+ latest_run = item
178
+ break
179
+
180
+ if latest_run is None:
181
+ raise SystemExit("timeline does not contain a latest run entry")
182
+
183
+ run_manifest_path = project_root / latest_run["runManifestPath"]
184
+ run_manifest = load_json(run_manifest_path)
185
+ team_state_path = project_root / run_manifest["teamStatePath"]
186
+ team_state = load_json(team_state_path)
187
+ report_path = project_root / run_manifest["expectedReportPath"]
188
+ final_status_path = project_root / run_manifest["expectedStatusPath"]
189
+
190
+ status_by_worker_id = {
191
+ "claude": "completed",
192
+ "codex": "timeout",
193
+ "gemini": "error",
194
+ "report-writer": "completed",
195
+ }
196
+ reason_by_worker_id = {
197
+ "claude": "",
198
+ "codex": "Validation fixture timeout",
199
+ "gemini": "Validation fixture execution error",
200
+ "report-writer": "",
201
+ }
202
+
203
+ for worker in team_state.get("workers", []):
204
+ if not isinstance(worker, dict):
205
+ continue
206
+ worker_id = str(worker.get("workerId", "")).strip()
207
+ if not worker_id:
208
+ continue
209
+ worker["status"] = status_by_worker_id.get(worker_id, "not-run")
210
+ worker["reason"] = reason_by_worker_id.get(worker_id, "Validation fixture not used")
211
+
212
+ prompt_relative = str(worker.get("promptPath", "")).strip()
213
+ if prompt_relative and worker_id != omitted_worker_id:
214
+ prompt_path = project_root / prompt_relative
215
+ prompt_path.parent.mkdir(parents=True, exist_ok=True)
216
+ prompt_path.write_text(
217
+ "\n".join(
218
+ [
219
+ f"# {worker.get('role', worker_id)} Prompt Snapshot",
220
+ "",
221
+ f"Assigned worker prompt history path: {prompt_relative}",
222
+ f"Task Key: {task_manifest.get('taskKey', '')}",
223
+ "Validation fixture prompt body.",
224
+ ]
225
+ )
226
+ + "\n"
227
+ )
228
+
229
+ result_relative = str(worker.get("resultPath", "")).strip()
230
+ if worker["status"] == "completed" and result_relative:
231
+ result_path = project_root / result_relative
232
+ result_path.parent.mkdir(parents=True, exist_ok=True)
233
+ result_path.write_text(
234
+ "\n".join(
235
+ [
236
+ "# Findings",
237
+ "- Validation fixture finding.",
238
+ "",
239
+ "# Missing Information or Assumptions",
240
+ "- None.",
241
+ "",
242
+ "# Safe or Reasonable Areas",
243
+ "- Fixture output format is valid.",
244
+ "",
245
+ "# Uncertain Points",
246
+ "- None.",
247
+ "",
248
+ "# Recommended Next Actions",
249
+ "- Continue validator coverage.",
250
+ ]
251
+ )
252
+ + "\n"
253
+ )
254
+
255
+ lead = team_state.get("lead")
256
+ if isinstance(lead, dict):
257
+ lead["status"] = "completed"
258
+ team_state["workflowState"] = "worker-results-collected"
259
+
260
+ # Phase 7 token-usage collection is normally produced by okstra-token-usage.py.
261
+ # The validator (`team-state.usageSummary is empty`) treats absence as a contract
262
+ # violation, so the fixture must mirror that step with a synthetic-but-valid object.
263
+ team_state["usageSummary"] = {
264
+ "leadTotalTokens": 0,
265
+ "workerTotalTokens": 0,
266
+ "grandTotalTokens": 0,
267
+ "leadBillableEquivalentTokens": 0,
268
+ "workerBillableEquivalentTokens": 0,
269
+ "grandBillableEquivalentTokens": 0,
270
+ "estimatedCostUsd": {
271
+ "lead": 0.0,
272
+ "claudeWorkers": 0.0,
273
+ "cliWorkers": 0.0,
274
+ "grandTotal": 0.0,
275
+ },
276
+ "collectedAt": "1970-01-01T00:00:00Z",
277
+ "teamName": team_state.get("teamName", "validation-fixture"),
278
+ "sessionsFound": 0,
279
+ "definitions": {
280
+ "totalTokens": "Validation fixture placeholder.",
281
+ "billableEquivalentTokens": "Validation fixture placeholder.",
282
+ "estimatedCostUsd": "Validation fixture placeholder.",
283
+ },
284
+ "intake": {
285
+ "totalTokens": 0,
286
+ "billableEquivalentTokens": 0,
287
+ "estimatedCostUsd": 0,
288
+ },
289
+ }
290
+
291
+ required_status_entries = run_manifest.get("teamContract", {}).get(
292
+ "requiredAgentStatusEntries", []
293
+ )
294
+ if not isinstance(required_status_entries, list):
295
+ required_status_entries = []
296
+
297
+ report_lines = [
298
+ "# Validation Fixture Report",
299
+ "",
300
+ "## Agent Execution Status",
301
+ ]
302
+ for label in required_status_entries:
303
+ if isinstance(label, str) and label.strip():
304
+ report_lines.append(f"- {label}: fixture status recorded")
305
+ report_lines.extend(
306
+ [
307
+ "",
308
+ "## Final Verdict",
309
+ "- Validation fixture report generated.",
310
+ ]
311
+ )
312
+ report_path.parent.mkdir(parents=True, exist_ok=True)
313
+ report_path.write_text("\n".join(report_lines) + "\n")
314
+
315
+ if final_status_path.exists():
316
+ final_status_path.unlink()
317
+
318
+ write_json(team_state_path, team_state)
319
+ write_json(run_manifest_path, run_manifest)
320
+ write_json(task_manifest_path, task_manifest)
321
+ PY
322
+ }
@@ -0,0 +1,44 @@
1
+ # shellcheck shell=bash
2
+
3
+ validate_project_root_safety() {
4
+ if [[ -z "${PROJECT_ROOT:-}" ]]; then
5
+ fail "PROJECT_ROOT is not defined"
6
+ fi
7
+
8
+ if [[ "$PROJECT_ROOT" != /tmp/okstra-validate.* ]]; then
9
+ fail "Refusing to reset unexpected PROJECT_ROOT: $PROJECT_ROOT"
10
+ fi
11
+ }
12
+
13
+ reset_validation_root() {
14
+ rm -rf "$PROJECT_ROOT"
15
+ mkdir -p "$PROJECT_ROOT"
16
+ }
17
+
18
+ task_key() {
19
+ local task_group="$1"
20
+ local task_id="$2"
21
+
22
+ printf '%s:%s:%s\n' "$PROJECT_ID" "$task_group" "$task_id"
23
+ }
24
+
25
+ task_root() {
26
+ local task_group="$1"
27
+ local task_id="$2"
28
+
29
+ printf '%s/.project-docs/okstra/tasks/%s/%s\n' "$PROJECT_ROOT" "$task_group" "$task_id"
30
+ }
31
+
32
+ task_manifest_relative_path() {
33
+ local task_group="$1"
34
+ local task_id="$2"
35
+
36
+ printf '.project-docs/okstra/tasks/%s/%s/task-manifest.json\n' "$task_group" "$task_id"
37
+ }
38
+
39
+ reference_expectations_relative_path() {
40
+ local task_group="$1"
41
+ local task_id="$2"
42
+
43
+ printf '.project-docs/okstra/tasks/%s/%s/instruction-set/reference-expectations.md\n' "$task_group" "$task_id"
44
+ }
@@ -0,0 +1,140 @@
1
+ # shellcheck shell=bash
2
+
3
+ run_okstra() {
4
+ local task_group="$1"
5
+ local task_id="$2"
6
+ local brief_filename="$3"
7
+ shift 3
8
+
9
+ if (($# > 0)); then
10
+ local -a extra_args=("$@")
11
+ bash "$OKSTRA_SCRIPT" \
12
+ --render-only \
13
+ --yes \
14
+ --task-type "$TASK_TYPE" \
15
+ --project-id "$PROJECT_ID" \
16
+ --project-root "$PROJECT_ROOT" \
17
+ --task-group "$task_group" \
18
+ --task-id "$task_id" \
19
+ --task-brief "$brief_filename" \
20
+ "${extra_args[@]}"
21
+ return
22
+ fi
23
+
24
+ bash "$OKSTRA_SCRIPT" \
25
+ --render-only \
26
+ --yes \
27
+ --task-type "$TASK_TYPE" \
28
+ --project-id "$PROJECT_ID" \
29
+ --project-root "$PROJECT_ROOT" \
30
+ --task-group "$task_group" \
31
+ --task-id "$task_id" \
32
+ --task-brief "$brief_filename"
33
+ }
34
+
35
+ run_validator_expectation() {
36
+ local task_group="$1"
37
+ local task_id="$2"
38
+ local expected_status="$3"
39
+ local expected_failure_substring="${4-}"
40
+ local expected_task_manifest_relative_path=""
41
+
42
+ expected_task_manifest_relative_path="$(task_manifest_relative_path "$task_group" "$task_id")"
43
+
44
+ python3 - "$PROJECT_ROOT" "$expected_task_manifest_relative_path" "$RUN_VALIDATOR_SCRIPT" "$expected_status" "$expected_failure_substring" <<'PY'
45
+ from pathlib import Path
46
+ import json
47
+ import subprocess
48
+ import sys
49
+
50
+ project_root = Path(sys.argv[1])
51
+ task_manifest_path = project_root / sys.argv[2]
52
+ validator_script = Path(sys.argv[3])
53
+ expected_status = sys.argv[4]
54
+ expected_failure_substring = sys.argv[5]
55
+
56
+ task_manifest = json.loads(task_manifest_path.read_text())
57
+ timeline_path = project_root / task_manifest["historyTimelinePath"]
58
+ timeline = json.loads(timeline_path.read_text())
59
+ runs = timeline.get("runs", [])
60
+ latest_run = None
61
+ if isinstance(runs, list):
62
+ for item in reversed(runs):
63
+ if isinstance(item, dict):
64
+ latest_run = item
65
+ break
66
+
67
+ if latest_run is None:
68
+ raise SystemExit("timeline does not contain a latest run entry")
69
+
70
+ run_manifest_path = project_root / latest_run["runManifestPath"]
71
+ run_manifest = json.loads(run_manifest_path.read_text())
72
+ team_state_path = project_root / run_manifest["teamStatePath"]
73
+ report_path = project_root / run_manifest["expectedReportPath"]
74
+ final_status_path = project_root / run_manifest["expectedStatusPath"]
75
+
76
+ process = subprocess.run(
77
+ [
78
+ sys.executable,
79
+ str(validator_script),
80
+ "--team-state",
81
+ str(team_state_path),
82
+ "--report",
83
+ str(report_path),
84
+ "--run-manifest",
85
+ str(run_manifest_path),
86
+ "--task-manifest",
87
+ str(task_manifest_path),
88
+ "--final-status",
89
+ str(final_status_path),
90
+ ],
91
+ capture_output=True,
92
+ text=True,
93
+ )
94
+
95
+ if not process.stdout.strip():
96
+ raise SystemExit("validator did not produce JSON output")
97
+
98
+ payload = json.loads(process.stdout)
99
+ actual_status = payload.get("validationStatus")
100
+ if actual_status != expected_status:
101
+ raise SystemExit(
102
+ f"validator status mismatch: expected {expected_status}, got {actual_status}"
103
+ )
104
+
105
+ if expected_status == "passed" and process.returncode != 0:
106
+ raise SystemExit(f"validator returned non-zero on success: {process.stderr}")
107
+ if expected_status == "failed" and process.returncode == 0:
108
+ raise SystemExit("validator unexpectedly succeeded")
109
+
110
+ if expected_failure_substring:
111
+ failures = payload.get("failures", [])
112
+ if not isinstance(failures, list) or not any(
113
+ expected_failure_substring in str(item) for item in failures
114
+ ):
115
+ raise SystemExit(
116
+ f"validator failure output did not include expected text: {expected_failure_substring}"
117
+ )
118
+
119
+ if not final_status_path.is_file():
120
+ raise SystemExit(f"validator did not write final status file: {final_status_path}")
121
+
122
+ final_status = final_status_path.read_text().strip()
123
+ expected_final_status = "completed" if expected_status == "passed" else "contract-violated"
124
+ if final_status != expected_final_status:
125
+ raise SystemExit(
126
+ f"final status file mismatch: expected {expected_final_status}, got {final_status}"
127
+ )
128
+
129
+ updated_run_manifest = json.loads(run_manifest_path.read_text())
130
+ updated_task_manifest = json.loads(task_manifest_path.read_text())
131
+ updated_team_state = json.loads(team_state_path.read_text())
132
+
133
+ if updated_run_manifest.get("validation", {}).get("status") != expected_status:
134
+ raise SystemExit("run manifest validation status was not updated correctly")
135
+ if updated_task_manifest.get("contractValidation", {}).get("status") != expected_status:
136
+ raise SystemExit("task manifest validation status was not updated correctly")
137
+ if updated_team_state.get("validator", {}).get("status") != expected_status:
138
+ raise SystemExit("team-state validator status was not updated correctly")
139
+ PY
140
+ }
@@ -0,0 +1,15 @@
1
+ # shellcheck shell=bash
2
+
3
+ print_summary() {
4
+ cat <<EOF
5
+
6
+ Validation completed successfully.
7
+ - Validation root: $PROJECT_ROOT
8
+ - Latest-task discovery file: $DISCOVERY_FILE
9
+ - Task catalog file: $CATALOG_FILE
10
+ - Primary task root: $(task_root "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID")
11
+ - Secondary task root: $(task_root "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID")
12
+
13
+ You can inspect the generated artifacts directly under the validation root.
14
+ EOF
15
+ }
@@ -0,0 +1,44 @@
1
+ # shellcheck shell=bash
2
+
3
+ validate_seeded_assets() {
4
+ local validation_mode="$1"
5
+
6
+ python3 - "$SOURCE_ASSET_ROOT" "$PROJECT_ROOT/.claude" "$validation_mode" <<'PY'
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ source_root = Path(sys.argv[1])
11
+ target_root = Path(sys.argv[2])
12
+ validation_mode = sys.argv[3]
13
+ errors = []
14
+
15
+ for source_path in sorted(source_root.rglob("*.md")):
16
+ relative_path = source_path.relative_to(source_root)
17
+ parts = relative_path.parts
18
+
19
+ if relative_path.as_posix() == "SKILL.md":
20
+ target_path = target_root / "skills" / "okstra" / "SKILL.md"
21
+ elif parts[0] == "skills":
22
+ target_path = target_root / "skills" / Path(*parts[1:])
23
+ elif parts[0] == "workers":
24
+ # `agents/workers/<name>.md` 는 `.claude/agents/<name>.md` 로 시드된다.
25
+ # seeding.sh 의 분기와 동일하게 유지해야 한다.
26
+ target_path = target_root / "agents" / Path(*parts[1:])
27
+ elif parts[0] == "agents":
28
+ target_path = target_root / "agents" / Path(*parts[1:])
29
+ else:
30
+ target_path = target_root / "skills" / "okstra" / relative_path
31
+
32
+ if not target_path.is_file():
33
+ errors.append(f"missing seeded asset: {target_path}")
34
+ continue
35
+
36
+ if validation_mode == "match" and target_path.read_bytes() != source_path.read_bytes():
37
+ errors.append(f"seeded asset content does not match source: {target_path}")
38
+
39
+ if errors:
40
+ for error in errors:
41
+ print(error, file=sys.stderr)
42
+ sys.exit(1)
43
+ PY
44
+ }