prizmkit 1.1.160 → 1.1.161
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/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +9 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +145 -110
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +9 -15
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +8 -5
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +4 -2
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +4 -2
- package/bundled/dev-pipeline/tests/test_runtime_commit.py +170 -66
- package/bundled/dev-pipeline/tests/test_unified_cli.py +172 -4
- package/bundled/rules/prizm/prizm-commit-workflow.md +12 -5
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +5 -5
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +4 -5
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +4 -6
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +1 -4
- package/bundled/skills/prizmkit-committer/SKILL.md +47 -63
- package/bundled/skills/prizmkit-test/SKILL.md +2 -2
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +1 -1
- package/bundled/skills/prizmkit-workflow/SKILL.md +4 -6
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +10 -8
- package/package.json +1 -1
|
@@ -171,16 +171,15 @@ def _write_commit_ready_checkpoint(root: Path) -> tuple[Path, Path]:
|
|
|
171
171
|
return artifact_dir, checkpoint_path
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
def _write_request(root: Path, artifact_dir: Path, base_head: str
|
|
174
|
+
def _write_request(root: Path, artifact_dir: Path, base_head: str) -> Path:
|
|
175
175
|
request_path = artifact_dir / "runtime-commit-request.json"
|
|
176
176
|
request_path.write_text(
|
|
177
177
|
json.dumps(
|
|
178
178
|
{
|
|
179
|
-
"schema_version":
|
|
179
|
+
"schema_version": 2,
|
|
180
180
|
"artifact_dir": artifact_dir.relative_to(root).as_posix(),
|
|
181
181
|
"base_head": base_head,
|
|
182
182
|
"commit_message": "fix(runtime): finalize prepared task",
|
|
183
|
-
"intended_paths": paths,
|
|
184
183
|
}
|
|
185
184
|
),
|
|
186
185
|
encoding="utf-8",
|
|
@@ -188,7 +187,7 @@ def _write_request(root: Path, artifact_dir: Path, base_head: str, paths: list[s
|
|
|
188
187
|
return request_path
|
|
189
188
|
|
|
190
189
|
|
|
191
|
-
def
|
|
190
|
+
def test_runtime_commit_executes_complete_workspace_and_finalizes_only_checkpoint(tmp_path):
|
|
192
191
|
base_head = _init_repo(tmp_path)
|
|
193
192
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
194
193
|
caller_state = tmp_path / ".prizmkit" / "state" / "workflows" / "caller.json"
|
|
@@ -196,7 +195,7 @@ def test_runtime_commit_executes_exact_request_and_finalizes_only_checkpoint(tmp
|
|
|
196
195
|
caller_state.write_text('{"owner":"external-coordinator"}\n', encoding="utf-8")
|
|
197
196
|
expected_caller_state = caller_state.read_bytes()
|
|
198
197
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
199
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
198
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
200
199
|
|
|
201
200
|
result = finalize_runtime_commit(
|
|
202
201
|
tmp_path,
|
|
@@ -229,7 +228,7 @@ def test_runtime_commit_executes_exact_request_and_finalizes_only_checkpoint(tmp
|
|
|
229
228
|
".prizmkit/prizm-docs/src/counter.prizm",
|
|
230
229
|
],
|
|
231
230
|
)
|
|
232
|
-
def
|
|
231
|
+
def test_runtime_commit_includes_tracked_prizmkit_path_from_workspace(tmp_path, durable_path):
|
|
233
232
|
_init_repo(tmp_path)
|
|
234
233
|
durable = tmp_path / durable_path
|
|
235
234
|
durable.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -240,7 +239,7 @@ def test_runtime_commit_admits_tracked_prizmkit_path_in_exact_request(tmp_path,
|
|
|
240
239
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
241
240
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
242
241
|
durable.write_text("after\n", encoding="utf-8")
|
|
243
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
242
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
244
243
|
|
|
245
244
|
result = finalize_runtime_commit(
|
|
246
245
|
tmp_path,
|
|
@@ -261,7 +260,7 @@ def test_runtime_commit_admits_tracked_prizmkit_path_in_exact_request(tmp_path,
|
|
|
261
260
|
assert durable.read_text(encoding="utf-8") == "after\n"
|
|
262
261
|
|
|
263
262
|
|
|
264
|
-
def
|
|
263
|
+
def test_runtime_commit_includes_valid_host_support_paths_from_workspace(tmp_path):
|
|
265
264
|
base_head = _init_repo(tmp_path)
|
|
266
265
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
267
266
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
@@ -269,7 +268,7 @@ def test_runtime_commit_ignores_runner_support_paths_but_leaves_them_unmodified(
|
|
|
269
268
|
codex_config = tmp_path / ".codex" / "config.toml"
|
|
270
269
|
codex_config.parent.mkdir()
|
|
271
270
|
codex_config.write_text("[project]\ntrusted = true\n", encoding="utf-8")
|
|
272
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
271
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
273
272
|
|
|
274
273
|
result = finalize_runtime_commit(
|
|
275
274
|
tmp_path,
|
|
@@ -278,11 +277,11 @@ def test_runtime_commit_ignores_runner_support_paths_but_leaves_them_unmodified(
|
|
|
278
277
|
)
|
|
279
278
|
|
|
280
279
|
assert result.status == "committed"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
assert
|
|
280
|
+
changed = set(
|
|
281
|
+
_git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD").splitlines()
|
|
282
|
+
)
|
|
283
|
+
assert changed == {"tracked.txt", "AGENTS.md", ".codex/config.toml"}
|
|
284
|
+
assert _git(tmp_path, "status", "--porcelain") == ""
|
|
286
285
|
|
|
287
286
|
|
|
288
287
|
def test_runtime_commit_allows_task_path_adjacent_to_internal_namespace(tmp_path):
|
|
@@ -291,7 +290,7 @@ def test_runtime_commit_allows_task_path_adjacent_to_internal_namespace(tmp_path
|
|
|
291
290
|
adjacent_path = tmp_path / ".prizmkitx" / "task.txt"
|
|
292
291
|
adjacent_path.parent.mkdir()
|
|
293
292
|
adjacent_path.write_text("task-owned\n", encoding="utf-8")
|
|
294
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
293
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
295
294
|
|
|
296
295
|
result = finalize_runtime_commit(
|
|
297
296
|
tmp_path,
|
|
@@ -315,7 +314,7 @@ def test_runtime_commit_ignores_project_gitignored_transient_data(tmp_path):
|
|
|
315
314
|
transient = tmp_path / "tmp" / "tool-cache.json"
|
|
316
315
|
transient.parent.mkdir()
|
|
317
316
|
transient.write_text('{"cache":true}\n', encoding="utf-8")
|
|
318
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
317
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
319
318
|
|
|
320
319
|
result = finalize_runtime_commit(
|
|
321
320
|
tmp_path,
|
|
@@ -328,12 +327,12 @@ def test_runtime_commit_ignores_project_gitignored_transient_data(tmp_path):
|
|
|
328
327
|
assert transient.read_text(encoding="utf-8") == '{"cache":true}\n'
|
|
329
328
|
|
|
330
329
|
|
|
331
|
-
def
|
|
330
|
+
def test_runtime_commit_includes_preexisting_git_visible_change_without_manifest(tmp_path):
|
|
332
331
|
base_head = _init_repo(tmp_path)
|
|
333
332
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
334
333
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
335
|
-
(tmp_path / "
|
|
336
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
334
|
+
(tmp_path / "preexisting.txt").write_text("validated existing work\n", encoding="utf-8")
|
|
335
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
337
336
|
|
|
338
337
|
result = finalize_runtime_commit(
|
|
339
338
|
tmp_path,
|
|
@@ -341,16 +340,18 @@ def test_runtime_commit_rejects_unlisted_git_visible_change(tmp_path):
|
|
|
341
340
|
checkpoint_path=checkpoint_path,
|
|
342
341
|
)
|
|
343
342
|
|
|
344
|
-
assert result.status == "
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
assert result.status == "committed"
|
|
344
|
+
changed = set(
|
|
345
|
+
_git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD").splitlines()
|
|
346
|
+
)
|
|
347
|
+
assert changed == {"tracked.txt", "preexisting.txt"}
|
|
347
348
|
|
|
348
349
|
|
|
349
350
|
def test_runtime_commit_recovers_commit_created_before_state_finalization(tmp_path):
|
|
350
351
|
base_head = _init_repo(tmp_path)
|
|
351
352
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
352
353
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
353
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
354
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
354
355
|
_git(tmp_path, "add", "tracked.txt")
|
|
355
356
|
_git(tmp_path, "commit", "-m", "fix(runtime): finalize prepared task")
|
|
356
357
|
committed_head = _git(tmp_path, "rev-parse", "HEAD")
|
|
@@ -367,15 +368,38 @@ def test_runtime_commit_recovers_commit_created_before_state_finalization(tmp_pa
|
|
|
367
368
|
assert _git(tmp_path, "rev-list", "--count", f"{base_head}..HEAD") == "1"
|
|
368
369
|
|
|
369
370
|
|
|
371
|
+
def test_runtime_commit_rejects_recovery_commit_containing_causal_bookkeeping(tmp_path):
|
|
372
|
+
base_head = _init_repo(tmp_path)
|
|
373
|
+
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
374
|
+
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
375
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
376
|
+
_git(
|
|
377
|
+
tmp_path,
|
|
378
|
+
"add",
|
|
379
|
+
"-f",
|
|
380
|
+
"tracked.txt",
|
|
381
|
+
checkpoint_path.relative_to(tmp_path).as_posix(),
|
|
382
|
+
request_path.relative_to(tmp_path).as_posix(),
|
|
383
|
+
)
|
|
384
|
+
_git(tmp_path, "commit", "-m", "fix(runtime): finalize prepared task")
|
|
385
|
+
|
|
386
|
+
result = finalize_runtime_commit(
|
|
387
|
+
tmp_path,
|
|
388
|
+
artifact_dir=artifact_dir,
|
|
389
|
+
checkpoint_path=checkpoint_path,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
assert result.status == "failed"
|
|
393
|
+
assert result.reason == "base_head_mismatch"
|
|
394
|
+
|
|
395
|
+
|
|
370
396
|
def test_runtime_commit_recovers_after_checkpoint_finalization_failure(tmp_path, monkeypatch):
|
|
371
397
|
import prizmkit_runtime.runtime_commit as runtime_commit
|
|
372
398
|
|
|
373
399
|
base_head = _init_repo(tmp_path)
|
|
374
400
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
375
401
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
376
|
-
request_path = _write_request(
|
|
377
|
-
tmp_path, artifact_dir, base_head, ["tracked.txt"]
|
|
378
|
-
)
|
|
402
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
379
403
|
|
|
380
404
|
original_finalize = runtime_commit._finalize_checkpoint
|
|
381
405
|
monkeypatch.setattr(
|
|
@@ -412,9 +436,7 @@ def test_runtime_commit_recovers_cleanup_after_completed_checkpoint(tmp_path, mo
|
|
|
412
436
|
base_head = _init_repo(tmp_path)
|
|
413
437
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
414
438
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
415
|
-
request_path = _write_request(
|
|
416
|
-
tmp_path, artifact_dir, base_head, ["tracked.txt"]
|
|
417
|
-
)
|
|
439
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
418
440
|
|
|
419
441
|
original_unlink = Path.unlink
|
|
420
442
|
|
|
@@ -457,7 +479,7 @@ def test_runtime_commit_disables_repository_hooks(tmp_path):
|
|
|
457
479
|
)
|
|
458
480
|
hook.chmod(0o755)
|
|
459
481
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
460
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
482
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
461
483
|
|
|
462
484
|
result = finalize_runtime_commit(
|
|
463
485
|
tmp_path,
|
|
@@ -479,12 +501,7 @@ def test_runtime_commit_handles_rename_as_exact_delete_and_add(tmp_path):
|
|
|
479
501
|
base_head = _git(tmp_path, "rev-parse", "HEAD")
|
|
480
502
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
481
503
|
old_path.rename(tmp_path / "new-name.txt")
|
|
482
|
-
_write_request(
|
|
483
|
-
tmp_path,
|
|
484
|
-
artifact_dir,
|
|
485
|
-
base_head,
|
|
486
|
-
["old-name.txt", "new-name.txt"],
|
|
487
|
-
)
|
|
504
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
488
505
|
|
|
489
506
|
result = finalize_runtime_commit(
|
|
490
507
|
tmp_path,
|
|
@@ -508,27 +525,116 @@ def test_runtime_commit_handles_rename_as_exact_delete_and_add(tmp_path):
|
|
|
508
525
|
@pytest.mark.parametrize(
|
|
509
526
|
"unsafe_path",
|
|
510
527
|
[
|
|
511
|
-
"../escape.txt",
|
|
512
|
-
".git/config",
|
|
513
528
|
".env",
|
|
514
|
-
"
|
|
515
|
-
"
|
|
516
|
-
".
|
|
517
|
-
".
|
|
518
|
-
".prizmkit/prizm-docs/.env",
|
|
519
|
-
".prizmkit/state/runtime.json",
|
|
520
|
-
".prizmkit/dev-pipeline/cli.py",
|
|
521
|
-
".prizmkit/scripts/helper.py",
|
|
522
|
-
".prizmkit/manifest.json",
|
|
523
|
-
".prizmkit/specs/001-runtime-commit/workflow-checkpoint.json",
|
|
524
|
-
".prizmkit/specs/001-runtime-commit/runtime-commit-request.json",
|
|
529
|
+
"credentials.json",
|
|
530
|
+
"secrets.json",
|
|
531
|
+
"private.pem",
|
|
532
|
+
".codex/settings.local.json",
|
|
525
533
|
],
|
|
526
534
|
)
|
|
527
|
-
def
|
|
535
|
+
def test_runtime_commit_rejects_sensitive_git_visible_workspace_path(tmp_path, unsafe_path):
|
|
536
|
+
base_head = _init_repo(tmp_path)
|
|
537
|
+
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
538
|
+
unsafe = tmp_path / unsafe_path
|
|
539
|
+
unsafe.parent.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
unsafe.write_text("sensitive\n", encoding="utf-8")
|
|
541
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
542
|
+
|
|
543
|
+
result = finalize_runtime_commit(
|
|
544
|
+
tmp_path,
|
|
545
|
+
artifact_dir=artifact_dir,
|
|
546
|
+
checkpoint_path=checkpoint_path,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
assert result.status == "failed"
|
|
550
|
+
assert result.reason == "sensitive_workspace_path"
|
|
551
|
+
assert _git(tmp_path, "rev-parse", "HEAD") == base_head
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def test_runtime_commit_rejects_empty_workspace(tmp_path):
|
|
555
|
+
base_head = _init_repo(tmp_path)
|
|
556
|
+
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
557
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
558
|
+
|
|
559
|
+
result = finalize_runtime_commit(
|
|
560
|
+
tmp_path,
|
|
561
|
+
artifact_dir=artifact_dir,
|
|
562
|
+
checkpoint_path=checkpoint_path,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
assert result.status == "failed"
|
|
566
|
+
assert result.reason == "empty_workspace_change"
|
|
567
|
+
assert _git(tmp_path, "rev-parse", "HEAD") == base_head
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def test_runtime_commit_rejects_unresolved_merge_state(tmp_path):
|
|
571
|
+
_init_repo(tmp_path)
|
|
572
|
+
initial_branch = _git(tmp_path, "branch", "--show-current")
|
|
573
|
+
_git(tmp_path, "checkout", "-b", "other")
|
|
574
|
+
(tmp_path / "tracked.txt").write_text("other\n", encoding="utf-8")
|
|
575
|
+
_git(tmp_path, "add", "tracked.txt")
|
|
576
|
+
_git(tmp_path, "commit", "-m", "other change")
|
|
577
|
+
_git(tmp_path, "checkout", initial_branch)
|
|
578
|
+
(tmp_path / "tracked.txt").write_text("master\n", encoding="utf-8")
|
|
579
|
+
_git(tmp_path, "add", "tracked.txt")
|
|
580
|
+
_git(tmp_path, "commit", "-m", "master change")
|
|
581
|
+
base_head = _git(tmp_path, "rev-parse", "HEAD")
|
|
582
|
+
merged = subprocess.run(
|
|
583
|
+
["git", "merge", "other"],
|
|
584
|
+
cwd=tmp_path,
|
|
585
|
+
stdout=subprocess.PIPE,
|
|
586
|
+
stderr=subprocess.PIPE,
|
|
587
|
+
text=True,
|
|
588
|
+
check=False,
|
|
589
|
+
)
|
|
590
|
+
assert merged.returncode != 0
|
|
591
|
+
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
592
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
593
|
+
|
|
594
|
+
result = finalize_runtime_commit(
|
|
595
|
+
tmp_path,
|
|
596
|
+
artifact_dir=artifact_dir,
|
|
597
|
+
checkpoint_path=checkpoint_path,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
assert result.status == "failed"
|
|
601
|
+
assert result.reason == "unresolved_merge_state"
|
|
602
|
+
assert _git(tmp_path, "rev-parse", "HEAD") == base_head
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def test_runtime_commit_rejects_already_staged_runtime_bookkeeping(tmp_path):
|
|
528
606
|
base_head = _init_repo(tmp_path)
|
|
529
607
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
530
608
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
531
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
609
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
610
|
+
_git(
|
|
611
|
+
tmp_path,
|
|
612
|
+
"add",
|
|
613
|
+
"-f",
|
|
614
|
+
checkpoint_path.relative_to(tmp_path).as_posix(),
|
|
615
|
+
request_path.relative_to(tmp_path).as_posix(),
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
result = finalize_runtime_commit(
|
|
619
|
+
tmp_path,
|
|
620
|
+
artifact_dir=artifact_dir,
|
|
621
|
+
checkpoint_path=checkpoint_path,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
assert result.status == "failed"
|
|
625
|
+
assert result.reason == "runtime_bookkeeping_staged"
|
|
626
|
+
assert _git(tmp_path, "rev-parse", "HEAD") == base_head
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_runtime_commit_rejects_schema_v1_path_manifest(tmp_path):
|
|
630
|
+
base_head = _init_repo(tmp_path)
|
|
631
|
+
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
632
|
+
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
633
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
634
|
+
request = json.loads(request_path.read_text(encoding="utf-8"))
|
|
635
|
+
request["schema_version"] = 1
|
|
636
|
+
request["intended_paths"] = ["tracked.txt"]
|
|
637
|
+
request_path.write_text(json.dumps(request), encoding="utf-8")
|
|
532
638
|
|
|
533
639
|
result = finalize_runtime_commit(
|
|
534
640
|
tmp_path,
|
|
@@ -537,11 +643,11 @@ def test_runtime_commit_rejects_unsafe_intended_paths(tmp_path, unsafe_path):
|
|
|
537
643
|
)
|
|
538
644
|
|
|
539
645
|
assert result.status == "failed"
|
|
540
|
-
assert result.reason == "
|
|
646
|
+
assert result.reason == "commit_request_invalid_schema"
|
|
541
647
|
assert _git(tmp_path, "rev-parse", "HEAD") == base_head
|
|
542
648
|
|
|
543
649
|
|
|
544
|
-
def
|
|
650
|
+
def test_runtime_commit_includes_already_staged_and_unstaged_workspace(tmp_path):
|
|
545
651
|
_init_repo(tmp_path)
|
|
546
652
|
support = tmp_path / "AGENTS.md"
|
|
547
653
|
support.write_text("before\n", encoding="utf-8")
|
|
@@ -552,7 +658,7 @@ def test_runtime_commit_verifies_staged_set_before_commit(tmp_path):
|
|
|
552
658
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
553
659
|
support.write_text("after\n", encoding="utf-8")
|
|
554
660
|
_git(tmp_path, "add", "AGENTS.md")
|
|
555
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
661
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
556
662
|
|
|
557
663
|
result = finalize_runtime_commit(
|
|
558
664
|
tmp_path,
|
|
@@ -560,9 +666,11 @@ def test_runtime_commit_verifies_staged_set_before_commit(tmp_path):
|
|
|
560
666
|
checkpoint_path=checkpoint_path,
|
|
561
667
|
)
|
|
562
668
|
|
|
563
|
-
assert result.status == "
|
|
564
|
-
|
|
565
|
-
|
|
669
|
+
assert result.status == "committed"
|
|
670
|
+
changed = set(
|
|
671
|
+
_git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD").splitlines()
|
|
672
|
+
)
|
|
673
|
+
assert changed == {"AGENTS.md", "tracked.txt"}
|
|
566
674
|
|
|
567
675
|
|
|
568
676
|
def test_runtime_commit_keeps_tracked_post_commit_bookkeeping_out_of_snapshot(tmp_path):
|
|
@@ -570,13 +678,13 @@ def test_runtime_commit_keeps_tracked_post_commit_bookkeeping_out_of_snapshot(tm
|
|
|
570
678
|
|
|
571
679
|
initial_head = _init_repo(tmp_path)
|
|
572
680
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
573
|
-
request_path = _write_request(tmp_path, artifact_dir, initial_head
|
|
681
|
+
request_path = _write_request(tmp_path, artifact_dir, initial_head)
|
|
574
682
|
checkpoint_rel = checkpoint_path.relative_to(tmp_path).as_posix()
|
|
575
683
|
request_rel = request_path.relative_to(tmp_path).as_posix()
|
|
576
684
|
_git(tmp_path, "add", "-f", checkpoint_rel, request_rel)
|
|
577
685
|
_git(tmp_path, "commit", "-m", "track runtime bookkeeping by project policy")
|
|
578
686
|
base_head = _git(tmp_path, "rev-parse", "HEAD")
|
|
579
|
-
_write_request(tmp_path, artifact_dir, base_head
|
|
687
|
+
_write_request(tmp_path, artifact_dir, base_head)
|
|
580
688
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
581
689
|
|
|
582
690
|
result = finalize_runtime_commit(
|
|
@@ -606,9 +714,7 @@ def test_runtime_commit_rejects_caller_orchestration_fields(tmp_path):
|
|
|
606
714
|
base_head = _init_repo(tmp_path)
|
|
607
715
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
608
716
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
609
|
-
request_path = _write_request(
|
|
610
|
-
tmp_path, artifact_dir, base_head, ["tracked.txt"]
|
|
611
|
-
)
|
|
717
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
612
718
|
request = json.loads(request_path.read_text(encoding="utf-8"))
|
|
613
719
|
request["caller_state_path"] = ".runtime/caller-state.json"
|
|
614
720
|
request_path.write_text(json.dumps(request), encoding="utf-8")
|
|
@@ -628,9 +734,7 @@ def test_runtime_commit_rejects_ai_supplied_premature_runtime_hash(tmp_path):
|
|
|
628
734
|
base_head = _init_repo(tmp_path)
|
|
629
735
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
630
736
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
631
|
-
request_path = _write_request(
|
|
632
|
-
tmp_path, artifact_dir, base_head, ["tracked.txt"]
|
|
633
|
-
)
|
|
737
|
+
request_path = _write_request(tmp_path, artifact_dir, base_head)
|
|
634
738
|
request = json.loads(request_path.read_text(encoding="utf-8"))
|
|
635
739
|
request["runtime_commit_hash"] = "a" * 40
|
|
636
740
|
request_path.write_text(json.dumps(request), encoding="utf-8")
|
|
@@ -650,7 +754,7 @@ def test_runtime_commit_rejects_stale_base_head(tmp_path):
|
|
|
650
754
|
base_head = _init_repo(tmp_path)
|
|
651
755
|
artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
|
|
652
756
|
(tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
|
|
653
|
-
_write_request(tmp_path, artifact_dir, "a" * 40
|
|
757
|
+
_write_request(tmp_path, artifact_dir, "a" * 40)
|
|
654
758
|
|
|
655
759
|
result = finalize_runtime_commit(
|
|
656
760
|
tmp_path,
|
|
@@ -1313,8 +1313,9 @@ def test_headless_review_guidance_maps_stage_local_skill_result():
|
|
|
1313
1313
|
|
|
1314
1314
|
skill_text = skill_path.read_text(encoding="utf-8")
|
|
1315
1315
|
assert "the staged diff, and the unstaged diff" in skill_text
|
|
1316
|
-
assert "untracked, deleted, and
|
|
1317
|
-
assert "
|
|
1316
|
+
assert "non-ignored untracked, deleted, renamed, and copied paths" in skill_text
|
|
1317
|
+
assert "Do not maintain excluded-path metadata" in skill_text
|
|
1318
|
+
assert "complete current Git-visible workspace" in skill_text
|
|
1318
1319
|
assert "accepted = 0" in skill_text
|
|
1319
1320
|
assert "ten completed review rounds" in skill_text
|
|
1320
1321
|
assert "## Phase 3: Independent Code Review" in skill_text
|
|
@@ -2391,7 +2392,7 @@ def test_heartbeat_observation_stale_accumulates_by_elapsed_time_and_resets_on_p
|
|
|
2391
2392
|
session_log = tmp_path / "session.log"
|
|
2392
2393
|
progress = tmp_path / "progress.json"
|
|
2393
2394
|
session_log.write_text("initial\n", encoding="utf-8")
|
|
2394
|
-
progress.write_text('{"current_phase":"plan","current_tool":
|
|
2395
|
+
progress.write_text('{"current_phase":"plan","current_tool":null,"message_count":1,"total_tool_calls":1}\n', encoding="utf-8")
|
|
2395
2396
|
config = HeartbeatConfig(
|
|
2396
2397
|
heartbeat_file=tmp_path / "heartbeat.json",
|
|
2397
2398
|
session_log=session_log,
|
|
@@ -2419,6 +2420,72 @@ def test_heartbeat_observation_stale_accumulates_by_elapsed_time_and_resets_on_p
|
|
|
2419
2420
|
assert reset.last_seen_epoch == 1095.0
|
|
2420
2421
|
|
|
2421
2422
|
|
|
2423
|
+
def test_heartbeat_observation_active_tool_resets_stale_until_tool_clears(tmp_path, monkeypatch):
|
|
2424
|
+
import prizmkit_runtime.heartbeat as heartbeat
|
|
2425
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, observe_heartbeat_state
|
|
2426
|
+
|
|
2427
|
+
progress = tmp_path / "progress.json"
|
|
2428
|
+
progress.write_text(
|
|
2429
|
+
'{"current_tool":"bash","message_count":1,"total_tool_calls":1}\n',
|
|
2430
|
+
encoding="utf-8",
|
|
2431
|
+
)
|
|
2432
|
+
config = HeartbeatConfig(
|
|
2433
|
+
heartbeat_file=tmp_path / "heartbeat.json",
|
|
2434
|
+
progress_file=progress,
|
|
2435
|
+
stale_after_seconds=60,
|
|
2436
|
+
)
|
|
2437
|
+
ticks = iter([1000.0, 1100.0, 1200.0, 1205.0, 1230.0])
|
|
2438
|
+
monkeypatch.setattr(heartbeat.time, "time", lambda: next(ticks))
|
|
2439
|
+
|
|
2440
|
+
first = observe_heartbeat_state(config)
|
|
2441
|
+
active = observe_heartbeat_state(config, first)
|
|
2442
|
+
still_active = observe_heartbeat_state(config, active)
|
|
2443
|
+
progress.write_text(
|
|
2444
|
+
'{"current_tool":null,"message_count":1,"total_tool_calls":1}\n',
|
|
2445
|
+
encoding="utf-8",
|
|
2446
|
+
)
|
|
2447
|
+
tool_finished = observe_heartbeat_state(config, still_active)
|
|
2448
|
+
idle = observe_heartbeat_state(config, tool_finished)
|
|
2449
|
+
|
|
2450
|
+
assert active.details == "progress"
|
|
2451
|
+
assert active.stale_seconds == 0
|
|
2452
|
+
assert active.last_seen_epoch == 1100.0
|
|
2453
|
+
assert still_active.details == "progress"
|
|
2454
|
+
assert still_active.stale_seconds == 0
|
|
2455
|
+
assert still_active.last_seen_epoch == 1200.0
|
|
2456
|
+
assert tool_finished.details == "progress"
|
|
2457
|
+
assert tool_finished.stale_seconds == 0
|
|
2458
|
+
assert tool_finished.last_seen_epoch == 1205.0
|
|
2459
|
+
assert idle.details == "no progress"
|
|
2460
|
+
assert idle.stale_seconds == 25.0
|
|
2461
|
+
|
|
2462
|
+
|
|
2463
|
+
@pytest.mark.parametrize("current_tool", [None, "", " ", 0, ["bash"]])
|
|
2464
|
+
def test_heartbeat_observation_rejects_inactive_or_malformed_tool_values(
|
|
2465
|
+
tmp_path,
|
|
2466
|
+
monkeypatch,
|
|
2467
|
+
current_tool,
|
|
2468
|
+
):
|
|
2469
|
+
import prizmkit_runtime.heartbeat as heartbeat
|
|
2470
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, observe_heartbeat_state
|
|
2471
|
+
|
|
2472
|
+
progress = tmp_path / "progress.json"
|
|
2473
|
+
progress.write_text(json.dumps({"current_tool": current_tool}), encoding="utf-8")
|
|
2474
|
+
config = HeartbeatConfig(
|
|
2475
|
+
heartbeat_file=tmp_path / "heartbeat.json",
|
|
2476
|
+
progress_file=progress,
|
|
2477
|
+
stale_after_seconds=60,
|
|
2478
|
+
)
|
|
2479
|
+
ticks = iter([1000.0, 1070.0])
|
|
2480
|
+
monkeypatch.setattr(heartbeat.time, "time", lambda: next(ticks))
|
|
2481
|
+
|
|
2482
|
+
first = observe_heartbeat_state(config)
|
|
2483
|
+
stale = observe_heartbeat_state(config, first)
|
|
2484
|
+
|
|
2485
|
+
assert stale.details == "no progress"
|
|
2486
|
+
assert stale.stale is True
|
|
2487
|
+
assert stale.stale_seconds == 70.0
|
|
2488
|
+
|
|
2422
2489
|
|
|
2423
2490
|
def test_heartbeat_state_tracks_progress_json_child_activity(tmp_path, monkeypatch):
|
|
2424
2491
|
import prizmkit_runtime.heartbeat as heartbeat
|
|
@@ -2495,6 +2562,104 @@ def test_heartbeat_monitor_stale_kill_uses_elapsed_no_progress_threshold(tmp_pat
|
|
|
2495
2562
|
assert marker_data["stale_seconds"] >= 0
|
|
2496
2563
|
|
|
2497
2564
|
|
|
2565
|
+
def test_heartbeat_monitor_active_tool_defers_stale_kill_until_tool_clears(tmp_path):
|
|
2566
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, HeartbeatMonitor
|
|
2567
|
+
from prizmkit_runtime.processes import ProcessSpec, is_process_running, launch_process
|
|
2568
|
+
|
|
2569
|
+
sleeper = tmp_path / "active_tool_sleep.py"
|
|
2570
|
+
sleeper.write_text("import time\ntime.sleep(5)\n", encoding="utf-8")
|
|
2571
|
+
progress = tmp_path / "progress.json"
|
|
2572
|
+
progress.write_text('{"current_tool":"bash"}\n', encoding="utf-8")
|
|
2573
|
+
handle = launch_process(ProcessSpec(command=(sys.executable, str(sleeper)), cwd=tmp_path))
|
|
2574
|
+
monitor = HeartbeatMonitor(
|
|
2575
|
+
handle,
|
|
2576
|
+
HeartbeatConfig(
|
|
2577
|
+
heartbeat_file=tmp_path / "heartbeat.json",
|
|
2578
|
+
session_log=tmp_path / "session.log",
|
|
2579
|
+
progress_file=progress,
|
|
2580
|
+
marker_dir=tmp_path,
|
|
2581
|
+
interval_seconds=0.05,
|
|
2582
|
+
stale_after_seconds=0.1,
|
|
2583
|
+
kill_after_seconds=0.2,
|
|
2584
|
+
grace_seconds=0.1,
|
|
2585
|
+
),
|
|
2586
|
+
)
|
|
2587
|
+
monitor.start()
|
|
2588
|
+
|
|
2589
|
+
time.sleep(0.5)
|
|
2590
|
+
assert is_process_running(handle)
|
|
2591
|
+
assert not (tmp_path / "stale-kill.json").exists()
|
|
2592
|
+
|
|
2593
|
+
progress.write_text('{"current_tool":null}\n', encoding="utf-8")
|
|
2594
|
+
deadline = time.time() + 3
|
|
2595
|
+
while time.time() < deadline and not (tmp_path / "stale-kill.json").exists():
|
|
2596
|
+
time.sleep(0.05)
|
|
2597
|
+
result = monitor.stop()
|
|
2598
|
+
|
|
2599
|
+
marker = json.loads((tmp_path / "stale-kill.json").read_text(encoding="utf-8"))
|
|
2600
|
+
assert result.killed is True
|
|
2601
|
+
assert result.reason == "stale_session"
|
|
2602
|
+
assert marker["reason"] == "stale_session"
|
|
2603
|
+
|
|
2604
|
+
|
|
2605
|
+
def test_pi_session_active_tool_stream_survives_stale_threshold(tmp_path, monkeypatch):
|
|
2606
|
+
import prizmkit_runtime.sessions as sessions
|
|
2607
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
2608
|
+
|
|
2609
|
+
parser_classes = sessions._load_stream_parser_classes()
|
|
2610
|
+
monkeypatch.setattr(sessions, "_load_stream_parser_classes", lambda: parser_classes)
|
|
2611
|
+
|
|
2612
|
+
fake_cli = tmp_path / "pi_active_tool_cli.py"
|
|
2613
|
+
fake_cli.write_text(
|
|
2614
|
+
"#!/usr/bin/env python3\n"
|
|
2615
|
+
"import json, sys, time\n"
|
|
2616
|
+
"_prompt = sys.stdin.read(5)\n"
|
|
2617
|
+
"for event in [\n"
|
|
2618
|
+
" {'type':'session','id':'active-tool-session'},\n"
|
|
2619
|
+
" {'type':'agent_start'},\n"
|
|
2620
|
+
" {'type':'turn_start'},\n"
|
|
2621
|
+
" {'type':'tool_execution_start','toolCallId':'call-1','toolName':'long-running-tool','args':{}},\n"
|
|
2622
|
+
"]:\n"
|
|
2623
|
+
" print(json.dumps(event), flush=True)\n"
|
|
2624
|
+
"time.sleep(3.0)\n"
|
|
2625
|
+
"for event in [\n"
|
|
2626
|
+
" {'type':'tool_execution_end','toolCallId':'call-1','toolName':'long-running-tool','result':{'content':[]},'isError':False},\n"
|
|
2627
|
+
" {'type':'message_end','message':{'role':'assistant','content':[{'type':'text','text':'done'}],'stopReason':'stop'}},\n"
|
|
2628
|
+
" {'type':'agent_end','willRetry':False,'messages':[{'role':'assistant','content':[{'type':'text','text':'done'}],'stopReason':'stop'}]},\n"
|
|
2629
|
+
"]:\n"
|
|
2630
|
+
" print(json.dumps(event), flush=True)\n",
|
|
2631
|
+
encoding="utf-8",
|
|
2632
|
+
)
|
|
2633
|
+
fake_cli.chmod(0o755)
|
|
2634
|
+
prompt = tmp_path / "prompt.md"
|
|
2635
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
2636
|
+
session_log = tmp_path / "state" / "sessions" / "active-tool" / "logs" / "session.log"
|
|
2637
|
+
progress = session_log.with_name("progress.json")
|
|
2638
|
+
|
|
2639
|
+
result = AISessionLauncher(
|
|
2640
|
+
AISessionConfig(
|
|
2641
|
+
cli_command=str(fake_cli),
|
|
2642
|
+
platform="pi",
|
|
2643
|
+
model=None,
|
|
2644
|
+
prompt_path=prompt,
|
|
2645
|
+
cwd=tmp_path,
|
|
2646
|
+
log_path=session_log,
|
|
2647
|
+
progress_path=progress,
|
|
2648
|
+
heartbeat_path=session_log.with_name("heartbeat.json"),
|
|
2649
|
+
stale_kill_threshold_seconds=1.5,
|
|
2650
|
+
heartbeat_stale_threshold_seconds=1.0,
|
|
2651
|
+
stale_kill_grace_seconds=0.1,
|
|
2652
|
+
live_progress_interval_seconds=0.05,
|
|
2653
|
+
)
|
|
2654
|
+
).run()
|
|
2655
|
+
|
|
2656
|
+
final_progress = json.loads(progress.read_text(encoding="utf-8"))
|
|
2657
|
+
assert result.exit_code == 0
|
|
2658
|
+
assert result.termination_reason is None
|
|
2659
|
+
assert final_progress["current_tool"] is None
|
|
2660
|
+
assert not (session_log.parent / "stale-kill.json").exists()
|
|
2661
|
+
|
|
2662
|
+
|
|
2498
2663
|
def test_stale_marker_preserves_subsecond_threshold(tmp_path):
|
|
2499
2664
|
from prizmkit_runtime.heartbeat import write_stale_marker
|
|
2500
2665
|
|
|
@@ -2913,7 +3078,10 @@ def test_heartbeat_fatal_error_writes_shell_compatible_markers(tmp_path):
|
|
|
2913
3078
|
sleeper = tmp_path / "fatal_sleep.py"
|
|
2914
3079
|
sleeper.write_text("import time\ntime.sleep(5)\n", encoding="utf-8")
|
|
2915
3080
|
progress = tmp_path / "progress.json"
|
|
2916
|
-
progress.write_text(
|
|
3081
|
+
progress.write_text(
|
|
3082
|
+
'{"current_tool":"bash","fatal_error_code":"context_overflow"}\n',
|
|
3083
|
+
encoding="utf-8",
|
|
3084
|
+
)
|
|
2917
3085
|
handle = launch_process(ProcessSpec(command=(sys.executable, str(sleeper)), cwd=tmp_path))
|
|
2918
3086
|
monitor = HeartbeatMonitor(
|
|
2919
3087
|
handle,
|