okstra 0.50.0 → 0.51.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 +8 -7
- package/README.md +8 -7
- package/bin/okstra +2 -0
- package/docs/kr/architecture.md +15 -16
- package/docs/kr/cli.md +5 -5
- package/docs/project-structure-overview.md +10 -6
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +15 -11
- package/runtime/agents/workers/claude-worker.md +3 -3
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/gemini-worker.md +2 -2
- package/runtime/bin/lib/okstra/cli.sh +8 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/interactive.sh +14 -12
- package/runtime/bin/lib/okstra/usage.sh +6 -0
- package/runtime/bin/okstra-team-reconcile.sh +28 -0
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/launch.template.md +3 -1
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/implementation.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +259 -0
- package/runtime/python/okstra_ctl/context_cost.py +308 -0
- package/runtime/python/okstra_ctl/migrate.py +2 -12
- package/runtime/python/okstra_ctl/paths.py +22 -0
- package/runtime/python/okstra_ctl/render.py +284 -125
- package/runtime/python/okstra_ctl/render_final_report.py +31 -0
- package/runtime/python/okstra_ctl/run.py +507 -245
- package/runtime/python/okstra_ctl/sequence.py +2 -5
- package/runtime/python/okstra_ctl/team_reconcile.py +131 -0
- package/runtime/python/okstra_ctl/wizard.py +129 -133
- package/runtime/python/okstra_ctl/worktree.py +13 -5
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/skills/okstra-coding-preflight/SKILL.md +69 -0
- package/runtime/skills/okstra-coding-preflight/architecture/hexagonal.md +116 -0
- package/runtime/skills/okstra-coding-preflight/clean-code.md +254 -0
- package/runtime/skills/okstra-coding-preflight/languages/java.md +64 -0
- package/runtime/skills/okstra-coding-preflight/languages/javascript-typescript.md +87 -0
- package/runtime/skills/okstra-coding-preflight/languages/kotlin.md +69 -0
- package/runtime/skills/okstra-coding-preflight/languages/nodejs.md +66 -0
- package/runtime/skills/okstra-coding-preflight/languages/python.md +179 -0
- package/runtime/skills/okstra-coding-preflight/languages/rust.md +105 -0
- package/runtime/skills/okstra-coding-preflight/languages/sql.md +68 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +12 -6
- package/runtime/skills/okstra-inspect/SKILL.md +100 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +5 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-team-contract/SKILL.md +7 -4
- package/runtime/templates/reports/final-report.template.md +1 -0
- package/runtime/templates/worker-prompt-preamble.md +3 -3
- package/src/_python-helper.mjs +3 -3
- package/src/context-cost.mjs +27 -0
- package/src/install.mjs +1 -0
- package/src/uninstall.mjs +1 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Read-side context cost estimator for prepared okstra task bundles.
|
|
2
|
+
|
|
3
|
+
The estimator answers a narrow operational question: how much file/context
|
|
4
|
+
surface does the current task bundle ask the lead, analysis workers, and
|
|
5
|
+
report-writer to absorb? It does not mutate task artifacts.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
from okstra_project import ResolverError, find_task_root, resolve_project_root
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
INPUT_FILES = (
|
|
20
|
+
"task-brief.md",
|
|
21
|
+
"analysis-profile.md",
|
|
22
|
+
"analysis-material.md",
|
|
23
|
+
"reference-expectations.md",
|
|
24
|
+
"clarification-response.md",
|
|
25
|
+
)
|
|
26
|
+
TIMESTAMPED_ARTIFACT_RE = re.compile(r"\d{4}-\d{2}-\d{2}[_T]\d{2}-\d{2}-\d{2}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _file_size(path: Path) -> int:
|
|
30
|
+
return path.stat().st_size if path.is_file() else 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _count_files(paths: Iterable[Path]) -> tuple[int, int]:
|
|
34
|
+
file_count = 0
|
|
35
|
+
byte_count = 0
|
|
36
|
+
for path in paths:
|
|
37
|
+
if path.is_file():
|
|
38
|
+
file_count += 1
|
|
39
|
+
byte_count += path.stat().st_size
|
|
40
|
+
return file_count, byte_count
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _all_files(root: Path) -> list[Path]:
|
|
44
|
+
if not root.exists():
|
|
45
|
+
return []
|
|
46
|
+
return [path for path in root.rglob("*") if path.is_file()]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_json(path: Path) -> dict:
|
|
50
|
+
if not path.is_file():
|
|
51
|
+
return {}
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
54
|
+
except Exception:
|
|
55
|
+
return {}
|
|
56
|
+
return data if isinstance(data, dict) else {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_task_root(target: str, project_root: str, cwd: str) -> tuple[Path, Path]:
|
|
60
|
+
target_path = Path(target).expanduser()
|
|
61
|
+
if target_path.exists():
|
|
62
|
+
task_root = target_path.resolve()
|
|
63
|
+
return task_root, _infer_project_root(task_root)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
resolved_project = Path(
|
|
67
|
+
resolve_project_root(explicit_root=project_root, cwd=cwd)
|
|
68
|
+
).resolve()
|
|
69
|
+
except ResolverError as exc:
|
|
70
|
+
raise SystemExit(f"project root resolution failed: {exc}") from exc
|
|
71
|
+
|
|
72
|
+
task_root = find_task_root(resolved_project, target)
|
|
73
|
+
if task_root is None:
|
|
74
|
+
raise SystemExit(f"no task root for task key: {target}")
|
|
75
|
+
return task_root.resolve(), resolved_project
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _infer_project_root(task_root: Path) -> Path:
|
|
79
|
+
parts = task_root.parts
|
|
80
|
+
if ".okstra" not in parts:
|
|
81
|
+
return task_root
|
|
82
|
+
idx = parts.index(".okstra")
|
|
83
|
+
return Path(*parts[:idx]).resolve()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _project_rel(path: Path, project_root: Path) -> str:
|
|
87
|
+
try:
|
|
88
|
+
return str(path.resolve().relative_to(project_root.resolve()))
|
|
89
|
+
except ValueError:
|
|
90
|
+
return str(path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _find_current_run_dir(
|
|
94
|
+
task_root: Path, manifest: dict, project_root: Path
|
|
95
|
+
) -> Path | None:
|
|
96
|
+
artifacts = manifest.get("artifacts", {})
|
|
97
|
+
prompt_dir = artifacts.get("workerPromptsDirectoryPath")
|
|
98
|
+
if isinstance(prompt_dir, str) and prompt_dir:
|
|
99
|
+
path = project_root / prompt_dir
|
|
100
|
+
if path.is_dir():
|
|
101
|
+
return path.parent
|
|
102
|
+
|
|
103
|
+
task_type = manifest.get("workflow", {}).get("currentPhase") or manifest.get("taskType")
|
|
104
|
+
if not isinstance(task_type, str) or not task_type:
|
|
105
|
+
return None
|
|
106
|
+
candidate = task_root / "runs" / task_type
|
|
107
|
+
return candidate if candidate.is_dir() else None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _latest_matching_file(directory: Path, pattern: str) -> Path | None:
|
|
111
|
+
matches = sorted(directory.glob(pattern))
|
|
112
|
+
if not matches:
|
|
113
|
+
return None
|
|
114
|
+
return max(matches, key=lambda path: path.stat().st_mtime)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_timestamped_legacy_artifact(path: Path) -> bool:
|
|
118
|
+
return bool(TIMESTAMPED_ARTIFACT_RE.search(path.name))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _instruction_set_metric(task_root: Path, project_root: Path) -> dict:
|
|
122
|
+
instruction_set = task_root / "instruction-set"
|
|
123
|
+
files = _all_files(instruction_set)
|
|
124
|
+
file_count, byte_count = _count_files(files)
|
|
125
|
+
packet = instruction_set / "analysis-packet.md"
|
|
126
|
+
legacy_packet = instruction_set / "task-packet.md"
|
|
127
|
+
return {
|
|
128
|
+
"path": _project_rel(instruction_set, project_root),
|
|
129
|
+
"fileCount": file_count,
|
|
130
|
+
"bytes": byte_count,
|
|
131
|
+
"analysisPacketBytes": _file_size(packet),
|
|
132
|
+
"legacyTaskPacketBytes": _file_size(legacy_packet),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _lead_phase1_metric(
|
|
137
|
+
task_root: Path, run_dir: Path | None, manifest: dict, project_root: Path
|
|
138
|
+
) -> dict:
|
|
139
|
+
active_path = None
|
|
140
|
+
artifacts = manifest.get("artifacts", {})
|
|
141
|
+
active_rel = artifacts.get("activeRunContextPath")
|
|
142
|
+
if isinstance(active_rel, str) and active_rel:
|
|
143
|
+
active_path = project_root / active_rel
|
|
144
|
+
|
|
145
|
+
if active_path and active_path.is_file():
|
|
146
|
+
instruction_set = task_root / "instruction-set"
|
|
147
|
+
packet = instruction_set / "analysis-packet.md"
|
|
148
|
+
files = [
|
|
149
|
+
task_root / "task-manifest.json",
|
|
150
|
+
active_path,
|
|
151
|
+
instruction_set / "analysis-profile.md",
|
|
152
|
+
packet if packet.is_file() else instruction_set / "task-brief.md",
|
|
153
|
+
]
|
|
154
|
+
mode = "active-run-context"
|
|
155
|
+
else:
|
|
156
|
+
current_phase = manifest.get("workflow", {}).get("currentPhase") or manifest.get("taskType", "")
|
|
157
|
+
run_manifest = None
|
|
158
|
+
team_state = None
|
|
159
|
+
if run_dir is not None:
|
|
160
|
+
manifests_dir = run_dir / "manifests"
|
|
161
|
+
state_dir = run_dir / "state"
|
|
162
|
+
run_manifest = _latest_matching_file(
|
|
163
|
+
manifests_dir, f"run-manifest-{current_phase}-*.json"
|
|
164
|
+
)
|
|
165
|
+
team_state = _latest_matching_file(
|
|
166
|
+
state_dir, f"team-state-{current_phase}-*.json"
|
|
167
|
+
)
|
|
168
|
+
files = [
|
|
169
|
+
task_root / "task-manifest.json",
|
|
170
|
+
task_root / "instruction-set" / "task-brief.md",
|
|
171
|
+
task_root / "instruction-set" / "analysis-profile.md",
|
|
172
|
+
]
|
|
173
|
+
if run_manifest:
|
|
174
|
+
files.append(run_manifest)
|
|
175
|
+
if team_state:
|
|
176
|
+
files.append(team_state)
|
|
177
|
+
mode = "legacy-five-file"
|
|
178
|
+
|
|
179
|
+
file_count, byte_count = _count_files(files)
|
|
180
|
+
return {
|
|
181
|
+
"mode": mode,
|
|
182
|
+
"fileCount": file_count,
|
|
183
|
+
"bytes": byte_count,
|
|
184
|
+
"files": [_project_rel(path, project_root) for path in files if path.is_file()],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _analysis_worker_metric(task_root: Path, project_root: Path) -> dict:
|
|
189
|
+
instruction_set = task_root / "instruction-set"
|
|
190
|
+
full_contract_files = [instruction_set / name for name in INPUT_FILES]
|
|
191
|
+
preamble = Path.home() / ".okstra" / "templates" / "worker-prompt-preamble.md"
|
|
192
|
+
if not preamble.is_file():
|
|
193
|
+
repo_preamble = (
|
|
194
|
+
Path(__file__).resolve().parents[2]
|
|
195
|
+
/ "templates"
|
|
196
|
+
/ "worker-prompt-preamble.md"
|
|
197
|
+
)
|
|
198
|
+
preamble = repo_preamble
|
|
199
|
+
full_contract_files.append(preamble)
|
|
200
|
+
full_file_count, full_byte_count = _count_files(full_contract_files)
|
|
201
|
+
|
|
202
|
+
packet = instruction_set / "analysis-packet.md"
|
|
203
|
+
legacy_packet = instruction_set / "task-packet.md"
|
|
204
|
+
primary_packet = packet if packet.is_file() else legacy_packet
|
|
205
|
+
has_packet = primary_packet.is_file()
|
|
206
|
+
packet_files = [primary_packet, preamble] if has_packet else []
|
|
207
|
+
packet_file_count, packet_byte_count = _count_files(packet_files)
|
|
208
|
+
mode = "analysis-packet-primary" if packet.is_file() else "full-input-contract"
|
|
209
|
+
byte_count = packet_byte_count if packet.is_file() else full_byte_count
|
|
210
|
+
file_count = packet_file_count if packet.is_file() else full_file_count
|
|
211
|
+
current_files = packet_files if packet.is_file() else full_contract_files
|
|
212
|
+
reduction_percent = 0
|
|
213
|
+
if full_byte_count and has_packet:
|
|
214
|
+
reduction_percent = round((1 - packet_byte_count / full_byte_count) * 100)
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"mode": mode,
|
|
218
|
+
"fileCount": file_count,
|
|
219
|
+
"bytesPerWorker": byte_count,
|
|
220
|
+
"legacyFullContractBytesPerWorker": full_byte_count,
|
|
221
|
+
"legacyFullContractFileCount": full_file_count,
|
|
222
|
+
"estimatedPacketModeBytesPerWorker": packet_byte_count,
|
|
223
|
+
"estimatedReductionPercent": reduction_percent,
|
|
224
|
+
"files": [_project_rel(path, project_root) for path in current_files if path.is_file()],
|
|
225
|
+
"legacyFullContractFiles": [
|
|
226
|
+
_project_rel(path, project_root)
|
|
227
|
+
for path in full_contract_files
|
|
228
|
+
if path.is_file()
|
|
229
|
+
],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: Path) -> dict:
|
|
234
|
+
instruction_set = task_root / "instruction-set"
|
|
235
|
+
files = [instruction_set / name for name in INPUT_FILES]
|
|
236
|
+
files.extend([
|
|
237
|
+
instruction_set / "final-report-template.md",
|
|
238
|
+
instruction_set / "final-report-schema.json",
|
|
239
|
+
])
|
|
240
|
+
if run_dir is not None:
|
|
241
|
+
worker_results = run_dir / "worker-results"
|
|
242
|
+
files.extend(
|
|
243
|
+
path for path in worker_results.glob("*.md")
|
|
244
|
+
if "-audit-" not in path.name and "report-writer" not in path.name
|
|
245
|
+
)
|
|
246
|
+
convergence = _latest_matching_file(
|
|
247
|
+
run_dir / "state", f"convergence-{run_dir.name}-*.json"
|
|
248
|
+
)
|
|
249
|
+
if convergence:
|
|
250
|
+
files.append(convergence)
|
|
251
|
+
file_count, byte_count = _count_files(files)
|
|
252
|
+
return {
|
|
253
|
+
"mode": "raw-synthesis-inputs",
|
|
254
|
+
"fileCount": file_count,
|
|
255
|
+
"bytes": byte_count,
|
|
256
|
+
"files": [_project_rel(path, project_root) for path in files if path.is_file()],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def analyze_task_bundle(task_root: Path, project_root: Path) -> dict:
|
|
261
|
+
manifest = _load_json(task_root / "task-manifest.json")
|
|
262
|
+
run_dir = _find_current_run_dir(task_root, manifest, project_root)
|
|
263
|
+
all_task_files = _all_files(task_root)
|
|
264
|
+
task_file_count, task_bytes = _count_files(all_task_files)
|
|
265
|
+
current_run_file_count, current_run_bytes = _count_files(_all_files(run_dir)) if run_dir else (0, 0)
|
|
266
|
+
legacy_timestamp_files = [
|
|
267
|
+
path for path in (task_root / "runs").rglob("*")
|
|
268
|
+
if path.is_file() and _is_timestamped_legacy_artifact(path)
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"ok": True,
|
|
273
|
+
"taskRoot": _project_rel(task_root, project_root),
|
|
274
|
+
"projectRoot": str(project_root),
|
|
275
|
+
"taskKey": manifest.get("taskKey", ""),
|
|
276
|
+
"taskType": manifest.get("taskType", ""),
|
|
277
|
+
"currentRunPath": _project_rel(run_dir, project_root) if run_dir else "",
|
|
278
|
+
"totals": {
|
|
279
|
+
"taskFileCount": task_file_count,
|
|
280
|
+
"taskBytes": task_bytes,
|
|
281
|
+
"currentRunFileCount": current_run_file_count,
|
|
282
|
+
"currentRunBytes": current_run_bytes,
|
|
283
|
+
"legacyTimestampFileCount": len(legacy_timestamp_files),
|
|
284
|
+
},
|
|
285
|
+
"instructionSet": _instruction_set_metric(task_root, project_root),
|
|
286
|
+
"leadPhase1": _lead_phase1_metric(task_root, run_dir, manifest, project_root),
|
|
287
|
+
"analysisWorker": _analysis_worker_metric(task_root, project_root),
|
|
288
|
+
"reportWriter": _report_writer_metric(run_dir, task_root, project_root),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main(argv: list[str] | None = None) -> int:
|
|
293
|
+
parser = argparse.ArgumentParser(
|
|
294
|
+
description="Estimate context/file-read cost for an okstra task bundle."
|
|
295
|
+
)
|
|
296
|
+
parser.add_argument("target", help="task root path or task-key")
|
|
297
|
+
parser.add_argument("--project-root", default="", help="project root for task-key lookup")
|
|
298
|
+
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
299
|
+
args = parser.parse_args(argv)
|
|
300
|
+
|
|
301
|
+
task_root, project_root = _resolve_task_root(args.target, args.project_root, args.cwd)
|
|
302
|
+
result = analyze_task_bundle(task_root, project_root)
|
|
303
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -23,6 +23,7 @@ from dataclasses import dataclass, field
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Optional
|
|
25
25
|
|
|
26
|
+
from okstra_ctl.worktree import is_git_work_tree
|
|
26
27
|
from okstra_project.dirs import (
|
|
27
28
|
LEGACY_OKSTRA_DIR_NAME,
|
|
28
29
|
LEGACY_OKSTRA_RELATIVE,
|
|
@@ -112,7 +113,7 @@ def prepare_migration_plan(
|
|
|
112
113
|
f"Remove it manually if you intend to overwrite, then re-run."
|
|
113
114
|
)
|
|
114
115
|
|
|
115
|
-
use_git =
|
|
116
|
+
use_git = is_git_work_tree(project_root)
|
|
116
117
|
parent = source.parent # <project>/.project-docs
|
|
117
118
|
remove_empty_parent = _would_be_empty_after_remove(parent, source)
|
|
118
119
|
|
|
@@ -194,17 +195,6 @@ def _resolve_okstra_home(override: Optional[Path]) -> Path:
|
|
|
194
195
|
return Path.home() / ".okstra"
|
|
195
196
|
|
|
196
197
|
|
|
197
|
-
def _is_git_worktree(project_root: Path) -> bool:
|
|
198
|
-
try:
|
|
199
|
-
rc = subprocess.run(
|
|
200
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
201
|
-
cwd=str(project_root), capture_output=True, text=True, check=False,
|
|
202
|
-
)
|
|
203
|
-
except (OSError, FileNotFoundError):
|
|
204
|
-
return False
|
|
205
|
-
return rc.returncode == 0 and bool(rc.stdout.strip())
|
|
206
|
-
|
|
207
|
-
|
|
208
198
|
def _would_be_empty_after_remove(parent: Path, child: Path) -> bool:
|
|
209
199
|
"""Return True if `parent` would be empty after `child` is moved out."""
|
|
210
200
|
if not parent.is_dir():
|
|
@@ -31,9 +31,25 @@ __all__ = [
|
|
|
31
31
|
"DISCOVERY_RELATIVE",
|
|
32
32
|
"compute_run_paths",
|
|
33
33
|
"next_run_seq",
|
|
34
|
+
"task_dir",
|
|
35
|
+
"task_runs_dir",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
def task_dir(project_root: Path, task_group: str, task_id: str) -> Path:
|
|
40
|
+
"""task root 경로: ``<project>/.okstra/tasks/<group-seg>/<id-seg>``.
|
|
41
|
+
|
|
42
|
+
raw group/id 를 받아 내부에서 slugify 한다 — compute_run_paths 가 쓰는 것과
|
|
43
|
+
동일한 segment 규칙이다. 과거 sequence/wizard 가 이 구조를 손으로 재유도해
|
|
44
|
+
silent drift 위험이 있던 것을 이 SSOT 로 모은다."""
|
|
45
|
+
return Path(project_root) / TASKS_RELATIVE / slugify(task_group) / slugify(task_id)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def task_runs_dir(project_root: Path, task_group: str, task_id: str) -> Path:
|
|
49
|
+
"""task 의 runs 디렉터리: ``task_dir/runs``."""
|
|
50
|
+
return task_dir(project_root, task_group, task_id) / "runs"
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
def next_run_seq(run_seq_dir: Path, task_type_segment: str) -> int:
|
|
38
54
|
"""run_seq_dir 안에서 `*-<task-type>-NNN.<ext>` 파일을 스캔해 다음 seq 번호를
|
|
39
55
|
돌려준다. 디렉터리 부재 시 1.
|
|
@@ -100,6 +116,7 @@ def compute_run_paths(
|
|
|
100
116
|
task_manifest = task_root / "task-manifest.json"
|
|
101
117
|
task_index = task_root / "task-index.md"
|
|
102
118
|
instruction_set = task_root / "instruction-set"
|
|
119
|
+
analysis_packet = instruction_set / "analysis-packet.md"
|
|
103
120
|
runs_dir = task_root / "runs"
|
|
104
121
|
history_dir = task_root / "history"
|
|
105
122
|
timeline_file = history_dir / "timeline.json"
|
|
@@ -143,6 +160,7 @@ def compute_run_paths(
|
|
|
143
160
|
final_report = run_reports / f"final-report{suffixes['reports']}.md"
|
|
144
161
|
final_status = run_status / f"final{suffixes['status']}.status"
|
|
145
162
|
team_state = run_state / f"team-state{suffixes['state']}.json"
|
|
163
|
+
active_run_context = run_state / f"active-run-context{suffixes['state']}.json"
|
|
146
164
|
final_report_template = instruction_set / "final-report-template.md"
|
|
147
165
|
final_report_schema = instruction_set / "final-report-schema.json"
|
|
148
166
|
reference_expectations = instruction_set / "reference-expectations.md"
|
|
@@ -183,6 +201,7 @@ def compute_run_paths(
|
|
|
183
201
|
"TASK_MANIFEST_PATH": str(task_manifest),
|
|
184
202
|
"TASK_INDEX_PATH": str(task_index),
|
|
185
203
|
"INSTRUCTION_SET_PATH": str(instruction_set),
|
|
204
|
+
"ANALYSIS_PACKET_PATH": str(analysis_packet),
|
|
186
205
|
"RUNS_DIR": str(runs_dir),
|
|
187
206
|
"HISTORY_DIR": str(history_dir),
|
|
188
207
|
"TIMELINE_PATH": str(timeline_file),
|
|
@@ -205,6 +224,7 @@ def compute_run_paths(
|
|
|
205
224
|
"FINAL_REPORT_PATH": str(final_report),
|
|
206
225
|
"FINAL_STATUS_PATH": str(final_status),
|
|
207
226
|
"TEAM_STATE_PATH": str(team_state),
|
|
227
|
+
"ACTIVE_RUN_CONTEXT_PATH": str(active_run_context),
|
|
208
228
|
"FINAL_REPORT_TEMPLATE_PATH": str(final_report_template),
|
|
209
229
|
"FINAL_REPORT_SCHEMA_PATH": str(final_report_schema),
|
|
210
230
|
"REFERENCE_EXPECTATIONS_FILE": str(reference_expectations),
|
|
@@ -244,6 +264,7 @@ def compute_run_paths(
|
|
|
244
264
|
("TASK_MANIFEST_RELATIVE_PATH", task_manifest),
|
|
245
265
|
("TASK_INDEX_RELATIVE_PATH", task_index),
|
|
246
266
|
("INSTRUCTION_SET_RELATIVE_PATH", instruction_set),
|
|
267
|
+
("ANALYSIS_PACKET_RELATIVE_PATH", analysis_packet),
|
|
247
268
|
("RUNS_RELATIVE_PATH", runs_dir),
|
|
248
269
|
("HISTORY_RELATIVE_PATH", history_dir),
|
|
249
270
|
("TIMELINE_RELATIVE_PATH", timeline_file),
|
|
@@ -263,6 +284,7 @@ def compute_run_paths(
|
|
|
263
284
|
("FINAL_REPORT_RELATIVE_PATH", final_report),
|
|
264
285
|
("FINAL_STATUS_RELATIVE_PATH", final_status),
|
|
265
286
|
("TEAM_STATE_RELATIVE_PATH", team_state),
|
|
287
|
+
("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", active_run_context),
|
|
266
288
|
("WORKER_RESULTS_RELATIVE_PATH", worker_results),
|
|
267
289
|
("RUN_CARRY_RELATIVE_PATH", run_carry),
|
|
268
290
|
("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", final_report_template),
|