@1aboveio/skills 1.0.18 → 1.0.19

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 (76) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +138 -132
  3. package/package.json +4 -4
  4. package/skills/app-dev/airflow/SKILL.md +1 -1
  5. package/skills/app-dev/android-development/SKILL.md +1 -1
  6. package/skills/app-dev/app-debug/SKILL.md +1 -1
  7. package/skills/app-dev/better-auth/SKILL.md +1 -1
  8. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  9. package/skills/app-dev/data-quality/SKILL.md +1 -1
  10. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  11. package/skills/app-dev/maestro/SKILL.md +1 -1
  12. package/skills/app-dev/monorepo/SKILL.md +1 -1
  13. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  14. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  15. package/skills/app-dev/pyspark/SKILL.md +1 -1
  16. package/skills/app-dev/python-backend/SKILL.md +1 -1
  17. package/skills/app-dev/shadcn/SKILL.md +1 -1
  18. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  19. package/skills/cicd/cloud-build/SKILL.md +1 -1
  20. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  21. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  22. package/skills/cicd/docker/SKILL.md +1 -1
  23. package/skills/cicd/github-actions/SKILL.md +1 -1
  24. package/skills/cicd/github-flow/SKILL.md +1 -1
  25. package/skills/cicd/google-cloud/SKILL.md +1 -1
  26. package/skills/cicd/mergify/SKILL.md +1 -1
  27. package/skills/cicd/podman/SKILL.md +1 -1
  28. package/skills/content/article-review/SKILL.md +1 -1
  29. package/skills/content/article-to-image/SKILL.md +1 -1
  30. package/skills/content/flowchart-svg/SKILL.md +1 -1
  31. package/skills/content/image-gen/SKILL.md +1 -1
  32. package/skills/content/mermaid/SKILL.md +1 -1
  33. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  34. package/skills/content/wechat-publisher/SKILL.md +1 -1
  35. package/skills/content/wechat-search/SKILL.md +1 -1
  36. package/skills/content/xai-search/SKILL.md +1 -1
  37. package/skills/core/distill-writing/SKILL.md +1 -1
  38. package/skills/core/doc-to-md/SKILL.md +1 -1
  39. package/skills/core/editorial-review/SKILL.md +1 -1
  40. package/skills/core/house-style/SKILL.md +1 -1
  41. package/skills/core/plain-language/SKILL.md +1 -1
  42. package/skills/core/polish-cn/SKILL.md +1 -1
  43. package/skills/core/reg-parser/SKILL.md +1 -1
  44. package/skills/core/terminology/SKILL.md +1 -1
  45. package/skills/core/write-doc/SKILL.md +1 -1
  46. package/skills/core/write-report/SKILL.md +1 -1
  47. package/skills/engineering/better-review/SKILL.md +1 -1
  48. package/skills/engineering/e2e-test/SKILL.md +1 -1
  49. package/skills/engineering/engineering/SKILL.md +1 -1
  50. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  51. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  52. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  53. package/skills/engineering/golden-path/SKILL.md +1 -1
  54. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  55. package/skills/engineering/resolve-issues/SKILL.md +4 -2
  56. package/skills/engineering/resolve-issues/references/preflight.md +17 -3
  57. package/skills/engineering/resolve-issues/references/retention.md +162 -0
  58. package/skills/engineering/resolve-issues/references/workspaces.md +40 -10
  59. package/skills/engineering/resolve-issues/scripts/daily-cleanup.mjs +169 -0
  60. package/skills/engineering/resolve-issues/scripts/resource-formats.mjs +625 -0
  61. package/skills/engineering/resolve-issues/scripts/retention-lock.mjs +548 -0
  62. package/skills/engineering/resolve-issues/scripts/retention-plans.mjs +57 -0
  63. package/skills/engineering/resolve-issues/scripts/retention.mjs +2314 -0
  64. package/skills/engineering/resolve-release/SKILL.md +1 -1
  65. package/skills/engineering/rush-issues/SKILL.md +1 -1
  66. package/skills/engineering/rush-release/SKILL.md +1 -1
  67. package/skills/engineering/test-quality/SKILL.md +1 -1
  68. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  69. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  70. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  71. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  72. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  73. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  74. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  75. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  76. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
@@ -369,7 +369,7 @@
369
369
  "repository": "1aboveio/ai",
370
370
  "revision": "main"
371
371
  },
372
- "contentDigest": "65d5a8011e64fb3a49cb45bf8fdb066d21138faed67287421bcd17f10b0c7bc0",
372
+ "contentDigest": "93ac20a3d5d41b233b13e521ba1b65d0a3e339958422a56ffad00a844656a23f",
373
373
  "digestExcludes": []
374
374
  },
375
375
  {
@@ -382,7 +382,7 @@
382
382
  "repository": "1aboveio/ai",
383
383
  "revision": "main"
384
384
  },
385
- "contentDigest": "b554ced1c8af517ed49ad3a0c6c2d2f5b2f74bb6e1313b7e56e00177e30bae7c",
385
+ "contentDigest": "915715a605bf78f6f69cb8c95f7227548a6d71a6ef0ce502b5b5c5a357676d19",
386
386
  "digestExcludes": []
