@aiwg/cli 2026.10.5 → 2026.10.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 (263) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +7 -3
  6. package/dist/src/a2a/agent-card.js +11 -0
  7. package/dist/src/api/index.d.ts +1 -0
  8. package/dist/src/api/index.js +1 -0
  9. package/dist/src/artifacts/browser-export.js +1 -0
  10. package/dist/src/artifacts/citation-parser.js +8 -1
  11. package/dist/src/artifacts/cli.js +57 -6
  12. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  13. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  14. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  15. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  16. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  17. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  18. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  19. package/dist/src/artifacts/graph-query.js +18 -8
  20. package/dist/src/artifacts/index-adapters.js +339 -0
  21. package/dist/src/artifacts/index-builder.js +143 -14
  22. package/dist/src/artifacts/index-files.js +15 -4
  23. package/dist/src/artifacts/index-preparation.js +2 -0
  24. package/dist/src/artifacts/index-reader.js +44 -0
  25. package/dist/src/artifacts/index-refresh.js +114 -55
  26. package/dist/src/artifacts/index-status.js +20 -5
  27. package/dist/src/artifacts/memory-maintenance.js +503 -0
  28. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  29. package/dist/src/artifacts/query-engine.js +263 -108
  30. package/dist/src/artifacts/relations-builder.js +2 -2
  31. package/dist/src/artifacts/source-graph.js +2 -2
  32. package/dist/src/artifacts/types.js +9 -1
  33. package/dist/src/auth/client.js +5 -1
  34. package/dist/src/auth/credential-store.js +5 -1
  35. package/dist/src/auth/sensitive-input.js +151 -0
  36. package/dist/src/channel/manager.mjs +3 -0
  37. package/dist/src/cli/handlers/agentcard.js +47 -25
  38. package/dist/src/cli/handlers/auth.js +22 -2
  39. package/dist/src/cli/handlers/decision.js +22 -3
  40. package/dist/src/cli/handlers/effect.js +4 -84
  41. package/dist/src/cli/handlers/guided-setup.js +470 -0
  42. package/dist/src/cli/handlers/help.js +2 -2
  43. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  44. package/dist/src/cli/handlers/index.js +4 -2
  45. package/dist/src/cli/handlers/init.js +41 -6
  46. package/dist/src/cli/handlers/install.js +6 -0
  47. package/dist/src/cli/handlers/local-executor.js +5 -3
  48. package/dist/src/cli/handlers/marketplace.js +15 -4
  49. package/dist/src/cli/handlers/mc.js +509 -53
  50. package/dist/src/cli/handlers/models.js +32 -2
  51. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  52. package/dist/src/cli/handlers/rules.js +1 -1
  53. package/dist/src/cli/handlers/script-runner.js +73 -2
  54. package/dist/src/cli/handlers/serve.js +5 -3
  55. package/dist/src/cli/handlers/sessions.js +1 -0
  56. package/dist/src/cli/handlers/setup.js +432 -1
  57. package/dist/src/cli/handlers/skill-lint.js +1 -1
  58. package/dist/src/cli/handlers/subcommands.js +118 -8
  59. package/dist/src/cli/handlers/use.js +335 -107
  60. package/dist/src/cli/handlers/workspace.js +3 -1
  61. package/dist/src/cli/prompt-utils.js +81 -0
  62. package/dist/src/cli/router.js +16 -1
  63. package/dist/src/config/aiwg-config.js +42 -0
  64. package/dist/src/config/cli.js +7 -3
  65. package/dist/src/config/decision-config.js +14 -1
  66. package/dist/src/config/gitignore.js +4 -0
  67. package/dist/src/config/native-sandbox-warnings.js +25 -0
  68. package/dist/src/config/workspace.js +2 -1
  69. package/dist/src/contributors/validation.js +6 -5
  70. package/dist/src/decision/adapters/jev.js +20 -31
  71. package/dist/src/decision/admission.js +7 -3
  72. package/dist/src/decision/ask.js +6 -1
  73. package/dist/src/decision/benchmark.js +482 -0
  74. package/dist/src/decision/ledger.js +22 -3
  75. package/dist/src/decision/pricing.js +147 -0
  76. package/dist/src/decision/qualification/gate-evidence.js +23 -18
  77. package/dist/src/decision/qualification/load-staged.js +576 -0
  78. package/dist/src/decision/qualification/load.js +109 -33
  79. package/dist/src/decision/qualification/runner.js +12 -5
  80. package/dist/src/decision/review/service.js +6 -4
  81. package/dist/src/decision/review/validate.js +23 -2
  82. package/dist/src/decision/savings.js +8 -3
  83. package/dist/src/discovery/index.js +492 -0
  84. package/dist/src/discovery/interview.js +238 -0
  85. package/dist/src/discovery/plan-apply.js +567 -0
  86. package/dist/src/discovery/plan-store.js +171 -0
  87. package/dist/src/discovery/plan-types.js +2 -0
  88. package/dist/src/discovery/plan-validation.js +139 -0
  89. package/dist/src/discovery/types.js +2 -0
  90. package/dist/src/effects/host.js +91 -0
  91. package/dist/src/extensions/commands/definitions.js +49 -9
  92. package/dist/src/extensions/manifest.js +71 -44
  93. package/dist/src/extensions/project-local-discovery.js +1 -1
  94. package/dist/src/extensions/project-local-gitignore.js +2 -0
  95. package/dist/src/extensions/project-quickref.js +18 -17
  96. package/dist/src/extensions/validation.js +32 -29
  97. package/dist/src/housekeeper/daemon-config.js +148 -0
  98. package/dist/src/housekeeper/ipc.js +84 -0
  99. package/dist/src/housekeeper/lifecycle.js +148 -0
  100. package/dist/src/housekeeper/platform.js +35 -0
  101. package/dist/src/housekeeper/status.js +70 -0
  102. package/dist/src/housekeeper/units.js +114 -0
  103. package/dist/src/issues/cli.js +16 -5
  104. package/dist/src/issues/live.js +44 -12
  105. package/dist/src/jobs/flow.js +22 -21
  106. package/dist/src/lint/rule-activation.js +20 -5
  107. package/dist/src/lint/runner.js +1 -1
  108. package/dist/src/marketplace/exchange.js +18 -6
  109. package/dist/src/marketplace/provenance.js +3 -2
  110. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  111. package/dist/src/mcp/cli.mjs +22 -96
  112. package/dist/src/mcp/config-file.mjs +6 -2
  113. package/dist/src/mcp/config-layers.mjs +23 -7
  114. package/dist/src/mcp/grok-build-config.mjs +2 -1
  115. package/dist/src/mcp/muse-native-config.mjs +2 -1
  116. package/dist/src/mcp/profiles.js +18 -31
  117. package/dist/src/mcp/profiles.mjs +18 -32
  118. package/dist/src/mcp/registry.js +7 -27
  119. package/dist/src/mcp/registry.mjs +7 -28
  120. package/dist/src/mcp/server.mjs +34 -1
  121. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  122. package/dist/src/mcp/tools/command-run.mjs +3 -2
  123. package/dist/src/mcp/tools/decision.mjs +49 -48
  124. package/dist/src/mcp/tools/discovery.mjs +46 -21
  125. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  126. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  127. package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
  128. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  129. package/dist/src/memory/audit.js +719 -0
  130. package/dist/src/memory/canonical-context.js +392 -13
  131. package/dist/src/memory/canonical-maintenance.js +95 -0
  132. package/dist/src/memory/cli-result.js +22 -0
  133. package/dist/src/memory/cli.js +176 -0
  134. package/dist/src/memory/context-pack.js +57 -6
  135. package/dist/src/memory/cost.js +512 -0
  136. package/dist/src/memory/firewall-classification.d.mts +7 -0
  137. package/dist/src/memory/firewall-classification.mjs +32 -0
  138. package/dist/src/memory/index.js +8 -0
  139. package/dist/src/memory/line-maintenance.js +68 -0
  140. package/dist/src/memory/maintenance-cli.js +286 -0
  141. package/dist/src/memory/maintenance-contract.js +136 -0
  142. package/dist/src/memory/maintenance-index.js +18 -0
  143. package/dist/src/memory/maintenance-owner.js +2 -0
  144. package/dist/src/memory/maintenance-record.js +138 -0
  145. package/dist/src/memory/maintenance.js +254 -0
  146. package/dist/src/memory/project-registry.js +2 -1
  147. package/dist/src/memory/provider-memory-adapter.js +154 -0
  148. package/dist/src/memory/recommend-cli.js +83 -0
  149. package/dist/src/memory/recommend.js +269 -0
  150. package/dist/src/memory/scheduled.js +81 -0
  151. package/dist/src/memory/taxonomy.js +44 -0
  152. package/dist/src/mission-protocol/codecs.js +19 -9
  153. package/dist/src/mission-protocol/worker-status.js +91 -1
  154. package/dist/src/models/config-loader.js +11 -1
  155. package/dist/src/models/model-capabilities.v1.json +74 -50
  156. package/dist/src/models/provider-policy.js +125 -62
  157. package/dist/src/models/resolver.js +45 -8
  158. package/dist/src/models/worker-installation.d.mts +8 -1
  159. package/dist/src/models/worker-installation.mjs +231 -38
  160. package/dist/src/models/wrapper-route.js +1 -1
  161. package/dist/src/output-modes/registry.js +8 -7
  162. package/dist/src/packages/adapters/git.js +99 -23
  163. package/dist/src/packages/adapters/gitea.js +8 -27
  164. package/dist/src/packages/adapters/github.js +7 -25
  165. package/dist/src/packages/registry.js +4 -4
  166. package/dist/src/plugin/metadata-validator.js +1 -1
  167. package/dist/src/process/cli-command.d.mts +5 -0
  168. package/dist/src/process/cli-command.mjs +42 -0
  169. package/dist/src/providers/deploy-version.d.mts +1 -0
  170. package/dist/src/providers/deploy-version.mjs +17 -0
  171. package/dist/src/providers/provider-definitions.js +19 -16
  172. package/dist/src/providers/skill-ownership.d.mts +11 -2
  173. package/dist/src/providers/skill-ownership.mjs +35 -17
  174. package/dist/src/rules/activation.d.mts +11 -2
  175. package/dist/src/rules/activation.mjs +82 -6
  176. package/dist/src/rules/inventory.js +1 -1
  177. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  178. package/dist/src/schema/zod-validation.d.mts +6 -0
  179. package/dist/src/schema/zod-validation.mjs +50 -0
  180. package/dist/src/serve/executor-registry.js +297 -20
  181. package/dist/src/serve/executor-token.d.mts +1 -0
  182. package/dist/src/serve/executor-token.mjs +8 -0
  183. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  184. package/dist/src/serve/worker-invocation.js +124 -15
  185. package/dist/src/sessions/adapters/claude.js +19 -18
  186. package/dist/src/sessions/adapters/codex.js +10 -9
  187. package/dist/src/sessions/adapters/copilot.js +14 -13
  188. package/dist/src/sessions/adapters/cursor.js +19 -18
  189. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  190. package/dist/src/sessions/adapters/factory.js +11 -10
  191. package/dist/src/sessions/adapters/generic.js +32 -23
  192. package/dist/src/sessions/adapters/grokbot.js +35 -35
  193. package/dist/src/sessions/adapters/hermes.js +20 -19
  194. package/dist/src/sessions/adapters/muse.js +50 -49
  195. package/dist/src/sessions/adapters/omp.js +5 -4
  196. package/dist/src/sessions/adapters/openclaw.js +30 -29
  197. package/dist/src/sessions/adapters/opencode.js +26 -26
  198. package/dist/src/sessions/adapters/openhuman.js +24 -23
  199. package/dist/src/sessions/adapters/pi.js +5 -4
  200. package/dist/src/sessions/adapters/windsurf.js +8 -8
  201. package/dist/src/sessions/analytics.js +10 -9
  202. package/dist/src/sessions/candidates.js +7 -6
  203. package/dist/src/sessions/contracts.js +63 -53
  204. package/dist/src/sessions/output-registration.js +11 -10
  205. package/dist/src/sessions/workspace-discovery.js +3 -4
  206. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  207. package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
  208. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  209. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  210. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  211. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  212. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  213. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  214. package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
  215. package/dist/src/tracker/project-inputs.js +2 -0
  216. package/dist/src/update/checker.d.mts +4 -0
  217. package/dist/src/update/checker.mjs +14 -0
  218. package/dist/src/update/notifier.mjs +2 -4
  219. package/dist/src/writing/exemplar-selection.js +6 -6
  220. package/dist/src/writing/voice-evaluation.js +24 -23
  221. package/dist/src/writing/writer-migration.js +14 -13
  222. package/dist/src/writing/writer-profile.js +19 -18
  223. package/dist/src/writing/writing-brief.js +18 -17
  224. package/dist/src/writing/writing-receipt.js +47 -46
  225. package/package.json +5 -2
  226. package/schemas/decision/DecisionReview.schema.json +78 -1
  227. package/schemas/mission-v1.schema.json +3 -0
  228. package/tools/agents/deploy-agents.mjs +67 -39
  229. package/tools/agents/providers/antigravity.mjs +6 -1
  230. package/tools/agents/providers/base.mjs +24 -14
  231. package/tools/agents/providers/claude.mjs +6 -3
  232. package/tools/agents/providers/codex.mjs +54 -57
  233. package/tools/agents/providers/context-write.mjs +28 -0
  234. package/tools/agents/providers/copilot.mjs +13 -4
  235. package/tools/agents/providers/cursor.mjs +1 -1
  236. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  237. package/tools/agents/providers/factory.mjs +7 -1
  238. package/tools/agents/providers/grok-build.mjs +18 -2
  239. package/tools/agents/providers/grokbot.mjs +1 -1
  240. package/tools/agents/providers/hermes.mjs +12 -15
  241. package/tools/agents/providers/muse-hooks.mjs +21 -14
  242. package/tools/agents/providers/muse.mjs +7 -2
  243. package/tools/agents/providers/omp.mjs +31 -5
  244. package/tools/agents/providers/opencode.mjs +14 -5
  245. package/tools/agents/providers/openhuman.mjs +1 -1
  246. package/tools/agents/providers/pi.mjs +1 -1
  247. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  248. package/tools/agents/providers/rule-delivery.mjs +289 -80
  249. package/tools/agents/providers/warp.mjs +4 -1
  250. package/tools/agents/providers/windsurf.mjs +13 -3
  251. package/tools/cli/workspace-status.mjs +5 -0
  252. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  253. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  254. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  255. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  256. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  257. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  258. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  259. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  260. package/tools/ralph-external/process-monitor.mjs +49 -8
  261. package/tools/ralph-external/session-launcher.mjs +405 -35
  262. package/tools/skills/deploy-skills-codex.mjs +11 -7
  263. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -11,11 +11,40 @@
