@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.
- package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +7 -3
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +57 -6
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +143 -14
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +114 -55
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +22 -3
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +1 -0
- package/dist/src/cli/handlers/setup.js +432 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +335 -107
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +81 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +482 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +23 -18
- package/dist/src/decision/qualification/load-staged.js +576 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +12 -5
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +238 -0
- package/dist/src/discovery/plan-apply.js +567 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +22 -96
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +8 -1
- package/dist/src/models/worker-installation.mjs +231 -38
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +124 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +10 -9
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +63 -53
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/workspace-discovery.js +3 -4
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +24 -14
- package/tools/agents/providers/claude.mjs +6 -3
- package/tools/agents/providers/codex.mjs +54 -57
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +13 -4
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +12 -15
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +31 -5
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +289 -80
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +13 -3
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +49 -8
- package/tools/ralph-external/session-launcher.mjs +405 -35
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- 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.
|
|
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.
|
|
13
|
-
start:
|
|
14
|
-
end:
|
|
13
|
+
span: z.strictObject({
|
|
14
|
+
start: integer().nonnegative(),
|
|
15
|
+
end: integer().positive(),
|
|
15
16
|
quoteDigest: DigestSchema,
|
|
16
|
-
}).
|
|
17
|
-
}).
|
|
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.
|
|
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.
|
|
26
|
+
contextPack: z.strictObject({
|
|
26
27
|
id: ReferenceSchema,
|
|
27
28
|
digest: DigestSchema,
|
|
28
29
|
sources: z.array(OutputSourceReferenceSchema).min(1).max(256),
|
|
29
|
-
})
|
|
30
|
+
}),
|
|
30
31
|
supersedes: z.array(ReferenceSchema).max(128).default([]),
|
|
31
32
|
conflictsWith: z.array(ReferenceSchema).max(128).default([]),
|
|
32
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
|
|
146
182
|
result.action = 'updated';
|
|
147
183
|
return result;
|
|
148
184
|
}
|
|
@@ -1,18 +1,339 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
writeContextFileSync(artifactPath, normalizedContent, 'utf8');
|
|
142
143
|
}
|
|
143
144
|
catch (error) {
|
|
144
145
|
if (error.code !== 'ENOENT')
|