okstra 0.186.1 → 0.186.3
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 +3 -3
- package/docs/for-ai/skills/okstra-run.md +1 -1
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +2 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +18 -5
- package/runtime/prompts/lead/plan-body-verification.md +5 -5
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +4 -5
- package/runtime/python/okstra_ctl/approval_decisions.py +1 -1
- package/runtime/python/okstra_ctl/clarification_items.py +141 -28
- package/runtime/python/okstra_ctl/design_surfaces.py +103 -18
- package/runtime/python/okstra_ctl/implementation_direction.py +8 -9
- package/runtime/python/okstra_ctl/next_phase.py +24 -22
- package/runtime/python/okstra_ctl/render_final_report.py +4 -8
- package/runtime/python/okstra_ctl/report_assembly.py +5 -1
- package/runtime/python/okstra_ctl/run.py +34 -3
- package/runtime/python/okstra_ctl/user_response.py +2 -1
- package/runtime/python/okstra_ctl/worker_prompt_body.py +4 -15
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +2 -5
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +4 -2
- package/runtime/templates/reports/html/assets/base.css +19 -9
- package/runtime/templates/reports/html/assets/base.js +21 -0
- package/runtime/templates/reports/html/base.template.html +5 -2
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/reports/html/macros/forms.html +8 -3
- package/runtime/templates/reports/report.js +33 -0
- package/runtime/validators/validate-run.py +138 -47
- package/runtime/validators/validate_analysis_report.py +7 -6
- package/runtime/validators/validate_session_conformance.py +24 -3
|
@@ -274,6 +274,38 @@
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
// 선택 카드는 보여 주기만 하면 고른 것처럼 보이지만 값이 안 실린다.
|
|
278
|
+
// 카드를 누르면 같은 칸의 select 를 맞춘다.
|
|
279
|
+
function bindOptionCardPicks() {
|
|
280
|
+
var articles = document.querySelectorAll("article[data-response-id]");
|
|
281
|
+
for (var i = 0; i < articles.length; i++) {
|
|
282
|
+
(function (article) {
|
|
283
|
+
var sel = article.querySelector("select[data-response-id]");
|
|
284
|
+
if (!sel) return;
|
|
285
|
+
var cards = article.querySelectorAll(".clarification-option[data-option-value]");
|
|
286
|
+
var sync = function () {
|
|
287
|
+
for (var c = 0; c < cards.length; c++) {
|
|
288
|
+
cards[c].classList.toggle(
|
|
289
|
+
"is-selected",
|
|
290
|
+
cards[c].getAttribute("data-option-value") === sel.value
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
for (var c = 0; c < cards.length; c++) {
|
|
295
|
+
cards[c].addEventListener("click", function (event) {
|
|
296
|
+
if (sel.disabled) return;
|
|
297
|
+
if (event.target.closest && event.target.closest("a")) return;
|
|
298
|
+
sel.value = event.currentTarget.getAttribute("data-option-value") || "";
|
|
299
|
+
sel.dispatchEvent(new Event("change"));
|
|
300
|
+
sync();
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
sel.addEventListener("change", sync);
|
|
304
|
+
sync();
|
|
305
|
+
})(articles[i]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
277
309
|
function quotedReviewField(label, value) {
|
|
278
310
|
var cleaned = trimMultiline(value);
|
|
279
311
|
if (!cleaned) return "- " + label + ":\n";
|
|
@@ -547,6 +579,7 @@
|
|
|
547
579
|
setReaderMode("action");
|
|
548
580
|
}
|
|
549
581
|
bindOtherInputToggle();
|
|
582
|
+
bindOptionCardPicks();
|
|
550
583
|
}
|
|
551
584
|
|
|
552
585
|
if (typeof window !== "undefined") {
|
|
@@ -88,6 +88,13 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
|
88
88
|
stages_for_clarification,
|
|
89
89
|
)
|
|
90
90
|
from okstra_ctl import next_phase # noqa: E402
|
|
91
|
+
from okstra_ctl.clarification_items import ( # noqa: E402
|
|
92
|
+
APPROVAL_BLOCKS,
|
|
93
|
+
PROCEEDING_DISPOSITIONS,
|
|
94
|
+
clarification_disposition,
|
|
95
|
+
progress_blocking_ids,
|
|
96
|
+
row_blocks_progress,
|
|
97
|
+
)
|
|
91
98
|
from okstra_ctl.workflow import ( # noqa: E402
|
|
92
99
|
ERROR_ANALYSIS_ROUTING_DIRECTIONS,
|
|
93
100
|
PHASE_SEQUENCE,
|
|
@@ -4117,34 +4124,102 @@ def _state_classification(item: dict, gate_class: str) -> str:
|
|
|
4117
4124
|
return "dissent-isolated" if dissenting == 1 else "partial-consensus"
|
|
4118
4125
|
|
|
4119
4126
|
|
|
4120
|
-
def
|
|
4121
|
-
"""
|
|
4127
|
+
def _clarification_resolution(row: dict, context: dict) -> dict | None:
|
|
4128
|
+
"""계약 3.0 은 행의 `resolution`, 2.0 은 `approvalContext.resolution`."""
|
|
4129
|
+
for candidate in (row.get("resolution"), context.get("resolution")):
|
|
4130
|
+
if isinstance(candidate, dict):
|
|
4131
|
+
return candidate
|
|
4132
|
+
return None
|
|
4133
|
+
|
|
4134
|
+
|
|
4135
|
+
def _clarification_ids_on_activity(activity: dict) -> set[str]:
|
|
4136
|
+
refs: set[str] = set()
|
|
4137
|
+
for key in ("clarificationRefs", "evidenceRefs"):
|
|
4138
|
+
for value in activity.get(key) or []:
|
|
4139
|
+
if isinstance(value, str) and _APPROVAL_CLARIFICATION_ID_RE.fullmatch(value):
|
|
4140
|
+
refs.add(value)
|
|
4141
|
+
return refs
|
|
4142
|
+
|
|
4143
|
+
|
|
4144
|
+
def _plan_item_ids_for_clarification(
|
|
4145
|
+
row: dict, context: dict, data: dict,
|
|
4146
|
+
) -> list[str]:
|
|
4147
|
+
"""이 C 행이 가리키는 계획 항목.
|
|
4148
|
+
|
|
4149
|
+
계약 3.0 `approvalContext` 는 `planItemIds` 를 갖지 않는다. 활동
|
|
4150
|
+
`evidenceRefs` / `clarificationRefs` 와 `planItems[].clarificationRefs` 가
|
|
4151
|
+
역추적이다. 이 C 만 인용한 활동을 묶음 활동보다 앞세운다.
|
|
4152
|
+
"""
|
|
4153
|
+
linked = [
|
|
4154
|
+
item_id
|
|
4155
|
+
for item_id in (context.get("planItemIds") or [])
|
|
4156
|
+
if isinstance(item_id, str) and item_id
|
|
4157
|
+
]
|
|
4158
|
+
if linked:
|
|
4159
|
+
return linked
|
|
4160
|
+
row_id = str(row.get("id") or "")
|
|
4161
|
+
if not row_id:
|
|
4162
|
+
return []
|
|
4163
|
+
singleton: list[str] = []
|
|
4164
|
+
bulk: list[str] = []
|
|
4165
|
+
for activity in data.get("agentActivity") or []:
|
|
4166
|
+
if not isinstance(activity, dict):
|
|
4167
|
+
continue
|
|
4168
|
+
refs = _clarification_ids_on_activity(activity)
|
|
4169
|
+
if row_id not in refs:
|
|
4170
|
+
continue
|
|
4171
|
+
ids = [
|
|
4172
|
+
item_id
|
|
4173
|
+
for item_id in (activity.get("planItemIds") or [])
|
|
4174
|
+
if isinstance(item_id, str) and item_id
|
|
4175
|
+
]
|
|
4176
|
+
if refs == {row_id}:
|
|
4177
|
+
singleton.extend(ids)
|
|
4178
|
+
else:
|
|
4179
|
+
bulk.extend(ids)
|
|
4180
|
+
if singleton or bulk:
|
|
4181
|
+
return singleton or bulk
|
|
4182
|
+
items = (
|
|
4183
|
+
((data.get("implementationPlanning") or {}).get("planBodyVerification")
|
|
4184
|
+
or {}).get("planItems") or []
|
|
4185
|
+
)
|
|
4186
|
+
return [
|
|
4187
|
+
str(item.get("id") or "")
|
|
4188
|
+
for item in items
|
|
4189
|
+
if isinstance(item, dict)
|
|
4190
|
+
and row_id in {
|
|
4191
|
+
ref for ref in (item.get("clarificationRefs") or [])
|
|
4192
|
+
if isinstance(ref, str)
|
|
4193
|
+
}
|
|
4194
|
+
and item.get("id")
|
|
4195
|
+
]
|
|
4196
|
+
|
|
4197
|
+
|
|
4198
|
+
def _user_accepted_plan_item_ids(data: dict) -> set[str]:
|
|
4199
|
+
"""사용자가 진행 처분을 고른 승인 행이 가리키는 계획 항목.
|
|
4200
|
+
|
|
4201
|
+
DISAGREE 표는 그대로 남는다. 게이트만 `has-dissent` 로 내린다.
|
|
4202
|
+
"""
|
|
4122
4203
|
accepted: set[str] = set()
|
|
4123
4204
|
for row in data.get("clarificationItems") or []:
|
|
4124
4205
|
if not isinstance(row, dict) or row.get("blocks") != "approval":
|
|
4125
4206
|
continue
|
|
4207
|
+
if row_blocks_progress(
|
|
4208
|
+
str(row.get("status") or ""), clarification_disposition(row)
|
|
4209
|
+
):
|
|
4210
|
+
continue
|
|
4126
4211
|
context = row.get("approvalContext")
|
|
4127
4212
|
if not isinstance(context, dict):
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
if (
|
|
4131
|
-
row.get("status") == "resolved"
|
|
4132
|
-
and context.get("classification") == "noncritical-dissent"
|
|
4133
|
-
and isinstance(resolution, dict)
|
|
4134
|
-
and resolution.get("disposition") == "accept-risk"
|
|
4135
|
-
and str(resolution.get("userText") or "").strip()
|
|
4136
|
-
and _approval_context_activity_refs_exist(
|
|
4137
|
-
data, str(row.get("id") or ""), context, resolution
|
|
4138
|
-
)
|
|
4139
|
-
):
|
|
4140
|
-
accepted.update(
|
|
4141
|
-
item_id
|
|
4142
|
-
for item_id in context.get("planItemIds") or []
|
|
4143
|
-
if isinstance(item_id, str)
|
|
4144
|
-
)
|
|
4213
|
+
context = {}
|
|
4214
|
+
accepted.update(_plan_item_ids_for_clarification(row, context, data))
|
|
4145
4215
|
return accepted
|
|
4146
4216
|
|
|
4147
4217
|
|
|
4218
|
+
def _resolved_noncritical_dissent_ids(data: dict) -> set[str]:
|
|
4219
|
+
"""호환 별칭. 새 코드는 `_user_accepted_plan_item_ids` 를 쓴다."""
|
|
4220
|
+
return _user_accepted_plan_item_ids(data)
|
|
4221
|
+
|
|
4222
|
+
|
|
4148
4223
|
def _is_dissent_downgraded(
|
|
4149
4224
|
item: dict,
|
|
4150
4225
|
pbv: dict,
|
|
@@ -4152,16 +4227,11 @@ def _is_dissent_downgraded(
|
|
|
4152
4227
|
) -> bool:
|
|
4153
4228
|
"""Whether a surviving `majority-disagree` item stops blocking approval.
|
|
4154
4229
|
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
row can lower the item to `has-dissent`. Correctness-critical defects remain
|
|
4158
|
-
blocking regardless of the user's selected disposition.
|
|
4230
|
+
사용자 진행 처분(`accept-risk` / `select` / `answer`)이 있으면 표는 남기고
|
|
4231
|
+
게이트만 `has-dissent` 로 내린다. 분류와 자가수정 소진 여부는 보지 않는다.
|
|
4159
4232
|
"""
|
|
4160
4233
|
return (
|
|
4161
4234
|
_classify_plan_item_gate(item) == "majority-disagree"
|
|
4162
|
-
and not _is_correctness_critical(item)
|
|
4163
|
-
and _has_planner_fixable_majority(item)
|
|
4164
|
-
and _self_fix_budget_exhausted(pbv)
|
|
4165
4235
|
and str(item.get("id") or "") in accepted_item_ids
|
|
4166
4236
|
)
|
|
4167
4237
|
|
|
@@ -4477,11 +4547,15 @@ def _gate_blocking_causes(
|
|
|
4477
4547
|
|
|
4478
4548
|
|
|
4479
4549
|
_APPROVAL_DISPOSITIONS_BY_CLASSIFICATION = {
|
|
4480
|
-
"user-decision": frozenset(
|
|
4550
|
+
"user-decision": frozenset(
|
|
4551
|
+
{"select", "accept-risk", "request-revision", "reject"}
|
|
4552
|
+
),
|
|
4481
4553
|
"noncritical-dissent": frozenset(
|
|
4482
4554
|
{"accept-risk", "request-revision", "reject"}
|
|
4483
4555
|
),
|
|
4484
|
-
"correctness-critical": frozenset(
|
|
4556
|
+
"correctness-critical": frozenset(
|
|
4557
|
+
{"accept-risk", "request-revision", "reject"}
|
|
4558
|
+
),
|
|
4485
4559
|
}
|
|
4486
4560
|
|
|
4487
4561
|
|
|
@@ -4949,6 +5023,8 @@ def _validate_resolved_approval(
|
|
|
4949
5023
|
f"final-report data.json: resolved approval clarification `{row_id}` "
|
|
4950
5024
|
"requires non-empty resolution.userText."
|
|
4951
5025
|
)
|
|
5026
|
+
if str(resolution.get("disposition") or "") in PROCEEDING_DISPOSITIONS:
|
|
5027
|
+
return
|
|
4952
5028
|
check_refs = resolution.get("checkRefs")
|
|
4953
5029
|
if not isinstance(check_refs, list) or not any(
|
|
4954
5030
|
isinstance(value, str) and value for value in check_refs
|
|
@@ -5097,6 +5173,7 @@ def _validate_correctness_resolution(
|
|
|
5097
5173
|
if (
|
|
5098
5174
|
context.get("classification") != "correctness-critical"
|
|
5099
5175
|
or row.get("status") != "resolved"
|
|
5176
|
+
or clarification_disposition(row) in PROCEEDING_DISPOSITIONS
|
|
5100
5177
|
):
|
|
5101
5178
|
return
|
|
5102
5179
|
row_id = str(row.get("id") or "<unknown>")
|
|
@@ -5349,11 +5426,13 @@ def _validate_approval_context(
|
|
|
5349
5426
|
activity_timestamps,
|
|
5350
5427
|
failures,
|
|
5351
5428
|
)
|
|
5352
|
-
if report_approved and
|
|
5429
|
+
if report_approved and row_blocks_progress(
|
|
5430
|
+
str(row.get("status") or ""), clarification_disposition(row)
|
|
5431
|
+
):
|
|
5353
5432
|
failures.append(
|
|
5354
5433
|
f"final-report data.json: approval is true while clarification `{row_id}` "
|
|
5355
|
-
f"has status `{row.get('status')}`; open and
|
|
5356
|
-
"rows remain blocking."
|
|
5434
|
+
f"has status `{row.get('status')}`; open and return-disposition "
|
|
5435
|
+
"approval rows remain blocking."
|
|
5357
5436
|
)
|
|
5358
5437
|
|
|
5359
5438
|
|
|
@@ -5425,7 +5504,9 @@ def _validate_v3_approval_context(data: dict, failures: list[str]) -> None:
|
|
|
5425
5504
|
row, context, failures, schema_version="3.0"
|
|
5426
5505
|
)
|
|
5427
5506
|
_validate_v3_resolution_links(row, activities, failures)
|
|
5428
|
-
if approved and
|
|
5507
|
+
if approved and row_blocks_progress(
|
|
5508
|
+
str(row.get("status") or ""), clarification_disposition(row)
|
|
5509
|
+
):
|
|
5429
5510
|
failures.append(
|
|
5430
5511
|
f"final-report data.json: approval is true while clarification "
|
|
5431
5512
|
f"`{row.get('id')}` remains `{row.get('status')}`."
|
|
@@ -6165,6 +6246,7 @@ def _validate_approval_clarification_backtrace(
|
|
|
6165
6246
|
|
|
6166
6247
|
|
|
6167
6248
|
_RERUN_FLAG = "--answered-clarifications"
|
|
6249
|
+
_USER_RESPONSE_HINT = re.compile(r"okstra-user-response", re.IGNORECASE)
|
|
6168
6250
|
_APPROVE_HINT = re.compile(r"--approve|\bapprov", re.IGNORECASE)
|
|
6169
6251
|
|
|
6170
6252
|
|
|
@@ -6182,10 +6264,9 @@ def _next_step_texts(steps: object) -> list[str]:
|
|
|
6182
6264
|
return texts
|
|
6183
6265
|
|
|
6184
6266
|
|
|
6185
|
-
def
|
|
6186
|
-
return
|
|
6187
|
-
|
|
6188
|
-
for row in data.get("clarificationItems") or []
|
|
6267
|
+
def _has_unresolved_approval_blocker(data: dict) -> bool:
|
|
6268
|
+
return bool(
|
|
6269
|
+
progress_blocking_ids(data.get("clarificationItems"), APPROVAL_BLOCKS)
|
|
6189
6270
|
)
|
|
6190
6271
|
|
|
6191
6272
|
|
|
@@ -6197,7 +6278,14 @@ def _planning_gate_blocks_approval(data: dict) -> bool:
|
|
|
6197
6278
|
if not isinstance(verification, dict):
|
|
6198
6279
|
return False
|
|
6199
6280
|
gate = str(verification.get("gateResult") or "").strip().lower()
|
|
6200
|
-
|
|
6281
|
+
if gate == "aborted-non-result":
|
|
6282
|
+
return True
|
|
6283
|
+
if gate != "blocked-by-disagreement":
|
|
6284
|
+
return False
|
|
6285
|
+
return _has_unresolved_approval_blocker(data) or not any(
|
|
6286
|
+
isinstance(row, dict) and row.get("blocks") == "approval"
|
|
6287
|
+
for row in data.get("clarificationItems") or []
|
|
6288
|
+
)
|
|
6201
6289
|
|
|
6202
6290
|
|
|
6203
6291
|
def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
|
|
@@ -6212,19 +6300,20 @@ def _validate_rerun_guidance(data: dict, failures: list[str]) -> None:
|
|
|
6212
6300
|
"""
|
|
6213
6301
|
if (data.get("header") or {}).get("taskType") != "implementation-planning":
|
|
6214
6302
|
return
|
|
6215
|
-
|
|
6216
|
-
if not has_blocker:
|
|
6303
|
+
if not _has_unresolved_approval_blocker(data):
|
|
6217
6304
|
return
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6305
|
+
texts = _next_step_texts(data.get("recommendedNextSteps"))
|
|
6306
|
+
if any(_RERUN_FLAG in text for text in texts) and any(
|
|
6307
|
+
_USER_RESPONSE_HINT.search(text) for text in texts
|
|
6308
|
+
):
|
|
6221
6309
|
return
|
|
6222
6310
|
failures.append(
|
|
6223
|
-
"final-report data.json: this plan withholds approval on
|
|
6311
|
+
"final-report data.json: this plan withholds approval on an unresolved "
|
|
6224
6312
|
"`blocks: approval` clarification, but no `recommendedNextSteps` entry "
|
|
6225
|
-
|
|
6226
|
-
"step's `text` or one of its
|
|
6227
|
-
"prints the exact ids and flag
|
|
6313
|
+
"tells the reader the command to run now — name `/okstra-user-response` "
|
|
6314
|
+
f"and the `{_RERUN_FLAG}` re-run in a step's `text` or one of its "
|
|
6315
|
+
"`commands`. `okstra recap assemble` prints the exact ids and flag "
|
|
6316
|
+
"value once the answers are recorded."
|
|
6228
6317
|
)
|
|
6229
6318
|
|
|
6230
6319
|
|
|
@@ -6239,7 +6328,7 @@ def _validate_approval_guidance(data: dict, failures: list[str]) -> None:
|
|
|
6239
6328
|
planning = data.get("implementationPlanning")
|
|
6240
6329
|
if not isinstance(planning, dict) or planning.get("outcome") != "plan-ready":
|
|
6241
6330
|
return
|
|
6242
|
-
if
|
|
6331
|
+
if _has_unresolved_approval_blocker(data) or _planning_gate_blocks_approval(data):
|
|
6243
6332
|
return
|
|
6244
6333
|
if _report_already_approved(data):
|
|
6245
6334
|
return
|
|
@@ -7265,11 +7354,13 @@ def _validate_tie_received_extra_vote(
|
|
|
7265
7354
|
pbv = ip.get("planBodyVerification")
|
|
7266
7355
|
if not isinstance(pbv, dict):
|
|
7267
7356
|
return
|
|
7357
|
+
accepted = _resolved_noncritical_dissent_ids(data)
|
|
7268
7358
|
missing = sorted({
|
|
7269
7359
|
str(item.get("id") or "").strip()
|
|
7270
7360
|
for item in pbv.get("planItems") or []
|
|
7271
7361
|
if isinstance(item, dict)
|
|
7272
7362
|
and not item.get("carriedForwardFromSeq")
|
|
7363
|
+
and str(item.get("id") or "").strip() not in accepted
|
|
7273
7364
|
and _stage_scope_bucket(item, pbv) == "in-scope"
|
|
7274
7365
|
and _is_even_blocking_split(item)
|
|
7275
7366
|
and _distinct_verdict_workers(item) < 3
|
|
@@ -54,11 +54,12 @@ _ANALYSIS_PARENT_KEYS = {
|
|
|
54
54
|
"feature-analysis": "featureAnalysis",
|
|
55
55
|
"change-impact-analysis": "changeImpactAnalysis",
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_report_writer_role(role: str) -> bool:
|
|
60
|
+
return "reportwriter" in re.sub(r"[^a-z0-9]", "", role.lower())
|
|
61
|
+
|
|
62
|
+
|
|
62
63
|
_FINAL_ANALYSIS_REPORT_RE = re.compile(
|
|
63
64
|
r"^final-report-(?P<task_type>project-analysis|feature-analysis|"
|
|
64
65
|
r"change-impact-analysis)-(?P<seq>\d{3})\.md$"
|
|
@@ -467,7 +468,7 @@ def validate_analysis_semantics(
|
|
|
467
468
|
) -> None:
|
|
468
469
|
required_roles = _required_worker_roles(run_manifest, errors)
|
|
469
470
|
analysis_worker_roles = tuple(
|
|
470
|
-
role for role in required_roles if role
|
|
471
|
+
role for role in required_roles if not _is_report_writer_role(role)
|
|
471
472
|
)
|
|
472
473
|
verdict_roles_unambiguous = _validate_execution_status_roles(
|
|
473
474
|
data, analysis_worker_roles, errors
|
|
@@ -235,8 +235,24 @@ def _worker_role(value: str) -> str | None:
|
|
|
235
235
|
|
|
236
236
|
|
|
237
237
|
def _worker_roles(worker: dict) -> set[str]:
|
|
238
|
-
role
|
|
239
|
-
|
|
238
|
+
"""명단 `role` 과 `workerId` 를 활동 `agent` 가 쓸 수 있는 별칭으로 펼친다.
|
|
239
|
+
|
|
240
|
+
활동은 `codex` / `codex-worker` 를 쓰고 명단은 `Codex worker` 를 쓴다.
|
|
241
|
+
역할 문자열만 보면 둘은 다른 사람이 된다.
|
|
242
|
+
"""
|
|
243
|
+
roles: set[str] = set()
|
|
244
|
+
for raw in (worker.get("role"), worker.get("workerId")):
|
|
245
|
+
role = _worker_role(str(raw or ""))
|
|
246
|
+
if not role:
|
|
247
|
+
continue
|
|
248
|
+
compact = role.replace(" ", "-")
|
|
249
|
+
roles.add(role)
|
|
250
|
+
roles.add(compact)
|
|
251
|
+
if role.endswith(" worker"):
|
|
252
|
+
roles.add(role[: -len(" worker")])
|
|
253
|
+
if compact.endswith("-worker"):
|
|
254
|
+
roles.add(compact[: -len("-worker")])
|
|
255
|
+
return roles
|
|
240
256
|
|
|
241
257
|
|
|
242
258
|
def _analysis_workers(team_state: Mapping[str, Any]) -> list[dict]:
|
|
@@ -1036,12 +1052,17 @@ def _check_activity_contract(
|
|
|
1036
1052
|
if not _is_activity_contract_v1_planning(run_manifest):
|
|
1037
1053
|
return
|
|
1038
1054
|
indexed = _activity_index(evidence.activities)
|
|
1055
|
+
workers = [
|
|
1056
|
+
worker
|
|
1057
|
+
for worker in (team_state.get("workers") or [])
|
|
1058
|
+
if isinstance(worker, dict)
|
|
1059
|
+
]
|
|
1039
1060
|
analysis_workers = _analysis_workers(team_state)
|
|
1040
1061
|
report_data = _read_report_data(report_path)
|
|
1041
1062
|
_check_activity_id_order(evidence.activities, errors)
|
|
1042
1063
|
_check_projected_agent_activity(evidence.activities, report_data, errors)
|
|
1043
1064
|
_check_activity_worker_pairs(evidence, indexed, analysis_workers, errors)
|
|
1044
|
-
_check_activity_worker_agents(indexed,
|
|
1065
|
+
_check_activity_worker_agents(indexed, workers, errors)
|
|
1045
1066
|
_check_activity_round_counts(indexed, report_data, run_dir, suffix, errors)
|
|
1046
1067
|
_check_activity_user_decisions(indexed, report_data, errors)
|
|
1047
1068
|
|