okstra 0.180.0 → 0.183.2
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/dist/cli-registry.mjs +16 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture.md +11 -0
- package/docs/cli.md +26 -13
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +29 -26
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +3 -3
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +3 -7
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +8 -6
- package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
- package/runtime/prompts/lead/plan-body-verification.md +187 -14
- package/runtime/prompts/lead/report-writer.md +20 -1
- package/runtime/prompts/lead/team-contract.md +8 -53
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -2
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +118 -35
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
- package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
- package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/paths.py +9 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +26 -15
- package/runtime/python/okstra_ctl/render_final_report.py +3 -2
- package/runtime/python/okstra_ctl/report_assembly.py +172 -17
- package/runtime/python/okstra_ctl/report_finalize.py +7 -10
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +13 -1
- package/runtime/python/okstra_ctl/report_narrative.py +40 -8
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +159 -56
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
- package/runtime/python/okstra_ctl/stage_map.py +26 -6
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +2 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +9 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +17 -1
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +379 -63
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -32,9 +32,9 @@ SHIM_DIR_MARKER = "cmux-cli-shims"
|
|
|
32
32
|
|
|
33
33
|
# The workspace is split in fifths: two for the lead, three for the workers.
|
|
34
34
|
# Every worker lands in that one column and the column divides downward, so all
|
|
35
|
-
# workers hold the same width
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
# workers hold the same width. Splitting sideways instead would make each
|
|
36
|
+
# worker's width a function of how many rounds preceded it. The split itself
|
|
37
|
+
# halves the pane it lands on; worker heights are evened out afterwards.
|
|
38
38
|
LEAD_SHARE_WITH_WORKERS = 2 / 5
|
|
39
39
|
|
|
40
40
|
# Three fifths is what a worker needs to be worth watching: measured against a
|
|
@@ -46,10 +46,9 @@ LEAD_SHARE_WITH_WORKERS = 2 / 5
|
|
|
46
46
|
# whole workspace back rather than sitting at its working width.
|
|
47
47
|
LEAD_SHARE_ALONE = 1.0
|
|
48
48
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
WORKER_MIN_ROWS = 20
|
|
49
|
+
# Two passes per pane above the bottom. A roster of four analysers is four
|
|
50
|
+
# workers in the column; eight moves is that bound with one extra pass.
|
|
51
|
+
MAX_WORKER_HEIGHT_MOVES = 8
|
|
53
52
|
|
|
54
53
|
# Sidebar entries are keyed by source so tools do not overwrite each other's.
|
|
55
54
|
SIDEBAR_SOURCE = "okstra"
|
|
@@ -163,15 +162,29 @@ class PaneGeometry:
|
|
|
163
162
|
width_points: float = 0.0
|
|
164
163
|
ref: str = ""
|
|
165
164
|
selected_surface_id: str = ""
|
|
165
|
+
height_points: float = 0.0
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
@dataclass(frozen=True)
|
|
169
169
|
class Placement:
|
|
170
|
-
"""Where the next worker goes: split `pane_id
|
|
170
|
+
"""Where the next worker goes: split `pane_id` in `direction`."""
|
|
171
171
|
|
|
172
172
|
pane_id: str
|
|
173
173
|
direction: str
|
|
174
|
-
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class WorkerHeightResize:
|
|
178
|
+
"""One vertical border move that equalizes the worker column.
|
|
179
|
+
|
|
180
|
+
`down` on a pane grows it into the neighbour below. `up` on a pane pulls
|
|
181
|
+
the shared border up, shrinking the neighbour above — the top pane has no
|
|
182
|
+
upper border, so shrinking it is the pane below's request.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
pane_id: str
|
|
186
|
+
direction: str
|
|
187
|
+
amount: int
|
|
175
188
|
|
|
176
189
|
|
|
177
190
|
def plan_worker_placement(
|
|
@@ -179,15 +192,14 @@ def plan_worker_placement(
|
|
|
179
192
|
*,
|
|
180
193
|
lead_pane_id: str,
|
|
181
194
|
owned_surface_ids: Collection[str],
|
|
182
|
-
min_rows: int,
|
|
183
195
|
) -> Placement:
|
|
184
196
|
"""Pick the next worker slot from the workspace's current geometry.
|
|
185
197
|
|
|
186
198
|
One column for every worker: the first split takes it off the lead, and each
|
|
187
|
-
one after that divides the
|
|
188
|
-
by where the lead's border sits, and every worker
|
|
189
|
-
computes a width, and no worker's width depends
|
|
190
|
-
before it.
|
|
199
|
+
one after that divides the worker next to the lead downward. Width is
|
|
200
|
+
therefore decided once, by where the lead's border sits, and every worker
|
|
201
|
+
inherits it — nothing here computes a width, and no worker's width depends
|
|
202
|
+
on how many rounds ran before it.
|
|
191
203
|
|
|
192
204
|
Stateless by design: okstra records surface UUIDs, never a layout, so a
|
|
193
205
|
resumed or crashed run cannot carry a layout model that no longer matches
|
|
@@ -207,8 +219,8 @@ def plan_worker_placement(
|
|
|
207
219
|
and _holds_an_okstra_surface(pane, owned_surface_ids)
|
|
208
220
|
]
|
|
209
221
|
if not workers:
|
|
210
|
-
return Placement(pane_id=lead_pane_id, direction="right"
|
|
211
|
-
return _extend_the_worker_column(workers
|
|
222
|
+
return Placement(pane_id=lead_pane_id, direction="right")
|
|
223
|
+
return _extend_the_worker_column(workers)
|
|
212
224
|
|
|
213
225
|
|
|
214
226
|
def lead_target_width(
|
|
@@ -273,28 +285,52 @@ def _holds_an_okstra_surface(
|
|
|
273
285
|
) -> bool:
|
|
274
286
|
"""Whether okstra opened anything in this pane.
|
|
275
287
|
|
|
276
|
-
Any one recorded surface is enough:
|
|
277
|
-
|
|
288
|
+
Any one recorded surface is enough: a pane may hold more than one surface,
|
|
289
|
+
and only one of those UUIDs has to be in the ledger.
|
|
278
290
|
"""
|
|
279
291
|
return any(surface_id in owned_surface_ids for surface_id in pane.surface_ids)
|
|
280
292
|
|
|
281
293
|
|
|
282
|
-
def _extend_the_worker_column(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
294
|
+
def _extend_the_worker_column(workers: Sequence[PaneGeometry]) -> Placement:
|
|
295
|
+
"""Divide the worker next to the lead downward. A tab would hide the new worker.
|
|
296
|
+
|
|
297
|
+
The first worker is split off the lead, so it stays at the lead's y. Later
|
|
298
|
+
workers stack below it. Splitting the bottom one parks each new pane at the
|
|
299
|
+
far end of the column; splitting the neighbour keeps the new pane next to
|
|
300
|
+
the lead.
|
|
301
|
+
"""
|
|
302
|
+
neighbour = min(workers, key=lambda pane: pane.y)
|
|
303
|
+
return Placement(pane_id=neighbour.pane_id, direction="down")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def plan_worker_height_resize(
|
|
307
|
+
workers: Sequence[PaneGeometry],
|
|
308
|
+
) -> WorkerHeightResize | None:
|
|
309
|
+
"""The next vertical move that gives every worker an equal share of the column.
|
|
310
|
+
|
|
311
|
+
Only the first off-share pane above the bottom is reported. Growing or
|
|
312
|
+
shrinking it changes every pane below, so the caller re-reads and asks
|
|
313
|
+
again. The bottom pane is never the one named: it absorbs the remainder.
|
|
286
314
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
worse than a visible pane, but better than two panes too short to show a
|
|
291
|
-
command and its first lines of output.
|
|
315
|
+
`new-split` halves the pane it lands on and does not take a ratio, so a
|
|
316
|
+
third worker split off the top would otherwise stay at a quarter while
|
|
317
|
+
the bottom one keeps a half.
|
|
292
318
|
"""
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
if len(workers) < 2:
|
|
320
|
+
return None
|
|
321
|
+
ordered = sorted(workers, key=lambda pane: pane.y)
|
|
322
|
+
total = sum(pane.height_points for pane in ordered)
|
|
323
|
+
if total <= 0:
|
|
324
|
+
return None
|
|
325
|
+
share = total / len(ordered)
|
|
326
|
+
for index, pane in enumerate(ordered[:-1]):
|
|
327
|
+
delta = round(pane.height_points - share)
|
|
328
|
+
if delta == 0:
|
|
329
|
+
continue
|
|
330
|
+
if delta < 0:
|
|
331
|
+
return WorkerHeightResize(pane.pane_id, "down", -delta)
|
|
332
|
+
return WorkerHeightResize(ordered[index + 1].pane_id, "up", delta)
|
|
333
|
+
return None
|
|
298
334
|
|
|
299
335
|
|
|
300
336
|
def shim_free_login_path() -> str:
|
|
@@ -331,6 +367,17 @@ def worker_command_line(
|
|
|
331
367
|
The `cd` is part of the command because neither `new-split` nor
|
|
332
368
|
`respawn-pane` accepts a working directory the way `tmux split-window -c`
|
|
333
369
|
does.
|
|
370
|
+
|
|
371
|
+
`exec` is here for termination, not for the pane's life, and removing it
|
|
372
|
+
does not keep a finished worker's pane on screen. Measured 2026-08-21: a
|
|
373
|
+
surface's life is bound to the command `respawn-pane` starts — cmux runs it
|
|
374
|
+
through `login … bash --noprofile --norc` and closes the surface when it
|
|
375
|
+
exits — and cmux's settings schema carries no option that keeps a surface
|
|
376
|
+
open past its process. Dropping `exec` was tried against a live dispatch and
|
|
377
|
+
the pane still went with the worker; what it changed was that
|
|
378
|
+
`close_surface` would kill an intermediate shell rather than the worker
|
|
379
|
+
itself. So the adapter contract's "finished workers leave their panes
|
|
380
|
+
behind" does not hold under cmux, and no edit to this line makes it hold.
|
|
334
381
|
"""
|
|
335
382
|
return (
|
|
336
383
|
f"cd {shlex.quote(str(cwd))} && "
|
|
@@ -366,13 +413,14 @@ def spawn_worker_surface(
|
|
|
366
413
|
panes,
|
|
367
414
|
lead_pane_id=lead.pane_id,
|
|
368
415
|
owned_surface_ids=owned_surface_ids,
|
|
369
|
-
min_rows=WORKER_MIN_ROWS,
|
|
370
416
|
)
|
|
371
417
|
target = _pane_by_id(panes, placement.pane_id)
|
|
372
418
|
surface_uuid = _open_worker_surface(workspace, placement, target)
|
|
373
419
|
run_cmux(["rename-tab", "--surface", surface_uuid, "--title", title])
|
|
374
420
|
_exec_worker(surface_uuid, cwd=cwd, command=command)
|
|
375
|
-
|
|
421
|
+
owned = (*owned_surface_ids, surface_uuid)
|
|
422
|
+
_size_lead_pane(workspace, owned)
|
|
423
|
+
_equalize_worker_heights(workspace, owned)
|
|
376
424
|
return surface_uuid
|
|
377
425
|
|
|
378
426
|
|
|
@@ -575,21 +623,16 @@ def open_surface_ids(workspace: str) -> set[str]:
|
|
|
575
623
|
def _create_surface(
|
|
576
624
|
workspace: str, placement: Placement, target: PaneGeometry
|
|
577
625
|
) -> None:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
workspace,
|
|
589
|
-
"--surface",
|
|
590
|
-
target.selected_surface_id or target.surface_ids[0],
|
|
591
|
-
]
|
|
592
|
-
)
|
|
626
|
+
created = run_cmux(
|
|
627
|
+
[
|
|
628
|
+
"new-split",
|
|
629
|
+
placement.direction,
|
|
630
|
+
"--workspace",
|
|
631
|
+
workspace,
|
|
632
|
+
"--surface",
|
|
633
|
+
target.selected_surface_id or target.surface_ids[0],
|
|
634
|
+
]
|
|
635
|
+
)
|
|
593
636
|
if created.returncode != 0:
|
|
594
637
|
raise RuntimeError(created.stderr.strip() or "cmux could not open a pane")
|
|
595
638
|
|
|
@@ -606,9 +649,10 @@ def _exec_worker(surface_uuid: str, *, cwd: Path, command: Sequence[str]) -> Non
|
|
|
606
649
|
def _size_lead_pane(workspace: str, owned_surface_ids: Collection[str]) -> None:
|
|
607
650
|
"""Move the lead's border to its share of the workspace.
|
|
608
651
|
|
|
609
|
-
This is the
|
|
610
|
-
and
|
|
611
|
-
|
|
652
|
+
This is the horizontal border okstra places. Workers divide their column
|
|
653
|
+
downward and inherit the remaining width, so this one move sizes the lead
|
|
654
|
+
and every worker's width. Worker heights are equalized separately after
|
|
655
|
+
the split that just halved one of them.
|
|
612
656
|
|
|
613
657
|
Which pane carries the request follows from what `pane.resize` does: it
|
|
614
658
|
moves the named pane's own border in the direction given. The lead can push
|
|
@@ -659,6 +703,39 @@ def _size_lead_pane(workspace: str, owned_surface_ids: Collection[str]) -> None:
|
|
|
659
703
|
)
|
|
660
704
|
|
|
661
705
|
|
|
706
|
+
def _equalize_worker_heights(
|
|
707
|
+
workspace: str, owned_surface_ids: Collection[str]
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Give every owned worker the same height in the column.
|
|
710
|
+
|
|
711
|
+
`new-split` halves whichever pane it lands on, so the third worker of a
|
|
712
|
+
round would otherwise sit at a quarter with the bottom one still at a half.
|
|
713
|
+
Each call moves one border and re-reads: growing the top pane steals from
|
|
714
|
+
the one below, and that is what the next plan has to see.
|
|
715
|
+
"""
|
|
716
|
+
for _ in range(MAX_WORKER_HEIGHT_MOVES):
|
|
717
|
+
panes = list_panes(workspace)
|
|
718
|
+
lead = _lead_pane(panes)
|
|
719
|
+
workers = [
|
|
720
|
+
pane
|
|
721
|
+
for pane in panes
|
|
722
|
+
if pane.pane_id != lead.pane_id
|
|
723
|
+
and _holds_an_okstra_surface(pane, owned_surface_ids)
|
|
724
|
+
]
|
|
725
|
+
move = plan_worker_height_resize(workers)
|
|
726
|
+
if move is None:
|
|
727
|
+
return
|
|
728
|
+
rpc(
|
|
729
|
+
"pane.resize",
|
|
730
|
+
{
|
|
731
|
+
"workspace_id": workspace,
|
|
732
|
+
"pane_id": move.pane_id,
|
|
733
|
+
"direction": move.direction,
|
|
734
|
+
"amount": move.amount,
|
|
735
|
+
},
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
|
|
662
739
|
def _lead_pane(panes: Sequence[PaneGeometry]) -> PaneGeometry:
|
|
663
740
|
caller_pane_ref = identify_caller().get("pane_ref", "")
|
|
664
741
|
for pane in panes:
|
|
@@ -706,6 +783,7 @@ def _pane_geometry(entry: dict[str, Any]) -> PaneGeometry:
|
|
|
706
783
|
width_points=float(frame.get("width") or 0),
|
|
707
784
|
ref=str(entry.get("ref", "")),
|
|
708
785
|
selected_surface_id=str(entry.get("selected_surface_id", "")),
|
|
786
|
+
height_points=float(frame.get("height") or 0),
|
|
709
787
|
)
|
|
710
788
|
|
|
711
789
|
|
|
@@ -24,6 +24,7 @@ from okstra_project import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
from .code_review_paths import branch_review_dir, next_branch_review
|
|
27
|
+
from .fixed_text import line
|
|
27
28
|
|
|
28
29
|
DEFAULT_BRANCH_CANDIDATES = ("main", "master")
|
|
29
30
|
|
|
@@ -105,7 +106,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
105
106
|
parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
|
|
106
107
|
parser.add_argument("--project-root", default="", help="explicit project root")
|
|
107
108
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
108
|
-
|
|
109
|
+
output = parser.add_mutually_exclusive_group()
|
|
110
|
+
output.add_argument("--json", action="store_true", help="emit JSON (default)")
|
|
111
|
+
output.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
109
112
|
return parser
|
|
110
113
|
|
|
111
114
|
|
|
@@ -121,6 +124,25 @@ def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
|
|
|
121
124
|
raise StateError("pass either --task-key --stage or --branch", stage="args")
|
|
122
125
|
|
|
123
126
|
|
|
127
|
+
def render_code_review_target_text(payload: dict) -> str:
|
|
128
|
+
"""모델이 소비할 리뷰 범위와 결과 경로만 고정 줄로 투영한다."""
|
|
129
|
+
rows = ["Okstra code review target\n"]
|
|
130
|
+
rows.append(line("Status", "ready" if payload.get("ok") else "error"))
|
|
131
|
+
fields = (
|
|
132
|
+
("Project root", "projectRoot"), ("Mode", "mode"),
|
|
133
|
+
("Worktree path", "worktreePath"), ("Branch", "branch"),
|
|
134
|
+
("Base commit", "baseCommit"), ("Head commit", "headCommit"),
|
|
135
|
+
("Review path", "reviewPath"), ("Round", "round"),
|
|
136
|
+
("Task key", "taskKey"), ("Task root", "taskRoot"), ("Stage", "stage"),
|
|
137
|
+
("Failure stage", "stage"), ("Failure reason", "reason"),
|
|
138
|
+
)
|
|
139
|
+
for label, key in fields:
|
|
140
|
+
failure_field = label.startswith("Failure")
|
|
141
|
+
if key in payload and failure_field != bool(payload.get("ok")):
|
|
142
|
+
rows.append(line(label, payload.get(key)))
|
|
143
|
+
return "".join(rows)
|
|
144
|
+
|
|
145
|
+
|
|
124
146
|
def main(argv: list[str] | None = None) -> int:
|
|
125
147
|
args = _build_parser().parse_args(argv)
|
|
126
148
|
try:
|
|
@@ -128,23 +150,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
128
150
|
explicit_root=args.project_root, cwd=args.cwd
|
|
129
151
|
)
|
|
130
152
|
except ResolverError as exc:
|
|
131
|
-
|
|
153
|
+
payload = {"ok": False, "stage": "resolve", "reason": str(exc)}
|
|
154
|
+
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
132
155
|
return 2
|
|
133
156
|
|
|
134
157
|
try:
|
|
135
158
|
snapshot = _resolve(args, Path(project_root))
|
|
136
159
|
except StateError as exc:
|
|
137
|
-
|
|
160
|
+
payload = {
|
|
138
161
|
"ok": False,
|
|
139
162
|
"stage": exc.stage or "target",
|
|
140
163
|
"reason": str(exc),
|
|
141
|
-
}
|
|
164
|
+
}
|
|
165
|
+
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
142
166
|
return 1
|
|
143
167
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
payload = {"ok": True, "projectRoot": str(project_root), **snapshot}
|
|
169
|
+
print(
|
|
170
|
+
render_code_review_target_text(payload)
|
|
171
|
+
if args.text
|
|
172
|
+
else json.dumps(payload, ensure_ascii=False, indent=2)
|
|
173
|
+
)
|
|
148
174
|
return 0
|
|
149
175
|
|
|
150
176
|
|
|
@@ -16,6 +16,8 @@ from dataclasses import dataclass
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Dict, List, Optional
|
|
18
18
|
|
|
19
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
20
|
+
from .jsonl import append_jsonl
|
|
19
21
|
from .run_context import consumers_mutex
|
|
20
22
|
|
|
21
23
|
CONSUMERS_FILENAME = "consumers.jsonl"
|
|
@@ -265,8 +267,7 @@ def _release_stage_occupancy_keeping_branch(
|
|
|
265
267
|
|
|
266
268
|
|
|
267
269
|
def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
|
|
268
|
-
|
|
269
|
-
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
270
|
+
append_jsonl(_path(plan_run_root), record, ensure_ascii=False, compact=False)
|
|
270
271
|
|
|
271
272
|
|
|
272
273
|
def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
@@ -414,8 +415,8 @@ def backfill_done_from_carry(plan_run_root: Path) -> int:
|
|
|
414
415
|
recovered = 0
|
|
415
416
|
for cf in sorted(carry_dir.glob("stage-*.json")):
|
|
416
417
|
try:
|
|
417
|
-
carry =
|
|
418
|
-
except (
|
|
418
|
+
carry = load_owned_object(cf, artifact="implementation stage carry")
|
|
419
|
+
except (JsonBoundaryError, OSError):
|
|
419
420
|
continue
|
|
420
421
|
if not isinstance(carry, dict):
|
|
421
422
|
continue
|
|
@@ -28,6 +28,8 @@ from typing import Callable, Iterable
|
|
|
28
28
|
|
|
29
29
|
from .stage_targets import PrepareError # 단일 PrepareError 타입 재노출(run.py 와 공유)
|
|
30
30
|
from .stage_map import StageMapError, parse_stage_map_file, stage_map_records
|
|
31
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
32
|
+
from .fixed_text import line
|
|
31
33
|
|
|
32
34
|
OKSTRA_LABEL_KEYS = ("okstra.task-key", "okstra.project-name", "okstra.run-trace")
|
|
33
35
|
|
|
@@ -426,13 +428,19 @@ def query_containers_by_label(project_name: str) -> list[dict]:
|
|
|
426
428
|
out = _docker_run([
|
|
427
429
|
"docker", "ps", "-a", "--filter",
|
|
428
430
|
f"label=okstra.project-name={project_name}",
|
|
429
|
-
"--format",
|
|
431
|
+
"--format", (
|
|
432
|
+
"{{.ID}}\t{{.Names}}\t{{.State}}\t"
|
|
433
|
+
'{{.Label "com.docker.compose.service"}}\t{{.Ports}}'
|
|
434
|
+
),
|
|
430
435
|
])
|
|
431
436
|
rows: list[dict] = []
|
|
432
437
|
for line in out.splitlines():
|
|
433
438
|
parts = line.split("\t")
|
|
434
|
-
if len(parts) >=
|
|
435
|
-
rows.append({
|
|
439
|
+
if len(parts) >= 5:
|
|
440
|
+
rows.append({
|
|
441
|
+
"id": parts[0], "name": parts[1], "state": parts[2],
|
|
442
|
+
"service": parts[3], "ports": parts[4],
|
|
443
|
+
})
|
|
436
444
|
return rows
|
|
437
445
|
|
|
438
446
|
|
|
@@ -774,8 +782,7 @@ def _spawn_service_panes(
|
|
|
774
782
|
|
|
775
783
|
def _write_deploy_state(cp: dict, **state) -> None:
|
|
776
784
|
cp["container_dir"].mkdir(parents=True, exist_ok=True)
|
|
777
|
-
cp["deploy_state"]
|
|
778
|
-
json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
785
|
+
write_owned_object_atomic(cp["deploy_state"], state, artifact="container deploy state")
|
|
779
786
|
|
|
780
787
|
|
|
781
788
|
# --------------------------------------------------------------------------- #
|
|
@@ -879,8 +886,10 @@ def _read_project_id(project_json: Path) -> str:
|
|
|
879
886
|
if not project_json.is_file():
|
|
880
887
|
return ""
|
|
881
888
|
try:
|
|
882
|
-
return
|
|
883
|
-
|
|
889
|
+
return load_owned_object(project_json, artifact="project configuration").get(
|
|
890
|
+
"projectId", ""
|
|
891
|
+
)
|
|
892
|
+
except JsonBoundaryError:
|
|
884
893
|
return ""
|
|
885
894
|
|
|
886
895
|
|
|
@@ -928,6 +937,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
928
937
|
sp = sub.add_parser(name)
|
|
929
938
|
sp.add_argument("--project-root", required=True)
|
|
930
939
|
sp.add_argument("--task-key", default="")
|
|
940
|
+
sp.add_argument("--text", action="store_true")
|
|
931
941
|
if name == "down":
|
|
932
942
|
sp.add_argument("--all", action="store_true")
|
|
933
943
|
if name == "logs":
|
|
@@ -944,10 +954,95 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
944
954
|
findings_path=args.findings, scan_interval_seconds=args.scan_interval)
|
|
945
955
|
return 0
|
|
946
956
|
result = _dispatch(args)
|
|
947
|
-
print(
|
|
957
|
+
print(
|
|
958
|
+
render_container_text(args.command, result)
|
|
959
|
+
if args.text
|
|
960
|
+
else json.dumps(result, ensure_ascii=False, indent=2)
|
|
961
|
+
)
|
|
948
962
|
return 0
|
|
949
963
|
|
|
950
964
|
|
|
965
|
+
def render_container_text(command: str, payload: dict) -> str:
|
|
966
|
+
"""container 명령별 승인된 상태만 고정 순서로 투영한다."""
|
|
967
|
+
if command not in {"up", "status", "logs", "stop-watcher", "down"}:
|
|
968
|
+
raise ValueError("unknown container text purpose")
|
|
969
|
+
rows = [f"Okstra container {command}\n", line("Status", "ready")]
|
|
970
|
+
for label, key in (("Project name", "projectName"), ("Watchers dir", "watchersDir"),
|
|
971
|
+
("Note", "note")):
|
|
972
|
+
if key in payload:
|
|
973
|
+
rows.append(line(label, payload.get(key)))
|
|
974
|
+
if command == "up":
|
|
975
|
+
rows.extend(_render_container_up(payload))
|
|
976
|
+
elif command in {"status", "logs"}:
|
|
977
|
+
rows.extend(_render_container_watchers(payload))
|
|
978
|
+
if command == "status":
|
|
979
|
+
rows.extend(_render_container_status(payload))
|
|
980
|
+
elif command == "stop-watcher":
|
|
981
|
+
rows.extend(_render_reaped_panes("Reaped pane", payload.get("reapedPanes")))
|
|
982
|
+
else:
|
|
983
|
+
rows.extend(_render_container_down(payload))
|
|
984
|
+
return "".join(rows)
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _render_container_up(payload: dict) -> list[str]:
|
|
988
|
+
rows: list[str] = []
|
|
989
|
+
for index, service in enumerate(payload.get("services") or [], 1):
|
|
990
|
+
rows.append(line(f"Service {index} name", service))
|
|
991
|
+
for index, warning in enumerate(payload.get("warnings") or [], 1):
|
|
992
|
+
rows.append(line(f"Warning {index} text", warning))
|
|
993
|
+
watch = payload.get("watch") if isinstance(payload.get("watch"), dict) else {}
|
|
994
|
+
for label, key in (("Watcher enabled", "enabled"), ("Watcher session", "session"),
|
|
995
|
+
("Watcher note", "note")):
|
|
996
|
+
if key in watch:
|
|
997
|
+
rows.append(line(label, watch.get(key)))
|
|
998
|
+
for index, service in enumerate(watch.get("services") or [], 1):
|
|
999
|
+
rows.append(line(f"Watcher service {index} name", service))
|
|
1000
|
+
return rows
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _render_container_watchers(payload: dict) -> list[str]:
|
|
1004
|
+
watchers = payload.get("watchers") if isinstance(payload.get("watchers"), dict) else {}
|
|
1005
|
+
rows: list[str] = []
|
|
1006
|
+
for index, (service, metadata) in enumerate(sorted(watchers.items()), 1):
|
|
1007
|
+
rows.append(line(f"Watcher {index} service", service))
|
|
1008
|
+
values = metadata if isinstance(metadata, dict) else {}
|
|
1009
|
+
for label, key in (("pane ID", "paneId"), ("session", "sessionName"),
|
|
1010
|
+
("findings path", "findingsPath")):
|
|
1011
|
+
if key in values:
|
|
1012
|
+
rows.append(line(f"Watcher {index} {label}", values.get(key)))
|
|
1013
|
+
return rows
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _render_container_status(payload: dict) -> list[str]:
|
|
1017
|
+
containers = payload.get("containers") if isinstance(payload.get("containers"), list) else []
|
|
1018
|
+
rows: list[str] = []
|
|
1019
|
+
for index, item in enumerate(containers, 1):
|
|
1020
|
+
values = item if isinstance(item, dict) else {}
|
|
1021
|
+
for label, key in (("name", "name"), ("service", "service"), ("state", "state"),
|
|
1022
|
+
("status", "status"), ("ports", "ports")):
|
|
1023
|
+
if key in values:
|
|
1024
|
+
rows.append(line(f"Container {index} {label}", values.get(key)))
|
|
1025
|
+
return rows
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _render_reaped_panes(prefix: str, raw_panes: object) -> list[str]:
|
|
1029
|
+
panes = raw_panes if isinstance(raw_panes, list) else []
|
|
1030
|
+
return [line(f"{prefix} {index}", pane) for index, pane in enumerate(panes, 1)]
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _render_container_down(payload: dict) -> list[str]:
|
|
1034
|
+
downed = payload.get("downed") if isinstance(payload.get("downed"), list) else []
|
|
1035
|
+
rows: list[str] = []
|
|
1036
|
+
for index, item in enumerate(downed, 1):
|
|
1037
|
+
values = item if isinstance(item, dict) else {}
|
|
1038
|
+
rows.append(line(f"Downed {index} project name", values.get("projectName")))
|
|
1039
|
+
rows.extend(_render_reaped_panes(
|
|
1040
|
+
f"Downed {index} reaped pane", values.get("reapedPanes")
|
|
1041
|
+
))
|
|
1042
|
+
rows.extend(_render_reaped_panes("Orphan pane", payload.get("orphanPanesReaped")))
|
|
1043
|
+
return rows
|
|
1044
|
+
|
|
1045
|
+
|
|
951
1046
|
def _dispatch(args) -> dict:
|
|
952
1047
|
pr = args.project_root
|
|
953
1048
|
if args.command == "up":
|
|
@@ -15,6 +15,7 @@ from typing import Iterable
|
|
|
15
15
|
|
|
16
16
|
from okstra_ctl.paths import RunRef, runs_dir_of, task_manifest_file
|
|
17
17
|
from okstra_ctl.task_target import resolve_task_root, project_rel
|
|
18
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
INPUT_FILES = (
|
|
@@ -83,7 +84,7 @@ def _load_json(path: Path) -> dict:
|
|
|
83
84
|
if not path.is_file():
|
|
84
85
|
return {}
|
|
85
86
|
try:
|
|
86
|
-
data =
|
|
87
|
+
data = load_owned_object(path, artifact="context cost input")
|
|
87
88
|
except Exception:
|
|
88
89
|
return {}
|
|
89
90
|
return data if isinstance(data, dict) else {}
|