387
387
  },
388
388
  {
@@ -395,7 +395,7 @@
395
395
  "repository": "1aboveio/ai",
396
396
  "revision": "main"
397
397
  },
398
- "contentDigest": "8e35a14aea5d7e7b4830f7817438ce6a6eb7c5a6e9c31768aa76c4328e181115",
398
+ "contentDigest": "1ef2fd2ad328c373c7c6fe42007908766860cd8051e1807739797303aaadeae4",
399
399
  "digestExcludes": []
400
400
  },
401
401
  {
@@ -408,7 +408,7 @@
408
408
  "repository": "1aboveio/ai",
409
409
  "revision": "main"
410
410
  },
411
- "contentDigest": "38150518fe71cd567eeee1c76cec15bcd4b0cd5cde9ef6f90748e462807b8b9f",
411
+ "contentDigest": "3c9509027191a2b2612f3905defb5ed9c47580709ba77b46c0e8433d354c4d33",
412
412
  "digestExcludes": []
413
413
  },
414
414
  {
@@ -421,7 +421,7 @@
421
421
  "repository": "1aboveio/ai",
422
422
  "revision": "main"
423
423
  },
424
- "contentDigest": "a53c1f2212fd38fc464ea2b86ce5ed37f61052fc884a4873308bd158b33ee174",
424
+ "contentDigest": "5e419edc77397a9021a24fc9da83d07c83aa786853dcb24916d8fb2655f81ea9",
425
425
  "digestExcludes": []
426
426
  },
427
427
  {
@@ -434,7 +434,7 @@
434
434
  "repository": "1aboveio/ai",
435
435
  "revision": "main"
436
436
  },
437
- "contentDigest": "cc5d24bb0952a4c67bed48bc154fd3ef4115bd3162459411b0a71e48251fe342",
437
+ "contentDigest": "ecee12f24d18d49ec1be030e3f831c3d07a66f37b0b16aa5efef365ca381e558",
438
438
  "digestExcludes": []
439
439
  },
440
440
  {
@@ -447,7 +447,7 @@
447
447
  "repository": "1aboveio/ai",
448
448
  "revision": "main"
449
449
  },
450
- "contentDigest": "3579deef1765207f521b231bd1d21795c791f773f44c634830b0a301c42998f9",
450
+ "contentDigest": "2fbd1a9e15087045fa80b47d9b6db8695009c900c2e4aeeab71207875b071c74",
451
451
  "digestExcludes": []
452
452
  },
453
453
  {
@@ -460,7 +460,7 @@
460
460
  "repository": "1aboveio/ai",
461
461
  "revision": "main"
462
462
  },
463
- "contentDigest": "be5333b8c2dcd229e7caa395274a5e7695dc301a7ad855122d881d73cb99911d",
463
+ "contentDigest": "8c33282acdf9a7b3ba3adf46065249126efdd8669487ad7def1b4302155db3dd",
464
464
  "digestExcludes": []
465
465
  },
466
466
  {
@@ -473,7 +473,7 @@
473
473
  "repository": "1aboveio/ai",
474
474
  "revision": "main"
475
475
  },
476
- "contentDigest": "36bd7b643df2a9217b297f5da084cfd9113f0d578a56721cb302b92f8b9f1aad",
476
+ "contentDigest": "e612b320be1b3aee539a26494e2048e0bbdc1947166fa4cffbdfe8085a64b50a",
477
477
  "digestExcludes": []
478
478
  },
479
479
  {
@@ -486,7 +486,7 @@
486
486
  "repository": "1aboveio/ai",
487
487
  "revision": "main"
488
488
  },
489
- "contentDigest": "6fe762cf05a852f4c52007106e29f606e8c716a29510f7c1d4abc3ec59b96442",
489
+ "contentDigest": "53732d354995ca40c0325b2f860a4facf2e79b9d6d373fbc76a5ce5b3a1f1e29",
490
490
  "digestExcludes": []
491
491
  },
492
492
  {
@@ -499,7 +499,7 @@
499
499
  "repository": "1aboveio/ai",
500
500
  "revision": "main"
501
501
  },
502
- "contentDigest": "f7286a2fe3a4f1c0d233f25fff89f5a46bb0e932d1b6f07b857b5b3e3a398d54",
502
+ "contentDigest": "61f8b7f04887b86338984f03527fdaf66ec90df7ff89c19ee9dca6f362fe212a",
503
503
  "digestExcludes": []
504
504
  },
505
505
  {
@@ -512,13 +512,13 @@
512
512
  "repository": "1aboveio/ai",
513
513
  "revision": "main"
514
514
  },
515
- "contentDigest": "35d54af1d31fb8e0bd0e80929e807be0f8d4a342882897ba1e9bdc396e4e0db6",
515
+ "contentDigest": "ad9bdbfbaeb4c575f8fad5b53367403096594aafce8e686926ddb734c185155a",
516
516
  "digestExcludes": [
517
517
  "coherence/workflow.json"
518
518
  ]
519
519
  }
520
520
  ],
