@open-agent-toolkit/cli 0.2.51 → 0.2.53
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/bootstrap.md +6 -2
- package/assets/docs/cli-utilities/tool-packs.md +87 -33
- package/assets/docs/provider-sync/commands.md +30 -2
- package/assets/docs/provider-sync/config.md +19 -0
- package/assets/docs/provider-sync/manifest-and-drift.md +37 -0
- package/assets/docs/provider-sync/providers.md +69 -2
- package/assets/docs/provider-sync/scope-and-surface.md +44 -0
- package/assets/docs/reference/cli-reference.md +1 -0
- package/assets/docs/reference/oat-directory-structure.md +11 -4
- package/assets/docs/workflows/projects/evidence-layers.md +110 -1
- package/assets/docs/workflows/projects/implementation-execution.md +13 -0
- package/assets/docs/workflows/projects/orchestration-model.md +18 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +57 -1
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +77 -1
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +24 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-plan-writing/SKILL.md +12 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-review-provide/SKILL.md +12 -1
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +12 -1
- package/dist/agents/canonical/index.d.ts +2 -0
- package/dist/agents/canonical/index.d.ts.map +1 -1
- package/dist/agents/canonical/index.js +1 -0
- package/dist/agents/canonical/resolve.d.ts +45 -0
- package/dist/agents/canonical/resolve.d.ts.map +1 -0
- package/dist/agents/canonical/resolve.js +175 -0
- package/dist/commands/decision/agents-guidance.d.ts +2 -2
- package/dist/commands/decision/agents-guidance.d.ts.map +1 -1
- package/dist/commands/decision/agents-guidance.js +1 -4
- package/dist/commands/decision/index.d.ts.map +1 -1
- package/dist/commands/decision/index.js +25 -6
- package/dist/commands/docs/init/index.d.ts +7 -1
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +53 -20
- package/dist/commands/doctor/index.d.ts +8 -2
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +157 -46
- package/dist/commands/init/index.d.ts +8 -3
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +66 -28
- package/dist/commands/init/tools/index.d.ts +12 -12
- package/dist/commands/init/tools/index.d.ts.map +1 -1
- package/dist/commands/init/tools/index.js +430 -99
- package/dist/commands/init/tools/install-state.d.ts +2 -0
- package/dist/commands/init/tools/install-state.d.ts.map +1 -1
- package/dist/commands/init/tools/install-state.js +11 -0
- package/dist/commands/init/tools/project-guidance.d.ts +49 -0
- package/dist/commands/init/tools/project-guidance.d.ts.map +1 -0
- package/dist/commands/init/tools/project-guidance.js +144 -0
- package/dist/commands/init/tools/workflows/index.d.ts +7 -0
- package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
- package/dist/commands/init/tools/workflows/index.js +160 -7
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +28 -3
- package/dist/commands/pjm/init.d.ts +5 -0
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +19 -3
- package/dist/commands/project/dispatch/index.d.ts +10 -0
- package/dist/commands/project/dispatch/index.d.ts.map +1 -0
- package/dist/commands/project/dispatch/index.js +137 -0
- package/dist/commands/project/dispatch/record.d.ts +91 -0
- package/dist/commands/project/dispatch/record.d.ts.map +1 -0
- package/dist/commands/project/dispatch/record.js +402 -0
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/providers/inspect/inspect.d.ts +8 -1
- package/dist/commands/providers/inspect/inspect.d.ts.map +1 -1
- package/dist/commands/providers/inspect/inspect.js +146 -26
- package/dist/commands/providers/list/list.d.ts +8 -1
- package/dist/commands/providers/list/list.d.ts.map +1 -1
- package/dist/commands/providers/list/list.js +139 -23
- package/dist/commands/providers/providers.types.d.ts +25 -1
- package/dist/commands/providers/providers.types.d.ts.map +1 -1
- package/dist/commands/providers/set/index.d.ts +9 -1
- package/dist/commands/providers/set/index.d.ts.map +1 -1
- package/dist/commands/providers/set/index.js +43 -33
- package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
- package/dist/commands/remove/skill/remove-skill.js +11 -0
- package/dist/commands/shared/agents-md.d.ts +42 -14
- package/dist/commands/shared/agents-md.d.ts.map +1 -1
- package/dist/commands/shared/agents-md.js +364 -70
- package/dist/commands/status/index.d.ts +8 -3
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +149 -37
- package/dist/commands/sync/apply.d.ts.map +1 -1
- package/dist/commands/sync/apply.js +220 -29
- package/dist/commands/sync/dry-run.d.ts.map +1 -1
- package/dist/commands/sync/dry-run.js +30 -6
- package/dist/commands/sync/index.d.ts +17 -2
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +108 -43
- package/dist/commands/sync/sync.types.d.ts +39 -2
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/commands/sync/sync.utils.d.ts +5 -1
- package/dist/commands/sync/sync.utils.d.ts.map +1 -1
- package/dist/commands/sync/sync.utils.js +73 -0
- package/dist/commands/tools/info/info-tool.d.ts +2 -0
- package/dist/commands/tools/info/info-tool.d.ts.map +1 -1
- package/dist/commands/tools/info/info-tool.js +34 -12
- package/dist/commands/tools/install/index.d.ts.map +1 -1
- package/dist/commands/tools/install/index.js +19 -3
- package/dist/commands/tools/list/list-tools.d.ts +2 -0
- package/dist/commands/tools/list/list-tools.d.ts.map +1 -1
- package/dist/commands/tools/list/list-tools.js +36 -7
- package/dist/commands/tools/remove/index.d.ts.map +1 -1
- package/dist/commands/tools/remove/index.js +150 -21
- package/dist/commands/tools/remove/remove-tools.d.ts +20 -0
- package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
- package/dist/commands/tools/remove/remove-tools.js +122 -1
- package/dist/commands/tools/shared/format-pack-inventory.d.ts +19 -0
- package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/format-pack-inventory.js +109 -0
- package/dist/commands/tools/shared/pack-evidence.d.ts +66 -0
- package/dist/commands/tools/shared/pack-evidence.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-evidence.js +132 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts +1 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +44 -0
- package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-lifecycle-outcome.js +91 -0
- package/dist/commands/tools/update/index.d.ts.map +1 -1
- package/dist/commands/tools/update/index.js +74 -4
- package/dist/commands/tools/update/update-tools.d.ts +5 -0
- package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.js +64 -2
- package/dist/config/oat-config.d.ts +5 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +17 -5
- package/dist/config/user-sync-config.d.ts +1 -0
- package/dist/config/user-sync-config.d.ts.map +1 -1
- package/dist/config/user-sync-config.js +8 -0
- package/dist/drift/detector.d.ts +2 -2
- package/dist/drift/detector.d.ts.map +1 -1
- package/dist/drift/detector.js +18 -0
- package/dist/drift/strays.d.ts.map +1 -1
- package/dist/drift/strays.js +5 -0
- package/dist/engine/collection-sync.d.ts +14 -0
- package/dist/engine/collection-sync.d.ts.map +1 -0
- package/dist/engine/collection-sync.js +282 -0
- package/dist/engine/compute-plan.d.ts +8 -1
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +303 -12
- package/dist/engine/engine.types.d.ts +61 -0
- package/dist/engine/engine.types.d.ts.map +1 -1
- package/dist/engine/execute-plan.d.ts +22 -2
- package/dist/engine/execute-plan.d.ts.map +1 -1
- package/dist/engine/execute-plan.js +459 -27
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/provider-path-safety.d.ts +1 -0
- package/dist/engine/provider-path-safety.d.ts.map +1 -1
- package/dist/engine/provider-path-safety.js +15 -0
- package/dist/engine/scanner.d.ts +9 -2
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +58 -4
- package/dist/fs/io.d.ts +67 -0
- package/dist/fs/io.d.ts.map +1 -1
- package/dist/fs/io.js +536 -3
- package/dist/manifest/index.d.ts +2 -2
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/manager.d.ts +2 -2
- package/dist/manifest/manager.d.ts.map +1 -1
- package/dist/manifest/manager.js +20 -3
- package/dist/manifest/manifest.types.d.ts +704 -13
- package/dist/manifest/manifest.types.d.ts.map +1 -1
- package/dist/manifest/manifest.types.js +131 -15
- package/dist/providers/codex/codec/sync-extension.d.ts +1 -1
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +48 -17
- package/dist/providers/cursor/codec/sync-extension.d.ts +1 -1
- package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/cursor/codec/sync-extension.js +52 -23
- package/dist/providers/identity/absolute-paths.d.ts +68 -0
- package/dist/providers/identity/absolute-paths.d.ts.map +1 -0
- package/dist/providers/identity/absolute-paths.js +144 -0
- package/dist/providers/identity/claude-runtime-observation.d.ts +105 -0
- package/dist/providers/identity/claude-runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +40 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.js +146 -0
- package/dist/providers/identity/claude-runtime-observation.js +229 -0
- package/dist/providers/identity/codex-runtime-observation.d.ts +75 -0
- package/dist/providers/identity/codex-runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts +37 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts.map +1 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.js +539 -0
- package/dist/providers/identity/codex-runtime-observation.js +255 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts +313 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -0
- package/dist/providers/identity/generic-dispatch-record.js +539 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts +1964 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -0
- package/dist/providers/identity/oat-dispatch-record.js +691 -0
- package/dist/providers/identity/runtime-observation.d.ts +100 -0
- package/dist/providers/identity/runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/runtime-observation.js +169 -0
- package/dist/providers/shared/index.d.ts +2 -0
- package/dist/providers/shared/index.d.ts.map +1 -1
- package/dist/providers/shared/index.js +1 -0
- package/dist/providers/shared/materialization-extension.d.ts +12 -0
- package/dist/providers/shared/materialization-extension.d.ts.map +1 -1
- package/dist/providers/shared/registry.d.ts +69 -0
- package/dist/providers/shared/registry.d.ts.map +1 -0
- package/dist/providers/shared/registry.js +252 -0
- package/dist/providers/shared/restart-adviser.d.ts +26 -0
- package/dist/providers/shared/restart-adviser.d.ts.map +1 -0
- package/dist/providers/shared/restart-adviser.js +96 -0
- package/dist/shared/types.d.ts +4 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
4
|
+
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
|
+
import { fileExists } from '../../../fs/io.js';
|
|
6
|
+
import { resolveProjectRoot, validateRealPathWithinScope } from '../../../fs/paths.js';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { recordProjectDispatch, redactDispatchMessage, } from './record.js';
|
|
9
|
+
const DEFAULT_DEPENDENCIES = {
|
|
10
|
+
buildCommandContext,
|
|
11
|
+
resolveProjectRoot,
|
|
12
|
+
readFile: (path) => readFile(path, 'utf8'),
|
|
13
|
+
readStdin: async () => {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of process.stdin) {
|
|
16
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
17
|
+
}
|
|
18
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
async function resolveProjectPath(repoRoot, project) {
|
|
22
|
+
if (!project)
|
|
23
|
+
return null;
|
|
24
|
+
const candidate = isAbsolute(project) ? project : join(repoRoot, project);
|
|
25
|
+
const validated = await validateRealPathWithinScope(candidate, repoRoot);
|
|
26
|
+
if (!(await fileExists(join(validated.realPath, 'state.md')))) {
|
|
27
|
+
throw new Error('Project path must contain a readable state.md.');
|
|
28
|
+
}
|
|
29
|
+
return validated.realPath;
|
|
30
|
+
}
|
|
31
|
+
function axes(values) {
|
|
32
|
+
return values
|
|
33
|
+
.map(([name, value]) => `${name}=${value ?? 'not-reported'}`)
|
|
34
|
+
.join(' ');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render the two layers separately. Configured invocation is launcher-owned and
|
|
38
|
+
* immutable; runtime observation is optional per-run corroboration. They are
|
|
39
|
+
* never combined into one "effective" identity, because a mismatch must stay
|
|
40
|
+
* legible as a mismatch rather than silently overwrite either side.
|
|
41
|
+
*/
|
|
42
|
+
function runtimeIdentityLines(identity) {
|
|
43
|
+
const { configured, observed } = identity;
|
|
44
|
+
return [
|
|
45
|
+
`Configured invocation (immutable): ${axes([
|
|
46
|
+
['role', configured.roleName],
|
|
47
|
+
['role_selector', configured.roleSelector],
|
|
48
|
+
['model', configured.model],
|
|
49
|
+
['effort', configured.effort],
|
|
50
|
+
['service_tier', configured.serviceTier],
|
|
51
|
+
])}`,
|
|
52
|
+
observed === null
|
|
53
|
+
? `Observed runtime identity: not reported${identity.reason === null ? '' : ` (${identity.reason})`} (corroboration only; the configured invocation is unchanged).`
|
|
54
|
+
: `Observed runtime identity (${observed.source}, ${identity.match} on ${identity.comparedAxes.length === 0
|
|
55
|
+
? 'no comparable axis'
|
|
56
|
+
: identity.comparedAxes.join('+')}): ${axes([
|
|
57
|
+
['lineage', observed.childLineage],
|
|
58
|
+
['role', observed.role],
|
|
59
|
+
['model', observed.model],
|
|
60
|
+
['effort', observed.effort],
|
|
61
|
+
['service_tier', observed.serviceTier],
|
|
62
|
+
])}`,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
async function runRecordCommand(options, context, dependencies) {
|
|
66
|
+
let repoRoot = null;
|
|
67
|
+
let projectPath = null;
|
|
68
|
+
try {
|
|
69
|
+
repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
70
|
+
projectPath = await resolveProjectPath(repoRoot, options.project);
|
|
71
|
+
const content = options.eventFile === '-'
|
|
72
|
+
? await dependencies.readStdin()
|
|
73
|
+
: await dependencies.readFile(options.eventFile);
|
|
74
|
+
// Hand the raw event to the recorder; it owns the single authoritative
|
|
75
|
+
// parse, and parsing twice would relabel the provenance of its own output.
|
|
76
|
+
const raw = await recordProjectDispatch({
|
|
77
|
+
projectPath,
|
|
78
|
+
input: JSON.parse(content),
|
|
79
|
+
});
|
|
80
|
+
// The degradation reason is caller-influenced text on the success path, so
|
|
81
|
+
// it goes through the same single redaction boundary as every failure
|
|
82
|
+
// message. Producing a message that skips this boundary is exactly the
|
|
83
|
+
// per-producer regression this command was restructured to prevent.
|
|
84
|
+
const result = {
|
|
85
|
+
...raw,
|
|
86
|
+
runtimeIdentity: {
|
|
87
|
+
...raw.runtimeIdentity,
|
|
88
|
+
reason: raw.runtimeIdentity.reason === null
|
|
89
|
+
? null
|
|
90
|
+
: redactDispatchMessage(raw.runtimeIdentity.reason, {
|
|
91
|
+
project: projectPath,
|
|
92
|
+
repo: repoRoot,
|
|
93
|
+
home: context.home,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
if (context.json) {
|
|
98
|
+
context.logger.json(result);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (result.status === 'persisted') {
|
|
102
|
+
context.logger.success(`Recorded project dispatch: ${result.path}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
context.logger.info('Dispatch evidence is valid; no project path was supplied, so nothing was persisted.');
|
|
106
|
+
}
|
|
107
|
+
for (const line of runtimeIdentityLines(result.runtimeIdentity)) {
|
|
108
|
+
context.logger.info(line);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
process.exitCode = 0;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Single redaction boundary for every failure this command can surface.
|
|
115
|
+
const message = redactDispatchMessage(error instanceof Error ? error.message : String(error), { project: projectPath, repo: repoRoot, home: context.home });
|
|
116
|
+
if (context.json) {
|
|
117
|
+
context.logger.json({ status: 'error', message });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
context.logger.error(message);
|
|
121
|
+
}
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function createProjectDispatchCommand(overrides = {}) {
|
|
126
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
127
|
+
return new Command('dispatch')
|
|
128
|
+
.description('Validate and persist project dispatch provenance')
|
|
129
|
+
.addCommand(new Command('record')
|
|
130
|
+
.description('Record one generic dispatch plus namespaced OAT evidence')
|
|
131
|
+
.option('--project <project-path>', 'Project path; omit to validate without persistence')
|
|
132
|
+
.requiredOption('--event-file <json-file-or-dash>', 'Complete record and event JSON file, or - for standard input')
|
|
133
|
+
.action(async (options, command) => {
|
|
134
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
135
|
+
await runRecordCommand(options, context, dependencies);
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type GenericDispatchRecord } from '../../../providers/identity/generic-dispatch-record.js';
|
|
2
|
+
import { type OatDispatchEvidenceEvent, type PersistedOatDispatchRecordV1 } from '../../../providers/identity/oat-dispatch-record.js';
|
|
3
|
+
/**
|
|
4
|
+
* Test seam for deterministic concurrency coverage. `afterRead` fires after the
|
|
5
|
+
* caller has read the journal revision it will publish against and before the
|
|
6
|
+
* writer lock is acquired, which is exactly the window a stale concurrent
|
|
7
|
+
* writer occupies.
|
|
8
|
+
*/
|
|
9
|
+
export interface DispatchRecordRaceBarriers {
|
|
10
|
+
afterRead?: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hard redaction boundary. Producers redact their own messages, but this is the
|
|
14
|
+
* last stop before a message reaches `--json`, so a future call site that
|
|
15
|
+
* forgets cannot regress NFR1. Known roots become stable labels; anything else
|
|
16
|
+
* that still looks like an absolute path is scrubbed.
|
|
17
|
+
*
|
|
18
|
+
* The detector is shared with the record sanitizer. Two independent notions of
|
|
19
|
+
* "looks like a path" is how a path could be scrubbed from an error message and
|
|
20
|
+
* written verbatim into the durable record by the same command.
|
|
21
|
+
*/
|
|
22
|
+
export declare function redactDispatchMessage(message: string, roots?: {
|
|
23
|
+
project?: string | null;
|
|
24
|
+
repo?: string | null;
|
|
25
|
+
home?: string | null;
|
|
26
|
+
}): string;
|
|
27
|
+
export interface DispatchRecordInput {
|
|
28
|
+
record: GenericDispatchRecord;
|
|
29
|
+
event: OatDispatchEvidenceEvent;
|
|
30
|
+
/** Why an observation degraded, when one did. Not durable evidence. */
|
|
31
|
+
observationReason?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Configured and observed identity, reported side by side and never merged.
|
|
35
|
+
* `configured` is the launcher-owned immutable selection; `observed` is
|
|
36
|
+
* optional per-run corroboration that is `null` whenever the provider reported
|
|
37
|
+
* nothing. A `match` of `mismatching` is evidence, not authorization.
|
|
38
|
+
*/
|
|
39
|
+
export interface DispatchRecordRuntimeIdentity {
|
|
40
|
+
configured: {
|
|
41
|
+
roleName: string;
|
|
42
|
+
roleSelector: string | null;
|
|
43
|
+
model: string | null;
|
|
44
|
+
effort: string | null;
|
|
45
|
+
serviceTier: string | null;
|
|
46
|
+
};
|
|
47
|
+
observed: {
|
|
48
|
+
provider: string;
|
|
49
|
+
source: string;
|
|
50
|
+
observedAt: string;
|
|
51
|
+
childLineage: string | null;
|
|
52
|
+
role: string | null;
|
|
53
|
+
model: string | null;
|
|
54
|
+
effort: string | null;
|
|
55
|
+
serviceTier: string | null;
|
|
56
|
+
} | null;
|
|
57
|
+
match: 'matching' | 'mismatching' | 'not-comparable' | null;
|
|
58
|
+
/**
|
|
59
|
+
* The axes `match` rests on. A `matching` verdict says nothing about an axis
|
|
60
|
+
* absent from this list.
|
|
61
|
+
*/
|
|
62
|
+
comparedAxes: readonly string[];
|
|
63
|
+
/**
|
|
64
|
+
* Why an observation is `not-reported`, so a caller attaching a 25 MB rollout
|
|
65
|
+
* can tell that outcome from one attaching an empty array. A per-command
|
|
66
|
+
* diagnostic, never durable evidence.
|
|
67
|
+
*/
|
|
68
|
+
reason: string | null;
|
|
69
|
+
status: 'reported' | 'not-reported';
|
|
70
|
+
}
|
|
71
|
+
export type ProjectDispatchRecordResult = {
|
|
72
|
+
status: 'persisted' | 'validated-only';
|
|
73
|
+
path: string | null;
|
|
74
|
+
created: boolean;
|
|
75
|
+
record: PersistedOatDispatchRecordV1;
|
|
76
|
+
runtimeIdentity: DispatchRecordRuntimeIdentity;
|
|
77
|
+
};
|
|
78
|
+
export declare function parseDispatchRecordInput(value: unknown): DispatchRecordInput;
|
|
79
|
+
/**
|
|
80
|
+
* Parse once, here. An earlier revision parsed in the command layer and again
|
|
81
|
+
* here; because the metadata path resolves to a finished observation, the second
|
|
82
|
+
* parse saw its own output as caller-supplied and overwrote the parser source
|
|
83
|
+
* with `caller-asserted`. Parsing is not idempotent by design — provenance
|
|
84
|
+
* depends on which input form arrived — so it must happen exactly once.
|
|
85
|
+
*/
|
|
86
|
+
export declare function recordProjectDispatch(input: {
|
|
87
|
+
projectPath: string | null;
|
|
88
|
+
input: unknown;
|
|
89
|
+
raceBarriers?: DispatchRecordRaceBarriers;
|
|
90
|
+
}): Promise<ProjectDispatchRecordResult>;
|
|
91
|
+
//# sourceMappingURL=record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/record.ts"],"names":[],"mappings":"AAcA,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAOL,KAAK,wBAAwB,EAE7B,KAAK,4BAA4B,EAElC,MAAM,yCAAyC,CAAC;AAWjD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAoCD;;;;;;;;;GASG;AAEH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,GAAE;IACL,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjB,GACL,MAAM,CAcR;AAaD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,KAAK,EAAE,wBAAwB,CAAC;IAChC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;IACF,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,GAAG,IAAI,CAAC;IACT,KAAK,EAAE,UAAU,GAAG,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC5D;;;OAGG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;CACrC;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAAC;IACvC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,4BAA4B,CAAC;IACrC,eAAe,EAAE,6BAA6B,CAAC;CAChD,CAAC;AAsEF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAgF5E;AAgKD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,0BAA0B,CAAC;CAC3C,GAAG,OAAO,CAAC,2BAA2B,CAAC,CA6HvC"}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { publishContainedJsonRevision, redactedFsError, withContainedWriterLock, } from '../../../fs/io.js';
|
|
4
|
+
import { assertJournalIdentityHasNoAbsolutePath, redactAbsolutePaths, } from '../../../providers/identity/absolute-paths.js';
|
|
5
|
+
import { CLAUDE_OBSERVATION_SOURCE } from '../../../providers/identity/claude-runtime-observation.js';
|
|
6
|
+
import { CODEX_OBSERVATION_SOURCE } from '../../../providers/identity/codex-runtime-observation.js';
|
|
7
|
+
import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, } from '../../../providers/identity/generic-dispatch-record.js';
|
|
8
|
+
import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, } from '../../../providers/identity/oat-dispatch-record.js';
|
|
9
|
+
import { observationFromFacts, parseRuntimeObservationEnvelope, projectRuntimeObservation, providerSupportsRuntimeObservation, } from '../../../providers/identity/runtime-observation.js';
|
|
10
|
+
const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
11
|
+
const DISPATCH_LOCK_NAME = '.dispatch-lock';
|
|
12
|
+
/**
|
|
13
|
+
* The journal is append-only. Revision 1 keeps the plain `<request-id>.json`
|
|
14
|
+
* name; every later revision is published create-only under
|
|
15
|
+
* `<request-id>@<NNNN>.json`. Publishing revision N+1 exclusively is the
|
|
16
|
+
* compare-and-swap: a caller that lost the race observes `EEXIST` instead of
|
|
17
|
+
* replacing the winner, and no publication can ever clobber an existing file.
|
|
18
|
+
* `@` is outside the request-ID character class, so the split is unambiguous.
|
|
19
|
+
*/
|
|
20
|
+
const REVISION_SEPARATOR = '@';
|
|
21
|
+
const REVISION_SUFFIX_PATTERN = /^(.*)@(\d{4})$/;
|
|
22
|
+
const MAX_REVISION = 9999;
|
|
23
|
+
function revisionFileName(requestId, revision) {
|
|
24
|
+
return revision === 1
|
|
25
|
+
? `${requestId}.json`
|
|
26
|
+
: `${requestId}${REVISION_SEPARATOR}${String(revision).padStart(4, '0')}.json`;
|
|
27
|
+
}
|
|
28
|
+
function parseJournalEntry(fileName) {
|
|
29
|
+
if (!fileName.endsWith('.json'))
|
|
30
|
+
return null;
|
|
31
|
+
const stem = fileName.slice(0, -'.json'.length);
|
|
32
|
+
const suffixed = REVISION_SUFFIX_PATTERN.exec(stem);
|
|
33
|
+
const requestId = suffixed?.[1] ?? stem;
|
|
34
|
+
const revision = suffixed ? Number(suffixed[2]) : 1;
|
|
35
|
+
if (!REQUEST_ID_PATTERN.test(requestId) || revision < 1)
|
|
36
|
+
return null;
|
|
37
|
+
return { requestId, revision, fileName };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hard redaction boundary. Producers redact their own messages, but this is the
|
|
41
|
+
* last stop before a message reaches `--json`, so a future call site that
|
|
42
|
+
* forgets cannot regress NFR1. Known roots become stable labels; anything else
|
|
43
|
+
* that still looks like an absolute path is scrubbed.
|
|
44
|
+
*
|
|
45
|
+
* The detector is shared with the record sanitizer. Two independent notions of
|
|
46
|
+
* "looks like a path" is how a path could be scrubbed from an error message and
|
|
47
|
+
* written verbatim into the durable record by the same command.
|
|
48
|
+
*/
|
|
49
|
+
export function redactDispatchMessage(message, roots = {}) {
|
|
50
|
+
let redacted = message;
|
|
51
|
+
const labelled = [
|
|
52
|
+
['<project>', roots.project],
|
|
53
|
+
['<repo>', roots.repo],
|
|
54
|
+
['<home>', roots.home],
|
|
55
|
+
];
|
|
56
|
+
// Longest root first so a nested project path is not masked by its repo.
|
|
57
|
+
for (const [label, root] of [...labelled].sort(([, left], [, right]) => (right?.length ?? 0) - (left?.length ?? 0))) {
|
|
58
|
+
if (root)
|
|
59
|
+
redacted = redacted.split(root).join(label);
|
|
60
|
+
}
|
|
61
|
+
return redactAbsolutePaths(redacted);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Source strings that state provenance. A parser source is only ever written by
|
|
65
|
+
* the projection path that actually parsed provider output; a caller-supplied
|
|
66
|
+
* observation is always recorded as caller-asserted.
|
|
67
|
+
*/
|
|
68
|
+
const OBSERVATION_SOURCE_BY_PROVIDER = {
|
|
69
|
+
codex: CODEX_OBSERVATION_SOURCE,
|
|
70
|
+
claude: CLAUDE_OBSERVATION_SOURCE,
|
|
71
|
+
};
|
|
72
|
+
const CALLER_ASSERTED_OBSERVATION_SOURCE = 'caller-asserted';
|
|
73
|
+
function isRecord(value) {
|
|
74
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a post-launch observation event.
|
|
78
|
+
*
|
|
79
|
+
* A caller may submit either a finished `observation` or the sanitized provider
|
|
80
|
+
* `metadata` it was derived from. Submitting both is ambiguous and is refused
|
|
81
|
+
* rather than silently preferring one.
|
|
82
|
+
*
|
|
83
|
+
* Both forms are validated here, at the durable-write boundary, rather than
|
|
84
|
+
* being trusted from the producer. An earlier revision normalized only the
|
|
85
|
+
* metadata form, which left the finished-observation form able to assert its
|
|
86
|
+
* own `match`, name a provider the record does not use, and store values the
|
|
87
|
+
* metadata path would have refused. That is the per-producer-obligation class
|
|
88
|
+
* this boundary exists to prevent, so `match` is always derived and never read
|
|
89
|
+
* from the caller, and the provider is always bound to the record's own.
|
|
90
|
+
*
|
|
91
|
+
* Value shape is enforced once more in `runtimeObservationSchema`, so both
|
|
92
|
+
* forms converge on the same identifier rules without either path carrying its
|
|
93
|
+
* own copy of them.
|
|
94
|
+
*/
|
|
95
|
+
function resolveObservationEvent(record, event) {
|
|
96
|
+
if (event.kind !== 'runtime-observation')
|
|
97
|
+
return event;
|
|
98
|
+
if ('metadata' in event && 'observation' in event) {
|
|
99
|
+
throw new Error('A runtime observation event carries either observation or metadata, not both.');
|
|
100
|
+
}
|
|
101
|
+
if ('metadata' in event)
|
|
102
|
+
return event;
|
|
103
|
+
const observation = event.observation;
|
|
104
|
+
if (!isRecord(observation) || observation.status !== 'reported') {
|
|
105
|
+
return event;
|
|
106
|
+
}
|
|
107
|
+
if (observation.provider !== record.provider) {
|
|
108
|
+
throw new Error('A runtime observation must name the same provider as its dispatch record.');
|
|
109
|
+
}
|
|
110
|
+
if (!providerSupportsRuntimeObservation(record.provider)) {
|
|
111
|
+
throw new Error(`Provider ${record.provider} has no runtime observation capability, so it cannot report an observation.`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
...event,
|
|
115
|
+
observation: parseRuntimeObservation({
|
|
116
|
+
...observation,
|
|
117
|
+
// Provenance is derived, never accepted: a hand-authored observation
|
|
118
|
+
// cannot borrow a parser's source string and pass as a genuine parse.
|
|
119
|
+
source: CALLER_ASSERTED_OBSERVATION_SOURCE,
|
|
120
|
+
match: compareObservedRuntimeMetadata(observation, configuredInvocationForObservation(record)),
|
|
121
|
+
comparedAxes: comparedObservationAxes(observation, configuredInvocationForObservation(record)),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function parseDispatchRecordInput(value) {
|
|
126
|
+
if (!isRecord(value)) {
|
|
127
|
+
throw new Error('Dispatch record input must be a JSON object.');
|
|
128
|
+
}
|
|
129
|
+
const keys = Object.keys(value).sort();
|
|
130
|
+
if (keys.length !== 2 || keys[0] !== 'event' || keys[1] !== 'record') {
|
|
131
|
+
throw new Error('Dispatch record input accepts only record and event.');
|
|
132
|
+
}
|
|
133
|
+
if (!isRecord(value.event)) {
|
|
134
|
+
throw new Error('Dispatch record event must be a JSON object.');
|
|
135
|
+
}
|
|
136
|
+
// Raw provider metadata is the one input a caller may legitimately supply
|
|
137
|
+
// unmodified, so it is projected through the owning parser's allowlist and
|
|
138
|
+
// asserted as the projection. Everything else is asserted exactly as given.
|
|
139
|
+
// Asserting the raw entries instead would refuse real rollouts outright —
|
|
140
|
+
// a Codex `session_id` alone classifies as sensitive — which is what pushed
|
|
141
|
+
// sanitization onto callers and invited a hand-rolled stripper.
|
|
142
|
+
const rawMetadata = isRecord(value.event.metadata)
|
|
143
|
+
? value.event.metadata
|
|
144
|
+
: null;
|
|
145
|
+
assertNoSensitiveDispatchContent(rawMetadata === null
|
|
146
|
+
? value
|
|
147
|
+
: {
|
|
148
|
+
...value,
|
|
149
|
+
event: {
|
|
150
|
+
...value.event,
|
|
151
|
+
metadata: { ...rawMetadata, entries: null },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const record = parseGenericDispatchRecord(value.record);
|
|
155
|
+
if (rawMetadata !== null) {
|
|
156
|
+
if ('observation' in value.event) {
|
|
157
|
+
throw new Error('A runtime observation event carries either observation or metadata, not both.');
|
|
158
|
+
}
|
|
159
|
+
const envelope = parseRuntimeObservationEnvelope(rawMetadata);
|
|
160
|
+
const projected = envelope.provider === record.provider
|
|
161
|
+
? projectRuntimeObservation(envelope.provider, envelope.entries)
|
|
162
|
+
: {
|
|
163
|
+
facts: null,
|
|
164
|
+
reason: `observation metadata names provider ${envelope.provider}, but the record names ${record.provider}`,
|
|
165
|
+
};
|
|
166
|
+
const facts = projected.facts;
|
|
167
|
+
assertNoSensitiveDispatchContent(facts, '<observation-metadata>');
|
|
168
|
+
const { metadata: _metadata, ...rest } = value.event;
|
|
169
|
+
// Built here from parsed provider output, so it carries a parser source and
|
|
170
|
+
// must not be re-processed as if a caller had supplied it.
|
|
171
|
+
const observation = observationFromFacts({
|
|
172
|
+
record,
|
|
173
|
+
provider: envelope.provider,
|
|
174
|
+
source: OBSERVATION_SOURCE_BY_PROVIDER[envelope.provider] ?? 'unknown',
|
|
175
|
+
observedAt: envelope.observedAt,
|
|
176
|
+
facts,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
record,
|
|
180
|
+
event: { ...rest, observation },
|
|
181
|
+
observationReason: observation.status === 'reported'
|
|
182
|
+
? null
|
|
183
|
+
: // Facts can be produced and still be declined downstream, when the
|
|
184
|
+
// session declares a different request than this record does.
|
|
185
|
+
(projected.reason ??
|
|
186
|
+
'observation metadata correlates to a different request'),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
record,
|
|
191
|
+
event: resolveObservationEvent(record, value.event),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function runtimeIdentityFor(record, reason = null) {
|
|
195
|
+
const observation = record.oat.runtimeObservation;
|
|
196
|
+
return {
|
|
197
|
+
configured: {
|
|
198
|
+
roleName: record.role_name,
|
|
199
|
+
roleSelector: record.role_selector,
|
|
200
|
+
model: record.model_selector,
|
|
201
|
+
effort: record.effort_selector,
|
|
202
|
+
serviceTier: record.service_tier_selector ?? null,
|
|
203
|
+
},
|
|
204
|
+
observed: observation.status === 'reported'
|
|
205
|
+
? {
|
|
206
|
+
provider: observation.provider,
|
|
207
|
+
source: observation.source,
|
|
208
|
+
observedAt: observation.observedAt,
|
|
209
|
+
childLineage: observation.childLineage ?? null,
|
|
210
|
+
role: observation.role ?? null,
|
|
211
|
+
model: observation.model ?? null,
|
|
212
|
+
effort: observation.effort ?? null,
|
|
213
|
+
serviceTier: observation.serviceTier ?? null,
|
|
214
|
+
}
|
|
215
|
+
: null,
|
|
216
|
+
match: observation.status === 'reported' ? observation.match : null,
|
|
217
|
+
comparedAxes: observation.status === 'reported' ? (observation.comparedAxes ?? []) : [],
|
|
218
|
+
reason: observation.status === 'reported' ? null : reason,
|
|
219
|
+
status: observation.status,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function genericPart(record) {
|
|
223
|
+
const { oat: _oat, ...generic } = record;
|
|
224
|
+
return parseGenericDispatchRecord(generic);
|
|
225
|
+
}
|
|
226
|
+
function sameGenericRecord(left, right) {
|
|
227
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
228
|
+
}
|
|
229
|
+
function isMissingError(error) {
|
|
230
|
+
return (typeof error === 'object' &&
|
|
231
|
+
error !== null &&
|
|
232
|
+
'code' in error &&
|
|
233
|
+
error.code === 'ENOENT');
|
|
234
|
+
}
|
|
235
|
+
async function readPersistedRecord(path, reportedPath) {
|
|
236
|
+
let content;
|
|
237
|
+
try {
|
|
238
|
+
content = await readFile(path, 'utf8');
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (isMissingError(error)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
// A damaged or unreadable journal must not put an absolute project path
|
|
245
|
+
// into durable output; NFR1 treats that as a P0 disclosure.
|
|
246
|
+
throw redactedFsError(error, 'read', reportedPath);
|
|
247
|
+
}
|
|
248
|
+
return parsePersistedOatDispatchRecord(JSON.parse(content));
|
|
249
|
+
}
|
|
250
|
+
/** Highest published revision per request ID. */
|
|
251
|
+
async function readLatestRevisions(dispatchDir) {
|
|
252
|
+
let names;
|
|
253
|
+
try {
|
|
254
|
+
names = await readdir(dispatchDir);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (isMissingError(error)) {
|
|
258
|
+
return new Map();
|
|
259
|
+
}
|
|
260
|
+
throw redactedFsError(error, 'scan', 'dispatch/');
|
|
261
|
+
}
|
|
262
|
+
const latest = new Map();
|
|
263
|
+
for (const name of names.sort()) {
|
|
264
|
+
const entry = parseJournalEntry(name);
|
|
265
|
+
if (!entry)
|
|
266
|
+
continue;
|
|
267
|
+
const known = latest.get(entry.requestId);
|
|
268
|
+
if (!known || entry.revision > known.revision) {
|
|
269
|
+
latest.set(entry.requestId, entry);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return latest;
|
|
273
|
+
}
|
|
274
|
+
function fallbackTriggerRequestId(event) {
|
|
275
|
+
if (event.kind !== 'fallback-link')
|
|
276
|
+
return null;
|
|
277
|
+
const requestId = event.evidence?.triggerRequestId;
|
|
278
|
+
if (typeof requestId !== 'string' || !REQUEST_ID_PATTERN.test(requestId)) {
|
|
279
|
+
throw new Error('Fallback trigger request ID is invalid.');
|
|
280
|
+
}
|
|
281
|
+
return requestId;
|
|
282
|
+
}
|
|
283
|
+
function isAlreadyPublished(error) {
|
|
284
|
+
return (typeof error === 'object' &&
|
|
285
|
+
error !== null &&
|
|
286
|
+
'code' in error &&
|
|
287
|
+
error.code === 'EEXIST');
|
|
288
|
+
}
|
|
289
|
+
async function publishRevision(projectPath, dispatchDir, requestId, revision, record) {
|
|
290
|
+
if (revision > MAX_REVISION) {
|
|
291
|
+
throw new Error(`Dispatch journal for ${requestId} reached its ${MAX_REVISION}-revision limit.`);
|
|
292
|
+
}
|
|
293
|
+
const fileName = revisionFileName(requestId, revision);
|
|
294
|
+
// Last check before bytes reach the journal. The sanitizer runs at parse, but
|
|
295
|
+
// the journal is committed to a shared repository, so the guarantee is
|
|
296
|
+
// enforced on the exact value being written rather than trusted upstream.
|
|
297
|
+
// Guarantees identity and control fields only; prose redaction is
|
|
298
|
+
// best-effort by design. See absolute-paths.ts.
|
|
299
|
+
assertJournalIdentityHasNoAbsolutePath(identityFieldsOf(genericPart(record)));
|
|
300
|
+
assertBoundedDispatchRecordSize(record);
|
|
301
|
+
try {
|
|
302
|
+
await publishContainedJsonRevision(join(dispatchDir, fileName), record, projectPath);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (isAlreadyPublished(error)) {
|
|
306
|
+
throw new Error('The dispatch journal advanced to a newer revision while this write was prepared; the concurrent update was preserved and this stale write was refused.', { cause: error });
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
return `dispatch/${fileName}`;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse once, here. An earlier revision parsed in the command layer and again
|
|
314
|
+
* here; because the metadata path resolves to a finished observation, the second
|
|
315
|
+
* parse saw its own output as caller-supplied and overwrote the parser source
|
|
316
|
+
* with `caller-asserted`. Parsing is not idempotent by design — provenance
|
|
317
|
+
* depends on which input form arrived — so it must happen exactly once.
|
|
318
|
+
*/
|
|
319
|
+
export async function recordProjectDispatch(input) {
|
|
320
|
+
const parsedInput = parseDispatchRecordInput(input.input);
|
|
321
|
+
if (input.projectPath === null) {
|
|
322
|
+
const record = augmentDispatchRecord(parsedInput);
|
|
323
|
+
return {
|
|
324
|
+
status: 'validated-only',
|
|
325
|
+
path: null,
|
|
326
|
+
created: false,
|
|
327
|
+
record,
|
|
328
|
+
runtimeIdentity: runtimeIdentityFor(record, parsedInput.observationReason ?? null),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const projectPath = input.projectPath;
|
|
332
|
+
const dispatchDir = join(projectPath, 'dispatch');
|
|
333
|
+
const requestId = parsedInput.record.request_id;
|
|
334
|
+
const latest = await readLatestRevisions(dispatchDir);
|
|
335
|
+
const existingEntry = latest.get(requestId) ?? null;
|
|
336
|
+
const existing = existingEntry
|
|
337
|
+
? await readPersistedRecord(join(dispatchDir, existingEntry.fileName), `dispatch/${existingEntry.fileName}`)
|
|
338
|
+
: null;
|
|
339
|
+
if (existing &&
|
|
340
|
+
!sameGenericRecord(genericPart(existing), parsedInput.record)) {
|
|
341
|
+
throw new Error('Existing generic fields are immutable and cannot be redefined.');
|
|
342
|
+
}
|
|
343
|
+
const relatedRecords = [];
|
|
344
|
+
for (const [otherId, entry] of [...latest].sort(([left], [right]) => left < right ? -1 : 1)) {
|
|
345
|
+
if (otherId === requestId)
|
|
346
|
+
continue;
|
|
347
|
+
const related = await readPersistedRecord(join(dispatchDir, entry.fileName), `dispatch/${entry.fileName}`);
|
|
348
|
+
if (related)
|
|
349
|
+
relatedRecords.push(related);
|
|
350
|
+
}
|
|
351
|
+
const triggerRequestId = fallbackTriggerRequestId(parsedInput.event);
|
|
352
|
+
const triggerEntry = triggerRequestId
|
|
353
|
+
? (latest.get(triggerRequestId) ?? null)
|
|
354
|
+
: null;
|
|
355
|
+
const trigger = triggerEntry
|
|
356
|
+
? await readPersistedRecord(join(dispatchDir, triggerEntry.fileName), `dispatch/${triggerEntry.fileName}`)
|
|
357
|
+
: null;
|
|
358
|
+
if (triggerRequestId !== null && trigger === null) {
|
|
359
|
+
throw new Error('Fallback requires the rejected trigger record.');
|
|
360
|
+
}
|
|
361
|
+
await input.raceBarriers?.afterRead?.();
|
|
362
|
+
// One shared, identity-bound transition. The lock serializes the trigger
|
|
363
|
+
// claim and the fallback publication; the exclusive revision name is the
|
|
364
|
+
// compare-and-swap, so a stale concurrent writer loses without any write
|
|
365
|
+
// being able to replace the winner's evidence.
|
|
366
|
+
return withContainedWriterLock(join(projectPath, DISPATCH_LOCK_NAME), projectPath, async () => {
|
|
367
|
+
let triggerRecord = trigger ?? undefined;
|
|
368
|
+
if (trigger && triggerEntry && triggerRequestId) {
|
|
369
|
+
const claimed = augmentDispatchRecord({
|
|
370
|
+
record: trigger,
|
|
371
|
+
event: {
|
|
372
|
+
kind: 'fallback-claim',
|
|
373
|
+
requestId: triggerRequestId,
|
|
374
|
+
source: 'provider-wrapper',
|
|
375
|
+
claim: {
|
|
376
|
+
fallbackRequestId: requestId,
|
|
377
|
+
claimedAt: new Date().toISOString(),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
if (JSON.stringify(claimed) !== JSON.stringify(trigger)) {
|
|
382
|
+
await publishRevision(projectPath, dispatchDir, triggerRequestId, triggerEntry.revision + 1, claimed);
|
|
383
|
+
}
|
|
384
|
+
triggerRecord = claimed;
|
|
385
|
+
}
|
|
386
|
+
const record = augmentDispatchRecord({
|
|
387
|
+
record: existing ?? parsedInput.record,
|
|
388
|
+
event: parsedInput.event,
|
|
389
|
+
...(triggerRecord ? { triggerRecord } : {}),
|
|
390
|
+
relatedRecords,
|
|
391
|
+
});
|
|
392
|
+
const revision = (existingEntry?.revision ?? 0) + 1;
|
|
393
|
+
const path = await publishRevision(projectPath, dispatchDir, requestId, revision, record);
|
|
394
|
+
return {
|
|
395
|
+
status: 'persisted',
|
|
396
|
+
path,
|
|
397
|
+
created: revision === 1,
|
|
398
|
+
record,
|
|
399
|
+
runtimeIdentity: runtimeIdentityFor(record, parsedInput.observationReason ?? null),
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBpC,wBAAgB,oBAAoB,IAAI,OAAO,CAuB9C"}
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { createProjectArchiveCommand } from './archive/index.js';
|
|
3
3
|
import { createProjectCompleteDiscoveryCommand } from './complete-discovery/index.js';
|
|
4
4
|
import { createProjectCompleteStateCommand } from './complete-state/index.js';
|
|
5
|
+
import { createProjectDispatchCommand } from './dispatch/index.js';
|
|
5
6
|
import { createProjectDispatchCeilingCommand } from './dispatch-ceiling/index.js';
|
|
6
7
|
import { createProjectLinksCommand } from './links/index.js';
|
|
7
8
|
import { createProjectListCommand } from './list.js';
|
|
@@ -24,6 +25,7 @@ export function createProjectCommand() {
|
|
|
24
25
|
.addCommand(createProjectArchiveCommand())
|
|
25
26
|
.addCommand(createProjectCompleteDiscoveryCommand())
|
|
26
27
|
.addCommand(createProjectCompleteStateCommand())
|
|
28
|
+
.addCommand(createProjectDispatchCommand())
|
|
27
29
|
.addCommand(createProjectDispatchCeilingCommand())
|
|
28
30
|
.addCommand(createProjectListCommand())
|
|
29
31
|
.addCommand(createProjectLinksCommand())
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { ProvidersInspectDependencies } from '../../providers/providers.types.js';
|
|
2
|
+
import { loadSyncConfig } from '../../../config/index.js';
|
|
3
|
+
import { resolveProviderScopeContext } from '../../../providers/shared/index.js';
|
|
2
4
|
import { Command } from 'commander';
|
|
3
|
-
|
|
5
|
+
type ContextualInspectDependencies = ProvidersInspectDependencies & {
|
|
6
|
+
loadSyncConfig?: typeof loadSyncConfig;
|
|
7
|
+
resolveProviderScopeContext?: typeof resolveProviderScopeContext;
|
|
8
|
+
};
|
|
9
|
+
export declare function createProvidersInspectCommand(overrides?: Partial<ContextualInspectDependencies>): Command;
|
|
10
|
+
export {};
|
|
4
11
|
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../../../src/commands/providers/inspect/inspect.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../../../src/commands/providers/inspect/inspect.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,4BAA4B,EAC7B,MAAM,qCAAqC,CAAC;AAM7C,OAAO,EAAuB,cAAc,EAAE,MAAM,eAAe,CAAC;AAQpE,OAAO,EAGL,2BAA2B,EAG5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,KAAK,6BAA6B,GAAG,4BAA4B,GAAG;IAClE,cAAc,CAAC,EAAE,OAAO,cAAc,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,2BAA2B,CAAC;CAClE,CAAC;AAmWF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,6BAA6B,CAAM,GACrD,OAAO,CAuBT"}
|