@1aboveio/skills 1.0.9 → 1.0.11

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 (213) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +297 -216
  3. package/README.md +3 -3
  4. package/package.json +10 -8
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-foundation/SKILL.md +35 -37
  7. package/skills/app-dev/android-foundation/references/delivery-focus.md +4 -2
  8. package/skills/app-dev/android-foundation/references/feature-work.md +50 -0
  9. package/skills/app-dev/android-foundation/references/setup.md +24 -9
  10. package/skills/app-dev/android-foundation/references/workspace.md +81 -0
  11. package/skills/app-dev/android-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
  12. package/skills/app-dev/android-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
  13. package/skills/app-dev/android-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
  14. package/skills/app-dev/app-debug/SKILL.md +1 -1
  15. package/skills/app-dev/better-auth/SKILL.md +1 -1
  16. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  17. package/skills/app-dev/data-quality/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/SKILL.md +15 -10
  19. package/skills/app-dev/ios-foundation/references/setup.md +25 -9
  20. package/skills/app-dev/ios-foundation/references/workspace.md +82 -0
  21. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
  22. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
  23. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
  24. package/skills/app-dev/monorepo/SKILL.md +1 -1
  25. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  26. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  27. package/skills/app-dev/pyspark/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/SKILL.md +1 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  31. package/skills/cicd/cloud-build/SKILL.md +1 -1
  32. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  33. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  34. package/skills/cicd/docker/SKILL.md +1 -1
  35. package/skills/cicd/github-actions/SKILL.md +1 -1
  36. package/skills/cicd/github-flow/SKILL.md +1 -1
  37. package/skills/cicd/google-cloud/SKILL.md +1 -1
  38. package/skills/cicd/mergify/SKILL.md +1 -1
  39. package/skills/cicd/podman/SKILL.md +1 -1
  40. package/skills/content/article-review/SKILL.md +1 -1
  41. package/skills/content/article-to-image/SKILL.md +1 -1
  42. package/skills/content/flowchart-svg/SKILL.md +1 -1
  43. package/skills/content/image-gen/SKILL.md +1 -1
  44. package/skills/content/mermaid/SKILL.md +1 -1
  45. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  46. package/skills/content/wechat-publisher/SKILL.md +1 -1
  47. package/skills/content/wechat-search/SKILL.md +1 -1
  48. package/skills/content/xai-search/SKILL.md +1 -1
  49. package/skills/core/distill-writing/SKILL.md +3 -3
  50. package/skills/core/distill-writing/references/extraction.md +1 -1
  51. package/skills/core/doc-to-md/SKILL.md +1 -1
  52. package/skills/core/editorial-review/SKILL.md +82 -12
  53. package/skills/core/editorial-review/references/checklist.md +26 -1
  54. package/skills/core/house-style/SKILL.md +8 -3
  55. package/skills/core/house-style/references/analytical-reporting.md +123 -0
  56. package/skills/core/house-style/references/analytical-writing-patterns.md +79 -0
  57. package/skills/core/house-style/references/analytical-writing-research.md +129 -0
  58. package/skills/core/{write-doc → house-style}/references/bilingual.md +7 -5
  59. package/skills/core/house-style/references/definitions-and-metrics.md +86 -0
  60. package/skills/core/house-style/references/document-workflow.md +193 -0
  61. package/skills/core/house-style/references/initialization.md +66 -0
  62. package/skills/core/house-style/references/persistence.md +9 -4
  63. package/skills/core/house-style/references/rules.md +11 -4
  64. package/skills/core/house-style/scripts/init-guidance.mjs +213 -0
  65. package/skills/core/house-style/templates/analytical-report.md +54 -0
  66. package/skills/core/house-style/templates/gap-analysis.md +1 -1
  67. package/skills/core/plain-language/SKILL.md +2 -2
  68. package/skills/core/polish-cn/SKILL.md +9 -2
  69. package/skills/core/reg-parser/SKILL.md +3 -3
  70. package/skills/core/terminology/SKILL.md +8 -4
  71. package/skills/core/write-doc/SKILL.md +40 -151
  72. package/skills/core/write-report/SKILL.md +129 -0
  73. package/skills/core/{write-doc → write-report}/references/compliance-reports.md +4 -4
  74. package/skills/engineering/better-review/SKILL.md +37 -37
  75. package/skills/engineering/better-review/references/passes.md +35 -41
  76. package/skills/engineering/better-review/references/result.md +10 -9
  77. package/skills/engineering/e2e-test/SKILL.md +17 -17
  78. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +11 -13
  79. package/skills/engineering/e2e-test/references/presentation-sweep.md +7 -7
  80. package/skills/engineering/e2e-test/references/reviewing.md +9 -11
  81. package/skills/engineering/e2e-test/references/route-manifest.md +1 -1
  82. package/skills/engineering/engineering/SKILL.md +124 -0
  83. package/skills/engineering/engineering/references/engineering-conventions.md +32 -0
  84. package/skills/engineering/engineering/references/feature-delivery.md +8 -0
  85. package/skills/engineering/engineering/references/init-scope.md +37 -0
  86. package/skills/engineering/engineering/scripts/init.mjs +160 -0
  87. package/skills/engineering/{contributing → engineering}/scripts/persist-conventions.mjs +1 -1
  88. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  89. package/skills/engineering/engineering-runtime/coherence/workflow.json +19 -19
  90. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
  91. package/skills/engineering/ensure-coverage/SKILL.md +22 -109
  92. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +7 -5
  93. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +4 -4
  94. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +3 -3
  95. package/skills/engineering/ensure-coverage/references/depth/characterization.md +4 -65
  96. package/skills/engineering/ensure-coverage/references/depth/grading.md +5 -7
  97. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +6 -12
  98. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +1 -1
  99. package/skills/engineering/ensure-coverage/references/presentation/axis.md +6 -6
  100. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +7 -23
  101. package/skills/engineering/ensure-coverage/references/process/output-template.md +18 -133
  102. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +7 -12
  103. package/skills/engineering/ensure-coverage/scripts/README.md +12 -4
  104. package/skills/engineering/golden-path/SKILL.md +1 -1
  105. package/skills/engineering/harness-runtime/SKILL.md +3 -1
  106. package/skills/engineering/harness-runtime/bin/workspaces.mjs +52 -0
  107. package/skills/engineering/harness-runtime/native-workspace.mjs +239 -0
  108. package/skills/engineering/harness-runtime/references/workspaces.md +78 -0
  109. package/skills/engineering/harness-runtime/workspace-lifecycle.mjs +2154 -0
  110. package/skills/engineering/resolve-issues/SKILL.md +40 -61
  111. package/skills/engineering/resolve-issues/references/ci.md +13 -3
  112. package/skills/engineering/resolve-issues/references/delivery-progress.md +4 -3
  113. package/skills/engineering/resolve-issues/references/done.md +11 -2
  114. package/skills/engineering/resolve-issues/references/expire.md +9 -2
  115. package/skills/engineering/resolve-issues/references/implementation.md +33 -17
  116. package/skills/engineering/resolve-issues/references/preflight.md +3 -1
  117. package/skills/engineering/resolve-issues/references/principles.md +52 -0
  118. package/skills/engineering/resolve-issues/references/review.md +25 -15
  119. package/skills/engineering/resolve-issues/references/task-plan.md +64 -27
  120. package/skills/engineering/resolve-issues/references/workspaces.md +164 -0
  121. package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +185 -317
  122. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +44 -7
  123. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +36 -2
  124. package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +9 -8
  125. package/skills/engineering/resolve-issues/scripts/workspace-lifecycle.mjs +1 -0
  126. package/skills/engineering/resolve-release/SKILL.md +1 -1
  127. package/skills/engineering/rush-issues/SKILL.md +7 -5
  128. package/skills/engineering/rush-release/SKILL.md +1 -1
  129. package/skills/engineering/test-quality/SKILL.md +19 -4
  130. package/skills/engineering/test-quality/references/lint.md +1 -1
  131. package/skills/engineering/test-quality/references/principles.md +2 -2
  132. package/skills/engineering/test-quality/references/testing-conventions.md +1 -1
  133. package/skills/risk-compliance/compliance-audit/SKILL.md +4 -8
  134. package/skills/risk-compliance/compliance-audit/references/audience-and-reports.md +12 -0
  135. package/skills/risk-compliance/contract-compare/SKILL.md +17 -1
  136. package/skills/risk-compliance/contract-review/SKILL.md +6 -1
  137. package/skills/risk-compliance/contract-review/references/report-stage.md +17 -0
  138. package/skills/risk-compliance/doc-authenticity/SKILL.md +11 -1
  139. package/skills/risk-compliance/fraud-detection/LICENSE +3 -0
  140. package/skills/risk-compliance/fraud-detection/SKILL.md +148 -0
  141. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/first-party-fraud.md +3 -2
  142. package/skills/risk-compliance/fraud-detection/references/archetypes/third-party-fraud.md +47 -0
  143. package/skills/risk-compliance/fraud-detection/references/collection-payout-features.md +151 -0
  144. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/contexts/bank-transfer.md +4 -3
  145. package/skills/risk-compliance/fraud-detection/references/contexts/card-payment.md +70 -0
  146. package/skills/risk-compliance/fraud-detection/references/contexts/payment-collection.md +34 -0
  147. package/skills/risk-compliance/fraud-detection/references/contexts/payout.md +34 -0
  148. package/skills/risk-compliance/fraud-detection/references/enrichment.md +74 -0
  149. package/skills/risk-compliance/fraud-detection/references/entities.md +48 -0
  150. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/feature-engineering.md +69 -33
  151. package/skills/risk-compliance/fraud-detection/references/feature-sql.md +150 -0
  152. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-rationale.md +4 -2
  153. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-template.md +45 -23
  154. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/review-checklist.md +60 -20
  155. package/skills/risk-compliance/fraud-detection/references/workflow.md +216 -0
  156. package/skills/risk-compliance/fraud-detection/release-check.mjs +95 -0
  157. package/skills/risk-compliance/payment-analysis/SKILL.md +36 -17
  158. package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +2 -2
  159. package/skills/risk-compliance/payment-analysis/references/report-template.md +10 -2
  160. package/skills/risk-compliance/payment-analysis/references/visualization.md +11 -1
  161. package/skills/risk-compliance/portfolio-payment-analysis/LICENSE +3 -0
  162. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +170 -0
  163. package/skills/risk-compliance/portfolio-payment-analysis/references/identity-and-reconciliation.md +46 -0
  164. package/skills/risk-compliance/portfolio-payment-analysis/references/metrics-and-reporting.md +170 -0
  165. package/skills/risk-compliance/portfolio-payment-analysis/references/risk-history.md +89 -0
  166. package/skills/risk-compliance/portfolio-payment-analysis/release-check.mjs +95 -0
  167. package/skills/risk-compliance/portfolio-payment-analysis/scripts/analyze.py +636 -0
  168. package/skills/risk-compliance/portfolio-payment-analysis/scripts/build_report.py +386 -0
  169. package/skills/risk-compliance/portfolio-payment-analysis/scripts/comparison_charts.py +425 -0
  170. package/skills/risk-compliance/portfolio-payment-analysis/scripts/csv_export.py +52 -0
  171. package/skills/risk-compliance/portfolio-payment-analysis/scripts/render.py +160 -0
  172. package/skills/risk-compliance/portfolio-payment-analysis/scripts/risk_history.py +435 -0
  173. package/skills/risk-compliance/similar-domain/SKILL.md +9 -4
  174. package/skills/risk-compliance/tld-detection/SKILL.md +124 -239
  175. package/skills/risk-compliance/tld-detection/agents/openai.yaml +1 -1
  176. package/skills/risk-compliance/tld-detection/references/clean.md +18 -51
  177. package/skills/risk-compliance/tld-detection/references/fx-conversion.md +56 -219
  178. package/skills/risk-compliance/tld-detection/references/metric-contract.md +109 -0
  179. package/skills/risk-compliance/tld-detection/references/pattern.md +99 -0
  180. package/skills/risk-compliance/tld-detection/references/report-template.md +49 -272
  181. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +46 -13
  182. package/skills/risk-compliance/tld-detection/scripts/detect_patterns.py +346 -0
  183. package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +51 -19
  184. package/skills/risk-compliance/tld-detection/scripts/profile_groups.py +109 -0
  185. package/skills/risk-compliance/tld-detection/scripts/render_charts.py +218 -0
  186. package/skills/risk-compliance/tld-detection/scripts/tld_common.py +243 -0
  187. package/skills/engineering/contributing/SKILL.md +0 -70
  188. package/skills/risk-compliance/fraud-analysis/SKILL.md +0 -114
  189. package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +0 -27
  190. package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +0 -30
  191. package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +0 -20
  192. package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +0 -20
  193. package/skills/risk-compliance/fraud-analysis/references/workflow.md +0 -175
  194. package/skills/risk-compliance/tld-detection/references/chinese-polish.md +0 -55
  195. package/skills/risk-compliance/tld-detection/references/clustering.md +0 -235
  196. package/skills/risk-compliance/tld-detection/references/decision.md +0 -189
  197. package/skills/risk-compliance/tld-detection/references/streams.md +0 -141
  198. package/skills/risk-compliance/tld-detection/references/terminology.md +0 -95
  199. package/skills/risk-compliance/tld-detection/references/workflow.md +0 -95
  200. /package/skills/{engineering/contributing → core/write-report}/LICENSE +0 -0
  201. /package/skills/core/{write-doc → write-report}/references/reports/audit-report-template.md +0 -0
  202. /package/skills/core/{write-doc → write-report}/references/reports/external-assessment-template.md +0 -0
  203. /package/skills/core/{write-doc → write-report}/references/reports/management-report-template.md +0 -0
  204. /package/skills/core/{write-doc → write-report}/references/reports/vars-report-template.md +0 -0
  205. /package/skills/{engineering/contributing → core/write-report}/release-check.mjs +0 -0
  206. /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/LICENSE +0 -0
  207. /package/skills/engineering/{contributing → engineering}/references/tracker-conventions.md +0 -0
  208. /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/release-check.mjs +0 -0
  209. /package/skills/engineering/{contributing → engineering}/scripts/persist-conventions-core.mjs +0 -0
  210. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/authorized-payment-scam.md +0 -0
  211. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/mechanisms/account-takeover.md +0 -0
  212. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/taxonomy.md +0 -0
  213. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/terminology.md +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: app-debug
