@multileaf/ai-workflow 0.9.0 → 0.11.0
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/CHANGELOG.md +34 -0
- package/README.md +12 -1
- package/dist/blocking-reasons.d.ts +67 -0
- package/dist/blocking-reasons.js +199 -0
- package/dist/command-evidence.d.ts +32 -0
- package/dist/command-evidence.js +61 -0
- package/dist/human-approval.d.ts +19 -2
- package/dist/human-approval.js +82 -9
- package/dist/recommendations.js +16 -1
- package/dist/risk-assess.d.ts +50 -0
- package/dist/risk-assess.js +273 -0
- package/dist/workflow.js +280 -30
- package/docs/README.md +2 -0
- package/docs/architecture.md +50 -0
- package/docs/backlog.md +13 -0
- package/docs/continuation-inventory.md +24 -0
- package/docs/decision-model-evaluation.md +4 -0
- package/docs/opportunities/OPP-001-proportional-ceremony-light-lane.md +62 -0
- package/docs/opportunities/OPP-002-risk-assess-command.md +70 -0
- package/docs/opportunities/OPP-003-adversarial-review-package.md +63 -0
- package/docs/opportunities/OPP-004-exact-continuation-commands.md +59 -0
- package/docs/opportunities/OPP-005-friction-benchmark.md +62 -0
- package/docs/opportunities/OPP-006-emission-usability-test-policy.md +57 -0
- package/docs/opportunities/OPP-007-decision-findings-triage-scope-watch.md +54 -0
- package/docs/opportunities/OPP-008-embedding-covering-tests-judge-context.md +52 -0
- package/docs/opportunities/README.md +33 -0
- package/docs/product-requirements.md +3 -0
- package/docs/product-vision.md +1 -0
- package/docs/quality.md +10 -0
- package/docs/release-process.md +9 -1
- package/docs/semantic-mapping.md +4 -0
- package/package.json +1 -1
- package/resources/package.yaml +3 -0
- package/resources/skills/code-review.md +2 -0
- package/resources/skills/review-judge-a.md +24 -0
- package/resources/skills/review-judge-b.md +24 -0
- package/resources/skills/review-refuter.md +28 -0
- package/resources/skills/ticket.md +2 -0
package/dist/recommendations.js
CHANGED
|
@@ -3,6 +3,7 @@ const CATALOG = [
|
|
|
3
3
|
{
|
|
4
4
|
id: "brainstorming",
|
|
5
5
|
provider: "multileaf",
|
|
6
|
+
universal: true,
|
|
6
7
|
rationale: "A project-specific brainstorming flow helps clarify goals and constraints before implementation.",
|
|
7
8
|
permissions: [],
|
|
8
9
|
resources: local([
|
|
@@ -17,6 +18,7 @@ const CATALOG = [
|
|
|
17
18
|
{
|
|
18
19
|
id: "ticket-driven-development",
|
|
19
20
|
provider: "multileaf",
|
|
21
|
+
universal: true,
|
|
20
22
|
rationale: "Work often arrives as a ticket or an already-clear requirement; this entry point starts the SDD flow from the ticket without a brainstorming phase.",
|
|
21
23
|
permissions: [],
|
|
22
24
|
resources: local([
|
|
@@ -30,6 +32,7 @@ const CATALOG = [
|
|
|
30
32
|
{
|
|
31
33
|
id: "requirements-specification",
|
|
32
34
|
provider: "multileaf",
|
|
35
|
+
universal: true,
|
|
33
36
|
rationale: "The project can use explicit requirements and acceptance criteria before changes are planned.",
|
|
34
37
|
permissions: [],
|
|
35
38
|
resources: local([
|
|
@@ -44,6 +47,7 @@ const CATALOG = [
|
|
|
44
47
|
{
|
|
45
48
|
id: "implementation-planning",
|
|
46
49
|
provider: "multileaf",
|
|
50
|
+
universal: true,
|
|
47
51
|
rationale: "A traceable implementation plan can connect project requirements, tests, risks, and validation.",
|
|
48
52
|
permissions: [],
|
|
49
53
|
resources: local([
|
|
@@ -59,6 +63,7 @@ const CATALOG = [
|
|
|
59
63
|
{
|
|
60
64
|
id: "technical-design",
|
|
61
65
|
provider: "multileaf",
|
|
66
|
+
universal: true,
|
|
62
67
|
rationale: "Architecture changes benefit from explicit alternatives, trade-offs, and decision records.",
|
|
63
68
|
permissions: [],
|
|
64
69
|
resources: local([
|
|
@@ -74,6 +79,7 @@ const CATALOG = [
|
|
|
74
79
|
{
|
|
75
80
|
id: "code-review",
|
|
76
81
|
provider: "multileaf",
|
|
82
|
+
universal: true,
|
|
77
83
|
rationale: "A consistent review checklist helps validate changes against this repository's conventions.",
|
|
78
84
|
permissions: [],
|
|
79
85
|
resources: local([
|
|
@@ -164,6 +170,7 @@ const CATALOG = [
|
|
|
164
170
|
{
|
|
165
171
|
id: "verification",
|
|
166
172
|
provider: "multileaf",
|
|
173
|
+
universal: true,
|
|
167
174
|
rationale: "Verification guidance can use the project's detected test runner and quality commands.",
|
|
168
175
|
permissions: [],
|
|
169
176
|
resources: local([
|
|
@@ -241,7 +248,15 @@ const CATALOG = [
|
|
|
241
248
|
];
|
|
242
249
|
export function recommendCapabilities(profile) {
|
|
243
250
|
return CATALOG.flatMap((capability) => {
|
|
244
|
-
const match = capability.matches(profile)
|
|
251
|
+
const match = capability.matches(profile) ??
|
|
252
|
+
(capability.universal
|
|
253
|
+
? {
|
|
254
|
+
evidence: [
|
|
255
|
+
"Workflow capability: applies to any project, with or without detected signals.",
|
|
256
|
+
],
|
|
257
|
+
confidence: 0.9,
|
|
258
|
+
}
|
|
259
|
+
: undefined);
|
|
245
260
|
return match
|
|
246
261
|
? [
|
|
247
262
|
{
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, content-based risk classification for changed paths (OPP-002,
|
|
3
|
+
* REQ-OPP-002). Risk is classified by WHAT changed, never by how much: a
|
|
4
|
+
* documentation-only diff is passive; two lines of authentication code are
|
|
5
|
+
* high. Rules are path-based approximations, deliberately biased toward
|
|
6
|
+
* `high` — a failed or uncertain assessment never implies low risk.
|
|
7
|
+
*
|
|
8
|
+
* The six high-risk items follow the escalation list of the source concept
|
|
9
|
+
* (RDD `review assess`): 1 data/irreversible effects, 2 security,
|
|
10
|
+
* 3 contracts others consume, 4 concurrency, 5 delivery/environment,
|
|
11
|
+
* 6 no test would catch a regression.
|
|
12
|
+
*/
|
|
13
|
+
import type { CommandResult, FileSystem } from "./types.js";
|
|
14
|
+
export type RiskTier = "passive" | "medium" | "high";
|
|
15
|
+
export type RiskCategoryCode = "data_irreversible" | "security" | "consumed_contract" | "concurrency" | "delivery_environment" | "untested_regression";
|
|
16
|
+
export type RiskReason = {
|
|
17
|
+
code: RiskCategoryCode | "agent_escalation";
|
|
18
|
+
path?: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
};
|
|
21
|
+
export type ChangedFile = {
|
|
22
|
+
path: string;
|
|
23
|
+
};
|
|
24
|
+
export type RiskCategory = {
|
|
25
|
+
item: 1 | 2 | 3 | 4 | 5 | 6;
|
|
26
|
+
code: RiskCategoryCode;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const RISK_CATEGORIES: readonly RiskCategory[];
|
|
30
|
+
export type AssessOptions = {
|
|
31
|
+
/**
|
|
32
|
+
* Predicate telling whether a changed source file has a covering test.
|
|
33
|
+
* When absent, the untested_regression category is not asserted
|
|
34
|
+
* (unknown is not low; the tier then rests on the other categories).
|
|
35
|
+
*/
|
|
36
|
+
hasCoveringTest?: (path: string) => boolean;
|
|
37
|
+
};
|
|
38
|
+
export type RiskAssessment = {
|
|
39
|
+
tier: RiskTier;
|
|
40
|
+
reasons: RiskReason[];
|
|
41
|
+
changedPaths: string[];
|
|
42
|
+
};
|
|
43
|
+
export declare function assessRisk(changes: readonly ChangedFile[], options?: AssessOptions): RiskAssessment;
|
|
44
|
+
export declare function runRiskAssess(args: string[], root: string, fs: FileSystem): Promise<CommandResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Agent escalation (RDD parity): an agent may raise — never lower — the tier
|
|
47
|
+
* citing one of the six high-risk items. Item must be 1..6; the reason is a
|
|
48
|
+
* non-empty single line of at most 500 bytes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyEscalation(assessment: RiskAssessment, item: number, reason: string): RiskAssessment;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, content-based risk classification for changed paths (OPP-002,
|
|
3
|
+
* REQ-OPP-002). Risk is classified by WHAT changed, never by how much: a
|
|
4
|
+
* documentation-only diff is passive; two lines of authentication code are
|
|
5
|
+
* high. Rules are path-based approximations, deliberately biased toward
|
|
6
|
+
* `high` — a failed or uncertain assessment never implies low risk.
|
|
7
|
+
*
|
|
8
|
+
* The six high-risk items follow the escalation list of the source concept
|
|
9
|
+
* (RDD `review assess`): 1 data/irreversible effects, 2 security,
|
|
10
|
+
* 3 contracts others consume, 4 concurrency, 5 delivery/environment,
|
|
11
|
+
* 6 no test would catch a regression.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
export const RISK_CATEGORIES = [
|
|
16
|
+
{
|
|
17
|
+
item: 1,
|
|
18
|
+
code: "data_irreversible",
|
|
19
|
+
description: "Data or irreversible effects: migrations, schema, destructive data operations.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
item: 2,
|
|
23
|
+
code: "security",
|
|
24
|
+
description: "Security: authentication, authorization, credentials, secrets, cryptography, sessions, permissions.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
item: 3,
|
|
28
|
+
code: "consumed_contract",
|
|
29
|
+
description: "Contracts others consume: package manifests, public type declarations, API schemas.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
item: 4,
|
|
33
|
+
code: "concurrency",
|
|
34
|
+
description: "Concurrency: workers, pools, locks, queues, and other shared-state primitives.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
item: 5,
|
|
38
|
+
code: "delivery_environment",
|
|
39
|
+
description: "Delivery or environment: CI pipelines, containers, deployment, release and install scripts.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
item: 6,
|
|
43
|
+
code: "untested_regression",
|
|
44
|
+
description: "No test would catch a regression: changed source with no covering test detected.",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Path rules are intentionally broad: over-flagging costs one review, while
|
|
49
|
+
* under-flagging ships unreviewed risk. Test files are excluded before
|
|
50
|
+
* matching so test-only changes stay out of the high categories.
|
|
51
|
+
*/
|
|
52
|
+
const CATEGORY_RULES = [
|
|
53
|
+
{
|
|
54
|
+
code: "data_irreversible",
|
|
55
|
+
pattern: /(^|\/)(migrations?|db\/migrate)\//i,
|
|
56
|
+
},
|
|
57
|
+
{ code: "data_irreversible", pattern: /migration|schema\.prisma$/i },
|
|
58
|
+
{
|
|
59
|
+
code: "security",
|
|
60
|
+
pattern: /(^|\/)(auth|login|credential|secret|token|password|session|permission|acl)\b|(^|\/)(crypto|ssl|tls|jwt)|\bsecurity\b|\.env/i,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: "consumed_contract",
|
|
64
|
+
pattern: /(^|\/)(package|pyproject|cargo|go)\.(json|toml)$|\.d\.ts$|(^|\/)(api|openapi|swagger)(\/|$)|\.graphql$|\.proto$/i,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
code: "concurrency",
|
|
68
|
+
pattern: /worker|pool|lock|queue|mutex|atomic|parallel|thread|scheduler/i,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
code: "delivery_environment",
|
|
72
|
+
pattern: /(^|\/)\.github\/workflows\/|dockerfile|docker-compose|jenkinsfile|gitlab-ci|(^|\/)(ci|deploy|helm|k8s|terraform)(\/|\.|$)|(^|\/)scripts\/(release|install|deploy)|(^|\/)release-|\.npmrc$|^makefile$/i,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
/** Documentation is passive by its own nature (path approximation). */
|
|
76
|
+
const PASSIVE_PATTERN = /\.(md|rst|adoc|txt)|(^|\/)(docs?|documentation)(\/|$)|(^|\/)(license|licence|changelog|notice|authors|contributors)(\.|$)/i;
|
|
77
|
+
const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$|(^|\/)(__tests__|tests?|e2e)(\/|$)/i;
|
|
78
|
+
const SOURCE_FILE_PATTERN = /\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|sql|sh)$/i;
|
|
79
|
+
/**
|
|
80
|
+
* Workflow-managed state (`.aiw/`) is never authored source: excluding it
|
|
81
|
+
* keeps `aiw`'s own lockfiles, checkpoints, and generated artifacts out of
|
|
82
|
+
* the risk picture (a real-flow defect: `.aiw/lock.yml` matched "lock").
|
|
83
|
+
*/
|
|
84
|
+
const WORKFLOW_STATE_PATTERN = /(^|\/)\.aiw\//;
|
|
85
|
+
export function assessRisk(changes, options = {}) {
|
|
86
|
+
const reasons = [];
|
|
87
|
+
let anyCode = false;
|
|
88
|
+
const isTestCovered = options.hasCoveringTest;
|
|
89
|
+
for (const change of changes) {
|
|
90
|
+
if (WORKFLOW_STATE_PATTERN.test(change.path))
|
|
91
|
+
continue;
|
|
92
|
+
const isPassive = PASSIVE_PATTERN.test(change.path);
|
|
93
|
+
const isTest = TEST_FILE_PATTERN.test(change.path);
|
|
94
|
+
if (!isPassive)
|
|
95
|
+
anyCode = true;
|
|
96
|
+
// Passive documentation never ships executable risk; test files are
|
|
97
|
+
// evidence, not surface. Neither triggers the high-risk categories.
|
|
98
|
+
if (isPassive || isTest)
|
|
99
|
+
continue;
|
|
100
|
+
for (const rule of CATEGORY_RULES) {
|
|
101
|
+
if (rule.pattern.test(change.path))
|
|
102
|
+
reasons.push({ code: rule.code, path: change.path });
|
|
103
|
+
}
|
|
104
|
+
if (isTestCovered && SOURCE_FILE_PATTERN.test(change.path) && !isTestCovered(change.path)) {
|
|
105
|
+
reasons.push({ code: "untested_regression", path: change.path });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const tier = reasons.length > 0 ? "high" : anyCode ? "medium" : "passive";
|
|
109
|
+
return { tier, reasons, changedPaths: changes.map((change) => change.path) };
|
|
110
|
+
}
|
|
111
|
+
const CATEGORY_BY_ITEM = new Map(RISK_CATEGORIES.map((category) => [category.item, category]));
|
|
112
|
+
const RISK_ASSESS_SCHEMA = "aiw.risk-assessment/v1";
|
|
113
|
+
const RISK_ASSESS_USAGE = "Usage: aiw risk assess [--base-ref=ref] [--ticket=ID] [--escalate-item=1-6 --escalate-reason=text] [--json]";
|
|
114
|
+
/** Raw git output lines: no per-line trimming — porcelain columns are fixed-width. */
|
|
115
|
+
function gitRawLines(root, args) {
|
|
116
|
+
try {
|
|
117
|
+
const output = execFileSync("git", args, {
|
|
118
|
+
cwd: root,
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
121
|
+
});
|
|
122
|
+
return output
|
|
123
|
+
.split("\n")
|
|
124
|
+
.map((line) => line.replace(/\r$/, ""))
|
|
125
|
+
.filter((line) => line.length > 0);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function gitLines(root, args) {
|
|
132
|
+
return gitRawLines(root, args)?.map((line) => line.trim());
|
|
133
|
+
}
|
|
134
|
+
/** Working-tree changes from `git status --porcelain` (staged, unstaged, untracked). */
|
|
135
|
+
function workingTreeChanges(root) {
|
|
136
|
+
const lines = gitRawLines(root, ["status", "--porcelain", "--untracked-files=all"]);
|
|
137
|
+
if (!lines)
|
|
138
|
+
return undefined;
|
|
139
|
+
return lines.map((line) => {
|
|
140
|
+
const path = line.slice(3).trim();
|
|
141
|
+
const renameArrow = path.indexOf(" -> ");
|
|
142
|
+
return renameArrow === -1 ? path : path.slice(renameArrow + 4);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function committedRangeChanges(root, baseRef) {
|
|
146
|
+
return gitLines(root, ["diff", "--name-only", `${baseRef}...HEAD`]);
|
|
147
|
+
}
|
|
148
|
+
/** Deterministic covering-test probe: sibling *.test.* / *.spec.* variants. */
|
|
149
|
+
function hasCoveringTest(root, fs) {
|
|
150
|
+
return (path) => {
|
|
151
|
+
const directory = path.slice(0, path.lastIndexOf("/"));
|
|
152
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
153
|
+
const dot = base.lastIndexOf(".");
|
|
154
|
+
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
155
|
+
const extension = dot === -1 ? "" : base.slice(dot);
|
|
156
|
+
const candidates = [`${stem}.test${extension}`, `${stem}.spec${extension}`].map((name) => join(root, directory, name));
|
|
157
|
+
return candidates.some((candidate) => fs.exists(candidate));
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parseRiskAssessArgs(rest) {
|
|
161
|
+
const parsed = {};
|
|
162
|
+
for (const argument of rest) {
|
|
163
|
+
const match = argument.match(/^--([a-z-]+)=(.*)$/);
|
|
164
|
+
if (!match)
|
|
165
|
+
return undefined;
|
|
166
|
+
const [, name, value] = match;
|
|
167
|
+
if (name === "base-ref")
|
|
168
|
+
parsed.baseRef = value;
|
|
169
|
+
else if (name === "ticket")
|
|
170
|
+
parsed.ticket = value;
|
|
171
|
+
else if (name === "escalate-item") {
|
|
172
|
+
if (!/^\d+$/.test(value))
|
|
173
|
+
return undefined;
|
|
174
|
+
parsed.escalateItem = Number(value);
|
|
175
|
+
}
|
|
176
|
+
else if (name === "escalate-reason")
|
|
177
|
+
parsed.escalateReason = value;
|
|
178
|
+
else
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if ((parsed.escalateItem === undefined) !== (parsed.escalateReason === undefined) ||
|
|
182
|
+
(parsed.ticket !== undefined && parsed.ticket.length === 0))
|
|
183
|
+
return undefined;
|
|
184
|
+
return parsed;
|
|
185
|
+
}
|
|
186
|
+
export async function runRiskAssess(args, root, fs) {
|
|
187
|
+
const jsonRequested = args.includes("--json");
|
|
188
|
+
const parsed = parseRiskAssessArgs(args.filter((argument) => argument !== "--json"));
|
|
189
|
+
if (!parsed)
|
|
190
|
+
return { exitCode: 1, error: RISK_ASSESS_USAGE };
|
|
191
|
+
const insideWorkTree = gitLines(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
192
|
+
if (!insideWorkTree || insideWorkTree[0] !== "true")
|
|
193
|
+
return {
|
|
194
|
+
exitCode: 1,
|
|
195
|
+
error: "risk assess requires a git repository; run it inside one or pass --base-ref for a committed range.",
|
|
196
|
+
};
|
|
197
|
+
const changedPaths = parsed.baseRef
|
|
198
|
+
? committedRangeChanges(root, parsed.baseRef)
|
|
199
|
+
: workingTreeChanges(root);
|
|
200
|
+
if (!changedPaths)
|
|
201
|
+
return {
|
|
202
|
+
exitCode: 1,
|
|
203
|
+
error: `git failed to enumerate changes${parsed.baseRef ? ` for --base-ref=${parsed.baseRef}` : ""}; verify the reference exists.`,
|
|
204
|
+
};
|
|
205
|
+
let assessment = assessRisk(changedPaths.map((path) => ({ path })), { hasCoveringTest: hasCoveringTest(root, fs) });
|
|
206
|
+
if (parsed.escalateItem !== undefined && parsed.escalateReason !== undefined) {
|
|
207
|
+
try {
|
|
208
|
+
assessment = applyEscalation(assessment, parsed.escalateItem, parsed.escalateReason);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const riskDue = assessment.tier === "high";
|
|
215
|
+
const activeWorkflow = fs.exists(join(root, ".aiw", "workflow.yml"));
|
|
216
|
+
const nextCommand = riskDue && !activeWorkflow ? "aiw workflow start" : undefined;
|
|
217
|
+
const nextAction = riskDue && activeWorkflow
|
|
218
|
+
? "Continue the active workflow: gate and approve the stages this change touches."
|
|
219
|
+
: undefined;
|
|
220
|
+
if (jsonRequested) {
|
|
221
|
+
const envelope = {
|
|
222
|
+
schema: RISK_ASSESS_SCHEMA,
|
|
223
|
+
risk: assessment.tier,
|
|
224
|
+
reasons: assessment.reasons,
|
|
225
|
+
changed_paths: assessment.changedPaths.length,
|
|
226
|
+
risk_due: riskDue,
|
|
227
|
+
};
|
|
228
|
+
if (parsed.ticket)
|
|
229
|
+
envelope.ticket = parsed.ticket;
|
|
230
|
+
if (nextCommand)
|
|
231
|
+
envelope.next_command = nextCommand;
|
|
232
|
+
if (nextAction)
|
|
233
|
+
envelope.next_action = nextAction;
|
|
234
|
+
return { exitCode: 0, output: JSON.stringify(envelope, null, 2) };
|
|
235
|
+
}
|
|
236
|
+
const lines = [`risk: ${assessment.tier}`];
|
|
237
|
+
for (const reason of assessment.reasons)
|
|
238
|
+
lines.push(` - ${reason.code}${reason.path ? `: ${reason.path}` : reason.detail ? ` (${reason.detail})` : ""}`);
|
|
239
|
+
const next = nextCommand
|
|
240
|
+
? `risk due: yes -> ${nextCommand}`
|
|
241
|
+
: nextAction
|
|
242
|
+
? `risk due: yes -> ${nextAction}`
|
|
243
|
+
: "risk due: no";
|
|
244
|
+
lines.push(next);
|
|
245
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Agent escalation (RDD parity): an agent may raise — never lower — the tier
|
|
249
|
+
* citing one of the six high-risk items. Item must be 1..6; the reason is a
|
|
250
|
+
* non-empty single line of at most 500 bytes.
|
|
251
|
+
*/
|
|
252
|
+
export function applyEscalation(assessment, item, reason) {
|
|
253
|
+
if (!Number.isInteger(item) || item < 1 || item > 6)
|
|
254
|
+
throw new Error("Escalation item must be an integer from 1 to 6.");
|
|
255
|
+
if (reason.length === 0)
|
|
256
|
+
throw new Error("Escalation reason must not be empty.");
|
|
257
|
+
if (reason.length > 500)
|
|
258
|
+
throw new Error("Escalation reason must be at most 500 bytes on one line.");
|
|
259
|
+
if (/[\r\n]/.test(reason))
|
|
260
|
+
throw new Error("Escalation reason must be a single line.");
|
|
261
|
+
const category = CATEGORY_BY_ITEM.get(item);
|
|
262
|
+
if (!category)
|
|
263
|
+
throw new Error("Escalation item must be an integer from 1 to 6.");
|
|
264
|
+
const escalation = {
|
|
265
|
+
code: "agent_escalation",
|
|
266
|
+
detail: `item ${item}: ${category.code} — ${reason}`,
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
...assessment,
|
|
270
|
+
tier: "high",
|
|
271
|
+
reasons: [...assessment.reasons, escalation],
|
|
272
|
+
};
|
|
273
|
+
}
|