@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/blocking-reasons.d.ts +1 -1
  3. package/dist/blocking-reasons.js +42 -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 +16 -3
  10. package/dist/human-approval.js +56 -12
  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 +472 -62
  23. package/docs/architecture.md +1 -1
  24. package/docs/backlog.md +10 -3
  25. package/docs/continuation-inventory.md +20 -13
  26. package/docs/opportunities/OPP-009-process-entry-points.md +56 -0
  27. package/docs/opportunities/OPP-010-orchestration-worktree-isolation.md +52 -0
  28. package/docs/opportunities/OPP-011-evals-redteam-generated-artifacts.md +56 -0
  29. package/docs/opportunities/OPP-012-standards-discovery.md +51 -0
  30. package/docs/opportunities/OPP-013-read-optimized-artifacts-digest-layer.md +52 -0
  31. package/docs/opportunities/OPP-014-paired-verification-contract.md +54 -0
  32. package/docs/opportunities/OPP-015-token-recovery-governance.md +52 -0
  33. package/docs/opportunities/README.md +25 -10
  34. package/package.json +1 -1
  35. package/resources/skills/implementation-planning.md +1 -1
  36. package/resources/templates/brainstorm.md +0 -1
  37. package/resources/templates/decision-record.md +0 -1
  38. package/resources/templates/implementation-evidence.md +1 -1
  39. package/resources/templates/implementation-plan.md +2 -1
  40. package/resources/templates/migration-report.md +0 -1
  41. package/resources/templates/security-review.md +0 -1
  42. package/resources/templates/task.md +0 -1
  43. package/resources/templates/technical-design.md +0 -1
  44. package/resources/templates/test-plan.md +0 -1
  45. package/resources/templates/verification-report.md +2 -1
@@ -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 complete.");
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 stageIndex = APPROVAL_STAGES.indexOf(stage);
123
- return APPROVAL_STAGES.slice(0, stageIndex).filter((prerequisite) => {
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
- for (const affected of APPROVAL_STAGES.slice(stageIndex)) {
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 { schema: 1, workflowId: ledger.workflowId, stages };
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" && session.status !== "complete")
270
- throw new Error("Workflow session status must be active or complete.");
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 APPROVAL_STAGES) {
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" ||
@@ -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): Promise<OrchestrationTaskResult[]>;
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;
@@ -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);