@open-agent-toolkit/cli 0.2.66 → 0.2.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +14 -1
  3. package/assets/docs/cli-utilities/project-log.md +63 -4
  4. package/assets/docs/contributing/code.md +8 -4
  5. package/assets/docs/contributing/skills.md +29 -17
  6. package/assets/docs/docs-tooling/commands.md +3 -1
  7. package/assets/docs/provider-sync/manifest-and-drift.md +2 -18
  8. package/assets/docs/provider-sync/scope-and-surface.md +3 -2
  9. package/assets/docs/reference/cli-reference.md +1 -1
  10. package/assets/docs/workflows/projects/evidence-layers.md +6 -3
  11. package/assets/docs/workflows/projects/implementation-execution.md +4 -3
  12. package/assets/docs/workflows/projects/lifecycle.md +17 -0
  13. package/assets/docs/workflows/projects/orchestration-model.md +6 -4
  14. package/assets/docs/workflows/projects/picking-up-projects.md +18 -0
  15. package/assets/docs/workflows/skills/repo-improve.md +7 -3
  16. package/assets/public-package-versions.json +4 -4
  17. package/assets/skills/create-agnostic-skill/SKILL.md +19 -37
  18. package/assets/skills/create-agnostic-skill/references/docs/skills-guide.md +58 -15
  19. package/assets/skills/create-agnostic-skill/references/skill-template.md +17 -15
  20. package/assets/skills/oat-agent-instructions-apply/SKILL.md +1 -1
  21. package/assets/skills/oat-agent-instructions-apply/references/instruction-file-templates/glob-scoped-rule.md +1 -4
  22. package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -2
  23. package/assets/skills/oat-doctor/SKILL.md +1 -4
  24. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +3 -2
  25. package/assets/skills/oat-project-complete/SKILL.md +158 -30
  26. package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +26 -7
  27. package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +183 -0
  28. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -7
  29. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +3 -2
  30. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  31. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -10
  32. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +3 -2
  33. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +3 -2
  34. package/assets/skills/oat-project-plan-writing/SKILL.md +7 -6
  35. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +3 -2
  36. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +3 -2
  37. package/assets/skills/oat-project-retro/SKILL.md +1 -1
  38. package/assets/skills/oat-project-retro/references/apply-procedure.md +11 -0
  39. package/assets/skills/oat-project-review-provide/SKILL.md +14 -13
  40. package/assets/skills/oat-project-review-provide-remote/SKILL.md +7 -6
  41. package/assets/skills/oat-project-summary/SKILL.md +24 -2
  42. package/assets/skills/oat-repo-improve/SKILL.md +9 -2
  43. package/assets/skills/oat-repo-improve/references/plan-template.md +4 -6
  44. package/assets/skills/oat-repo-knowledge-index/SKILL.md +11 -9
  45. package/assets/skills/oat-wave-execute/SKILL.md +20 -6
  46. package/assets/skills/oat-wave-program/SKILL.md +9 -4
  47. package/dist/commands/config/index.d.ts +2 -1
  48. package/dist/commands/config/index.d.ts.map +1 -1
  49. package/dist/commands/config/index.js +133 -38
  50. package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
  51. package/dist/commands/docs/index-generate/index.js +68 -12
  52. package/dist/commands/docs/init/index.d.ts +11 -0
  53. package/dist/commands/docs/init/index.d.ts.map +1 -1
  54. package/dist/commands/docs/init/index.js +19 -1
  55. package/dist/commands/gate/index.d.ts.map +1 -1
  56. package/dist/commands/gate/index.js +10 -2
  57. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  58. package/dist/commands/instructions/instructions.utils.js +42 -9
  59. package/dist/commands/internal/validate-skill-version-bumps.d.ts.map +1 -1
  60. package/dist/commands/internal/validate-skill-version-bumps.js +5 -5
  61. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  62. package/dist/commands/project/dispatch-ceiling/index.js +32 -18
  63. package/dist/commands/project/log/append.d.ts +48 -0
  64. package/dist/commands/project/log/append.d.ts.map +1 -1
  65. package/dist/commands/project/log/append.js +207 -30
  66. package/dist/commands/project/log/check.d.ts +87 -1
  67. package/dist/commands/project/log/check.d.ts.map +1 -1
  68. package/dist/commands/project/log/check.js +139 -5
  69. package/dist/commands/project/log/grammar.d.ts +165 -0
  70. package/dist/commands/project/log/grammar.d.ts.map +1 -1
  71. package/dist/commands/project/log/grammar.js +219 -1
  72. package/dist/commands/project/log/rollup.d.ts +16 -1
  73. package/dist/commands/project/log/rollup.d.ts.map +1 -1
  74. package/dist/commands/project/log/rollup.js +22 -2
  75. package/dist/commands/project/log/synthesize.d.ts +16 -0
  76. package/dist/commands/project/log/synthesize.d.ts.map +1 -1
  77. package/dist/commands/project/log/synthesize.js +85 -9
  78. package/dist/commands/status/index.d.ts.map +1 -1
  79. package/dist/commands/status/index.js +24 -2
  80. package/dist/commands/sync/apply.d.ts.map +1 -1
  81. package/dist/commands/sync/apply.js +6 -4
  82. package/dist/config/dispatch-matrix.d.ts.map +1 -1
  83. package/dist/config/dispatch-matrix.js +13 -5
  84. package/dist/config/oat-config.d.ts +15 -0
  85. package/dist/config/oat-config.d.ts.map +1 -1
  86. package/dist/config/oat-config.js +61 -7
  87. package/dist/config/own-keys.d.ts +22 -0
  88. package/dist/config/own-keys.d.ts.map +1 -0
  89. package/dist/config/own-keys.js +32 -0
  90. package/dist/config/resolve.d.ts +11 -0
  91. package/dist/config/resolve.d.ts.map +1 -1
  92. package/dist/config/resolve.js +35 -11
  93. package/dist/config/sync-config.d.ts.map +1 -1
  94. package/dist/config/sync-config.js +16 -0
  95. package/dist/drift/detector.d.ts.map +1 -1
  96. package/dist/drift/detector.js +52 -1
  97. package/dist/drift/skill-view-diagnostic.d.ts.map +1 -1
  98. package/dist/drift/skill-view-diagnostic.js +2 -23
  99. package/dist/engine/compute-plan.d.ts.map +1 -1
  100. package/dist/engine/compute-plan.js +22 -72
  101. package/dist/engine/managed-copy-hash.d.ts +69 -0
  102. package/dist/engine/managed-copy-hash.d.ts.map +1 -0
  103. package/dist/engine/managed-copy-hash.js +175 -0
  104. package/dist/fs/assets.d.ts +11 -0
  105. package/dist/fs/assets.d.ts.map +1 -1
  106. package/dist/fs/assets.js +1 -1
  107. package/dist/manifest/hash.d.ts +110 -0
  108. package/dist/manifest/hash.d.ts.map +1 -1
  109. package/dist/manifest/hash.js +111 -13
  110. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  111. package/dist/providers/ceiling/registry.js +6 -1
  112. package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
  113. package/dist/providers/identity/dispatch-report.js +4 -1
  114. package/dist/release/public-package-contract.d.ts.map +1 -1
  115. package/dist/release/public-package-contract.js +21 -0
  116. package/dist/validation/skills.d.ts.map +1 -1
  117. package/dist/validation/skills.js +305 -40
  118. package/package.json +2 -2
