okstra 0.189.4 → 0.190.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 +1 -1
- package/dist/cli-registry.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +54 -4
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +12 -5
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.mjs +2 -1
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +5 -3
- package/dist/lib/host-config.mjs +89 -31
- package/dist/lib/host-config.mjs.map +1 -1
- package/docs/architecture/storage-model.md +4 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +10 -8
- package/docs/for-ai/skills/okstra-brief-gen.md +1 -1
- package/docs/for-ai/skills/okstra-chat.md +3 -3
- package/docs/for-ai/skills/okstra-inspect.md +11 -1
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/for-ai/skills/okstra-run.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +1 -1
- package/docs/project-structure-overview.md +17 -5
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +8 -6
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -0
- package/runtime/bin/okstra-report-translate.py +10 -4
- package/runtime/prompts/launch.template.md +11 -5
- package/runtime/prompts/lead/adapters/cmux.md +4 -7
- package/runtime/prompts/lead/context-loader.md +2 -0
- package/runtime/prompts/lead/convergence.md +20 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -6
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +7 -4
- package/runtime/prompts/lead/team-contract.md +15 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -0
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
- package/runtime/prompts/profiles/_implementation-diff-review.md +4 -0
- package/runtime/prompts/profiles/_implementation-executor.md +9 -5
- package/runtime/prompts/profiles/_implementation-self-check.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -4
- package/runtime/prompts/profiles/_stage-discipline.md +12 -5
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -3
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +9 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +17 -3
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +3 -3
- package/runtime/python/okstra_ctl/agent/invocation.py +107 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +16 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +3 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +87 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +72 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -6
- package/runtime/python/okstra_ctl/cmux.py +34 -13
- package/runtime/python/okstra_ctl/consumers.py +30 -7
- package/runtime/python/okstra_ctl/convergence.py +110 -21
- package/runtime/python/okstra_ctl/convergence_provenance.py +83 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +127 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -9
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +1 -0
- package/runtime/python/okstra_ctl/error_log_write.py +44 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +84 -7
- package/runtime/python/okstra_ctl/fixed_text.py +25 -7
- package/runtime/python/okstra_ctl/group_context.py +475 -10
- package/runtime/python/okstra_ctl/handoff.py +109 -32
- package/runtime/python/okstra_ctl/handoff_verification.py +80 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +52 -10
- package/runtime/python/okstra_ctl/lead_progress.py +291 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +60 -1
- package/runtime/python/okstra_ctl/model_io_cli.py +6 -1
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/recap.py +179 -17
- package/runtime/python/okstra_ctl/reconcile.py +55 -4
- package/runtime/python/okstra_ctl/render.py +10 -1
- package/runtime/python/okstra_ctl/report_assembly.py +7 -6
- package/runtime/python/okstra_ctl/report_finalize.py +204 -11
- package/runtime/python/okstra_ctl/report_html/render.py +15 -4
- package/runtime/python/okstra_ctl/run.py +48 -14
- package/runtime/python/okstra_ctl/stage_targets.py +66 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +39 -12
- package/runtime/python/okstra_ctl/wizard/__init__.py +148 -0
- package/runtime/python/okstra_ctl/wizard/__main__.py +13 -0
- package/runtime/python/okstra_ctl/wizard/cli.py +176 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +331 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +363 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +411 -0
- package/runtime/python/okstra_ctl/wizard/prompts.py +202 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +836 -0
- package/runtime/python/okstra_ctl/wizard/render.py +189 -0
- package/runtime/python/okstra_ctl/wizard/roles.py +737 -0
- package/runtime/python/okstra_ctl/wizard/sources.py +703 -0
- package/runtime/python/okstra_ctl/wizard/state.py +689 -0
- package/runtime/python/okstra_ctl/wizard/statefile.py +145 -0
- package/runtime/python/okstra_ctl/wizard/steps_analysis.py +558 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +828 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +340 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +959 -0
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +672 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +43 -3
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +14 -0
- package/runtime/python/okstra_ctl/workflow.py +2 -2
- package/runtime/python/okstra_ctl/worktree/__init__.py +2 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +7 -4
- package/runtime/python/okstra_ctl/worktree/git_ops.py +7 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +15 -6
- package/runtime/python/okstra_ctl/worktree/provision.py +2 -1
- package/runtime/python/okstra_ctl/worktree_registry.py +27 -0
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +22 -3
- package/runtime/skills/okstra-chat/SKILL.md +8 -6
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +52 -4
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +17 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +2 -0
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/reports/brief.template.md +15 -7
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/group-context.template.md +7 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +3 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +3 -0
- package/runtime/templates/reports/release-handoff-input.template.md +5 -3
- package/runtime/templates/reports/schedule.template.md +13 -4
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/templates/reverify-output-contract.md +5 -0
- package/runtime/templates/worker-error-contract.md +2 -0
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +81 -22
- package/runtime/validators/validate_session_conformance.py +102 -2
- package/runtime/python/okstra_ctl/wizard.py +0 -7168
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import datetime as dt
|
|
13
13
|
import json
|
|
14
|
+
import re
|
|
14
15
|
import os
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
@@ -186,6 +187,37 @@ def append_jsonl_line(path, record):
|
|
|
186
187
|
os.close(fd)
|
|
187
188
|
|
|
188
189
|
|
|
190
|
+
_LEAD_PHASE_LABEL = re.compile(r"^(?:phase-)?\d+(?:\.\d+)?$")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def task_type_from_errors_log_path(out_path) -> str | None:
|
|
194
|
+
"""`…/runs/<task-type>[/stage-N]/logs/errors-*.jsonl` 에서 task type 을 읽는다."""
|
|
195
|
+
parts = Path(str(out_path)).parts
|
|
196
|
+
if "runs" not in parts:
|
|
197
|
+
return None
|
|
198
|
+
index = parts.index("runs")
|
|
199
|
+
return parts[index + 1] if index + 1 < len(parts) else None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def normalize_phase_label(phase, out_path) -> tuple[str | None, str | None]:
|
|
203
|
+
"""``(phase, leadPhase)`` — 원장의 `phase` 는 task type 어휘 하나로 통일한다.
|
|
204
|
+
|
|
205
|
+
리드 계약은 자기 내부 단계 번호(`--phase 5.5` = 수렴 라운드)를 쓰고 워커는
|
|
206
|
+
task type 이름을 쓰므로, 한 run 의 errors.jsonl 에 `final-verification`·`5`·
|
|
207
|
+
`5.5` 가 섞였고 `prior-run-errors.md` 의 표가 그대로 그 셋을 나열했다(실측
|
|
208
|
+
2026-09-06, fontsninja-v3-site dev-10626). 숫자 라벨은 `leadPhase` 로 보존하고
|
|
209
|
+
`phase` 는 로그 경로가 말하는 task type 으로 적는다. 경로에서 읽을 수 없으면
|
|
210
|
+
라벨을 그대로 둔다 — 지어내지 않는다.
|
|
211
|
+
"""
|
|
212
|
+
if phase is None:
|
|
213
|
+
return None, None
|
|
214
|
+
raw = str(phase)
|
|
215
|
+
if not _LEAD_PHASE_LABEL.match(raw):
|
|
216
|
+
return raw, None
|
|
217
|
+
task_type = task_type_from_errors_log_path(out_path)
|
|
218
|
+
return (task_type or raw), raw
|
|
219
|
+
|
|
220
|
+
|
|
189
221
|
def append_observed(
|
|
190
222
|
*,
|
|
191
223
|
out_path,
|
|
@@ -216,11 +248,12 @@ def append_observed(
|
|
|
216
248
|
# log: a written-then-flagged record is still a record someone can cite.
|
|
217
249
|
context = normalize_cause_context(context, message=message)
|
|
218
250
|
ts = _iso(now or _now_utc())
|
|
251
|
+
phase_value, lead_phase = normalize_phase_label(phase, out_path)
|
|
219
252
|
rec = {
|
|
220
253
|
"ts": ts,
|
|
221
254
|
"recordedAt": ts,
|
|
222
255
|
"taskKey": task_key,
|
|
223
|
-
"phase":
|
|
256
|
+
"phase": phase_value,
|
|
224
257
|
"agent": agent,
|
|
225
258
|
"agentRole": agent_role,
|
|
226
259
|
"model": model,
|
|
@@ -234,6 +267,8 @@ def append_observed(
|
|
|
234
267
|
"stderrExcerpt": truncate_stderr(stderr_excerpt),
|
|
235
268
|
"context": context,
|
|
236
269
|
}
|
|
270
|
+
if lead_phase is not None:
|
|
271
|
+
rec["leadPhase"] = lead_phase
|
|
237
272
|
from okstra_ctl.execution_identity import stored_identity
|
|
238
273
|
|
|
239
274
|
rec.update(stored_identity(identity))
|
|
@@ -241,14 +276,17 @@ def append_observed(
|
|
|
241
276
|
return rec
|
|
242
277
|
|
|
243
278
|
|
|
244
|
-
def _worker_sidecar_record(
|
|
279
|
+
def _worker_sidecar_record(
|
|
280
|
+
entry, *, recorded_at, task_key, agent, agent_role, model, identity, out_path=None,
|
|
281
|
+
):
|
|
245
282
|
error_type = entry.get("errorType")
|
|
246
283
|
if error_type not in ALLOWED_ERROR_TYPES:
|
|
247
284
|
raise ValueError(f"invalid errorType in sidecar: {error_type!r}")
|
|
285
|
+
phase_value, lead_phase = normalize_phase_label(entry.get("phase"), out_path)
|
|
248
286
|
rec = {
|
|
249
287
|
"ts": entry.get("ts"), "recordedAt": recorded_at,
|
|
250
288
|
"taskKey": task_key,
|
|
251
|
-
"phase":
|
|
289
|
+
"phase": phase_value,
|
|
252
290
|
"agent": agent, "agentRole": agent_role, "model": model,
|
|
253
291
|
"source": "worker-reported", "errorType": error_type,
|
|
254
292
|
"command": entry.get("command"), "commandKind": entry.get("commandKind"),
|
|
@@ -259,6 +297,8 @@ def _worker_sidecar_record(entry, *, recorded_at, task_key, agent, agent_role, m
|
|
|
259
297
|
entry.get("context"), message=entry.get("message")
|
|
260
298
|
),
|
|
261
299
|
}
|
|
300
|
+
if lead_phase is not None:
|
|
301
|
+
rec["leadPhase"] = lead_phase
|
|
262
302
|
from okstra_ctl.execution_identity import stored_identity
|
|
263
303
|
|
|
264
304
|
rec.update(stored_identity(identity))
|
|
@@ -315,7 +355,7 @@ def dump_from_worker_sidecar(
|
|
|
315
355
|
for entry in payload.get("errors") or []:
|
|
316
356
|
rec = _worker_sidecar_record(
|
|
317
357
|
entry, recorded_at=recorded_at, task_key=task_key, agent=agent,
|
|
318
|
-
agent_role=agent_role, model=model, identity=identity,
|
|
358
|
+
agent_role=agent_role, model=model, identity=identity, out_path=out_path,
|
|
319
359
|
)
|
|
320
360
|
append_jsonl_line(out_path, rec)
|
|
321
361
|
count += 1
|
|
@@ -10,6 +10,7 @@ from dataclasses import dataclass
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
+
from .worktree.cleanliness import nested_worktree_excludes
|
|
13
14
|
from .write_policy import WritePolicy
|
|
14
15
|
|
|
15
16
|
|
|
@@ -95,17 +96,27 @@ class MutationAuditResult:
|
|
|
95
96
|
before_digest: str
|
|
96
97
|
after_digest: str
|
|
97
98
|
git_projection: Mapping[str, Any]
|
|
99
|
+
# artifact root(프로젝트 루트)에 새로 생긴 비추적 파일 중 `.okstra/` 밖의 것.
|
|
100
|
+
# 워커의 도구가 남긴 로그·캐시라 위반이 아니라 기록이다 — 실측(2026-09-06,
|
|
101
|
+
# `fontsninja-v3-site` final-verification 001): 검증자의 브라우저 도구가
|
|
102
|
+
# `.playwright-mcp/*.log` 2건을 프로젝트 루트에 남겼고, 그 둘로 26KB 결과가
|
|
103
|
+
# `contract-failed-unattributed` 로 폐기됐다. 도구 사용 자체의 규칙 위반은
|
|
104
|
+
# 에러 원장(contract-violation)이 따로 잡는다.
|
|
105
|
+
untracked_artifact_paths: tuple[str, ...] = ()
|
|
106
|
+
warnings: tuple[str, ...] = ()
|
|
98
107
|
|
|
99
108
|
def change_summary(self) -> dict[str, Any]:
|
|
100
109
|
return {
|
|
101
110
|
"mutationStatus": self.status,
|
|
102
111
|
"changedPaths": list(self.changed_paths),
|
|
103
112
|
"untrackedPaths": list(self.untracked_paths),
|
|
113
|
+
"untrackedArtifactPaths": list(self.untracked_artifact_paths),
|
|
104
114
|
"sourceChanged": self.source_changed,
|
|
105
115
|
"gitChanged": self.git_changed,
|
|
106
116
|
"attribution": self.attribution,
|
|
107
117
|
"retryAllowed": self.retry_allowed,
|
|
108
118
|
"violations": list(self.violations),
|
|
119
|
+
"warnings": list(self.warnings),
|
|
109
120
|
"beforeDigest": self.before_digest,
|
|
110
121
|
"afterDigest": self.after_digest,
|
|
111
122
|
}
|
|
@@ -123,12 +134,12 @@ class ExecutionMutationAudit:
|
|
|
123
134
|
rows = tuple(policies)
|
|
124
135
|
root, artifact_root = _validate_batch(rows)
|
|
125
136
|
generated = _generated_paths(rows)
|
|
126
|
-
file_digests = _content_snapshot(root, generated |
|
|
137
|
+
file_digests = _content_snapshot(root, generated | _non_source_paths(root))
|
|
127
138
|
artifact_digests = (
|
|
128
139
|
file_digests
|
|
129
140
|
if artifact_root == root
|
|
130
141
|
else _content_snapshot(
|
|
131
|
-
artifact_root, generated |
|
|
142
|
+
artifact_root, generated | _non_source_paths(artifact_root)
|
|
132
143
|
)
|
|
133
144
|
)
|
|
134
145
|
scratch_digests = _scratch_snapshot(rows)
|
|
@@ -195,9 +206,10 @@ class ExecutionMutationAudit:
|
|
|
195
206
|
source_changes,
|
|
196
207
|
out_of_plan_edits,
|
|
197
208
|
)
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
artifact_failures, untracked_artifact_changes = _artifact_policy_failures(
|
|
210
|
+
before, rows, artifact_changed
|
|
200
211
|
)
|
|
212
|
+
violations.extend(artifact_failures)
|
|
201
213
|
status = _terminal_status(
|
|
202
214
|
rows,
|
|
203
215
|
source_changed=bool(source_changes),
|
|
@@ -221,9 +233,23 @@ class ExecutionMutationAudit:
|
|
|
221
233
|
before_digest=before.digest,
|
|
222
234
|
after_digest=after.digest,
|
|
223
235
|
git_projection=after.git_projection,
|
|
236
|
+
untracked_artifact_paths=tuple(sorted(untracked_artifact_changes)),
|
|
237
|
+
warnings=_audit_warnings(untracked_artifact_changes),
|
|
224
238
|
)
|
|
225
239
|
|
|
226
240
|
|
|
241
|
+
def _audit_warnings(untracked_artifact_changes: set[str]) -> tuple[str, ...]:
|
|
242
|
+
if not untracked_artifact_changes:
|
|
243
|
+
return ()
|
|
244
|
+
shown = sorted(untracked_artifact_changes)[:5]
|
|
245
|
+
more = len(untracked_artifact_changes) - len(shown)
|
|
246
|
+
tail = f" (+{more} more)" if more else ""
|
|
247
|
+
return (
|
|
248
|
+
"worker left untracked files in the artifact root outside `.okstra`: "
|
|
249
|
+
+ ", ".join(shown) + tail,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
227
253
|
def _validate_batch(policies: tuple[WritePolicy, ...]) -> tuple[Path, Path]:
|
|
228
254
|
if not policies:
|
|
229
255
|
raise MutationAuditError("mutation audit requires at least one policy")
|
|
@@ -280,6 +306,11 @@ file failed both acceptance verifiers of an otherwise clean stage.
|
|
|
280
306
|
|
|
281
307
|
One ecosystem, because one is what has been observed. A second name belongs here
|
|
282
308
|
when a run produces the same evidence for it, not before.
|
|
309
|
+
|
|
310
|
+
Tool logs that land in the *artifact root* (e.g. `.playwright-mcp/`) are not
|
|
311
|
+
listed here either: `_artifact_policy_failures` records untracked files outside
|
|
312
|
+
`.okstra/` instead of failing on them (observed 2026-09-06, `fontsninja-v3-site`
|
|
313
|
+
final-verification 001).
|
|
283
314
|
"""
|
|
284
315
|
|
|
285
316
|
|
|
@@ -344,6 +375,30 @@ def _excluded(path: Path, excluded: frozenset[str]) -> bool:
|
|
|
344
375
|
return any(relative == item or relative.startswith(item + "/") for item in excluded)
|
|
345
376
|
|
|
346
377
|
|
|
378
|
+
def _non_source_paths(root: Path) -> frozenset[str]:
|
|
379
|
+
"""감사에서 뺄 경로 — 레포가 무시하는 경로와 이 트리 안에 등록된 다른 worktree."""
|
|
380
|
+
return _ignored_paths(root) | _nested_worktree_paths(root)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _nested_worktree_paths(root: Path) -> frozenset[str]:
|
|
384
|
+
"""`root` 아래에 git 이 등록한 다른 worktree 의 상대 경로.
|
|
385
|
+
|
|
386
|
+
형제 배치 이전의 implementation stage worktree 는 task worktree 안
|
|
387
|
+
(`<task>/stage-<N>/`)에 만들어졌고 final-verification 은 판정 뒤까지 그것을
|
|
388
|
+
남긴다. 중첩 트리는
|
|
389
|
+
이 워커의 소스가 아니라 다른 브랜치의 체크아웃이므로, 그 안에서 일어난
|
|
390
|
+
일(빌드 산출물, 다른 run 의 커밋)은 이 워커의 변경이 아니다. 실측
|
|
391
|
+
(2026-09-06, `fontsninja-v3-site` final-verification 001): 두 검증자의
|
|
392
|
+
attempt 가 `stage-1/.next/**` 107·109 경로를 `untrackedPaths` 로 실었다.
|
|
393
|
+
`nested_worktree_excludes` 가 okstra 의 clean 게이트에 쓰는 것과 같은
|
|
394
|
+
답(`git worktree list`)을 여기서도 쓴다 — 패턴을 따로 들고 있으면 둘이
|
|
395
|
+
갈린다.
|
|
396
|
+
"""
|
|
397
|
+
return frozenset(
|
|
398
|
+
Path(entry).as_posix() for entry in nested_worktree_excludes(root)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
347
402
|
def _ignored_paths(root: Path) -> frozenset[str]:
|
|
348
403
|
"""레포가 스스로 소스가 아니라고 선언한 경로.
|
|
349
404
|
|
|
@@ -536,11 +591,22 @@ def _source_changes(
|
|
|
536
591
|
}
|
|
537
592
|
|
|
538
593
|
|
|
594
|
+
_OKSTRA_ARTIFACT_SUBTREE = ".okstra"
|
|
595
|
+
|
|
596
|
+
|
|
539
597
|
def _artifact_policy_failures(
|
|
540
598
|
snapshot: MutationSnapshot,
|
|
541
599
|
policies: Sequence[WritePolicy],
|
|
542
600
|
changed: set[str],
|
|
543
|
-
) -> list[str]:
|
|
601
|
+
) -> tuple[list[str], set[str]]:
|
|
602
|
+
"""``(위반 목록, 위반이 아닌 비추적 신규 경로)``.
|
|
603
|
+
|
|
604
|
+
artifact root 의 변경 중 위반으로 남는 것은 두 부류다 — okstra 산출물
|
|
605
|
+
서브트리(`.okstra/`) 안의 허용 밖 쓰기와, git 이 추적하는 파일의 변경.
|
|
606
|
+
그 밖의 비추적 신규 파일은 워커의 도구가 남긴 로그·캐시이므로 소스 root 의
|
|
607
|
+
`_split_tracked` 와 같은 이유로 기록만 한다. artifact root 가 git 레포가
|
|
608
|
+
아니면 추적 여부를 알 수 없으므로 종전대로 전부 위반으로 본다.
|
|
609
|
+
"""
|
|
544
610
|
allowed = _allowed_artifact_paths(policies)
|
|
545
611
|
orchestrator = {
|
|
546
612
|
Path(value).relative_to(snapshot.artifact_root).as_posix()
|
|
@@ -553,8 +619,19 @@ def _artifact_policy_failures(
|
|
|
553
619
|
and not any(_is_within(path, item) for item in orchestrator)
|
|
554
620
|
}
|
|
555
621
|
if snapshot.artifact_root == snapshot.root:
|
|
556
|
-
return []
|
|
557
|
-
|
|
622
|
+
return [], set()
|
|
623
|
+
tracked = _tracked_paths(snapshot.artifact_root)
|
|
624
|
+
if tracked is None:
|
|
625
|
+
untracked_outside: set[str] = set()
|
|
626
|
+
else:
|
|
627
|
+
untracked_outside = {
|
|
628
|
+
path for path in unauthorized
|
|
629
|
+
if path not in tracked
|
|
630
|
+
and not _is_within(path, _OKSTRA_ARTIFACT_SUBTREE)
|
|
631
|
+
}
|
|
632
|
+
violating = unauthorized - untracked_outside
|
|
633
|
+
failures = ["artifact-root change exceeds batch policy union"] if violating else []
|
|
634
|
+
return failures, untracked_outside
|
|
558
635
|
|
|
559
636
|
|
|
560
637
|
def _allowed_artifact_paths(policies: Sequence[WritePolicy]) -> set[str]:
|
|
@@ -4,16 +4,34 @@ from __future__ import annotations
|
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
_CONTROL = re.compile(r"[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\u2028\u2029]")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _one_line(text: str) -> str:
|
|
11
|
+
"""줄바꿈은 공백으로, 나머지 제어 문자는 `\\uXXXX` 로 바꿔 한 줄로 만든다."""
|
|
12
|
+
text = text.replace("\n", " ").replace("\r", " ")
|
|
13
|
+
return _CONTROL.sub(lambda match: f"\\u{ord(match.group(0)):04X}", text)
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
def scalar(value: object) -> str:
|
|
8
|
-
"""복합 값과 Markdown 줄 삽입을 노출하지 않는 scalar 문자열이다.
|
|
17
|
+
"""복합 값과 Markdown 줄 삽입을 노출하지 않는 scalar 문자열이다.
|
|
18
|
+
|
|
19
|
+
`line` 이 이 값을 인라인 코드 span(백틱 한 쌍) 안에 넣으므로 값 안의 백틱은
|
|
20
|
+
이스케이프한다. 백틱이 표기 그대로 남아야 하는 본문에는 `block` 을 쓴다.
|
|
21
|
+
"""
|
|
9
22
|
if value is None or value == "" or not isinstance(value, (str, int, float, bool)):
|
|
10
23
|
return "-"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
return _one_line(str(value).replace("`", "\\`"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def block(text: str) -> str:
|
|
28
|
+
"""코드 span 밖에 놓이는 본문 한 덩어리의 투영이다.
|
|
29
|
+
|
|
30
|
+
백틱은 그대로 둔다 — 본문의 백틱은 코드 표기이고, 이스케이프하면 번역자가 그
|
|
31
|
+
백슬래시를 그대로 옮겨 렌더링에 남는다. 줄바꿈과 제어 문자 처리는 `scalar`
|
|
32
|
+
와 같아서 본문이 다음 헤딩을 위조할 수 없다.
|
|
33
|
+
"""
|
|
34
|
+
return _one_line(text)
|
|
17
35
|
|
|
18
36
|
|
|
19
37
|
def line(label: str, value: object) -> str:
|