521
- "releaseIdentity": "ea12741260694df094ae6fda1597a15df1e8f31a6db2069e38b8bc1cf13083f2",
521
+ "releaseIdentity": "2ed547516d442d0bfad21952001956420f71a972849049507530f80ad16eda83",
522
522
  "lifecycleAuthority": "native-skills-cli",
523
523
  "repairRecipe": {
524
524
  "id": "engineering-workflow-dependency-first",
@@ -2,7 +2,7 @@
2
2
  name: ensure-coverage
3
3
  description: "Define or audit behavior-first test criteria — ACs, required test levels, mock/integration/E2E policy — across three axes: breadth (Coverage Ledger), depth (over-mocking), presentation (perceivability of browser surfaces). NOT judging whether a single test is any good (test-quality), authoring browser tests (e2e-test), reviewing a PR (better-review), or delivering issues (resolve-issues)."
4
4
  disable-model-invocation: true
5
- release-version: 0.49.0
5
+ release-version: 0.49.1
6
6
  ---
7
7
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
8
8
  # Ensure Coverage
@@ -3,7 +3,7 @@ name: golden-path
3
3
  description: "Golden-path E2E: the short list of user journeys whose failure blocks a release, kept in a repo as one contract (golden-paths.json) with a generated GOLDEN_PATHS.md and a generated GitHub Actions workflow, and run against a local build or a deployed URL for an ALIVE/DEAD verdict. Use to set golden paths up in a repo ('init golden paths', 'add golden-path CI'), to migrate an older smoke.manifest.json or a hand-built smoke setup ('audit', 'migrate smoke'), to add, change, or remove a golden path, to ask 'is the build alive?' or run them before a PR or release, or when a change touches a flow listed in GOLDEN_PATHS.md. NOT for writing the Playwright specs themselves (e2e-test), deciding what a change must prove (ensure-coverage), or general workflow YAML (github-actions)."
4
4
  dependencies:
5
5
  - e2e-test
6
- release-version: 0.49.0
6
+ release-version: 0.49.1
7
7
  ---
8
8
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
9
9
  # Golden path
@@ -3,7 +3,7 @@ name: harness-runtime
3
3
  description: Internal import-only library shared by first-party engineering packages. Not a workflow skill; never select or invoke it as a user capability.
4
4
  disable-model-invocation: true
5
5
  user-invocable: false
6
- release-version: 0.49.0
6
+ release-version: 0.49.1
7
7
  ---
8
8
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
9
9
  # Harness Runtime
@@ -8,7 +8,7 @@ dependencies:
8
8
  - better-review
9
9
  - golden-path
10
10
  - harness-runtime
11
- release-version: 0.49.0
11
+ release-version: 0.49.1
12
12
  ---
13
13
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
14
14
  # Resolve issues
@@ -83,6 +83,7 @@ Read each procedure right before its stage; never substitute your own.
83
83
  | Implementation loop and opening each PR | [references/implementation.md](references/implementation.md) |
84
84
  | Read-only warehouse canary | [references/canary.md](references/canary.md) |
85
85
  | Owned workspaces: plan, prepare, cleanup, recover, inventory | [references/workspaces.md](references/workspaces.md) |
86
+ | Daily resource retention: enrollment, age tiers, preview, registration, apply | [references/retention.md](references/retention.md) |
86
87
  | Independent review | [references/review.md](references/review.md) |
87
88
  | Routing small problems; posting them at the end | [references/dropbox.md](references/dropbox.md) |
88
89
  | CI, queue, and merge | [references/ci.md](references/ci.md) |
@@ -143,7 +144,8 @@ Treat a missing or unreadable result as incomplete; resume a dead spawn once in
143
144
 
144
145
  Follow [preflight.md](references/preflight.md):
145
146
 
146
- 0. **Ready label:** resume the spec's run if one exists; run `ready-check.mjs`;
147
+ 0. **Retention and ready label:** enroll this repository's daily resource cleanup
148
+ through the persistent installed skill; resume the spec's run if one exists; run `ready-check.mjs`;
147
149
  stop on an `unreadable` outcome; ask at step 6 on `unlabeled` or a `missing` compaction
148
150
  hook (`install-hooks.mjs --check`; shortcut: warn).
149
151
  1. **Model slots:** record the fast-path fills without asking; the models and
@@ -3,7 +3,21 @@
3
3
  Run these steps in order, in every run, including a one-unit run. Ask nothing
4
4
  until step 6.
5
5
 
6
- ## 0. Ready label
6
+ ## 0. Repository retention and ready label
7
+
8
+ Enroll this repository's daily tiered retention through the installed skill copy:
9
+
10
+ ```bash
11
+ node <skill-base-dir>/scripts/daily-cleanup.mjs install --repo <canonical-repo-root>
12
+ ```
13
+
14
+ Repeat on each invocation to refresh the same job, never discover sibling projects.
15
+ Use a persistent installed skill path, not a temporary worktree. Report an enrollment
16
+ failure as "daily cleanup not enabled" and continue the issue workflow. The daily
17
+ job cleans only explicitly registered disposable resources; it preserves run history.
18
+ See [retention.md](retention.md) for age tiers, registration and uninstall. Register
19
+ owned reconstructable resources when they are created and update their activity before
20
+ reuse; never turn an inventory guess into deletion authority.
7
21
 
8
22
  **One run per spec.** Before anything else, find the spec's run:
9
23
 
@@ -52,8 +66,8 @@ The script exits 0 whenever it ran (2 if it could not); read `outcome` in its JS
52
66
  planned from an issue that cannot be read.
53
67
  - **Exit 2** (the script could not run): fix the issue reference and rerun.
54
68
 
55
- Skip this step for a local or pasted spec. Count only GitHub's native
56
- sub-issues, never a checklist.
69
+ Skip only the ready-label check for a local or pasted spec; repository retention
70
+ enrollment still runs. Count only GitHub's native sub-issues, never a checklist.
57
71
 
58
72
  ### Compaction hook
59
73
 
@@ -0,0 +1,162 @@
1
+ # Daily resource retention
2
+
3
+ Keep run history and reclaim explicitly owned, reconstructable generated resources.
4
+ Enrollment belongs to the repository where `resolve-issues` or `rush-issues` is invoked;
5
+ it never discovers or enrolls sibling projects.
6
+
7
+ ## Enroll at preflight
8
+
9
+ Use the persistent installed skill copy and canonical main checkout:
10
+
11
+ ```bash
12
+ node <skill-base-dir>/scripts/daily-cleanup.mjs install --repo <repo-root>
13
+ ```
14
+
15
+ Repeat enrollment on each invocation to refresh the same job, including after a
16
+ skill or Node update. The installer prefers the stable PATH alias for its current
17
+ Node runtime. The skill directory must remain available; a temporary checkout or
18
+ unpack directory is not an installation.
19
+
20
+ macOS runs a user LaunchAgent every 12 hours and at load. Linux uses a persistent
21
+ user systemd daily timer with boot catch-up. The scheduler must be available; it
22
+ does not wake a powered-off machine. Report an unsupported host or failed enrollment
23
+ as "daily cleanup not enabled" and continue the issue workflow. A saved job file
24
+ alone does not prove that the job is loaded.
25
+
26
+ ```bash
27
+ node <skill-base-dir>/scripts/daily-cleanup.mjs plan --repo <repo-root>
28
+ node <skill-base-dir>/scripts/daily-cleanup.mjs uninstall --repo <repo-root>
29
+ ```
30
+
31
+ ## Preserve records; register resources
32
+
33
+ Keep plans, source requirements, decisions, findings, results, review verdicts,
34
+ progress and retry history, checkpoints, Git metadata, unpublished work, required
35
+ evidence and credentials. A directory name or an old timestamp proves no authority.
36
+ Whole-workspace cleanup still follows [the delivered-unit lifecycle](workspaces.md#cleanup-after-delivery-before-finish).
37
+
38
+ The default age floors are seven full days for generated resources and 30 full
39
+ days for explicitly disposable raw artifacts. Recorded activity authorizes the age
40
+ calculation; newer filesystem writes can only postpone removal. Eligible targets
41
+ are ranked by measured allocation, largest first. Shared blocks, hardlinks and local
42
+ filesystem snapshots mean allocation removed is not necessarily free space gained.
43
+ Report the two measurements separately. Removing OS backup snapshots is a separate,
44
+ explicitly authorized operation, never part of this scheduler.
45
+
46
+ The manifest `<repo>/.resolve-issues/retention.json` binds each resource to its
47
+ run/unit, exact path and filesystem identity, activity, reconstruction recipe and
48
+ explicit registration. Register only paths already owned by the task plan. A
49
+ legacy path needs explicit ownership reconciliation first; inventory never adopts it.
50
+
51
+ Registration records inventory; it does not release an unfinished owner's resource
52
+ for deletion. Declare each generated resource's `retention` intent in the task plan
53
+ as described in [workspaces.md](workspaces.md#what-the-plan-records). Register the
54
+ ready declarations in one operation before worker admission:
55
+
56
+ ```bash
57
+ node <skill-base-dir>/scripts/retention.mjs register-owned \
58
+ --repo <repo-root> --run <absolute-run> --unit <unit-id> \
59
+ --last-used-at <current-ISO-timestamp> --apply --json
60
+ ```
61
+
62
+ Without `--apply`, this previews registration and writes nothing. The command
63
+ considers only `lifecycle.resources` and their retention intent; legacy top-level
64
+ resource lists require explicit reconciliation. It never discovers neighboring
65
+ paths. Exit 0 means complete coverage, 1 means deferred or uncovered resources,
66
+ and 2 means the call was refused. Record its registered, deferred and uncovered results.
67
+ Refresh activity before reuse and repeat registration for newly populated resources
68
+ after their producing worker has settled. Keep that worker's resource report with
69
+ its result. A refusal means the resource remains unregistered; a loaded daily job
70
+ does not mean that every generated environment is covered.
71
+
72
+ ```bash
73
+ node <skill-base-dir>/scripts/retention.mjs register \
74
+ --repo <repo-root> --run <absolute-run> --unit <unit-id> \
75
+ --path <absolute-generated-child> --kind generated \
76
+ --last-used-at <ISO-timestamp> --recipe '<reconstruction instructions>' --json
77
+ ```
78
+
79
+ A resource containing credentials, private configuration, unique scripts or required
80
+ proof is retained. Malformed required-evidence declarations also retain candidates;
81
+ fix the ownership/evidence record rather than ignoring it. Registration and every
82
+ apply pass check these boundaries. Explicitly optional evidence is not a required pin.
83
+
84
+ For an unfinished task, use `--release-owner` only after explicit resource release
85
+ has been authorized and its reconstruction inputs remain available. It releases the
86
+ resource, not the task or source. Re-registering before reuse withdraws that release;
87
+ an existing pin remains pinned.
88
+
89
+ ## Verify generated formats and rebuild inputs
90
+
91
+ Use `generic` for explicitly classified caches without a supported format. Its
92
+ conservative metadata and link checks remain in force. A supported generated format
93
+ can distinguish installed package source directories from the run's own source:
94
+
95
+ | Format | Positive proof | Retained reconstruction inputs |
96
+ | --- | --- | --- |
97
+ | `pnpm-install` | pnpm installation metadata and virtual store | `package.json`, `pnpm-lock.yaml` |
98
+ | `pnpm-store` | versioned content-addressed package store | `package.json`, `pnpm-lock.yaml` |
99
+ | `uv-venv` | virtual-environment configuration, interpreter and installed-package layout | `pyproject.toml`, `uv.lock` |
100
+ | `next-build` | Next.js build output markers | `package.json`, `pnpm-lock.yaml` |
101
+
102
+ Supply the format and actual input paths explicitly. The inputs must be canonical
103
+ regular files outside the disposable target. The registration records their identity
104
+ and content hashes; changed or uncertain inputs require reassessment. Format proof
105
+ does not exempt credentials, nested Git, run metadata, required evidence or ownership
106
+ checks. Links in a verified generated installation are references: removal unlinks
107
+ them and preserves their destinations.
108
+
109
+ ```bash
110
+ node <skill-base-dir>/scripts/retention.mjs register \
111
+ --repo <repo-root> --run <absolute-run> --unit <unit-id> \
112
+ --path <workspace>/node_modules --kind generated --format pnpm-install \
113
+ --input <workspace>/package.json --input <workspace>/pnpm-lock.yaml \
114
+ --last-used-at <ISO-timestamp> --recipe 'pnpm install --frozen-lockfile' --json
115
+ ```
116
+
117
+ ## Check before reuse
118
+
119
+ Before admitting a worker that needs a registered resource, inspect its state:
120
+
121
+ ```bash
122
+ node <skill-base-dir>/scripts/retention.mjs status \
123
+ --repo <repo-root> --run <absolute-run> --unit <unit-id> --json
124
+ ```
125
+
126
+ Read `rebuildRequired`, the reason and retained recipe. A pin or protected credential
127
+ can prevent deletion without making a valid installation need reconstruction.
128
+ Readiness and cleanup eligibility are separate. A missing tree, changed
129
+ reconstruction inputs or a partial installation is not ready merely because its
130
+ parent directory exists. This includes environments left with protected credential
131
+ or configuration fragments after an explicitly authorized manual cleanup.
132
+
133
+ An unregistered or legacy resource has no readiness proof from this command. Read
134
+ its prior cleanup receipts and verify the repository's install/build contract before
135
+ reuse; do not infer readiness from an empty status result.
136
+
137
+ Reconstruct with the repository's retained lockfile and existing install/build
138
+ contract before continuing. Preserve protected files at their original paths; do
139
+ not run a clear/reset command that would discard them. Do not execute an arbitrary
140
+ recipe from an untrusted manifest. Verify the reconstructed format and then
141
+ re-register its new identity and current activity without `--release-owner`.
142
+ Until those steps pass, the dependent worker waits and the task stays unfinished.
143
+
144
+ ## Preview, apply and recover
145
+
146
+ ```bash
147
+ node <skill-base-dir>/scripts/retention.mjs plan --repo <repo-root> --json
148
+ node <skill-base-dir>/scripts/retention.mjs apply --repo <repo-root> --apply --json
149
+ node <skill-base-dir>/scripts/retention.mjs recover --repo <repo-root> --json
150
+ ```
151
+
152
+ Apply locks retention and rechecks owner activity, claims, required evidence, source,
153
+ filesystem identity and process use before each removal. Receipts live outside the
154
+ target under `.resolve-issues/retention-receipts`. A partial removal is a failure;
155
+ preserve both the original failure and cleanup errors.
156
+
157
+ Recovery is read-only by default. `recover --apply` may clear only a lock whose
158
+ record proves its owner is dead on this host. Live, foreign, unreadable, legacy or
159
+ ambiguous locks stay. The daily apply path can make the same proven-dead-owner
160
+ recovery. It never breaks a lock by age. An interrupted receipt or quarantined tree
161
+ remains visible for reconciliation; releasing its dead lock does not declare its
162
+ cleanup complete or remove the quarantined data.
@@ -17,7 +17,9 @@ checkout) and `targetBranch`. A unit already records `workspace`, `branch` and
17
17
  "baseSha": "<exact SHA>", "identity": "<returned by the helper>" },
18
18
  "resources": [ { "role": "review|scratch|tools", "kind": "directory|clone|worktree",
19
19
  "path": "<absolute>", "disposable": true,
20
- "state": "preparing|ready", "identity": "<returned by the helper>" } ],
20
+ "state": "preparing|ready", "identity": "<returned by the helper>",
21
+ "retention": { "kind": "generated", "format": "generic|pnpm-install|pnpm-store|uv-venv|next-build",
22
+ "recipe": "<retained rebuild command>", "inputs": ["<absolute source input>"] } } ],
21
23
  "cleanup": { "state": "pending|applying|complete|retained",
22
24
  "headSha": "<the unit's headSha>", "reason": "<retained only>" } }
