@multileaf/ai-workflow 0.10.0 → 0.12.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 +29 -0
- package/dist/blocking-reasons.d.ts +1 -1
- package/dist/blocking-reasons.js +42 -0
- package/dist/digest.d.ts +48 -0
- package/dist/digest.js +106 -0
- package/dist/eval/artifact-evals.d.ts +42 -0
- package/dist/eval/artifact-evals.js +141 -0
- package/dist/generator.js +1 -1
- package/dist/human-approval.d.ts +16 -3
- package/dist/human-approval.js +56 -12
- package/dist/orchestration.d.ts +38 -1
- package/dist/orchestration.js +82 -3
- package/dist/quality-gates.d.ts +1 -1
- package/dist/quality-gates.js +23 -0
- package/dist/risk-assess.d.ts +4 -0
- package/dist/risk-assess.js +5 -1
- package/dist/standards.d.ts +34 -0
- package/dist/standards.js +159 -0
- package/dist/types.d.ts +15 -1
- package/dist/types.js +24 -0
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +472 -62
- package/docs/architecture.md +1 -1
- package/docs/backlog.md +10 -3
- package/docs/continuation-inventory.md +20 -13
- package/docs/opportunities/OPP-009-process-entry-points.md +56 -0
- package/docs/opportunities/OPP-010-orchestration-worktree-isolation.md +52 -0
- package/docs/opportunities/OPP-011-evals-redteam-generated-artifacts.md +56 -0
- package/docs/opportunities/OPP-012-standards-discovery.md +51 -0
- package/docs/opportunities/OPP-013-read-optimized-artifacts-digest-layer.md +52 -0
- package/docs/opportunities/OPP-014-paired-verification-contract.md +54 -0
- package/docs/opportunities/OPP-015-token-recovery-governance.md +52 -0
- package/docs/opportunities/README.md +25 -10
- package/package.json +1 -1
- package/resources/skills/implementation-planning.md +1 -1
- package/resources/templates/brainstorm.md +0 -1
- package/resources/templates/decision-record.md +0 -1
- package/resources/templates/implementation-evidence.md +1 -1
- package/resources/templates/implementation-plan.md +2 -1
- package/resources/templates/migration-report.md +0 -1
- package/resources/templates/security-review.md +0 -1
- package/resources/templates/task.md +0 -1
- package/resources/templates/technical-design.md +0 -1
- package/resources/templates/test-plan.md +0 -1
- package/resources/templates/verification-report.md +2 -1
package/dist/human-approval.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { parseDocument, stringify } from "yaml";
|
|
3
|
-
import { HUMAN_APPROVAL_STAGES } from "./types.js";
|
|
3
|
+
import { HUMAN_APPROVAL_STAGES, PROCESS_IDS, PROCESSES, } from "./types.js";
|
|
4
|
+
export { PROCESS_IDS };
|
|
4
5
|
export const APPROVAL_STAGES = HUMAN_APPROVAL_STAGES;
|
|
5
6
|
/** The lane that currently governs stage commands: escalation moves light → full. */
|
|
6
7
|
export function effectiveLane(session) {
|
|
@@ -19,6 +20,8 @@ const STAGE_DEFINITIONS = {
|
|
|
19
20
|
verification: { optional: false, requiresQualityGate: true },
|
|
20
21
|
review: { optional: false, requiresQualityGate: true },
|
|
21
22
|
traceability: { optional: false, requiresQualityGate: true },
|
|
23
|
+
diagnosis: { optional: false, requiresQualityGate: true },
|
|
24
|
+
assessment: { optional: false, requiresQualityGate: true },
|
|
22
25
|
};
|
|
23
26
|
const APPROVAL_STATUSES = ["draft", "pending-human", "approved", "rejected", "skipped", "stale"];
|
|
24
27
|
const QUALITY_GATE_RESULTS = ["not-run", "passed", "failed", "not-applicable"];
|
|
@@ -29,7 +32,7 @@ const APPROVAL_RECORD_KEYS = new Set([
|
|
|
29
32
|
"reason",
|
|
30
33
|
"updatedAt",
|
|
31
34
|
]);
|
|
32
|
-
const LEDGER_KEYS = new Set(["schema", "workflowId", "stages", "lane", "laneReason"]);
|
|
35
|
+
const LEDGER_KEYS = new Set(["schema", "workflowId", "stages", "lane", "laneReason", "process"]);
|
|
33
36
|
const SESSION_KEYS = new Set([
|
|
34
37
|
"schema",
|
|
35
38
|
"workflowId",
|
|
@@ -42,6 +45,7 @@ const SESSION_KEYS = new Set([
|
|
|
42
45
|
"laneReason",
|
|
43
46
|
"laneEscalatedAt",
|
|
44
47
|
"laneEscalationReason",
|
|
48
|
+
"abandonedReason",
|
|
45
49
|
]);
|
|
46
50
|
export function createApprovalLedger(workflowId, options = {}) {
|
|
47
51
|
assertWorkflowId(workflowId);
|
|
@@ -54,8 +58,18 @@ export function createApprovalLedger(workflowId, options = {}) {
|
|
|
54
58
|
...(options.lane === "light" && options.laneReason
|
|
55
59
|
? { lane: "light", laneReason: options.laneReason }
|
|
56
60
|
: {}),
|
|
61
|
+
...(options.process && options.process !== "feature" ? { process: options.process } : {}),
|
|
57
62
|
};
|
|
58
63
|
}
|
|
64
|
+
/** The ordered stage list for the ledger's process (OPP-009); feature when absent. */
|
|
65
|
+
export function processStages(ledger) {
|
|
66
|
+
if (!ledger.process || ledger.process === "feature")
|
|
67
|
+
return [...APPROVAL_STAGES].filter((stage) => stage !== "diagnosis" && stage !== "assessment");
|
|
68
|
+
return [...PROCESSES[ledger.process].stages];
|
|
69
|
+
}
|
|
70
|
+
export function isWorkflowProcess(value) {
|
|
71
|
+
return PROCESS_IDS.includes(value);
|
|
72
|
+
}
|
|
59
73
|
export function serializeWorkflowSession(session) {
|
|
60
74
|
validateWorkflowSession(session);
|
|
61
75
|
return stringify(session, { lineWidth: 0 });
|
|
@@ -72,8 +86,8 @@ export function parseWorkflowSession(content) {
|
|
|
72
86
|
throw new Error(`Workflow session has unknown field: ${key}.`);
|
|
73
87
|
if (typeof value.workflowId !== "string")
|
|
74
88
|
throw new Error("Workflow session requires a workflowId.");
|
|
75
|
-
if (value.status !== "active" && value.status !== "complete")
|
|
76
|
-
throw new Error("Workflow session status must be active or
|
|
89
|
+
if (value.status !== "active" && value.status !== "complete" && value.status !== "abandoned")
|
|
90
|
+
throw new Error("Workflow session status must be active, complete, or abandoned.");
|
|
77
91
|
if (typeof value.startedAt !== "string" || typeof value.updatedAt !== "string")
|
|
78
92
|
throw new Error("Workflow session requires start and update timestamps.");
|
|
79
93
|
if (value.ticket !== undefined && typeof value.ticket !== "string")
|
|
@@ -96,6 +110,9 @@ export function parseWorkflowSession(content) {
|
|
|
96
110
|
const laneEscalationReason = value.laneEscalationReason;
|
|
97
111
|
if (laneEscalationReason !== undefined && typeof laneEscalationReason !== "string")
|
|
98
112
|
throw new Error("Workflow session lane escalation reason must be a string.");
|
|
113
|
+
const abandonedReason = value.abandonedReason;
|
|
114
|
+
if (abandonedReason !== undefined && typeof abandonedReason !== "string")
|
|
115
|
+
throw new Error("Workflow session abandonment reason must be a string.");
|
|
99
116
|
const session = {
|
|
100
117
|
schema: 1,
|
|
101
118
|
workflowId: value.workflowId,
|
|
@@ -108,6 +125,7 @@ export function parseWorkflowSession(content) {
|
|
|
108
125
|
...(laneReason === undefined ? {} : { laneReason }),
|
|
109
126
|
...(laneEscalatedAt === undefined ? {} : { laneEscalatedAt }),
|
|
110
127
|
...(laneEscalationReason === undefined ? {} : { laneEscalationReason }),
|
|
128
|
+
...(abandonedReason === undefined ? {} : { abandonedReason }),
|
|
111
129
|
};
|
|
112
130
|
validateWorkflowSession(session);
|
|
113
131
|
return session;
|
|
@@ -119,8 +137,9 @@ export function stageFingerprint(content) {
|
|
|
119
137
|
return `sha256-${createHash("sha256").update(content).digest("hex")}`;
|
|
120
138
|
}
|
|
121
139
|
export function getUnresolvedPrerequisites(ledger, stage) {
|
|
122
|
-
const
|
|
123
|
-
|
|
140
|
+
const stages = processStages(ledger);
|
|
141
|
+
const stageIndex = stages.indexOf(stage);
|
|
142
|
+
return stages.slice(0, stageIndex).filter((prerequisite) => {
|
|
124
143
|
const status = ledger.stages[prerequisite]?.status;
|
|
125
144
|
return status !== "approved" && status !== "skipped";
|
|
126
145
|
});
|
|
@@ -191,14 +210,23 @@ export function skipOptionalStage(ledger, stage, input) {
|
|
|
191
210
|
}
|
|
192
211
|
export function invalidateStageAndDescendants(ledger, stage, updatedAt) {
|
|
193
212
|
assertTimestamp(updatedAt);
|
|
194
|
-
const stageIndex = APPROVAL_STAGES.indexOf(stage);
|
|
195
213
|
const stages = { ...ledger.stages };
|
|
196
|
-
|
|
214
|
+
const ordered = processStages(ledger);
|
|
215
|
+
const stageIndex = ordered.indexOf(stage);
|
|
216
|
+
for (const affected of ordered.slice(stageIndex)) {
|
|
197
217
|
const record = stages[affected];
|
|
198
218
|
if (record && record.status !== "draft")
|
|
199
219
|
stages[affected] = { ...record, status: "stale", updatedAt };
|
|
200
220
|
}
|
|
201
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
schema: 1,
|
|
223
|
+
workflowId: ledger.workflowId,
|
|
224
|
+
stages,
|
|
225
|
+
...(ledger.lane === "light" && ledger.laneReason
|
|
226
|
+
? { lane: ledger.lane, laneReason: ledger.laneReason }
|
|
227
|
+
: {}),
|
|
228
|
+
...(ledger.process && ledger.process !== "feature" ? { process: ledger.process } : {}),
|
|
229
|
+
};
|
|
202
230
|
}
|
|
203
231
|
export function serializeApprovalLedger(ledger) {
|
|
204
232
|
validateLedger(ledger);
|
|
@@ -210,6 +238,7 @@ export function serializeApprovalLedger(ledger) {
|
|
|
210
238
|
...(ledger.lane === "light" && ledger.laneReason
|
|
211
239
|
? { lane: ledger.lane, laneReason: ledger.laneReason }
|
|
212
240
|
: {}),
|
|
241
|
+
...(ledger.process && ledger.process !== "feature" ? { process: ledger.process } : {}),
|
|
213
242
|
}, { lineWidth: 0 });
|
|
214
243
|
}
|
|
215
244
|
export function parseApprovalLedger(content) {
|
|
@@ -233,10 +262,14 @@ export function parseApprovalLedger(content) {
|
|
|
233
262
|
throw new Error(`Approval ledger has unknown workflow stage: ${stageName}.`);
|
|
234
263
|
stages[stageName] = parseStageRecord(stageName, record);
|
|
235
264
|
}
|
|
265
|
+
const process = value.process === undefined ? undefined : String(value.process);
|
|
266
|
+
if (process !== undefined && !isWorkflowProcess(process))
|
|
267
|
+
throw new Error("Approval ledger process must be a known workflow process.");
|
|
236
268
|
const ledger = {
|
|
237
269
|
schema: 1,
|
|
238
270
|
workflowId: value.workflowId,
|
|
239
271
|
stages,
|
|
272
|
+
...(process && process !== "feature" ? { process: process } : {}),
|
|
240
273
|
...(value.lane === "light" && typeof value.laneReason === "string"
|
|
241
274
|
? { lane: "light", laneReason: value.laneReason }
|
|
242
275
|
: {}),
|
|
@@ -266,8 +299,10 @@ function validateWorkflowSession(session) {
|
|
|
266
299
|
if (session.schema !== 1)
|
|
267
300
|
throw new Error("Workflow session schema must be version 1.");
|
|
268
301
|
assertWorkflowId(session.workflowId);
|
|
269
|
-
if (session.status !== "active" &&
|
|
270
|
-
|
|
302
|
+
if (session.status !== "active" &&
|
|
303
|
+
session.status !== "complete" &&
|
|
304
|
+
session.status !== "abandoned")
|
|
305
|
+
throw new Error("Workflow session status must be active, complete, or abandoned.");
|
|
271
306
|
assertTimestamp(session.startedAt);
|
|
272
307
|
assertTimestamp(session.updatedAt);
|
|
273
308
|
if (session.lane !== undefined && session.lane !== "light")
|
|
@@ -281,12 +316,21 @@ function validateWorkflowSession(session) {
|
|
|
281
316
|
if (!session.laneEscalationReason)
|
|
282
317
|
throw new Error("Lane escalation requires a reason.");
|
|
283
318
|
}
|
|
319
|
+
if (session.status === "abandoned" && !session.abandonedReason)
|
|
320
|
+
throw new Error("Abandoned sessions require an abandonment reason.");
|
|
284
321
|
}
|
|
285
322
|
function withStage(ledger, stage, record) {
|
|
286
323
|
return {
|
|
287
324
|
schema: 1,
|
|
288
325
|
workflowId: ledger.workflowId,
|
|
289
326
|
stages: { ...ledger.stages, [stage]: record },
|
|
327
|
+
// Preserve lane/process provenance: dropping them here silently stripped
|
|
328
|
+
// the process from the ledger on every stage transition (found by the
|
|
329
|
+
// OPP-009 sweep — the bug made bugfix workflows gate like feature ones).
|
|
330
|
+
...(ledger.lane === "light" && ledger.laneReason
|
|
331
|
+
? { lane: ledger.lane, laneReason: ledger.laneReason }
|
|
332
|
+
: {}),
|
|
333
|
+
...(ledger.process && ledger.process !== "feature" ? { process: ledger.process } : {}),
|
|
290
334
|
};
|
|
291
335
|
}
|
|
292
336
|
function isApprovalStage(value) {
|
|
@@ -356,7 +400,7 @@ function validateLedger(ledger) {
|
|
|
356
400
|
throw new Error(`Approval ledger has unknown workflow stage: ${stage}.`);
|
|
357
401
|
parseStageRecord(stage, record);
|
|
358
402
|
}
|
|
359
|
-
for (const stage of
|
|
403
|
+
for (const stage of processStages(ledger)) {
|
|
360
404
|
const status = ledger.stages[stage]?.status;
|
|
361
405
|
if (status === "pending-human" ||
|
|
362
406
|
status === "approved" ||
|
package/dist/orchestration.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type OrchestrationTask = {
|
|
|
5
5
|
agent: OrchestrationAgent;
|
|
6
6
|
prompt: string;
|
|
7
7
|
dependsOn: string[];
|
|
8
|
+
/** Set by `orchestrate --isolate`: the per-task git worktree the adapter must run in (OPP-010). */
|
|
9
|
+
worktree?: string;
|
|
8
10
|
};
|
|
9
11
|
export type OrchestrationPlan = {
|
|
10
12
|
schema: 1;
|
|
@@ -15,7 +17,36 @@ export type OrchestrationTaskResult = {
|
|
|
15
17
|
id: string;
|
|
16
18
|
status: "passed" | "failed" | "blocked";
|
|
17
19
|
detail: string;
|
|
20
|
+
/** OPP-010 isolation evidence, present when the plan ran with --isolate. */
|
|
21
|
+
worktree?: string;
|
|
22
|
+
baseCommit?: string;
|
|
23
|
+
finalCommit?: string;
|
|
18
24
|
};
|
|
25
|
+
export type IsolationOptions = {
|
|
26
|
+
/** Repository root the plan runs in. */
|
|
27
|
+
root: string;
|
|
28
|
+
/** AI Workflow state directory (usually `.aiw`). */
|
|
29
|
+
aiw: string;
|
|
30
|
+
planId: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function worktreeDirectory(aiw: string, planId: string, taskId: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* OPP-010: create one branch-per-task worktree from the current HEAD so
|
|
35
|
+
* parallel tasks never share a working tree. Branches are named
|
|
36
|
+
* `aiw/<plan>/<task>` so later human merge is explicit.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createTaskWorktrees(root: string, aiw: string, plan: OrchestrationPlan): Map<string, {
|
|
39
|
+
worktree: string;
|
|
40
|
+
baseCommit: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** Final commit recorded from an isolated worktree (evidence for the checkpoint). */
|
|
43
|
+
export declare function worktreeFinalCommit(worktree: string): string | undefined;
|
|
44
|
+
/** Remove isolated worktrees; failed-task worktrees are kept unless forced. */
|
|
45
|
+
export declare function removeWorktrees(root: string, paths: Iterable<string>, options?: {
|
|
46
|
+
force?: boolean;
|
|
47
|
+
}): void;
|
|
48
|
+
/** List isolated worktrees managed under `<aiw>/worktrees` (for cleanup/doctor). */
|
|
49
|
+
export declare function listManagedWorktrees(root: string, aiw: string): string[];
|
|
19
50
|
export interface AgentOrchestrator {
|
|
20
51
|
run(task: OrchestrationTask): Promise<{
|
|
21
52
|
success: boolean;
|
|
@@ -24,7 +55,13 @@ export interface AgentOrchestrator {
|
|
|
24
55
|
}
|
|
25
56
|
export declare function parseOrchestrationPlan(content: string): OrchestrationPlan;
|
|
26
57
|
export declare function orchestrationWaves(plan: OrchestrationPlan): string[][];
|
|
27
|
-
export declare function executeOrchestrationPlan(plan: OrchestrationPlan, orchestrator: AgentOrchestrator, maxParallel: number
|
|
58
|
+
export declare function executeOrchestrationPlan(plan: OrchestrationPlan, orchestrator: AgentOrchestrator, maxParallel: number, isolation?: {
|
|
59
|
+
options: IsolationOptions;
|
|
60
|
+
worktrees: Map<string, {
|
|
61
|
+
worktree: string;
|
|
62
|
+
baseCommit: string;
|
|
63
|
+
}>;
|
|
64
|
+
}): Promise<OrchestrationTaskResult[]>;
|
|
28
65
|
export declare function validateOrchestrationParallelism(value: number): number;
|
|
29
66
|
export declare function serializeOrchestrationPreview(plan: OrchestrationPlan): string;
|
|
30
67
|
export declare function serializeOrchestrationResults(plan: OrchestrationPlan, results: OrchestrationTaskResult[]): string;
|
package/dist/orchestration.js
CHANGED
|
@@ -1,4 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
1
4
|
export const ORCHESTRATION_AGENTS = ["codex", "claude", "gemini"];
|
|
5
|
+
function git(root, args) {
|
|
6
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
|
|
7
|
+
}
|
|
8
|
+
export function worktreeDirectory(aiw, planId, taskId) {
|
|
9
|
+
return join(aiw, "worktrees", planId, taskId);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* OPP-010: create one branch-per-task worktree from the current HEAD so
|
|
13
|
+
* parallel tasks never share a working tree. Branches are named
|
|
14
|
+
* `aiw/<plan>/<task>` so later human merge is explicit.
|
|
15
|
+
*/
|
|
16
|
+
export function createTaskWorktrees(root, aiw, plan) {
|
|
17
|
+
const created = new Map();
|
|
18
|
+
const head = git(root, ["rev-parse", "HEAD"]);
|
|
19
|
+
for (const task of plan.tasks) {
|
|
20
|
+
const worktree = worktreeDirectory(aiw, plan.id, task.id);
|
|
21
|
+
const branch = `aiw/${plan.id}/${task.id}`;
|
|
22
|
+
git(root, ["worktree", "add", "-b", branch, worktree, head]);
|
|
23
|
+
created.set(task.id, { worktree, baseCommit: head });
|
|
24
|
+
}
|
|
25
|
+
return created;
|
|
26
|
+
}
|
|
27
|
+
/** Final commit recorded from an isolated worktree (evidence for the checkpoint). */
|
|
28
|
+
export function worktreeFinalCommit(worktree) {
|
|
29
|
+
try {
|
|
30
|
+
return execFileSync("git", ["-C", worktree, "rev-parse", "HEAD"], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Remove isolated worktrees; failed-task worktrees are kept unless forced. */
|
|
39
|
+
export function removeWorktrees(root, paths, options = {}) {
|
|
40
|
+
for (const path of paths) {
|
|
41
|
+
try {
|
|
42
|
+
git(root, ["worktree", "remove", path, options.force ? "--force" : "--force"]);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A worktree whose branch has unmerged changes refuses plain removal;
|
|
46
|
+
// callers decide whether to force. Removal failures are non-fatal:
|
|
47
|
+
// `aiw orchestrate --cleanup` can always retry.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** List isolated worktrees managed under `<aiw>/worktrees` (for cleanup/doctor). */
|
|
52
|
+
export function listManagedWorktrees(root, aiw) {
|
|
53
|
+
try {
|
|
54
|
+
const output = git(root, ["worktree", "list", "--porcelain"]);
|
|
55
|
+
// git reports fully resolved paths (macOS /tmp is a symlink to /private/tmp)
|
|
56
|
+
const prefix = realpathSync(join(aiw, "worktrees")) + "/";
|
|
57
|
+
return output
|
|
58
|
+
.split("\n")
|
|
59
|
+
.filter((line) => line.startsWith("worktree "))
|
|
60
|
+
.map((line) => line.slice("worktree ".length))
|
|
61
|
+
.filter((path) => path.startsWith(prefix));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
2
67
|
export function parseOrchestrationPlan(content) {
|
|
3
68
|
if (/\r(?!\n)/.test(content))
|
|
4
69
|
throw new Error("Orchestration plan contains unsupported characters.");
|
|
@@ -59,7 +124,7 @@ export function orchestrationWaves(plan) {
|
|
|
59
124
|
}
|
|
60
125
|
return waves;
|
|
61
126
|
}
|
|
62
|
-
export async function executeOrchestrationPlan(plan, orchestrator, maxParallel) {
|
|
127
|
+
export async function executeOrchestrationPlan(plan, orchestrator, maxParallel, isolation) {
|
|
63
128
|
validateOrchestrationParallelism(maxParallel);
|
|
64
129
|
const tasks = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
65
130
|
const results = new Map();
|
|
@@ -75,11 +140,20 @@ export async function executeOrchestrationPlan(plan, orchestrator, maxParallel)
|
|
|
75
140
|
return;
|
|
76
141
|
}
|
|
77
142
|
try {
|
|
78
|
-
const
|
|
143
|
+
const isolated = isolation?.worktrees.get(id);
|
|
144
|
+
const outcome = await orchestrator.run(isolated ? { ...task, worktree: isolated.worktree } : task);
|
|
145
|
+
const base = isolation?.worktrees.get(id);
|
|
79
146
|
results.set(id, {
|
|
80
147
|
id,
|
|
81
148
|
status: outcome.success ? "passed" : "failed",
|
|
82
149
|
detail: safeDetail(outcome.detail),
|
|
150
|
+
...(base
|
|
151
|
+
? {
|
|
152
|
+
worktree: base.worktree,
|
|
153
|
+
baseCommit: base.baseCommit,
|
|
154
|
+
finalCommit: worktreeFinalCommit(base.worktree),
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
83
157
|
});
|
|
84
158
|
}
|
|
85
159
|
catch (error) {
|
|
@@ -106,7 +180,12 @@ export function serializeOrchestrationPreview(plan) {
|
|
|
106
180
|
}
|
|
107
181
|
export function serializeOrchestrationResults(plan, results) {
|
|
108
182
|
return `schema: 1\nplan: ${plan.id}\nresults:\n${results
|
|
109
|
-
.map((result) =>
|
|
183
|
+
.map((result) => {
|
|
184
|
+
const isolation = result.worktree
|
|
185
|
+
? `\n worktree: ${JSON.stringify(result.worktree)}\n baseCommit: ${JSON.stringify(result.baseCommit ?? "")}\n finalCommit: ${JSON.stringify(result.finalCommit ?? "")}`
|
|
186
|
+
: "";
|
|
187
|
+
return ` - id: ${result.id}\n status: ${result.status}\n detail: ${JSON.stringify(result.detail)}${isolation}`;
|
|
188
|
+
})
|
|
110
189
|
.join("\n")}\n`;
|
|
111
190
|
}
|
|
112
191
|
function assertPlanGraph(tasks) {
|
package/dist/quality-gates.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type QualityGateStage = "brainstorming" | "specification" | "technical-design" | "plan" | "implementation" | "verification" | "review" | "traceability";
|
|
1
|
+
export type QualityGateStage = "brainstorming" | "specification" | "technical-design" | "plan" | "implementation" | "verification" | "review" | "traceability" | "diagnosis" | "assessment";
|
|
2
2
|
export declare function evaluateQualityGate(stage: QualityGateStage, content: string): string[];
|
package/dist/quality-gates.js
CHANGED
|
@@ -30,6 +30,29 @@ export function evaluateQualityGate(stage, content) {
|
|
|
30
30
|
issues.push("Requirement acceptance criteria must use Given, When, and Then.");
|
|
31
31
|
return issues;
|
|
32
32
|
}
|
|
33
|
+
if (stage === "diagnosis") {
|
|
34
|
+
const issues = [];
|
|
35
|
+
if (!/## Reproduction/.test(content) || !/Command:/.test(content) || !/Observed:/.test(content))
|
|
36
|
+
issues.push("Diagnosis must include a Reproduction with Command and Observed.");
|
|
37
|
+
if (!/## Expected/.test(content) || section(content, "Expected") === undefined)
|
|
38
|
+
issues.push("Diagnosis must state the Expected behavior.");
|
|
39
|
+
if (!/## Root Cause/.test(content) || section(content, "Root Cause") === undefined)
|
|
40
|
+
issues.push("Diagnosis must state an evidence-backed Root Cause.");
|
|
41
|
+
if (!/## Scope/.test(content) || section(content, "Scope") === undefined)
|
|
42
|
+
issues.push("Diagnosis must state the fix Scope.");
|
|
43
|
+
return issues;
|
|
44
|
+
}
|
|
45
|
+
if (stage === "assessment") {
|
|
46
|
+
const issues = [];
|
|
47
|
+
if (!/## Evidence/.test(content) || section(content, "Evidence") === undefined)
|
|
48
|
+
issues.push("Assessment must include attributed Evidence.");
|
|
49
|
+
const outcome = /\b(go|clarify|stop)\b/i.exec(section(content, "Outcome") ?? "");
|
|
50
|
+
if (!outcome)
|
|
51
|
+
issues.push("Assessment Outcome must be go, clarify, or stop.");
|
|
52
|
+
if (!/## Rationale/.test(content) || section(content, "Rationale") === undefined)
|
|
53
|
+
issues.push("Assessment must state the Rationale.");
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
33
56
|
if (stage === "technical-design")
|
|
34
57
|
return requireSections(content, "Technical Design", [
|
|
35
58
|
"Context",
|
package/dist/risk-assess.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export type RiskAssessment = {
|
|
|
41
41
|
changedPaths: string[];
|
|
42
42
|
};
|
|
43
43
|
export declare function assessRisk(changes: readonly ChangedFile[], options?: AssessOptions): RiskAssessment;
|
|
44
|
+
/** Working-tree changes for advisory classification (exported for OPP-015 lane suggestion). */
|
|
45
|
+
export declare function collectWorkingTreeChanges(root: string): string[] | undefined;
|
|
46
|
+
/** Deterministic covering-test probe: sibling *.test.* / *.spec.* variants. */
|
|
47
|
+
export declare function hasCoveringTest(root: string, fs: FileSystem): (path: string) => boolean;
|
|
44
48
|
export declare function runRiskAssess(args: string[], root: string, fs: FileSystem): Promise<CommandResult>;
|
|
45
49
|
/**
|
|
46
50
|
* Agent escalation (RDD parity): an agent may raise — never lower — the tier
|
package/dist/risk-assess.js
CHANGED
|
@@ -145,8 +145,12 @@ function workingTreeChanges(root) {
|
|
|
145
145
|
function committedRangeChanges(root, baseRef) {
|
|
146
146
|
return gitLines(root, ["diff", "--name-only", `${baseRef}...HEAD`]);
|
|
147
147
|
}
|
|
148
|
+
/** Working-tree changes for advisory classification (exported for OPP-015 lane suggestion). */
|
|
149
|
+
export function collectWorkingTreeChanges(root) {
|
|
150
|
+
return workingTreeChanges(root);
|
|
151
|
+
}
|
|
148
152
|
/** Deterministic covering-test probe: sibling *.test.* / *.spec.* variants. */
|
|
149
|
-
function hasCoveringTest(root, fs) {
|
|
153
|
+
export function hasCoveringTest(root, fs) {
|
|
150
154
|
return (path) => {
|
|
151
155
|
const directory = path.slice(0, path.lastIndexOf("/"));
|
|
152
156
|
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPP-012: Standards discovery — deterministic extraction of codebase
|
|
3
|
+
* conventions into versioned, evidence-cited standards documents.
|
|
4
|
+
*
|
|
5
|
+
* The floor is fully deterministic and offline: conventions are detected from
|
|
6
|
+
* repository structure and content (test layout, naming, error classes), each
|
|
7
|
+
* statement carries file:line evidence and an occurrence count, and every
|
|
8
|
+
* standard starts `inferred` until a human confirms or rejects it. Semantic
|
|
9
|
+
* retrieval (OPP-008) may later widen coverage; it never bypasses confirmation.
|
|
10
|
+
*/
|
|
11
|
+
import type { FileSystem } from "./types.js";
|
|
12
|
+
export type StandardStatus = "inferred" | "confirmed" | "rejected";
|
|
13
|
+
export type StandardEvidence = {
|
|
14
|
+
path: string;
|
|
15
|
+
line: number;
|
|
16
|
+
};
|
|
17
|
+
export type StandardDocument = {
|
|
18
|
+
topic: string;
|
|
19
|
+
status: StandardStatus;
|
|
20
|
+
statements: {
|
|
21
|
+
convention: string;
|
|
22
|
+
occurrences: number;
|
|
23
|
+
evidence: StandardEvidence[];
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
export declare function extractStandards(root: string, fs: FileSystem): StandardDocument[];
|
|
27
|
+
export type StandardsIndex = {
|
|
28
|
+
schema: 1;
|
|
29
|
+
standards: Record<string, {
|
|
30
|
+
status: StandardStatus;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
export declare function parseStandardsIndex(content: string): StandardsIndex;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPP-012: Standards discovery — deterministic extraction of codebase
|
|
3
|
+
* conventions into versioned, evidence-cited standards documents.
|
|
4
|
+
*
|
|
5
|
+
* The floor is fully deterministic and offline: conventions are detected from
|
|
6
|
+
* repository structure and content (test layout, naming, error classes), each
|
|
7
|
+
* statement carries file:line evidence and an occurrence count, and every
|
|
8
|
+
* standard starts `inferred` until a human confirms or rejects it. Semantic
|
|
9
|
+
* retrieval (OPP-008) may later widen coverage; it never bypasses confirmation.
|
|
10
|
+
*/
|
|
11
|
+
import { parse as parseYaml } from "yaml";
|
|
12
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
13
|
+
"node_modules",
|
|
14
|
+
".git",
|
|
15
|
+
".aiw",
|
|
16
|
+
".agents",
|
|
17
|
+
".claude",
|
|
18
|
+
".cursor",
|
|
19
|
+
"dist",
|
|
20
|
+
"build",
|
|
21
|
+
"coverage",
|
|
22
|
+
]);
|
|
23
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
24
|
+
".ts",
|
|
25
|
+
".tsx",
|
|
26
|
+
".js",
|
|
27
|
+
".jsx",
|
|
28
|
+
".mjs",
|
|
29
|
+
".cjs",
|
|
30
|
+
".py",
|
|
31
|
+
".rb",
|
|
32
|
+
".go",
|
|
33
|
+
]);
|
|
34
|
+
function walkFiles(fs, root, directory, limit) {
|
|
35
|
+
const files = [];
|
|
36
|
+
const walk = (current, depth) => {
|
|
37
|
+
if (files.length >= limit || depth > 8)
|
|
38
|
+
return;
|
|
39
|
+
const entries = fs.list?.(current) ?? [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (files.length >= limit)
|
|
42
|
+
return;
|
|
43
|
+
const path = `${current}/${entry}`;
|
|
44
|
+
const stat = fs.pathType?.(path);
|
|
45
|
+
if (stat === "directory") {
|
|
46
|
+
if (!IGNORED_DIRECTORIES.has(entry))
|
|
47
|
+
walk(path, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
else if (SOURCE_EXTENSIONS.has(entry.slice(entry.lastIndexOf(".")))) {
|
|
50
|
+
files.push(path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
walk(directory, 0);
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
function relative(root, path) {
|
|
58
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
59
|
+
}
|
|
60
|
+
export function extractStandards(root, fs) {
|
|
61
|
+
const files = walkFiles(fs, root, root, 2000);
|
|
62
|
+
const relativePaths = files.map((path) => relative(root, path));
|
|
63
|
+
// Testing convention: co-located *.test.* files vs a dedicated tests tree.
|
|
64
|
+
const coLocated = relativePaths.filter((path) => /\.(test|spec)\.[a-z]+$/i.test(path));
|
|
65
|
+
const testsDirectory = relativePaths.filter((path) => /(^|\/)tests?\//i.test(path));
|
|
66
|
+
const testing = {
|
|
67
|
+
topic: "testing",
|
|
68
|
+
status: "inferred",
|
|
69
|
+
statements: [
|
|
70
|
+
coLocated.length > 0
|
|
71
|
+
? {
|
|
72
|
+
convention: `Tests live beside the sources they cover, using *.test.* / *.spec.* file names (${coLocated.length} files).`,
|
|
73
|
+
occurrences: coLocated.length,
|
|
74
|
+
evidence: coLocated.slice(0, 3).map((path) => ({ path, line: 1 })),
|
|
75
|
+
}
|
|
76
|
+
: testsDirectory.length > 0
|
|
77
|
+
? {
|
|
78
|
+
convention: `Tests live in a dedicated tests/ tree (${testsDirectory.length} files).`,
|
|
79
|
+
occurrences: testsDirectory.length,
|
|
80
|
+
evidence: testsDirectory.slice(0, 3).map((path) => ({ path, line: 1 })),
|
|
81
|
+
}
|
|
82
|
+
: {
|
|
83
|
+
convention: "No deterministic testing layout detected.",
|
|
84
|
+
occurrences: 0,
|
|
85
|
+
evidence: [],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
// Error-handling convention: custom error classes.
|
|
90
|
+
const errorEvidence = [];
|
|
91
|
+
let errorCount = 0;
|
|
92
|
+
for (const path of files) {
|
|
93
|
+
const content = fs.read(path);
|
|
94
|
+
const lines = content.split("\n");
|
|
95
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
96
|
+
if (/class\s+\w+Error\b\s+extends\s+(?:\w+Error|Error)\b/.test(lines[index])) {
|
|
97
|
+
errorCount += 1;
|
|
98
|
+
if (errorEvidence.length < 3)
|
|
99
|
+
errorEvidence.push({ path: relative(root, path), line: index + 1 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const errorHandling = {
|
|
104
|
+
topic: "error-handling",
|
|
105
|
+
status: "inferred",
|
|
106
|
+
statements: [
|
|
107
|
+
errorCount > 0
|
|
108
|
+
? {
|
|
109
|
+
convention: `Custom error classes (\`class …Error extends …Error\`) are the error convention (${errorCount} definitions).`,
|
|
110
|
+
occurrences: errorCount,
|
|
111
|
+
evidence: errorEvidence,
|
|
112
|
+
}
|
|
113
|
+
: {
|
|
114
|
+
convention: "No custom error classes detected; error convention unknown.",
|
|
115
|
+
occurrences: 0,
|
|
116
|
+
evidence: [],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
// Naming convention: source file casing.
|
|
121
|
+
const kebab = relativePaths.filter((path) => /(^|\/)[a-z0-9]+(-[a-z0-9]+)*\.[a-z]+$/.test(path) &&
|
|
122
|
+
!/[A-Z_]/.test(path.split("/").pop() ?? ""));
|
|
123
|
+
const camel = relativePaths.filter((path) => /[a-z][A-Z]/.test(path.split("/").pop() ?? ""));
|
|
124
|
+
const naming = {
|
|
125
|
+
topic: "naming",
|
|
126
|
+
status: "inferred",
|
|
127
|
+
statements: [
|
|
128
|
+
kebab.length >= camel.length
|
|
129
|
+
? {
|
|
130
|
+
convention: `Source files use kebab-case naming (${kebab.length} files).`,
|
|
131
|
+
occurrences: kebab.length,
|
|
132
|
+
evidence: kebab.slice(0, 3).map((path) => ({ path, line: 1 })),
|
|
133
|
+
}
|
|
134
|
+
: {
|
|
135
|
+
convention: `Source files use camelCase naming (${camel.length} files).`,
|
|
136
|
+
occurrences: camel.length,
|
|
137
|
+
evidence: camel.slice(0, 3).map((path) => ({ path, line: 1 })),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
return [testing, errorHandling, naming];
|
|
142
|
+
}
|
|
143
|
+
export function parseStandardsIndex(content) {
|
|
144
|
+
const value = parseYaml(content);
|
|
145
|
+
const standards = {};
|
|
146
|
+
if (value && typeof value === "object") {
|
|
147
|
+
const entries = value.standards;
|
|
148
|
+
if (entries && typeof entries === "object")
|
|
149
|
+
for (const [topic, entry] of Object.entries(entries)) {
|
|
150
|
+
const status = entry?.status;
|
|
151
|
+
if (typeof status === "string" && ["inferred", "confirmed", "rejected"].includes(status))
|
|
152
|
+
standards[topic] = {
|
|
153
|
+
status: status,
|
|
154
|
+
updatedAt: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { schema: 1, standards };
|
|
159
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
export declare const TARGETS: readonly ["codex", "claude", "cursor", "gemini", "copilot", "universal"];
|
|
2
2
|
export type Target = (typeof TARGETS)[number];
|
|
3
|
-
export declare const HUMAN_APPROVAL_STAGES: readonly ["brainstorming", "specification", "technical-design", "plan", "implementation", "verification", "review", "traceability"];
|
|
3
|
+
export declare const HUMAN_APPROVAL_STAGES: readonly ["brainstorming", "specification", "technical-design", "plan", "implementation", "verification", "review", "traceability", "diagnosis", "assessment"];
|
|
4
4
|
export type HumanApprovalStage = (typeof HUMAN_APPROVAL_STAGES)[number];
|
|
5
|
+
/** OPP-009: processes are independent entry points assembled from shared stage primitives. */
|
|
6
|
+
export declare const PROCESSES: {
|
|
7
|
+
readonly feature: {
|
|
8
|
+
readonly stages: readonly ["brainstorming", "specification", "technical-design", "plan", "implementation", "verification", "review", "traceability"];
|
|
9
|
+
};
|
|
10
|
+
readonly bugfix: {
|
|
11
|
+
readonly stages: readonly ["diagnosis", "implementation", "verification", "review"];
|
|
12
|
+
};
|
|
13
|
+
readonly "idea-assessment": {
|
|
14
|
+
readonly stages: readonly ["assessment"];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type WorkflowProcess = keyof typeof PROCESSES;
|
|
18
|
+
export declare const PROCESS_IDS: WorkflowProcess[];
|
|
5
19
|
export type CommandResult = {
|
|
6
20
|
exitCode: number;
|
|
7
21
|
output?: string;
|
package/dist/types.js
CHANGED
|
@@ -8,4 +8,28 @@ export const HUMAN_APPROVAL_STAGES = [
|
|
|
8
8
|
"verification",
|
|
9
9
|
"review",
|
|
10
10
|
"traceability",
|
|
11
|
+
"diagnosis",
|
|
12
|
+
"assessment",
|
|
11
13
|
];
|
|
14
|
+
/** OPP-009: processes are independent entry points assembled from shared stage primitives. */
|
|
15
|
+
export const PROCESSES = {
|
|
16
|
+
feature: {
|
|
17
|
+
stages: [
|
|
18
|
+
"brainstorming",
|
|
19
|
+
"specification",
|
|
20
|
+
"technical-design",
|
|
21
|
+
"plan",
|
|
22
|
+
"implementation",
|
|
23
|
+
"verification",
|
|
24
|
+
"review",
|
|
25
|
+
"traceability",
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
bugfix: {
|
|
29
|
+
stages: ["diagnosis", "implementation", "verification", "review"],
|
|
30
|
+
},
|
|
31
|
+
"idea-assessment": {
|
|
32
|
+
stages: ["assessment"],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export const PROCESS_IDS = Object.keys(PROCESSES);
|