pdl-taskmaster 2.4.0__py3-none-any.whl

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.
Files changed (92) hide show
  1. pdl_taskmaster/__init__.py +6 -0
  2. pdl_taskmaster/contracts/AUTHORITY_MAP.json +19 -0
  3. pdl_taskmaster/contracts/CALIBRATION.json +7 -0
  4. pdl_taskmaster/contracts/CONTRACT_MANIFEST.json +231 -0
  5. pdl_taskmaster/contracts/EXECUTION_CONTRACT.json +615 -0
  6. pdl_taskmaster/contracts/IMPLEMENTATION_BOUNDARY.json +32 -0
  7. pdl_taskmaster/contracts/VERIFICATION_CONTRACT.json +379 -0
  8. pdl_taskmaster/contracts/standards/ARTIFACT_STANDARD.md +7 -0
  9. pdl_taskmaster/contracts/standards/AUTHORITY_STANDARD.md +17 -0
  10. pdl_taskmaster/contracts/standards/CONFORMANCE_STANDARD.md +15 -0
  11. pdl_taskmaster/contracts/standards/CONTEXT_STANDARD.md +15 -0
  12. pdl_taskmaster/contracts/standards/EXECUTION_STANDARD.md +14 -0
  13. pdl_taskmaster/contracts/standards/HANDOFF_STANDARD.md +21 -0
  14. pdl_taskmaster/contracts/standards/INSTALLATION_STANDARD.md +17 -0
  15. pdl_taskmaster/contracts/standards/PDL_STANDARD.md +19 -0
  16. pdl_taskmaster/contracts/standards/PROMPT_STANDARD.md +13 -0
  17. pdl_taskmaster/contracts/standards/PROTOCOL_STANDARD.md +17 -0
  18. pdl_taskmaster/contracts/standards/RESPONSE_PLAN_STANDARD.md +24 -0
  19. pdl_taskmaster/contracts/standards/RESULT_STANDARD.md +35 -0
  20. pdl_taskmaster/contracts/standards/REVIEW_STANDARD.md +31 -0
  21. pdl_taskmaster/contracts/standards/SEMANTIC_INPUT_STANDARD.md +15 -0
  22. pdl_taskmaster/contracts/standards/TASK_SEMANTICS_STANDARD.md +9 -0
  23. pdl_taskmaster/controller/__init__.py +0 -0
  24. pdl_taskmaster/controller/mechanical_controller.py +542 -0
  25. pdl_taskmaster/controller/schemas/activation_decision.schema.json +34 -0
  26. pdl_taskmaster/controller/schemas/artifact_body.schema.json +14 -0
  27. pdl_taskmaster/controller/schemas/artifact_review_facts.schema.json +91 -0
  28. pdl_taskmaster/controller/schemas/bootstrap_analysis.schema.json +60 -0
  29. pdl_taskmaster/controller/schemas/execution_draft.schema.json +47 -0
  30. pdl_taskmaster/controller/schemas/execution_input_decision.schema.json +43 -0
  31. pdl_taskmaster/controller/schemas/execution_outcome.schema.json +201 -0
  32. pdl_taskmaster/controller/schemas/plan_artifact.schema.json +14 -0
  33. pdl_taskmaster/controller/schemas/prompt_artifact.schema.json +63 -0
  34. pdl_taskmaster/controller/schemas/protocol_discussion.schema.json +8 -0
  35. pdl_taskmaster/controller/schemas/result_ir_repair.schema.json +140 -0
  36. pdl_taskmaster/eval/__init__.py +1 -0
  37. pdl_taskmaster/eval/build_adversarial_battery.py +351 -0
  38. pdl_taskmaster/eval/compare_eval_runs.py +251 -0
  39. pdl_taskmaster/eval/control_api_call.py +428 -0
  40. pdl_taskmaster/eval/export_rlcd_dataset.py +799 -0
  41. pdl_taskmaster/eval/fidelity_scan.py +268 -0
  42. pdl_taskmaster/eval/gen_fidelity_battery.py +457 -0
  43. pdl_taskmaster/eval/leak_scan.py +260 -0
  44. pdl_taskmaster/eval/rerecord_fixtures.py +139 -0
  45. pdl_taskmaster/eval/run_canary_shape_probe.py +229 -0
  46. pdl_taskmaster/eval/run_dual_gate_probe.py +404 -0
  47. pdl_taskmaster/eval/run_qualified_batch.py +841 -0
  48. pdl_taskmaster/eval/summarize_run.py +194 -0
  49. pdl_taskmaster/host/__init__.py +1 -0
  50. pdl_taskmaster/host/app.py +164 -0
  51. pdl_taskmaster/host/cli.py +100 -0
  52. pdl_taskmaster/host/repl.py +866 -0
  53. pdl_taskmaster/observation/__init__.py +1 -0
  54. pdl_taskmaster/observation/cli.py +47 -0
  55. pdl_taskmaster/observation/observed_session.py +219 -0
  56. pdl_taskmaster/observation/parsers.py +66 -0
  57. pdl_taskmaster/observation/records.py +58 -0
  58. pdl_taskmaster/observation/session.py +3 -0
  59. pdl_taskmaster/observation/sink.py +3 -0
  60. pdl_taskmaster/observation/sinks.py +20 -0
  61. pdl_taskmaster/providers/README.md +66 -0
  62. pdl_taskmaster/providers/__init__.py +1 -0
  63. pdl_taskmaster/providers/api_worker.py +411 -0
  64. pdl_taskmaster/providers/base.py +26 -0
  65. pdl_taskmaster/providers/codex_worker.py +328 -0
  66. pdl_taskmaster/providers/fixtures.py +157 -0
  67. pdl_taskmaster/providers/live_stub.py +66 -0
  68. pdl_taskmaster/providers/recorded.py +120 -0
  69. pdl_taskmaster/py.typed +1 -0
  70. pdl_taskmaster/runtime/__init__.py +0 -0
  71. pdl_taskmaster/runtime/context_compiler.py +151 -0
  72. pdl_taskmaster/runtime/model_classification.py +290 -0
  73. pdl_taskmaster/runtime/normative_store.py +157 -0
  74. pdl_taskmaster/runtime/operation_bridge.py +336 -0
  75. pdl_taskmaster/runtime/presentation.py +18 -0
  76. pdl_taskmaster/runtime/quarantine.py +91 -0
  77. pdl_taskmaster/runtime/result_ir.py +401 -0
  78. pdl_taskmaster/runtime/semantic_observer.py +40 -0
  79. pdl_taskmaster/runtime/session_engine.py +1168 -0
  80. pdl_taskmaster/runtime/standard_registry.py +55 -0
  81. pdl_taskmaster/runtime/wire_payloads.py +462 -0
  82. pdl_taskmaster/runtime/worker-bootstrap.txt +4 -0
  83. pdl_taskmaster/runtime/workspace.py +602 -0
  84. pdl_taskmaster/tracking/__init__.py +1 -0
  85. pdl_taskmaster/tracking/log_live_session.py +98 -0
  86. pdl_taskmaster/tracking/mlflow_sink.py +58 -0
  87. pdl_taskmaster/verify/verify_repl_baseline.py +299 -0
  88. pdl_taskmaster-2.4.0.dist-info/METADATA +155 -0
  89. pdl_taskmaster-2.4.0.dist-info/RECORD +92 -0
  90. pdl_taskmaster-2.4.0.dist-info/WHEEL +4 -0
  91. pdl_taskmaster-2.4.0.dist-info/entry_points.txt +2 -0
  92. pdl_taskmaster-2.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,602 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any
