claude-dev-env 8.26.6 → 8.28.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/.agents/skills/orchestrator/SKILL.md +125 -329
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +29 -57
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +26 -57
- package/.agents/skills/orchestrator/reference/host-detect.md +14 -11
- package/.agents/skills/orchestrator/reference/platform-evidence.md +49 -0
- package/.agents/skills/orchestrator/reference/recovery.md +95 -0
- package/.agents/skills/orchestrator/reference/run-state.md +113 -0
- package/.agents/skills/orchestrator/reference/scheduling.md +67 -0
- package/.agents/skills/orchestrator/scripts/status_gate.py +59 -27
- package/.agents/skills/orchestrator/scripts/status_gate_constants/config/constants.py +1 -0
- package/.agents/skills/orchestrator/scripts/test_status_gate.py +165 -0
- package/.agents/skills/orchestrator-refresh/SKILL.md +43 -114
- package/bin/export-pstack-skills.mjs +71 -0
- package/bin/export-pstack-skills.test.mjs +64 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +4 -3
- package/package.json +1 -1
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +14 -0
- package/scripts/grok_run_ledger.py +96 -3
- package/scripts/test_grok_run_ledger.py +373 -0
|
@@ -48,6 +48,7 @@ from status_gate_constants.config.constants import (
|
|
|
48
48
|
REASON_REARM_ALREADY_PENDING,
|
|
49
49
|
REASON_REARM_SLOT_CLAIMED,
|
|
50
50
|
REASON_REARM_SLOT_RELEASED,
|
|
51
|
+
REASON_RUN_SLUG_MISMATCH,
|
|
51
52
|
REASON_STATUS_NOT_ACTIVE,
|
|
52
53
|
RESCHEDULE_FIELD_NAME,
|
|
53
54
|
RUN_SLUG_ENV_VAR,
|
|
@@ -62,7 +63,6 @@ from status_gate_constants.config.constants import (
|
|
|
62
63
|
UTF8_ENCODING,
|
|
63
64
|
)
|
|
64
65
|
|
|
65
|
-
|
|
66
66
|
class StatusFilePayload(TypedDict, total=False):
|
|
67
67
|
"""On-disk orchestrator run status JSON shape."""
|
|
68
68
|
|
|
@@ -192,10 +192,12 @@ def write_status_file(
|
|
|
192
192
|
The payload written to disk.
|
|
193
193
|
|
|
194
194
|
Raises:
|
|
195
|
-
ValueError: When
|
|
195
|
+
ValueError: When status is invalid or scoped existing state is unowned.
|
|
196
196
|
"""
|
|
197
197
|
if run_status not in ALL_VALID_RUN_STATUSES:
|
|
198
198
|
raise ValueError(f"invalid status: {run_status}")
|
|
199
|
+
if run_slug:
|
|
200
|
+
_require_status_file_owner(status_file_path, run_slug)
|
|
199
201
|
all_status_fields = _encode_status_file_payload(
|
|
200
202
|
run_status=run_status,
|
|
201
203
|
run_slug=run_slug,
|
|
@@ -229,11 +231,13 @@ def _atomic_write_payload(
|
|
|
229
231
|
|
|
230
232
|
def _load_status_payload(
|
|
231
233
|
status_file_path: Path,
|
|
234
|
+
run_slug: str = "",
|
|
232
235
|
) -> tuple[StatusFilePayload | None, str | None]:
|
|
233
236
|
"""Load and validate the status file payload.
|
|
234
237
|
|
|
235
238
|
Args:
|
|
236
239
|
status_file_path: Path to the run status file.
|
|
240
|
+
run_slug: Expected owner when nonempty; empty accepts legacy state.
|
|
237
241
|
|
|
238
242
|
Returns:
|
|
239
243
|
``(payload, None)`` on success, or ``(None, reason_code)`` on failure.
|
|
@@ -249,9 +253,18 @@ def _load_status_payload(
|
|
|
249
253
|
all_status_fields = _decode_status_file_payload(loaded_payload)
|
|
250
254
|
if all_status_fields is None:
|
|
251
255
|
return None, REASON_INVALID_STATUS_FILE
|
|
256
|
+
if run_slug and all_status_fields.get(RUN_SLUG_FIELD_NAME) != run_slug:
|
|
257
|
+
return None, REASON_RUN_SLUG_MISMATCH
|
|
252
258
|
return all_status_fields, None
|
|
253
259
|
|
|
254
260
|
|
|
261
|
+
def _require_status_file_owner(status_file_path: Path, run_slug: str) -> None:
|
|
262
|
+
"""Require matching scope before overwriting an existing status file."""
|
|
263
|
+
_existing_fields, failure_reason = _load_status_payload(status_file_path, run_slug)
|
|
264
|
+
if failure_reason is not None and failure_reason != REASON_MISSING_STATUS_FILE:
|
|
265
|
+
raise ValueError(failure_reason)
|
|
266
|
+
|
|
267
|
+
|
|
255
268
|
def _is_rearm_pending(all_status_fields: StatusFilePayload) -> bool:
|
|
256
269
|
"""Return whether the payload marks a re-arm slot as pending.
|
|
257
270
|
|
|
@@ -285,17 +298,21 @@ def _status_file_run_slug(
|
|
|
285
298
|
|
|
286
299
|
def _load_active_status(
|
|
287
300
|
status_file_path: Path,
|
|
301
|
+
run_slug: str = "",
|
|
288
302
|
) -> tuple[StatusFilePayload | None, str | None]:
|
|
289
303
|
"""Load the status payload and require an active run.
|
|
290
304
|
|
|
291
305
|
Args:
|
|
292
306
|
status_file_path: Path to the run status file.
|
|
307
|
+
run_slug: Expected owner when nonempty; empty accepts legacy state.
|
|
293
308
|
|
|
294
309
|
Returns:
|
|
295
310
|
``(payload, None)`` when the file loads and status is ``active``;
|
|
296
311
|
``(None, reason_code)`` on a missing/invalid file or inactive status.
|
|
297
312
|
"""
|
|
298
|
-
all_status_fields, load_failure_reason = _load_status_payload(
|
|
313
|
+
all_status_fields, load_failure_reason = _load_status_payload(
|
|
314
|
+
status_file_path, run_slug
|
|
315
|
+
)
|
|
299
316
|
if all_status_fields is None:
|
|
300
317
|
return None, load_failure_reason
|
|
301
318
|
if all_status_fields.get(STATUS_FIELD_NAME) != RUN_STATUS_ACTIVE:
|
|
@@ -303,17 +320,22 @@ def _load_active_status(
|
|
|
303
320
|
return all_status_fields, None
|
|
304
321
|
|
|
305
322
|
|
|
306
|
-
def decide_should_reschedule(
|
|
323
|
+
def decide_should_reschedule(
|
|
324
|
+
status_file_path: Path, run_slug: str = ""
|
|
325
|
+
) -> tuple[bool, str]:
|
|
307
326
|
"""Decide whether the orchestrator refresh loop may re-arm.
|
|
308
327
|
|
|
309
328
|
Args:
|
|
310
329
|
status_file_path: Path to the run status file.
|
|
330
|
+
run_slug: Expected owner when nonempty; empty accepts legacy state.
|
|
311
331
|
|
|
312
332
|
Returns:
|
|
313
333
|
``(is_reschedule_allowed, reason_code)``. Fail closed on missing/invalid
|
|
314
334
|
status, inactive status, or an already-pending re-arm slot.
|
|
315
335
|
"""
|
|
316
|
-
all_status_fields, load_failure_reason = _load_active_status(
|
|
336
|
+
all_status_fields, load_failure_reason = _load_active_status(
|
|
337
|
+
status_file_path, run_slug
|
|
338
|
+
)
|
|
317
339
|
if all_status_fields is None:
|
|
318
340
|
assert load_failure_reason is not None
|
|
319
341
|
return False, load_failure_reason
|
|
@@ -336,14 +358,16 @@ def _apply_rearm_latch(
|
|
|
336
358
|
|
|
337
359
|
Args:
|
|
338
360
|
status_file_path: Path to the run status file.
|
|
339
|
-
run_slug:
|
|
361
|
+
run_slug: Expected owner when nonempty; empty preserves the stored owner.
|
|
340
362
|
is_rearm_pending: Latch value to write.
|
|
341
363
|
success_reason: Reason code returned when the rewrite lands.
|
|
342
364
|
|
|
343
365
|
Returns:
|
|
344
366
|
``(is_applied, reason_code, payload_or_none)``.
|
|
345
367
|
"""
|
|
346
|
-
all_status_fields, load_failure_reason = _load_active_status(
|
|
368
|
+
all_status_fields, load_failure_reason = _load_active_status(
|
|
369
|
+
status_file_path, run_slug
|
|
370
|
+
)
|
|
347
371
|
if all_status_fields is None:
|
|
348
372
|
assert load_failure_reason is not None
|
|
349
373
|
return False, load_failure_reason, None
|
|
@@ -366,7 +390,7 @@ def begin_firing(
|
|
|
366
390
|
|
|
367
391
|
Args:
|
|
368
392
|
status_file_path: Path to the run status file.
|
|
369
|
-
run_slug:
|
|
393
|
+
run_slug: Expected owner when nonempty; empty preserves the stored owner.
|
|
370
394
|
|
|
371
395
|
Returns:
|
|
372
396
|
``(is_allowed, reason_code, payload_or_none)``.
|
|
@@ -387,7 +411,7 @@ def claim_rearm_slot(
|
|
|
387
411
|
|
|
388
412
|
Args:
|
|
389
413
|
status_file_path: Path to the run status file.
|
|
390
|
-
run_slug:
|
|
414
|
+
run_slug: Expected owner when nonempty; empty preserves the stored owner.
|
|
391
415
|
|
|
392
416
|
Returns:
|
|
393
417
|
``(is_claimed, reason_code, payload_or_none)``.
|
|
@@ -408,7 +432,7 @@ def release_rearm_slot(
|
|
|
408
432
|
|
|
409
433
|
Args:
|
|
410
434
|
status_file_path: Path to the run status file.
|
|
411
|
-
run_slug:
|
|
435
|
+
run_slug: Expected owner when nonempty; empty preserves the stored owner.
|
|
412
436
|
|
|
413
437
|
Returns:
|
|
414
438
|
``(is_released, reason_code, payload_or_none)``.
|
|
@@ -511,22 +535,26 @@ def _run_set_command(
|
|
|
511
535
|
Process exit code.
|
|
512
536
|
"""
|
|
513
537
|
status_file_path = resolve_status_file_path(status_file, None, run_slug)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
written_status = write_status_file(
|
|
525
|
-
status_file_path=status_file_path,
|
|
526
|
-
run_status=run_status,
|
|
527
|
-
run_slug=run_slug,
|
|
528
|
-
is_rearm_pending=is_rearm_pending,
|
|
538
|
+
all_existing_fields, failure_reason = _load_status_payload(
|
|
539
|
+
status_file_path, run_slug
|
|
540
|
+
)
|
|
541
|
+
if run_slug and failure_reason not in (None, REASON_MISSING_STATUS_FILE):
|
|
542
|
+
return _report_and_exit(False, failure_reason, status_file_path)
|
|
543
|
+
is_rearm_pending = (
|
|
544
|
+
run_status == RUN_STATUS_ACTIVE
|
|
545
|
+
and all_existing_fields is not None
|
|
546
|
+
and all_existing_fields.get(STATUS_FIELD_NAME) == RUN_STATUS_ACTIVE
|
|
547
|
+
and _is_rearm_pending(all_existing_fields)
|
|
529
548
|
)
|
|
549
|
+
try:
|
|
550
|
+
written_status = write_status_file(
|
|
551
|
+
status_file_path=status_file_path,
|
|
552
|
+
run_status=run_status,
|
|
553
|
+
run_slug=run_slug,
|
|
554
|
+
is_rearm_pending=is_rearm_pending,
|
|
555
|
+
)
|
|
556
|
+
except ValueError as error:
|
|
557
|
+
return _report_and_exit(False, str(error), status_file_path)
|
|
530
558
|
print(json.dumps(written_status, indent=JSON_INDENT_SPACES))
|
|
531
559
|
return EXIT_CODE_SUCCESS
|
|
532
560
|
|
|
@@ -566,7 +594,9 @@ def _run_should_reschedule_command(
|
|
|
566
594
|
Exit 0 when active and free; exit 1 when the loop must not re-arm.
|
|
567
595
|
"""
|
|
568
596
|
status_file_path = resolve_status_file_path(status_file, None, run_slug)
|
|
569
|
-
is_reschedule_allowed, reason_code = decide_should_reschedule(
|
|
597
|
+
is_reschedule_allowed, reason_code = decide_should_reschedule(
|
|
598
|
+
status_file_path, run_slug
|
|
599
|
+
)
|
|
570
600
|
return _report_and_exit(is_reschedule_allowed, reason_code, status_file_path)
|
|
571
601
|
|
|
572
602
|
|
|
@@ -597,7 +627,9 @@ def main() -> int:
|
|
|
597
627
|
Process exit code.
|
|
598
628
|
"""
|
|
599
629
|
parsed_arguments = _build_argument_parser().parse_args()
|
|
600
|
-
selected_run_slug = parsed_arguments.run_slug or
|
|
630
|
+
selected_run_slug = parsed_arguments.run_slug or os.environ.get(
|
|
631
|
+
RUN_SLUG_ENV_VAR, ""
|
|
632
|
+
)
|
|
601
633
|
if parsed_arguments.command == COMMAND_SET:
|
|
602
634
|
return _run_set_command(
|
|
603
635
|
status_file=parsed_arguments.status_file,
|
|
@@ -36,6 +36,7 @@ EXIT_CODE_STOP = 1
|
|
|
36
36
|
|
|
37
37
|
REASON_MISSING_STATUS_FILE = "missing_status_file"
|
|
38
38
|
REASON_INVALID_STATUS_FILE = "invalid_status_file"
|
|
39
|
+
REASON_RUN_SLUG_MISMATCH = "run_slug_mismatch"
|
|
39
40
|
REASON_STATUS_NOT_ACTIVE = "status_not_active"
|
|
40
41
|
REASON_ACTIVE = "active"
|
|
41
42
|
REASON_REARM_ALREADY_PENDING = "rearm_already_pending"
|
|
@@ -437,3 +437,168 @@ class TestResolveAndCli:
|
|
|
437
437
|
@pytest.fixture
|
|
438
438
|
def temporary_directory(tmp_path: Path) -> Path:
|
|
439
439
|
return tmp_path
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@pytest.mark.parametrize("stored_slug", ["owner-a", ""])
|
|
443
|
+
@pytest.mark.parametrize(
|
|
444
|
+
"run_status,is_rearm_pending",
|
|
445
|
+
[("active", False), ("active", True), ("done", False)],
|
|
446
|
+
)
|
|
447
|
+
@pytest.mark.parametrize(
|
|
448
|
+
"operation",
|
|
449
|
+
[
|
|
450
|
+
"decide_should_reschedule",
|
|
451
|
+
"begin_firing",
|
|
452
|
+
"claim_rearm_slot",
|
|
453
|
+
"release_rearm_slot",
|
|
454
|
+
],
|
|
455
|
+
)
|
|
456
|
+
def test_scoped_api_rejects_foreign_or_missing_owner(
|
|
457
|
+
tmp_path: Path,
|
|
458
|
+
stored_slug: str,
|
|
459
|
+
operation: str,
|
|
460
|
+
run_status: str,
|
|
461
|
+
is_rearm_pending: bool,
|
|
462
|
+
) -> None:
|
|
463
|
+
status_gate = load_status_gate_module()
|
|
464
|
+
status_path = tmp_path / "status.json"
|
|
465
|
+
status_gate.write_status_file(
|
|
466
|
+
status_path, run_status, stored_slug, is_rearm_pending=is_rearm_pending
|
|
467
|
+
)
|
|
468
|
+
previous_bytes = status_path.read_bytes()
|
|
469
|
+
decision = getattr(status_gate, operation)(status_path, run_slug="caller-b")
|
|
470
|
+
assert decision[:2] == (False, "run_slug_mismatch")
|
|
471
|
+
assert status_path.read_bytes() == previous_bytes
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@pytest.mark.parametrize(
|
|
475
|
+
"stored_text",
|
|
476
|
+
['{"status":"active","run_slug":"owner-a"}', '{"status":"active"}', "{broken"],
|
|
477
|
+
)
|
|
478
|
+
def test_scoped_write_rejects_unowned_existing_file(
|
|
479
|
+
tmp_path: Path, stored_text: str
|
|
480
|
+
) -> None:
|
|
481
|
+
status_gate = load_status_gate_module()
|
|
482
|
+
status_path = tmp_path / "status.json"
|
|
483
|
+
status_path.write_text(stored_text, encoding="utf-8")
|
|
484
|
+
previous_bytes = status_path.read_bytes()
|
|
485
|
+
with pytest.raises(ValueError):
|
|
486
|
+
status_gate.write_status_file(
|
|
487
|
+
status_path, "done", "caller-b", is_rearm_pending=False
|
|
488
|
+
)
|
|
489
|
+
assert status_path.read_bytes() == previous_bytes
|
|
490
|
+
assert sorted(each_path.name for each_path in tmp_path.iterdir()) == ["status.json"]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@pytest.mark.parametrize(
|
|
494
|
+
"command",
|
|
495
|
+
["set", "should-reschedule", "begin-firing", "claim-rearm", "release-rearm"],
|
|
496
|
+
)
|
|
497
|
+
@pytest.mark.parametrize("scope_source", ["argument", "environment", "empty_argument"])
|
|
498
|
+
def test_scoped_cli_rejects_other_run(
|
|
499
|
+
tmp_path: Path,
|
|
500
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
501
|
+
capsys: pytest.CaptureFixture[str],
|
|
502
|
+
command: str,
|
|
503
|
+
scope_source: str,
|
|
504
|
+
) -> None:
|
|
505
|
+
status_gate = load_status_gate_module()
|
|
506
|
+
status_path = tmp_path / "status.json"
|
|
507
|
+
status_gate.write_status_file(
|
|
508
|
+
status_path, "active", "owner-a", is_rearm_pending=False
|
|
509
|
+
)
|
|
510
|
+
previous_bytes = status_path.read_bytes()
|
|
511
|
+
all_arguments = ["status_gate.py", command, "--status-file", str(status_path)]
|
|
512
|
+
if command == "set":
|
|
513
|
+
all_arguments.extend(["--status", "done"])
|
|
514
|
+
if scope_source == "argument":
|
|
515
|
+
all_arguments.extend(["--run-slug", "caller-b"])
|
|
516
|
+
if scope_source in ("environment", "empty_argument"):
|
|
517
|
+
monkeypatch.setenv("ORCHESTRATOR_RUN_SLUG", "caller-b")
|
|
518
|
+
if scope_source == "empty_argument":
|
|
519
|
+
all_arguments.extend(["--run-slug", ""])
|
|
520
|
+
monkeypatch.setattr(sys, "argv", all_arguments)
|
|
521
|
+
assert status_gate.main() == EXIT_CODE_STOP
|
|
522
|
+
assert json.loads(capsys.readouterr().out)["reason"] == "run_slug_mismatch"
|
|
523
|
+
assert status_path.read_bytes() == previous_bytes
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@pytest.mark.parametrize("run_status", ["active", "done"])
|
|
527
|
+
def test_scoped_cli_set_rejects_malformed_file(
|
|
528
|
+
tmp_path: Path,
|
|
529
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
530
|
+
capsys: pytest.CaptureFixture[str],
|
|
531
|
+
run_status: str,
|
|
532
|
+
) -> None:
|
|
533
|
+
status_gate = load_status_gate_module()
|
|
534
|
+
status_path = tmp_path / "status.json"
|
|
535
|
+
status_path.write_text("{broken", encoding="utf-8")
|
|
536
|
+
monkeypatch.setattr(
|
|
537
|
+
sys,
|
|
538
|
+
"argv",
|
|
539
|
+
[
|
|
540
|
+
"status_gate.py",
|
|
541
|
+
"set",
|
|
542
|
+
"--status",
|
|
543
|
+
run_status,
|
|
544
|
+
"--status-file",
|
|
545
|
+
str(status_path),
|
|
546
|
+
"--run-slug",
|
|
547
|
+
"scope",
|
|
548
|
+
],
|
|
549
|
+
)
|
|
550
|
+
assert status_gate.main() == EXIT_CODE_STOP
|
|
551
|
+
assert json.loads(capsys.readouterr().out)["reason"] == "invalid_status_file"
|
|
552
|
+
assert status_path.read_text(encoding="utf-8") == "{broken"
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_environment_scope_initializes_and_runs_matching_lifecycle(
|
|
556
|
+
tmp_path: Path,
|
|
557
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
558
|
+
) -> None:
|
|
559
|
+
status_gate = load_status_gate_module()
|
|
560
|
+
monkeypatch.chdir(tmp_path)
|
|
561
|
+
monkeypatch.delenv(STATUS_FILE_ENV_VAR, raising=False)
|
|
562
|
+
monkeypatch.setenv("ORCHESTRATOR_RUN_SLUG", "scope")
|
|
563
|
+
monkeypatch.setattr(sys, "argv", ["status_gate.py", "set", "--status", "active"])
|
|
564
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
565
|
+
status_path = status_gate.resolve_status_file_path(None, tmp_path, "scope")
|
|
566
|
+
assert json.loads(status_path.read_text(encoding="utf-8"))["run_slug"] == "scope"
|
|
567
|
+
for each_command in [
|
|
568
|
+
"should-reschedule",
|
|
569
|
+
"claim-rearm",
|
|
570
|
+
"begin-firing",
|
|
571
|
+
"claim-rearm",
|
|
572
|
+
"release-rearm",
|
|
573
|
+
]:
|
|
574
|
+
monkeypatch.setattr(sys, "argv", ["status_gate.py", each_command])
|
|
575
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
576
|
+
assert status_gate.decide_should_reschedule(status_path, run_slug="scope") == (
|
|
577
|
+
True,
|
|
578
|
+
"active",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def test_cli_argument_scope_overrides_environment(
|
|
583
|
+
tmp_path: Path,
|
|
584
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
585
|
+
) -> None:
|
|
586
|
+
status_gate = load_status_gate_module()
|
|
587
|
+
status_path = tmp_path / "status.json"
|
|
588
|
+
monkeypatch.setenv("ORCHESTRATOR_RUN_SLUG", "environment")
|
|
589
|
+
monkeypatch.setattr(
|
|
590
|
+
sys,
|
|
591
|
+
"argv",
|
|
592
|
+
[
|
|
593
|
+
"status_gate.py",
|
|
594
|
+
"set",
|
|
595
|
+
"--status",
|
|
596
|
+
"active",
|
|
597
|
+
"--status-file",
|
|
598
|
+
str(status_path),
|
|
599
|
+
"--run-slug",
|
|
600
|
+
"argument",
|
|
601
|
+
],
|
|
602
|
+
)
|
|
603
|
+
assert status_gate.main() == EXIT_CODE_SUCCESS
|
|
604
|
+
assert json.loads(status_path.read_text(encoding="utf-8"))["run_slug"] == "argument"
|
|
@@ -1,135 +1,64 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orchestrator-refresh
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Recover an existing orchestrator's goals, task ownership, and next actions.
|
|
5
|
+
Triggers: /orchestrator-refresh, refresh the orchestrator, resume coordination,
|
|
6
|
+
recover after compaction, recover an orchestration run.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
#
|
|
9
|
+
# Refresh an orchestrator
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
[`../orchestrator/reference/host-detect.md`](../orchestrator/reference/host-detect.md)).
|
|
13
|
-
Re-assert the discipline for that host only.
|
|
11
|
+
## Principle
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Recover the ordinary-agent role in [orchestrator](../orchestrator/SKILL.md).
|
|
14
|
+
This session is the advisor. The parent remains responsible for its own tasks and every open user goal.
|
|
15
|
+
An ordinary refresh works without a scheduler or a gate status file.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Gotchas
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
19
|
+
- A supplied run locator selects one root. Another root's latest checkpoint cannot replace it.
|
|
20
|
+
- Unknown liveness leaves ownership intact until evidence supports a takeover.
|
|
21
|
+
- A missing scheduling gate stops re-arming. It does not prove that the user's goals are complete.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## When this applies
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Use for a manual refresh, lost context, a handoff, or a supported wake for an existing run.
|
|
26
|
+
Keep current authorization and configured model routing when restoring work.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
python <status_gate.py> begin-firing [--run-slug SLUG]
|
|
29
|
-
```
|
|
28
|
+
## Process
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## The refresh never interrupts the run
|
|
48
|
-
|
|
49
|
-
A refresh firing reinforces discipline alongside work already in flight.
|
|
50
|
-
It never pauses, cancels, or waits on a running executor. Reconcile the
|
|
51
|
-
ledger, re-assert the routing, re-arm once, and hand control straight back
|
|
52
|
-
to the work in progress.
|
|
53
|
-
|
|
54
|
-
Inside the re-arm protocol (step 6), every "stop" ends the *re-arm* and
|
|
55
|
-
nothing else. A `should-reschedule` exit 1 means no schedule is created this
|
|
56
|
-
firing, and a `claim-rearm` exit 1 means the schedule just created is
|
|
57
|
-
cancelled; either way the session keeps orchestrating in the same turn.
|
|
58
|
-
|
|
59
|
-
Two stops end the whole firing, and both leave running executors alone:
|
|
60
|
-
`begin-firing` exit 1 (step 0a) and the done branch (step 0b). Each means the
|
|
61
|
-
run is finished, not active, or has no readable status file, so the refresh
|
|
62
|
-
reports and adds nothing further.
|
|
63
|
-
|
|
64
|
-
## Discipline steps
|
|
65
|
-
|
|
66
|
-
1. **Reconcile the task ledger first.** Read the ledger after the gate.
|
|
67
|
-
Use `TaskList` when the host exposes task tools, and the file-backed
|
|
68
|
-
`scripts/grok_run_ledger.py` ledger the orchestrator skill names otherwise.
|
|
69
|
-
The ledger is stale when any of these holds: a running or finished
|
|
70
|
-
executor has no `in_progress` task naming it as owner; a finished
|
|
71
|
-
executor's task is still open (or was closed without its result
|
|
72
|
-
merged); the next phase you will dispatch has no pending task; a
|
|
73
|
-
`blockedBy` link contradicts the run order. Fix every mismatch
|
|
74
|
-
in that same ledger during this firing.
|
|
75
|
-
2. **You are the orchestrator.** Orchestrate and hold the user
|
|
76
|
-
conversation; spawn executor subagents for every code edit and build
|
|
77
|
-
or test run.
|
|
78
|
-
- **Worker model.** Every coding spawn sets `model: sonnet` on a
|
|
79
|
-
Claude host, or the resolver-printed sonnet-equivalent id on a
|
|
80
|
-
third-party host, and fails closed — see Workflow Agent Routing in
|
|
81
|
-
[`skills/orchestrator/SKILL.md`](../orchestrator/SKILL.md#workflow-agent-routing).
|
|
82
|
-
Advisor spawns use `flags: ["--advisor"]` only when they intentionally
|
|
83
|
-
select Astra.
|
|
84
|
-
- **Focused tickets.** One mechanical done-check per ticket; resume a
|
|
85
|
-
warm agent with a thin next-slice ticket rather than a fresh cold
|
|
86
|
-
spawn, and keep thick context in the assignment file.
|
|
87
|
-
3. **This session is the advisor.** Executors consult here. Follow
|
|
88
|
-
[`../orchestrator/reference/consult-the-orchestrator.md`](../orchestrator/reference/consult-the-orchestrator.md).
|
|
89
|
-
Reply with ENDORSE / CORRECTION / PLAN / STOP. When this session
|
|
90
|
-
cannot settle a question, ask the human, then reply. Keep tool use
|
|
91
|
-
to orchestration and light verification reads.
|
|
92
|
-
4. **Resume before you spawn.** `SendMessage` an existing *executor* by
|
|
93
|
-
name or `agentId` before a cold spawn.
|
|
94
|
-
5. **Fresh spawn only for a task switch.** Never tell an agent
|
|
95
|
-
to compact for a clean context.
|
|
96
|
-
6. **Single-pending re-arm only.** Same protocol as the orchestrator
|
|
97
|
-
skill (host-agnostic; **create then claim**):
|
|
98
|
-
|
|
99
|
-
1. Cancel matching schedules only when the host can list/cancel by
|
|
100
|
-
prompt (`/orchestrator-refresh` + `--run-slug` if used). On Claude,
|
|
101
|
-
skip selective cancel — the latch is the sole stacking enforcement.
|
|
102
|
-
2. `python <status_gate.py> should-reschedule [--run-slug SLUG]`
|
|
103
|
-
- Exit **1** → stop; do not schedule.
|
|
104
|
-
- Exit **0** → continue.
|
|
105
|
-
3. Create **exactly one** non-recurring delayed wake (~1200–2700s)
|
|
106
|
-
with prompt `/orchestrator-refresh` (plus `--run-slug` when used).
|
|
107
|
-
Host one-shot tool only (on Claude: `ScheduleWakeup`). Never
|
|
108
|
-
recurring / never cadence / never a second create this firing.
|
|
109
|
-
4. `python <status_gate.py> claim-rearm [--run-slug SLUG]` right
|
|
110
|
-
after a successful create. Exit 1 → cancel that schedule and stop.
|
|
111
|
-
5. If create fails: do not claim; stop or retry once from cancel.
|
|
30
|
+
1. Read the current message and loaded instructions. Load the orchestrator entrypoint.
|
|
31
|
+
2. Resolve the run from its supplied locator or `.orchestrator/active-runs/` under the project directory.
|
|
32
|
+
Read the run record's owner and optional wake metadata first.
|
|
33
|
+
For a recorded one-shot firing, confirm both the invocation's wake identity and current root ownership.
|
|
34
|
+
Immediately run `begin-firing` with its explicit `--status-file` and `--run-slug` through [optional scheduling](../orchestrator/reference/scheduling.md).
|
|
35
|
+
Do this before recovery or task-authority steps can exit. Unknown ownership leaves the latch intact.
|
|
36
|
+
A gate mismatch or missing or invalid state ends that gate attempt. Continue ordinary unresolved work within confirmed ownership.
|
|
37
|
+
Read [recovery](../orchestrator/reference/recovery.md) and reconcile before dispatch.
|
|
38
|
+
Register applicable recovery task seeds once the task authority is accessible.
|
|
39
|
+
3. Restore every goal and the parent's follow-up task. Rebuild the short follow list from the task authority.
|
|
40
|
+
Inspect results and live workers. Keep pending approvals and unknown owners visible.
|
|
41
|
+
4. Continue permitted next actions. Send consult replies through the [local contract](../orchestrator/reference/consult-the-orchestrator.md).
|
|
42
|
+
Reuse a reachable owner where appropriate. Resolve writer ownership before replacement.
|
|
43
|
+
5. Save the updated recovery record. Complete only goals whose acceptance and delivery evidence is present.
|
|
44
|
+
Keep remaining goals open and assign the parent's next action.
|
|
112
45
|
|
|
113
|
-
|
|
46
|
+
A manual refresh does not consume an outstanding wake's latch.
|
|
47
|
+
Scheduling remains optional and follows the current runtime's supported automation rules.
|
|
48
|
+
Retire only this run's owned wake after the run's completion predicates hold.
|
|
49
|
+
|
|
50
|
+
## Sub-skills
|
|
114
51
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
denies `ScheduleWakeup` when pending).
|
|
119
|
-
- **Skipping `begin-firing`.** Prior `rearm_pending` stays set; re-arm
|
|
120
|
-
stays denied. Always run step 0a first.
|
|
121
|
-
- **Create without claim.** Skip claim after create and a second create
|
|
122
|
-
can stack. Claim immediately after success.
|
|
52
|
+
| Skill | When | Produces | If unavailable |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `orchestrator` | Every refresh | Current role and completion predicates | Report the missing entrypoint and stop new dispatch. |
|
|
123
55
|
|
|
124
|
-
## File
|
|
56
|
+
## File index
|
|
125
57
|
|
|
126
58
|
| File | Purpose |
|
|
127
59
|
|---|---|
|
|
128
|
-
| `SKILL.md` | Refresh
|
|
60
|
+
| `SKILL.md` | Refresh entrypoint. |
|
|
129
61
|
|
|
130
|
-
## Folder
|
|
62
|
+
## Folder map
|
|
131
63
|
|
|
132
|
-
|
|
133
|
-
`skills/orchestrator/scripts/`.
|
|
134
|
-
- Consult contract:
|
|
135
|
-
[`../orchestrator/reference/consult-the-orchestrator.md`](../orchestrator/reference/consult-the-orchestrator.md).
|
|
64
|
+
This skill uses the sibling orchestrator's recovery, consult, and optional scheduling references.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { cpSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
function inventory(directory, prefix = '') {
|
|
7
|
+
return readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))
|
|
8
|
+
.flatMap(entry => {
|
|
9
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
10
|
+
const entryPath = join(directory, entry.name);
|
|
11
|
+
if (entry.isDirectory()) return inventory(entryPath, relativePath);
|
|
12
|
+
if (!entry.isFile()) throw new Error(`Unsupported source entry: ${relativePath}`);
|
|
13
|
+
return [[relativePath, createHash('sha256').update(readFileSync(entryPath)).digest('hex')]];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function exportPstackSkills(pluginRoot, destination) {
|
|
18
|
+
const source = resolve(pluginRoot);
|
|
19
|
+
const target = resolve(destination);
|
|
20
|
+
const manifest = JSON.parse(readFileSync(join(source, '.codex-plugin', 'plugin.json'), 'utf8'));
|
|
21
|
+
if (manifest.name !== 'pstack' || manifest.skills !== './skills/' || !manifest.version || !manifest.repository) {
|
|
22
|
+
throw new Error('Expected a versioned pstack Codex plugin with a skills directory and repository.');
|
|
23
|
+
}
|
|
24
|
+
if (lstatSync(target, { throwIfNoEntry: false })) throw new Error(`Destination already exists: ${target}`);
|
|
25
|
+
const skillsRoot = join(source, 'skills');
|
|
26
|
+
const poteto = readFileSync(join(skillsRoot, 'poteto-mode', 'SKILL.md'), 'utf8');
|
|
27
|
+
if (!/^name: poteto-mode\r?$/m.test(poteto)) throw new Error('Missing upstream poteto-mode metadata.');
|
|
28
|
+
const files = Object.fromEntries(inventory(skillsRoot));
|
|
29
|
+
const license = readFileSync(join(skillsRoot, 'poteto-mode', 'references', 'licenses', 'LICENSE'));
|
|
30
|
+
const receipt = {
|
|
31
|
+
repository: manifest.repository,
|
|
32
|
+
version: manifest.version,
|
|
33
|
+
sourceManifestSha256: createHash('sha256').update(readFileSync(join(source, '.codex-plugin', 'plugin.json'))).digest('hex'),
|
|
34
|
+
licenseSha256: createHash('sha256').update(license).digest('hex'),
|
|
35
|
+
skillCount: Object.keys(files).filter(path => /^[^/]+\/SKILL\.md$/.test(path)).length,
|
|
36
|
+
expectedRepositorySkillName: 'poteto-mode',
|
|
37
|
+
cloudDiscovery: 'unverified',
|
|
38
|
+
cloudInvocation: 'unverified',
|
|
39
|
+
files,
|
|
40
|
+
};
|
|
41
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
42
|
+
const staging = mkdtempSync(join(dirname(target), '.pstack-export-'));
|
|
43
|
+
try {
|
|
44
|
+
cpSync(skillsRoot, staging, { recursive: true });
|
|
45
|
+
writeFileSync(join(staging, 'PSTACK-LICENSE'), license);
|
|
46
|
+
writeFileSync(join(staging, 'pstack-export.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
47
|
+
if (lstatSync(target, { throwIfNoEntry: false })) throw new Error(`Destination appeared: ${target}`);
|
|
48
|
+
renameSync(staging, target);
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(staging, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
return receipt;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
56
|
+
const [pluginRoot, destination, ...extra] = process.argv.slice(2);
|
|
57
|
+
if (!pluginRoot || !destination || extra.length) {
|
|
58
|
+
console.error('Usage: node export-pstack-skills.mjs <plugin-root> <new-repository/.agents/skills>');
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
} else {
|
|
61
|
+
try {
|
|
62
|
+
const receipt = exportPstackSkills(pluginRoot, destination);
|
|
63
|
+
console.log(JSON.stringify({ destination: resolve(destination), version: receipt.version, skillCount: receipt.skillCount,
|
|
64
|
+
expectedRepositorySkillName: receipt.expectedRepositorySkillName, cloudDiscovery: receipt.cloudDiscovery,
|
|
65
|
+
cloudInvocation: receipt.cloudInvocation }, null, 2));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error.message);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|