3
3
  description: "Diagnose runtime failures in deployed Cloud Run services — 5xx/422 errors, tracebacks, Next.js SSR/hydration or Server Action failures, DB/Secret Manager connection errors, cold-start timeouts, container OOM. NOT for build or deploy failures (cloud-debug)."
4
- release-version: 0.44.0
4
+ release-version: 0.45.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # App Debug
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: better-auth
3
3
  description: "Add or debug Better Auth for Next.js App Router: login, sessions, protected routes, middleware, OAuth (including Feishu/Lark), cookies, Prisma, and Auth.js migration."
4
- release-version: 0.44.0
4
+ release-version: 0.45.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Better Auth for Next.js
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: data-catalog
3
3
  description: "Column definitions for warehouse tables, written inside the Spark/PySpark or SQL script that builds each table (CATALOG_TABLE / CATALOG_FIELDS, or a `-- @field` comment block), with a generated markdown catalog in docs/catalog/ and a CI check that fails a PR whose catalog is stale. Use when writing or changing a table script that adds, renames, removes, or changes the meaning of an output column or feature; when asked what a warehouse column or feature means; to set a repo up ('init the data catalog', 'generate the data dictionary', '字段字典', 'add catalog CI'); or to audit existing SQL or Spark scripts for undocumented columns. NOT for implementing or tuning the Spark job itself (pyspark), DAG wiring (airflow), or general workflow YAML (github-actions)."