23
25
  ```
@@ -75,11 +77,31 @@ Do not admit another worker while that owner's cleanup is `applying`; the
75
77
  3. Persist the returned `nextLifecycle` on the unit **before** spawning the
76
78
  worker, and give the worker that workspace path and only the resources it
77
79
  needs. Resources are recorded before they are used.
78
- 4. Reuse it. Retries, fix rounds, and every review round run in the same
79
- workspace and the same review clone; a later prepare verifies identity,
80
- branch, and base instead of recreating. Never reset, replace, or allocate a
81
- second tree for one attempt, and plan a private tool cache or scratch copy
82
- only when the work needs one.
80
+ 4. Each disposable generated resource carries its explicit `retention` intent:
81
+ kind, supported format, reconstruction recipe and retained source input paths
82
+ ([retention.md](retention.md#verify-generated-formats-and-rebuild-inputs)).
83
+ After persisting the ready lifecycle and before worker admission, run:
84
+
85
+ ```bash
86
+ node <skill-base-dir>/scripts/retention.mjs register-owned \
87
+ --repo <repo-root> --run <run> --unit <unit> \
88
+ --last-used-at <current-ISO-timestamp> --apply --json
89
+ ```
90
+
91
+ The command considers only declared resources. Record its registered, deferred
92
+ and uncovered results; report a refusal rather than claiming full daily coverage.
93
+ An empty typed installation can be deferred while its producing worker installs
94
+ it. After that worker settles and its handle is cleared, repeat registration
95
+ for the populated resource. Registration grants no early release of unfinished
96
+ work. Whole workspaces, credential directories, unique scripts and required
97
+ evidence stay outside disposable declarations.
98
+ 5. Before reusing a registered resource, run the read-only
99
+ [rebuild status check](retention.md#check-before-reuse). Rebuild a missing or
100
+ partial installation with the retained repository contract before admitting its
101
+ dependent worker; directory existence is not readiness. Refresh registration and
102
+ activity without releasing the owner after reconstruction. Retries, fix rounds
103
+ and review rounds reuse the same workspace and review clone. A later prepare
104
+ verifies identity, branch and base; it does not reset or replace them.
83
105
 
84
106
  `kind`: a linked `worktree` where a plain checkout is enough; an independent
85
107
  `clone` when the first Git-mutating worker runs in a sandbox that cannot write a
@@ -126,8 +148,10 @@ It removes nothing else: no ignored-file stripping, no pruning, and no
126
148
  answer retains everything, whole. Keep, never clean: a blocked, cancelled,
127
149
  pending, or supervised merge-ready unit's environment, whatever its timestamp; a
128
150
  tree a worker may still use; a tree whose ownership or delivery cannot be proved.
129
- No cleanup runs by age, idle time, or a name pattern, and a legacy
130
- `*-workspaces` directory is never adopted or deleted.
151
+ Whole-workspace cleanup never runs by age, idle time, or a name pattern, and a legacy
152
+ `*-workspaces` directory is never adopted or deleted. Separate [resource retention](retention.md)
153
+ uses explicit registrations and age thresholds for reconstructable children while keeping
154
+ the workspace, Git history and progress records.
131
155
 
132
156
  ## Evidence and handoff
133
157
 
@@ -149,8 +173,14 @@ On startup, before scheduling, read only:
149
173
  node <skill-base-dir>/scripts/cleanup-runs.mjs recover --repo <repo-root> --json
150
174
  ```
