@multileaf/ai-workflow 0.11.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/blocking-reasons.d.ts +1 -1
  3. package/dist/blocking-reasons.js +24 -0
  4. package/dist/digest.d.ts +48 -0
  5. package/dist/digest.js +106 -0
  6. package/dist/eval/artifact-evals.d.ts +42 -0
  7. package/dist/eval/artifact-evals.js +141 -0
  8. package/dist/generator.js +1 -1
  9. package/dist/human-approval.d.ts +13 -2
  10. package/dist/human-approval.js +43 -8
  11. package/dist/orchestration.d.ts +38 -1
  12. package/dist/orchestration.js +82 -3
  13. package/dist/quality-gates.d.ts +1 -1
  14. package/dist/quality-gates.js +23 -0
  15. package/dist/risk-assess.d.ts +4 -0
  16. package/dist/risk-assess.js +5 -1
  17. package/dist/standards.d.ts +34 -0
  18. package/dist/standards.js +159 -0
  19. package/dist/types.d.ts +15 -1
  20. package/dist/types.js +24 -0
  21. package/dist/workflow.d.ts +7 -0
  22. package/dist/workflow.js +426 -57
  23. package/docs/backlog.md +10 -3
  24. package/docs/continuation-inventory.md +20 -16
  25. package/docs/opportunities/OPP-009-process-entry-points.md +56 -0
  26. package/docs/opportunities/OPP-010-orchestration-worktree-isolation.md +52 -0
  27. package/docs/opportunities/OPP-011-evals-redteam-generated-artifacts.md +56 -0
  28. package/docs/opportunities/OPP-012-standards-discovery.md +51 -0
  29. package/docs/opportunities/OPP-013-read-optimized-artifacts-digest-layer.md +52 -0
  30. package/docs/opportunities/OPP-014-paired-verification-contract.md +54 -0
  31. package/docs/opportunities/OPP-015-token-recovery-governance.md +52 -0
  32. package/docs/opportunities/README.md +25 -10
  33. package/package.json +1 -1
  34. package/resources/skills/implementation-planning.md +1 -1
  35. package/resources/templates/brainstorm.md +0 -1
  36. package/resources/templates/decision-record.md +0 -1
  37. package/resources/templates/implementation-evidence.md +1 -1
  38. package/resources/templates/implementation-plan.md +2 -1
  39. package/resources/templates/migration-report.md +0 -1
  40. package/resources/templates/security-review.md +0 -1
  41. package/resources/templates/task.md +0 -1
  42. package/resources/templates/technical-design.md +0 -1
  43. package/resources/templates/test-plan.md +0 -1
  44. package/resources/templates/verification-report.md +2 -1
@@ -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 outcome = await orchestrator.run(task);
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) => ` - id: ${result.id}\n status: ${result.status}\n detail: ${JSON.stringify(result.detail)}`)
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) {
@@ -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[];
@@ -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",
@@ -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
@@ -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);
@@ -2,3 +2,10 @@ import type { CommandResult, FileSystem } from "./types.js";
2
2
  import type { WorkflowDependencies } from "./services.js";
3
3
  export type WorkflowServices = WorkflowDependencies;
4
4
  export declare function runCommand(args: string[], root: string, fs: FileSystem, services?: WorkflowServices): Promise<CommandResult>;
5
+ /**
6
+ * OPP-014 paired verification: plan tasks declare `Validation: <command> →
7
+ * <expected>`; reports record `Validation: <command> → <observed>`. The gate
8
+ * compares the pairs — command match, observed presence, exit-code agreement.
9
+ * Legacy plans without pair arrows are accepted unchanged (ratchet).
10
+ */
11
+ export declare function compareVerificationPairs(aiw: string, fs: FileSystem, stage: "implementation" | "verification"): string[];