4
- release-version: 0.44.0
4
+ release-version: 0.45.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Data catalog
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: data-quality
3
3
  description: "Check a published warehouse table's data against its data-catalog definitions with read-only SELECTs: columns never filled or stuck at one value, counts below zero, ratios outside 0–1, declared values that never appear and undeclared values that do. Then classify each finding as a wrong definition, wrong code, or wrong data. Use when asked whether a table's data is right, to profile or sanity-check a dwd/ads table, to find dead or constant columns, to check a column's values against its documented list, after a pipeline change ships, or when a data-catalog definition and the data might disagree. NOT for validating a Spark change before it ships (pyspark canary and reconciliation), writing definitions (data-catalog), or monitoring dashboards."
4
- release-version: 0.44.0
4
+ release-version: 0.45.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Data quality
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ios-foundation
3
- description: "Set up and prove an Expo / React Native native iOS workspace from a clean checkout to a manual Login smoke on an owned iOS Simulator: isolated worktree and private caches, Xcode/runtime/CocoaPods preflight, frozen install with `pod install --deployment`, platform-scoped prebuild, an ad-hoc-signed Debug simulator build, a private simulator by exact UDID, Metro on a free port, a disposable PostgreSQL backend with owned cleanup, and the evidence record (setup proof only; feature work reports the smoke result). Use for 'set up iOS', 'build the iOS app', 'run the app on the iOS simulator', 'add iOS to the React Native app', 'prove the iOS build', or when a pod install, xcodebuild, simulator boot, Metro connection or Keychain session fails. NOT for Android (android-foundation), signing, TestFlight or App Store release, or general workflow YAML (github-actions)."
4
- release-version: 0.44.0
3
+ description: "Set up and prove an Expo / React Native native iOS workspace from an assigned or fresh isolated workspace to a manual Login smoke on an owned iOS Simulator: isolated workspace and private caches, Xcode/runtime/CocoaPods preflight, frozen install with `pod install --deployment`, platform-scoped prebuild, an ad-hoc-signed Debug simulator build, a private simulator by exact UDID, Metro on a free port, a disposable PostgreSQL backend with owned cleanup, and the evidence record (setup proof only; feature work reports the smoke result). Use for 'set up iOS', 'build the iOS app', 'run the app on the iOS simulator', 'add iOS to the React Native app', 'prove the iOS build', or when a pod install, xcodebuild, simulator boot, Metro connection or Keychain session fails. NOT for Android (android-foundation), signing, TestFlight or App Store release, or general workflow YAML (github-actions)."
4
+ release-version: 0.45.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # iOS foundation
@@ -22,16 +22,19 @@ Pick the mode first:
22
22
 
