@aiwg/cli 2026.9.4 → 2026.9.5

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 (93) hide show
  1. package/THIRD_PARTY_NOTICES.md +16 -0
  2. package/agentic/code/providers/capability-matrix.yaml +45 -3
  3. package/agentic/code/providers/deepseek-harness/README.md +8 -0
  4. package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
  5. package/agentic/code/providers/model-capabilities.v1.json +11 -0
  6. package/agentic/code/providers/model-catalog.v1.json +8 -0
  7. package/bin/aiwg.mjs +1 -0
  8. package/dist/src/api/index.d.ts +14 -0
  9. package/dist/src/api/index.js +14 -0
  10. package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
  11. package/dist/src/artifacts/index-files.js +17 -2
  12. package/dist/src/artifacts/repair.js +55 -6
  13. package/dist/src/catalog/cli.js +21 -7
  14. package/dist/src/catalog/cli.mjs +22 -7
  15. package/dist/src/cli/agent-spawn.js +10 -1
  16. package/dist/src/cli/handlers/artifacts.js +22 -3
  17. package/dist/src/cli/handlers/help.js +3 -0
  18. package/dist/src/cli/handlers/index.js +5 -1
  19. package/dist/src/cli/handlers/models.js +2 -2
  20. package/dist/src/cli/handlers/output-mode.js +1 -1
  21. package/dist/src/cli/handlers/runtime-info.js +1 -1
  22. package/dist/src/cli/handlers/sessions.js +55 -15
  23. package/dist/src/cli/handlers/steward.js +1 -1
  24. package/dist/src/cli/handlers/subcommands.js +5 -0
  25. package/dist/src/cli/handlers/writer-profile.js +110 -0
  26. package/dist/src/cli/handlers/writing.js +122 -0
  27. package/dist/src/cli/router.js +5 -1
  28. package/dist/src/config/project-artifacts-runtime.mjs +33 -1
  29. package/dist/src/config/project-artifacts.js +1 -1
  30. package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
  31. package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
  32. package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
  33. package/dist/src/dataset/fortemi-live-qualification.js +20 -21
  34. package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
  35. package/dist/src/dataset/fortemi-run-receipt.js +74 -0
  36. package/dist/src/dataset/index.d.ts +2 -0
  37. package/dist/src/dataset/index.js +2 -0
  38. package/dist/src/extensions/commands/definitions.js +24 -0
  39. package/dist/src/extensions/manifest.js +3 -0
  40. package/dist/src/mcp/server.mjs +2 -0
  41. package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
  42. package/dist/src/models/model-capabilities.v1.json +11 -0
  43. package/dist/src/models/model-catalog.v1.json +8 -0
  44. package/dist/src/models/provider-policy.js +1 -1
  45. package/dist/src/network-analysis/analyzer.js +667 -0
  46. package/dist/src/network-analysis/citations.js +107 -0
  47. package/dist/src/network-analysis/forensics.js +132 -0
  48. package/dist/src/network-analysis/governance.js +216 -0
  49. package/dist/src/network-analysis/index.js +10 -0
  50. package/dist/src/network-analysis/probe.js +405 -0
  51. package/dist/src/network-analysis/recipes.js +88 -0
  52. package/dist/src/network-analysis/research.js +181 -0
  53. package/dist/src/network-analysis/termshark.js +252 -0
  54. package/dist/src/network-analysis/verification.js +171 -0
  55. package/dist/src/output-modes/registry.js +37 -6
  56. package/dist/src/output-modes/runtime.js +164 -28
  57. package/dist/src/providers/capability-matrix.yaml +45 -3
  58. package/dist/src/providers/provider-definitions.js +49 -0
  59. package/dist/src/providers/provider-inventory.js +1 -0
  60. package/dist/src/providers/transformation-receipt.js +3 -2
  61. package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
  62. package/dist/src/sessions/batch-import.js +7 -0
  63. package/dist/src/sessions/contracts.js +2 -1
  64. package/dist/src/sessions/index.js +1 -0
  65. package/dist/src/sessions/workspace-discovery.js +5 -1
  66. package/dist/src/skills/deployer.js +6 -6
  67. package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
  68. package/dist/src/writing/channel-packs.js +13 -0
  69. package/dist/src/writing/contextual-diagnostics.js +142 -0
  70. package/dist/src/writing/example-generator.js +7 -6
  71. package/dist/src/writing/exemplar-selection.js +186 -0
  72. package/dist/src/writing/fidelity.js +61 -0
  73. package/dist/src/writing/validation-engine.js +32 -15
  74. package/dist/src/writing/voice-evaluation.js +301 -0
  75. package/dist/src/writing/voice-revision.js +201 -0
  76. package/dist/src/writing/writer-migration.js +216 -0
  77. package/dist/src/writing/writer-profile-legacy.js +145 -0
  78. package/dist/src/writing/writer-profile-store.js +117 -0
  79. package/dist/src/writing/writer-profile.js +222 -0
  80. package/dist/src/writing/writing-brief.js +166 -0
  81. package/dist/src/writing/writing-channels.js +63 -0
  82. package/dist/src/writing/writing-consumer.js +39 -0
  83. package/dist/src/writing/writing-receipt.js +266 -0
  84. package/package.json +1 -1
  85. package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
  86. package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
  87. package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
  88. package/tools/agents/deploy-agents.mjs +7 -3
  89. package/tools/agents/providers/antigravity.mjs +1 -1
  90. package/tools/agents/providers/base.mjs +3 -2
  91. package/tools/agents/providers/deepseek-harness.mjs +66 -0
  92. package/tools/agents/providers/hermes.mjs +1 -1
  93. package/tools/agents/providers/openhuman.mjs +2 -2
