@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
@@ -2,34 +2,35 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameS
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { dirname, relative, resolve, sep } from 'node:path';
4
4
  import { z } from 'zod';
5
+ import { integer } from '../schema/zod-validation.mjs';
5
6
  import { SessionContractError, sha256 } from './contracts.js';
6
7
  const DigestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
7
8
  const ReferenceSchema = z.string().min(1).max(512).refine(value => !/[\r\n\0]/.test(value), 'references must be single-line inert locators');
8
- export const OutputSourceReferenceSchema = z.object({
9
+ export const OutputSourceReferenceSchema = z.strictObject({
9
10
  kind: z.enum(['file', 'url', 'note', 'session', 'artifact', 'context-pack']),
10
11
  ref: ReferenceSchema,
11
12
  digest: DigestSchema.nullable().default(null),
12
- span: z.object({
13
- start: z.number().int().nonnegative(),
14
- end: z.number().int().positive(),
13
+ span: z.strictObject({
14
+ start: integer().nonnegative(),
15
+ end: integer().positive(),
15
16
  quoteDigest: DigestSchema,
16
- }).strict().nullable().default(null),
17
- }).strict().superRefine((value, context) => {
17
+ }).nullable().default(null),
18
+ }).superRefine((value, context) => {
18
19
  if (value.span && value.span.end <= value.span.start) {
19
20
  context.addIssue({ code: z.ZodIssueCode.custom, message: 'source span end must exceed start' });
20
21
  }
21
22
  });
22
- export const OutputRegistrationRequestSchema = z.object({
23
+ export const OutputRegistrationRequestSchema = z.strictObject({
23
24
  outputPath: z.string().min(1),
24
25
  mediaType: z.string().min(1).max(128),
25
- contextPack: z.object({
26
+ contextPack: z.strictObject({
26
27
  id: ReferenceSchema,
27
28
  digest: DigestSchema,
28
29
  sources: z.array(OutputSourceReferenceSchema).min(1).max(256),
29
- }).strict(),
30
+ }),
30
31
  supersedes: z.array(ReferenceSchema).max(128).default([]),
31
32
  conflictsWith: z.array(ReferenceSchema).max(128).default([]),
32
- }).strict();
33
+ });
33
34
  /**
34
35
  * Minimal incremental index sink. Each registration is independently atomic,
35
36
  * discoverable, and replay-safe; corpus-wide index builders can consume these
@@ -466,13 +466,12 @@ async function rediscoverManifestIdentity(manifest) {
466
466
  }
467
467
  async function codexDiscoveryRoots(options) {
468
468
  const explicit = [...(options.codexRoots ?? []), ...(options.codexRoot ? [options.codexRoot] : [])];
469
- if (explicit.length > 0)
470
- return [...new Set(explicit.map((root) => resolve(root)))];
469
+ const roots = [...new Set(explicit.map((root) => resolve(root)))];
471
470
  // Shared stores require the same explicit provider-home authorization as the default root.
472
471
  if (!options.providerHome)
473
- return [];
472
+ return roots;
474
473
  const codexHome = join(resolve(options.providerHome), '.codex');
475
- const roots = [join(codexHome, 'sessions')];
474
+ roots.push(join(codexHome, 'sessions'));
476
475
  if (process.env.CODEX_HOME)
477
476
  roots.push(resolve(process.env.CODEX_HOME, 'sessions'));
478
477
  try {
@@ -18,6 +18,7 @@ import { promises as fs } from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
  import { projectControlPath } from '../../config/project-artifacts.js';
20
20
  import { buildClaudeHookBlock, CLAUDE_HOOK_START, CLAUDE_HOOK_END } from './claude-hook.js';
21
+ import { dedupeProviderBootstrap } from './workspace-context.js';
21
22
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
22
23
  import { buildParallelismSection, replaceOrAppendParallelismBlock } from './parallelism-section.js';
23
24
  import { buildContextFinalizationBlock, replaceOrAppendFinalizationBlock } from './finalization.js';
@@ -60,6 +61,7 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
60
61
  // Regenerate may mirror CLAUDE.md before its hook updater runs. Normalize
61
62
  // that managed block here too so legacy duplicate policy never reaches
62
63
  // AIWG.md, even on the first refresh. Operator content stays untouched.
64
+ claudeMdContent = dedupeProviderBootstrap(claudeMdContent);
63
65
  const start = claudeMdContent.indexOf(CLAUDE_HOOK_START);
64
66
  const end = claudeMdContent.indexOf(CLAUDE_HOOK_END, start);
65
67
  if (start >= 0 && end >= start) {
@@ -72,7 +74,22 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
72
74
  // context, so the directive becomes a self-reference when copied through.
73
75
  // Match the directive when it appears on its own line (possibly indented).
74
76
  const SELF_INCLUDE_PATTERN = /^\s*@AIWG\.md\s*$/;
75
- const filtered = lines.filter((line) => !SELF_INCLUDE_PATTERN.test(line));
77
+ // #3451: CLAUDE.md already `@`-imports WORKSPACE.md; a second `@` path to
78
+ // the same file would load it twice, so AIWG.md refers to it in plain text.
79
+ const CONFIG_IMPORT_PATTERN = /^\s*@\S*aiwg\.config\s*$/;
80
+ const WORKSPACE_IMPORT_PATTERN = /^\s*@(?:\.\.\/)?WORKSPACE\.md\s*$/;
81
+ const filtered = lines
82
+ .filter((line) => !SELF_INCLUDE_PATTERN.test(line))
83
+ .map((line) => {
84
+ if (WORKSPACE_IMPORT_PATTERN.test(line)) {
85
+ return 'WORKSPACE.md is the canonical project context (loaded by CLAUDE.md; read it first if your harness did not).';
86
+ }
87
+ // Legacy CLAUDE.md files may still `@`-import the raw JSON config.
88
+ if (CONFIG_IMPORT_PATTERN.test(line)) {
89
+ return 'Project configuration: `.aiwg/aiwg.config` (not auto-loaded; run `aiwg config show --project` to view it).';
90
+ }
91
+ return line;
92
+ });
76
93
  const firstLine = filtered[0] ?? '';
77
94
  const rest = filtered.slice(1);
78
95
  // If the file already carries the signature elsewhere, don't double-insert.
@@ -21,9 +21,9 @@
21
21
  */
22
22
  import * as fs from 'fs/promises';
23
23
  import * as path from 'path';
24
- import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
24
+ import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, dedupeProviderBootstrap, } from './workspace-context.js';
25
25
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
26
- import { assertSafeContextFile } from './context-file-safety.js';
26
+ import { assertSafeContextFile, writeContextFileSync } from './context-file-safety.js';
27
27
  export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
28
28
  export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
29
29
  export function buildClaudeHookBlock(_policy = {}) {
@@ -31,7 +31,9 @@ export function buildClaudeHookBlock(_policy = {}) {
31
31
  CLAUDE_HOOK_START,
32
32
  '',
33
33
  buildProviderBootstrapBlock('claude'),
34
- '@.aiwg/aiwg.config',
34
+ // Plain-text pointer, deliberately not an `@` import: the raw JSON config
35
+ // is several KB and would be pulled into every system prompt (#3451).
36
+ 'Project configuration: `.aiwg/aiwg.config` (not auto-loaded; run `aiwg config show --project` to view it).',
35
37
  '',
36
38
  'Artifact destination authority: `aiwg show rule artifact-output-policy` (kernel).',
37
39
  '',
@@ -45,6 +47,30 @@ export function buildClaudeHookBlock(_policy = {}) {
45
47
  CLAUDE_HOOK_END,
46
48
  ].join('\n');
47
49
  }
50
+ /** Keep the first managed hook block and drop any later complete copies. */
51
+ function collapseExtraHookBlocks(content) {
52
+ const first = content.indexOf(CLAUDE_HOOK_START);
53
+ if (first === -1)
54
+ return content;
55
+ const firstEnd = content.indexOf(CLAUDE_HOOK_END, first);
56
+ if (firstEnd === -1)
57
+ return content;
58
+ const blockEnd = firstEnd + CLAUDE_HOOK_END.length;
59
+ let tail = content.slice(blockEnd);
60
+ const before = tail;
61
+ for (;;) {
62
+ const s = tail.indexOf(CLAUDE_HOOK_START);
63
+ if (s === -1)
64
+ break;
65
+ const e = tail.indexOf(CLAUDE_HOOK_END, s);
66
+ if (e === -1)
67
+ break;
68
+ tail = tail.slice(0, s) + tail.slice(e + CLAUDE_HOOK_END.length);
69
+ }
70
+ if (tail === before)
71
+ return content;
72
+ return content.slice(0, blockEnd) + tail.replace(/(?:\r?\n){3,}/g, '\n\n');
73
+ }
48
74
  /**
49
75
  * Ensure CLAUDE.md at projectPath contains the AIWG-managed marker block
50
76
  * pointing at AIWG.md. Returns a result describing what was done.
@@ -66,12 +92,17 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
66
92
  catch (err) {
67
93
  if (err.code === 'ENOENT') {
68
94
  const minimal = `${block}\n`;
69
- await fs.writeFile(claudeMdPath, minimal, 'utf8');
95
+ writeContextFileSync(claudeMdPath, minimal, 'utf8');
70
96
  result.action = 'created';
71
97
  return result;
72
98
  }
73
99
  throw err;
74
100
  }
101
+ // Repair duplicates left by earlier generations before locating markers, so
102
+ // regeneration converges on exactly one bootstrap (#3451).
103
+ const original = existing;
104
+ existing = collapseExtraHookBlocks(dedupeProviderBootstrap(existing));
105
+ const repaired = existing !== original;
75
106
  const lineEnding = dominantLineEnding(existing);
76
107
  block = withLineEnding(block, lineEnding);
77
108
  const startIdx = existing.indexOf(CLAUDE_HOOK_START);
@@ -90,7 +121,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
90
121
  const updated = existing.substring(0, bootstrapStartIdx)
91
122
  + block
92
123
  + existing.substring(bootstrapEndIdx);
93
- await fs.writeFile(claudeMdPath, updated, 'utf8');
124
+ writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
94
125
  result.action = 'updated';
95
126
  return result;
96
127
  }
@@ -101,7 +132,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
101
132
  // Ensure the file ends with a single newline before appending.
102
133
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
103
134
  const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
104
- await fs.writeFile(claudeMdPath, updated, 'utf8');
135
+ writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
105
136
  result.action = 'inserted';
106
137
  return result;
107
138
  }
@@ -114,12 +145,12 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
114
145
  // With --force, back up and append a fresh block.
115
146
  if (opts.detectExistingFiles !== false) {
116
147
  const backupPath = `${claudeMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
117
- await fs.writeFile(backupPath, existing, 'utf8');
148
+ writeContextFileSync(backupPath, existing, 'utf8');
118
149
  result.backupPath = backupPath;
119
150
  }
120
151
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
121
152
  const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
122
- await fs.writeFile(claudeMdPath, updated, 'utf8');
153
+ writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
123
154
  result.action = 'inserted';
124
155
  return result;
125
156
  }
@@ -127,6 +158,11 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
127
158
  const blockEndOffset = endIdx + CLAUDE_HOOK_END.length;
128
159
  const currentBlock = existing.substring(startIdx, blockEndOffset);
129
160
  if (currentBlock === block) {
161
+ if (repaired) {
162
+ writeContextFileSync(claudeMdPath, existing, 'utf8');
163
+ result.action = 'updated';
164
+ return result;
165
+ }
130
166
  result.action = 'unchanged';
131
167
  return result;
132
168
  }
@@ -135,14 +171,14 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
135
171
  // the case where an operator made deliberate edits inside the block).
136
172
  if (opts.force) {
137
173
  const backupPath = `${claudeMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
138
- await fs.writeFile(backupPath, existing, 'utf8');
174
+ writeContextFileSync(backupPath, existing, 'utf8');
139
175
  result.backupPath = backupPath;
140
176
  }
141
177
  else {
142
178
  result.warnings.push(`CLAUDE.md AIWG hook block has been modified inside the markers; updating to canonical content. Operator content outside the block is preserved.`);
143
179
  }
144
180
  const updated = existing.substring(0, startIdx) + block + existing.substring(blockEndOffset);
145
- await fs.writeFile(claudeMdPath, updated, 'utf8');
181
+ writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
146
182
  result.action = 'updated';
147
183
  return result;
148
184
  }
@@ -1,18 +1,339 @@
1
- import { lstat } from 'node:fs/promises';
2
- import { basename } from 'node:path';
3
- /** Never read or update a provider instruction file through a filesystem link. */
4
- export async function assertSafeContextFile(filePath) {
5
- let stat;
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { createHash } from 'node:crypto';
5
+ import { trackTemporaryDirectory } from '../../utils/temporary-directory.js';
6
+ const JOURNAL_ENV = 'AIWG_CONTEXT_WRITE_JOURNAL';
7
+ export const CONTEXT_STAGE_ENV = 'AIWG_CONTEXT_STAGE_LOG';
8
+ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
9
+ const identity = (stat) => ({ dev: String(stat.dev), ino: String(stat.ino) });
10
+ const sameIdentity = (a, b) => a.dev === b.dev && a.ino === b.ino;
11
+ const sameAncestors = (a, b) => a.length === b.length && a.every((identity, index) => sameIdentity(identity, b[index]));
12
+ // Optional flags are absent on some supported hosts (notably Windows).
13
+ function flags(...names) {
14
+ return names.reduce((value, name) => {
15
+ const flag = fs.constants[name];
16
+ return typeof flag === 'number' ? value | flag : value;
17
+ }, 0);
18
+ }
19
+ // Identity is dev/ino/size/mtime. A replacement that recycles the freed inode
20
+ // number with the same size inside one coarse timestamp tick is
21
+ // indistinguishable by metadata; the content hash still has to match deploy
22
+ // output, so restoring over such a file loses nothing the deploy wrote.
23
+ const version = (stat) => ({ ...identity(stat), size: String(stat.size), mtime: String(stat.mtimeNs) });
24
+ const sameVersion = (a, b) => sameIdentity(a, b) && a.size === b.size && a.mtime === b.mtime;
25
+ /** Probe child traversal, not just the descriptor link itself. /dev/fd on macOS
26
+ * can expose a descriptor without supporting directory-relative child opens. */
27
+ function descriptorRoot(root) {
28
+ if (process.platform !== 'linux')
29
+ return null;
30
+ let fd;
31
+ try {
32
+ fd = fs.openSync(root, flags('O_RDONLY', 'O_DIRECTORY', 'O_NOFOLLOW'));
33
+ const child = `/proc/self/fd/${fd}/.`;
34
+ return sameIdentity(identity(fs.statSync(child, { bigint: true })), identity(fs.fstatSync(fd, { bigint: true }))) ? '/proc/self/fd' : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ finally {
40
+ if (fd !== undefined)
41
+ fs.closeSync(fd);
42
+ }
43
+ }
44
+ /** Pin directories on Linux when descriptor-relative child traversal works.
45
+ * Elsewhere lstat every ancestor and recheck paths around each mutation. The
46
+ * portable fallback has a residual check-then-write race: only descriptor
47
+ * anchoring closes the interval between the last path check and the write. */
48
+ function withParent(file, create, action) {
49
+ const absolute = path.resolve(file);
50
+ const root = path.parse(absolute).root;
51
+ const parts = path.relative(root, absolute).split(path.sep);
52
+ const descriptors = [];
53
+ const identities = [];
54
+ const fdRoot = descriptorRoot(root);
55
+ try {
56
+ let directory = root;
57
+ for (const part of ['', ...parts.slice(0, -1)]) {
58
+ const next = part ? path.join(directory, part) : directory;
59
+ // Drive roots already exist; mkdir on a Windows drive root can be EPERM.
60
+ if (create && part) {
61
+ try {
62
+ fs.mkdirSync(next);
63
+ }
64
+ catch (error) {
65
+ if (error.code !== 'EEXIST')
66
+ throw error;
67
+ }
68
+ }
69
+ const checked = fs.lstatSync(next, { bigint: true });
70
+ if (checked.isSymbolicLink() || !checked.isDirectory())
71
+ throw new Error('Context ancestor changed');
72
+ identities.push(identity(checked));
73
+ directory = next;
74
+ if (fdRoot) {
75
+ const fd = fs.openSync(next, flags('O_RDONLY', 'O_DIRECTORY', 'O_NOFOLLOW'));
76
+ descriptors.push(fd);
77
+ if (!sameIdentity(identity(fs.fstatSync(fd, { bigint: true })), identity(checked)))
78
+ throw new Error('Context ancestor changed');
79
+ directory = `${fdRoot}/${fd}`;
80
+ }
81
+ }
82
+ const verify = () => {
83
+ let current = root;
84
+ for (let i = 0; i < identities.length; i++) {
85
+ if (i)
86
+ current = path.join(current, parts[i - 1]);
87
+ const stat = fs.lstatSync(current, { bigint: true });
88
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !sameIdentity(identity(stat), identities[i]))
89
+ throw new Error('Context ancestor changed');
90
+ }
91
+ };
92
+ verify();
93
+ return action(path.join(directory, parts[parts.length - 1]), identities, verify);
94
+ }
95
+ finally {
96
+ for (const fd of descriptors.reverse())
97
+ fs.closeSync(fd);
98
+ }
99
+ }
100
+ function checkedVersion(stat) {
101
+ if (stat.isSymbolicLink() || !stat.isFile() || !(stat.mode & 292n) || !(stat.mode & 146n) || stat.nlink !== 1n)
102
+ throw new Error('Refusing unreadable, unwritable, linked or non-file context target');
103
+ return version(stat);
104
+ }
105
+ function verifyTarget(file, expected) {
106
+ if (!sameVersion(checkedVersion(fs.lstatSync(file, { bigint: true })), expected))
107
+ throw new Error('Context identity changed');
108
+ }
109
+ function state(fd) {
110
+ const stat = fs.fstatSync(fd, { bigint: true });
111
+ const before = checkedVersion(stat);
112
+ const bytes = Buffer.alloc(Number(stat.size));
113
+ let offset = 0;
114
+ while (offset < bytes.length) {
115
+ const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
116
+ if (!count)
117
+ throw new Error('Context changed while reading');
118
+ offset += count;
119
+ }
120
+ if (!sameVersion(checkedVersion(fs.fstatSync(fd, { bigint: true })), before))
121
+ throw new Error('Context changed while reading');
122
+ return { ...before, mode: Number(stat.mode & 4095n), bytes: bytes.toString('base64'), hash: digest(bytes) };
123
+ }
124
+ function readState(file) {
125
+ let checked;
6
126
  try {
7
- stat = await lstat(filePath);
127
+ checked = checkedVersion(fs.lstatSync(file, { bigint: true }));
8
128
  }
9
129
  catch (error) {
10
130
  if (error.code === 'ENOENT')
11
- return;
131
+ return null;
12
132
  throw error;
13
133
  }
14
- if (stat.isSymbolicLink() || !stat.isFile()) {
15
- throw new Error(`Refusing unsafe ${basename(filePath)} target: ${filePath}`);
134
+ const fd = fs.openSync(file, flags('O_RDONLY', 'O_NOFOLLOW', 'O_NONBLOCK'));
135
+ try {
136
+ const result = state(fd);
137
+ if (!sameVersion(result, checked))
138
+ throw new Error('Context identity changed');
139
+ verifyTarget(file, result);
140
+ return result;
141
+ }
142
+ finally {
143
+ fs.closeSync(fd);
144
+ }
145
+ }
146
+ function matches(a, b) {
147
+ return sameVersion(a, b) && a.hash === b.hash && a.mode === b.mode;
148
+ }
149
+ function entries() {
150
+ const file = process.env[JOURNAL_ENV];
151
+ return file ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line)) : [];
152
+ }
153
+ /** Every context writer uses this boundary, including staged provider writers.
154
+ * Record immediately after the mutation, before returning to a fallible step. */
155
+ export function writeContextFileSync(file, content, _encoding) {
156
+ const absolute = path.resolve(file);
157
+ const stageLog = process.env[CONTEXT_STAGE_ENV];
158
+ const priorEntries = stageLog ? [] : entries();
159
+ withParent(absolute, true, (anchored, ancestors, verify) => {
160
+ const prior = readState(anchored);
161
+ const previous = priorEntries.reverse().find(entry => entry.path === absolute);
162
+ if (previous && (!prior || !matches(prior, previous.result) || !sameAncestors(ancestors, previous.ancestors)))
163
+ throw new Error(`Context changed during deployment: ${absolute}`);
164
+ verify();
165
+ const fd = fs.openSync(anchored, flags('O_RDWR', 'O_NOFOLLOW', 'O_NONBLOCK', ...(prior ? [] : ['O_CREAT', 'O_EXCL'])), prior?.mode ?? 0o666);
166
+ try {
167
+ verify();
168
+ if (prior && !matches(state(fd), prior))
169
+ throw new Error(`Context identity changed: ${absolute}`);
170
+ verify();
171
+ verifyTarget(anchored, prior ?? checkedVersion(fs.fstatSync(fd, { bigint: true })));
172
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
173
+ const opened = fs.fstatSync(fd, { bigint: true });
174
+ const priorBytes = prior ? Buffer.from(prior.bytes, 'base64') : Buffer.alloc(0);
175
+ const expected = Buffer.alloc(Math.max(buffer.length, priorBytes.length));
176
+ priorBytes.copy(expected);
177
+ let length = priorBytes.length;
178
+ try {
179
+ let offset = 0;
180
+ while (offset < buffer.length) {
181
+ const count = fs.writeSync(fd, buffer, offset, buffer.length - offset, offset);
182
+ if (!count)
183
+ throw new Error('Context write made no progress');
184
+ buffer.copy(expected, offset, offset, offset + count);
185
+ offset += count;
186
+ length = Math.max(length, offset);
187
+ }
188
+ fs.ftruncateSync(fd, buffer.length);
189
+ length = buffer.length;
190
+ }
191
+ finally {
192
+ // Hash the bytes this writer produced, never a later read that could
193
+ // accidentally claim an operator's concurrent edit as deploy-owned.
194
+ const completed = fs.fstatSync(fd, { bigint: true });
195
+ const result = { ...version(completed), ...identity(opened), size: String(length), mode: Number(opened.mode & 4095n), hash: digest(expected.subarray(0, length)) };
196
+ if (stageLog)
197
+ fs.appendFileSync(stageLog, `${JSON.stringify(absolute)}\n`);
198
+ else if (process.env[JOURNAL_ENV])
199
+ fs.appendFileSync(process.env[JOURNAL_ENV], `${JSON.stringify({ path: absolute, prior, result, ancestors })}\n`);
200
+ verify();
201
+ verifyTarget(anchored, result);
202
+ if (!matches(state(fd), result))
203
+ throw new Error(`Context changed during write: ${absolute}`);
204
+ }
205
+ }
206
+ finally {
207
+ fs.closeSync(fd);
208
+ }
209
+ });
210
+ }
211
+ /** Verify before reading provider startup files as well as at the write boundary. */
212
+ export async function assertSafeContextFile(file) {
213
+ try {
214
+ withParent(file, false, anchored => { readState(anchored); });
215
+ }
216
+ catch (error) {
217
+ if (error.code === 'ENOENT')
218
+ return;
219
+ throw new Error(`Refusing unsafe ${path.basename(file)} target: ${file}`, { cause: error });
220
+ }
221
+ }
222
+ /** Owned by the outer use invocation; the private log also crosses subprocesses. */
223
+ export class ContextWriteJournal {
224
+ directory = fs.mkdtempSync(path.join(tmpdir(), 'aiwg-context-writes-'));
225
+ removeDirectory = trackTemporaryDirectory(this.directory);
226
+ previous = process.env[JOURNAL_ENV];
227
+ constructor() {
228
+ fs.chmodSync(this.directory, 0o700);
229
+ const file = path.join(this.directory, 'writes.jsonl');
230
+ fs.writeFileSync(file, '', { mode: 0o600, flag: 'wx' });
231
+ process.env[JOURNAL_ENV] = file;
232
+ }
233
+ rollback() {
234
+ const result = { restored: [], conflicts: [] };
235
+ const blocked = new Set();
236
+ // Restoring a later pass changes mtime. Carry that verified version back
237
+ // through earlier entries rather than treating our own restore as an edit.
238
+ const restoredVersions = new Map();
239
+ for (const entry of entries().reverse()) {
240
+ if (blocked.has(entry.path))
241
+ continue;
242
+ try {
243
+ withParent(entry.path, false, (anchored, ancestors, verify) => {
244
+ if (!sameAncestors(ancestors, entry.ancestors))
245
+ throw new Error('Context ancestor identity changed');
246
+ const checked = checkedVersion(fs.lstatSync(anchored, { bigint: true }));
247
+ const fd = fs.openSync(anchored, flags('O_RDWR', 'O_NOFOLLOW', 'O_NONBLOCK'));
248
+ try {
249
+ verify();
250
+ const current = state(fd);
251
+ if (!sameVersion(current, checked))
252
+ throw new Error('Context identity changed');
253
+ if (!matches(current, restoredVersions.get(entry.path) ?? entry.result))
254
+ throw new Error('Context changed since deployment wrote it');
255
+ verify();
256
+ verifyTarget(anchored, current);
257
+ if (!matches(state(fd), current))
258
+ throw new Error('Context changed before restore');
259
+ if (entry.prior) {
260
+ const bytes = Buffer.from(entry.prior.bytes, 'base64');
261
+ let offset = 0;
262
+ while (offset < bytes.length) {
263
+ const count = fs.writeSync(fd, bytes, offset, bytes.length - offset, offset);
264
+ if (!count)
265
+ throw new Error('Context restore made no progress');
266
+ offset += count;
267
+ }
268
+ fs.ftruncateSync(fd, bytes.length);
269
+ fs.fchmodSync(fd, entry.prior.mode);
270
+ verify();
271
+ const restored = state(fd);
272
+ if (!sameIdentity(restored, current) || restored.hash !== entry.prior.hash || restored.mode !== entry.prior.mode)
273
+ throw new Error('Context changed during restore');
274
+ verifyTarget(anchored, restored);
275
+ restoredVersions.set(entry.path, restored);
276
+ }
277
+ else {
278
+ // Quarantine before deleting: a final-component swap between the
279
+ // identity check and rename must be kept, never silently unlinked.
280
+ const quarantine = fs.mkdtempSync(path.join(path.dirname(anchored), '.aiwg-context-rollback-'));
281
+ const retained = path.join(quarantine, 'context');
282
+ try {
283
+ fs.renameSync(anchored, retained);
284
+ try {
285
+ const moved = readState(retained);
286
+ if (!moved || !matches(moved, current))
287
+ throw new Error('Context changed during removal');
288
+ verify();
289
+ fs.unlinkSync(retained);
290
+ }
291
+ catch (error) {
292
+ try {
293
+ fs.linkSync(retained, anchored);
294
+ fs.unlinkSync(retained);
295
+ }
296
+ catch {
297
+ const saved = path.join(path.dirname(entry.path), path.basename(quarantine), 'context');
298
+ throw new Error(`Context changed during removal; preserved at ${saved}`, { cause: error });
299
+ }
300
+ throw error;
301
+ }
302
+ verify();
303
+ if (fs.existsSync(anchored))
304
+ throw new Error('Context path replaced during rollback');
305
+ }
306
+ finally {
307
+ try {
308
+ fs.rmdirSync(quarantine);
309
+ }
310
+ catch (error) {
311
+ if (error.code !== 'ENOTEMPTY')
312
+ throw error;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ finally {
318
+ fs.closeSync(fd);
319
+ }
320
+ });
321
+ if (!result.restored.includes(entry.path))
322
+ result.restored.push(entry.path);
323
+ }
324
+ catch (error) {
325
+ blocked.add(entry.path);
326
+ result.conflicts.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
327
+ }
328
+ }
329
+ return result;
330
+ }
331
+ dispose() {
332
+ if (this.previous === undefined)
333
+ delete process.env[JOURNAL_ENV];
334
+ else
335
+ process.env[JOURNAL_ENV] = this.previous;
336
+ this.removeDirectory();
16
337
  }
17
338
  }
18
339
  //# sourceMappingURL=context-file-safety.js.map
@@ -1,3 +1,4 @@
1
+ import { writeContextFileSync } from './context-file-safety.js';
1
2
  /**
2
3
  * Context-finalization pass for generated provider context files.
3
4
  *
@@ -132,13 +133,13 @@ export async function writeNormalizedAiwgMd(projectPath) {
132
133
  const content = await buildNormalizedAiwgMd(projectPath, existing);
133
134
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
134
135
  const normalizedContent = content.endsWith('\n') ? content : `${content}\n`;
135
- await fs.writeFile(targetPath, normalizedContent, 'utf8');
136
+ writeContextFileSync(targetPath, normalizedContent, 'utf8');
136
137
  const artifactRoot = resolveProjectAiwgDir(projectPath);
137
138
  const artifactPath = projectAiwgPath(projectPath, 'AIWG.md');
138
139
  if (artifactPath !== targetPath) {
139
140
  try {
140
141
  await fs.access(artifactRoot);
141
- await fs.writeFile(artifactPath, normalizedContent, 'utf8');
142
+ writeContextFileSync(artifactPath, normalizedContent, 'utf8');
142
143
  }
143
144
  catch (error) {
144
145
  if (error.code !== 'ENOENT')