7
+ import json
8
+ import os
9
+ import re
10
+ import tempfile
11
+ import uuid
12
+
13
+
14
+ class WorkspaceError(RuntimeError):
15
+ pass
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class WorkspaceInvocation:
20
+ operation: str
21
+ stage: str
22
+ invocation_id: str
23
+ input_dir: Path
24
+ output_dir: Path
25
+
26
+
27
+ class WorkspaceRun:
28
+ """Filesystem-backed ICM context-flow workspace for one candidate session.
29
+
30
+ The workspace is an orchestration and handoff substrate only. It does not
31
+ define protocol correctness. Standard Contracts remain the sole normative
32
+ authority; the Execution Contract selects which Standard IDs and working
33
+ artifacts are exposed to each semantic operation.
34
+ """
35
+
36
+ SCHEMA = "C0-ICM-WORKSPACE-5"
37
+
38
+ def __init__(self, repo_root: Path, path: Path):
39
+ self.repo_root = repo_root
40
+ self.path = path
41
+ self.metadata_path = self.path / "workspace.json"
42
+ if not self.metadata_path.is_file():
43
+ raise WorkspaceError("workspace_metadata_missing")
44
+ self.metadata = json.loads(self.metadata_path.read_text(encoding="utf-8"))
45
+ if self.metadata.get("schema_version") != self.SCHEMA:
46
+ raise WorkspaceError("workspace_schema")
47
+ # Session-hierarchy mode (ADR-0008 / Phase 9 S3): when turn_id is set,
48
+ # turn-scoped state (controller state, events, stages) lives under
49
+ # turns/<turn_id>/ while shared/ and workspace.json stay session-level.
50
+ # Legacy single-turn workspaces (turn_id None) keep the flat layout.
51
+ self.turn_id: str | None = self.metadata.get("turn_id")
52
+ from pdl_taskmaster.runtime.normative_store import NormativeStore
53
+ contract_path = NormativeStore.resolve_contract(self.repo_root, "EXECUTION_CONTRACT.json")
54
+ if not contract_path.is_file():
55
+ contract_path = self.repo_root / "contracts" / "EXECUTION_CONTRACT.json"
56
+ self.execution_contract = json.loads(contract_path.read_text(encoding="utf-8"))
57
+
58
+ @classmethod
59
+ def create(
60
+ cls,
61
+ repo_root: str | Path,
62
+ workspace_root: str | Path,
63
+ *,
64
+ turn_id: str | None = None,
65
+ ) -> "WorkspaceRun":
66
+ repo_root = Path(repo_root)
67
+ workspace_root = Path(workspace_root)
68
+ workspace_root.mkdir(parents=True, exist_ok=True)
69
+ workspace_id = f"W-{uuid.uuid4().hex[:12]}"
70
+ path = workspace_root / workspace_id
71
+ if path.exists():
72
+ raise WorkspaceError("workspace_collision")
73
+ path.mkdir(parents=True, exist_ok=True)
74
+ if turn_id is None:
75
+ (path / "state").mkdir(exist_ok=True)
76
+ (path / "events").mkdir(exist_ok=True)
77
+ (path / "stages").mkdir(exist_ok=True)
78
+ (path / "shared").mkdir(exist_ok=True)
79
+ else:
80
+ # Session-hierarchy mode (S3): session-level dirs plus the first
81
+ # turn's scoped tree. Stage directories materialize on demand.
82
+ (path / "shared").mkdir(exist_ok=True)
83
+ cls._scaffold_turn(path, turn_id)
84
+ metadata = {
85
+ "schema_version": cls.SCHEMA,
86
+ "workspace_id": workspace_id,
87
+ "protocol_instance_id": None,
88
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
89
+ "icm_reference": "arXiv:2603.16021v2",
90
+ "context_flow": "filesystem_stage_handoffs",
91
+ "control_flow": "mechanical_controller",
92
+ }
93
+ if turn_id is not None:
94
+ metadata["turn_id"] = turn_id
95
+ metadata["session_hierarchy"] = True
96
+ cls._atomic_write(path / "workspace.json", json.dumps(metadata, ensure_ascii=False, indent=2) + "\n")
97
+ run = cls(repo_root, path)
98
+ run.append_event("WORKSPACE_CREATED", {"workspace_id": workspace_id, "turn_id": turn_id})
99
+ return run
100
+
101
+ @classmethod
102
+ def _scaffold_turn(cls, path: Path, turn_id: str) -> Path:
103
+ turn_dir = path / "turns" / turn_id
104
+ if turn_dir.exists():
105
+ raise WorkspaceError(f"turn_exists:{turn_id}")
106
+ (turn_dir / "state").mkdir(parents=True, exist_ok=False)
107
+ (turn_dir / "events").mkdir(parents=True, exist_ok=False)
108
+ (turn_dir / "stages").mkdir(parents=True, exist_ok=False)
109
+ cls._atomic_write(
110
+ turn_dir / "turn.json",
111
+ json.dumps(
112
+ {
113
+ "turn_id": turn_id,
114
+ "status": "ACTIVE",
115
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
116
+ },
117
+ ensure_ascii=False,
118
+ indent=2,
119
+ )
120
+ + "\n",
121
+ )
122
+ return turn_dir
123
+
124
+ # -- Session-hierarchy helpers (S3/S4) ---------------------------------
125
+
126
+ def _turn_base(self) -> Path:
127
+ """Root for turn-scoped state: turns/<turn_id> in hierarchy mode,
128
+ the workspace root in legacy flat mode."""
129
+ return self.path / "turns" / self.turn_id if self.turn_id else self.path
130
+
131
+ def stages_root(self) -> Path:
132
+ """Public root for this workspace's stage tree (turn-scoped in
133
+ hierarchy mode; workspace root in legacy flat mode). Readers that
134
+ walk stage output must use this instead of path/"stages" so both
135
+ layouts stay supported."""
136
+ return self._turn_base() / "stages"
137
+
138
+ def start_turn(self, new_turn_id: str) -> None:
139
+ """Open a new turn in this session workspace and move the pointer.
140
+
141
+ The prior turn's scoped state (controller state, events, stages) is
142
+ preserved verbatim; only the pointer moves. Per ADR-0008 S4, the new
143
+ turn starts clean: prior drafts, rejected plans, and review dialogue
144
+ are never carried forward except through previous_deliverable()."""
145
+ if self.turn_id is None:
146
+ raise WorkspaceError("session_hierarchy_required")
147
+ if new_turn_id == self.turn_id:
148
+ raise WorkspaceError("turn_pointer_unchanged")
149
+ self._scaffold_turn(self.path, new_turn_id)
150
+ self.metadata["turn_id"] = new_turn_id
151
+ self._write(
152
+ self.metadata_path,
153
+ json.dumps(self.metadata, ensure_ascii=False, indent=2) + "\n",
154
+ )
155
+ self.turn_id = new_turn_id
156
+
157
+ def read_turn_status(self, turn_id: str) -> dict[str, Any]:
158
+ path = self.path / "turns" / turn_id / "turn.json"
159
+ if not path.is_file():
160
+ raise WorkspaceError(f"turn_missing:{turn_id}")
161
+ return json.loads(self._read(path))
162
+
163
+ def mark_turn_status(self, status: str, *, deliverable_sha256: str | None = None) -> None:
164
+ if self.turn_id is None:
165
+ raise WorkspaceError("session_hierarchy_required")
166
+ path = self.path / "turns" / self.turn_id / "turn.json"
167
+ data = json.loads(self._read(path)) if path.is_file() else {"turn_id": self.turn_id}
168
+ data["status"] = status
169
+ if deliverable_sha256 is not None:
170
+ data["deliverable_sha256"] = deliverable_sha256
171
+ data["updated_at_utc"] = datetime.now(timezone.utc).isoformat()
172
+ self._write(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
173
+ self.append_event("TURN_STATUS", {"turn_id": self.turn_id, "status": status})
174
+
175
+ def next_turn_id(self) -> str:
176
+ """Next free turn identifier (turn_001, turn_002, ...) for this workspace."""
177
+ turns_dir = self.path / "turns"
178
+ highest = 0
179
+ if turns_dir.is_dir():
180
+ for turn_dir in turns_dir.iterdir():
181
+ m = re.match(r"^turn_(\d+)$", turn_dir.name)
182
+ if m:
183
+ highest = max(highest, int(m.group(1)))
184
+ return f"turn_{highest + 1:03d}"
185
+
186
+ def closed_turns(self) -> list[dict[str, Any]]:
187
+ """All turns with a terminal status, in turn order."""
188
+ turns_dir = self.path / "turns"
189
+ if not turns_dir.is_dir():
190
+ return []
191
+ out = []
192
+ for turn_dir in sorted(turns_dir.iterdir()):
193
+ marker = turn_dir / "turn.json"
194
+ if marker.is_file():
195
+ data = json.loads(marker.read_text(encoding="utf-8"))
196
+ if data.get("status") in {"CLOSED_SUCCESS", "CLOSED_CANCELLED"}:
197
+ out.append(data)
198
+ return out
199
+
200
+ def previous_deliverable(self) -> str | None:
201
+ """S4: the confirmed deliverable of the most recent CLOSED_SUCCESS turn.
202
+
203
+ Reads ONLY that turn's published execution artifact
204
+ (turns/<id>/stages/50_execution/output/current.md). Drafts, rejected
205
+ plans, and review dialogue are structurally unreachable from here.
206
+ Returns None when no prior turn has closed successfully."""
207
+ closed = [t for t in self.closed_turns() if t.get("status") == "CLOSED_SUCCESS"]
208
+ if not closed:
209
+ return None
210
+ turn_id = closed[-1].get("turn_id")
211
+ body = self.path / "turns" / str(turn_id) / "stages" / "50_execution" / "output" / "current.md"
212
+ if not body.is_file():
213
+ return None
214
+ return body.read_text(encoding="utf-8").rstrip("\n")
215
+
216
+ @classmethod
217
+ def open(cls, repo_root: str | Path, path: str | Path) -> "WorkspaceRun":
218
+ return cls(Path(repo_root), Path(path))
219
+
220
+ def _write(self, path: Path, content: str) -> None:
221
+ self._atomic_write(path, content)
222
+
223
+ def _read(self, path: Path) -> str:
224
+ return path.read_text(encoding="utf-8")
225
+
226
+ @staticmethod
227
+ def _atomic_write(path: Path, content: str) -> None:
228
+ path.parent.mkdir(parents=True, exist_ok=True)
229
+ fd, tmp = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=path.parent)
230
+ try:
231
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
232
+ handle.write(content)
233
+ handle.flush()
234
+ os.fsync(handle.fileno())
235
+ os.replace(tmp, path)
236
+ finally:
237
+ if os.path.exists(tmp):
238
+ os.unlink(tmp)
239
+
240
+ @staticmethod
241
+ def _serialize_symbol(value: Any) -> tuple[str, str]:
242
+ if isinstance(value, str):
243
+ return ".md", value.rstrip() + "\n"
244
+ return ".json", json.dumps(value, ensure_ascii=False, indent=2) + "\n"
245
+
246
+ def _deserialize_symbol(self, path: Path) -> Any:
247
+ content = self._read(path)
248
+ if path.suffix == ".json":
249
+ return json.loads(content)
250
+ return content.rstrip("\n")
251
+
252
+
253
+ def append_event(self, kind: str, payload: dict[str, Any]) -> None:
254
+ event = {
255
+ "at_utc": datetime.now(timezone.utc).isoformat(),
256
+ "kind": kind,
257
+ "payload": payload,
258
+ }
259
+ self._record_event(event)
260
+
261
+ def _record_event(self, event: dict[str, Any]) -> None:
262
+ events = self._turn_base() / "events" / "events.jsonl"
263
+ events.parent.mkdir(parents=True, exist_ok=True)
264
+ with events.open("a", encoding="utf-8", newline="\n") as handle:
265
+ handle.write(json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n")
266
+ handle.flush()
267
+ os.fsync(handle.fileno())
268
+
269
+ @property
270
+ def controller_state_path(self) -> Path:
271
+ return self._turn_base() / "state" / "controller-state.json"
272
+
273
+ @property
274
+ def protocol_instance_id(self) -> str | None:
275
+ value = self.metadata.get("protocol_instance_id")
276
+ return value if isinstance(value, str) else None
277
+
278
+ def bind_protocol(self, instance_id: str) -> None:
279
+ current = self.protocol_instance_id
280
+ if self.turn_id is not None:
281
+ # Session-hierarchy mode: each turn runs its own protocol lifecycle,
282
+ # so re-binding across turns is expected. Record the instance on
283
+ # the current turn; the root pointer tracks the latest binding.
284
+ turn_json = self.path / "turns" / self.turn_id / "turn.json"
285
+ data = json.loads(self._read(turn_json)) if turn_json.is_file() else {"turn_id": self.turn_id}
286
+ data["protocol_instance_id"] = instance_id
287
+ self._write(turn_json, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
288
+ self.metadata["protocol_instance_id"] = instance_id
289
+ self._write(
290
+ self.metadata_path,
291
+ json.dumps(self.metadata, ensure_ascii=False, indent=2) + "\n",
292
+ )
293
+ self.append_event("PROTOCOL_BOUND", {"instance_id": instance_id, "turn_id": self.turn_id})
294
+ return
295
+ if current is not None and current != instance_id:
296
+ raise WorkspaceError("workspace_already_bound")
297
+ self.metadata["protocol_instance_id"] = instance_id
298
+ self._write(
299
+ self.metadata_path,
300
+ json.dumps(self.metadata, ensure_ascii=False, indent=2) + "\n",
301
+ )
302
+ self.append_event("PROTOCOL_BOUND", {"instance_id": instance_id})
303
+
304
+ def _stage_for(self, operation: str, values: dict[str, Any]) -> str:
305
+ try:
306
+ spec = self.execution_contract["operations"][operation]
307
+ except KeyError as exc:
308
+ raise WorkspaceError(f"operation:{operation}") from exc
309
+ stage = spec.get("workspace_stage")
310
+ selectors = spec.get("workspace_stage_by")
311
+ if selectors:
312
+ symbol = selectors.get("symbol")
313
+ mapping = selectors.get("mapping", {})
314
+ selected = mapping.get(values.get(symbol))
315
+ if not selected:
316
+ raise WorkspaceError(f"stage_selector:{operation}:{values.get(symbol)}")
317
+ stage = selected
318
+ stage_dir = self._turn_base() / "stages" / stage
319
+ stage_dir.mkdir(parents=True, exist_ok=True)
320
+ return stage
321
+
322
+ def _next_invocation_id(self, stage: str, operation: str) -> str:
323
+ counter_path = self._turn_base() / "state" / "invocation-counter.json"
324
+ if counter_path.is_file():
325
+ value = json.loads(self._read(counter_path))
326
+ counter = int(value.get("counter", 0)) + 1
327
+ else:
328
+ counter = 1
329
+ self._write(counter_path, json.dumps({"counter": counter}, indent=2) + "\n")
330
+ return f"{counter:04d}-{operation.lower()}"
331
+
332
+ def materialize_operation(
333
+ self,
334
+ operation: str,
335
+ values: dict[str, Any],
336
+ *,
337
+ higher_priority_constraints: Any = None,
338
+ ) -> WorkspaceInvocation:
339
+ stage = self._stage_for(operation, values)
340
+ invocation_id = self._next_invocation_id(stage, operation)
341
+ stage_path = self._turn_base() / "stages" / stage
342
+ input_dir = stage_path / "input" / invocation_id
343
+ output_dir = stage_path / "output" / invocation_id
344
+ input_dir.mkdir(parents=True, exist_ok=False)
345
+ output_dir.mkdir(parents=True, exist_ok=False)
346
+
347
+ index: dict[str, str] = {}
348
+ for symbol, value in values.items():
349
+ suffix, content = self._serialize_symbol(value)
350
+ filename = symbol.lower() + suffix
351
+ self._write(input_dir / filename, content)
352
+ index[symbol] = filename
353
+ if higher_priority_constraints is not None:
354
+ suffix, content = self._serialize_symbol(higher_priority_constraints)
355
+ filename = "higher_priority_constraints" + suffix
356
+ self._write(input_dir / filename, content)
357
+ index["HIGHER_PRIORITY_CONSTRAINTS"] = filename
358
+ self._write(input_dir / "index.json", json.dumps(index, ensure_ascii=False, indent=2) + "\n")
359
+ self._write(
360
+ input_dir / "invocation.json",
361
+ json.dumps(
362
+ {
363
+ "operation": operation,
364
+ "stage": stage,
365
+ "invocation_id": invocation_id,
366
+ "input_symbols": list(values),
367
+ },
368
+ ensure_ascii=False,
369
+ indent=2,
370
+ )
371
+ + "\n",
372
+ )
373
+ self.append_event(
374
+ "OPERATION_MATERIALIZED",
375
+ {"operation": operation, "stage": stage, "invocation_id": invocation_id},
376
+ )
377
+ return WorkspaceInvocation(operation, stage, invocation_id, input_dir, output_dir)
378
+
379
+ def load_operation_values(self, invocation: WorkspaceInvocation) -> tuple[dict[str, Any], Any]:
380
+ index = json.loads(self._read(invocation.input_dir / "index.json"))
381
+ values: dict[str, Any] = {}
382
+ higher_priority = None
383
+ for symbol, filename in index.items():
384
+ value = self._deserialize_symbol(invocation.input_dir / filename)
385
+ if symbol == "HIGHER_PRIORITY_CONSTRAINTS":
386
+ higher_priority = value
387
+ else:
388
+ values[symbol] = value
389
+ return values, higher_priority
390
+
391
+ def record_projection(self, invocation: WorkspaceInvocation, manifest: dict[str, Any], document: dict[str, Any]) -> None:
392
+ self._write(
393
+ invocation.output_dir / "projection-manifest.json",
394
+ json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
395
+ )
396
+ self._write(
397
+ invocation.output_dir / "compiled-projection.json",
398
+ json.dumps(document, ensure_ascii=False, indent=2) + "\n",
399
+ )
400
+
401
+ def record_model_output(self, invocation: WorkspaceInvocation, model_text: str) -> None:
402
+ self._write(invocation.output_dir / "model-response.txt", model_text.rstrip() + "\n")
403
+ self.append_event(
404
+ "MODEL_OUTPUT_RECORDED",
405
+ {"operation": invocation.operation, "stage": invocation.stage, "invocation_id": invocation.invocation_id},
406
+ )
407
+
408
+ def record_parsed_output(self, invocation: WorkspaceInvocation, value: Any) -> None:
409
+ self._write(
410
+ invocation.output_dir / "parsed-output.json",
411
+ json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n",
412
+ )
413
+
414
+ def _artifact_stage(self, kind: str) -> Path:
415
+ if kind == "prompt":
416
+ return self._turn_base() / "stages" / "10_prompt" / "output"
417
+ if kind == "plan":
418
+ return self._turn_base() / "stages" / "30_plan" / "output"
419
+ if kind == "result":
420
+ return self._turn_base() / "stages" / "50_execution" / "output"
421
+ raise WorkspaceError(f"artifact_kind:{kind}")
422
+
423
+ def publish_artifact(self, kind: str, artifact_id: str, body: str, *, confirmed: bool, source_prompt_id: str | None = None, confirmed_prompt_hash: str | None = None) -> None:
424
+ output = self._artifact_stage(kind)
425
+ versions = output / "versions"
426
+ versions.mkdir(parents=True, exist_ok=True)
427
+ self._write(versions / f"{artifact_id}.md", body.rstrip() + "\n")
428
+ self._write(output / "current.md", body.rstrip() + "\n")
429
+ payload: dict[str, Any] = {
430
+ "artifact_id": artifact_id,
431
+ "confirmed": confirmed,
432
+ "source_prompt_id": source_prompt_id,
433
+ }
434
+ if confirmed_prompt_hash:
435
+ payload["confirmed_prompt_hash"] = confirmed_prompt_hash
436
+ self._write(output / "current.json", json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
437
+ self.append_event(
438
+ "ARTIFACT_PUBLISHED",
439
+ {"kind": kind, "artifact_id": artifact_id, "confirmed": confirmed},
440
+ )
441
+
442
+ def invalidate_artifact(self, kind: str, reason: str) -> None:
443
+ output = self._artifact_stage(kind)
444
+ meta_path = output / "current.json"
445
+ body_path = output / "current.md"
446
+ if not meta_path.is_file() or not body_path.is_file():
447
+ return
448
+ meta = json.loads(self._read(meta_path))
449
+ artifact_id = meta.get("artifact_id")
450
+ invalidated = output / "invalidated"
451
+ invalidated.mkdir(parents=True, exist_ok=True)
452
+ self._write(
453
+ invalidated / f"{artifact_id}.json",
454
+ json.dumps({**meta, "status": "invalidated", "reason": reason}, ensure_ascii=False, indent=2) + "\n",
455
+ )
456
+ meta_path.unlink()
457
+ body_path.unlink()
458
+ self.append_event("ARTIFACT_INVALIDATED", {"kind": kind, "artifact_id": artifact_id, "reason": reason})
459
+
460
+ def mark_artifact_confirmed(self, kind: str, artifact_id: str) -> None:
461
+ output = self._artifact_stage(kind)
462
+ meta_path = output / "current.json"
463
+ meta = json.loads(self._read(meta_path))
464
+ if meta.get("artifact_id") != artifact_id:
465
+ raise WorkspaceError("artifact_confirmation_identity")
466
+ meta["confirmed"] = True
467
+ self._write(meta_path, json.dumps(meta, ensure_ascii=False, indent=2) + "\n")
468
+ self.append_event("ARTIFACT_CONFIRMED", {"kind": kind, "artifact_id": artifact_id})
469
+
470
+ def read_artifact(self, kind: str) -> tuple[dict[str, Any], str]:
471
+ output = self._artifact_stage(kind)
472
+ meta_path = output / "current.json"
473
+ body_path = output / "current.md"
474
+ if not meta_path.is_file() or not body_path.is_file():
475
+ raise WorkspaceError(f"artifact_missing:{kind}")
476
+ return (
477
+ json.loads(self._read(meta_path)),
478
+ self._read(body_path).rstrip("\n"),
479
+ )
480
+
481
+ def publish_approach_sources(self, sources: list[str]) -> None:
482
+ shared = self.path / "shared"
483
+ shared.mkdir(exist_ok=True)
484
+ self._write(
485
+ shared / "approach-sources.json",
486
+ json.dumps({"sources": sources}, ensure_ascii=False, indent=2) + "\n",
487
+ )
488
+
489
+ def read_approach_sources(self) -> list[str]:
490
+ path = self.path / "shared" / "approach-sources.json"
491
+ if not path.is_file():
492
+ return []
493
+ value = json.loads(self._read(path))
494
+ sources = value.get("sources", [])
495
+ if not isinstance(sources, list) or any(not isinstance(x, str) or not x.strip() for x in sources):
496
+ raise WorkspaceError("approach_source_file")
497
+ return sources
498
+
499
+ def publish_execution_outcome(self, kind: str, body: str, metadata: dict[str, Any] | None = None) -> None:
500
+ output = self._turn_base() / "stages" / "50_execution" / "output"
501
+ output.mkdir(parents=True, exist_ok=True)
502
+ self._write(output / "current.md", body.rstrip() + "\n")
503
+ payload = {"kind": kind}
504
+ if metadata:
505
+ payload.update(metadata)
506
+ self._write(output / "current.json", json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
507
+ self.append_event("EXECUTION_OUTCOME_PUBLISHED", payload)
508
+
509
+ def validate_confirmed_artifact(self, kind: str, artifact_id: str, body: str) -> None:
510
+ meta, workspace_body = self.read_artifact(kind)
511
+ if meta.get("artifact_id") != artifact_id or not meta.get("confirmed"):
512
+ raise WorkspaceError(f"confirmed_artifact_binding:{kind}")
513
+ if workspace_body != body:
514
+ raise WorkspaceError(f"confirmed_artifact_modified:{kind}")
515
+
516
+ def sync_unconfirmed_edit(self, kind: str, artifact_id: str, controller_body: str) -> str:
517
+ output = self._artifact_stage(kind)
518
+ meta, workspace_body = self.read_artifact(kind)
519
+ if meta.get("artifact_id") != artifact_id:
520
+ raise WorkspaceError(f"artifact_identity:{kind}")
521
+ if meta.get("confirmed"):
522
+ if workspace_body != controller_body:
523
+ raise WorkspaceError(f"confirmed_artifact_modified:{kind}")
524
+ return controller_body
525
+ if workspace_body != controller_body:
526
+ versions = output / "versions"
527
+ versions.mkdir(parents=True, exist_ok=True)
528
+ self._write(versions / f"{artifact_id}.md", workspace_body.rstrip() + "\n")
529
+ meta["workspace_edited"] = True
530
+ self._write(output / "current.json", json.dumps(meta, ensure_ascii=False, indent=2) + "\n")
531
+ self.append_event(
532
+ "UNCONFIRMED_ARTIFACT_EDIT_DETECTED",
533
+ {"kind": kind, "artifact_id": artifact_id},
534
+ )
535
+ return workspace_body
536
+ return controller_body
537
+
538
+
539
+ class MemoryWorkspaceRun(WorkspaceRun):
540
+ """Software-defined In-Memory VFS WorkspaceRun (ADR-0011).
541
+
542
+ Eliminates blocking fsync and mkstemp latency on Windows NTFS during
543
+ active turn materialization by maintaining stage symbols in memory buffers
544
+ and performing fast, unjournaled writes to disk without synchronous fsync.
545
+ """
546
+
547
+ def __init__(self, repo_root: Path, path: Path):
548
+ super().__init__(repo_root, path)
549
+ self._vfs: dict[Path, str] = {}
550
+ self._events: list[dict[str, Any]] = []
551
+
552
+ def _write(self, path: Path, content: str) -> None:
553
+ self._vfs[path] = content
554
+ path.parent.mkdir(parents=True, exist_ok=True)
555
+ path.write_text(content, encoding="utf-8", newline="\n")
556
+
557
+ def _read(self, path: Path) -> str:
558
+ if path in self._vfs:
559
+ return self._vfs[path]
560
+ return path.read_text(encoding="utf-8")
561
+
562
+ def _record_event(self, event: dict[str, Any]) -> None:
563
+ self._events.append(event)
564
+ events = self._turn_base() / "events" / "events.jsonl"
565
+ events.parent.mkdir(parents=True, exist_ok=True)
566
+ with events.open("a", encoding="utf-8", newline="\n") as handle:
567
+ handle.write(json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n")
568
+ handle.flush()
569
+ # Bypasses blocking os.fsync(handle.fileno()) per ADR-0011 §1
570
+
571
+ def start_turn(self, new_turn_id: str) -> None:
572
+ super().start_turn(new_turn_id)
573
+ self._events = []
574
+
575
+ def mark_turn_status(self, status: str, *, deliverable_sha256: str | None = None) -> None:
576
+ super().mark_turn_status(status, deliverable_sha256=deliverable_sha256)
577
+ if status in {"CLOSED_SUCCESS", "CLOSED_CANCELLED"}:
578
+ self.flush_turn_archive()
579
+
580
+ def flush_turn_archive(self, turn_id: str | None = None) -> Path:
581
+ """Atomic turn flush: persist turn state and events to a single archive record (ADR-0011 §2)."""
582
+ tid = turn_id or self.turn_id
583
+ if tid:
584
+ turn_dir = self.path / "turns" / tid
585
+ turn_meta = self.read_turn_status(tid)
586
+ status = turn_meta.get("status")
587
+ deliverable_sha256 = turn_meta.get("deliverable_sha256")
588
+ else:
589
+ turn_dir = self.path
590
+ status = self.metadata.get("status")
591
+ deliverable_sha256 = self.metadata.get("deliverable_sha256")
592
+ archive_path = turn_dir / "turn_archive.json"
593
+ archive_data = {
594
+ "turn_id": tid,
595
+ "status": status,
596
+ "deliverable_sha256": deliverable_sha256,
597
+ "events": list(self._events),
598
+ "flushed_at_utc": datetime.now(timezone.utc).isoformat(),
599
+ }
600
+ archive_path.write_text(json.dumps(archive_data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
601
+ return archive_path
602
+
@@ -0,0 +1 @@
1
+ """Tracking-only MLflow tooling."""