forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7

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 (268) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +120 -9
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +78 -22
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/validation-receipt.js +190 -0
  107. package/lib/workflow/enforce-stage.js +44 -0
  108. package/lib/workflow/plan-authority.js +225 -0
  109. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  110. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  111. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  112. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  113. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  114. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  115. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  116. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  117. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  118. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  119. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  120. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  121. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  122. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  123. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  124. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  125. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  126. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  127. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  128. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  129. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  130. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  131. package/node_modules/@forge/contracts/index.js +32 -0
  132. package/node_modules/@forge/contracts/package.json +35 -0
  133. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  134. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  135. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  136. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  137. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  138. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  139. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  140. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  141. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  142. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  143. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  144. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  145. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  146. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  147. package/node_modules/@forge/contracts/src/identity.js +42 -0
  148. package/node_modules/@forge/contracts/src/schema.js +72 -0
  149. package/node_modules/@forge/contracts/src/validate.js +305 -0
  150. package/node_modules/@forge/flow/index.js +114 -0
  151. package/node_modules/@forge/flow/package.json +34 -0
  152. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  153. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  154. package/node_modules/@forge/flow/src/executor.js +279 -0
  155. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  156. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  157. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  158. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  159. package/node_modules/@forge/memory/index.js +331 -0
  160. package/node_modules/@forge/memory/package.json +34 -0
  161. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  162. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  163. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  164. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  165. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  166. package/package.json +16 -4
  167. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  168. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  169. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  170. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  171. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  172. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  173. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  174. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  175. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  176. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  177. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  178. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  179. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  180. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  181. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  182. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  183. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  184. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  185. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  186. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  187. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  188. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  189. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  190. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  191. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  192. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  193. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  194. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  195. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  196. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  197. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  198. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  199. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  200. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  201. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  202. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  203. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  204. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  205. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  206. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  207. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  208. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  209. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  210. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  211. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  212. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  213. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  214. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  215. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  216. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  217. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  218. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  219. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  220. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  221. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  222. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  223. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  224. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  225. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  226. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  227. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  228. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  229. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  230. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  231. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  232. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  233. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  234. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  235. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  236. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  237. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  238. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  239. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  240. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  241. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  242. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  243. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  244. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  245. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  246. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  247. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  248. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  249. package/scripts/commitlint.js +13 -15
  250. package/scripts/generate-risk-manifest.js +91 -0
  251. package/scripts/github-context-bridge.sh +10 -0
  252. package/scripts/legacy-claim-repair.js +145 -0
  253. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  254. package/scripts/process-tree.js +14 -2
  255. package/scripts/protected-state-check.js +440 -17
  256. package/scripts/sync-agent-skills.js +333 -34
  257. package/scripts/test-full-suite.js +704 -18
  258. package/scripts/test-profile.js +13 -3
  259. package/scripts/test.js +96 -15
  260. package/skills/coverage.json +1 -0
  261. package/skills/review/SKILL.md +2 -0
  262. package/skills/review/evals/scorecard.json +2 -2
  263. package/skills/setup/SKILL.md +18 -0
  264. package/skills/setup/evals/scorecard.json +3 -3
  265. package/skills/shepherd/SKILL.md +19 -2
  266. package/skills/shepherd/evals/scorecard.json +3 -3
  267. package/skills/validate/SKILL.md +3 -0
  268. package/skills/validate/evals/scorecard.json +1 -1
@@ -1,26 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- /**
5
- * Pre-commit auto-sync for the committed `.agents/skills` mirror.
6
- *
7
- * `.agents/skills` is Codex's repo-local discovery path and the ONE skill mirror
8
- * committed to the repo (so a teammate who clones WITHOUT running `forge setup`
9
- * still gets Forge skills/stages auto-discovered). It must stay byte-identical to
10
- * the canonical `skills/` source, which the structural drift gate enforces.
11
- *
12
- * This hook removes the regen friction: whenever `skills/**` is staged, it
13
- * regenerates `.agents/skills` from `skills/` (reusing the same
14
- * `populateCodexRepoSkills` / `populateAgentSkills` sync the drift gate checks
15
- * against) and re-stages it, so the committed mirror never drifts from canonical.
16
- *
17
- * It is intentionally scoped to `skills/**` via lefthook's `glob`, so it only runs
18
- * when the canonical source actually changes.
19
- */
4
+ /** Command-owned pre-commit sync for the committed `.agents/skills` mirror. */
20
5
 
