okstra 0.171.0 → 0.172.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/docs/architecture.md +13 -0
- package/docs/cli.md +4 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +3 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +4 -0
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +36 -12
- package/runtime/prompts/lead/plan-body-verification.md +22 -11
- package/runtime/prompts/lead/report-writer.md +11 -10
- package/runtime/prompts/lead/team-contract.md +2 -0
- package/runtime/prompts/profiles/_clarification-recommendation.md +3 -1
- package/runtime/prompts/profiles/_common-contract.md +2 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -1
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
- package/runtime/python/okstra_ctl/agent_activity.py +306 -0
- package/runtime/python/okstra_ctl/clarification_items.py +37 -20
- package/runtime/python/okstra_ctl/lead_events.py +47 -4
- package/runtime/python/okstra_ctl/render.py +11 -3
- package/runtime/python/okstra_ctl/report_finalize.py +51 -14
- package/runtime/python/okstra_ctl/report_html/common.py +5 -3
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +17 -1
- package/runtime/python/okstra_ctl/report_translation.py +14 -0
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +150 -0
- package/runtime/schemas/final-report-v2.0.schema.json +189 -0
- package/runtime/skills/okstra-user-response/SKILL.md +2 -2
- package/runtime/templates/reports/final-report-v2.template.md +8 -0
- package/runtime/templates/reports/html/assets/base.css +7 -0
- package/runtime/templates/reports/html/i18n/en.json +6 -1
- package/runtime/templates/reports/html/i18n/ko.json +6 -1
- package/runtime/templates/reports/html/macros/forms.html +21 -2
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +25 -0
- package/runtime/templates/reports/i18n/en.json +4 -0
- package/runtime/templates/reports/report.js +26 -17
- package/runtime/templates/reports/user-response.template.md +3 -1
- package/runtime/templates/worker-prompt-preamble.md +8 -0
- package/runtime/validators/validate-run.py +989 -29
- package/runtime/validators/validate_session_conformance.py +523 -35
- package/src/cli-registry.mjs +7 -0
- package/src/commands/report/agent-activity.mjs +21 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Phase 7 report post-processing — the single reference point.
|
|
2
2
|
|
|
3
3
|
Phase 7 turns a Phase 6 final-report data.json into shippable artifacts through
|
|
4
|
-
|
|
5
|
-
rendering, follow-up task spawning, and run validation.
|
|
6
|
-
load-bearing — rendering before substitution ships `--` token
|
|
7
|
-
validating before rendering trips the report-views contract.
|
|
4
|
+
six ordered steps: activity projection, English-SSOT verification, usage
|
|
5
|
+
substitution, html view rendering, follow-up task spawning, and run validation.
|
|
6
|
+
The order is load-bearing — rendering before substitution ships `--` token
|
|
7
|
+
cells, and validating before rendering trips the report-views contract.
|
|
8
8
|
|
|
9
9
|
The translation sidecar is NOT one of these steps. `render-views` overlays it,
|
|
10
10
|
so a non-English run dispatches the translator before this sequence starts —
|
|
@@ -25,11 +25,13 @@ from dataclasses import dataclass
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from typing import Any, Callable, Mapping, Sequence
|
|
27
27
|
|
|
28
|
+
from .agent_activity import ActivityProjectionError, project_agent_activity
|
|
29
|
+
from .dispatch_state import DispatchError, link_agent_dispatch_result
|
|
28
30
|
from .final_report_paths import final_report_data_path, final_report_markdown_path
|
|
29
31
|
from .paths import task_dir, task_manifest_file
|
|
30
|
-
from .dispatch_state import DispatchError, link_agent_dispatch_result
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
STEP_PROJECT_ACTIVITY = "project-activity"
|
|
33
35
|
STEP_CHECK_SOURCE = "check-source"
|
|
34
36
|
STEP_TOKEN_USAGE = "token-usage"
|
|
35
37
|
STEP_RENDER_VIEWS = "render-views"
|
|
@@ -37,6 +39,7 @@ STEP_SPAWN_FOLLOWUPS = "spawn-followups"
|
|
|
37
39
|
STEP_VALIDATE_RUN = "validate-run"
|
|
38
40
|
|
|
39
41
|
STEP_ORDER = (
|
|
42
|
+
STEP_PROJECT_ACTIVITY,
|
|
40
43
|
# First, because everything after it derives from the data.json: rendering
|
|
41
44
|
# a Korean SSOT into English chrome, spawning follow-ups from it, and
|
|
42
45
|
# validating it all succeed on a record the next phase cannot read.
|
|
@@ -235,6 +238,16 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
|
235
238
|
"""Assemble the ordered Phase 7 argv list. Order is contractual."""
|
|
236
239
|
markdown_path = ctx.markdown_path
|
|
237
240
|
return [
|
|
241
|
+
(
|
|
242
|
+
STEP_PROJECT_ACTIVITY,
|
|
243
|
+
[
|
|
244
|
+
"<in-process>",
|
|
245
|
+
"agent-activity",
|
|
246
|
+
"project",
|
|
247
|
+
str(ctx.manifest_path),
|
|
248
|
+
str(ctx.data_path),
|
|
249
|
+
],
|
|
250
|
+
),
|
|
238
251
|
(
|
|
239
252
|
STEP_CHECK_SOURCE,
|
|
240
253
|
[
|
|
@@ -349,8 +362,8 @@ def run_finalize(
|
|
|
349
362
|
|
|
350
363
|
``only`` restricts the run to the named steps, preserving contractual
|
|
351
364
|
order. The last step (`validate-run`) is the one that usually fails, and
|
|
352
|
-
re-running the whole sequence to retry it repeats
|
|
353
|
-
at full token and wall-clock cost.
|
|
365
|
+
re-running the whole sequence to retry it repeats every preceding idempotent
|
|
366
|
+
step at full token and wall-clock cost.
|
|
354
367
|
"""
|
|
355
368
|
steps: list[dict[str, Any]] = []
|
|
356
369
|
try:
|
|
@@ -372,6 +385,29 @@ def run_finalize(
|
|
|
372
385
|
for name, command in commands:
|
|
373
386
|
if before_step is not None:
|
|
374
387
|
before_step(name)
|
|
388
|
+
if name == STEP_PROJECT_ACTIVITY:
|
|
389
|
+
try:
|
|
390
|
+
rows = project_agent_activity(
|
|
391
|
+
ctx.project_root,
|
|
392
|
+
ctx.manifest_path,
|
|
393
|
+
ctx.data_path,
|
|
394
|
+
)
|
|
395
|
+
except ActivityProjectionError as exc:
|
|
396
|
+
result = subprocess.CompletedProcess(
|
|
397
|
+
command,
|
|
398
|
+
1,
|
|
399
|
+
"",
|
|
400
|
+
str(exc),
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
result = subprocess.CompletedProcess(
|
|
404
|
+
command,
|
|
405
|
+
0,
|
|
406
|
+
json.dumps({"count": len(rows)}),
|
|
407
|
+
"",
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
result = None
|
|
375
411
|
if name == STEP_VALIDATE_RUN:
|
|
376
412
|
try:
|
|
377
413
|
_link_lead_result_for_validation(ctx)
|
|
@@ -381,12 +417,13 @@ def run_finalize(
|
|
|
381
417
|
"reason": f"lead result linkage failed: {exc}",
|
|
382
418
|
"steps": steps,
|
|
383
419
|
}
|
|
384
|
-
result
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
420
|
+
if result is None:
|
|
421
|
+
result = subprocess.run(
|
|
422
|
+
command,
|
|
423
|
+
cwd=ctx.project_root,
|
|
424
|
+
text=True,
|
|
425
|
+
capture_output=True,
|
|
426
|
+
)
|
|
390
427
|
steps.append(step_payload(name, command, result))
|
|
391
428
|
if result.returncode != 0:
|
|
392
429
|
return {
|
|
@@ -489,7 +526,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
489
526
|
help=(
|
|
490
527
|
"run only these steps (repeatable, contractual order preserved). "
|
|
491
528
|
"Use `--only validate-run` to retry the step that usually fails "
|
|
492
|
-
"without repeating the
|
|
529
|
+
"without repeating the preceding idempotent steps."
|
|
493
530
|
),
|
|
494
531
|
)
|
|
495
532
|
return parser
|
|
@@ -85,6 +85,7 @@ def _own_section_ids(data: dict, omitted_fields: tuple[str, ...] = ()) -> set[st
|
|
|
85
85
|
|
|
86
86
|
found: set[str] = set()
|
|
87
87
|
_collect_ids(data.get("clarificationItems", []), found)
|
|
88
|
+
_collect_ids(data.get("agentActivity", []), found)
|
|
88
89
|
property_name = TASK_TYPE_DATA_PROPERTY.get(data.get("header", {}).get("taskType", ""))
|
|
89
90
|
if property_name:
|
|
90
91
|
block = data.get(property_name, {})
|
|
@@ -120,9 +121,10 @@ def evidence_index(data: dict) -> dict[str, object]:
|
|
|
120
121
|
|
|
121
122
|
def _collect_ids(value: object, found: set[str]) -> None:
|
|
122
123
|
if isinstance(value, dict):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
for key in ("id", "activityId"):
|
|
125
|
+
row_id = value.get(key)
|
|
126
|
+
if isinstance(row_id, str) and row_id:
|
|
127
|
+
found.add(row_id)
|
|
126
128
|
for nested in value.values():
|
|
127
129
|
_collect_ids(nested, found)
|
|
128
130
|
elif isinstance(value, list):
|
|
@@ -91,6 +91,16 @@ def build_implementation_planning_view(data: dict) -> HumanReportView:
|
|
|
91
91
|
planning = data["implementationPlanning"]
|
|
92
92
|
figure = _stage_figure(planning)
|
|
93
93
|
approval = plan_approval_state(data)
|
|
94
|
+
activities = tuple(
|
|
95
|
+
row for row in data.get("agentActivity", []) if isinstance(row, dict)
|
|
96
|
+
)
|
|
97
|
+
decision_cards = tuple(
|
|
98
|
+
row
|
|
99
|
+
for row in data.get("clarificationItems", [])
|
|
100
|
+
if isinstance(row, dict)
|
|
101
|
+
and row.get("blocks") == "approval"
|
|
102
|
+
and isinstance(row.get("approvalContext"), dict)
|
|
103
|
+
)
|
|
94
104
|
context = {
|
|
95
105
|
"humanSummary": data["humanSummary"],
|
|
96
106
|
"planning": planning,
|
|
@@ -100,9 +110,15 @@ def build_implementation_planning_view(data: dict) -> HumanReportView:
|
|
|
100
110
|
"openDecisions": [
|
|
101
111
|
row
|
|
102
112
|
for row in data.get("clarificationItems", [])
|
|
103
|
-
if row
|
|
113
|
+
if isinstance(row, dict)
|
|
114
|
+
and row.get("blocks") == "approval"
|
|
104
115
|
and row.get("status") in {"open", "answered"}
|
|
105
116
|
],
|
|
117
|
+
"agentActivities": activities,
|
|
118
|
+
"activityById": {
|
|
119
|
+
row["activityId"]: row for row in activities if row.get("activityId")
|
|
120
|
+
},
|
|
121
|
+
"decisionCards": decision_cards,
|
|
106
122
|
"evidenceIndex": evidence_index(data),
|
|
107
123
|
}
|
|
108
124
|
return HumanReportView(
|
|
@@ -125,8 +125,10 @@ PROSE_KEYS = frozenset({
|
|
|
125
125
|
"topic",
|
|
126
126
|
"trigger",
|
|
127
127
|
"triggerSignal",
|
|
128
|
+
"unblockCondition",
|
|
128
129
|
"userChoice",
|
|
129
130
|
"userInput",
|
|
131
|
+
"userText",
|
|
130
132
|
"verification",
|
|
131
133
|
"verificationMethod",
|
|
132
134
|
"verificationSignal",
|
|
@@ -138,6 +140,8 @@ PROSE_KEYS = frozenset({
|
|
|
138
140
|
# that must stay verbatim to be evidence at all.
|
|
139
141
|
STRUCTURAL_KEYS = frozenset({
|
|
140
142
|
"action", # implementation.py: _FILE_ACTIONS lookup key
|
|
143
|
+
"activityId",
|
|
144
|
+
"activityIds",
|
|
141
145
|
"adapter",
|
|
142
146
|
"affectedId",
|
|
143
147
|
"affectedTarget",
|
|
@@ -152,12 +156,15 @@ STRUCTURAL_KEYS = frozenset({
|
|
|
152
156
|
"capturedHeadSha",
|
|
153
157
|
"carriedForwardFromSeq",
|
|
154
158
|
"carryJson",
|
|
159
|
+
"checkRefs",
|
|
155
160
|
"clarificationId",
|
|
161
|
+
"classification",
|
|
156
162
|
"claudeCode",
|
|
157
163
|
"collectorBranch",
|
|
158
164
|
"command",
|
|
159
165
|
"commandOrObservation",
|
|
160
166
|
"commandOrTest",
|
|
167
|
+
"commands",
|
|
161
168
|
"commitListQuote",
|
|
162
169
|
"complexity",
|
|
163
170
|
"componentId",
|
|
@@ -165,14 +172,18 @@ STRUCTURAL_KEYS = frozenset({
|
|
|
165
172
|
"conformanceTests",
|
|
166
173
|
"coveredBy",
|
|
167
174
|
"createdAt",
|
|
175
|
+
"cwd",
|
|
168
176
|
"cycle",
|
|
169
177
|
"dataStore",
|
|
170
178
|
"date",
|
|
171
179
|
"dependsOn",
|
|
172
180
|
"diffSummaryQuote",
|
|
173
181
|
"direction",
|
|
182
|
+
"disposition",
|
|
174
183
|
"dispatches",
|
|
175
184
|
"executorWorktreePath",
|
|
185
|
+
"evidenceRefs",
|
|
186
|
+
"exitCode",
|
|
176
187
|
"existingPrUrl",
|
|
177
188
|
"externalSystem",
|
|
178
189
|
"featureId",
|
|
@@ -215,6 +226,7 @@ STRUCTURAL_KEYS = frozenset({
|
|
|
215
226
|
"ownerComponentId",
|
|
216
227
|
"path",
|
|
217
228
|
"planFile",
|
|
229
|
+
"planItemIds",
|
|
218
230
|
"planStep",
|
|
219
231
|
"prepItemId",
|
|
220
232
|
"project",
|
|
@@ -223,11 +235,13 @@ STRUCTURAL_KEYS = frozenset({
|
|
|
223
235
|
"rawStat",
|
|
224
236
|
"readBoundary",
|
|
225
237
|
"readOnlyCommandLog",
|
|
238
|
+
"recommendedDisposition",
|
|
226
239
|
"relatedIds",
|
|
227
240
|
"relation",
|
|
228
241
|
"replacedInTest",
|
|
229
242
|
"replacedWith",
|
|
230
243
|
"reportPath",
|
|
244
|
+
"resultPath",
|
|
231
245
|
"requestKind",
|
|
232
246
|
"requestPath",
|
|
233
247
|
"requestVerbatim", # the user's own words, quoted as evidence
|
|
@@ -10,7 +10,9 @@ so the rules live here rather than inside either one — the same split
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import json
|
|
13
14
|
import re
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
14
16
|
from dataclasses import dataclass
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
|
|
@@ -39,6 +41,29 @@ _EVIDENCE_READ_RE = re.compile(
|
|
|
39
41
|
r"^- Evidence read: `?(?P<path>[^`\n]+?)`?\s*$",
|
|
40
42
|
re.MULTILINE,
|
|
41
43
|
)
|
|
44
|
+
_EVIDENCE_COMMAND_RE = re.compile(
|
|
45
|
+
r"^- Evidence command:[ \t]*(?P<payload>.*)$", re.MULTILINE
|
|
46
|
+
)
|
|
47
|
+
_EVIDENCE_COMMAND_FIELDS = ("command", "cwd", "exitCode", "outputSummary")
|
|
48
|
+
_EVIDENCE_COMMAND_STRING_FIELDS = ("command", "cwd", "outputSummary")
|
|
49
|
+
_SENSITIVE_KEY_RE = re.compile(
|
|
50
|
+
r"(?i)(authorization|auth|credential|pass(?:wd|word)?|pwd|token|secret|"
|
|
51
|
+
r"api[_-]?key|access[_-]?key)"
|
|
52
|
+
)
|
|
53
|
+
_SENSITIVE_TEXT_RE = re.compile(
|
|
54
|
+
r"(?i)(authorization\s*:\s*\S+|"
|
|
55
|
+
r"\b(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:auth|credential|pass(?:wd|word)?|pwd|"
|
|
56
|
+
r"token|secret|api[_-]?key|access[_-]?key)[A-Za-z0-9_-]*\s*=|"
|
|
57
|
+
r"--[\w-]*(?:auth|credential|pass(?:wd|word)?|pwd|token|secret|"
|
|
58
|
+
r"api[-_]?key|access[-_]?key)[\w-]*(?:[ =])\S+|"
|
|
59
|
+
r"\w+://[^\s:/@]+:[^\s@/]+@)"
|
|
60
|
+
)
|
|
61
|
+
_KNOWN_SECRET_RE = re.compile(
|
|
62
|
+
r"(?:AKIA|ASIA)[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{8,}|"
|
|
63
|
+
r"github_pat_[A-Za-z0-9_]{8,}|xox[abprs]-[A-Za-z0-9-]{8,}|"
|
|
64
|
+
r"\bsk-[A-Za-z0-9_-]{8,}\b|"
|
|
65
|
+
r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b"
|
|
66
|
+
)
|
|
42
67
|
_FILE_LINE_CITATION_RE = re.compile(
|
|
43
68
|
r"`(?P<path>(?!https?://)[^`\n]+?):(?P<line>\d+(?:-\d+)?)`"
|
|
44
69
|
)
|
|
@@ -85,6 +110,129 @@ def _audit_evidence_read_paths(content: str) -> set[str]:
|
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class EvidenceCommand:
|
|
115
|
+
command: str
|
|
116
|
+
cwd: str
|
|
117
|
+
exit_code: int
|
|
118
|
+
output_summary: str
|
|
119
|
+
|
|
120
|
+
def to_record(self) -> dict[str, object]:
|
|
121
|
+
return {
|
|
122
|
+
"command": self.command,
|
|
123
|
+
"cwd": self.cwd,
|
|
124
|
+
"exitCode": self.exit_code,
|
|
125
|
+
"outputSummary": self.output_summary,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _contains_sensitive_material(value: object) -> bool:
|
|
130
|
+
if isinstance(value, Mapping):
|
|
131
|
+
return any(
|
|
132
|
+
(
|
|
133
|
+
_SENSITIVE_KEY_RE.search(str(key)) is not None
|
|
134
|
+
and nested not in (None, "", False)
|
|
135
|
+
)
|
|
136
|
+
or _contains_sensitive_material(nested)
|
|
137
|
+
for key, nested in value.items()
|
|
138
|
+
)
|
|
139
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
140
|
+
return any(_contains_sensitive_material(item) for item in value)
|
|
141
|
+
if not isinstance(value, str):
|
|
142
|
+
return False
|
|
143
|
+
return (
|
|
144
|
+
_SENSITIVE_TEXT_RE.search(value) is not None
|
|
145
|
+
or _KNOWN_SECRET_RE.search(value) is not None
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _evidence_command_contract_failures(
|
|
150
|
+
payload: Mapping[str, object], row_number: int
|
|
151
|
+
) -> list[str]:
|
|
152
|
+
failures: list[str] = []
|
|
153
|
+
missing = [key for key in _EVIDENCE_COMMAND_FIELDS if key not in payload]
|
|
154
|
+
unexpected = sorted(set(payload) - set(_EVIDENCE_COMMAND_FIELDS))
|
|
155
|
+
if missing:
|
|
156
|
+
failures.append(
|
|
157
|
+
f"Evidence command row {row_number} is missing {', '.join(missing)}"
|
|
158
|
+
)
|
|
159
|
+
if unexpected:
|
|
160
|
+
failures.append(
|
|
161
|
+
f"Evidence command row {row_number} has unexpected "
|
|
162
|
+
f"{', '.join(unexpected)}"
|
|
163
|
+
)
|
|
164
|
+
for field_name in _EVIDENCE_COMMAND_STRING_FIELDS:
|
|
165
|
+
if field_name in payload and not isinstance(payload[field_name], str):
|
|
166
|
+
failures.append(
|
|
167
|
+
f"Evidence command row {row_number} {field_name} must be a string"
|
|
168
|
+
)
|
|
169
|
+
if "exitCode" in payload and type(payload["exitCode"]) is not int:
|
|
170
|
+
failures.append(
|
|
171
|
+
f"Evidence command row {row_number} exitCode must be an integer"
|
|
172
|
+
)
|
|
173
|
+
return failures
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _evidence_command_from_payload(
|
|
177
|
+
payload: Mapping[str, object], sensitive: bool
|
|
178
|
+
) -> EvidenceCommand:
|
|
179
|
+
if sensitive:
|
|
180
|
+
return EvidenceCommand(
|
|
181
|
+
command="[redacted: potential secret]",
|
|
182
|
+
cwd="[redacted: potential secret]",
|
|
183
|
+
exit_code=payload["exitCode"],
|
|
184
|
+
output_summary="omitted because potential secret material was detected",
|
|
185
|
+
)
|
|
186
|
+
return EvidenceCommand(
|
|
187
|
+
command=payload["command"],
|
|
188
|
+
cwd=payload["cwd"],
|
|
189
|
+
exit_code=payload["exitCode"],
|
|
190
|
+
output_summary=payload["outputSummary"],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_evidence_commands(
|
|
195
|
+
content: str,
|
|
196
|
+
) -> tuple[tuple[EvidenceCommand, ...], tuple[str, ...]]:
|
|
197
|
+
commands: list[EvidenceCommand] = []
|
|
198
|
+
failures: list[str] = []
|
|
199
|
+
for row_number, match in enumerate(
|
|
200
|
+
_EVIDENCE_COMMAND_RE.finditer(content), start=1
|
|
201
|
+
):
|
|
202
|
+
try:
|
|
203
|
+
payload = json.loads(match.group("payload"))
|
|
204
|
+
except json.JSONDecodeError:
|
|
205
|
+
failures.append(f"Evidence command row {row_number} is not valid JSON")
|
|
206
|
+
continue
|
|
207
|
+
if not isinstance(payload, Mapping):
|
|
208
|
+
failures.append(
|
|
209
|
+
f"Evidence command row {row_number} must be a JSON object"
|
|
210
|
+
)
|
|
211
|
+
continue
|
|
212
|
+
sensitive = _contains_sensitive_material(payload)
|
|
213
|
+
if sensitive:
|
|
214
|
+
failures.append(
|
|
215
|
+
f"Evidence command row {row_number} contains sensitive material"
|
|
216
|
+
)
|
|
217
|
+
contract_failures = _evidence_command_contract_failures(payload, row_number)
|
|
218
|
+
failures.extend(contract_failures)
|
|
219
|
+
if contract_failures:
|
|
220
|
+
continue
|
|
221
|
+
commands.append(_evidence_command_from_payload(payload, sensitive))
|
|
222
|
+
return tuple(commands), tuple(failures)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def read_evidence_commands(
|
|
226
|
+
audit_path: Path,
|
|
227
|
+
) -> tuple[tuple[EvidenceCommand, ...], tuple[str, ...]]:
|
|
228
|
+
try:
|
|
229
|
+
return parse_evidence_commands(audit_path.read_text(encoding="utf-8"))
|
|
230
|
+
except OSError as exc:
|
|
231
|
+
return (), (
|
|
232
|
+
f"worker audit sidecar unreadable: {audit_path.name} ({exc})",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
88
236
|
def _worker_prompt_path(
|
|
89
237
|
run_dir: Path,
|
|
90
238
|
worker_role: str,
|
|
@@ -276,6 +424,8 @@ def check_worker_results_audit(
|
|
|
276
424
|
)
|
|
277
425
|
continue
|
|
278
426
|
|
|
427
|
+
_, command_failures = read_evidence_commands(audit_path)
|
|
428
|
+
failures.extend(command_failures)
|
|
279
429
|
failures.extend(_evidence_read_ledger_failures(
|
|
280
430
|
run_dir=run_dir,
|
|
281
431
|
worker_role=worker_role,
|
|
@@ -171,6 +171,12 @@
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
|
+
"agentActivity": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": {
|
|
177
|
+
"$ref": "#/$defs/AgentActivityRow"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
174
180
|
"verificationScope": {
|
|
175
181
|
"enum": [
|
|
176
182
|
"whole-task",
|
|
@@ -2587,6 +2593,103 @@
|
|
|
2587
2593
|
}
|
|
2588
2594
|
],
|
|
2589
2595
|
"$defs": {
|
|
2596
|
+
"ActivityCommand": {
|
|
2597
|
+
"type": "object",
|
|
2598
|
+
"required": [
|
|
2599
|
+
"command",
|
|
2600
|
+
"cwd",
|
|
2601
|
+
"exitCode",
|
|
2602
|
+
"outputSummary"
|
|
2603
|
+
],
|
|
2604
|
+
"additionalProperties": false,
|
|
2605
|
+
"properties": {
|
|
2606
|
+
"command": {
|
|
2607
|
+
"type": "string"
|
|
2608
|
+
},
|
|
2609
|
+
"cwd": {
|
|
2610
|
+
"type": "string"
|
|
2611
|
+
},
|
|
2612
|
+
"exitCode": {
|
|
2613
|
+
"type": "integer"
|
|
2614
|
+
},
|
|
2615
|
+
"outputSummary": {
|
|
2616
|
+
"type": "string"
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2620
|
+
"AgentActivityRow": {
|
|
2621
|
+
"type": "object",
|
|
2622
|
+
"required": [
|
|
2623
|
+
"activityId",
|
|
2624
|
+
"kind",
|
|
2625
|
+
"agent",
|
|
2626
|
+
"summary",
|
|
2627
|
+
"planItemIds",
|
|
2628
|
+
"resultPath",
|
|
2629
|
+
"commands",
|
|
2630
|
+
"evidenceRefs",
|
|
2631
|
+
"outcome"
|
|
2632
|
+
],
|
|
2633
|
+
"additionalProperties": false,
|
|
2634
|
+
"properties": {
|
|
2635
|
+
"activityId": {
|
|
2636
|
+
"type": "string",
|
|
2637
|
+
"pattern": "^A-\\d{3,}$"
|
|
2638
|
+
},
|
|
2639
|
+
"kind": {
|
|
2640
|
+
"type": "string",
|
|
2641
|
+
"enum": [
|
|
2642
|
+
"worker-dispatched",
|
|
2643
|
+
"worker-completed",
|
|
2644
|
+
"verification-round-completed",
|
|
2645
|
+
"self-fix-applied",
|
|
2646
|
+
"user-decision-required",
|
|
2647
|
+
"user-decision-evaluated"
|
|
2648
|
+
]
|
|
2649
|
+
},
|
|
2650
|
+
"agent": {
|
|
2651
|
+
"type": "string",
|
|
2652
|
+
"minLength": 1
|
|
2653
|
+
},
|
|
2654
|
+
"summary": {
|
|
2655
|
+
"type": "string",
|
|
2656
|
+
"minLength": 1
|
|
2657
|
+
},
|
|
2658
|
+
"planItemIds": {
|
|
2659
|
+
"type": "array",
|
|
2660
|
+
"items": {
|
|
2661
|
+
"type": "string",
|
|
2662
|
+
"minLength": 1
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2665
|
+
"resultPath": {
|
|
2666
|
+
"type": "string"
|
|
2667
|
+
},
|
|
2668
|
+
"commands": {
|
|
2669
|
+
"type": "array",
|
|
2670
|
+
"items": {
|
|
2671
|
+
"$ref": "#/$defs/ActivityCommand"
|
|
2672
|
+
}
|
|
2673
|
+
},
|
|
2674
|
+
"evidenceRefs": {
|
|
2675
|
+
"type": "array",
|
|
2676
|
+
"items": {
|
|
2677
|
+
"type": "string",
|
|
2678
|
+
"minLength": 1
|
|
2679
|
+
}
|
|
2680
|
+
},
|
|
2681
|
+
"outcome": {
|
|
2682
|
+
"type": "string",
|
|
2683
|
+
"enum": [
|
|
2684
|
+
"pending",
|
|
2685
|
+
"completed",
|
|
2686
|
+
"failed",
|
|
2687
|
+
"blocked",
|
|
2688
|
+
"resolved"
|
|
2689
|
+
]
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
},
|
|
2590
2693
|
"TaskType": {
|
|
2591
2694
|
"enum": [
|
|
2592
2695
|
"requirements-discovery",
|
|
@@ -3553,6 +3656,86 @@
|
|
|
3553
3656
|
"deferred-no-interactive-session"
|
|
3554
3657
|
]
|
|
3555
3658
|
},
|
|
3659
|
+
"ApprovalDisposition": {
|
|
3660
|
+
"enum": [
|
|
3661
|
+
"select",
|
|
3662
|
+
"accept-risk",
|
|
3663
|
+
"request-revision",
|
|
3664
|
+
"reject"
|
|
3665
|
+
]
|
|
3666
|
+
},
|
|
3667
|
+
"ApprovalResolution": {
|
|
3668
|
+
"type": "object",
|
|
3669
|
+
"required": [
|
|
3670
|
+
"disposition",
|
|
3671
|
+
"userText",
|
|
3672
|
+
"checkRefs"
|
|
3673
|
+
],
|
|
3674
|
+
"additionalProperties": false,
|
|
3675
|
+
"properties": {
|
|
3676
|
+
"disposition": {
|
|
3677
|
+
"$ref": "#/$defs/ApprovalDisposition"
|
|
3678
|
+
},
|
|
3679
|
+
"userText": {
|
|
3680
|
+
"type": "string",
|
|
3681
|
+
"minLength": 1
|
|
3682
|
+
},
|
|
3683
|
+
"checkRefs": {
|
|
3684
|
+
"type": "array",
|
|
3685
|
+
"minItems": 1,
|
|
3686
|
+
"items": {
|
|
3687
|
+
"type": "string",
|
|
3688
|
+
"pattern": "^A-\\d{3,}$"
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
},
|
|
3693
|
+
"ApprovalContext": {
|
|
3694
|
+
"type": "object",
|
|
3695
|
+
"required": [
|
|
3696
|
+
"classification",
|
|
3697
|
+
"planItemIds",
|
|
3698
|
+
"activityIds",
|
|
3699
|
+
"unblockCondition",
|
|
3700
|
+
"recommendedDisposition"
|
|
3701
|
+
],
|
|
3702
|
+
"additionalProperties": false,
|
|
3703
|
+
"properties": {
|
|
3704
|
+
"classification": {
|
|
3705
|
+
"enum": [
|
|
3706
|
+
"user-decision",
|
|
3707
|
+
"noncritical-dissent",
|
|
3708
|
+
"correctness-critical"
|
|
3709
|
+
]
|
|
3710
|
+
},
|
|
3711
|
+
"planItemIds": {
|
|
3712
|
+
"type": "array",
|
|
3713
|
+
"minItems": 1,
|
|
3714
|
+
"items": {
|
|
3715
|
+
"type": "string",
|
|
3716
|
+
"pattern": "^P-[A-Za-z]+-.+"
|
|
3717
|
+
}
|
|
3718
|
+
},
|
|
3719
|
+
"activityIds": {
|
|
3720
|
+
"type": "array",
|
|
3721
|
+
"minItems": 1,
|
|
3722
|
+
"items": {
|
|
3723
|
+
"type": "string",
|
|
3724
|
+
"pattern": "^A-\\d{3,}$"
|
|
3725
|
+
}
|
|
3726
|
+
},
|
|
3727
|
+
"unblockCondition": {
|
|
3728
|
+
"type": "string",
|
|
3729
|
+
"minLength": 1
|
|
3730
|
+
},
|
|
3731
|
+
"recommendedDisposition": {
|
|
3732
|
+
"$ref": "#/$defs/ApprovalDisposition"
|
|
3733
|
+
},
|
|
3734
|
+
"resolution": {
|
|
3735
|
+
"$ref": "#/$defs/ApprovalResolution"
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
},
|
|
3556
3739
|
"ClarificationOption": {
|
|
3557
3740
|
"type": "object",
|
|
3558
3741
|
"required": [
|
|
@@ -3593,6 +3776,9 @@
|
|
|
3593
3776
|
"directionChange": {
|
|
3594
3777
|
"type": "string",
|
|
3595
3778
|
"minLength": 1
|
|
3779
|
+
},
|
|
3780
|
+
"disposition": {
|
|
3781
|
+
"$ref": "#/$defs/ApprovalDisposition"
|
|
3596
3782
|
}
|
|
3597
3783
|
}
|
|
3598
3784
|
},
|
|
@@ -8421,6 +8607,9 @@
|
|
|
8421
8607
|
"items": {
|
|
8422
8608
|
"$ref": "#/$defs/ClarificationOption"
|
|
8423
8609
|
}
|
|
8610
|
+
},
|
|
8611
|
+
"approvalContext": {
|
|
8612
|
+
"$ref": "#/$defs/ApprovalContext"
|
|
8424
8613
|
}
|
|
8425
8614
|
}
|
|
8426
8615
|
},
|
|
@@ -73,7 +73,7 @@ okstra user-response show --report <reportPath>
|
|
|
73
73
|
|
|
74
74
|
Returns `{reportPath, rows: [{id, kind, blocks, status, statement, expectedForm, options, contextRefs, resolvedRefs}]}`.
|
|
75
75
|
|
|
76
|
-
Each entry of `options[]` is `{role, answer, rationale, scopeImpact, addedWork, directionChange}`. `role` is `recommended` for exactly one entry and `alternative` for the rest; `scopeImpact` is a token list drawn from `in-repo` / `cross-repo` / `new-schema` / `deferrable`; `addedWork` and `directionChange` are one sentence each. A schema-v1 report has nowhere to record impact, so those
|
|
76
|
+
Each entry of `options[]` is `{role, answer, rationale, scopeImpact, addedWork, directionChange, disposition}`. `role` is `recommended` for exactly one entry and `alternative` for the rest; `scopeImpact` is a token list drawn from `in-repo` / `cross-repo` / `new-schema` / `deferrable`; `addedWork` and `directionChange` are one sentence each. Activity-contract v1 approval options also carry the gate action in `disposition`. A legacy schema-v1 report has nowhere to record impact or a gate action, so those fields arrive empty — the CLI reconstructs only the answers from the report's `Expected form` cell.
|
|
77
77
|
|
|
78
78
|
`resolvedRefs` is `[{ref, definition}]` — the CLI has already looked up what each internal token (`RB-002`, `FU-001`, `§4.7`, …) means in the report body; `definition` is `null` only when the report text alone could not resolve it (e.g. a `path:line` pointer).
|
|
79
79
|
|
|
@@ -127,7 +127,7 @@ Record one entry — `{id, kind, value, rationale?, disposition}`, `kind` copied
|
|
|
127
127
|
|
|
128
128
|
| The user picks | `value` | `disposition` |
|
|
129
129
|
|---|---|---|
|
|
130
|
-
| One of the `options[]` entries | that option's `answer` text, not its short label | `answer` |
|
|
130
|
+
| One of the `options[]` entries | that option's `answer` text, not its short label | that option's `disposition` (`answer` only when absent on a legacy option) |
|
|
131
131
|
| `Enter directly` → their own answer | the user's utterance verbatim (rationale into `rationale`) | `answer` |
|
|
132
132
|
| Free text asking for the item to be re-asked | what the user wants re-asked, verbatim (empty → the raw statement) | `reframe` |
|
|
133
133
|
|
|
@@ -110,6 +110,14 @@ schema-version: {{ schemaVersion | yaml_scalar }}
|
|
|
110
110
|
{{ md_rest(aiTaskProperty, 3) }}
|
|
111
111
|
{% endif %}
|
|
112
112
|
|
|
113
|
+
{% if md_has("agentActivity") %}
|
|
114
|
+
## {{ t("agentActivity.heading") }}
|
|
115
|
+
|
|
116
|
+
{{ t("agentActivity.intro") }}
|
|
117
|
+
|
|
118
|
+
{{ md("agentActivity", 3) }}
|
|
119
|
+
{% endif %}
|
|
120
|
+
|
|
113
121
|
## Cross Verification Audit
|
|
114
122
|
|
|
115
123
|
{{ md("crossVerification", 3) }}
|