@@ -0,0 +1,252 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { lstat, mkdir, realpath } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import semver from 'semver';
6
+ import { formatEvidenceCitation } from './citations.js';
7
+ import { hashEvidenceFile, safeProcessSpec } from './governance.js';
8
+ export const TERMSHARK_HANDOFF_SCHEMA = 'aiwg.network-analysis.termshark-handoff.v1';
9
+ export function createDefaultTermsharkLaunchHost() {
10
+ return {
11
+ async run(spec, options) {
12
+ const cacheRoot = path.join(options.configRoot, 'cache');
13
+ const dataRoot = path.join(options.configRoot, 'data');
14
+ await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
15
+ await mkdir(dataRoot, { recursive: true, mode: 0o700 });
16
+ return new Promise((resolve, reject) => {
17
+ const child = spawn(spec.file, [...spec.args], {
18
+ shell: false,
19
+ stdio: 'inherit',
20
+ signal: options.signal,
21
+ env: {
22
+ PATH: path.dirname(options.tsharkPath),
23
+ XDG_CONFIG_HOME: options.configRoot,
24
+ XDG_CACHE_HOME: cacheRoot,
25
+ XDG_DATA_HOME: dataRoot,
26
+ HOME: options.configRoot,
27
+ TERM: process.env.TERM || 'xterm-256color',
28
+ LC_ALL: 'C',
29
+ LANG: 'C',
30
+ },
31
+ });
32
+ child.once('error', reject);
33
+ child.once('close', (exitCode, signal) => resolve({ exitCode, signal }));
34
+ });
35
+ },
36
+ };
37
+ }
38
+ export async function createTermsharkHandoff(options) {
39
+ validateHandoffOptions(options);
40
+ const context = findAnalysisContext(options.evidence, options.contextDigest);
41
+ if (context.tool?.name !== 'tshark' || context.tool?.executable_path !== options.tsharkPath) {
42
+ throw new Error('Termshark handoff TShark path does not match the evidence analysis context');
43
+ }
44
+ const sourceHash = String(options.evidence.capture.hashes?.source?.value ?? '');
45
+ const actual = await hashEvidenceFile(options.capturePath, 'source-capture');
46
+ if (sourceHash !== actual.value || options.evidence.capture.capture_digest !== `sha256:${actual.value}`) {
47
+ throw new Error('Termshark handoff capture does not match the evidence bundle identity');
48
+ }
49
+ const displayFilterDigest = `sha256:${sha256(options.displayFilter)}`;
50
+ if (context.display_filter_digest !== displayFilterDigest) {
51
+ throw new Error('Termshark handoff display filter does not match the evidence analysis context');
52
+ }
53
+ const canonicalConfigRoot = await validateDirectory(options.configRoot, 'Termshark config root');
54
+ const profile = await validateProfile(options.profile, canonicalConfigRoot);
55
+ const focus = normalizeFocus(options.focus);
56
+ validateFocusEvidence(options.evidence, options.contextDigest, focus);
57
+ const effectiveFilter = focusedDisplayFilter(options.displayFilter, focus);
58
+ const base = baseHandoff(options, profile, canonicalConfigRoot, focus, displayFilterDigest, effectiveFilter);
59
+ if (options.termshark.status === 'missing' || !options.termshark.path || !options.termshark.version) {
60
+ return {
61
+ ...base,
62
+ status: 'unavailable',
63
+ diagnostics: [...options.termshark.diagnostics, 'Termshark is optional; TShark evidence remains valid and available.'],
64
+ command: null,
65
+ commandPreview: null,
66
+ };
67
+ }
68
+ if (options.termshark.status !== 'supported' || !semver.valid(options.termshark.version)
69
+ || !semver.satisfies(options.termshark.version, '>=2.4.0 <3.0.0')) {
70
+ return {
71
+ ...base,
72
+ status: 'incompatible',
73
+ diagnostics: [...options.termshark.diagnostics, `Termshark ${options.termshark.version} is outside the supported >=2.4.0 <3.0.0 range; TShark evidence remains valid.`],
74
+ command: null,
75
+ commandPreview: null,
76
+ };
77
+ }
78
+ const command = safeProcessSpec(options.termshark.path, [
79
+ '-r', options.capturePath,
80
+ '-Y', effectiveFilter,
81
+ '-C', profile.name,
82
+ ]);
83
+ return {
84
+ ...base,
85
+ status: 'ready',
86
+ diagnostics: [...options.termshark.diagnostics],
87
+ command,
88
+ commandPreview: formatCommandPreview(command),
89
+ };
90
+ }
91
+ export async function launchTermsharkHandoff(handoff, action, host = createDefaultTermsharkLaunchHost()) {
92
+ if (action.explicitOperatorAction !== true)
93
+ throw new Error('Termshark launch requires an explicit operator action');
94
+ if (handoff.status !== 'ready' || !handoff.command)
95
+ throw new Error('Termshark handoff is not ready to launch');
96
+ await validateLaunchIdentity(handoff);
97
+ const actual = await hashEvidenceFile(handoff.capturePath, 'source-capture');
98
+ if (`sha256:${actual.value}` !== handoff.captureDigest) {
99
+ throw new Error('Termshark launch capture no longer matches the reviewed handoff');
100
+ }
101
+ return host.run(handoff.command, { configRoot: handoff.configRoot, tsharkPath: handoff.tsharkPath, signal: action.signal });
102
+ }
103
+ export function recordTermsharkReview(handoff, evidence, input) {
104
+ if (evidence.bundle_id !== handoff.evidenceBundleId || evidence.capture.capture_digest !== handoff.captureDigest) {
105
+ throw new Error('Operator note evidence does not match the reviewed Termshark handoff');
106
+ }
107
+ if (!input.analyst.trim() || !input.note.trim() || input.citations.length === 0)
108
+ throw new Error('Analyst, note, and at least one packet locator are required');
109
+ const evidenceCitations = new Set(evidence.evidence_items.flatMap(item => item.citations.map(formatEvidenceCitation)));
110
+ for (const citation of input.citations) {
111
+ if (citation.capture_digest !== handoff.captureDigest || !evidenceCitations.has(formatEvidenceCitation(citation))) {
112
+ throw new Error('Operator note citation is not present in the evidence bundle');
113
+ }
114
+ }
115
+ return {
116
+ analyst: input.analyst,
117
+ recordedAt: input.recordedAt ?? new Date().toISOString(),
118
+ note: input.note,
119
+ citations: structuredClone(input.citations),
120
+ source: 'operator-returned-note',
121
+ };
122
+ }
123
+ function baseHandoff(options, profile, configRoot, focus, displayFilterDigest, effectiveFilter) {
124
+ const fingerprint = sha256(JSON.stringify({
125
+ bundle: options.evidence.bundle_id,
126
+ capture: options.evidence.capture.capture_digest,
127
+ context: options.contextDigest,
128
+ filter: effectiveFilter,
129
+ profile,
130
+ tool: options.termshark,
131
+ }));
132
+ return {
133
+ schema: TERMSHARK_HANDOFF_SCHEMA,
134
+ handoffId: `termshark-handoff:${fingerprint.slice(0, 24)}`,
135
+ evidenceBundleId: options.evidence.bundle_id,
136
+ capturePath: options.capturePath,
137
+ captureDigest: String(options.evidence.capture.capture_digest),
138
+ analysisContextDigest: options.contextDigest,
139
+ displayFilterDigest,
140
+ effectiveFilterDigest: `sha256:${sha256(effectiveFilter)}`,
141
+ focus,
142
+ tool: { path: options.termshark.path, version: options.termshark.version },
143
+ profile,
144
+ configRoot,
145
+ tsharkPath: options.tsharkPath,
146
+ tsharkVersion: String(findAnalysisContext(options.evidence, options.contextDigest).tool.version),
147
+ createdAt: (options.now ?? (() => new Date()))().toISOString(),
148
+ completionClaim: 'operator-returned-notes-only',
149
+ };
150
+ }
151
+ function findAnalysisContext(evidence, digest) {
152
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
153
+ throw new Error('Termshark handoff requires a canonical analysis context digest');
154
+ const contexts = evidence.capture.analysis_contexts;
155
+ const context = contexts.find(candidate => candidate.context_digest === digest);
156
+ if (!context)
157
+ throw new Error('Termshark handoff analysis context is absent from the evidence bundle');
158
+ return context;
159
+ }
160
+ async function validateDirectory(directory, label) {
161
+ if (!path.isAbsolute(directory))
162
+ throw new Error(`${label} must be an explicit absolute path`);
163
+ const stat = await lstat(directory);
164
+ if (stat.isSymbolicLink() || !stat.isDirectory())
165
+ throw new Error(`${label} must be a real non-symlink directory`);
166
+ return realpath(directory);
167
+ }
168
+ async function validateProfile(profile, configRoot) {
169
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profile.name))
170
+ throw new Error('Termshark profile name is invalid');
171
+ if (!profile.path)
172
+ return { name: profile.name };
173
+ if (!path.isAbsolute(profile.path))
174
+ throw new Error('Termshark profile path must be explicit and absolute');
175
+ const canonical = await realpath(profile.path);
176
+ const relative = path.relative(configRoot, canonical);
177
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
178
+ throw new Error('Termshark profile path must remain below the config root');
179
+ }
180
+ const identity = await hashEvidenceFile(canonical, 'derived-artifact');
181
+ return { name: profile.name, path: canonical, digest: `sha256:${identity.value}` };
182
+ }
183
+ function normalizeFocus(focus) {
184
+ if (!focus)
185
+ return {};
186
+ if (focus.frame !== undefined && (!Number.isSafeInteger(focus.frame) || focus.frame < 1))
187
+ throw new Error('Termshark frame focus is invalid');
188
+ if (focus.stream && (!['tcp', 'udp'].includes(focus.stream.protocol) || !Number.isSafeInteger(focus.stream.id) || focus.stream.id < 0)) {
189
+ throw new Error('Termshark stream focus is invalid');
190
+ }
191
+ return structuredClone(focus);
192
+ }
193
+ function validateFocusEvidence(evidence, contextDigest, focus) {
194
+ if (focus.frame === undefined && focus.stream === undefined)
195
+ return;
196
+ const citations = evidence.evidence_items.flatMap(item => (item.citations ?? []));
197
+ if (focus.frame !== undefined && !citations.some(citation => citation.locator.type === 'frame'
198
+ && citation.locator.frame_number === focus.frame)) {
199
+ throw new Error('Termshark frame focus is absent from the evidence bundle');
200
+ }
201
+ if (focus.stream && !citations.some(citation => citation.locator.type === 'stream'
202
+ && citation.locator.protocol === focus.stream?.protocol
203
+ && citation.locator.stream_id === focus.stream.id
204
+ && citation.locator.context_digest === contextDigest)) {
205
+ throw new Error('Termshark stream focus is absent from the selected analysis context');
206
+ }
207
+ }
208
+ async function validateLaunchIdentity(handoff) {
209
+ if (!handoff.command || handoff.command.shell !== false || handoff.command.file !== handoff.tool.path) {
210
+ throw new Error('Termshark launch command no longer matches the reviewed tool');
211
+ }
212
+ const args = handoff.command.args;
213
+ if (args.length !== 6 || args[0] !== '-r' || args[1] !== handoff.capturePath
214
+ || args[2] !== '-Y' || args[4] !== '-C' || args[5] !== handoff.profile.name
215
+ || `sha256:${sha256(args[3])}` !== handoff.effectiveFilterDigest) {
216
+ throw new Error('Termshark launch arguments no longer match the reviewed handoff');
217
+ }
218
+ const configRoot = await validateDirectory(handoff.configRoot, 'Termshark config root');
219
+ if (configRoot !== handoff.configRoot)
220
+ throw new Error('Termshark config root identity changed after review');
221
+ if (handoff.profile.path) {
222
+ const profile = await validateProfile({ name: handoff.profile.name, path: handoff.profile.path }, configRoot);
223
+ if (profile.path !== handoff.profile.path || profile.digest !== handoff.profile.digest) {
224
+ throw new Error('Termshark profile identity changed after review');
225
+ }
226
+ }
227
+ }
228
+ function focusedDisplayFilter(displayFilter, focus) {
229
+ if (!displayFilter.trim() || displayFilter.length > 8192 || displayFilter.includes('\0'))
230
+ throw new Error('Termshark display filter is invalid');
231
+ const additions = [];
232
+ if (focus.frame !== undefined)
233
+ additions.push(`frame.number == ${focus.frame}`);
234
+ if (focus.stream)
235
+ additions.push(`${focus.stream.protocol}.stream == ${focus.stream.id}`);
236
+ return [displayFilter, ...additions].map(expression => `(${expression})`).join(' && ');
237
+ }
238
+ function validateHandoffOptions(options) {
239
+ if (!path.isAbsolute(options.capturePath))
240
+ throw new Error('Termshark capture path must be explicit and absolute');
241
+ if (!path.isAbsolute(options.tsharkPath))
242
+ throw new Error('TShark path must be explicit and absolute');
243
+ if (options.termshark.path !== null && !path.isAbsolute(options.termshark.path))
244
+ throw new Error('Termshark path must be explicit and absolute');
245
+ }
246
+ function formatCommandPreview(spec) {
247
+ return [spec.file, ...spec.args].map(argument => `'${argument.replace(/'/g, `'"'"'`)}'`).join(' ');
248
+ }
249
+ function sha256(value) {
250
+ return createHash('sha256').update(value).digest('hex');
251
+ }
252
+ //# sourceMappingURL=termshark.js.map
@@ -0,0 +1,171 @@
1
+ export const PACKET_VERIFICATION_SCHEMA = 'aiwg.sdlc.packet-verification.v1';
2
+ const METRICS = [
3
+ 'summary.packet_count',
4
+ 'summary.endpoint_count',
5
+ 'summary.conversation_count',
6
+ 'summary.duration_seconds',
7
+ 'summary.tcp_retransmission_count',
8
+ 'summary.tcp_reset_count',
9
+ 'summary.encrypted_transport_frame_count',
10
+ ];
11
+ export function comparePacketEvidenceForVerification(input) {
12
+ validateInput(input);
13
+ const baselineRecipe = recipeId(input.baseline.evidence);
14
+ const candidateRecipe = recipeId(input.candidate.evidence);
15
+ const baselineContext = analysisContext(input.baseline.evidence);
16
+ const candidateContext = analysisContext(input.candidate.evidence);
17
+ const hardReasons = [];
18
+ if (baselineRecipe !== candidateRecipe)
19
+ hardReasons.push(`Recipe mismatch: ${baselineRecipe} vs ${candidateRecipe}.`);
20
+ if (baselineContext.display_filter_digest !== candidateContext.display_filter_digest)
21
+ hardReasons.push('Display-filter digests differ.');
22
+ if (baselineContext.tool?.version !== candidateContext.tool?.version)
23
+ hardReasons.push('TShark versions differ.');
24
+ if (input.baseline.topologyFingerprint !== input.candidate.topologyFingerprint)
25
+ hardReasons.push('Environment topology fingerprints differ.');
26
+ if (!['completed', 'empty'].includes(input.baseline.evidence.status) || !['completed', 'empty'].includes(input.candidate.evidence.status)) {
27
+ hardReasons.push('At least one evidence bundle is partial or unusable.');
28
+ }
29
+ const excludedMetrics = [];
30
+ const clockReasons = [];
31
+ if (input.baseline.clockOffsetMs === null || input.candidate.clockOffsetMs === null) {
32
+ excludedMetrics.push('summary.duration_seconds');
33
+ clockReasons.push('Clock alignment is unknown; timing comparison is excluded.');
34
+ }
35
+ else if (Math.abs(input.baseline.clockOffsetMs - input.candidate.clockOffsetMs) > input.conditions.clockAlignment.toleranceMs) {
36
+ excludedMetrics.push('summary.duration_seconds');
37
+ clockReasons.push('Clock-offset difference exceeds the declared tolerance; timing comparison is excluded.');
38
+ }
39
+ const comparableMetrics = hardReasons.length > 0 ? [] : METRICS.filter(metric => !excludedMetrics.includes(metric));
40
+ const baselineMetrics = summaryMetrics(input.baseline.evidence);
41
+ const candidateMetrics = summaryMetrics(input.candidate.evidence);
42
+ const metricDifferences = comparableMetrics.flatMap(metric => {
43
+ const baseline = baselineMetrics.get(metric);
44
+ const candidate = candidateMetrics.get(metric);
45
+ return typeof baseline !== 'number' || typeof candidate !== 'number'
46
+ ? []
47
+ : [{ metric, baseline, candidate, delta: candidate - baseline }];
48
+ });
49
+ const protocolDifferences = hardReasons.length > 0 ? [] : compareProtocols(baselineMetrics, candidateMetrics);
50
+ const reasons = [...hardReasons, ...clockReasons];
51
+ return {
52
+ schema: PACKET_VERIFICATION_SCHEMA,
53
+ verificationId: input.verificationId,
54
+ useCase: input.useCase,
55
+ traceability: {
56
+ requirementIds: [...input.requirementIds],
57
+ testIds: [...input.testIds],
58
+ defectOrChangeId: input.defectOrChangeId,
59
+ baseline: traceSide(input.baseline, baselineRecipe),
60
+ candidate: traceSide(input.candidate, candidateRecipe),
61
+ },
62
+ comparability: {
63
+ status: hardReasons.length > 0 ? 'incomparable' : excludedMetrics.length > 0 ? 'partially-comparable' : 'comparable',
64
+ reasons,
65
+ excludedMetrics,
66
+ },
67
+ conditions: {
68
+ ...structuredClone(input.conditions),
69
+ environments: { baseline: input.baseline.environmentId, candidate: input.candidate.environmentId },
70
+ topologyFingerprints: { baseline: input.baseline.topologyFingerprint, candidate: input.candidate.topologyFingerprint },
71
+ clockOffsetsMs: { baseline: input.baseline.clockOffsetMs, candidate: input.candidate.clockOffsetMs },
72
+ },
73
+ normalization: {
74
+ comparedFields: [...comparableMetrics, 'summary.protocol_hierarchy'],
75
+ excludedVolatileFields: ['capture digest', 'frame number', 'stream ID', 'absolute timestamp', 'endpoint identity', 'ephemeral port'],
76
+ },
77
+ metricDifferences,
78
+ protocolDifferences,
79
+ attachments: [attachment('baseline', input.baseline, baselineRecipe), attachment('candidate', input.candidate, candidateRecipe)],
80
+ performanceInterpretation: {
81
+ causality: 'not-established',
82
+ statement: 'Differences are observations under the recorded traffic, load, topology, vantage-point, sample-window, and clock conditions; they do not establish that the build or change caused the result.',
83
+ },
84
+ };
85
+ }
86
+ function validateInput(input) {
87
+ for (const [label, value] of Object.entries({ verification: input.verificationId, change: input.defectOrChangeId }))
88
+ required(value, label);
89
+ if (input.requirementIds.length === 0 || input.testIds.length === 0)
90
+ throw new Error('Packet verification requires requirement and test identities');
91
+ stringArray(input.requirementIds, 'Requirement IDs');
92
+ stringArray(input.testIds, 'Test IDs');
93
+ stringArray(input.conditions.topologyAssumptions, 'Topology assumptions');
94
+ if (input.conditions.topologyAssumptions.length === 0)
95
+ throw new Error('Packet verification requires topology assumptions');
96
+ for (const value of [input.baseline.buildId, input.candidate.buildId, input.baseline.environmentId, input.candidate.environmentId,
97
+ input.baseline.topologyFingerprint, input.candidate.topologyFingerprint, input.conditions.vantagePoint, input.conditions.traffic.scenario,
98
+ input.conditions.traffic.isolation, input.conditions.load, input.conditions.clockAlignment.method])
99
+ required(value, 'Verification condition');
100
+ if (!Number.isFinite(input.conditions.sampleWindowSeconds) || input.conditions.sampleWindowSeconds <= 0
101
+ || !Number.isFinite(input.conditions.clockAlignment.toleranceMs) || input.conditions.clockAlignment.toleranceMs < 0) {
102
+ throw new Error('Packet verification measurement bounds are invalid');
103
+ }
104
+ if (input.conditions.traffic.capturesUnrelatedTraffic !== false)
105
+ throw new Error('Packet verification must exclude unrelated traffic');
106
+ if (input.executionContext === 'ci' && input.conditions.traffic.kind !== 'synthetic') {
107
+ throw new Error('CI packet verification requires isolated synthetic traffic');
108
+ }
109
+ }
110
+ function analysisContext(evidence) {
111
+ const contexts = evidence.capture.analysis_contexts;
112
+ if (!Array.isArray(contexts) || contexts.length !== 1)
113
+ throw new Error('Packet verification requires one analysis context per bundle');
114
+ return contexts[0];
115
+ }
116
+ function recipeId(evidence) {
117
+ return required(evidence.provenance?.recipe_id, 'Recipe identity');
118
+ }
119
+ function traceSide(side, recipe) {
120
+ return { buildId: side.buildId, bundleId: side.evidence.bundle_id, captureDigest: String(side.evidence.capture.capture_digest), recipeId: recipe };
121
+ }
122
+ function attachment(sideName, side, recipe) {
123
+ return {
124
+ side: sideName,
125
+ bundleId: side.evidence.bundle_id,
126
+ captureDigest: String(side.evidence.capture.capture_digest),
127
+ recipeId: recipe,
128
+ artifactDigests: side.evidence.artifacts.map((artifact) => `${artifact.digest.algorithm}:${artifact.digest.value}`),
129
+ rawCaptureIncluded: false,
130
+ };
131
+ }
132
+ function summaryMetrics(evidence) {
133
+ const summary = evidence.evidence_items.find(item => String(item.evidence_id).includes('capture-summary'));
134
+ const fields = Array.isArray(summary?.observed_fields) ? summary.observed_fields : [];
135
+ const result = new Map();
136
+ for (const field of fields) {
137
+ if (typeof field.value === 'number' || typeof field.value === 'string')
138
+ result.set(field.name, field.value);
139
+ }
140
+ return result;
141
+ }
142
+ function compareProtocols(baseline, candidate) {
143
+ const left = protocolCounts(baseline.get('summary.protocol_hierarchy'));
144
+ const right = protocolCounts(candidate.get('summary.protocol_hierarchy'));
145
+ return [...new Set([...left.keys(), ...right.keys()])].sort().map(protocol => ({
146
+ protocol, baseline: left.get(protocol) ?? 0, candidate: right.get(protocol) ?? 0,
147
+ delta: (right.get(protocol) ?? 0) - (left.get(protocol) ?? 0),
148
+ }));
149
+ }
150
+ function protocolCounts(value) {
151
+ const result = new Map();
152
+ if (typeof value !== 'string')
153
+ return result;
154
+ for (const entry of value.split(',')) {
155
+ const [protocol, count] = entry.split(':');
156
+ if (protocol && Number.isFinite(Number(count)))
157
+ result.set(protocol, Number(count));
158
+ }
159
+ return result;
160
+ }
161
+ function required(value, label) {
162
+ if (typeof value !== 'string' || !value.trim() || value.includes('\0'))
163
+ throw new Error(`${label} is required`);
164
+ return value;
165
+ }
166
+ function stringArray(value, label) {
167
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || !item.trim()))
168
+ throw new Error(`${label} must contain non-empty strings`);
169
+ return [...value];
170
+ }
171
+ //# sourceMappingURL=verification.js.map
@@ -5,6 +5,9 @@ import { createHash } from 'node:crypto';
5
5
  import { dirname, extname, join, resolve } from 'node:path';