6
+ const fs = require('node:fs');
21
7
  const path = require('node:path');
22
8
  const { execFileSync } = require('node:child_process');
23
9
  const { populateCodexRepoSkills, resolveCodexRepoSkillsDir } = require('../lib/codex-skills');
10
+ const { gitIgnoredCanonicalPaths, isValidSkillName, listCanonicalSkills } = require('../lib/skills-sync');
11
+ const {
12
+ completeGeneratedHarnessSkillAuthorization,
13
+ issueGeneratedHarnessSkillAuthorization,
14
+ } = require('../lib/protected-state-authority');
15
+
16
+ function strictIgnoredCanonicalPaths(root, runGit) {
17
+ return gitIgnoredCanonicalPaths(root, runGit, { strict: true });
18
+ }
24
19
 
25
20
  function repoRoot() {
26
21
  try {
@@ -33,30 +28,334 @@ function repoRoot() {
33
28
  }
34
29
  }
35
30
 
36
- function main() {
37
- const root = repoRoot();
38
- let written = [];
39
- try {
40
- // clean: true so skills removed from canonical are dropped from the mirror too.
41
- ({ written } = populateCodexRepoSkills({ sourceRoot: root, projectRoot: root, clean: true }));
42
- } catch (error) {
43
- console.error(`sync-agent-skills: failed to regenerate .agents/skills — ${error.message}`);
44
- process.exit(1);
31
+ function actorFromEnv(env) {
32
+ return env.FORGE_ACTOR || env.USER || env.USERNAME || 'unknown';
33
+ }
34
+
35
+ function parseGitPaths(output) {
36
+ return output.split('\0').filter(Boolean);
37
+ }
38
+
39
+ function toRepoPath(value) {
40
+ return path.sep === '\\' ? value.replace(/\\/g, '/') : value;
41
+ }
42
+
43
+ function generatedDriftPaths(root, runGit) {
44
+ const commands = [
45
+ ['diff', '--cached', '--name-only', '-z', '--diff-filter=ACMRDT', '--', '.agents/skills'],
46
+ ['diff', '--name-only', '-z', '--diff-filter=ACMRDT', '--', '.agents/skills'],
47
+ ['ls-files', '-z', '--others', '--exclude-standard', '--', '.agents/skills'],
48
+ ];
49
+ return new Set(commands.flatMap(args => parseGitPaths(runGit('git', args, {
50
+ cwd: root,
51
+ encoding: 'utf8',
52
+ stdio: ['ignore', 'pipe', 'pipe'],
53
+ }))));
54
+ }
55
+
56
+ function gitObjectHash(root, args, content, runGit) {
57
+ const hash = runGit('git', ['hash-object', ...args, '--stdin'], {
58
+ cwd: root,
59
+ encoding: 'utf8',
60
+ input: content,
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ }).trim();
63
+ if (!/^[0-9a-f]{40,64}$/.test(hash)) throw new Error('Git returned an invalid object hash while inspecting canonical skill parity');
64
+ return hash;
65
+ }
66
+
67
+ function worktreeMatchesIndex(root, repoPath, indexedContent, worktreeContent, runGit) {
68
+ return gitObjectHash(root, [], indexedContent, runGit)
69
+ === gitObjectHash(root, [`--path=${repoPath}`], worktreeContent, runGit);
70
+ }
71
+
72
+ function normalizedRepoPath(repoPath) {
73
+ return repoPath.normalize('NFC');
74
+ }
75
+
76
+ function recordNormalizedPath(paths, repoPath) {
77
+ const key = normalizedRepoPath(repoPath);
78
+ const existing = paths.get(key);
79
+ if (existing && existing !== repoPath) throw new Error(`Unicode-equivalent canonical skill paths are ambiguous: ${existing}, ${repoPath}`);
80
+ paths.set(key, repoPath);
81
+ }
82
+
83
+ function ambiguousCanonicalPaths(root, runGit) {
84
+ const indexedEntries = parseGitPaths(runGit('git', ['ls-files', '-t', '-z', '--', 'skills'], {
85
+ cwd: root,
86
+ encoding: 'utf8',
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ }));
89
+ const mirrorSkipWorktree = new Set(parseGitPaths(runGit('git', ['ls-files', '-t', '-z', '--', '.agents/skills'], {
90
+ cwd: root,
91
+ encoding: 'utf8',
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ })).filter(entry => entry.startsWith('S ')).map(entry => normalizedRepoPath(entry.slice(2))));
94
+ const ignoredWorktree = strictIgnoredCanonicalPaths(root, runGit);
95
+ const filesystemPrefixes = listCanonicalSkills(root).map(({ name }) => `skills/${name}/`);
96
+ const canonicalPrefixKeys = new Set(filesystemPrefixes.map(normalizedRepoPath));
97
+ for (const entry of indexedEntries) {
98
+ const match = /^skills\/([^/]+)\/SKILL\.md$/.exec(entry.slice(2));
99
+ if (match && isValidSkillName(match[1])) canonicalPrefixKeys.add(normalizedRepoPath(`skills/${match[1]}/`));
100
+ }
101
+ const participatesInMirror = repoPath => [...canonicalPrefixKeys].some(prefix => normalizedRepoPath(repoPath).startsWith(prefix));
102
+ const indexed = new Map();
103
+ for (const repoPath of indexedEntries.map(entry => entry.slice(2)).filter(participatesInMirror)) recordNormalizedPath(indexed, repoPath);
104
+ const skipWorktree = new Map();
105
+ for (const repoPath of indexedEntries
106
+ .filter(entry => entry.startsWith('S '))
107
+ .map(entry => entry.slice(2))
108
+ .filter(participatesInMirror)) recordNormalizedPath(skipWorktree, repoPath);
109
+ const worktree = new Map();
110
+ for (const prefix of filesystemPrefixes) {
111
+ const sourceRoot = path.join(root, prefix);
112
+ walkRegularFiles(
113
+ sourceRoot,
114
+ (_file, relative) => recordNormalizedPath(worktree, `${prefix}${relative}`),
115
+ sourceRoot,
116
+ (relative, entry) => entry.isSymbolicLink()
117
+ && ignoredWorktree.has(normalizedRepoPath(`${prefix}${relative}`)),
118
+ );
119
+ }
120
+ for (const key of ignoredWorktree) {
121
+ if (!indexed.has(key)) worktree.delete(key);
45
122
  }
123
+ for (const repoPath of skipWorktree.values()) {
124
+ const mirrorPath = `.agents/${repoPath}`;
125
+ if (!fs.existsSync(path.join(root, repoPath))
126
+ && (!mirrorSkipWorktree.has(normalizedRepoPath(mirrorPath)) || fs.existsSync(path.join(root, mirrorPath)))) {
127
+ throw new Error(`asymmetric sparse checkout exposes mirror without canonical: ${repoPath}`);
128
+ }
129
+ }
130
+ const paths = new Set([...indexed.keys(), ...worktree.keys()]);
131
+ return new Set([...paths].filter(key => {
132
+ const indexedPath = indexed.get(key);
133
+ const worktreePath = worktree.get(key);
134
+ const indexedContent = indexedPath ? indexedFile(root, indexedPath, runGit) : null;
135
+ let worktreeContent = null;
136
+ try {
137
+ if (worktreePath) worktreeContent = fs.readFileSync(path.join(root, worktreePath));
138
+ } catch (error) {
139
+ if (error.code !== 'ENOENT') throw error;
140
+ }
141
+ return indexedContent === null
142
+ ? worktreeContent !== null
143
+ : (worktreeContent === null && !skipWorktree.has(key))
144
+ || (worktreeContent !== null && !worktreeMatchesIndex(root, indexedPath, indexedContent, worktreeContent, runGit));
145
+ }).map(key => indexed.get(key) || worktree.get(key)));
146
+ }
147
+
148
+ function indexedFile(root, repoPath, runGit) {
149
+ const tracked = parseGitPaths(runGit('git', ['ls-files', '-z', '--', `:(literal)${repoPath}`], {
150
+ cwd: root,
151
+ encoding: 'utf8',
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ }));
154
+ const trackedPath = tracked.find(candidate => normalizedRepoPath(candidate) === normalizedRepoPath(repoPath));
155
+ if (!trackedPath) return null;
156
+ return runGit('git', ['show', `:${trackedPath}`], {
157
+ cwd: root,
158
+ encoding: null,
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ });
161
+ }
162
+
163
+ function walkRegularFiles(root, visit, current = root, shouldSkip = () => false) {
164
+ if (!fs.existsSync(current)) return;
165
+ const currentStat = fs.lstatSync(current);
166
+ if (!currentStat.isDirectory() || currentStat.isSymbolicLink()) throw new Error(`skill sync path is not a real directory: ${current}`);
167
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
168
+ const entryPath = path.join(current, entry.name);
169
+ const relative = toRepoPath(path.relative(root, entryPath));
170
+ if (shouldSkip(relative, entry)) continue;
171
+ const stat = fs.lstatSync(entryPath);
172
+ if (stat.isSymbolicLink()) throw new Error(`skill sync refuses symlink: ${entryPath}`);
173
+ if (stat.isDirectory()) walkRegularFiles(root, visit, entryPath, shouldSkip);
174
+ else if (stat.isFile()) visit(entryPath, relative);
175
+ }
176
+ }
46
177
 
178
+ function rejectIgnoredSkillDescriptors(root, ignoredCanonical) {
179
+ for (const { name } of listCanonicalSkills(root)) {
180
+ const descriptorRepoPath = `skills/${name}/SKILL.md`;
181
+ if (ignoredCanonical.has(normalizedRepoPath(descriptorRepoPath))) {
182
+ throw new Error(`ignored canonical skill descriptor: ${descriptorRepoPath}`);
183
+ }
184
+ }
185
+ }
186
+
187
+ function changedSkillFiles(root, runGit = execFileSync, ignoredCanonical = strictIgnoredCanonicalPaths(root, runGit)) {
47
188
  const mirror = resolveCodexRepoSkillsDir(root);
48
- try {
49
- // Stage the regenerated mirror (and any deletions within it).
50
- execFileSync('git', ['add', '--all', '--', mirror], {
51
- cwd: root,
52
- stdio: ['ignore', 'ignore', 'inherit'],
189
+ const drift = generatedDriftPaths(root, runGit);
190
+ const changed = [];
191
+ rejectIgnoredSkillDescriptors(root, ignoredCanonical);
192
+ for (const { name, sourcePath: sourceRoot } of listCanonicalSkills(root)) {
193
+ walkRegularFiles(sourceRoot, (sourcePath, relative) => {
194
+ const canonicalRepoPath = `skills/${name}/${relative}`;
195
+ if (ignoredCanonical.has(normalizedRepoPath(canonicalRepoPath))) {
196
+ const mirrorRepoPath = `.agents/${canonicalRepoPath}`;
197
+ if (indexedFile(root, mirrorRepoPath, runGit) !== null || fs.existsSync(path.join(root, mirrorRepoPath))) {
198
+ throw new Error(`ignored canonical skill path still has a mirror: ${canonicalRepoPath}`);
199
+ }
200
+ return;
201
+ }
202
+ const canonical = fs.readFileSync(sourcePath);
203
+ const mirrorPath = path.join(mirror, name, relative);
204
+ const repoPath = `.agents/skills/${name}/${relative}`;
205
+ const indexedCanonical = indexedFile(root, canonicalRepoPath, runGit);
206
+ if (indexedCanonical !== null
207
+ && gitObjectHash(root, [], indexedCanonical, runGit)
208
+ !== gitObjectHash(root, [`--path=${repoPath}`], canonical, runGit)) {
209
+ throw new Error(`canonical and generated mirror clean filters differ: ${canonicalRepoPath}`);
210
+ }
211
+ try {
212
+ if (canonical.equals(fs.readFileSync(mirrorPath)) && !drift.has(repoPath)) return;
213
+ } catch (error) {
214
+ if (error.code !== 'ENOENT') throw error;
215
+ }
216
+ changed.push({ path: repoPath, canonical, writeIntent: 'update' });
217
+ }, sourceRoot, (relative, entry) => entry.isSymbolicLink()
218
+ && ignoredCanonical.has(normalizedRepoPath(`skills/${name}/${relative}`)));
219
+ }
220
+ walkRegularFiles(mirror, (mirrorPath, relative) => {
221
+ const canonicalPath = path.join(root, 'skills', relative);
222
+ if (!fs.existsSync(canonicalPath)) {
223
+ changed.push({
224
+ path: `.agents/skills/${relative}`,
225
+ canonical: fs.readFileSync(mirrorPath),
226
+ writeIntent: 'delete',
227
+ });
228
+ }
229
+ });
230
+ const seen = new Set(changed.map(file => file.path));
231
+ for (const repoPath of drift) {
232
+ if (!repoPath.startsWith('.agents/skills/') || seen.has(repoPath)) continue;
233
+ const relative = repoPath.slice('.agents/skills/'.length);
234
+ const canonicalPath = path.join(root, 'skills', relative);
235
+ const mirrorPath = path.join(root, repoPath);
236
+ if (fs.existsSync(canonicalPath) || fs.existsSync(mirrorPath)) continue;
237
+ let priorContent;
238
+ try {
239
+ priorContent = runGit('git', ['show', `HEAD:${repoPath}`], {
240
+ cwd: root,
241
+ encoding: null,
242
+ stdio: ['ignore', 'pipe', 'pipe'],
243
+ });
244
+ } catch {
245
+ continue;
246
+ }
247
+ changed.push({ path: repoPath, canonical: priorContent, writeIntent: 'delete' });
248
+ }
249
+ return changed;
250
+ }
251
+
252
+ function sameSkillChanges(expected, actual) {
253
+ if (expected.length !== actual.length) return false;
254
+ const byPath = new Map(actual.map(file => [normalizedRepoPath(file.path), file]));
255
+ if (byPath.size !== actual.length) return false;
256
+ return expected.every(file => {
257
+ const candidate = byPath.get(normalizedRepoPath(file.path));
258
+ return candidate
259
+ && candidate.writeIntent === file.writeIntent
260
+ && candidate.canonical.equals(file.canonical);
261
+ });
262
+ }
263
+
264
+ async function syncAgentSkills(options = {}) {
265
+ const root = options.root || repoRoot();
266
+ const env = options.env || process.env;
267
+ const runGit = options.execFileSync || execFileSync;
268
+ const issueAuthorization = options.issueAuthorization || issueGeneratedHarnessSkillAuthorization;
269
+ const completeAuthorization = options.completeAuthorization || completeGeneratedHarnessSkillAuthorization;
270
+ const actor = actorFromEnv(env);
271
+ const sourceHead = runGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
272
+ cwd: root,
273
+ encoding: 'utf8',
274
+ stdio: ['ignore', 'pipe', 'pipe'],
275
+ }).trim();
276
+ if (!/^[0-9a-f]{40}$/.test(sourceHead)) throw new Error('source HEAD is not a full lowercase commit SHA');
277
+
278
+ const ignoredCanonical = strictIgnoredCanonicalPaths(root, runGit);
279
+ rejectIgnoredSkillDescriptors(root, ignoredCanonical);
280
+ const ambiguousCanonical = ambiguousCanonicalPaths(root, runGit);
281
+ for (const canonicalPath of ambiguousCanonical) {
282
+ const indexedCanonical = indexedFile(root, canonicalPath, runGit);
283
+ const indexedMirror = indexedFile(root, `.agents/${canonicalPath}`, runGit);
284
+ if (indexedCanonical === null ? indexedMirror !== null : indexedMirror === null || !indexedCanonical.equals(indexedMirror)) {
285
+ throw new Error(`canonical index differs from generated mirror index: ${canonicalPath}`);
286
+ }
287
+ }
288
+ if (ambiguousCanonical.size > 0) {
289
+ throw new Error(`unstaged canonical skill changes must be reconciled before mirror sync: ${[...ambiguousCanonical].join(', ')}`);
290
+ }
291
+
292
+ const changed = changedSkillFiles(root, runGit, ignoredCanonical);
293
+ const authorizations = [];
294
+ for (const file of changed) {
295
+ const authorization = await issueAuthorization(root, {
296
+ actor,
297
+ path: file.path,
298
+ sourceHead,
299
+ writeIntent: file.writeIntent,
300
+ ...(file.writeIntent === 'delete' ? { priorContent: file.canonical } : {}),
53
301
  });
54
- } catch (error) {
55
- console.error(`sync-agent-skills: failed to stage .agents/skills — ${error.message}`);
56
- process.exit(1);
302
+ if (!authorization.success) throw new Error(`authorization failed for ${file.path}: ${authorization.error}`);
303
+ authorizations.push({ ...file, capabilityId: authorization.capabilityId });
304
+ }
305
+
306
+ const refreshedIgnoredCanonical = strictIgnoredCanonicalPaths(root, runGit);
307
+ rejectIgnoredSkillDescriptors(root, refreshedIgnoredCanonical);
308
+ const refreshedAmbiguousCanonical = ambiguousCanonicalPaths(root, runGit);
309
+ if (refreshedAmbiguousCanonical.size > 0) {
310
+ throw new Error(`canonical skill state changed during authorization: ${[...refreshedAmbiguousCanonical].join(', ')}`);
311
+ }
312
+ const refreshedChanged = changedSkillFiles(root, runGit, refreshedIgnoredCanonical);
313
+ if (!sameSkillChanges(changed, refreshedChanged)) {
314
+ throw new Error('canonical skill state changed during authorization');
315
+ }
316
+
317
+ const excludeRelativePaths = new Set([...refreshedIgnoredCanonical]
318
+ .map(repoPath => repoPath.slice('skills/'.length)));
319
+ const { written } = populateCodexRepoSkills({
320
+ sourceRoot: root,
321
+ projectRoot: root,
322
+ clean: true,
323
+ excludeRelativePaths,
324
+ });
325
+ for (const file of authorizations) {
326
+ const mirrorExists = fs.existsSync(path.join(root, file.path));
327
+ if (file.writeIntent === 'delete' ? mirrorExists : !file.canonical.equals(fs.readFileSync(path.join(root, file.path)))) {
328
+ throw new Error(`canonical mirror intent failed for ${file.path}`);
329
+ }
330
+ const completion = await completeAuthorization(root, {
331
+ actor,
332
+ path: file.path,
333
+ sourceHead,
334
+ capabilityId: file.capabilityId,
335
+ writeIntent: file.writeIntent,
336
+ ...(file.writeIntent === 'delete' ? { priorContent: file.canonical } : {}),
337
+ });
338
+ if (!completion.success) throw new Error(`completion failed for ${file.path}: ${completion.error}`);
57
339
  }
58
340
 
341
+ const mirror = resolveCodexRepoSkillsDir(root);
342
+ runGit('git', ['add', '--all', '--', mirror], {
343
+ cwd: root,
344
+ stdio: ['ignore', 'ignore', 'inherit'],
345
+ });
59
346
  console.log(`sync-agent-skills: .agents/skills in sync with skills/ (${written.length} skills)`);
347
+ return { written, changed: changed.map(file => file.path), deferred: [], sourceHead };
60
348
  }
61
349
 
62
- main();
350
+ async function main() {
351
+ try {
352
+ await syncAgentSkills();
353
+ } catch (error) {
354
+ console.error(`sync-agent-skills: failed — ${error.message}`);
355
+ process.exitCode = 1;
356
+ }
357
+ }
358
+
359
+ if (require.main === module) main();
360
+
361
+ module.exports = { actorFromEnv, changedSkillFiles, parseGitPaths, syncAgentSkills };