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.
@@ -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 ``run_status`` is not a valid status token.
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(status_file_path)
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(status_file_path: Path) -> tuple[bool, str]:
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(status_file_path)
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: Fallback slug when the file has none.
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(status_file_path)
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: Fallback slug when the file has none.
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: Fallback slug when the file has none.
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: Fallback slug when the file has none.
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
- is_rearm_pending = False
515
- if run_status == RUN_STATUS_ACTIVE:
516
- all_existing_fields, _load_failure_reason = _load_status_payload(
517
- status_file_path
518
- )
519
- if (
520
- all_existing_fields is not None
521
- and all_existing_fields.get(STATUS_FIELD_NAME) == RUN_STATUS_ACTIVE
522
- ):
523
- is_rearm_pending = _is_rearm_pending(all_existing_fields)
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(status_file_path)
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
- Refresh a running orchestrator loop when its one-shot delayed wake fires,
5
- or when the user asks to refresh or re-arm the orchestrator. Terminates
6
- when the status gate says stop.
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
- # Orchestrator Refresh
9
+ # Refresh an orchestrator
10
10
 
11
- Name the session identity first (see
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
- ## 0. status_gate first (deterministic)
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
- Script home (prefer install path, else package checkout):
17
+ ## Gotchas
18
18
 
19
- - `%USERPROFILE%/.claude/skills/orchestrator/scripts/status_gate.py`
20
- - `~/.claude/skills/orchestrator/scripts/status_gate.py`
21
- - `skills/orchestrator/scripts/status_gate.py`
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
- Pass the same `--run-slug` used at activate, if any.
23
+ ## When this applies
24
24
 
25
- ### 0a. begin-firing (consume prior re-arm)
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
- | Exit | Action |
32
- |---|---|
33
- | **1** | End the refresh. Cancel matching host schedules for `/orchestrator-refresh` if the host allows. Report inactive/done. **Do not** re-arm. Do not spawn. Any work already in flight keeps running. |
34
- | **0** | Latch cleared. Continue with steps 1–6. |
35
-
36
- ### 0b. Done after ledger (step 1)
37
-
38
- After ledger reconcile: if every task is completed/cancelled and no
39
- executor is running:
40
-
41
- ```
42
- python <status_gate.py> set --status done [--run-slug SLUG]
43
- ```
44
-
45
- Cancel matching host schedules; stop without re-arming.
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
- ## Gotchas
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
- - **Stacking loops.** A second schedule create while one is already
116
- queued multiplies firings. Cancel → `should-reschedule` → one create
117
- → `claim-rearm`. Never claim before create on Claude (PreToolUse
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 Index
56
+ ## File index
125
57
 
126
58
  | File | Purpose |
127
59
  |---|---|
128
- | `SKILL.md` | Refresh firing steps; points at orchestrator `status_gate.py`. |
60
+ | `SKILL.md` | Refresh entrypoint. |
129
61
 
130
- ## Folder Map
62
+ ## Folder map
131
63
 
132
- - `SKILL.md` — this skill (thin); gate implementation lives under
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
+ }