11
11
  */
12
12
  import { execFile } from 'child_process';
13
13
  import { promisify } from 'util';
14
- import { mkdir, mkdtemp, readFile, rename, rm } from 'fs/promises';
14
+ import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'fs/promises';
15
15
  import { join } from 'path';
16
- import { homedir } from 'os';
16
+ import { homedir, tmpdir } from 'os';
17
17
  import { existsSync } from 'fs';
18
+ import { resolveSensitiveInput } from '../../auth/sensitive-input.js';
19
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST } from '../../process/cli-command.mjs';
18
20
  const execFileAsync = promisify(execFile);
21
+ // Deliberately separate from serializable sources, locks and provenance.
22
+ const credentials = new WeakMap();
23
+ function hasTokenSource(input) {
24
+ return Boolean(input.interactive || input.stdin || input.file !== undefined || input.store !== undefined || input.env !== undefined);
25
+ }
26
+ /** Explicit sources precede the established env/file lookup; no token means SSH. */
27
+ export async function resolvePackageToken(input = {}, envName, paths = []) {
28
+ if (hasTokenSource(input))
29
+ return resolveSensitiveInput({ ...input, prefix: 'token' });
30
+ if (envName && process.env[envName])
31
+ return resolveSensitiveInput({ prefix: 'token', env: envName });
32
+ for (const file of paths) {
33
+ if (existsSync(file))
34
+ return resolveSensitiveInput({ prefix: 'token', file });
35
+ }
36
+ return undefined;
37
+ }
38
+ // GitHub rejects Bearer on git smart HTTP; Basic with x-access-token works on GitHub and Gitea.
39
+ function basicCredential(token) {
40
+ return Buffer.from(`x-access-token:${token}`).toString('base64');
41
+ }
42
+ function redact(value, token) {
43
+ if (!token)
44
+ return value;
45
+ return [token, encodeURIComponent(token), basicCredential(token)]
46
+ .reduce((result, secret) => result.split(secret).join('[REDACTED]'), value);
47
+ }
19
48
  /**
20
49
  * Default cache root
21
50
  */
