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,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
|
+
}
|