@open-agent-toolkit/cli 0.3.6 → 0.3.8
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/configuration.md +38 -18
- package/assets/docs/cli-utilities/workflow-gates.md +37 -0
- package/assets/docs/contributing/updating-model-guidance.md +6 -6
- package/assets/docs/provider-sync/commands.md +6 -0
- package/assets/docs/provider-sync/providers.md +24 -0
- package/assets/docs/provider-sync/scope-and-surface.md +2 -0
- package/assets/docs/reference/cli-reference.md +2 -1
- package/assets/docs/workflows/projects/autonomy.md +1 -1
- package/assets/docs/workflows/projects/dispatch-ceiling.md +26 -20
- package/assets/docs/workflows/projects/evidence-layers.md +5 -1
- package/assets/docs/workflows/projects/retro.md +7 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-agent-instructions-analyze/SKILL.md +1 -1
- package/assets/skills/oat-agent-instructions-analyze/scripts/resolve-providers.sh +17 -6
- package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
- package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +387 -0
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +75 -0
- package/assets/skills/oat-doctor/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +24 -22
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-import-plan/SKILL.md +4 -4
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-plan/SKILL.md +4 -4
- package/assets/skills/oat-project-plan-writing/SKILL.md +4 -4
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-quick-start/SKILL.md +4 -4
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-retro/SKILL.md +70 -6
- package/assets/skills/oat-project-review-provide/SKILL.md +3 -1
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +3 -1
- package/dist/agents/canonical/resolve.d.ts +6 -4
- package/dist/agents/canonical/resolve.d.ts.map +1 -1
- package/dist/agents/canonical/resolve.js +6 -4
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +210 -28
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +51 -2
- package/dist/commands/gate/review-verdict.d.ts +22 -0
- package/dist/commands/gate/review-verdict.d.ts.map +1 -1
- package/dist/commands/gate/review-verdict.js +111 -0
- package/dist/commands/project/dispatch/canonical-role.d.ts +33 -0
- package/dist/commands/project/dispatch/canonical-role.d.ts.map +1 -0
- package/dist/commands/project/dispatch/canonical-role.js +114 -0
- package/dist/commands/project/dispatch/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch/index.js +3 -1
- package/dist/commands/project/dispatch/record.d.ts.map +1 -1
- package/dist/commands/project/dispatch/record.js +146 -4
- package/dist/commands/sync/apply.d.ts.map +1 -1
- package/dist/commands/sync/apply.js +28 -21
- package/dist/commands/tools/shared/sync-evidence.d.ts.map +1 -1
- package/dist/commands/tools/shared/sync-evidence.js +32 -5
- package/dist/config/oat-config.d.ts +8 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +116 -1
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +31 -3
- package/dist/providers/claude/dispatch-envelope.d.ts +53 -0
- package/dist/providers/claude/dispatch-envelope.d.ts.map +1 -1
- package/dist/providers/claude/dispatch-envelope.js +324 -48
- package/dist/providers/claude/rule-transform.js +1 -1
- package/dist/providers/copilot/rule-transform.js +1 -1
- package/dist/providers/cursor/rule-transform.js +1 -1
- package/dist/providers/identity/absolute-paths.d.ts +13 -0
- package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
- package/dist/providers/identity/absolute-paths.js +27 -14
- package/dist/providers/identity/generic-dispatch-record.d.ts +212 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -1
- package/dist/providers/identity/generic-dispatch-record.js +78 -19
- package/dist/providers/identity/oat-dispatch-record.d.ts +5 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
- package/dist/providers/identity/oat-dispatch-record.js +11 -2
- package/dist/providers/shared/registry.d.ts +6 -0
- package/dist/providers/shared/registry.d.ts.map +1 -1
- package/dist/providers/shared/registry.js +30 -0
- package/dist/rules/canonical/parse.d.ts.map +1 -1
- package/dist/rules/canonical/parse.js +38 -3
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@ import { normalizeToPosixPath, resolveProjectRoot, validateRealPathWithinScope,
|
|
|
16
16
|
import { buildDispatchReport, } from '../../providers/identity/dispatch-report.js';
|
|
17
17
|
import { classifyModelFamily, } from '../../providers/identity/family.js';
|
|
18
18
|
import { resolveIdentityConfidence, } from '../../providers/identity/provenance.js';
|
|
19
|
-
import { parseDispatchStamps } from '../../providers/identity/stamp.js';
|
|
19
|
+
import { parseDispatchStamps, } from '../../providers/identity/stamp.js';
|
|
20
20
|
import { Command } from 'commander';
|
|
21
21
|
import YAML from 'yaml';
|
|
22
22
|
import { createGateActivityProbe, } from './activity-probes.js';
|
|
@@ -84,7 +84,7 @@ const VALID_IDENTITY_PROVENANCES = [
|
|
|
84
84
|
'unknown',
|
|
85
85
|
];
|
|
86
86
|
const REVIEW_GATE_CONTEXT_NOTE = [
|
|
87
|
-
|
|
87
|
+
"This review is gate-originated. If you run `oat-project-review-provide`, set `oat_review_invocation: gate` in the review artifact. Write a canonical review artifact with `### Critical`, `### High`, `### Medium`, and `### Low` headings in that order, using `None` for empty sections. Gate-originated reviews write the resolver dispatch stamp on a line labeled `**Dispatch audit (policy view):**`, with the stamp in one backtick pair, because an unlabeled audit stamp must name this gate's target and effort.",
|
|
88
88
|
'Complete the review, artifact write, and required bookkeeping inline or through a synchronously awaited child before this headless process exits. Do not start background tasks, monitors, or waiters that outlive this turn.',
|
|
89
89
|
"Artifact hygiene contract: Before finishing or committing, format every file you created or edited. Use the concrete write/fix formatting command supplied by the governing plan, task, or brief. If none is usable, discover the repository's documented write/fix command from applicable `AGENTS.md`/`CLAUDE.md` instructions and relevant package manifests; do not infer or hardcode a formatter. Prefer a file-scoped invocation when supported, and avoid rewriting unrelated files. If no command is discoverable, warn once with `no format command discovered in repo instructions; skipping`, then continue.",
|
|
90
90
|
].join('\n\n');
|
|
@@ -281,6 +281,29 @@ function gateInvocationPromptContext(invocation) {
|
|
|
281
281
|
frontmatter,
|
|
282
282
|
].join('\n');
|
|
283
283
|
}
|
|
284
|
+
const NON_CONCRETE_REASONING_EFFORTS = new Set([
|
|
285
|
+
'unknown',
|
|
286
|
+
'provider-default',
|
|
287
|
+
'not-applicable',
|
|
288
|
+
]);
|
|
289
|
+
/**
|
|
290
|
+
* Unlabeled reviewer audit stamps that disagree with the gate frontmatter.
|
|
291
|
+
*
|
|
292
|
+
* The frontmatter (`oat_gate_target`, `oat_invocation_*`) is the authority for
|
|
293
|
+
* the gate's actual invocation. A stamp labeled as the reviewer policy view is
|
|
294
|
+
* exempt; any other audit stamp must name the gate target and, when the gate
|
|
295
|
+
* recorded a concrete reasoning effort, carry `effort_axis=selected:<effort>`.
|
|
296
|
+
*/
|
|
297
|
+
function disagreeingDispatchAuditStamps(verdict) {
|
|
298
|
+
const unlabeled = verdict.dispatchAudit?.unlabeled ?? [];
|
|
299
|
+
const target = verdict.gateInvocation?.targetId ?? null;
|
|
300
|
+
const effort = verdict.gateInvocation?.reasoningEffort ?? null;
|
|
301
|
+
const expectedEffortAxis = effort && !NON_CONCRETE_REASONING_EFFORTS.has(effort)
|
|
302
|
+
? `selected:${effort}`
|
|
303
|
+
: null;
|
|
304
|
+
return unlabeled.filter((stamp) => stamp.target !== target ||
|
|
305
|
+
(expectedEffortAxis !== null && stamp.effortAxis !== expectedEffortAxis));
|
|
306
|
+
}
|
|
284
307
|
function corroborateGateInvocation(expected, actual, targetCorroboration) {
|
|
285
308
|
const invocationFields = [
|
|
286
309
|
['targetId', expected.targetId, actual?.targetId],
|
|
@@ -2422,6 +2445,32 @@ async function disposeValidatedReviewArtifact(snapshot, identity, dependencies,
|
|
|
2422
2445
|
},
|
|
2423
2446
|
};
|
|
2424
2447
|
}
|
|
2448
|
+
const disagreeingAudit = disagreeingDispatchAuditStamps(verdict);
|
|
2449
|
+
if (disagreeingAudit.length > 0) {
|
|
2450
|
+
const lines = disagreeingAudit.map((stamp) => stamp.lineNumber).join(', ');
|
|
2451
|
+
const effort = verdict.gateInvocation?.reasoningEffort ?? 'unknown';
|
|
2452
|
+
return {
|
|
2453
|
+
eligible: false,
|
|
2454
|
+
status: 'artifact_validation_failed',
|
|
2455
|
+
cause: 'gate_dispatch_audit_mismatched',
|
|
2456
|
+
verdict,
|
|
2457
|
+
writeEnvelope: (context) => {
|
|
2458
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
2459
|
+
runId: identity.runId,
|
|
2460
|
+
target: identity.target,
|
|
2461
|
+
project: identity.project,
|
|
2462
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2463
|
+
artifactPath: snapshot.path,
|
|
2464
|
+
generatedAt: snapshot.generatedAt,
|
|
2465
|
+
message: `Review artifact dispatch audit line (line ${lines}) does not agree with the gate invocation frontmatter (oat_gate_target: ${verdict.gateInvocation?.targetId ?? 'unknown'}, oat_invocation_reasoning_effort: ${effort}). An unlabeled audit stamp must describe the gate invocation; label the reviewer policy stamp \`**Dispatch audit (policy view):**\`. Artifacts written by an older installed review skill need \`oat tools update\`.`,
|
|
2466
|
+
recovery: `Label the reviewer policy stamp in ${snapshot.path} as \`**Dispatch audit (policy view):**\`, or correct the stamp to the gate target and effort, then rerun the gate. If an older installed oat-project-review-provide wrote the unlabeled line, refresh installed skills with \`oat tools update\` and rerun the gate.`,
|
|
2467
|
+
gateInvocation: identity.gateInvocation,
|
|
2468
|
+
dispatchReport: identity.dispatchReport,
|
|
2469
|
+
corroboration,
|
|
2470
|
+
});
|
|
2471
|
+
},
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2425
2474
|
hooks.onStep?.('verdict-disposition');
|
|
2426
2475
|
return {
|
|
2427
2476
|
eligible: true,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DispatchStamp } from '../../providers/identity/stamp.js';
|
|
1
2
|
export interface ReviewGateVerdict {
|
|
2
3
|
artifactPath: string;
|
|
3
4
|
reviewType: 'code' | 'artifact' | 'unknown';
|
|
@@ -5,6 +6,8 @@ export interface ReviewGateVerdict {
|
|
|
5
6
|
invocation: string | null;
|
|
6
7
|
project: string | null;
|
|
7
8
|
gateInvocation?: ReviewArtifactGateInvocation;
|
|
9
|
+
/** Present only when the artifact carries at least one audit line. */
|
|
10
|
+
dispatchAudit?: ReviewDispatchAudit;
|
|
8
11
|
counts: {
|
|
9
12
|
critical: number;
|
|
10
13
|
high: number;
|
|
@@ -25,6 +28,15 @@ export interface ReviewArtifactGateInvocation {
|
|
|
25
28
|
reasoningEffort: string | null;
|
|
26
29
|
source: string | null;
|
|
27
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Reviewer dispatch stamps found on audit lines. `policyView` holds stamps
|
|
33
|
+
* whose label names the project reviewer policy view; every other audit stamp
|
|
34
|
+
* is `unlabeled` and must agree with the gate invocation frontmatter.
|
|
35
|
+
*/
|
|
36
|
+
export interface ReviewDispatchAudit {
|
|
37
|
+
unlabeled: DispatchStamp[];
|
|
38
|
+
policyView: DispatchStamp[];
|
|
39
|
+
}
|
|
28
40
|
export type Severity = keyof ReviewGateVerdict['counts'];
|
|
29
41
|
export interface ReviewGateArtifactSnapshot {
|
|
30
42
|
readonly content: string;
|
|
@@ -35,5 +47,15 @@ export interface ParseReviewGateVerdictOptions {
|
|
|
35
47
|
artifactSnapshot?: ReviewGateArtifactSnapshot;
|
|
36
48
|
}
|
|
37
49
|
export declare function severityDisplayName(severity: Severity): string;
|
|
50
|
+
/**
|
|
51
|
+
* Reviewer dispatch audit lines from every part of the artifact body except
|
|
52
|
+
* finding sections: `## Findings` (with its subsections) and any
|
|
53
|
+
* `### Critical`, `### High`, `### Medium`, or `### Low` section. Reviewers
|
|
54
|
+
* have placed audit stamps in the pre-heading metadata block and in sections
|
|
55
|
+
* such as `## Dispatch Audit`, `## Review Dispatch Audit`, `## Review Scope`,
|
|
56
|
+
* and `## Dispatch Evidence`, so recognition is by line shape, not section
|
|
57
|
+
* name. Fenced code blocks and prose that merely quotes a stamp are ignored.
|
|
58
|
+
*/
|
|
59
|
+
export declare function extractDispatchAudit(content: string): ReviewDispatchAudit;
|
|
38
60
|
export declare function parseReviewGateVerdict(artifactPath: string, options?: ParseReviewGateVerdictOptions): Promise<ReviewGateVerdict>;
|
|
39
61
|
//# sourceMappingURL=review-verdict.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,CAAC,EAAE,4BAA4B,CAAC;IAC9C,sEAAsE;IACtE,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE;QACd,kBAAkB,EAAE,QAAQ,EAAE,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;AAEzD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,6BAA6B;IAC5C,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;CAC/C;AA8OD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE9D;AAwJD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAkCzE;AAwbD,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAkG5B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { getFrontmatterBlock } from '../shared/frontmatter.js';
|
|
4
|
+
import { parseDispatchStamps, } from '../../providers/identity/stamp.js';
|
|
4
5
|
import YAML from 'yaml';
|
|
5
6
|
const SEVERITIES = ['critical', 'high', 'medium', 'low'];
|
|
6
7
|
const FRONTMATTER_COUNT_KEYS = {
|
|
@@ -214,6 +215,113 @@ function linesOutsideFences(content) {
|
|
|
214
215
|
}
|
|
215
216
|
return outsideFenceLines;
|
|
216
217
|
}
|
|
218
|
+
const TOP_SECTION_HEADING = /^##\s+\S/;
|
|
219
|
+
const FINDINGS_SECTION_HEADING = /^##\s+Findings\b/i;
|
|
220
|
+
const SEVERITY_SECTION_HEADING = /^#{3,6}\s+(?:Critical|High|Medium|Low)\s*#*\s*$/i;
|
|
221
|
+
const LIST_MARKER = /^(?:[-*+]|\d+[.)])\s+/;
|
|
222
|
+
const DISPATCH_TOKEN = /(^|[\s`])Dispatch:/;
|
|
223
|
+
const POLICY_VIEW_LABEL = /policy\s+view/i;
|
|
224
|
+
/** After a closing backtick: nothing, punctuation, or one parenthetical. */
|
|
225
|
+
const ALLOWED_TRAILING_TEXT = /^\s*(?:[.,;:!?]+|\([^()`]*\))?\s*$/;
|
|
226
|
+
function frontmatterEnd(content) {
|
|
227
|
+
const match = content.match(/^---\n[\s\S]*?\n---(?:\n|$)/);
|
|
228
|
+
return match ? match[0].length : 0;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Classifies one line as a reviewer dispatch audit line.
|
|
232
|
+
*
|
|
233
|
+
* Shape: an optional list marker, an optional bold or plain label ending in
|
|
234
|
+
* `:` (the label may itself hold backtick spans), and a `Dispatch:` stamp that
|
|
235
|
+
* is either bare to the end of the line or wrapped in one backtick pair, which
|
|
236
|
+
* may be followed only by punctuation or a single parenthetical. A wrapped
|
|
237
|
+
* stamp followed by other prose is a quotation, not an audit line. The stamp
|
|
238
|
+
* must parse as a reviewer stamp (`action=review role=reviewer`).
|
|
239
|
+
*/
|
|
240
|
+
function classifyAuditLine(text, lineNumber) {
|
|
241
|
+
const remainder = text.trim().replace(LIST_MARKER, '');
|
|
242
|
+
const token = DISPATCH_TOKEN.exec(remainder);
|
|
243
|
+
if (!token) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
let label = remainder.slice(0, token.index + (token[1] ?? '').length);
|
|
247
|
+
let stamp = remainder.slice(token.index + (token[1] ?? '').length);
|
|
248
|
+
const wrapped = label.endsWith('`');
|
|
249
|
+
if (wrapped) {
|
|
250
|
+
label = label.slice(0, -1);
|
|
251
|
+
const closing = stamp.indexOf('`');
|
|
252
|
+
if (closing === -1) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
// Only punctuation or one parenthetical may follow the closing backtick;
|
|
256
|
+
// longer prose means the line quotes a stamp rather than records one.
|
|
257
|
+
if (!ALLOWED_TRAILING_TEXT.test(stamp.slice(closing + 1))) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
stamp = stamp.slice(0, closing);
|
|
261
|
+
}
|
|
262
|
+
else if (stamp.includes('`')) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const trimmedLabel = label.trim();
|
|
266
|
+
if (trimmedLabel && !/:(?:\*{1,2}|_{1,2})?$/.test(trimmedLabel)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const parsed = parseDispatchStamps(stamp);
|
|
270
|
+
const first = parsed[0];
|
|
271
|
+
if (parsed.length !== 1 ||
|
|
272
|
+
!first ||
|
|
273
|
+
first.legacy ||
|
|
274
|
+
first.action !== 'review' ||
|
|
275
|
+
first.role !== 'reviewer') {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
stamp: { ...first, line: text, lineNumber },
|
|
280
|
+
policyView: POLICY_VIEW_LABEL.test(trimmedLabel),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Reviewer dispatch audit lines from every part of the artifact body except
|
|
285
|
+
* finding sections: `## Findings` (with its subsections) and any
|
|
286
|
+
* `### Critical`, `### High`, `### Medium`, or `### Low` section. Reviewers
|
|
287
|
+
* have placed audit stamps in the pre-heading metadata block and in sections
|
|
288
|
+
* such as `## Dispatch Audit`, `## Review Dispatch Audit`, `## Review Scope`,
|
|
289
|
+
* and `## Dispatch Evidence`, so recognition is by line shape, not section
|
|
290
|
+
* name. Fenced code blocks and prose that merely quotes a stamp are ignored.
|
|
291
|
+
*/
|
|
292
|
+
export function extractDispatchAudit(content) {
|
|
293
|
+
const bodyStart = frontmatterEnd(content);
|
|
294
|
+
const audit = { unlabeled: [], policyView: [] };
|
|
295
|
+
let inFindingSection = false;
|
|
296
|
+
for (const line of linesOutsideFences(content)) {
|
|
297
|
+
if (line.start < bodyStart) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (TOP_SECTION_HEADING.test(line.text)) {
|
|
301
|
+
inFindingSection = FINDINGS_SECTION_HEADING.test(line.text);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (SEVERITY_SECTION_HEADING.test(line.text)) {
|
|
305
|
+
inFindingSection = true;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (inFindingSection) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const lineNumber = content.slice(0, line.start).split('\n').length;
|
|
312
|
+
const classified = classifyAuditLine(line.text, lineNumber);
|
|
313
|
+
if (!classified) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (classified.policyView) {
|
|
317
|
+
audit.policyView.push(classified.stamp);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
audit.unlabeled.push(classified.stamp);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return audit;
|
|
324
|
+
}
|
|
217
325
|
function findFindingsSection(content) {
|
|
218
326
|
const lines = linesOutsideFences(content);
|
|
219
327
|
const findingsHeading = lines.find((line) => /^##\s+Findings\s*#*\s*$/i.test(line.text));
|
|
@@ -544,6 +652,8 @@ export async function parseReviewGateVerdict(artifactPath, options = {}) {
|
|
|
544
652
|
throw new Error(`Review artifact at ${artifactPath} does not contain recognizable review findings or explicit verdict counts.`);
|
|
545
653
|
}
|
|
546
654
|
const gateInvocation = readGateInvocation(frontmatter);
|
|
655
|
+
const dispatchAudit = extractDispatchAudit(content);
|
|
656
|
+
const hasDispatchAudit = dispatchAudit.unlabeled.length > 0 || dispatchAudit.policyView.length > 0;
|
|
547
657
|
if (options.artifactSnapshot) {
|
|
548
658
|
await assertArtifactContentCurrent(artifactPath, options.artifactSnapshot.content);
|
|
549
659
|
}
|
|
@@ -554,6 +664,7 @@ export async function parseReviewGateVerdict(artifactPath, options = {}) {
|
|
|
554
664
|
invocation: stringOrNull(frontmatter['oat_review_invocation']),
|
|
555
665
|
project: stringOrNull(frontmatter['oat_project']),
|
|
556
666
|
...(gateInvocation ? { gateInvocation } : {}),
|
|
667
|
+
...(hasDispatchAudit ? { dispatchAudit } : {}),
|
|
557
668
|
counts,
|
|
558
669
|
blocking: hasBlockingFindings(counts),
|
|
559
670
|
...(insertedSeverities.length > 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CanonicalRoleEvidence } from '../../../agents/canonical/index.js';
|
|
2
|
+
import type { CommandContext, GlobalOptions } from '../../../app/command-context.js';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
/** The pack that ships the canonical OAT dispatch roles. */
|
|
5
|
+
export declare const CANONICAL_ROLE_DEPENDENCY = "workflows";
|
|
6
|
+
export interface CanonicalRoleCommandDependencies {
|
|
7
|
+
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
8
|
+
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
export interface CanonicalRoleResolutionEvent {
|
|
11
|
+
kind: 'canonical-role-resolution';
|
|
12
|
+
requestId: string;
|
|
13
|
+
source: 'canonical-role-resolver';
|
|
14
|
+
evidence: CanonicalRoleEvidence;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Produce `canonical-role-resolution` evidence from the resolver itself, so a
|
|
18
|
+
* caller never hand-assembles redacted paths or a content digest. Read-only:
|
|
19
|
+
* it inspects the loaded, user, and project `.agents` roots and writes
|
|
20
|
+
* nothing. The emitted event is checked against the recorder's own event
|
|
21
|
+
* schema before it is printed, so `oat project dispatch record` accepts it
|
|
22
|
+
* unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export declare function produceCanonicalRoleEvent(input: {
|
|
25
|
+
role: string;
|
|
26
|
+
requestId: string;
|
|
27
|
+
skillDir: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
home: string;
|
|
30
|
+
repoRoot: string;
|
|
31
|
+
}): Promise<CanonicalRoleResolutionEvent>;
|
|
32
|
+
export declare function createCanonicalRoleCommand(dependencies: CanonicalRoleCommandDependencies): Command;
|
|
33
|
+
//# sourceMappingURL=canonical-role.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical-role.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/canonical-role.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,4DAA4D;AAC5D,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAKrD,MAAM,WAAW,gCAAgC;IAC/C,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACtD;AAQD,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,2BAA2B,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,yBAAyB,CAAC;IAClC,QAAQ,EAAE,qBAAqB,CAAC;CACjC;AAeD;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAAC,KAAK,EAAE;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAiCxC;AAwDD,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,gCAAgC,GAC7C,OAAO,CAyBT"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
2
|
+
import { resolveCanonicalRole, } from '../../../agents/canonical/index.js';
|
|
3
|
+
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
4
|
+
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
5
|
+
import { safeParseOatDispatchEvidenceEvent } from '../../../providers/identity/oat-dispatch-record.js';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { redactDispatchMessage } from './record.js';
|
|
8
|
+
/** The pack that ships the canonical OAT dispatch roles. */
|
|
9
|
+
export const CANONICAL_ROLE_DEPENDENCY = 'workflows';
|
|
10
|
+
const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
11
|
+
const ROLE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
12
|
+
async function loadedSkillDir(cwd, skillDir) {
|
|
13
|
+
const candidate = isAbsolute(skillDir) ? skillDir : resolve(cwd, skillDir);
|
|
14
|
+
if (!(await dirExists(candidate)) ||
|
|
15
|
+
!(await fileExists(join(candidate, 'SKILL.md')))) {
|
|
16
|
+
throw new Error(`--skill-dir must name a loaded skill directory containing SKILL.md (got ${skillDir}); the loaded tier is derived from it.`);
|
|
17
|
+
}
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Produce `canonical-role-resolution` evidence from the resolver itself, so a
|
|
22
|
+
* caller never hand-assembles redacted paths or a content digest. Read-only:
|
|
23
|
+
* it inspects the loaded, user, and project `.agents` roots and writes
|
|
24
|
+
* nothing. The emitted event is checked against the recorder's own event
|
|
25
|
+
* schema before it is printed, so `oat project dispatch record` accepts it
|
|
26
|
+
* unchanged.
|
|
27
|
+
*/
|
|
28
|
+
export async function produceCanonicalRoleEvent(input) {
|
|
29
|
+
if (!ROLE_NAME_PATTERN.test(input.role)) {
|
|
30
|
+
throw new Error('--role must be a canonical role name: expected [a-z0-9] followed by [a-z0-9_-].');
|
|
31
|
+
}
|
|
32
|
+
if (!REQUEST_ID_PATTERN.test(input.requestId)) {
|
|
33
|
+
throw new Error('--request-id must be a stable contained identifier: expected [a-zA-Z0-9] followed by up to 127 of [a-zA-Z0-9._-].');
|
|
34
|
+
}
|
|
35
|
+
const skillDir = await loadedSkillDir(input.cwd, input.skillDir);
|
|
36
|
+
const event = {
|
|
37
|
+
kind: 'canonical-role-resolution',
|
|
38
|
+
requestId: input.requestId,
|
|
39
|
+
source: 'canonical-role-resolver',
|
|
40
|
+
evidence: resolveCanonicalRole({
|
|
41
|
+
dependency: CANONICAL_ROLE_DEPENDENCY,
|
|
42
|
+
canonicalRole: input.role,
|
|
43
|
+
skillDir,
|
|
44
|
+
userCanonicalRoot: join(input.home, '.agents'),
|
|
45
|
+
projectCanonicalRoot: join(input.repoRoot, '.agents'),
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
const checked = safeParseOatDispatchEvidenceEvent(event);
|
|
49
|
+
if (!checked.success) {
|
|
50
|
+
throw new Error(`Canonical role evidence failed the dispatch-record event schema: ${checked.error.issues
|
|
51
|
+
.map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
|
|
52
|
+
.join('; ')}`);
|
|
53
|
+
}
|
|
54
|
+
return event;
|
|
55
|
+
}
|
|
56
|
+
function summaryLines(event) {
|
|
57
|
+
const { evidence } = event;
|
|
58
|
+
if (evidence.status === 'resolved') {
|
|
59
|
+
return [
|
|
60
|
+
`Canonical role ${evidence.canonicalRole} resolved from the ${evidence.tier} tier (${evidence.validation}, version ${evidence.roleVersion}).`,
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
`Canonical role ${evidence.canonicalRole} was not found in the loaded, user, or project tier.`,
|
|
65
|
+
...evidence.recovery.map(({ command }) => `Recovery: ${command}`),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
async function runCanonicalRoleCommand(options, context, dependencies) {
|
|
69
|
+
let repoRoot = null;
|
|
70
|
+
try {
|
|
71
|
+
repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
72
|
+
const event = await produceCanonicalRoleEvent({
|
|
73
|
+
role: options.role,
|
|
74
|
+
requestId: options.requestId,
|
|
75
|
+
skillDir: options.skillDir,
|
|
76
|
+
cwd: context.cwd,
|
|
77
|
+
home: context.home,
|
|
78
|
+
repoRoot,
|
|
79
|
+
});
|
|
80
|
+
if (context.json) {
|
|
81
|
+
context.logger.json(event);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const line of summaryLines(event)) {
|
|
85
|
+
context.logger.info(line);
|
|
86
|
+
}
|
|
87
|
+
context.logger.info(JSON.stringify(event, null, 2));
|
|
88
|
+
}
|
|
89
|
+
process.exitCode = 0;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// The same single redaction boundary as `dispatch record`: a skill
|
|
93
|
+
// directory or root that reaches a message is labelled or scrubbed.
|
|
94
|
+
const message = redactDispatchMessage(error instanceof Error ? error.message : String(error), { repo: repoRoot, home: context.home });
|
|
95
|
+
if (context.json) {
|
|
96
|
+
context.logger.json({ status: 'error', message });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
context.logger.error(message);
|
|
100
|
+
}
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function createCanonicalRoleCommand(dependencies) {
|
|
105
|
+
return new Command('canonical-role')
|
|
106
|
+
.description('Print canonical-role-resolution evidence for a dispatch record (read-only)')
|
|
107
|
+
.requiredOption('--role <name>', 'Canonical role name, e.g. oat-reviewer')
|
|
108
|
+
.requiredOption('--request-id <id>', 'Request ID of the dispatch record the evidence belongs to')
|
|
109
|
+
.requiredOption('--skill-dir <dir>', 'Directory of the loaded skill (contains SKILL.md); the loaded tier is derived from it')
|
|
110
|
+
.action(async (options, command) => {
|
|
111
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
112
|
+
await runCanonicalRoleCommand(options, context, dependencies);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,MAAM,WAAW,kCAAkC;IACjD,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AA0ID,wBAAgB,4BAA4B,CAC1C,SAAS,GAAE,OAAO,CAAC,kCAAkC,CAAM,GAC1D,OAAO,CA4BT"}
|
|
@@ -5,6 +5,7 @@ import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
|
5
5
|
import { fileExists } from '../../../fs/io.js';
|
|
6
6
|
import { resolveProjectRoot, validateRealPathWithinScope } from '../../../fs/paths.js';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
+
import { createCanonicalRoleCommand } from './canonical-role.js';
|
|
8
9
|
import { recordProjectDispatch, redactDispatchMessage, } from './record.js';
|
|
9
10
|
const DEFAULT_DEPENDENCIES = {
|
|
10
11
|
buildCommandContext,
|
|
@@ -133,5 +134,6 @@ export function createProjectDispatchCommand(overrides = {}) {
|
|
|
133
134
|
.action(async (options, command) => {
|
|
134
135
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
135
136
|
await runRecordCommand(options, context, dependencies);
|
|
136
|
-
}))
|
|
137
|
+
}))
|
|
138
|
+
.addCommand(createCanonicalRoleCommand(dependencies));
|
|
137
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/record.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/record.ts"],"names":[],"mappings":"AAuBA,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAQL,KAAK,wBAAwB,EAE7B,KAAK,4BAA4B,EAElC,MAAM,yCAAyC,CAAC;AAYjD;;;;;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,CAiBR;AAeD,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;AA8NF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CA6G5E;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"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { publishContainedJsonRevision, redactedFsError, withContainedWriterLock, } from '../../../fs/io.js';
|
|
4
|
-
import { acceptClaudeLaunchEnvelope, buildClaudeDispatchRecord, } from '../../../providers/claude/dispatch-envelope.js';
|
|
4
|
+
import { acceptClaudeLaunchEnvelope, buildClaudeDispatchRecord, collectClaudeLaunchViolations, collectClaudeRecordBaseViolations, ManagedClaudeDispatchValidationError, zodIssueViolations, } from '../../../providers/claude/dispatch-envelope.js';
|
|
5
5
|
import { assertJournalIdentityHasNoAbsolutePath, redactAbsolutePaths, } from '../../../providers/identity/absolute-paths.js';
|
|
6
6
|
import { CLAUDE_OBSERVATION_SOURCE } from '../../../providers/identity/claude-runtime-observation.js';
|
|
7
7
|
import { CODEX_OBSERVATION_SOURCE } from '../../../providers/identity/codex-runtime-observation.js';
|
|
8
|
-
import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, } from '../../../providers/identity/generic-dispatch-record.js';
|
|
9
|
-
import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, } from '../../../providers/identity/oat-dispatch-record.js';
|
|
8
|
+
import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, collectSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, redactSensitiveValues, } from '../../../providers/identity/generic-dispatch-record.js';
|
|
9
|
+
import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, safeParseOatDispatchEvidenceEvent, } from '../../../providers/identity/oat-dispatch-record.js';
|
|
10
10
|
import { observationFromFacts, parseRuntimeObservationEnvelope, projectRuntimeObservation, providerSupportsRuntimeObservation, } from '../../../providers/identity/runtime-observation.js';
|
|
11
|
+
import { ZodError } from 'zod';
|
|
11
12
|
const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
12
13
|
const DISPATCH_LOCK_NAME = '.dispatch-lock';
|
|
13
14
|
/**
|
|
@@ -48,7 +49,10 @@ function parseJournalEntry(fileName) {
|
|
|
48
49
|
* written verbatim into the durable record by the same command.
|
|
49
50
|
*/
|
|
50
51
|
export function redactDispatchMessage(message, roots = {}) {
|
|
51
|
-
|
|
52
|
+
// Secrets first: a rejected value is echoed by some validation messages (a
|
|
53
|
+
// Zod enum issue repeats what it received), so a secret-shaped value must be
|
|
54
|
+
// scrubbed here even though the same run also reports it as sensitive.
|
|
55
|
+
let redacted = redactSensitiveValues(message);
|
|
52
56
|
const labelled = [
|
|
53
57
|
['<project>', roots.project],
|
|
54
58
|
['<repo>', roots.repo],
|
|
@@ -71,6 +75,8 @@ const OBSERVATION_SOURCE_BY_PROVIDER = {
|
|
|
71
75
|
claude: CLAUDE_OBSERVATION_SOURCE,
|
|
72
76
|
};
|
|
73
77
|
const CALLER_ASSERTED_OBSERVATION_SOURCE = 'caller-asserted';
|
|
78
|
+
/** A managed Claude record's provider is derived, and always this value. */
|
|
79
|
+
const MANAGED_CLAUDE_PROVIDER = 'claude';
|
|
74
80
|
function isRecord(value) {
|
|
75
81
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
76
82
|
}
|
|
@@ -123,6 +129,139 @@ function resolveObservationEvent(record, event) {
|
|
|
123
129
|
}),
|
|
124
130
|
};
|
|
125
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Project a caller-form `runtime-observation` event onto the shape the event
|
|
134
|
+
* schema checks, so the single-run pass reports its errors alongside every
|
|
135
|
+
* other stage instead of on a later run.
|
|
136
|
+
*
|
|
137
|
+
* The caller controls the envelope (`kind`, `requestId`, `source`, unknown
|
|
138
|
+
* keys) and either a finished `observation` or a `metadata` envelope, and
|
|
139
|
+
* both are checked here. The recording path derives an observation's
|
|
140
|
+
* `source`, `match`, and `comparedAxes` from the record, so those are filled
|
|
141
|
+
* with valid stand-ins rather than demanded from the caller; the metadata
|
|
142
|
+
* form's projection and request-correlation guard degrade to `not-reported`
|
|
143
|
+
* rather than fail, so they contribute no violation. The provider binding is
|
|
144
|
+
* checked directly: a managed record's provider is always `claude`.
|
|
145
|
+
*/
|
|
146
|
+
function runtimeObservationCallerForm(event, violations) {
|
|
147
|
+
if ('metadata' in event) {
|
|
148
|
+
if ('observation' in event) {
|
|
149
|
+
violations.push({
|
|
150
|
+
stage: 'event',
|
|
151
|
+
path: '<root>',
|
|
152
|
+
message: 'A runtime observation event carries either observation or metadata, not both.',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
parseRuntimeObservationEnvelope(event.metadata);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof ZodError) {
|
|
160
|
+
violations.push(...zodIssueViolations('event', 'metadata', error));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
violations.push({
|
|
164
|
+
stage: 'event',
|
|
165
|
+
path: 'metadata',
|
|
166
|
+
message: error instanceof Error ? error.message : String(error),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const { metadata: _metadata, observation: _observation, ...rest } = event;
|
|
171
|
+
return { ...rest, observation: { status: 'not-reported' } };
|
|
172
|
+
}
|
|
173
|
+
const observation = event.observation;
|
|
174
|
+
if (!isRecord(observation) || observation.status !== 'reported') {
|
|
175
|
+
return event;
|
|
176
|
+
}
|
|
177
|
+
if (observation.provider !== MANAGED_CLAUDE_PROVIDER) {
|
|
178
|
+
violations.push({
|
|
179
|
+
stage: 'event',
|
|
180
|
+
path: 'observation.provider',
|
|
181
|
+
message: 'A runtime observation must name the same provider as its dispatch record.',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...event,
|
|
186
|
+
observation: {
|
|
187
|
+
...observation,
|
|
188
|
+
provider: MANAGED_CLAUDE_PROVIDER,
|
|
189
|
+
source: CALLER_ASSERTED_OBSERVATION_SOURCE,
|
|
190
|
+
match: 'not-comparable',
|
|
191
|
+
comparedAxes: [],
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Event violations for a managed input: the event must be an object, carry no
|
|
197
|
+
* sensitive content, satisfy the evidence-event schema, and name the same
|
|
198
|
+
* request as `recordBase`. Raw provider `metadata` entries are exempt from the
|
|
199
|
+
* sensitive walk exactly as on the recording path. A `runtime-observation`
|
|
200
|
+
* event is validated here too: `runtimeObservationCallerForm` projects its
|
|
201
|
+
* caller form onto the schema shape, filling the fields the recording path
|
|
202
|
+
* derives (source, match, compared axes) with valid stand-ins, so its errors
|
|
203
|
+
* join this single-run report.
|
|
204
|
+
*/
|
|
205
|
+
function collectManagedEventViolations(event, recordBase) {
|
|
206
|
+
if (!isRecord(event)) {
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
stage: 'event',
|
|
210
|
+
path: '<root>',
|
|
211
|
+
message: 'Dispatch record event must be a JSON object.',
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
const violations = [];
|
|
216
|
+
const walked = isRecord(event.metadata)
|
|
217
|
+
? { ...event, metadata: { ...event.metadata, entries: null } }
|
|
218
|
+
: event;
|
|
219
|
+
violations.push(...collectSensitiveDispatchContent(walked, 'event').map((violation) => ({
|
|
220
|
+
stage: 'event',
|
|
221
|
+
path: violation.path.slice('event.'.length) || '<root>',
|
|
222
|
+
message: violation.message,
|
|
223
|
+
})));
|
|
224
|
+
const parsed = safeParseOatDispatchEvidenceEvent(event.kind === 'runtime-observation'
|
|
225
|
+
? runtimeObservationCallerForm(event, violations)
|
|
226
|
+
: event);
|
|
227
|
+
if (!parsed.success) {
|
|
228
|
+
violations.push(...zodIssueViolations('event', null, parsed.error));
|
|
229
|
+
}
|
|
230
|
+
const recordRequestId = isRecord(recordBase) ? recordBase.request_id : null;
|
|
231
|
+
if (typeof event.requestId === 'string' &&
|
|
232
|
+
typeof recordRequestId === 'string' &&
|
|
233
|
+
event.requestId !== recordRequestId) {
|
|
234
|
+
violations.push({
|
|
235
|
+
stage: 'event',
|
|
236
|
+
path: 'requestId',
|
|
237
|
+
message: 'OAT event request ID must match the generic record.',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return violations;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The single-run pass for a managed Claude input. Every stage is validated
|
|
244
|
+
* independently and every violation is reported in one error, so a caller
|
|
245
|
+
* assembling the input fixes it in one round instead of one field per run.
|
|
246
|
+
* Checks that depend on a stage which failed to parse are skipped and named in
|
|
247
|
+
* the error; the recording path below still runs every check afterwards, so
|
|
248
|
+
* nothing this pass skips goes unchecked.
|
|
249
|
+
*/
|
|
250
|
+
function assertManagedClaudeInputValid(value) {
|
|
251
|
+
const launch = collectClaudeLaunchViolations(value.claudeLaunch);
|
|
252
|
+
const base = collectClaudeRecordBaseViolations(value.recordBase, launch.resolverRole);
|
|
253
|
+
const violations = [
|
|
254
|
+
...launch.violations,
|
|
255
|
+
...base.violations,
|
|
256
|
+
...collectManagedEventViolations(value.event, value.recordBase),
|
|
257
|
+
];
|
|
258
|
+
if (violations.length > 0) {
|
|
259
|
+
throw new ManagedClaudeDispatchValidationError(violations, [
|
|
260
|
+
...launch.skipped,
|
|
261
|
+
...base.skipped,
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
126
265
|
export function parseDispatchRecordInput(value) {
|
|
127
266
|
if (!isRecord(value)) {
|
|
128
267
|
throw new Error('Dispatch record input must be a JSON object.');
|
|
@@ -136,6 +275,9 @@ export function parseDispatchRecordInput(value) {
|
|
|
136
275
|
if (!isGenericInput && !isManagedClaudeInput) {
|
|
137
276
|
throw new Error('Dispatch record input accepts record and event, or claudeLaunch, event, and recordBase.');
|
|
138
277
|
}
|
|
278
|
+
if (isManagedClaudeInput) {
|
|
279
|
+
assertManagedClaudeInputValid(value);
|
|
280
|
+
}
|
|
139
281
|
if (!isRecord(value.event)) {
|
|
140
282
|
throw new Error('Dispatch record event must be a JSON object.');
|
|
141
283
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoB3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EAExB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoB3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EAExB,MAAM,cAAc,CAAC;AAwYtB,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,aAAa,EAAE,EAC3B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Mf"}
|