23
23
  | Step | Read |
24
24
  | --- | --- |
25
- | Shell snippets for every stage below | [references/setup.md](references/setup.md) |
25
+ | Workspace resolution, then shell snippets for every stage | [references/workspace.md](references/workspace.md), [references/setup.md](references/setup.md) |
26
26
  | Proving a product change on the simulator | [Feature work](#feature-work) |
27
27
  | A stage failed | [Triage](#triage) |
28
28
  | Writing up setup proof | [Evidence record](#evidence-record) |
29
29
 
30
30
  ## Rules
31
31
 
32
- - Work in a dedicated worktree on a new branch from `origin/main`. Never overwrite or reuse
33
- another checkout, and never symlink another worktree's `node_modules`, `Pods` or
34
- DerivedData.
32
+ - Resolve the workspace first; [references/workspace.md](references/workspace.md) owns
33
+ precedence: an assigned isolated workspace and candidate HEAD win, feature work, fixes and
34
+ triage reuse the owned workspace, and standalone setup creates one managed workspace only
35
+ with no usable assignment. Read its actual HEAD; never substitute `origin/main`, a new
36
+ branch or another checkout. Never overwrite a peer's checkout, and never symlink another
37
+ worktree's `node_modules`, `Pods` or DerivedData.
35
38
  - Tool shells keep no variables, functions or traps between calls. Write the environment
36
39
  to `.cache/ios-env.sh` once and source it at the start of every command. Run the
37
40
  disposable database and backend as one long-running command so its cleanup trap holds.
@@ -68,7 +71,8 @@ Pick the mode first:
68
71
 
69
72
  Run them in this order. Stop at the first failure and triage it.
70
73
 
71
- 1. **Checkout and environment.** Add the worktree and export the private cache paths,
74
+ 1. **Workspace and environment.** Resolve the assigned or standalone workspace
75
+ ([references/workspace.md](references/workspace.md)) and export the private cache paths,
72
76
  `CP_CACHE_DIR`, `CP_HOME_DIR` and the DerivedData path. Print `xcode-select -p`,
73
77
  `xcodebuild -version`, `xcodebuild -showsdks`, `xcrun simctl list runtimes` and
74
78
  `devicetypes`, and the node, pnpm, ruby and pod versions. Check that `pod --version`
@@ -110,7 +114,8 @@ Run them in this order. Stop at the first failure and triage it.
110
114
 
111
115
  - Clean up only what this run started: the simulator by its exact UDID
112
116
  (`simctl shutdown`, then `simctl delete`), the container by the ID in its CID file, and
113
- the backend and Metro by stopping their own commands.
117
+ the backend and Metro by stopping their own commands. These are short-lived; the
118
+ development workspace, its Pods, DerivedData and private caches outlive them.
114
119
  - Never use `simctl erase all`, `simctl shutdown all`, `killall Simulator`, broad `pkill`,
115
120
  or `docker system prune`.
116
121
  - Give every Docker startup, readiness and cleanup command a 30-second limit. Treat exit
@@ -167,8 +172,8 @@ reliable.
167
172
 
168
173
  For setup proof only. Record:
169
174
 
170
- - Source SHA, host OS and architecture, and the Xcode, iOS SDK, CocoaPods, Ruby, node and
171
- pnpm versions.
175
+ - Source SHA and the workspace root/path/base/branch/owner from the lifecycle record; host OS
176
+ and architecture, and the Xcode, iOS SDK, CocoaPods, Ruby, node and pnpm versions.
172
177
  - The simulator runtime, device type and exact UDID (a run identifier, not a shared
173
178
  resource), and the bundle identifier.
174
179
  - The exit status of pod install, tests, typecheck, export and the native build.
@@ -1,20 +1,31 @@
1
1
  # iOS foundation: shell snippets
2
2
 
3
3
  Replace each `<placeholder>` with the repo's real value. Tool shells do not keep variables,
4
- functions or traps between calls: step 1 writes `.cache/ios-env.sh`, which also `cd`s into
5
- the worktree, and every later block starts by sourcing it by absolute path. The backend
6
- (step 6) and Metro (step 7) each run as one long-running command in its own background shell.
4
+ functions or traps between calls: step 1 resolves the workspace and writes
5
+ `.cache/ios-env.sh`, which also `cd`s into it; every later block starts by sourcing it by
6
+ absolute path. The backend (step 6) and Metro (step 7) each run as one long-running command
7
+ in its own background shell.
7
8
 
8
- ## 1. Checkout and environment
9
+ ## 1. Workspace and environment
10
+
11
+ Workspace precedence is owned by [workspace.md](workspace.md). Do not create a worktree,
12
+ switch branch, fetch or reset when the caller supplied an isolated workspace.
9
13
 
10
14
  ```bash
11
15
  set -euo pipefail
12
- git fetch origin main
13
- git worktree add -b <new-branch> ../<repo>-ios-setup origin/main
14
- cd ../<repo>-ios-setup
15
- git rev-parse HEAD
16
+ ASSIGNED_WORKSPACE=<assigned-workspace-or-empty>
17
+ CANDIDATE_HEAD=<candidate-head-or-empty>
18
+ WORKSPACE="${ASSIGNED_WORKSPACE:?use the caller assignment or native-start result from workspace.md}"
19
+ test -d "$WORKSPACE"
20
+ git -C "$WORKSPACE" rev-parse --is-inside-work-tree >/dev/null
21
+ cd "$WORKSPACE"
22
+ actual_head="$(git rev-parse HEAD)"
23
+ test -z "$CANDIDATE_HEAD" || test "$actual_head" = "$(git rev-parse "$CANDIDATE_HEAD^{commit}")" || {
24
+ echo "workspace HEAD $actual_head != candidate $CANDIDATE_HEAD; report it; do not switch" >&2
25
+ exit 1
26
+ }
16
27
  mkdir -p .cache
17
- { echo 'set -euo pipefail'; printf 'cd %q\n' "$PWD"; } > .cache/ios-env.sh
28
+ { echo 'set -euo pipefail'; printf 'cd %q\n' "$PWD"; printf 'export IOS_SOURCE_HEAD=%q\n' "$actual_head"; } > .cache/ios-env.sh
18
29
  cat >> .cache/ios-env.sh <<'ENV'
19
30
 
20
31
  # Repo-specific names, read from the repo's app config, build script and device harness.
@@ -52,6 +63,11 @@ node --version; pnpm --version; ruby --version; pod --version
52
63
  grep '^COCOAPODS:' "$IOS_DIR/Podfile.lock" # must match pod --version
53
64
  ```
54
65
 
66
+ The recorded standalone path is reused on retries; verify its lifecycle record names this
67
+ run, branch and base. If an assigned path is missing, is not a Git worktree, or is at another
68
+ revision, stop and report instead of creating another checkout. Never fall back to
69
+ `../<repo>-ios-setup`.
70
+
55
71
  `xcode-select -p` must point into `Xcode.app`, not CommandLineTools. The workstation owner
56
72
  selects Xcode and accepts its license; do not run `sudo xcodebuild -license` yourself.
57
73
 
@@ -0,0 +1,82 @@
1
+ # iOS workspace lifecycle
2
+
3
+ The coordinator or caller owns workspace selection. Resolve it once, before any setup stage,
4
+ and record the result. This reference owns only that lifecycle: private caches, Xcode and
5
+ simulator isolation, and backend/Metro cleanup stay in [setup.md](setup.md) and
6
+ [Owned-resource cleanup](../SKILL.md#owned-resource-cleanup).
7
+
8
+ ## Precedence
9
+
10
+ 1. **Assigned workspace and candidate HEAD.** When the caller supplies an isolated workspace
11
+ and a candidate HEAD, use that workspace and that revision. Do not create a worktree or
12
+ branch, fetch, reset or switch revisions. Read `git rev-parse HEAD` and report it as the
13
+ source SHA. If it differs from the candidate HEAD, stop and report the mismatch; never
14
+ switch the workspace to match.
15
+ 2. **Existing owned workspace.** Feature work, fixes, triage and any read-only investigation
16
+ reuse the workspace this run or unit already owns, with its private caches, Pods and
17
+ DerivedData. Do not make a second workspace for a retry, a correction or a read-only
18
+ investigation.
19
+ 3. **Standalone fresh setup.** Only when the coordinator reports no usable assignment. Create
20
+ one managed workspace at
21
+ `<repo>/.resolve-issues/rush/<run-id>/units/native`. Never create a sibling checkout such
22
+ as `../<repo>-ios-setup`, and never add a second managed path for a retry.
23
+
24
+ Android and iOS setup in the same run share that one managed workspace; their private cache
25
+ subdirectories and short-lived processes stay separate.
26
+
27
+ An assigned path that is missing, is not a Git worktree, or is at another revision is not an
28
+ invitation to substitute a fresh checkout: stop and report it. For triage, name the actual
29
+ checkout and HEAD and confirm it contains the failing code; the repository root or another
30
+ checkout may lag the candidate.
31
+
32
+ ## Record before creating
33
+
34
+ The bundled `scripts/workspace-runtime` is generated from the shared lifecycle
35
+ engine and ships with this skill. Only for standalone fresh setup, run:
36
+
37
+ ```bash
38
+ node <skill-base-dir>/scripts/workspace-runtime/bin/workspaces.mjs native-start --repo <canonical-root> \
39
+ --platform ios --target <verified-target-branch> --base <candidate-SHA> --json
40
+ ```
41
+
42
+ The command records root, owner, exact base, branch, workspace and disposable cache
43
+ paths before creating anything. Use its returned `workspace` as `ASSIGNED_WORKSPACE`
44
+ and `sourceSha` as `CANDIDATE_HEAD` in setup. Keep `run` and `runId`; repeat with
45
+ `--run-id <returned-run-id>` and the same base, target and kind for retries. Never
46
+ invoke it inside a coordinator-assigned unit. It never substitutes `origin/main`
47
+ for the requested source or resets an existing workspace.
48
+ For a package below the root, pass `--native-dir <package-root-containing-ios>`.
49
+ Declare other installs or build directories before use with repeated
50
+ `--disposable <exact-workspace-relative-path>`, including package-local `node_modules`.
51
+
52
+ ## Development workspace vs short-lived processes
53
+
54
+ The workspace, its private caches under `.cache/`, its Pods and DerivedData are development
55
+ resources: retain them across retries and fixes, and keep them private to this workspace.
56
+ Never relocate them to a global cache or share them with a concurrent owner.
57
+
58
+ Simulators, backend and Metro children, containers and their databases are short-lived: start
59
+ only those the selected checks need and stop only the ones this run started, under the
60
+ existing [cleanup rules](../SKILL.md#owned-resource-cleanup). Never delete a worktree, remove
61
+ a cache with a chmod or `rm` pattern, or use a broad kill or `simctl erase all`.
62
+
63
+ A read-only investigation does not require a clean environment. Reuse the owned workspace and
64
+ its caches, create only the services the observation needs, and preserve the failed
65
+ environment's evidence. Do not rebuild, reinstall pods or re-provision merely because the
66
+ failure is being investigated.
67
+
68
+ ## Completion
69
+
70
+ At the end of the assignment, return the workspace, its cache paths and any retained process
71
+ records to the coordinator for final cleanup. The coordinator owns deletion and retention.
72
+ Register the path with the run's cleanup owner so final cleanup can tell a delivered
73
+ standalone run from unfinished or unpublished work.
74
+
75
+ - A successful, delivered standalone run must not retain an unbounded cache: mark the
76
+ workspace and its disposable caches for final cleanup. After stopping its owned
77
+ services, run `node <skill-base-dir>/scripts/workspace-runtime/bin/workspaces.mjs native-finish --run <run> --json`
78
+ from the owner checkout. This uses the same cleanup engine as issue workflows.
79
+ - Unfinished or unpublished standalone work keeps the workspace and caches for the next
80
+ attempt; report what is retained and why.
81
+
82
+ The parent lifecycle record is authoritative for ownership, retention and final cleanup.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process'
3
+ import { prepare, cleanup, recover, inventory } from '../workspace-lifecycle.mjs'
4
+ import { nativeStart, nativeFinish } from '../native-workspace.mjs'
5
+
6
+ const allowed = {
7
+ prepare: ['run', 'unit', 'revision', 'apply', 'json'],
8
+ cleanup: ['run', 'unit', 'revision', 'apply', 'json'],
9
+ recover: ['repo', 'json'],
10
+ inventory: ['repo', 'root', 'max-entries', 'max-depth', 'json'],
11
+ 'native-start': ['repo', 'run-id', 'base', 'branch', 'target', 'platform', 'kind', 'native-dir', 'disposable', 'json'],
12
+ 'native-finish': ['run', 'json'],
13
+ }
14
+
15
+ function options(command, args) {
16
+ if (!Object.hasOwn(allowed, command)) throw new Error('expected prepare, cleanup, recover, inventory, native-start or native-finish')
17
+ const out = {}
18
+ for (let index = 0; index < args.length; index++) {
19
+ const name = args[index].replace(/^--/, '')
20
+ if (!args[index].startsWith('--') || !allowed[command].includes(name)) throw new Error(`invalid ${command} option: ${args[index]}`)
21
+ const key = name.replace(/-([a-z])/g, (_, value) => value.toUpperCase())
22
+ if (Object.hasOwn(out, key) && name !== 'disposable') throw new Error(`duplicate option: --${name}`)
23
+ if (name === 'apply' || name === 'json') { out[key] = true; continue }
24
+ const value = args[++index]
25
+ if (value === undefined || value.startsWith('--')) throw new Error(`--${name} needs a value`)
26
+ if (name === 'disposable') (out[key] ??= []).push(value)
27
+ else out[key] = value
28
+ }
29
+ for (const key of ['revision', 'maxEntries', 'maxDepth']) {
30
+ if (out[key] === undefined) continue
31
+ if (!/^\d+$/.test(out[key]) || !Number.isSafeInteger(Number(out[key]))) throw new Error(`invalid ${key}`)
32
+ out[key] = Number(out[key])
33
+ }
34
+ return out
35
+ }
36
+
37
+ try {
38
+ const [command, ...args] = process.argv.slice(2)
39
+ const opts = options(command, args)
40
+ let result
41
+ if (command === 'native-start') result = nativeStart(opts)
42
+ else if (command === 'native-finish') result = nativeFinish(opts)
43
+ else if (command === 'prepare') result = prepare(opts)
44
+ else if (command === 'cleanup') result = cleanup({ ...opts, runGh: (argv) => execFileSync('gh', argv, { encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }) })
45
+ else if (command === 'recover') result = recover(opts)
46
+ else result = inventory(opts)
47
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
48
+ process.exitCode = result.ok === false ? 1 : 0
49
+ } catch (error) {
50
+ process.stderr.write(`${error.message}\n`)
51
+ process.exitCode = 2
52
+ }
@@ -0,0 +1,239 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process'
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
4
+ import { randomUUID } from 'node:crypto'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const cli = fileURLToPath(new URL('./bin/workspaces.mjs', import.meta.url))
8
+ const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'] }).trim()
9
+ const component = (value) => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) && value !== '.' && value !== '..'
10
+
11
+ function stat(path) {
12
+ try { return lstatSync(path) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
13
+ }
14
+
15
+ function noLinks(root, path) {
16
+ const rel = relative(root, path)
17
+ if (rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) throw new Error('workspace path escapes its repository')
18
+ let current = root
19
+ for (const part of rel.split(sep).filter(Boolean)) {
20
+ current = join(current, part)
21
+ if (stat(current)?.isSymbolicLink()) throw new Error(`symlink in managed path: ${current}`)
22
+ }
23
+ }
24
+
25
+ // This lock owns the plan, including initial creation. The engine independently locks
26
+ // workspace operations; its shorter lock cannot protect our read/modify/write cycle.
27
+ function withOwner(repo, run, operation) {
28
+ const locks = join(dirname(run), '.native-locks')
29
+ const lock = join(locks, basename(run))
30
+ noLinks(repo, run)
31
+ noLinks(repo, lock)
32
+ mkdirSync(locks, { recursive: true })
33
+ try { mkdirSync(lock) } catch (error) {
34
+ if (error.code !== 'EEXIST') throw error
35
+ let owner = 'unknown owner'
36
+ try { owner = `owner PID ${JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8')).pid ?? 'unknown'}` } catch { /* Unknown owners also block. */ }
37
+ throw new Error(`native run is locked at ${lock} (${owner}); locks are never reclaimed by age`)
38
+ }
39
+ try {
40
+ writeFileSync(join(lock, 'owner.json'), `${JSON.stringify({ pid: process.pid, run, at: new Date().toISOString() })}\n`, { flag: 'wx', mode: 0o600 })
41
+ return operation()
42
+ } finally {
43
+ try { unlinkSync(join(lock, 'owner.json')) } catch (error) {
44
+ if (error.code !== 'ENOENT') throw error
45
+ } finally {
46
+ rmdirSync(lock)
47
+ try { rmdirSync(locks) } catch (error) { if (!['ENOTEMPTY', 'ENOENT', 'EEXIST'].includes(error.code)) throw error }
48
+ }
49
+ }
50
+ }
51
+
52
+ function planStore(repo, run) {
53
+ const file = join(run, 'task-plan.json')
54
+ const read = () => {
55
+ noLinks(repo, file)
56
+ return stat(file) ? readFileSync(file, 'utf8') : null
57
+ }
58
+ let expected = read()
59
+ const plan = expected === null ? null : JSON.parse(expected)
60
+ let revision = plan?.planRevision ?? -1
61
+ return {
62
+ plan,
63
+ save(next) {
64
+ if (read() !== expected || (expected !== null && JSON.parse(expected).planRevision !== revision)) {
65
+ throw new Error('native plan revision/content changed; refusing to overwrite another owner')
66
+ }
67
+ next.planRevision = revision + 1
68
+ const text = `${JSON.stringify(next, null, 2)}\n`
69
+ const temp = `${file}.${randomUUID()}.tmp`
70
+ try {
71
+ writeFileSync(temp, text, { flag: 'wx', mode: 0o600 })
72
+ if (read() !== expected) throw new Error('native plan changed before persist; refusing to overwrite another owner')
73
+ renameSync(temp, file)
74
+ expected = text
75
+ revision = next.planRevision
76
+ } finally {
77
+ if (stat(temp)) unlinkSync(temp)
78
+ }
79
+ },
80
+ }
81
+ }
82
+
83
+ function ownedPlan(plan, repo, run) {
84
+ const unit = plan?.units?.[0]
85
+ if (plan?.skill !== 'native-setup' || plan.workspaceLifecycle !== 1 || plan.repoRoot !== repo || plan.runDir !== run ||
86
+ !Number.isSafeInteger(plan.planRevision) || plan.planRevision < 0 || plan.units?.length !== 1 || unit?.id !== 'native' ||
87
+ unit.workspace !== join(run, 'units', 'native') || unit.lifecycle?.version !== 1 ||
88
+ !['worktree', 'clone'].includes(unit.lifecycle.workspace?.kind) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(unit.lifecycle.workspace?.baseSha ?? '') ||
89
+ unit.base !== unit.lifecycle.workspace.baseSha || !Array.isArray(unit.lifecycle.resources)) {
90
+ throw new Error('not an owned standalone native run; refusing to replace its record')
91
+ }
92
+ git(repo, 'check-ref-format', '--branch', plan.targetBranch)
93
+ git(repo, 'check-ref-format', '--branch', unit.branch)
94
+ noLinks(repo, unit.workspace)
95
+ return unit
96
+ }
97
+
98
+ function relativePath(value, label, allowRoot = false) {
99
+ if (allowRoot && value === '.') return value
100
+ if (typeof value !== 'string' || !value || isAbsolute(value) || value.includes('\\') || /[\0*?\[\]]/.test(value) ||
101
+ value.split('/').some((part) => !part || part === '.' || part === '..' || part === '.git' || part === '.resolve-issues')) {
102
+ throw new Error(`${label} must be an exact relative path without .., symlinks, or managed roots`)
103
+ }
104
+ return value
105
+ }
106
+
107
+ function resourcePaths(repo, workspace, platform, nativeDir, disposable) {
108
+ relativePath(nativeDir, '--native-dir', true)
109
+ if (!Array.isArray(disposable)) throw new Error('disposable must be an array of workspace-relative paths')
110
+ const paths = [...new Set(['.cache', 'node_modules',
111
+ ...(platform === 'ios' ? ['ios/Pods'] : ['android/.gradle', 'android/build', 'android/app/build']).map((path) => nativeDir === '.' ? path : `${nativeDir}/${path}`),
112
+ ...disposable.map((path) => relativePath(path, '--disposable'))])]
113
+ noLinks(repo, join(repo, nativeDir))
114
+ noLinks(repo, join(workspace, nativeDir))
115
+ for (const path of paths) {
116
+ noLinks(repo, join(repo, path))
117
+ noLinks(repo, join(workspace, path))
118
+ }
119
+ return paths
120
+ }
121
+
122
+ function engine(args, repo) {
123
+ const result = spawnSync(process.execPath, [cli, ...args, '--json'], { cwd: repo, encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024 })
124
+ let value
125
+ try { value = JSON.parse(result.stdout) } catch { /* Report the failed command below. */ }
126
+ if (result.status === 0 || (result.status === 1 && value?.action === 'retain' && (value.lifecycle || value.nextLifecycle))) return value
127
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || result.error?.message || 'workspace operation failed')
128
+ }
129
+
130
+ function returnedLifecycle(result) {
131
+ const lifecycle = result?.lifecycle ?? result?.nextLifecycle
132
+ if (!lifecycle || lifecycle.version !== 1) throw new Error('workspace helper returned no lifecycle transition; the recorded intent remains recoverable')
133
+ return lifecycle
134
+ }
135
+
136
+ export function nativeStart({ repo: input, runId, base, branch, target, platform, kind, nativeDir, disposable = [] }) {
137
+ if (!input || !['android', 'ios'].includes(platform) || (kind !== undefined && !['worktree', 'clone'].includes(kind))) throw new Error('native-start requires --repo and --platform android|ios; --kind is worktree or clone')
138
+ const repo = realpathSync(resolve(input))
139
+ if (realpathSync(git(repo, 'rev-parse', '--show-toplevel')) !== repo) throw new Error('--repo must name the owning checkout root')
140
+ const id = runId ?? `native-${new Date().toISOString().replace(/[^0-9]/g, '')}-${process.pid}`
141
+ if (!component(id)) throw new Error('invalid native run id')
142
+ const run = join(repo, '.resolve-issues', 'rush', id)
143
+ return withOwner(repo, run, () => {
144
+ const store = planStore(repo, run)
145
+ let plan = store.plan
146
+ if (!plan && stat(run)) throw new Error('existing run has no ownership record; refusing to adopt it')
147
+ let unit
148
+ if (plan) {
149
+ unit = ownedPlan(plan, repo, run)
150
+ if ((base !== undefined && git(repo, 'rev-parse', '--verify', `${base}^{commit}`) !== unit.base) ||
151
+ (target !== undefined && target !== plan.targetBranch) || (kind !== undefined && kind !== unit.lifecycle.workspace.kind) ||
152
+ (branch !== undefined && branch !== unit.branch) || (nativeDir !== undefined && nativeDir !== (unit.nativeDir ?? '.'))) {
153
+ throw new Error('existing native run belongs to a different setup; refusing to replace it')
154
+ }
155
+ nativeDir = unit.nativeDir ?? '.'
156
+ } else {
157
+ const baseSha = git(repo, 'rev-parse', '--verify', `${base ?? 'HEAD'}^{commit}`)
158
+ if (!target) {
159
+ try { target = git(repo, 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD').replace(/^origin\//, '') } catch { throw new Error('record the repository target branch with --target; origin/HEAD is unavailable') }
160
+ }
161
+ branch ??= `native/${id}`
162
+ kind ??= 'worktree'
163
+ nativeDir ??= '.'
164
+ git(repo, 'check-ref-format', '--branch', target)
165
+ git(repo, 'check-ref-format', '--branch', branch)
166
+ unit = { id: 'native', title: 'Standalone native setup', workspace: join(run, 'units', 'native'), branch, base: baseSha, nativeDir, disposable: [], status: 'in-flight', stage: 'implement', worker: null,
167
+ lifecycle: { version: 1, workspace: { kind, state: 'preparing', baseSha }, resources: [], cleanup: { state: 'pending' } } }
168
+ plan = { skill: 'native-setup', workspaceLifecycle: 1, repoRoot: repo, runDir: run, targetBranch: target, planRevision: 0, units: [unit] }
169
+ }
170
+ if (unit.lifecycle.cleanup?.state === 'complete') throw new Error('this setup was cleaned; start a new run rather than recreating a used workspace')
171
+ if (!Array.isArray(disposable) || (unit.disposable !== undefined && !Array.isArray(unit.disposable))) throw new Error('disposable must be an array of workspace-relative paths')
172
+ const extras = [...new Set([...(unit.disposable ?? []), ...disposable])]
173
+ const paths = resourcePaths(repo, unit.workspace, platform, nativeDir, extras)
174
+ let changed = !store.plan || unit.nativeDir === undefined || JSON.stringify(unit.disposable) !== JSON.stringify(extras)
175
+ unit.nativeDir = nativeDir
176
+ unit.disposable = extras
177
+ for (const item of paths) {
178
+ const path = join(unit.workspace, item)
179
+ if (unit.lifecycle.resources.some((r) => r.path === path)) continue
180
+ unit.lifecycle.resources.push({ path, role: 'scratch', kind: 'directory', disposable: true, state: 'preparing' })
181
+ changed = true
182
+ }
183
+ if (!store.plan) mkdirSync(run)
184
+ if (changed) store.save(plan)
185
+ const result = engine(['prepare', '--run', run, '--unit', 'native', '--revision', String(plan.planRevision), '--apply'], repo)
186
+ unit.lifecycle = returnedLifecycle(result)
187
+ store.save(plan)
188
+ return { ...result, run, runId: id, workspace: unit.workspace, nativeDir, disposable: extras, sourceSha: git(unit.workspace, 'rev-parse', 'HEAD'), planRevision: plan.planRevision }
189
+ })
190
+ }
191
+
192
+ export function nativeFinish({ run: input }) {
193
+ if (!input) throw new Error('native-finish requires --run')
194
+ const run = resolve(input)
195
+ const repo = realpathSync(join(run, '..', '..', '..'))
196
+ if (!component(basename(run)) || run !== join(repo, '.resolve-issues', 'rush', basename(run))) throw new Error('not an owned standalone native run')
197
+ noLinks(repo, run)
198
+ if (!stat(run)?.isDirectory()) throw new Error('native run does not exist')
199
+ return withOwner(repo, run, () => {
200
+ const store = planStore(repo, run)
201
+ const plan = store.plan
202
+ const unit = ownedPlan(plan, repo, run)
203
+ // Completed and interrupted removals still go through the engine's postcondition
204
+ // checks, including owned resources outside the workspace and cleanup receipts.
205
+ const completeCleanup = () => {
206
+ const result = engine(['cleanup', '--run', run, '--unit', 'native', '--revision', String(plan.planRevision), '--apply'], repo)
207
+ const lifecycle = returnedLifecycle(result)
208
+ if (result.ok !== false && (lifecycle.cleanup?.state !== 'complete' || existsSync(unit.workspace) || lifecycle.resources.some((r) => existsSync(r.path)))) {
209
+ throw new Error('cleanup has not reached its postconditions; the recorded intent remains recoverable')
210
+ }
211
+ unit.lifecycle = lifecycle
212
+ store.save(plan)
213
+ return { ...result, run, planRevision: plan.planRevision }
214
+ }
215
+ if (unit.lifecycle.cleanup?.state === 'complete' || (!existsSync(unit.workspace) && unit.lifecycle.cleanup?.state === 'applying')) return completeCleanup()
216
+ const headSha = git(unit.workspace, 'rev-parse', 'HEAD')
217
+ const remote = git(repo, 'remote', 'get-url', 'origin')
218
+ git(unit.workspace, 'fetch', '--no-tags', '--', remote, `+refs/heads/${plan.targetBranch}:refs/remotes/native-delivery/${plan.targetBranch}`)
219
+ const targetRef = `refs/remotes/native-delivery/${plan.targetBranch}`
220
+ let landed = false
221
+ try { git(unit.workspace, 'merge-base', '--is-ancestor', headSha, targetRef); landed = true } catch (error) {
222
+ if (error.status !== 1) throw error
223
+ }
224
+ if (!landed) {
225
+ unit.headSha = headSha
226
+ const reason = 'native source has not landed on the recorded remote target; retain its environment'
227
+ unit.lifecycle.cleanup = { state: 'retained', headSha, reason }
228
+ store.save(plan)
229
+ return { ok: false, action: 'retain', outcome: 'retained', reason, run, lifecycle: unit.lifecycle, planRevision: plan.planRevision }
230
+ }
231
+ unit.headSha = headSha
232
+ unit.mergeSha = headSha
233
+ unit.status = 'succeeded'
234
+ unit.delivery = 'merged'
235
+ unit.lifecycle.cleanup = { state: 'applying', headSha }
236
+ store.save(plan)
237
+ return completeCleanup()
238
+ })
239
+ }