@@ -5,7 +5,7 @@ disable-model-invocation: true
5
5
  user-invocable: true
6
6
  allowed-tools: Read, Write, Bash, AskUserQuestion
7
7
  metadata:
8
- version: 1.7.9
8
+ version: 1.7.10
9
9
  ---
10
10
 
11
11
  # Complete Project
@@ -54,6 +54,7 @@ SYNCED_ARCHIVE_ENTRY_SCRIPT="$SKILL_DIR/scripts/resolve-synced-archive-entry.mjs
54
54
  SYNCED_ARCHIVE_EXECUTE_SCRIPT="$SKILL_DIR/scripts/execute-synced-archive-entry.mjs"
55
55
  SYNCED_ARCHIVE_RESUME_FIELDS_SCRIPT="$SKILL_DIR/scripts/parse-synced-archive-resume-fields.mjs"
56
56
  SYNCED_ARCHIVE_FINALIZE_SCRIPT="$SKILL_DIR/scripts/finalize-synced-archive.mjs"
57
+ DURABLE_ARCHIVE_RECEIPT_SCRIPT="$SKILL_DIR/scripts/validate-durable-archive-receipt.mjs"
57
58
  test -f "$COMPLETION_RECEIPT_SCRIPT" || {
58
59
  echo "oat: completion receipt recovery script is missing" >&2
59
60
  exit 1
@@ -86,6 +87,10 @@ test -f "$SYNCED_ARCHIVE_FINALIZE_SCRIPT" || {
86
87
  echo "Missing synced archive finalizer: $SYNCED_ARCHIVE_FINALIZE_SCRIPT" >&2
87
88
  exit 1
88
89
  }
90
+ test -f "$DURABLE_ARCHIVE_RECEIPT_SCRIPT" || {
91
+ echo "Missing durable archive receipt validator: $DURABLE_ARCHIVE_RECEIPT_SCRIPT" >&2
92
+ exit 1
93
+ }
89
94
 
90
95
  PROJECT_SCOPE=$(oat project scope "$PROJECT_PATH" --format value) || { echo "oat: cannot resolve project scope for $PROJECT_PATH; refusing completion" >&2; exit 1; }
91
96
  SYNCED_ARCHIVE_RESUME="false"
@@ -129,8 +134,45 @@ IS_DURABLE_PROJECT="false"
129
134
  if [[ "$PROJECT_SCOPE" == "shared" || "$PROJECT_SCOPE" == "synced" ]]; then
130
135
  IS_DURABLE_PROJECT="true"
131
136
  fi
137
+ # shared-archive-resume:start
138
+ SHARED_ARCHIVE_RESUME="false"
139
+ if [[ "$PROJECT_SCOPE" == "shared" && ! -d "$PROJECT_PATH" ]]; then
140
+ ARCHIVED_PROJECTS_ROOT="$(dirname "$(dirname "$PROJECT_PATH")")/archived"
141
+ if ! DISCOVERED_ARCHIVE_PATH=$(node "$DURABLE_ARCHIVE_RECEIPT_SCRIPT" \
142
+ --mode directory \
143
+ --archived-root "$ARCHIVED_PROJECTS_ROOT" \
144
+ --project-name "$PROJECT_NAME"); then
145
+ echo "Shared archive completion cannot resume automatically: the retained active pointer names a source directory that no longer exists and no single validated archive matches $PROJECT_NAME." >&2
146
+ echo "Manual recovery: locate the archive directory under $ARCHIVED_PROJECTS_ROOT, confirm 'oat_lifecycle: complete' in its state.md, then run: oat config set activeProject \"\"" >&2
147
+ exit 1
148
+ fi
149
+ SHARED_ARCHIVE_RESUME="true"
150
+ PROJECT_PATH="$DISCOVERED_ARCHIVE_PATH"
151
+ ARCHIVE_PATH="$DISCOVERED_ARCHIVE_PATH"
152
+ echo "Verified discovered shared archive at $DISCOVERED_ARCHIVE_PATH; clearing the retained active pointer without a second archive."
153
+ fi
154
+ # shared-archive-resume:end
132
155
  ```
133
156
 
157
+ When `SHARED_ARCHIVE_RESUME="true"`, continue directly at **Step 12**. This is
158
+ the single recognized shared-scope checkpoint: the archive already succeeded
159
+ and the run died before the Step 12 clear. Do not execute Steps 2 through 11.5,
160
+ ask the upfront questions again, invoke `oat project archive` a second time,
161
+ regenerate any artifact, or replay the Step 3.7 seal append. The discovered
162
+ archive's `state.md` is the terminal evidence, re-derived through the same
163
+ `oat_lifecycle: complete` and archived-phase-marker checks Step 12 derives from
164
+ `ARCHIVE_OUTPUT`, so no second completion seal is written.
165
+
166
+ Every other interruption is not a resume checkpoint. After `complete-state`, or
167
+ after the Step 7 PR artifact, the retained pointer still names an existing
168
+ source directory, so this branch is not taken and the normal completion entry
169
+ runs: Step 3.7 routes on the status probe's `sealed` field as described there,
170
+ so a log that is already sealed is left alone (and a probe answering
171
+ `status: "ambiguous"` stops completion until the log is repaired), and
172
+ Step 7 regenerates the PR artifact only when it is missing. When the source
173
+ directory is gone and zero or several archived candidates match, the branch
174
+ stops with the manual-recovery message above and leaves the pointer untouched.
175
+
134
176
  When `SYNCED_ARCHIVE_RESUME="true"`, continue directly at **Step 8.5**. Do not
135
177
  execute Steps 2 through 8, ask the upfront questions again, read or mutate the
136
178
  retired active checkout, regenerate active artifacts, or invoke archive a
@@ -637,15 +679,28 @@ Run the project-log status probe before any lifecycle mutation or archive work:
637
679
  PROJECT_LOG_CHECK=$(oat project log check --project "$PROJECT_PATH" --json)
638
680
  ```
639
681
 
640
- Route on the structured result:
682
+ Route on the structured result. Check `status: "ambiguous"` first: its counts
683
+ are all zero and `sealed` is `false`, so every other row below would otherwise
684
+ read it as an empty, unsealed log and fall through to the roll-up and the seal.
641
685
 
686
+ - `status: "ambiguous"`: the log's structure has two readings, so none of its
687
+ counts mean anything. Stop completion, report the result's `ambiguity` string
688
+ verbatim as the reason, and repair the log before re-running. Do not run the
689
+ summary hard gate, the roll-up, the seal append, or the retirement sweep. The
690
+ zeroed counts are not evidence that there is nothing to roll up.
642
691
  - `status: "absent"`: the feature is inert; proceed without a roll-up or seal
643
692
  append.
644
693
  - `status: "synthesis_pending"` or `synthesisPending: true`: emit
645
694
  `Warning: Project-log end-of-run synthesis is pending. Complete it with oat project log synthesize.`
646
695
  Offer to invoke `oat project log synthesize`, but do not block completion if
647
696
  the synthesis remains pending. Synthesis is warn-only.
648
- - When entry counts are nonzero, require a current `summary.md`. This hard gate
697
+ - `sealed: true`: this completion already sealed the log on an earlier run.
698
+ Skip the summary hard gate below, the roll-up, and the seal append, and run
699
+ the retirement sweep in report-only mode as described below. A sealed log is
700
+ closed; re-entering any of those steps would either duplicate the seal or
701
+ attempt an append the CLI now refuses.
702
+ - When entry counts are nonzero **and the log is not sealed**, require a
703
+ current `summary.md`. This hard gate
649
704
  overrides Step 3.5's tolerance for declined, skipped, missing, or failed
650
705
  summary generation: load the current `oat-project-summary/SKILL.md` and follow
651
706
  it; only when skill loading is unavailable in the current host/runtime, author
@@ -731,17 +786,30 @@ sweep runs:
731
786
  - **No project log** (`status: "absent"` from the probe above). Record the
732
787
  findings and their dispositions in the Step 12 completion summary
733
788
  instead, and never create a project log for them.
734
- - **Resumed completion whose log already carries a seal.** The status probe
735
- above reports no seal state, so detect the seal directly before appending
736
- anything: read `logPath` from `PROJECT_LOG_CHECK` and treat the log as sealed
737
- when it already contains a structural seal heading of the form
738
- `### <date> · structural · oat-project-complete · seal`. On a sealed log the
789
+ - **Resumed completion whose log already carries a seal.** Route on the status
790
+ probe: `sealed: true` in `PROJECT_LOG_CHECK` means the log is already sealed,
791
+ and `seal` carries that entry's heading and date. `oat project log check`
792
+ owns this claim — it reports the seal from the log's own structural entries,
793
+ so a seal written before the seal append was keyed is recognized the same way
794
+ as a keyed one. On a sealed log the
739
795
  sweep runs in report-only mode: surface the findings and dispositions in the
740
796
  Step 12 completion summary, append nothing to the sealed log, and never
741
797
  re-enter the roll-up or the seal. No project-log append may follow the seal,
742
- on a resume as much as on a first run.
743
-
744
- For a log with entries, reuse the summary flow's structured roll-up result only
798
+ on a resume as much as on a first run. The CLI enforces this rather than
799
+ trusting the routing: any non-seal append carrying new content onto a sealed
800
+ log is refused with
801
+ `status: "sealed"` and a non-zero exit, and a replayed seal reports
802
+ `already-appended` instead of writing a second one. An append that
803
+ `--idempotency-key` recognizes as its own earlier entry also reports
804
+ `already-appended` and exits 0, because that entry predates the seal and
805
+ nothing is written; that carve-out never adds content after the seal.
806
+
807
+ Skip this roll-up entirely when the probe reported `sealed: true`; a sealed log
808
+ has already been rolled up and sealed, and the seal must remain its final
809
+ entry.
810
+
811
+ For an unsealed log with entries, reuse the summary flow's structured roll-up
812
+ result only
745
813
  when this completion run has that exact result in memory and it reports
746
814
  `status: "ok"`. Otherwise run the idempotent enforcement surface:
747
815
 
@@ -756,11 +824,15 @@ Do not set lifecycle complete, seal, or archive unless the structured
756
824
  proceed.
757
825
  - `ledgerOutcome: "skipped_permitted"` with `status: "ok"`: proceed and report
758
826
  the permitted skip; the absent default reference layer is not a block.
827
+ - `status: "ambiguous"`: the log's structure has two readings, so nothing was
828
+ read and `summary.md` was not written. Stop, report the result's `ambiguity`
829
+ string verbatim, and repair the log. Never continue to seal or archive.
759
830
  - `status: "failed"`, `ledgerOutcome: "failed"`, malformed JSON, or a command
760
831
  error: stop and surface the roll-up failure. Never continue to seal or
761
832
  archive.
762
833
 
763
- When the status probe found an existing project log, append the completion seal
834
+ When the status probe found an existing project log and did not report
835
+ `sealed: true`, append the completion seal
764
836
  as the final project-log entry before any lifecycle-complete mutation:
765
837
 
766
838
  ```bash
@@ -769,9 +841,32 @@ oat project log append \
769
841
  --structural \
770
842
  --producer oat-project-complete \
771
843
  --ref seal \
772
- --body "Completion sealed at $(date -u +%Y-%m-%dT%H:%M:%SZ); project-log roll-up status: ok."
844
+ --idempotency-key "oat-seal:$PROJECT_NAME" \
845
+ --body "Completion sealed at $(date -u +%Y-%m-%dT%H:%M:%SZ); project-log roll-up status: ok. oat-seal:$PROJECT_NAME"
846
+ ```
847
+
848
+ The key makes a replayed seal a no-op: `oat project log append` reports
849
+ `already-appended` and leaves exactly one seal entry. It must stand alone as
850
+ its own whitespace-delimited word in `--body` — the command records the whole
851
+ word carrying the key, so a key glued to the varying timestamp would never
852
+ match its own earlier append, and the command rejects a key the body omits.
853
+
854
+ Then verify the seal landed, rather than trusting the append's exit status:
855
+
856
+ ```bash
857
+ SEAL_CHECK=$(oat project log check --project "$PROJECT_PATH" --json)
773
858
  ```
774
859
 
860
+ Require `sealed: true` in `SEAL_CHECK`, and require the `heading` the append
861
+ returned to be a seal heading — a `### <date> · structural · oat-project-complete · seal`
862
+ line. A zero exit with either condition unmet means the log is still open: stop
863
+ and report it; do not set lifecycle complete or archive. Re-reading the file is
864
+ the stronger of the two checks and the reason both are required: it reports what
865
+ the log now contains rather than what the append said about it, so it also
866
+ catches a seal that was written but is unreachable to the parser. This
867
+ verification exists because a seal that is silently not written reads exactly
868
+ like a successful one from the completion flow's side.
869
+
775
870
  Only append this seal after Step 3.7 has either confirmed there are no entries
776
871
  to roll up or obtained `status: "ok"`. If the append fails for an existing log,
777
872
  stop before setting lifecycle complete or archiving. No project-log append may follow the seal.
@@ -831,17 +926,26 @@ It must set `oat_lifecycle: complete`, completion timestamps, `**Status:** Compl
831
926
  ### Step 6: Clear or Defer the Active Project Pointer
832
927
 
833
928
  Clearing the active project pointer is implicit and requires no confirmation.
834
- For a synced archive, defer the clear until Step 8 has validated the full
835
- terminal report. Every archive or configured-S3 failure therefore exits with
836
- the original pointer intact and the completion remains directly resumable.
929
+ For every completion that will actually archive — that is, every durable
930
+ (`shared` or `synced`) project with archive selected — defer the clear until
931
+ the archive receipt has been validated in Step 12. Every archive or
932
+ configured-S3 failure therefore exits with the original pointer intact and the
933
+ completion remains directly resumable.
934
+
935
+ The guard mirrors the `complete-state` and Step 8 gate exactly. Keying it on
936
+ `SHOULD_ARCHIVE` alone would strand the pointer for `local` projects, which are
937
+ never durable and therefore never archive; they keep the immediate clear, as do
938
+ all non-archive completions.
837
939
 
838
940
  ```bash
839
- if [[ "$PROJECT_SCOPE" == "synced" && "$SHOULD_ARCHIVE" == "true" ]]; then
840
- echo "Active project pointer retained until synced archive terminal receipt validation."
941
+ # active-pointer-guard:start
942
+ if [[ "$SHOULD_ARCHIVE" == "true" && "$IS_DURABLE_PROJECT" == "true" ]]; then
943
+ echo "Active project pointer retained until durable archive receipt validation."
841
944
  else
842
945
  oat config set activeProject ""
843
946
  echo "Active project pointer cleared."
844
947
  fi
948
+ # active-pointer-guard:end
845
949
  ```
846
950
 
847
951
  ### Step 7: Generate PR Description
@@ -1140,8 +1244,12 @@ Use `ARCHIVE_S3_CONTEXT` in Step 12 if the command reports profile/region detail
1140
1244
 
1141
1245
  Only after every applicable synced terminal and recap-export field above has
1142
1246
  passed validation may the workflow continue. Keep the pointer through the
1143
- required link, dashboard, bookkeeping, push, and PR-closeout work below so a
1144
- failure after record retirement can retry through the recordless archive path.
1247
+ required link, dashboard, bookkeeping, push, and PR-closeout work below for
1248
+ every durable scope. For `synced`, a failure after record retirement retries
1249
+ through the recordless archive path. For `shared`, the archive has already
1250
+ removed the source directory, so a failure here retries through the
1251
+ shared-scope post-archive checkpoint in Step 1, which validates the discovered
1252
+ archive and clears the pointer without archiving again.
1145
1253
 
1146
1254
  #### Step 8.5: Finalize Archive-Aware Recap Links
1147
1255
 
@@ -1602,24 +1710,44 @@ Failure handling:
1602
1710
  before Step 12, retain the active pointer, and let the next invocation resume
1603
1711
  recordlessly. Other completion shapes retain their existing warning-only
1604
1712
  behavior and Step 12 summary guidance.
1713
+ - A shared archive completion also reaches this step with the pointer still
1714
+ retained, but its sync failure stays warning-only. Its shared-scope resume
1715
+ goes straight to the Step 12 clear and would not retry this PR update, so
1716
+ Step 12 clears the pointer after the receipt validates and the user updates
1717
+ the tracked PR by hand from the printed artifact path.
1605
1718
  - Never re-archive or re-commit on failure here — the lifecycle bookkeeping
1606
1719
  and any recap evidence update in Step 10.6 already shipped.
1607
1720
 
1608
1721
  ### Step 12: Confirm to User
1609
1722
 
1610
- Immediately before confirmation, clear the deferred synced archive pointer.
1611
- The finalizer validates the terminal receipt again before changing
1612
- configuration. This ordering keeps every failure before final confirmation
1613
- directly retryable, including a retry after record retirement:
1723
+ Immediately before confirmation, clear the deferred durable archive pointer.
1724
+ The clear always runs after a validated archive receipt, never before: the
1725
+ synced finalizer re-validates the full terminal report, and every other durable
1726
+ scope re-validates `ARCHIVE_OUTPUT` through the separate durable receipt
1727
+ validator. Do not widen the synced finalizer to accept non-synced input. This
1728
+ ordering keeps every failure before final confirmation directly retryable,
1729
+ including a retry after record retirement:
1614
1730
 
1615
1731
  ```bash
1616
- if [[ "$PROJECT_SCOPE" == "synced" && "$SHOULD_ARCHIVE" == "true" ]]; then
1617
- SYNCED_ARCHIVE_FINALIZATION=$(printf '%s\n' "$ARCHIVE_OUTPUT" | \
1618
- node "$SYNCED_ARCHIVE_FINALIZE_SCRIPT" \
1619
- --project-name "$PROJECT_NAME") || exit 1
1620
- printf '%s\n' "$SYNCED_ARCHIVE_FINALIZATION"
1621
- echo "Synced archive terminal receipt verified; active project pointer cleared."
1732
+ # deferred-pointer-clear:start
1733
+ if [[ "$SHARED_ARCHIVE_RESUME" == "true" ]]; then
1734
+ oat config set activeProject ""
1735
+ echo "Discovered shared archive already validated; active project pointer cleared without a second archive."
1736
+ elif [[ "$SHOULD_ARCHIVE" == "true" && "$IS_DURABLE_PROJECT" == "true" ]]; then
1737
+ if [[ "$PROJECT_SCOPE" == "synced" ]]; then
1738
+ SYNCED_ARCHIVE_FINALIZATION=$(printf '%s\n' "$ARCHIVE_OUTPUT" | \
1739
+ node "$SYNCED_ARCHIVE_FINALIZE_SCRIPT" \
1740
+ --project-name "$PROJECT_NAME") || exit 1
1741
+ printf '%s\n' "$SYNCED_ARCHIVE_FINALIZATION"
1742
+ echo "Synced archive terminal receipt verified; active project pointer cleared."
1743
+ else
1744
+ VALIDATED_ARCHIVE_PATH=$(printf '%s\n' "$ARCHIVE_OUTPUT" | \
1745
+ node "$DURABLE_ARCHIVE_RECEIPT_SCRIPT" --mode receipt) || exit 1
1746
+ oat config set activeProject ""
1747
+ echo "Durable archive receipt verified for $VALIDATED_ARCHIVE_PATH; active project pointer cleared."
1748
+ fi
1622
1749
  fi
1750
+ # deferred-pointer-clear:end
1623
1751
  ```
1624
1752
 
1625
1753
  Show user:
@@ -1,6 +1,7 @@
1
1
  import { execFile as execFileCallback } from 'node:child_process';
2
- import { readFile } from 'node:fs/promises';
3
- import { pathToFileURL } from 'node:url';
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { promisify } from 'node:util';
5
6
 
6
7
  const execFile = promisify(execFileCallback);
@@ -91,7 +92,7 @@ async function main(argv) {
91
92
  projectName,
92
93
  getArchiveReport: async () => {
93
94
  try {
94
- return JSON.parse(await readFile(0, 'utf8'));
95
+ return JSON.parse(readFileSync(0, 'utf8'));
95
96
  } catch (error) {
96
97
  throw finalizationError(
97
98
  `Unable to parse synced archive terminal report: ${error.message}`,
@@ -110,10 +111,28 @@ async function main(argv) {
110
111
  });
111
112
  }
112
113
 
113
- if (
114
- process.argv[1] &&
115
- import.meta.url === pathToFileURL(process.argv[1]).href
116
- ) {
114
+ /**
115
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
116
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
117
+ * that exits 0 whenever the skill is reached through a symlinked install root,
118
+ * and canonicalizing only one side has the same effect under
119
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
120
+ * way the caller reads "exited 0" as "verified" — the fail-open shape this
121
+ * finalizer exists to prevent.
122
+ */
123
+ function isDirectInvocation(invokedPath) {
124
+ if (!invokedPath) return false;
125
+ try {
126
+ return (
127
+ realpathSync(fileURLToPath(import.meta.url)) ===
128
+ realpathSync(resolve(invokedPath))
129
+ );
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ if (isDirectInvocation(process.argv[1])) {
117
136
  main(process.argv.slice(2))
118
137
  .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
119
138
  .catch((error) => {
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
+ import { readFile, readdir } from 'node:fs/promises';
5
+ import { join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const ARCHIVED_STATE_MARKER = 'Lifecycle complete; archived locally';
9
+
10
+ function receiptError(message) {
11
+ const error = new Error(message);
12
+ error.code = 'E_DURABLE_ARCHIVE_RECEIPT';
13
+ return error;
14
+ }
15
+
16
+ /**
17
+ * Validate a non-synced (`shared`) `oat project archive --json` report.
18
+ *
19
+ * Synced archives keep their own richer terminal contract in
20
+ * `finalize-synced-archive.mjs`; that finalizer must never be widened to
21
+ * accept non-synced input, so this validator stays separate and requires only
22
+ * the fields a shared archive receipt actually carries.
23
+ */
24
+ export function validateDurableArchiveReceipt(report) {
25
+ if (!report || typeof report !== 'object' || Array.isArray(report)) {
26
+ throw receiptError(
27
+ 'Durable archive validation requires a structured archive report.',
28
+ );
29
+ }
30
+ if (report.status !== 'ok' || report.mode !== 'apply') {
31
+ throw receiptError(
32
+ 'Durable archive report is not a successful apply result.',
33
+ );
34
+ }
35
+ if (
36
+ typeof report.archivePath !== 'string' ||
37
+ report.archivePath.length === 0
38
+ ) {
39
+ throw receiptError('Durable archive report has no archive path.');
40
+ }
41
+ return report.archivePath;
42
+ }
43
+
44
+ function isArchivedCandidateName(entryName, projectName) {
45
+ const escapedProjectName = projectName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
+ return (
47
+ entryName === projectName ||
48
+ new RegExp(`^\\d{8}-${escapedProjectName}$`).test(entryName)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * A discovered archive counts as terminal evidence only when its `state.md`
54
+ * carries both the completed lifecycle field and the archived phase marker
55
+ * that `oat project complete-state --archived` writes.
56
+ */
57
+ export function isTerminalArchivedState(stateContent) {
58
+ if (typeof stateContent !== 'string') return false;
59
+ return (
60
+ /^oat_lifecycle:[ \t]*complete[ \t]*$/m.test(stateContent) &&
61
+ stateContent.includes(ARCHIVED_STATE_MARKER)
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Re-derive the Step 2 archive checks from an archive that already exists on
67
+ * disk, for the single shared-scope resume checkpoint: the archive succeeded
68
+ * and the run died before the Step 12 pointer clear. Exactly one candidate
69
+ * must validate; zero or several are ambiguous and route to manual recovery.
70
+ */
71
+ export async function discoverValidatedDurableArchive({
72
+ archivedRoot,
73
+ projectName,
74
+ listDirectory,
75
+ readStateFile,
76
+ }) {
77
+ if (typeof archivedRoot !== 'string' || archivedRoot.length === 0) {
78
+ throw receiptError('Durable archive discovery requires an archived root.');
79
+ }
80
+ if (typeof projectName !== 'string' || projectName.length === 0) {
81
+ throw receiptError('Durable archive discovery requires a project name.');
82
+ }
83
+ let entries;
84
+ try {
85
+ entries = await listDirectory(archivedRoot);
86
+ } catch {
87
+ entries = [];
88
+ }
89
+ const candidates = [];
90
+ for (const entryName of entries) {
91
+ if (!isArchivedCandidateName(entryName, projectName)) continue;
92
+ const archivePath = join(archivedRoot, entryName);
93
+ let stateContent;
94
+ try {
95
+ stateContent = await readStateFile(join(archivePath, 'state.md'));
96
+ } catch {
97
+ continue;
98
+ }
99
+ if (isTerminalArchivedState(stateContent)) candidates.push(archivePath);
100
+ }
101
+ if (candidates.length !== 1) {
102
+ throw receiptError(
103
+ candidates.length === 0
104
+ ? `No archived project under ${archivedRoot} matches ${projectName} with a completed archived state.md.`
105
+ : `Multiple archived projects under ${archivedRoot} match ${projectName}; refusing an ambiguous resume.`,
106
+ );
107
+ }
108
+ return candidates[0];
109
+ }
110
+
111
+ function parseArguments(argv) {
112
+ const result = {};
113
+ for (let index = 0; index < argv.length; index += 2) {
114
+ const flag = argv[index];
115
+ const value = argv[index + 1];
116
+ if (value === undefined) {
117
+ throw receiptError(`Missing value for ${flag ?? 'argument'}.`);
118
+ }
119
+ if (flag === '--mode') result.mode = value;
120
+ else if (flag === '--archived-root') result.archivedRoot = value;
121
+ else if (flag === '--project-name') result.projectName = value;
122
+ else throw receiptError(`Unsupported argument: ${flag}.`);
123
+ }
124
+ if (result.mode !== 'receipt' && result.mode !== 'directory') {
125
+ throw receiptError(
126
+ 'Usage: validate-durable-archive-receipt.mjs --mode receipt | --mode directory --archived-root <dir> --project-name <name>',
127
+ );
128
+ }
129
+ return result;
130
+ }
131
+
132
+ async function main(argv) {
133
+ const options = parseArguments(argv);
134
+ if (options.mode === 'receipt') {
135
+ let report;
136
+ try {
137
+ report = JSON.parse(readFileSync(0, 'utf8'));
138
+ } catch (error) {
139
+ throw receiptError(
140
+ `Unable to parse durable archive report: ${error.message}`,
141
+ );
142
+ }
143
+ return validateDurableArchiveReceipt(report);
144
+ }
145
+ return discoverValidatedDurableArchive({
146
+ archivedRoot: options.archivedRoot,
147
+ projectName: options.projectName,
148
+ listDirectory: (directory) => readdir(directory),
149
+ readStateFile: (statePath) => readFile(statePath, 'utf8'),
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
155
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
156
+ * that exits 0 whenever the skill is reached through a symlinked install root,
157
+ * and canonicalizing only one side has the same effect under
158
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
159
+ * way the caller reads "exited 0" as "verified" — the fail-open shape this
160
+ * validator exists to prevent.
161
+ */
162
+ function isDirectInvocation(invokedPath) {
163
+ if (!invokedPath) return false;
164
+ try {
165
+ return (
166
+ realpathSync(fileURLToPath(import.meta.url)) ===
167
+ realpathSync(resolve(invokedPath))
168
+ );
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ if (isDirectInvocation(process.argv[1])) {
175
+ main(process.argv.slice(2))
176
+ .then((archivePath) => process.stdout.write(`${archivePath}\n`))
177
+ .catch((error) => {
178
+ process.stderr.write(
179
+ `Invalid durable archive receipt: ${error.message}\n`,
180
+ );
181
+ process.exitCode = 1;
182
+ });
183
+ }
@@ -5,7 +5,7 @@ disable-model-invocation: true
5
5
  user-invocable: false
6
6
  allowed-tools: Read, Bash
7
7
  metadata:
8
- version: 1.1.5
8
+ version: 1.1.6
9
9
  ---
10
10
 
11
11
  # Dispatching OAT Project Subagents
@@ -152,12 +152,14 @@ For every lifecycle dispatch:
152
152
  9. Add lifecycle outcome metadata and let the calling workflow perform state,
153
153
  plan, implementation-log, commit, or review-table writes.
154
154
 
155
- ### Persist native dispatch lineage
155
+ ### Record the launch
156
156
 
157
- For project-aware launches, apply the generic engine's native dispatch lineage
158
- contract. Construct and redact the complete generic record plus OAT event
159
- before the native host call. Immediately after the call returns accepted or
160
- `blocked-before-start`, persist the result with:
157
+ For project-aware launches, construct and redact the complete generic record
158
+ plus OAT event before the native host call. When the call returns accepted or
159
+ `blocked-before-start`, the calling workflow writes the request ID, the
160
+ `Dispatch:` stamp, the launch status, and later the terminal outcome into its
161
+ run record in `implementation.md`. Writing a per-dispatch file with `oat project dispatch record` is optional and off by default: no lifecycle skill or command consumes those files, so do not write them unless the host has explicitly opted in. A host that has opted in persists the
162
+ validated record with:
161
163
 
162
164
  ```bash
163
165
  oat project dispatch record \
@@ -166,7 +168,7 @@ oat project dispatch record \
166
168
  --json
167
169
  ```
168
170
 
169
- The accepted record closes replacement. A rejected record must include
171
+ An accepted launch closes replacement. A rejected record must include
170
172
  `provesNoChildStarted: true` before one target-preserving canonical-instruction
171
173
  fallback may receive its own fresh request ID. Preserve exact provider, model,
172
174
  effort, reasoning mode, service tier, route, authority, payload controls,