okstra 0.26.0 → 0.27.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.kr.md +15 -0
- package/README.md +15 -0
- package/docs/kr/architecture.md +2 -6
- package/docs/kr/cli.md +40 -6
- package/docs/kr/performance-improvement-plan-v2.md +23 -0
- package/docs/kr/performance-improvement-plan.md +22 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra.sh +0 -1
- package/runtime/prompts/profiles/_common-contract.md +25 -1
- package/runtime/prompts/profiles/error-analysis.md +12 -0
- package/runtime/prompts/profiles/implementation-planning.md +20 -0
- package/runtime/prompts/profiles/requirements-discovery.md +20 -0
- package/runtime/python/lib/okstra/cli.sh +1 -7
- package/runtime/python/lib/okstra/globals.sh +0 -1
- package/runtime/python/lib/okstra/usage.sh +1 -4
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/run.py +0 -6
- package/runtime/python/okstra_ctl/run_context.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +25 -2
- package/runtime/python/okstra_token_usage/blocks.py +5 -1
- package/runtime/python/okstra_token_usage/claude.py +16 -1
- package/runtime/python/okstra_token_usage/collect.py +17 -3
- package/runtime/python/okstra_token_usage/pricing.py +159 -24
- package/runtime/skills/okstra-brief/SKILL.md +532 -65
- package/runtime/skills/okstra-context-loader/SKILL.md +25 -11
- package/runtime/skills/okstra-convergence/SKILL.md +37 -13
- package/runtime/skills/okstra-history/SKILL.md +68 -37
- package/runtime/skills/okstra-logs/SKILL.md +26 -4
- package/runtime/skills/okstra-report-finder/SKILL.md +49 -22
- package/runtime/skills/okstra-report-writer/SKILL.md +59 -64
- package/runtime/skills/okstra-run/SKILL.md +35 -34
- package/runtime/skills/okstra-schedule/SKILL.md +51 -20
- package/runtime/skills/okstra-setup/SKILL.md +31 -12
- package/runtime/skills/okstra-status/SKILL.md +20 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -15
- package/runtime/skills/okstra-time-summary/SKILL.md +53 -16
- package/runtime/templates/reports/settings.template.json +7 -4
- package/runtime/validators/lib/fixtures.sh +10 -2
- package/runtime/validators/lib/validate-assets.sh +50 -24
- package/runtime/validators/validate-brief.py +385 -0
- package/runtime/validators/validate-brief.sh +35 -0
- package/runtime/validators/validate-workflow.sh +7 -33
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate brief markdown files produced by the okstra-brief skill.
|
|
3
|
+
|
|
4
|
+
Checks performed per brief file:
|
|
5
|
+
|
|
6
|
+
1. YAML frontmatter exists on line 1 with required keys.
|
|
7
|
+
2. brief-id matches the filename stem.
|
|
8
|
+
3. depth equals the number of `sub/` segments in the path (relative to the
|
|
9
|
+
`briefs/` root).
|
|
10
|
+
4. Every Open Questions row starts with one of the five signal prefixes
|
|
11
|
+
(general | terminology | intent-check | conversion-block | adr-candidate).
|
|
12
|
+
`adr-candidate:` targets okstra-internal
|
|
13
|
+
`<PROJECT_ROOT>/.project-docs/okstra/decisions/`, not external `docs/adr/`.
|
|
14
|
+
5. Every Augmentation entry (inline `> augmented: <label>` blockquotes and
|
|
15
|
+
`Augmentation` section bullets) carries one of the four labels
|
|
16
|
+
(evidence-link | format-conversion | terminology-mapping | intent-inference).
|
|
17
|
+
Both documented forms are accepted: `label: ...` and `label — ...`.
|
|
18
|
+
6. Every `intent-inference` augmentation has a corresponding
|
|
19
|
+
`intent-check:` row in Open Questions (auto-mirroring rule).
|
|
20
|
+
7. Every `terminology-mapping` augmentation (excluding Step 4.5 outcome
|
|
21
|
+
markers `applied glossary:` / `skipped glossary:`) has a corresponding
|
|
22
|
+
`terminology:` row in Open Questions.
|
|
23
|
+
8. `parent-id` chain: at depth 0 the value MUST be the literal `self`;
|
|
24
|
+
at depth ≥ 1 it MUST NOT be `self` and MUST differ from the brief's
|
|
25
|
+
own `brief-id`.
|
|
26
|
+
9. `reporter-confirmations` consistency: when `complete`, every
|
|
27
|
+
`intent-check:` and `conversion-block:` row in Open Questions MUST
|
|
28
|
+
carry a `[CONFIRMED YYYY-MM-DD → RC-N]` marker.
|
|
29
|
+
|
|
30
|
+
Exit code 0 on PASS, 1 on FAIL.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import re
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Iterable
|
|
40
|
+
|
|
41
|
+
REQUIRED_FRONTMATTER_KEYS = {
|
|
42
|
+
"type",
|
|
43
|
+
"brief-id",
|
|
44
|
+
"parent-id",
|
|
45
|
+
"ticket-id",
|
|
46
|
+
"source-type",
|
|
47
|
+
"task-group",
|
|
48
|
+
"depth",
|
|
49
|
+
"created",
|
|
50
|
+
"generator",
|
|
51
|
+
"reporter-confirmations",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
OPEN_QUESTIONS_PREFIXES = {
|
|
55
|
+
"general:",
|
|
56
|
+
"terminology:",
|
|
57
|
+
"intent-check:",
|
|
58
|
+
"conversion-block:",
|
|
59
|
+
"adr-candidate:",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
AUGMENTATION_LABELS = {
|
|
63
|
+
"evidence-link",
|
|
64
|
+
"format-conversion",
|
|
65
|
+
"terminology-mapping",
|
|
66
|
+
"intent-inference",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
REPORTER_CONFIRMATION_VALUES = {"complete", "partial", "pending", "skipped"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, str], int]:
|
|
73
|
+
"""Return (frontmatter dict, line after closing `---`)."""
|
|
74
|
+
lines = text.splitlines()
|
|
75
|
+
if not lines or lines[0].strip() != "---":
|
|
76
|
+
raise ValueError("missing opening frontmatter delimiter on line 1")
|
|
77
|
+
out: dict[str, str] = {}
|
|
78
|
+
for idx in range(1, len(lines)):
|
|
79
|
+
line = lines[idx]
|
|
80
|
+
if line.strip() == "---":
|
|
81
|
+
return out, idx + 1
|
|
82
|
+
# naive key: value (comments after #)
|
|
83
|
+
bare = line.split("#", 1)[0].strip()
|
|
84
|
+
if not bare:
|
|
85
|
+
continue
|
|
86
|
+
if ":" not in bare:
|
|
87
|
+
raise ValueError(f"frontmatter line without colon: {line!r}")
|
|
88
|
+
key, _, value = bare.partition(":")
|
|
89
|
+
out[key.strip()] = value.strip()
|
|
90
|
+
raise ValueError("missing closing frontmatter delimiter")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def section_body(text: str, heading: str) -> str:
|
|
94
|
+
"""Return the body lines between `## <heading>` and the next `## ` heading."""
|
|
95
|
+
pattern = re.compile(
|
|
96
|
+
r"^##\s+" + re.escape(heading) + r"\s*$(.*?)(?=^##\s|\Z)",
|
|
97
|
+
re.MULTILINE | re.DOTALL,
|
|
98
|
+
)
|
|
99
|
+
match = pattern.search(text)
|
|
100
|
+
if not match:
|
|
101
|
+
return ""
|
|
102
|
+
return match.group(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_placeholder(line: str) -> bool:
|
|
106
|
+
bare = line.strip().lstrip("-").strip()
|
|
107
|
+
return bare in {"_(none)_", "_(none — pending or skipped)_", ""}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_template_example(line: str) -> bool:
|
|
111
|
+
"""Lines that are template scaffolding (placeholder/example), not real entries."""
|
|
112
|
+
bare = line.strip().lstrip("-").strip()
|
|
113
|
+
return bare.startswith("<") and bare.endswith(">")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def open_questions_rows(text: str) -> list[str]:
|
|
117
|
+
body = section_body(text, "Open Questions")
|
|
118
|
+
rows: list[str] = []
|
|
119
|
+
for line in body.splitlines():
|
|
120
|
+
stripped = line.strip()
|
|
121
|
+
if not stripped.startswith("- "):
|
|
122
|
+
continue
|
|
123
|
+
content = stripped[2:].strip()
|
|
124
|
+
if is_placeholder(content) or is_template_example(content):
|
|
125
|
+
continue
|
|
126
|
+
# strip backticks if the row body is wrapped in `…`
|
|
127
|
+
content = content.strip("`")
|
|
128
|
+
rows.append(content)
|
|
129
|
+
return rows
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def augmentation_entries(text: str) -> list[str]:
|
|
133
|
+
"""Bullets under the `## Augmentation` section (entries that look like real data)."""
|
|
134
|
+
body = section_body(text, "Augmentation")
|
|
135
|
+
entries: list[str] = []
|
|
136
|
+
for line in body.splitlines():
|
|
137
|
+
stripped = line.strip()
|
|
138
|
+
if not stripped.startswith("- "):
|
|
139
|
+
continue
|
|
140
|
+
content = stripped[2:].strip()
|
|
141
|
+
if is_placeholder(content) or is_template_example(content):
|
|
142
|
+
continue
|
|
143
|
+
# strip backticks
|
|
144
|
+
content = content.strip("`")
|
|
145
|
+
entries.append(content)
|
|
146
|
+
return entries
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def inline_augmented_blockquotes(text: str) -> list[str]:
|
|
150
|
+
"""Lines starting with `> augmented:`."""
|
|
151
|
+
out: list[str] = []
|
|
152
|
+
for line in text.splitlines():
|
|
153
|
+
stripped = line.strip()
|
|
154
|
+
if stripped.startswith("> augmented:"):
|
|
155
|
+
payload = stripped[len("> augmented:"):].strip()
|
|
156
|
+
if payload.startswith("<") and payload.endswith(">"):
|
|
157
|
+
# template scaffold, e.g. `> augmented: <label> — <interpretation>`
|
|
158
|
+
continue
|
|
159
|
+
out.append(payload)
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_augmentation_label(entry: str) -> tuple[str | None, str]:
|
|
164
|
+
"""Return (label, payload) for documented augmentation forms."""
|
|
165
|
+
stripped = entry.strip()
|
|
166
|
+
for label in AUGMENTATION_LABELS:
|
|
167
|
+
if stripped == label:
|
|
168
|
+
return label, ""
|
|
169
|
+
for sep in (":", " — ", " - "):
|
|
170
|
+
prefix = f"{label}{sep}"
|
|
171
|
+
if stripped.startswith(prefix):
|
|
172
|
+
return label, stripped[len(prefix):].strip()
|
|
173
|
+
return None, stripped
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def validate_brief(path: Path, briefs_root: Path) -> list[str]:
|
|
177
|
+
text = path.read_text(encoding="utf-8")
|
|
178
|
+
errors: list[str] = []
|
|
179
|
+
|
|
180
|
+
# 1. frontmatter
|
|
181
|
+
try:
|
|
182
|
+
fm, _ = parse_frontmatter(text)
|
|
183
|
+
except ValueError as exc:
|
|
184
|
+
return [f"frontmatter: {exc}"]
|
|
185
|
+
|
|
186
|
+
missing = REQUIRED_FRONTMATTER_KEYS - fm.keys()
|
|
187
|
+
if missing:
|
|
188
|
+
errors.append(f"frontmatter missing keys: {sorted(missing)}")
|
|
189
|
+
|
|
190
|
+
if fm.get("type") != "brief":
|
|
191
|
+
errors.append(f"frontmatter type must be 'brief', got {fm.get('type')!r}")
|
|
192
|
+
|
|
193
|
+
if fm.get("generator") != "okstra-brief":
|
|
194
|
+
errors.append(
|
|
195
|
+
f"frontmatter generator must be 'okstra-brief', got {fm.get('generator')!r}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if fm.get("reporter-confirmations") not in REPORTER_CONFIRMATION_VALUES:
|
|
199
|
+
errors.append(
|
|
200
|
+
"frontmatter reporter-confirmations must be one of "
|
|
201
|
+
f"{sorted(REPORTER_CONFIRMATION_VALUES)}, got "
|
|
202
|
+
f"{fm.get('reporter-confirmations')!r}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# 2. brief-id matches filename stem
|
|
206
|
+
stem = path.stem
|
|
207
|
+
if fm.get("brief-id") and fm["brief-id"] != stem:
|
|
208
|
+
errors.append(
|
|
209
|
+
f"brief-id {fm['brief-id']!r} does not match filename stem {stem!r}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# 3. depth equals path's `sub/` nesting depth
|
|
213
|
+
try:
|
|
214
|
+
rel = path.relative_to(briefs_root)
|
|
215
|
+
except ValueError:
|
|
216
|
+
rel = path
|
|
217
|
+
# path components after the task-group dir: any number of `sub` segments + filename
|
|
218
|
+
parts = list(rel.parts)
|
|
219
|
+
if len(parts) >= 2:
|
|
220
|
+
nested = [p for p in parts[1:-1] if p == "sub"]
|
|
221
|
+
expected_depth = len(nested)
|
|
222
|
+
try:
|
|
223
|
+
actual_depth = int(fm.get("depth", "0"))
|
|
224
|
+
except ValueError:
|
|
225
|
+
actual_depth = -1
|
|
226
|
+
if actual_depth != expected_depth:
|
|
227
|
+
errors.append(
|
|
228
|
+
f"depth mismatch: path has {expected_depth} `sub/` segments, "
|
|
229
|
+
f"frontmatter says depth={fm.get('depth')!r}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# 4. Open Questions prefixes
|
|
233
|
+
oq_rows = open_questions_rows(text)
|
|
234
|
+
intent_check_rows: list[str] = []
|
|
235
|
+
terminology_rows: list[str] = []
|
|
236
|
+
conversion_block_rows: list[str] = []
|
|
237
|
+
for row in oq_rows:
|
|
238
|
+
if not any(row.startswith(prefix) for prefix in OPEN_QUESTIONS_PREFIXES):
|
|
239
|
+
errors.append(f"Open Questions row lacks a known prefix: {row!r}")
|
|
240
|
+
if row.startswith("intent-check:"):
|
|
241
|
+
intent_check_rows.append(row)
|
|
242
|
+
elif row.startswith("terminology:"):
|
|
243
|
+
terminology_rows.append(row)
|
|
244
|
+
elif row.startswith("conversion-block:"):
|
|
245
|
+
conversion_block_rows.append(row)
|
|
246
|
+
|
|
247
|
+
# 5. Augmentation labels
|
|
248
|
+
augmentation_lines: list[str] = []
|
|
249
|
+
augmentation_lines.extend(augmentation_entries(text))
|
|
250
|
+
augmentation_lines.extend(inline_augmented_blockquotes(text))
|
|
251
|
+
intent_inference_count = 0
|
|
252
|
+
terminology_mapping_count = 0
|
|
253
|
+
for entry in augmentation_lines:
|
|
254
|
+
label, payload = parse_augmentation_label(entry)
|
|
255
|
+
if label not in AUGMENTATION_LABELS:
|
|
256
|
+
errors.append(
|
|
257
|
+
f"Augmentation entry lacks a known label: {entry!r} "
|
|
258
|
+
f"(label parsed as {label!r})"
|
|
259
|
+
)
|
|
260
|
+
continue
|
|
261
|
+
if label == "intent-inference":
|
|
262
|
+
intent_inference_count += 1
|
|
263
|
+
elif label == "terminology-mapping":
|
|
264
|
+
# Step 4.5 outcome markers do not need a paired Open Questions row.
|
|
265
|
+
if payload.startswith("applied glossary:") or payload.startswith(
|
|
266
|
+
"skipped glossary:"
|
|
267
|
+
):
|
|
268
|
+
continue
|
|
269
|
+
terminology_mapping_count += 1
|
|
270
|
+
|
|
271
|
+
# 6. auto-mirroring rule (intent-inference ↔ intent-check:)
|
|
272
|
+
if intent_inference_count > len(intent_check_rows):
|
|
273
|
+
errors.append(
|
|
274
|
+
f"intent-inference augmentations present ({intent_inference_count}) "
|
|
275
|
+
f"but only {len(intent_check_rows)} intent-check: row(s) in Open Questions"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# 7. dual-record rule (terminology-mapping ↔ terminology:)
|
|
279
|
+
if terminology_mapping_count > 0 and not terminology_rows:
|
|
280
|
+
errors.append(
|
|
281
|
+
f"terminology-mapping augmentations present ({terminology_mapping_count}) "
|
|
282
|
+
f"but no terminology: row(s) in Open Questions"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# 8. parent-id chain
|
|
286
|
+
parent_id = fm.get("parent-id", "")
|
|
287
|
+
brief_id = fm.get("brief-id", "")
|
|
288
|
+
try:
|
|
289
|
+
depth_value = int(fm.get("depth", "0"))
|
|
290
|
+
except ValueError:
|
|
291
|
+
depth_value = -1
|
|
292
|
+
if depth_value == 0:
|
|
293
|
+
if parent_id != "self":
|
|
294
|
+
errors.append(
|
|
295
|
+
f"parent-id for the root (depth 0) brief must be 'self', "
|
|
296
|
+
f"got {parent_id!r}"
|
|
297
|
+
)
|
|
298
|
+
elif depth_value > 0:
|
|
299
|
+
if parent_id == "self":
|
|
300
|
+
errors.append(
|
|
301
|
+
f"parent-id for a descendant (depth {depth_value}) brief must not be 'self'"
|
|
302
|
+
)
|
|
303
|
+
elif parent_id == brief_id:
|
|
304
|
+
errors.append(
|
|
305
|
+
f"parent-id for a descendant brief must differ from its own brief-id "
|
|
306
|
+
f"({brief_id!r})"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# 9. reporter-confirmations consistency
|
|
310
|
+
rc_status = fm.get("reporter-confirmations")
|
|
311
|
+
if rc_status == "complete":
|
|
312
|
+
unconfirmed = [
|
|
313
|
+
row
|
|
314
|
+
for row in (intent_check_rows + conversion_block_rows)
|
|
315
|
+
if "[CONFIRMED" not in row
|
|
316
|
+
]
|
|
317
|
+
if unconfirmed:
|
|
318
|
+
sample = unconfirmed[0]
|
|
319
|
+
errors.append(
|
|
320
|
+
f"reporter-confirmations is 'complete' but {len(unconfirmed)} "
|
|
321
|
+
f"intent-check:/conversion-block: row(s) lack a [CONFIRMED …] "
|
|
322
|
+
f"marker (e.g. {sample!r})"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return errors
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def find_briefs(root: Path) -> Iterable[Path]:
|
|
329
|
+
yield from root.rglob("*.md")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def main(argv: list[str] | None = None) -> int:
|
|
333
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
334
|
+
parser.add_argument(
|
|
335
|
+
"briefs_dir",
|
|
336
|
+
type=Path,
|
|
337
|
+
help="Directory containing brief markdown files (recursed).",
|
|
338
|
+
)
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--briefs-root",
|
|
341
|
+
type=Path,
|
|
342
|
+
default=None,
|
|
343
|
+
help=(
|
|
344
|
+
"Root used for depth computation (defaults to briefs_dir). "
|
|
345
|
+
"Usually `<PROJECT_ROOT>/.project-docs/okstra/briefs`."
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
args = parser.parse_args(argv)
|
|
349
|
+
|
|
350
|
+
briefs_dir: Path = args.briefs_dir
|
|
351
|
+
if not briefs_dir.exists():
|
|
352
|
+
print(f"[FAIL] briefs directory not found: {briefs_dir}", file=sys.stderr)
|
|
353
|
+
return 1
|
|
354
|
+
|
|
355
|
+
briefs_root: Path = args.briefs_root or briefs_dir
|
|
356
|
+
|
|
357
|
+
total = 0
|
|
358
|
+
failed_files: list[tuple[Path, list[str]]] = []
|
|
359
|
+
for brief in find_briefs(briefs_dir):
|
|
360
|
+
total += 1
|
|
361
|
+
errors = validate_brief(brief, briefs_root)
|
|
362
|
+
if errors:
|
|
363
|
+
failed_files.append((brief, errors))
|
|
364
|
+
|
|
365
|
+
if total == 0:
|
|
366
|
+
print(f"[PASS] no briefs found under {briefs_dir} (nothing to validate)")
|
|
367
|
+
return 0
|
|
368
|
+
|
|
369
|
+
if not failed_files:
|
|
370
|
+
print(f"[PASS] {total} brief(s) validated under {briefs_dir}")
|
|
371
|
+
return 0
|
|
372
|
+
|
|
373
|
+
for path, errors in failed_files:
|
|
374
|
+
print(f"[FAIL] {path}")
|
|
375
|
+
for err in errors:
|
|
376
|
+
print(f" - {err}")
|
|
377
|
+
print(
|
|
378
|
+
f"[FAIL] {len(failed_files)}/{total} brief(s) failed validation",
|
|
379
|
+
file=sys.stderr,
|
|
380
|
+
)
|
|
381
|
+
return 1
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if __name__ == "__main__":
|
|
385
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Validate brief markdown files produced by okstra-brief.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# validators/validate-brief.sh <briefs-dir> [--briefs-root <dir>]
|
|
7
|
+
#
|
|
8
|
+
# Typical invocation (inside a project that has run okstra-setup):
|
|
9
|
+
# validators/validate-brief.sh "$PROJECT_ROOT/.project-docs/okstra/briefs"
|
|
10
|
+
#
|
|
11
|
+
# Thin bash entrypoint — delegates to validate-brief.py for content checks.
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SOURCE_PATH="${BASH_SOURCE[0]}"
|
|
16
|
+
while [[ -L "$SOURCE_PATH" ]]; do
|
|
17
|
+
SOURCE_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
|
|
18
|
+
SOURCE_PATH="$(readlink "$SOURCE_PATH")"
|
|
19
|
+
[[ "$SOURCE_PATH" != /* ]] && SOURCE_PATH="$SOURCE_DIR/$SOURCE_PATH"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
|
|
23
|
+
PYTHON_VALIDATOR="$SCRIPT_DIR/validate-brief.py"
|
|
24
|
+
|
|
25
|
+
if [[ ! -f "$PYTHON_VALIDATOR" ]]; then
|
|
26
|
+
echo "[FAIL] python helper not found: $PYTHON_VALIDATOR" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [[ $# -lt 1 ]]; then
|
|
31
|
+
echo "usage: $0 <briefs-dir> [--briefs-root <dir>]" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
exec python3 "$PYTHON_VALIDATOR" "$@"
|
|
@@ -18,7 +18,6 @@ OKSTRA_SCRIPT="$WORKSPACE_ROOT/scripts/okstra.sh"
|
|
|
18
18
|
RUN_VALIDATOR_SCRIPT="$WORKSPACE_ROOT/validators/validate-run.py"
|
|
19
19
|
SOURCE_ASSET_ROOT="$WORKSPACE_ROOT/agents"
|
|
20
20
|
TASK_TYPE="final-verification"
|
|
21
|
-
MARKER="MANUAL-VALIDATION-MARKER"
|
|
22
21
|
PRIMARY_TASK_GROUP="validation"
|
|
23
22
|
PRIMARY_TASK_ID="asset-refresh-and-reference-expectations"
|
|
24
23
|
PRIMARY_BRIEF_FILENAME="validation-brief-primary.md"
|
|
@@ -56,7 +55,6 @@ PRIMARY_BRIEF_PATH="$PROJECT_ROOT/$PRIMARY_BRIEF_FILENAME"
|
|
|
56
55
|
SECONDARY_BRIEF_PATH="$PROJECT_ROOT/$SECONDARY_BRIEF_FILENAME"
|
|
57
56
|
DISCOVERY_FILE="$PROJECT_ROOT/$LATEST_TASK_RELATIVE_PATH"
|
|
58
57
|
CATALOG_FILE="$PROJECT_ROOT/$TASK_CATALOG_RELATIVE_PATH"
|
|
59
|
-
MARKER_FILE="$PROJECT_ROOT/.claude/agents/codex-worker.md"
|
|
60
58
|
PRIMARY_TASK_KEY="$(task_key "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID")"
|
|
61
59
|
SECONDARY_TASK_KEY="$(task_key "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID")"
|
|
62
60
|
PRIMARY_REFERENCE_EXPECTATIONS_FILE="$(task_root "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID")/instruction-set/reference-expectations.md"
|
|
@@ -155,36 +153,12 @@ if ! validate_task_catalog "$SECONDARY_TASK_KEY" "$PRIMARY_TASK_KEY" "$SECONDARY
|
|
|
155
153
|
fi
|
|
156
154
|
pass "latest-task.json and task-catalog.json now reflect distinct primary and secondary tasks"
|
|
157
155
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
assert_contains "$MARKER_FILE" "$MARKER"
|
|
166
|
-
if ! validate_latest_task_pointer "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
167
|
-
fail "latest-task.json changed unexpectedly during secondary rerun without refresh"
|
|
168
|
-
fi
|
|
169
|
-
if ! validate_task_catalog "$SECONDARY_TASK_KEY" "$PRIMARY_TASK_KEY" "$SECONDARY_TASK_KEY"; then
|
|
170
|
-
fail "task-catalog.json changed unexpectedly during secondary rerun without refresh"
|
|
171
|
-
fi
|
|
172
|
-
pass "Rerun without refresh preserved the modified project-local asset and retained both catalog entries"
|
|
173
|
-
|
|
174
|
-
step "Verifying that rerun with refresh regenerates project-local assets"
|
|
175
|
-
if ! run_okstra "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID" "$SECONDARY_BRIEF_FILENAME" --refresh-assets; then
|
|
176
|
-
fail "Secondary task rerun with --refresh-assets failed"
|
|
177
|
-
fi
|
|
178
|
-
assert_not_contains "$MARKER_FILE" "$MARKER"
|
|
179
|
-
if ! validate_seeded_assets match; then
|
|
180
|
-
fail "Refreshed project-local okstra assets do not match the source files"
|
|
181
|
-
fi
|
|
182
|
-
if ! validate_latest_task_pointer "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
183
|
-
fail "latest-task.json became invalid after refresh"
|
|
184
|
-
fi
|
|
185
|
-
if ! validate_task_catalog "$SECONDARY_TASK_KEY" "$PRIMARY_TASK_KEY" "$SECONDARY_TASK_KEY"; then
|
|
186
|
-
fail "task-catalog.json became invalid after refresh"
|
|
187
|
-
fi
|
|
188
|
-
pass "Refresh regenerated the mapped project-local okstra assets while preserving both catalog entries"
|
|
156
|
+
# Removed: the historical "rerun without refresh preserves project-local
|
|
157
|
+
# assets" and "rerun with refresh regenerates project-local assets" stanzas.
|
|
158
|
+
# Those tested a contract from when `okstra install` seeded per-project
|
|
159
|
+
# `.claude/` files; install now writes only to `$HOME/.claude` and
|
|
160
|
+
# `$HOME/.okstra`, so the project-local sentinel is no longer a meaningful
|
|
161
|
+
# contract. The `--refresh-assets` flag was removed entirely in a paired
|
|
162
|
+
# commit; users with old scripts should switch to `okstra install --refresh`.
|
|
189
163
|
|
|
190
164
|
print_summary
|