151
175
 
152
- It lists unfinished recorded operations — a `preparing` workspace, an `applying`
153
- cleanup, a lock, an orphaned worktree — in every run regardless of age. It
176
+ Also inspect resource-retention recovery before admission:
177
+
178
+ ```bash
179
+ node <skill-base-dir>/scripts/retention.mjs recover --repo <repo-root> --json
180
+ ```
181
+
182
+ Workspace recovery lists unfinished recorded operations — a `preparing` workspace,
183
+ an `applying` cleanup, a lock, an orphaned worktree — in every run regardless of age. It
154
184
  activates no workflow and cleans nothing by itself: continue the plan, then
155
185
  prepare or clean what it names and persist the result.
156
186
 
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ // Enroll only the explicitly named repository; no project-directory discovery.
3
+ import { createHash } from 'node:crypto'
4
+ import { execFileSync, spawnSync } from 'node:child_process'
5
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
6
+ import { homedir } from 'node:os'
7
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
10
+
11
+ const SCRIPT = fileURLToPath(import.meta.url)
12
+ const RETENTION = join(dirname(SCRIPT), 'retention.mjs')
13
+ const PATH = '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
14
+
15
+ function days(value) {
16
+ const number = Number(value)
17
+ if (!Number.isSafeInteger(number) || number < 1) throw new Error('--older-than-days must be a positive integer')
18
+ return number
19
+ }
20
+
21
+ function escapeXml(value) {
22
+ return String(value).replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[char])
23
+ }
24
+
25
+ function systemdArgument(value) {
26
+ if (/[\r\n\0]/.test(value)) throw new Error('Scheduler paths cannot contain line breaks or NUL')
27
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%').replace(/\$/g, '$$')}"`
28
+ }
29
+
30
+ // Prefer the PATH alias for this exact runtime. Package managers can remove a
31
+ // versioned Cellar directory while keeping their stable opt/bin alias current.
32
+ export function durableNodePath(executable = process.execPath, searchPath = process.env.PATH ?? '') {
33
+ let identity
34
+ try { identity = realpathSync(executable) } catch { return executable }
35
+ for (const directory of searchPath.split(delimiter)) {
36
+ if (!isAbsolute(directory)) continue
37
+ const candidate = join(directory, 'node')
38
+ try { if (realpathSync(candidate) === identity) return candidate } catch { /* not this runtime */ }
39
+ }
40
+ return executable
41
+ }
42
+
43
+ export function schedulePlan({ repo, olderThanDays = 30, platform = process.platform,
44
+ home = homedir(), node = durableNodePath(), script = SCRIPT } = {}) {
45
+ if (!isAbsolute(repo ?? '')) throw new Error('--repo must be an absolute repository path')
46
+ const canonical = realpathSync(repo)
47
+ if (!existsSync(join(canonical, '.git'))) throw new Error('Daily cleanup requires a Git repository root')
48
+ if (!isAbsolute(home) || !isAbsolute(node) || !isAbsolute(script)) throw new Error('Scheduler paths must be absolute')
49
+ const retention = days(olderThanDays)
50
+ const id = `io.1above.resolve-issues.cleanup.${createHash('sha256').update(canonical).digest('hex').slice(0, 20)}`
51
+ const args = [node, script, 'run', '--repo', canonical, '--older-than-days', String(retention), '--apply']
52
+ if (args.some(value => /[\r\n\0]/.test(value))) throw new Error('Scheduler paths cannot contain line breaks or NUL')
53
+ if (platform === 'darwin') {
54
+ const logs = join(home, 'Library', 'Logs', 'resolve-issues')
55
+ const file = join(home, 'Library', 'LaunchAgents', `${id}.plist`)
56
+ const content = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${id}</string>\n<key>ProgramArguments</key><array>${args.map(arg => `<string>${escapeXml(arg)}</string>`).join('')}</array>\n<key>RunAtLoad</key><true/>\n<key>StartInterval</key><integer>43200</integer>\n<key>EnvironmentVariables</key><dict><key>PATH</key><string>${PATH}</string></dict>\n<key>StandardOutPath</key><string>${escapeXml(join(logs, `${id}.log`))}</string>\n<key>StandardErrorPath</key><string>${escapeXml(join(logs, `${id}.err`))}</string>\n</dict></plist>\n`
57
+ return { repo: canonical, id, retentionDays: retention, platform, files: [{ path: file, content }], directories: [logs], args }
58
+ }
59
+ if (platform === 'linux') {
60
+ const root = join(home, '.config', 'systemd', 'user')
61
+ const service = `[Unit]\nDescription=Reclaim declared disposable resolve-issues resources for ${id}\n\n[Service]\nType=oneshot\nEnvironment=PATH=${PATH}\nExecStart=${args.map(systemdArgument).join(' ')}\nTimeoutStartSec=30min\n`
62
+ const timer = `[Unit]\nDescription=Daily tiered resource retention for ${id}\n\n[Timer]\nOnCalendar=daily\nOnBootSec=5min\nPersistent=true\nUnit=${id}.service\n\n[Install]\nWantedBy=timers.target\n`
63
+ return { repo: canonical, id, retentionDays: retention, platform,
64
+ files: [{ path: join(root, `${id}.service`), content: service }, { path: join(root, `${id}.timer`), content: timer }], directories: [], args }
65
+ }
66
+ throw new Error(`Daily cleanup scheduling is unsupported on ${platform}; no job was installed`)
67
+ }
68
+
69
+ const defaultRun = (command, args) => execFileSync(command, args, { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] })
70
+
71
+ export function installSchedule(options = {}, { run = defaultRun, uid = process.getuid?.() } = {}) {
72
+ const plan = schedulePlan(options)
73
+ for (const file of plan.files) {
74
+ let current = file.path
75
+ while (current !== dirname(current)) {
76
+ try {
77
+ if (lstatSync(current).isSymbolicLink()) throw new Error(`Scheduler path is a symbolic link: ${current}`)
78
+ } catch (error) { if (error.code !== 'ENOENT') throw error }
79
+ current = dirname(current)
80
+ }
81
+ }
82
+ const domain = `gui/${uid}`
83
+ const same = plan.files.every(file => existsSync(file.path) && readFileSync(file.path, 'utf8') === file.content)
84
+ if (same) {
85
+ try {
86
+ if (plan.platform === 'darwin') run('launchctl', ['print', `${domain}/${plan.id}`])
87
+ else {
88
+ run('systemctl', ['--user', 'is-enabled', `${plan.id}.timer`])
89
+ run('systemctl', ['--user', 'is-active', `${plan.id}.timer`])
90
+ }
91
+ return { ok: true, action: 'unchanged', ...plan }
92
+ } catch { /* A saved job is not necessarily loaded. */ }
93
+ }
94
+ // Preflight the user scheduler before saving any files.
95
+ if (plan.platform === 'darwin') run('launchctl', ['print', domain])
96
+ else run('systemctl', ['--user', 'show-environment'])
97
+ for (const path of plan.directories) mkdirSync(path, { recursive: true })
98
+ if (plan.platform === 'darwin') {
99
+ let loaded = false
100
+ try { run('launchctl', ['print', `${domain}/${plan.id}`]); loaded = true } catch { /* absent */ }
101
+ if (loaded) run('launchctl', ['bootout', `${domain}/${plan.id}`])
102
+ }
103
+ for (const file of plan.files) {
104
+ mkdirSync(dirname(file.path), { recursive: true })
105
+ writeFileSync(file.path, file.content, { mode: 0o600 })
106
+ }
107
+ if (plan.platform === 'darwin') run('launchctl', ['bootstrap', domain, plan.files[0].path])
108
+ else {
109
+ run('systemctl', ['--user', 'daemon-reload'])
110
+ run('systemctl', ['--user', 'enable', '--now', `${plan.id}.timer`])
111
+ }
112
+ return { ok: true, action: 'installed', ...plan }
113
+ }
114
+
115
+ export function uninstallSchedule(options = {}, { run = defaultRun, uid = process.getuid?.() } = {}) {
116
+ const plan = schedulePlan(options)
117
+ if (plan.platform === 'darwin') {
118
+ let loaded = false
119
+ try { run('launchctl', ['print', `gui/${uid}/${plan.id}`]); loaded = true } catch { /* absent */ }
120
+ if (loaded) run('launchctl', ['bootout', `gui/${uid}/${plan.id}`])
121
+ } else run('systemctl', ['--user', 'disable', '--now', `${plan.id}.timer`])
122
+ for (const file of plan.files) rmSync(file.path, { force: true })
123
+ if (plan.platform === 'linux') run('systemctl', ['--user', 'daemon-reload'])
124
+ return { ok: true, action: 'uninstalled', repo: plan.repo, id: plan.id }
125
+ }
126
+
127
+ export function runDaily({ repo, olderThanDays = 30, apply = false } = {}, run = spawnSync) {
128
+ if (!isAbsolute(repo ?? '')) throw new Error('--repo must be absolute')
129
+ const args = [RETENTION, apply ? 'apply' : 'plan', '--repo', realpathSync(repo), '--generated-days', '7', '--artifact-days', String(days(olderThanDays)), '--json']
130
+ if (apply) args.push('--apply')
131
+ const result = run(process.execPath, args, { encoding: 'utf8', timeout: 25 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 })
132
+ if (result.error) throw result.error
133
+ return { status: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
134
+ }
135
+
136
+ export function main(argv) {
137
+ try {
138
+ const [command, ...rest] = argv
139
+ const options = {}
140
+ for (let i = 0; i < rest.length; i++) {
141
+ if (rest[i] === '--apply') { options.apply = true; continue }
142
+ if (!['--repo', '--older-than-days'].includes(rest[i])) throw new Error(`Unknown argument ${rest[i]}`)
143
+ const flag = rest[i]
144
+ const value = rest[++i]
145
+ if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`)
146
+ options[flag === '--repo' ? 'repo' : 'olderThanDays'] = value
147
+ }
148
+ if (!options.repo) throw new Error('--repo is required')
149
+ options.repo = resolve(options.repo)
150
+ if (command === 'run') {
151
+ const result = runDaily(options)
152
+ process.stdout.write(result.stdout)
153
+ process.stderr.write(result.stderr)
154
+ return result.status
155
+ }
156
+ if (options.apply) throw new Error('--apply is only valid with run')
157
+ const result = command === 'install' ? installSchedule(options)
158
+ : command === 'uninstall' ? uninstallSchedule(options)
159
+ : command === 'plan' ? schedulePlan(options) : null
160
+ if (!result) throw new Error('Usage: daily-cleanup.mjs install|uninstall|plan|run --repo PATH [--older-than-days 30] [--apply]')
161
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
162
+ return 0
163
+ } catch (error) {
164
+ process.stderr.write(`daily-cleanup: ${error.message}\n`)
165
+ return 1
166
+ }
167
+ }
168
+
169
+ if (isMainModule(import.meta.url)) process.exitCode = main(process.argv.slice(2))