@@ -34,16 +63,24 @@ export function buildCachePath(owner, name, version) {
34
63
  /**
35
64
  * Run a git command, returning stdout
36
65
  */
37
- async function git(args, cwd) {
38
- const env = { ...process.env };
66
+ async function git(args, cwd, authentication) {
67
+ const env = helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'SSH_AUTH_SOCK', 'SSH_AGENT_PID',
68
+ 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSH_VARIANT', 'XDG_CONFIG_HOME']);
39
69
  // Suppress interactive prompts
40
70
  env.GIT_TERMINAL_PROMPT = '0';
41
- const { stdout } = await execFileAsync('git', args, {
42
- cwd,
43
- env,
44
- timeout: 120_000,
45
- });
46
- return stdout.trim();
71
+ if (authentication) {
72
+ env.GIT_CONFIG_COUNT = '1';
73
+ env.GIT_CONFIG_KEY_0 = 'include.path';
74
+ env.GIT_CONFIG_VALUE_0 = authentication.config;
75
+ }
76
+ try {
77
+ const { stdout } = await execFileAsync('git', args, { cwd, env, timeout: 120_000 });
78
+ return redact(stdout.trim(), authentication?.token);
79
+ }
80
+ catch (error) {
81
+ // Never rethrow execFile's object: it carries raw stdout/stderr properties.
82
+ throw new Error(redact(error instanceof Error ? error.message : 'Git command failed.', authentication?.token));
83
+ }
47
84
  }
48
85
  /**
49
86
  * Detect the manifest type from a cloned package directory
@@ -69,9 +106,9 @@ async function detectManifestType(cachePath) {
69
106
  /**
70
107
  * Resolve the latest tag from a remote git repo
71
108
  */
72
- async function resolveLatestTag(gitUrl) {
109
+ async function resolveLatestTag(gitUrl, authentication) {
73
110
  try {
74
- const output = await git(['ls-remote', '--tags', '--sort=-v:refname', gitUrl]);
111
+ const output = await git(['ls-remote', '--tags', '--sort=-v:refname', gitUrl], undefined, authentication);
75
112
  const firstLine = output.split('\n')[0] ?? '';
76
113
  const match = firstLine.match(/refs\/tags\/(.+)/);
77
114
  if (match && match[1])
@@ -82,14 +119,14 @@ async function resolveLatestTag(gitUrl) {
82
119
  }
83
120
  return 'latest';
84
121
  }
85
- async function resolveRemoteCommit(gitUrl, requestedRef) {
122
+ async function resolveRemoteCommit(gitUrl, requestedRef, authentication) {
86
123
  if (/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(requestedRef))
87
124
  return requestedRef;
88
125
  const patterns = requestedRef === 'HEAD'
89
126
  ? ['HEAD']
90
127
  : [`refs/tags/${requestedRef}^{}`, `refs/tags/${requestedRef}`, `refs/heads/${requestedRef}`, requestedRef];
91
128
  try {
92
- const output = await git(['ls-remote', gitUrl, ...patterns]);
129
+ const output = await git(['ls-remote', gitUrl, ...patterns], undefined, authentication);
93
130
  const rows = output.split('\n').filter(Boolean).map((line) => {
94
131
  const [oid = '', ref = ''] = line.split(/\s+/, 2);
95
132
  return { oid, ref };
@@ -136,30 +173,69 @@ export class GitAdapter {
136
173
  ref.startsWith('ssh://') ||
137
174
  ref.startsWith('git+https://'));
138
175
  }
139
- async resolve(ref) {
176
+ async resolve(ref, options = {}) {
140
177
  if (!ref.rawUrl)
141
178
  return null;
142
- return {
179
+ const source = {
143
180
  gitUrl: ref.rawUrl,
144
181
  ref: ref.version,
145
182
  label: ref.rawUrl,
146
183
  };
184
+ const token = await resolvePackageToken(options.tokenInput);
185
+ return this.withToken(source, token);
186
+ }
187
+ withToken(source, token) {
188
+ if (/^https?:\/\//.test(source.gitUrl)) {
189
+ const url = new URL(source.gitUrl);
190
+ if (url.username || url.password)
191
+ throw new Error('Credentials in Git URLs are refused; use --token-stdin, --token-file, --token-store or --token-env.');
192
+ }
193
+ if (token)
194
+ credentials.set(source, token);
195
+ return source;
147
196
  }
148
197
  async fetch(source, options = {}) {
198
+ this.withToken(source);
199
+ const token = credentials.get(source) ?? (hasTokenSource(options.tokenInput ?? {})
200
+ ? await resolvePackageToken(options.tokenInput) : undefined);
201
+ let privateDir;
202
+ try {
203
+ let authentication;
204
+ if (token) {
205
+ if (process.platform === 'win32')
206
+ throw new Error('Git token authentication requires private temporary-file ACL validation on Windows; use SSH-agent authentication.');
207
+ const url = new URL(source.gitUrl);
208
+ if (url.protocol !== 'https:')
209
+ throw new Error('Git token authentication requires HTTPS.');
210
+ privateDir = await mkdtemp(join(tmpdir(), 'aiwg-git-credentials-'));
211
+ const config = join(privateDir, 'config');
212
+ // A path-only include survives NODE_DEBUG=child_process safely. The
213
+ // origin-scoped header is transient and never saved in .git/config.
214
+ await writeFile(config, `[http ${JSON.stringify(`${url.origin}/`)}]\n\textraHeader = ${JSON.stringify(`Authorization: Basic ${basicCredential(token)}`)}\n`, { mode: 0o600 });
215
+ authentication = { config, token };
216
+ }
217
+ return await this.fetchAuthenticated(source, options, authentication);
218
+ }
219
+ finally {
220
+ if (privateDir)
221
+ await rm(privateDir, { recursive: true, force: true });
222
+ }
223
+ }
224
+ async fetchAuthenticated(source, options, authentication) {
149
225
  // Resolve discovery inputs before any deployment and cache by immutable
150
226
  // commit rather than by a movable tag/branch name (#2009).
151
227
  let requestedRef = source.ref;
152
228
  if (!requestedRef) {
153
- const latest = await resolveLatestTag(source.gitUrl);
229
+ const latest = await resolveLatestTag(source.gitUrl, authentication);
154
230
  requestedRef = latest === 'latest' ? 'HEAD' : latest;
155
231
  source.ref = requestedRef;
156
232
  }
157
- const advertisedCommit = await resolveRemoteCommit(source.gitUrl, requestedRef);
233
+ const advertisedCommit = await resolveRemoteCommit(source.gitUrl, requestedRef, authentication);
158
234
  const { owner, name } = urlCacheIdentity(source.gitUrl);
159
235
  if (advertisedCommit) {
160
236
  const existing = buildCachePath(owner, name, advertisedCommit);
161
237
  if (!options.refresh && existsSync(join(existing, '.git'))) {
162
- const existingCommit = await git(['rev-parse', 'HEAD^{commit}'], existing);
238
+ const existingCommit = await git(['rev-parse', 'HEAD^{commit}'], existing, authentication);
163
239
  if (existingCommit !== advertisedCommit)
164
240
  throw new Error(`Immutable package cache mismatch at ${existing}`);
165
241
  return existing;
@@ -169,9 +245,9 @@ export class GitAdapter {
169
245
  await mkdir(ownerDir, { recursive: true });
170
246
  const stage = await mkdtemp(join(ownerDir, `.${name}.resolve-`));
171
247
  try {
172
- await git(['clone', '--no-checkout', source.gitUrl, stage]);
173
- await git(['checkout', '--detach', requestedRef], stage);
174
- const resolvedCommit = await git(['rev-parse', 'HEAD^{commit}'], stage);
248
+ await git(['clone', '--no-checkout', source.gitUrl, stage], undefined, authentication);
249
+ await git(['checkout', '--detach', requestedRef], stage, authentication);
250
+ const resolvedCommit = await git(['rev-parse', 'HEAD^{commit}'], stage, authentication);
175
251
  if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(resolvedCommit)) {
176
252
  throw new Error(`Git resolved '${requestedRef}' to invalid commit '${resolvedCommit}'`);
177
253
  }
@@ -180,7 +256,7 @@ export class GitAdapter {
180
256
  }
181
257
  const cachePath = buildCachePath(owner, name, resolvedCommit);
182
258
  if (existsSync(join(cachePath, '.git'))) {
183
- const cachedCommit = await git(['rev-parse', 'HEAD^{commit}'], cachePath);
259
+ const cachedCommit = await git(['rev-parse', 'HEAD^{commit}'], cachePath, authentication);
184
260
  if (cachedCommit !== resolvedCommit)
185
261
  throw new Error(`Immutable package cache mismatch at ${cachePath}`);
186
262
  await rm(stage, { recursive: true, force: true });
@@ -11,36 +11,17 @@
11
11
  *
12
12
  * Authentication:
13
13
  * - SSH clone: uses default SSH agent (recommended for teams)
14
- * - HTTPS clone: reads token from GITEA_TOKEN env or ~/.config/gitea/token
14
+ * - HTTPS clone: resolves explicit sources, otherwise reads token from GITEA_TOKEN env or ~/.config/gitea/token
15
15
  *
16
16
  * @implements #557
17
17
  */
18
- import { existsSync } from 'fs';
19
- import { readFile } from 'fs/promises';
20
18
  import { join } from 'path';
21
19
  import { homedir } from 'os';
22
- import { GitAdapter } from './git.js';
20
+ import { GitAdapter, resolvePackageToken } from './git.js';
23
21
  /**
24
22
  * Regex for "owner/name" or "owner/name@version" (no scheme prefix)
25
23
  */
26
24
  const GITEA_SHORTHAND = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(@[a-zA-Z0-9._/-]+)?$/;
27
- /**
28
- * Read Gitea token from standard file path (same as global CLAUDE.md convention)
29
- */
30
- async function readGiteaToken() {
31
- if (process.env.GITEA_TOKEN)
32
- return process.env.GITEA_TOKEN;
33
- const tokenPaths = [
34
- join(homedir(), '.config', 'gitea', 'token'),
35
- join(homedir(), '.aiwg', 'credentials', 'gitea'),
36
- ];
37
- for (const p of tokenPaths) {
38
- if (existsSync(p)) {
39
- return (await readFile(p, 'utf-8')).trim();
40
- }
41
- }
42
- return undefined;
43
- }
44
25
  /**
45
26
  * Resolve the Gitea host
46
27
  */
@@ -66,25 +47,25 @@ export class GiteaAdapter extends GitAdapter {
66
47
  return false; // has scheme prefix → not gitea shorthand
67
48
  return GITEA_SHORTHAND.test(ref);
68
49
  }
69
- async resolve(ref) {
50
+ async resolve(ref, options = {}) {
70
51
  if (!ref.owner || !ref.name)
71
52
  return null;
72
53
  const host = resolveGiteaHost();
73
- const token = await readGiteaToken();
54
+ const token = await resolvePackageToken(options.tokenInput, 'GITEA_TOKEN', [join(homedir(), '.config', 'gitea', 'token'), join(homedir(), '.aiwg', 'credentials', 'gitea')]);
74
55
  let gitUrl;
75
56
  if (token) {
76
- // HTTPS with token
77
- gitUrl = `https://${token}@${host}/${ref.owner}/${ref.name}.git`;
57
+ // HTTPS; credential is held outside the serializable source
58
+ gitUrl = `https://${host}/${ref.owner}/${ref.name}.git`;
78
59
  }
79
60
  else {
80
61
  // SSH (relies on SSH agent)
81
62
  gitUrl = `git@${host}:${ref.owner}/${ref.name}.git`;
82
63
  }
83
- return {
64
+ return this.withToken({
84
65
  gitUrl,
85
66
  ref: ref.version,
86
67
  label: `${host}/${ref.owner}/${ref.name}${ref.version ? `@${ref.version}` : ''}`,
87
- };
68
+ }, token);
88
69
  }
89
70
  async fetch(source, options = {}) {
90
71
  return super.fetch(source, options);
@@ -5,32 +5,14 @@
5
5
  *
6
6
  * Authentication:
7
7
  * - SSH clone: uses default SSH agent
8
- * - HTTPS clone: reads token from GITHUB_TOKEN env or ~/.aiwg/credentials/github
8
+ * - HTTPS clone: resolves explicit sources, otherwise reads token from GITHUB_TOKEN env or ~/.aiwg/credentials/github
9
9
  *
10
10
  * @implements #557
11
11
  */
12
- import { existsSync } from 'fs';
13
- import { readFile } from 'fs/promises';
14
12
  import { join } from 'path';
15
13
  import { homedir } from 'os';
16
- import { GitAdapter } from './git.js';
14
+ import { GitAdapter, resolvePackageToken } from './git.js';
17
15
  const GITHUB_HOST = 'github.com';
18
- /**
19
- * Read GitHub token from standard locations
20
- */
21
- async function readGitHubToken() {
22
- if (process.env.GITHUB_TOKEN)
23
- return process.env.GITHUB_TOKEN;
24
- const tokenPaths = [
25
- join(homedir(), '.aiwg', 'credentials', 'github'),
26
- ];
27
- for (const p of tokenPaths) {
28
- if (existsSync(p)) {
29
- return (await readFile(p, 'utf-8')).trim();
30
- }
31
- }
32
- return undefined;
33
- }
34
16
  /**
35
17
  * GitHubAdapter
36
18
  *
@@ -45,22 +27,22 @@ export class GitHubAdapter extends GitAdapter {
45
27
  canResolve(ref) {
46
28
  return ref.startsWith('github:');
47
29
  }
48
- async resolve(ref) {
30
+ async resolve(ref, options = {}) {
49
31
  if (!ref.owner || !ref.name)
50
32
  return null;
51
- const token = await readGitHubToken();
33
+ const token = await resolvePackageToken(options.tokenInput, 'GITHUB_TOKEN', [join(homedir(), '.aiwg', 'credentials', 'github')]);
52
34
  let gitUrl;
53
35
  if (token) {
54
- gitUrl = `https://${token}@${GITHUB_HOST}/${ref.owner}/${ref.name}.git`;
36
+ gitUrl = `https://${GITHUB_HOST}/${ref.owner}/${ref.name}.git`;
55
37
  }
56
38
  else {
57
39
  gitUrl = `git@${GITHUB_HOST}:${ref.owner}/${ref.name}.git`;
58
40
  }
59
- return {
41
+ return this.withToken({
60
42
  gitUrl,
61
43
  ref: ref.version,
62
44
  label: `github.com/${ref.owner}/${ref.name}${ref.version ? `@${ref.version}` : ''}`,
63
- };
45
+ }, token);
64
46
  }
65
47
  async fetch(source, options = {}) {
66
48
  return super.fetch(source, options);
@@ -99,11 +99,11 @@ export function parseRef(raw) {
99
99
  /**
100
100
  * Resolve a ref to a PackageSource using the appropriate adapter
101
101
  */
102
- export async function resolveRef(ref) {
102
+ export async function resolveRef(ref, options = {}) {
103
103
  for (const adapter of ALL_ADAPTERS) {
104
104
  if (!adapter.canResolve(ref.raw))
105
105
  continue;
106
- const source = await adapter.resolve(ref);
106
+ const source = await adapter.resolve(ref, options);
107
107
  if (source)
108
108
  return { source, adapter };
109
109
  }
@@ -171,7 +171,7 @@ export async function readPackageNamespace(cachePath, registryKey) {
171
171
  */
172
172
  export async function installPackage(rawRef, options = {}) {
173
173
  const ref = parseRef(rawRef);
174
- const resolved = await resolveRef(ref);
174
+ const resolved = await resolveRef(ref, options);
175
175
  if (!resolved) {
176
176
  throw new Error(`Cannot resolve package reference: '${rawRef}'\n` +
177
177
  `Supported formats:\n` +
@@ -185,7 +185,7 @@ export async function installPackage(rawRef, options = {}) {
185
185
  const { source, adapter } = resolved;
186
186
  if (options.ref)
187
187
  source.ref = options.ref;
188
- const cachePath = await adapter.fetch(source, { refresh: options.refresh });
188
+ const cachePath = await adapter.fetch(source, { refresh: options.refresh, tokenInput: options.tokenInput });
189
189
  const discovered = await discoverInstallablePackage(cachePath, options.packageSelector);
190
190
  const standardEnvelopePaths = [
191
191
  join(cachePath, '.aiwg', 'marketplace', 'envelope.json'),
@@ -177,7 +177,7 @@ export class MetadataValidator {
177
177
  result.manifest = manifest;
178
178
  }
179
179
  else {
180
- for (const issue of skillResult.errors.errors) {
180
+ for (const issue of skillResult.errors.issues) {
181
181
  result.errors.push({
182
182
  path: filename,
183
183
  field: issue.path.join('.'),
@@ -14,3 +14,8 @@ export function cliCommand(
14
14
  pathExt?: string,
15
15
  cwd?: string,
16
16
  ): [string, string[]];
17
+
18
+ export const HELPER_ENV_ALLOWLIST: readonly string[];
19
+ export const DESKTOP_HELPER_ENV_ALLOWLIST: readonly string[];
20
+ export const DEPLOYMENT_HELPER_ENV_ALLOWLIST: readonly string[];
21
+ export function helperEnvironment(source?: NodeJS.ProcessEnv, names?: readonly string[]): Record<string, string>;
@@ -1,6 +1,48 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ // Like worker dispatch, helper processes receive only explicitly named state.
5
+ // Keep provider credentials, NODE_OPTIONS and unrelated shell variables out of
6
+ // process launch diagnostics as well as the child's own environment.
7
+ export const HELPER_ENV_ALLOWLIST = Object.freeze([
8
+ 'PATH', 'PATHEXT', 'SYSTEMROOT', 'SYSTEMDRIVE', 'WINDIR', 'COMSPEC',
9
+ 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH',
10
+ 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM',
11
+ // Platform and network plumbing a child may need to reach anything at all
12
+ // (corporate proxies and CA bundles, Windows per-user data, git global config).
13
+ 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA',
14
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
15
+ 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
16
+ 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_SYSTEM', 'GIT_CONFIG_NOSYSTEM',
17
+ ]);
18
+
19
+ export const DESKTOP_HELPER_ENV_ALLOWLIST = Object.freeze([
20
+ 'DISPLAY', 'WAYLAND_DISPLAY', 'XAUTHORITY', 'XDG_RUNTIME_DIR',
21
+ 'DBUS_SESSION_BUS_ADDRESS', 'XDG_CURRENT_DESKTOP',
22
+ ]);
23
+
24
+ // Deployment helpers read project/configuration locations and policy switches;
25
+ // none consumes provider authentication or the caller's complete shell state.
26
+ export const DEPLOYMENT_HELPER_ENV_ALLOWLIST = Object.freeze([
27
+ ...HELPER_ENV_ALLOWLIST, 'AIWG_ROOT', 'AIWG_HOME', 'AIWG_CONFIG', 'AIWG_PROJECT_DIR',
28
+ 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'LOCALAPPDATA',
29
+ 'AIWG_ARTIFACTS_PATH', 'AIWG_PROJECT_ARTIFACTS_PATH', 'AIWG_PROJECT_AIWG_DIR',
30
+ 'AIWG_PROJECT_MEMORY_HOME', 'AIWG_USER_REGISTRY_PATH', 'AIWG_RULE_BINDING_INDEX_CACHE',
31
+ 'AIWG_DEPLOY_SUBPROCESS', 'AIWG_REPORT_DEPLOY_COLLISIONS', 'CODEX_HOME', 'HERMES_HOME',
32
+ 'OPENCLAW_PROFILE', 'OMP_PROFILE', 'PI_PROFILE', 'PI_CONFIG_DIR', 'PI_CODING_AGENT_DIR',
33
+ 'AIWG_RESOLVE_IMPL_FROM', 'NODE_ENV', 'VITEST', 'NO_UPDATE_NOTIFIER', 'NO_COLOR', 'CI',
34
+ ]);
35
+
36
+ /** Copy only the helper's declared variable names; never merge the parent. */
37
+ export function helperEnvironment(source = process.env, names = HELPER_ENV_ALLOWLIST) {
38
+ const environment = {};
39
+ for (const name of names) {
40
+ const value = source?.[name];
41
+ if (typeof value === 'string' && !value.includes('\0')) environment[name] = value;
42
+ }
43
+ return environment;
44
+ }
45
+
4
46
  // Share PATH/PATHEXT lookup with callers that can launch arbitrary batch files.
5
47
  function* executableCandidates(command, searchPath, pathExt, preferNative = false, cwd = process.cwd()) {
6
48
  const candidates = path.isAbsolute(command)
@@ -0,0 +1 @@
1
+ export function resolveDeployVersion(root: string, explicitVersion?: string | null, warn?: (message: string) => void): string;
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /** Resolve the selected corpus version before provider inputs are staged. */
5
+ export function resolveDeployVersion(root, explicitVersion, warn = console.warn) {
6
+ const valid = value => typeof value === 'string' && value.trim() &&
7
+ !/unknown|\s/i.test(value);
8
+ if (valid(explicitVersion)) return explicitVersion;
9
+ if (!explicitVersion) {
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
12
+ if (valid(pkg.version)) return pkg.version;
13
+ } catch { /* Report missing, unreadable and invalid versions consistently. */ }
14
+ }
15
+ warn(`Unable to resolve AIWG deploy version from ${path.join(root, 'package.json')}; managed markers will use vunknown. Restore the package version or pass --deploy-version to the deployer.`);
16
+ return 'unknown';
17
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { integer, url } from '../schema/zod-validation.mjs';
2
3
  import { homedir } from 'os';
3
- import { join } from 'path';
4
+ import { isAbsolute, join } from 'path';
4
5
  import { resolveHermesHomePath } from './hermes-home.js';
5
6
  import { resolveGrokbotSkillsDir } from './grokbot-paths.js';
6
7
  import { getProviderCapabilities, } from './capability-matrix.js';
@@ -38,9 +39,9 @@ const ProviderDefinitionSchema = z.object({
38
39
  status: z.enum(['stable', 'experimental', 'deprecated']),
39
40
  builtIn: z.boolean(),
40
41
  upstream: z.object({
41
- source: z.string().url(),
42
+ source: url(),
42
43
  version: z.string().min(1),
43
- revision: z.string().regex(/^[0-9a-f]{40}$/).refine((value) => value !== '0'.repeat(40), { message: 'upstream.revision must be a real reviewed SHA, not all-zero' }),
44
+ revision: z.string().regex(/^[0-9a-f]{40}$/).refine((value) => value !== '0'.repeat(40), { error: 'upstream.revision must be a real reviewed SHA, not all-zero' }),
44
45
  runtime: z.string().min(1),
45
46
  lastVerified: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
46
47
  }).optional(),
@@ -67,7 +68,7 @@ const ProviderDefinitionSchema = z.object({
67
68
  env: z.array(z.string().min(1)),
68
69
  process: z.array(z.string().min(1)),
69
70
  capabilityId: z.string().min(1),
70
- runtimeEnvPriority: z.number().int().positive().optional(),
71
+ runtimeEnvPriority: integer().positive().optional(),
71
72
  }),
72
73
  paths: z.object({
73
74
  deployTarget: z.enum(['project', 'home', 'mixed']),
@@ -95,8 +96,8 @@ const ProviderDefinitionSchema = z.object({
95
96
  includeSyntax: z.string().nullable(),
96
97
  configRegistration: z.object({ file: z.string().min(1), key: z.string().min(1) }).nullable(),
97
98
  bootstrapTargets: z.array(z.string().min(1)),
98
- maxContextBytes: z.number().int().positive().nullable(),
99
- recommendedMaxLines: z.number().int().positive().nullable(),
99
+ maxContextBytes: integer().positive().nullable(),
100
+ recommendedMaxLines: integer().positive().nullable(),
100
101
  nestedContext: z.boolean(),
101
102
  support: z.enum(['supported', 'degraded', 'unsupported']),
102
103
  verification: z.object({
@@ -119,8 +120,8 @@ const ProviderDefinitionSchema = z.object({
119
120
  pathType: z.enum(['project', 'home-dir']),
120
121
  skillsBaseDir: z.string().min(1),
121
122
  subdirLayout: z.boolean(),
122
- maxNameLength: z.number().int().positive().optional(),
123
- maxDescriptionLength: z.number().int().positive().optional(),
123
+ maxNameLength: integer().positive().optional(),
124
+ maxDescriptionLength: integer().positive().optional(),
124
125
  appendToDescription: z.string().min(1).optional(),
125
126
  }),
126
127
  adapters: z.object({
@@ -132,8 +133,8 @@ const ProviderDefinitionSchema = z.object({
132
133
  }),
133
134
  capabilities: z.object({
134
135
  matrixRef: z.string().nullable(),
135
- nativeFeatures: z.record(z.boolean()),
136
- emulation: z.record(z.string().nullable()),
136
+ nativeFeatures: z.record(z.string(), z.boolean()),
137
+ emulation: z.record(z.string(), z.string().nullable()),
137
138
  }),
138
139
  });
139
140
  export const PROVIDER_IDS = [
@@ -755,10 +756,11 @@ const BUILT_IN_SEEDS = [
755
756
  agents: null,
756
757
  commands: null,
757
758
  skills: resolveHermesHomePath('skills', '.aiwg'),
758
- rules: null,
759
+ rules: '.hermes/rules',
759
760
  behaviors: null,
760
761
  },
761
762
  kernelSkills: resolveHermesHomePath('skills'),
763
+ contextDiscovery: { agents: null, skills: resolveHermesHomePath('skills', '.aiwg'), rules: null, behaviors: null },
762
764
  configFile: 'AGENTS.md',
763
765
  contextFiles: { aiwgMd: true, agentsMd: true, claudeMdHook: false, hookFile: '.hermes.md', contextFile: 'AGENTS.md' },
764
766
  },
@@ -1080,7 +1082,7 @@ const BUILT_IN_SEEDS = [
1080
1082
  agents: '.agents/skills',
1081
1083
  commands: '.pi/prompts',
1082
1084
  skills: '.pi/.aiwg/skills',
1083
- rules: null,
1085
+ rules: '.pi/rules',
1084
1086
  behaviors: '.pi/extensions',
1085
1087
  },
1086
1088
  kernelSkills: '.agents/skills',
@@ -1425,13 +1427,14 @@ export function getProviderKernelSkillPath(provider) {
1425
1427
  const definition = getProviderDefinition(provider);
1426
1428
  return expandProviderHomePath(definition?.paths.kernelSkills ?? null);
1427
1429
  }
1428
- export function resolveProviderPathValue(providerPath, projectPath) {
1429
- const expandedPath = expandProviderHomePath(providerPath);
1430
+ export function resolveProviderPathValue(providerPath, projectPath, paths = { join, isAbsolute }, homeDirectory = homedir()) {
1431
+ const expandedPath = !providerPath ? '' : providerPath === '~' ? homeDirectory
1432
+ : providerPath.startsWith('~/') ? paths.join(homeDirectory, providerPath.slice(2)) : providerPath;
1430
1433
  if (!expandedPath)
1431
1434
  return '';
1432
- if (expandedPath.startsWith('/'))
1435
+ if (paths.isAbsolute(expandedPath))
1433
1436
  return expandedPath;
1434
- return join(projectPath, expandedPath);
1437
+ return paths.join(projectPath, expandedPath);
1435
1438
  }
1436
1439
  export function normalizeProviderDefinitionId(provider) {
1437
1440
  const normalized = provider?.trim().toLowerCase();
@@ -2,8 +2,17 @@ export interface SkillReceipt {
2
2
  skillSource?: string;
3
3
  managed?: Record<string, { hash: string; frameworkSlug?: string; source?: string; version?: string }>;
4
4
  }
5
+ export interface SkillRemovalPlanEntry {
6
+ path: string;
7
+ action: 'remove' | 'preserve';
8
+ reason: string;
9
+ }
10
+ export interface SkillRemovalOptions {
11
+ dryRun?: boolean;
12
+ plan?: SkillRemovalPlanEntry[];
13
+ }
5
14
  export function extractFrameworkSlug(source: string): string | null;
6
15
  export function sameSkillOwner(receipt: SkillReceipt | null, skillSource: string, frameworkSlug?: string | null): boolean;
7
16
  export function canDeploySkill(receipt: SkillReceipt | null, skillSource: string, opts?: { force?: boolean }): boolean;
8
- export function pruneRecordedSkillFiles(skillDir: string, managed: SkillReceipt['managed'], desired: Set<string>, opts?: { dryRun?: boolean }): void;
9
- export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?: { dryRun?: boolean }): boolean | null;
17
+ export function pruneRecordedSkillFiles(skillDir: string, managed: SkillReceipt['managed'], desired: Set<string>, opts?: SkillRemovalOptions): void;
18
+ export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?: SkillRemovalOptions): boolean | null;
@@ -40,28 +40,40 @@ export function canDeploySkill(receipt, skillSource, { force = false } = {}) {
40
40
 
41
41
  /** Remove unchanged recorded files, allowing symlinks above (but never inside) the skill root. */
42
42
  export function pruneRecordedSkillFiles(skillDir, managed, desired, opts = {}) {
43
- if (opts.dryRun) return;
43
+ const root = path.resolve(skillDir);
44
44
  for (const [relative, record] of Object.entries(managed || {})) {
45
- if (desired.has(relative)) continue;
46
45
  const target = path.resolve(skillDir, relative);
47
- if (!target.startsWith(`${path.resolve(skillDir)}${path.sep}`)) continue;
46
+ let action = 'preserve';
47
+ let reason = 'modified or unverifiable managed file';
48
+ if (desired.has(relative)) reason = 'retained managed file';
49
+ else if (!target.startsWith(`${root}${path.sep}`)) reason = 'path outside skill directory';
50
+ else {
51
+ try {
52
+ const expected = path.resolve(fs.realpathSync(skillDir), relative);
53
+ let current = target;
54
+ let symlink = false;
55
+ while (current !== root) {
56
+ if (fs.lstatSync(current).isSymbolicLink()) { symlink = true; break; }
57
+ current = path.dirname(current);
58
+ }
59
+ if (symlink) reason = 'symlink preserved';
60
+ else if (fs.realpathSync(target) === expected && fs.lstatSync(target).isFile()
61
+ && `sha256:${createHash('sha256').update(fs.readFileSync(target)).digest('hex')}` === record?.hash) {
62
+ action = 'remove';
63
+ reason = 'unchanged managed file';
64
+ }
65
+ } catch { reason = 'missing or unreadable path; cannot verify'; }
66
+ }
67
+ opts.plan?.push({ path: target, action, reason });
68
+ if (opts.dryRun || action !== 'remove') continue;
48
69
  try {
49
- const expected = path.resolve(fs.realpathSync(skillDir), relative);
50
- let current = target;
51
- let symlink = false;
52
- while (current !== path.resolve(skillDir)) {
53
- if (fs.lstatSync(current).isSymbolicLink()) { symlink = true; break; }
54
- current = path.dirname(current);
55
- }
56
- if (symlink || fs.realpathSync(target) !== expected || !fs.lstatSync(target).isFile()) continue;
57
- if (`sha256:${createHash('sha256').update(fs.readFileSync(target)).digest('hex')}` !== record.hash) continue;
58
70
  fs.unlinkSync(target);
59
71
  let parent = path.dirname(target);
60
- while (parent !== path.resolve(skillDir)) {
72
+ while (parent !== root) {
61
73
  try { fs.rmdirSync(parent); } catch { break; }
62
74
  parent = path.dirname(parent);
63
75
  }
64
- } catch { /* missing, modified, or unreadable files are left alone */ }
76
+ } catch { /* Files that cannot be removed are left alone. */ }
65
77
  }
66
78
  }
67
79
 
@@ -80,9 +92,15 @@ export function releaseSkillOwnership(skillDir, owner, opts = {}) {
80
92
  && !path.isAbsolute(path.relative(path.resolve(owner.sourceRoot), path.resolve(receipt.skillSource)));
81
93
  if (!insideRoot && (slug ? slug !== owner.frameworkSlug
82
94
  : !owner.skillSource || !sameSkillOwner(receipt, owner.skillSource, null))) return null;
83
- if (opts.dryRun) return false;
84
95
  pruneRecordedSkillFiles(skillDir, receipt.managed, new Set(), opts);
85
- fs.rmSync(manifestPath, { force: true });
86
- fs.rmSync(path.join(skillDir, '.aiwg-managed'), { force: true });
96
+ for (const target of [manifestPath, path.join(skillDir, '.aiwg-managed')]) {
97
+ // Metadata is released even when operator files keep the directory alive.
98
+ try {
99
+ fs.lstatSync(target);
100
+ opts.plan?.push({ path: path.resolve(target), action: 'remove', reason: 'ownership metadata released' });
101
+ } catch { /* Missing metadata has nothing to remove. */ }
102
+ if (!opts.dryRun) fs.rmSync(target, { force: true });
103
+ }
104
+ if (opts.dryRun) return false;
87
105
  try { fs.rmdirSync(skillDir); return true; } catch { return false; }
88
106
  }