okstra 0.202.0 → 0.205.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/install-assets.mjs +3 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/runtime-manifest.mjs +2 -1
- package/dist/lib/runtime-manifest.mjs.map +1 -1
- package/dist/lib/types.d.mts +2 -1
- package/docs/architecture/storage-model.md +14 -11
- package/docs/architecture.md +26 -20
- package/docs/cli.md +15 -12
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +3 -9
- package/docs/project-structure-overview.md +39 -11
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation-option-selection.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +2 -1
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
- package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
- package/runtime/prompts/profiles/error-analysis.json +39 -0
- package/runtime/prompts/profiles/error-analysis.md +0 -25
- package/runtime/prompts/profiles/feature-analysis.json +31 -0
- package/runtime/prompts/profiles/feature-analysis.md +0 -20
- package/runtime/prompts/profiles/final-verification.json +30 -0
- package/runtime/prompts/profiles/final-verification.md +4 -23
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +4 -29
- package/runtime/prompts/profiles/implementation.json +30 -0
- package/runtime/prompts/profiles/implementation.md +1 -20
- package/runtime/prompts/profiles/improvement-discovery.json +31 -0
- package/runtime/prompts/profiles/improvement-discovery.md +0 -20
- package/runtime/prompts/profiles/project-analysis.json +31 -0
- package/runtime/prompts/profiles/project-analysis.md +0 -20
- package/runtime/prompts/profiles/release-handoff.json +5 -0
- package/runtime/prompts/profiles/release-handoff.md +74 -74
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +14 -18
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +23 -8
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +38 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
- package/runtime/python/okstra_ctl/design_prep.py +34 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
- package/runtime/python/okstra_ctl/error_report.py +4 -3
- package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
- package/runtime/python/okstra_ctl/handoff.py +384 -286
- package/runtime/python/okstra_ctl/handoff_verification.py +25 -6
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -4
- package/runtime/python/okstra_ctl/manager_launch.py +1 -1
- package/runtime/python/okstra_ctl/manager_paths.py +14 -3
- package/runtime/python/okstra_ctl/manager_store.py +210 -3
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/manager_view.py +2 -1
- package/runtime/python/okstra_ctl/model_discovery.py +30 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
- package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
- package/runtime/python/okstra_ctl/models.py +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +16 -6
- package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
- package/runtime/python/okstra_ctl/option_comparison.py +168 -0
- package/runtime/python/okstra_ctl/path_hints.py +9 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
- package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
- package/runtime/python/okstra_ctl/render.py +79 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
- package/runtime/python/okstra_ctl/report_translation.py +2 -1
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
- package/runtime/python/okstra_ctl/role_requirements.py +142 -129
- package/runtime/python/okstra_ctl/rollup.py +3 -1
- package/runtime/python/okstra_ctl/run.py +76 -29
- package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
- package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
- package/runtime/python/okstra_ctl/stage_map.py +16 -2
- package/runtime/python/okstra_ctl/stage_targets.py +209 -43
- package/runtime/python/okstra_ctl/team.py +22 -13
- package/runtime/python/okstra_ctl/time_report.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +3 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +3 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +12 -21
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +18 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
- package/runtime/skills/okstra-rollup/SKILL.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +31 -12
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +12 -10
- package/runtime/skills/okstra-setup/references/project-config.md +7 -6
- package/runtime/skills/okstra-usage/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/manager/view.template.html +1 -0
- package/runtime/templates/report-writer-prompt-preamble.md +8 -0
- package/runtime/templates/reports/brief.template.md +14 -4
- package/runtime/templates/reports/html/i18n/en.json +5 -4
- package/runtime/templates/reports/html/i18n/ko.json +5 -4
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +6 -4
- package/runtime/templates/translator-prompt-preamble.md +36 -0
- package/runtime/validators/checks/validate-assets-01.py +7 -8
- package/runtime/validators/validate-brief.py +70 -0
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +72 -15
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -86
- package/docs/for-ai/skills/okstra-memory.md +0 -126
- package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
- package/docs/for-ai/skills/okstra-rollup.md +0 -114
- package/docs/for-ai/skills/okstra-run.md +0 -250
- package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
- package/docs/for-ai/skills/okstra-setup.md +0 -167
- package/docs/for-ai/skills/okstra-usage.md +0 -29
- package/docs/for-ai/skills/okstra-user-response.md +0 -72
- package/runtime/agents/workers/claude-worker.md +0 -128
- package/runtime/agents/workers/report-writer-worker.md +0 -37
- package/runtime/agents/workers/translator-worker.md +0 -63
- package/runtime/prompts/duties/acceptance-critic.md +0 -44
- package/runtime/prompts/duties/acceptance-verifier.md +0 -44
- package/runtime/prompts/duties/analysis-worker.md +0 -44
- package/runtime/prompts/duties/code-reviewer.md +0 -44
- package/runtime/prompts/duties/common.md +0 -39
- package/runtime/prompts/duties/diagnosis-worker.md +0 -44
- package/runtime/prompts/duties/direction-selection-worker.md +0 -44
- package/runtime/prompts/duties/discovery-worker.md +0 -44
- package/runtime/prompts/duties/implementation-executor.md +0 -44
- package/runtime/prompts/duties/implementation-verifier.md +0 -44
- package/runtime/prompts/duties/lead.md +0 -44
- package/runtime/prompts/duties/planning-worker.md +0 -52
- package/runtime/prompts/duties/report-writer.md +0 -44
- package/runtime/prompts/duties/reverification-worker.md +0 -44
- package/runtime/prompts/duties/schedule-verifier.md +0 -44
- package/runtime/prompts/duties/scope-critic.md +0 -44
- package/runtime/prompts/duties/technical-verification-worker.md +0 -44
- package/runtime/prompts/duties/translator.md +0 -44
- package/runtime/python/okstra_ctl/pane_title.py +0 -154
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Load the role structure a phase profile declares.
|
|
2
|
+
|
|
3
|
+
The structure lives in `prompts/profiles/<task-type>.json` beside the profile's
|
|
4
|
+
Markdown task instructions. The Markdown owns procedure and per-task inputs and
|
|
5
|
+
outputs; it declares no machine-readable roster. See ADR-0017.
|
|
6
|
+
"""
|
|
2
7
|
from __future__ import annotations
|
|
3
8
|
|
|
4
|
-
from dataclasses import dataclass
|
|
9
|
+
from dataclasses import dataclass, field
|
|
5
10
|
from pathlib import Path
|
|
11
|
+
from typing import Union
|
|
6
12
|
|
|
7
13
|
from .domain.role import RoleCatalogError, normalize_role, role_for_duty
|
|
14
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
class RoleProfileError(ValueError):
|
|
@@ -12,13 +19,16 @@ class RoleProfileError(ValueError):
|
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
@dataclass(frozen=True)
|
|
15
|
-
class
|
|
22
|
+
class StaticRoleRequirement:
|
|
23
|
+
"""A role slot whose count the run fixes before dispatch."""
|
|
24
|
+
|
|
16
25
|
role: str
|
|
17
26
|
min_count: int
|
|
18
27
|
recommended_count: int
|
|
19
28
|
max_count: int
|
|
20
29
|
duty: str
|
|
21
|
-
|
|
30
|
+
|
|
31
|
+
dynamic: bool = field(default=False, init=False)
|
|
22
32
|
|
|
23
33
|
def signature(self) -> tuple[str, int, int, int, str, bool]:
|
|
24
34
|
return (
|
|
@@ -27,138 +37,129 @@ class RoleRequirement:
|
|
|
27
37
|
self.recommended_count,
|
|
28
38
|
self.max_count,
|
|
29
39
|
self.duty,
|
|
30
|
-
|
|
40
|
+
False,
|
|
31
41
|
)
|
|
32
42
|
|
|
33
43
|
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class DynamicRoleRequirement:
|
|
46
|
+
"""A role slot the runtime adds once per activated source slot.
|
|
47
|
+
|
|
48
|
+
It carries no counts. Expressing it as 0/0/0 made a dynamic role look like a
|
|
49
|
+
static role nobody selected, which is a different thing: the upper bound is
|
|
50
|
+
the number of activated source slots, not zero.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
role: str
|
|
54
|
+
duty: str
|
|
55
|
+
source_role_ids: tuple[str, ...]
|
|
56
|
+
activation: str
|
|
57
|
+
|
|
58
|
+
dynamic: bool = field(default=True, init=False)
|
|
59
|
+
|
|
60
|
+
def signature(self) -> tuple[str, str, tuple[str, ...], str, bool]:
|
|
61
|
+
return (self.role, self.duty, self.source_role_ids, self.activation, True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
RoleRequirement = Union[StaticRoleRequirement, DynamicRoleRequirement]
|
|
65
|
+
|
|
66
|
+
|
|
34
67
|
@dataclass(frozen=True)
|
|
35
68
|
class RoleProfile:
|
|
36
69
|
roles: tuple[RoleRequirement, ...]
|
|
37
70
|
|
|
38
|
-
def role_signature(self) -> tuple[tuple[
|
|
71
|
+
def role_signature(self) -> tuple[tuple[object, ...], ...]:
|
|
39
72
|
return tuple(requirement.signature() for requirement in self.roles)
|
|
40
73
|
|
|
41
74
|
|
|
42
|
-
_REQUIRED_FIELDS = frozenset({"role", "min", "recommended", "max", "duty"})
|
|
43
|
-
_ALLOWED_FIELDS = _REQUIRED_FIELDS | {"dynamic"}
|
|
44
|
-
|
|
45
|
-
|
|
46
75
|
def load_role_profile(path: Path) -> RoleProfile:
|
|
47
|
-
"""Load
|
|
48
|
-
|
|
76
|
+
"""Load the structure contract for the profile at *path*.
|
|
77
|
+
|
|
78
|
+
Callers hold the profile's Markdown path because that is what the rest of
|
|
79
|
+
the prepare step reads, so this resolves the `.json` sibling itself. Passing
|
|
80
|
+
the `.json` path directly is the same call.
|
|
81
|
+
"""
|
|
82
|
+
profile_path = Path(path).with_suffix(".json")
|
|
49
83
|
try:
|
|
50
|
-
|
|
84
|
+
payload = load_owned_object(profile_path, artifact="role profile")
|
|
85
|
+
except JsonBoundaryError as exc:
|
|
86
|
+
raise RoleProfileError(f"invalid role profile: {profile_path}: {exc}") from exc
|
|
51
87
|
except OSError as exc:
|
|
52
88
|
raise RoleProfileError(f"cannot read role profile: {profile_path}") from exc
|
|
53
|
-
|
|
89
|
+
|
|
90
|
+
errors: list[str] = []
|
|
91
|
+
if payload.get("schemaVersion") != "1.0":
|
|
92
|
+
errors.append("role profile schemaVersion must be 1.0")
|
|
93
|
+
raw_roles = payload.get("roles")
|
|
94
|
+
if not isinstance(raw_roles, list):
|
|
95
|
+
raise RoleProfileError("invalid role profile:\n- roles must be a list")
|
|
96
|
+
|
|
54
97
|
requirements = tuple(
|
|
55
|
-
|
|
98
|
+
_requirement(raw, index, errors)
|
|
56
99
|
for index, raw in enumerate(raw_roles, 1)
|
|
57
100
|
)
|
|
58
101
|
_validate_static_role_duplicates(requirements, errors)
|
|
102
|
+
_validate_dynamic_sources(requirements, errors)
|
|
59
103
|
if errors:
|
|
60
104
|
raise RoleProfileError("invalid role profile:\n- " + "\n- ".join(errors))
|
|
61
105
|
return RoleProfile(requirements)
|
|
62
106
|
|
|
63
107
|
|
|
64
|
-
def
|
|
65
|
-
blocks = _yaml_blocks_with_roles(text)
|
|
66
|
-
if len(blocks) != 1:
|
|
67
|
-
return [], ["profile must declare exactly one fenced YAML roles block"]
|
|
68
|
-
lines = blocks[0]
|
|
69
|
-
if lines == ["roles: []"]:
|
|
70
|
-
return [], []
|
|
71
|
-
if not lines or lines[0] != "roles:":
|
|
72
|
-
return [], ["roles block must start with roles:"]
|
|
73
|
-
roles: list[dict[str, str]] = []
|
|
74
|
-
errors: list[str] = []
|
|
75
|
-
current: dict[str, str] | None = None
|
|
76
|
-
for line_number, line in enumerate(lines[1:], 2):
|
|
77
|
-
if not line.strip():
|
|
78
|
-
continue
|
|
79
|
-
if line.startswith(" - "):
|
|
80
|
-
if current is not None:
|
|
81
|
-
roles.append(current)
|
|
82
|
-
current = {}
|
|
83
|
-
_add_yaml_field(current, line[4:], line_number, errors)
|
|
84
|
-
continue
|
|
85
|
-
if line.startswith(" ") and current is not None:
|
|
86
|
-
_add_yaml_field(current, line[4:], line_number, errors)
|
|
87
|
-
continue
|
|
88
|
-
errors.append(f"roles block line {line_number} is not a role field")
|
|
89
|
-
if current is not None:
|
|
90
|
-
roles.append(current)
|
|
91
|
-
elif not errors:
|
|
92
|
-
errors.append("roles block must contain a role or use roles: []")
|
|
93
|
-
return roles, errors
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _yaml_blocks_with_roles(text: str) -> list[list[str]]:
|
|
97
|
-
blocks: list[list[str]] = []
|
|
98
|
-
current: list[str] | None = None
|
|
99
|
-
for line in text.splitlines():
|
|
100
|
-
if line.strip() == "```yaml":
|
|
101
|
-
current = []
|
|
102
|
-
continue
|
|
103
|
-
if line.strip() == "```" and current is not None:
|
|
104
|
-
if any(item.strip().startswith("roles:") for item in current):
|
|
105
|
-
blocks.append(current)
|
|
106
|
-
current = None
|
|
107
|
-
continue
|
|
108
|
-
if current is not None:
|
|
109
|
-
current.append(line)
|
|
110
|
-
return blocks
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _add_yaml_field(
|
|
114
|
-
fields: dict[str, str], line: str, line_number: int, errors: list[str],
|
|
115
|
-
) -> None:
|
|
116
|
-
if ":" not in line:
|
|
117
|
-
errors.append(f"roles block line {line_number} must contain key: value")
|
|
118
|
-
return
|
|
119
|
-
key, value = (part.strip() for part in line.split(":", 1))
|
|
120
|
-
if not key or not value:
|
|
121
|
-
errors.append(f"roles block line {line_number} must contain key: value")
|
|
122
|
-
return
|
|
123
|
-
if key in fields:
|
|
124
|
-
errors.append(f"roles block line {line_number} repeats {key}")
|
|
125
|
-
return
|
|
126
|
-
fields[key] = value
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _validate_requirement(
|
|
130
|
-
raw: dict[str, str], index: int, errors: list[str],
|
|
131
|
-
) -> RoleRequirement:
|
|
108
|
+
def _requirement(raw: object, index: int, errors: list[str]) -> RoleRequirement:
|
|
132
109
|
prefix = f"role requirement {index}"
|
|
133
|
-
|
|
134
|
-
errors.append(f"{prefix}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
raw
|
|
110
|
+
if not isinstance(raw, dict):
|
|
111
|
+
errors.append(f"{prefix} must be an object")
|
|
112
|
+
return StaticRoleRequirement("", 0, 0, 0, "")
|
|
113
|
+
mode = raw.get("mode")
|
|
114
|
+
if mode == "dynamic":
|
|
115
|
+
return _dynamic_requirement(raw, prefix, errors)
|
|
116
|
+
if mode == "static":
|
|
117
|
+
return _static_requirement(raw, prefix, errors)
|
|
118
|
+
errors.append(f"{prefix} mode must be static or dynamic")
|
|
119
|
+
return StaticRoleRequirement("", 0, 0, 0, "")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _static_requirement(
|
|
123
|
+
raw: dict[str, object], prefix: str, errors: list[str]
|
|
124
|
+
) -> StaticRoleRequirement:
|
|
125
|
+
role = _validate_role(raw.get("roleId"), prefix, errors)
|
|
126
|
+
duty = _validate_duty(raw.get("dutyId"), role, prefix, errors)
|
|
127
|
+
counts = tuple(
|
|
128
|
+
_validate_count(raw.get(key), key, prefix, errors)
|
|
129
|
+
for key in ("min", "recommended", "max")
|
|
141
130
|
)
|
|
142
|
-
|
|
143
|
-
raw.get("recommended", ""), "recommended", prefix, errors,
|
|
144
|
-
)
|
|
145
|
-
max_count = _validate_non_negative_integer(
|
|
146
|
-
raw.get("max", ""), "max", prefix, errors,
|
|
147
|
-
)
|
|
148
|
-
duty = _validate_duty(raw.get("duty", ""), role, prefix, errors)
|
|
149
|
-
dynamic = _validate_dynamic(raw.get("dynamic", "false"), prefix, errors)
|
|
150
|
-
if not (min_count <= recommended_count <= max_count):
|
|
131
|
+
if not (counts[0] <= counts[1] <= counts[2]):
|
|
151
132
|
errors.append(f"{prefix} min must be <= recommended <= max")
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
133
|
+
return StaticRoleRequirement(role, counts[0], counts[1], counts[2], duty)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _dynamic_requirement(
|
|
137
|
+
raw: dict[str, object], prefix: str, errors: list[str]
|
|
138
|
+
) -> DynamicRoleRequirement:
|
|
139
|
+
role = _validate_role(raw.get("roleId"), prefix, errors)
|
|
140
|
+
duty = _validate_duty(raw.get("dutyId"), role, prefix, errors)
|
|
141
|
+
sources = raw.get("sourceRoleIds")
|
|
142
|
+
if (
|
|
143
|
+
not isinstance(sources, list)
|
|
144
|
+
or not sources
|
|
145
|
+
or not all(isinstance(item, str) and item for item in sources)
|
|
146
|
+
):
|
|
147
|
+
errors.append(f"{prefix} sourceRoleIds must be a non-empty list of role ids")
|
|
148
|
+
sources = []
|
|
149
|
+
activation = raw.get("activation")
|
|
150
|
+
if activation != "per-selected-source":
|
|
151
|
+
errors.append(f"{prefix} activation must be per-selected-source")
|
|
152
|
+
activation = "per-selected-source"
|
|
153
|
+
for key in ("min", "recommended", "max"):
|
|
154
|
+
if key in raw:
|
|
155
|
+
errors.append(f"{prefix} dynamic roles declare no {key}")
|
|
156
|
+
return DynamicRoleRequirement(role, duty, tuple(sources), str(activation))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_role(raw: object, prefix: str, errors: list[str]) -> str:
|
|
160
|
+
if not isinstance(raw, str) or not raw:
|
|
161
|
+
errors.append(f"{prefix} roleId must be a role id")
|
|
162
|
+
return ""
|
|
162
163
|
try:
|
|
163
164
|
return normalize_role(raw)
|
|
164
165
|
except RoleCatalogError as exc:
|
|
@@ -166,7 +167,10 @@ def _validate_role(raw: str, prefix: str, errors: list[str]) -> str:
|
|
|
166
167
|
return raw
|
|
167
168
|
|
|
168
169
|
|
|
169
|
-
def _validate_duty(raw:
|
|
170
|
+
def _validate_duty(raw: object, role: str, prefix: str, errors: list[str]) -> str:
|
|
171
|
+
if not isinstance(raw, str) or not raw:
|
|
172
|
+
errors.append(f"{prefix} dutyId must be a duty id")
|
|
173
|
+
return ""
|
|
170
174
|
try:
|
|
171
175
|
duty_role = role_for_duty(raw)
|
|
172
176
|
except RoleCatalogError as exc:
|
|
@@ -177,25 +181,11 @@ def _validate_duty(raw: str, role: str, prefix: str, errors: list[str]) -> str:
|
|
|
177
181
|
return raw
|
|
178
182
|
|
|
179
183
|
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
except ValueError:
|
|
184
|
-
errors.append(f"{prefix} {field} must be a non-negative integer")
|
|
185
|
-
return 0
|
|
186
|
-
if value < 0 or str(value) != raw:
|
|
187
|
-
errors.append(f"{prefix} {field} must be a non-negative integer")
|
|
184
|
+
def _validate_count(raw: object, field_name: str, prefix: str, errors: list[str]) -> int:
|
|
185
|
+
if not isinstance(raw, int) or isinstance(raw, bool) or raw < 0:
|
|
186
|
+
errors.append(f"{prefix} {field_name} must be a non-negative integer")
|
|
188
187
|
return 0
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _validate_dynamic(raw: str, prefix: str, errors: list[str]) -> bool:
|
|
193
|
-
if raw == "true":
|
|
194
|
-
return True
|
|
195
|
-
if raw == "false":
|
|
196
|
-
return False
|
|
197
|
-
errors.append(f"{prefix} dynamic must be true or false")
|
|
198
|
-
return False
|
|
188
|
+
return raw
|
|
199
189
|
|
|
200
190
|
|
|
201
191
|
def _validate_static_role_duplicates(
|
|
@@ -208,3 +198,26 @@ def _validate_static_role_duplicates(
|
|
|
208
198
|
if requirement.role in static_roles:
|
|
209
199
|
errors.append(f"duplicate static role: {requirement.role}")
|
|
210
200
|
static_roles.add(requirement.role)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _validate_dynamic_sources(
|
|
204
|
+
requirements: tuple[RoleRequirement, ...], errors: list[str],
|
|
205
|
+
) -> None:
|
|
206
|
+
"""A dynamic slot activates per source slot, so its sources must be declared.
|
|
207
|
+
|
|
208
|
+
A source the profile never declares can never activate, which makes the row
|
|
209
|
+
unreachable rather than invalid at dispatch time — the failure would surface
|
|
210
|
+
as a round that silently opens no verifier.
|
|
211
|
+
"""
|
|
212
|
+
static_roles = {
|
|
213
|
+
requirement.role for requirement in requirements if not requirement.dynamic
|
|
214
|
+
}
|
|
215
|
+
for requirement in requirements:
|
|
216
|
+
if not requirement.dynamic:
|
|
217
|
+
continue
|
|
218
|
+
unknown = sorted(set(requirement.source_role_ids) - static_roles)
|
|
219
|
+
if unknown:
|
|
220
|
+
errors.append(
|
|
221
|
+
f"dynamic role {requirement.role} has unknown sourceRoleIds: "
|
|
222
|
+
+ ", ".join(unknown)
|
|
223
|
+
)
|
|
@@ -142,9 +142,11 @@ def build_rollup(project_root: Path, task_group: str | None) -> dict:
|
|
|
142
142
|
_CLI_EPILOG = r"""Usage:
|
|
143
143
|
okstra rollup [--task-group <group>] [--project-root <dir>] [--cwd <dir>]
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
Prints a JSON roll-up of every catalog task (optionally scoped to one
|
|
146
146
|
task-group): per-task run count, elapsed time (raw ms), error count, and the
|
|
147
147
|
latest report path, plus group-level totals and status/category/phase tallies.
|
|
148
|
+
Resolving a task may record a finished implementation outcome in its task
|
|
149
|
+
manifest, the task catalog, and consumers.jsonl; nothing else is written.
|
|
148
150
|
With no --task-group it covers the whole project catalog. Time is raw ms; the
|
|
149
151
|
caller formats HH:MM:SS and synthesizes the prose digest from the report files.
|
|
150
152
|
"""
|
|
@@ -1827,10 +1827,9 @@ def _materialize_release_handoff_input(
|
|
|
1827
1827
|
brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 이 run 의
|
|
1828
1828
|
`--task-brief` 자리는 여기서 생성된 input 문서가 채운다. stage 자격은
|
|
1829
1829
|
okstra_ctl.handoff 의 SSOT 판정을 prepare 에서도 그대로 강제한다.
|
|
1830
|
-
반환: ctx 에 올릴 {"
|
|
1830
|
+
반환: ctx 에 올릴 {"HANDOFF_STAGES": ...}."""
|
|
1831
1831
|
from .consumers import read_consumers
|
|
1832
|
-
from .handoff import
|
|
1833
|
-
latest_whole_task_fv_release_ready)
|
|
1832
|
+
from .handoff import HandoffError, _require_eligible, compute_eligibility
|
|
1834
1833
|
from .paths import task_dir
|
|
1835
1834
|
from .render import render_template_with_ctx
|
|
1836
1835
|
from .run_context import _now_task_date
|
|
@@ -1840,8 +1839,8 @@ def _materialize_release_handoff_input(
|
|
|
1840
1839
|
raise PrepareError(
|
|
1841
1840
|
"--task-brief is not accepted for --task-type release-handoff — "
|
|
1842
1841
|
"the input document is generated from the cited final-verification "
|
|
1843
|
-
"reports (pick stages via --stages, or leave it empty
|
|
1844
|
-
"
|
|
1842
|
+
"reports (pick stages via --stages, or leave it empty to take "
|
|
1843
|
+
"every eligible stage)"
|
|
1845
1844
|
)
|
|
1846
1845
|
if not inp.approved_plan_path:
|
|
1847
1846
|
raise PrepareError(
|
|
@@ -1868,21 +1867,20 @@ def _materialize_release_handoff_input(
|
|
|
1868
1867
|
_require_eligible(stage_map, rows, nums)
|
|
1869
1868
|
except HandoffError as exc:
|
|
1870
1869
|
raise PrepareError(str(exc)) from exc
|
|
1871
|
-
mode = "stage-group"
|
|
1872
|
-
stages_csv = ",".join(str(n) for n in nums)
|
|
1873
|
-
report_rows = _collect_handoff_source_report_rows(rows, nums)
|
|
1874
1870
|
else:
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
if not
|
|
1871
|
+
eligibility = compute_eligibility(stage_map, rows)
|
|
1872
|
+
nums = [e["stage"] for e in eligibility if e["eligible"]]
|
|
1873
|
+
if not nums:
|
|
1874
|
+
blocked = "; ".join(
|
|
1875
|
+
f"stage {e['stage']}: {', '.join(e['reasons'])}"
|
|
1876
|
+
for e in eligibility) or "(no stage in the Stage Map)"
|
|
1878
1877
|
raise PrepareError(
|
|
1879
|
-
"
|
|
1880
|
-
"
|
|
1881
|
-
"
|
|
1878
|
+
"release-handoff has no eligible stage — every stage must "
|
|
1879
|
+
"carry a release-ready single-stage final-verification of its "
|
|
1880
|
+
f"recorded implementation commit. {blocked}"
|
|
1882
1881
|
)
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
report_rows = [("(all)", Path(report))]
|
|
1882
|
+
stages_csv = ",".join(str(n) for n in nums)
|
|
1883
|
+
report_rows = _collect_handoff_source_report_rows(rows, nums)
|
|
1886
1884
|
|
|
1887
1885
|
source_rows, source_conditions = _render_handoff_sources(report_rows, project_root)
|
|
1888
1886
|
|
|
@@ -1902,13 +1900,12 @@ def _materialize_release_handoff_input(
|
|
|
1902
1900
|
"PROJECT_ID": inp.project_id,
|
|
1903
1901
|
"TASK_TYPE": inp.task_type,
|
|
1904
1902
|
"TASK_DATE": _now_task_date(),
|
|
1905
|
-
"
|
|
1906
|
-
"HANDOFF_STAGES": stages_csv or "(all)",
|
|
1903
|
+
"HANDOFF_STAGES": stages_csv,
|
|
1907
1904
|
"HANDOFF_SOURCE_REPORTS": source_rows,
|
|
1908
1905
|
"HANDOFF_CONDITIONS": source_conditions,
|
|
1909
1906
|
})
|
|
1910
1907
|
inp.brief_path = out
|
|
1911
|
-
return {"
|
|
1908
|
+
return {"HANDOFF_STAGES": stages_csv}
|
|
1912
1909
|
|
|
1913
1910
|
|
|
1914
1911
|
QA_COMMAND_EXECUTING_TASK_TYPES = ("implementation", "final-verification")
|
|
@@ -2768,7 +2765,7 @@ def recommended_role_models(
|
|
|
2768
2765
|
codex 이고 다른 provider 요청은 거부된다(`resolve_lead_provider`). 그래서
|
|
2769
2766
|
`lead_provider` 를 받아 그 provider 의 기본값으로 해소한다. 받지 않으면
|
|
2770
2767
|
role 별 레거시 기본값(claude 계열)으로 떨어지는데, 그 값을 codex 호스트
|
|
2771
|
-
화면에 그대로 쓰면 안내는 `opus` 인데 prepare 는 `gpt-
|
|
2768
|
+
화면에 그대로 쓰면 안내는 `opus` 인데 prepare 는 `gpt-6-sol` 을 배정한다.
|
|
2772
2769
|
"""
|
|
2773
2770
|
lead_provider_id = lead_provider or "claude"
|
|
2774
2771
|
report_writer_provider_id = report_writer_provider or "claude"
|
|
@@ -4250,6 +4247,9 @@ def _write_bundle_artifacts(
|
|
|
4250
4247
|
ctx["FIX_CYCLE_ID"] = _record_fix_cycle_events(inp, ctx)
|
|
4251
4248
|
|
|
4252
4249
|
|
|
4250
|
+
CONTRACT_FORMAT_VERSION = 2
|
|
4251
|
+
|
|
4252
|
+
|
|
4253
4253
|
def _prepare_agent_contract(
|
|
4254
4254
|
inp: PrepareInputs,
|
|
4255
4255
|
ctx: dict,
|
|
@@ -4258,11 +4258,14 @@ def _prepare_agent_contract(
|
|
|
4258
4258
|
) -> None:
|
|
4259
4259
|
source = _duty_catalog_source(workspace_root)
|
|
4260
4260
|
destination = Path(ctx["DUTY_CONTRACT_ROOT"])
|
|
4261
|
-
_snapshot_duty_catalog(source, destination)
|
|
4261
|
+
_snapshot_duty_catalog(source, destination, _common_contract_source(workspace_root))
|
|
4262
4262
|
digest = digest_duty_catalog(destination)
|
|
4263
4263
|
allowed_audiences = _allowed_agent_audiences(inp, models)
|
|
4264
4264
|
contract = {
|
|
4265
4265
|
"schemaVersion": 1,
|
|
4266
|
+
# 이 run 이 동결한 계약의 형식. 2 = JSON 계약(공통·역할·직무). 1 이거나
|
|
4267
|
+
# 없는 run 은 Markdown 직무를 동결했고, 그 파서는 더 이상 없다.
|
|
4268
|
+
"contractFormatVersion": CONTRACT_FORMAT_VERSION,
|
|
4266
4269
|
"dutyRootPath": ctx["DUTY_CONTRACT_ROOT_RELATIVE_PATH"],
|
|
4267
4270
|
"catalogDigest": digest,
|
|
4268
4271
|
"invocationReservationRootPath": (
|
|
@@ -4295,14 +4298,35 @@ def _duty_catalog_source(workspace_root: Path) -> Path:
|
|
|
4295
4298
|
okstra_home() / "prompts" / "duties",
|
|
4296
4299
|
)
|
|
4297
4300
|
for candidate in candidates:
|
|
4298
|
-
if (candidate / "
|
|
4301
|
+
if (candidate / "lead.json").is_file():
|
|
4299
4302
|
return candidate
|
|
4300
4303
|
raise PrepareError("agent duty catalog is missing from the runtime")
|
|
4301
4304
|
|
|
4302
4305
|
|
|
4303
|
-
def
|
|
4306
|
+
def _common_contract_source(workspace_root: Path) -> Path:
|
|
4307
|
+
"""공통 계약의 정본 위치. 직무가 아니므로 `agents/` 가 소유한다(ADR-0017)."""
|
|
4308
|
+
for candidate in (
|
|
4309
|
+
workspace_root / "agents" / "common.json",
|
|
4310
|
+
okstra_home() / "agents" / "common.json",
|
|
4311
|
+
):
|
|
4312
|
+
if candidate.is_file():
|
|
4313
|
+
return candidate
|
|
4314
|
+
raise PrepareError("agent common contract is missing from the runtime")
|
|
4315
|
+
|
|
4316
|
+
|
|
4317
|
+
def _snapshot_duty_catalog(
|
|
4318
|
+
source: Path, destination: Path, common_source: Path,
|
|
4319
|
+
) -> None:
|
|
4320
|
+
"""이 run 이 쓸 계약을 한 폴더로 동결한다.
|
|
4321
|
+
|
|
4322
|
+
공통 계약은 `agents/` 가 소유하지만 동결본은 한 뿌리여야 한다 — 두 곳에서
|
|
4323
|
+
읽으면 run 이 시작된 뒤 둘 중 하나만 바뀌어도 그 run 의 다이제스트가
|
|
4324
|
+
자기 입력과 맞지 않게 된다.
|
|
4325
|
+
"""
|
|
4304
4326
|
if destination.exists():
|
|
4305
|
-
if digest_duty_catalog(destination) !=
|
|
4327
|
+
if digest_duty_catalog(destination) != _runtime_catalog_digest(
|
|
4328
|
+
source, common_source
|
|
4329
|
+
):
|
|
4306
4330
|
raise PrepareError("existing run duty snapshot conflicts with runtime catalog")
|
|
4307
4331
|
return
|
|
4308
4332
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -4313,15 +4337,38 @@ def _snapshot_duty_catalog(source: Path, destination: Path) -> None:
|
|
|
4313
4337
|
))
|
|
4314
4338
|
try:
|
|
4315
4339
|
shutil.copytree(source, temp_dir, dirs_exist_ok=True)
|
|
4340
|
+
shutil.copyfile(common_source, temp_dir / "common.json")
|
|
4341
|
+
shutil.copytree(
|
|
4342
|
+
common_source.parent / "roles", temp_dir / "roles", dirs_exist_ok=True
|
|
4343
|
+
)
|
|
4316
4344
|
os.rename(temp_dir, destination)
|
|
4317
4345
|
except FileExistsError as exc:
|
|
4318
|
-
if digest_duty_catalog(destination) !=
|
|
4346
|
+
if digest_duty_catalog(destination) != _runtime_catalog_digest(
|
|
4347
|
+
source, common_source
|
|
4348
|
+
):
|
|
4319
4349
|
raise PrepareError("run duty snapshot publication conflict") from exc
|
|
4320
4350
|
finally:
|
|
4321
4351
|
if temp_dir.exists():
|
|
4322
4352
|
shutil.rmtree(temp_dir)
|
|
4323
4353
|
|
|
4324
4354
|
|
|
4355
|
+
def _runtime_catalog_digest(source: Path, common_source: Path) -> str:
|
|
4356
|
+
"""동결본과 비교할 런타임 카탈로그의 다이제스트.
|
|
4357
|
+
|
|
4358
|
+
동결본은 `common.json` 을 포함하므로 런타임 쪽도 같은 구성으로 세어야 한다.
|
|
4359
|
+
"""
|
|
4360
|
+
staged = Path(tempfile.mkdtemp(prefix=".okstra-catalog-digest."))
|
|
4361
|
+
try:
|
|
4362
|
+
shutil.copytree(source, staged, dirs_exist_ok=True)
|
|
4363
|
+
shutil.copyfile(common_source, staged / "common.json")
|
|
4364
|
+
shutil.copytree(
|
|
4365
|
+
common_source.parent / "roles", staged / "roles", dirs_exist_ok=True
|
|
4366
|
+
)
|
|
4367
|
+
return digest_duty_catalog(staged)
|
|
4368
|
+
finally:
|
|
4369
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
4370
|
+
|
|
4371
|
+
|
|
4325
4372
|
def _allowed_agent_audiences(
|
|
4326
4373
|
inp: PrepareInputs,
|
|
4327
4374
|
models: _ModelBindings,
|
|
@@ -4792,7 +4839,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4792
4839
|
|
|
4793
4840
|
# release-handoff: 검증 보고서 인용 input 문서를 생성해 brief 자리에 채운다.
|
|
4794
4841
|
# 이후의 모든 brief 소비 경로(material/instruction-set 복사)는 그대로 동작한다.
|
|
4795
|
-
handoff_tokens = {"
|
|
4842
|
+
handoff_tokens = {"HANDOFF_STAGES": ""}
|
|
4796
4843
|
if inp.task_type == "release-handoff":
|
|
4797
4844
|
handoff_tokens = _materialize_release_handoff_input(
|
|
4798
4845
|
workspace_root, project_root, inp)
|
|
@@ -5250,8 +5297,8 @@ def build_prepare_argument_parser():
|
|
|
5250
5297
|
p.add_argument(
|
|
5251
5298
|
"--stages", default="", dest="stages",
|
|
5252
5299
|
help=(
|
|
5253
|
-
"release-handoff only. Comma-separated stage numbers to
|
|
5254
|
-
"
|
|
5300
|
+
"release-handoff only. Comma-separated stage numbers to open a "
|
|
5301
|
+
"PR for, one PR per stage; empty = every eligible stage. "
|
|
5255
5302
|
"Distinct from --stage (implementation/final-verification "
|
|
5256
5303
|
"Stage Map selection)."
|
|
5257
5304
|
),
|
|
@@ -33,9 +33,20 @@ _WORK_BREAKDOWN_SEPARATOR = ["---:", "---", "---:", "---", "---:"]
|
|
|
33
33
|
# No `Task ID` and no `taskType`: the schedule is shared with people who do not
|
|
34
34
|
# run okstra, so a task is named by its work and a row binds to the selection by
|
|
35
35
|
# its `#` position — the same rule the `### <n>.` blocks follow.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
# 두 번째 열은 작업 이름 열이고 그 제목은 문서 언어를 따른다(`작업` / `Travail`).
|
|
37
|
+
# 언어별 제목은 형식 게이트(`validate-schedule.py` 의 `glance_work_column`)가
|
|
38
|
+
# 검사한다. 여기서 한국어로 고정하면 번역본이 이 게이트를 통과할 수 없다.
|
|
39
|
+
_AT_A_GLANCE_FIXED_COLUMNS = ["Category", "Priority", "Effort", "Days", "Risk"]
|
|
40
|
+
_AT_A_GLANCE_WIDTH = 2 + len(_AT_A_GLANCE_FIXED_COLUMNS)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_at_a_glance_header(cells: list[str]) -> bool:
|
|
44
|
+
return (
|
|
45
|
+
len(cells) == _AT_A_GLANCE_WIDTH
|
|
46
|
+
and cells[0] == "#"
|
|
47
|
+
and bool(cells[1])
|
|
48
|
+
and cells[2:] == _AT_A_GLANCE_FIXED_COLUMNS
|
|
49
|
+
)
|
|
39
50
|
_EFFORT_HEADER = ["Size", "Criteria", "Day(s)"]
|
|
40
51
|
_EFFORT_SIZES = {"S", "M", "L", "XL", "XXL"}
|
|
41
52
|
_DAY_NUMBER = r"\d+(?:\.\d+)?"
|
|
@@ -594,9 +605,9 @@ def _parse_at_a_glance_table(
|
|
|
594
605
|
if not line.strip().startswith("|"):
|
|
595
606
|
break
|
|
596
607
|
cells = split_pipe_row(line)
|
|
597
|
-
if cells
|
|
608
|
+
if _is_at_a_glance_header(cells):
|
|
598
609
|
break
|
|
599
|
-
if len(cells) !=
|
|
610
|
+
if len(cells) != _AT_A_GLANCE_WIDTH:
|
|
600
611
|
task_id = cells[1] if len(cells) > 1 else "<unknown>"
|
|
601
612
|
violations.append(
|
|
602
613
|
f"At a Glance: noncanonical row for task {task_id} "
|
|
@@ -621,7 +632,7 @@ def _parse_at_a_glance_rows(
|
|
|
621
632
|
lines = section.splitlines()
|
|
622
633
|
header_indexes = [
|
|
623
634
|
index for index, line in enumerate(lines)
|
|
624
|
-
if split_pipe_row(line)
|
|
635
|
+
if _is_at_a_glance_header(split_pipe_row(line))
|
|
625
636
|
]
|
|
626
637
|
if not header_indexes:
|
|
627
638
|
return [], ["At a Glance: missing canonical 7-column table"]
|
|
@@ -82,14 +82,33 @@ def derive_stage_fix_carry(
|
|
|
82
82
|
prev_report_seq=_REPORT_RE.search(report.name).group(1),
|
|
83
83
|
prev_head_commit=head,
|
|
84
84
|
failed_verifiers=[str(row.get("verifier", "")) for row in failed],
|
|
85
|
-
blocking_findings=
|
|
86
|
-
str(row.get("discrepancy") or row.get("verifier") or "")
|
|
87
|
-
for row in failed
|
|
88
|
-
],
|
|
85
|
+
blocking_findings=_blocking_findings(data, failed),
|
|
89
86
|
routing_recommendation=_routing_line(impl.get("routingRecommendation")),
|
|
90
87
|
)
|
|
91
88
|
|
|
92
89
|
|
|
90
|
+
def _blocking_findings(data: dict, failed: list[dict]) -> list[str]:
|
|
91
|
+
"""이전 run 을 FAIL 로 만든 finding 목록.
|
|
92
|
+
|
|
93
|
+
정본은 리포트의 `humanSummary.blockers` 다 — 스키마가 요구하는 배열이고,
|
|
94
|
+
FAIL 사유가 정적 검토 finding(F-NNN)이든 Discrepancy 규칙이든 그 문장이 여기
|
|
95
|
+
있다. 검증자 행의 `discrepancy` 는 재실행 결과 차이를 적는 칸이라 FAIL 사유가
|
|
96
|
+
아닐 수 있다. 실측(2026-09-22, jobs dev-10860 stage 1): Codex 의 FAIL 사유는
|
|
97
|
+
F-001~003 이었는데 캐리는 그 행의 VC-001 종료 코드 메모를 실었다.
|
|
98
|
+
blockers 가 비어 있는 옛 리포트만 검증자 행으로 대신한다.
|
|
99
|
+
"""
|
|
100
|
+
summary = data.get("humanSummary")
|
|
101
|
+
blockers = summary.get("blockers") if isinstance(summary, dict) else None
|
|
102
|
+
if isinstance(blockers, list):
|
|
103
|
+
rows = [row.strip() for row in blockers if isinstance(row, str) and row.strip()]
|
|
104
|
+
if rows:
|
|
105
|
+
return rows
|
|
106
|
+
return [
|
|
107
|
+
str(row.get("discrepancy") or row.get("verifier") or "")
|
|
108
|
+
for row in failed
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
93
112
|
def _routing_line(value: object) -> str:
|
|
94
113
|
"""구조체 라우팅을 캐리 프롬프트에 실을 한 줄로 편다.
|
|
95
114
|
|