6
6
  import { parse, stringify } from 'yaml';
7
7
  import { z } from 'zod';
8
+ import { getWritingChannelPack } from '../writing/channel-packs.js';
9
+ import { WriterProfileStore } from '../writing/writer-profile-store.js';
10
+ import { compileWriterProfile } from '../writing/writer-profile.js';
8
11
  import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
9
12
  const MODE_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*$/;
10
13
  const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers', 'machine-readable-blocks'];
@@ -41,6 +44,12 @@ const outputModeProfileSchema = z.object({
41
44
  }
42
45
  });
43
46
  const BUILTINS = [
47
+ ...['article', 'social', 'email', 'engineering', 'conversation'].map(channel => ({
48
+ id: `channel-${channel}`, version: '1.0.0', description: `Advisory ${channel} structure pack`,
49
+ kind: 'structure', stage: 'structure', order: 300,
50
+ instructions: getWritingChannelPack(channel).instructions, provenance: { source: 'AIWG channel packs', license: 'MIT' },
51
+ validation: { level: 'advisory' }, protectedContent: [...PROTECTED],
52
+ })),
44
53
  {
45
54
  id: 'unaltered', version: '1.0.0', description: 'No-op mode; preserves the provider output path unchanged.',
46
55
  kind: 'presentation', stage: 'presentation', order: -1000, instructions: '',
@@ -126,7 +135,18 @@ async function loadVoiceAdapters(frameworkRoot) {
126
135
  }
127
136
  return result;
128
137
  }
129
- export async function loadOutputModeRegistry(cwd, frameworkRoot) {
138
+ async function loadWriterAdapters(cwd, scope, context) {
139
+ const store = new WriterProfileStore({ cwd, scope });
140
+ const result = [];
141
+ for (const id of await store.list()) {
142
+ const compiled = compileWriterProfile(await store.read(id), context);
143
+ result.push({ ...validateOutputModeProfile(compiled.profile), source: scope, sourcePath: join(store.directory, `${id}.json`) });
144
+ }
145
+ return result;
146
+ }
147
+ export async function loadOutputModeRegistry(cwd, frameworkRoot, context = {}) {
148
+ if (context.task !== undefined && (typeof context.task !== 'string' || !context.task.trim() || context.task.length > 120))
149
+ throw new Error('Invalid writer task context');
130
150
  const registry = new Map();
131
151
  for (const profile of BUILTINS)
132
152
  registry.set(profile.id, { ...profile, source: 'builtin' });
@@ -134,9 +154,16 @@ export async function loadOutputModeRegistry(cwd, frameworkRoot) {
134
154
  if (!registry.has(profile.id))
135
155
  registry.set(profile.id, profile);
136
156
  // User overrides built-ins; project overrides user.
137
- for (const entry of [...profileDirs(cwd)].reverse())
138
- for (const profile of await loadDirectory(entry.dir, entry.source))
157
+ for (const entry of [...profileDirs(cwd)].reverse()) {
158
+ const profiles = await loadDirectory(entry.dir, entry.source);
159
+ const writers = await loadWriterAdapters(cwd, entry.source, context);
160
+ const ids = new Set(profiles.map(profile => profile.id));
161
+ for (const writer of writers)
162
+ if (ids.has(writer.id))
163
+ throw new Error(`Duplicate output mode '${writer.id}' from a writer sidecar and a mode file in the same scope.`);
164
+ for (const profile of [...profiles, ...writers])
139
165
  registry.set(profile.id, profile);
166
+ }
140
167
  return registry;
141
168
  }
142
169
  function statePath(cwd, scope) {
@@ -162,8 +189,8 @@ export async function writeOutputModeState(cwd, scope, modes) {
162
189
  await writeFile(path, stringify({ version: 1, modes }), 'utf8');
163
190
  return path;
164
191
  }
165
- export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], overrides = {}) {
166
- const registry = await loadOutputModeRegistry(cwd, frameworkRoot);
192
+ export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], overrides = {}, context = {}) {
193
+ const registry = await loadOutputModeRegistry(cwd, frameworkRoot, context);
167
194
  const project = overrides.project ?? (await readOutputModeState(cwd, 'project')).modes;
168
195
  const session = overrides.session ?? (await readOutputModeState(cwd, 'session')).modes;
169
196
  for (const [scope, modes] of [['project', project], ['session', session], ['invocation', invocation]]) {
@@ -188,7 +215,11 @@ export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], ov
188
215
  throw new Error(`Output modes '${a.id}' and '${b.id}' conflict. Disable one or configure an explicit merge strategy.`);
189
216
  if (a.kind === b.kind && a.kind !== 'voice')
190
217
  throw new Error(`Output modes '${a.id}' and '${b.id}' share kind '${a.kind}' without a merge strategy.`);
191
- if (a.kind === 'voice' && b.kind === 'voice' && a.mergeStrategy !== 'weighted-voice' && b.mergeStrategy !== 'weighted-voice')
218
+ const writerSelected = a.id.startsWith('writer-') || b.id.startsWith('writer-');
219
+ const missingMerge = writerSelected
220
+ ? a.mergeStrategy !== 'weighted-voice' || b.mergeStrategy !== 'weighted-voice'
221
+ : a.mergeStrategy !== 'weighted-voice' && b.mergeStrategy !== 'weighted-voice';
222
+ if (a.kind === 'voice' && b.kind === 'voice' && missingMerge)
192
223
  throw new Error(`Voice modes '${a.id}' and '${b.id}' require an explicit weighted-voice merge strategy.`);
193
224
  }
194
225
  for (const mode of modes)