@multileaf/ai-workflow 0.6.1 → 0.7.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 +8 -0
- package/README.md +9 -1
- package/dist/human-approval.d.ts +4 -0
- package/dist/human-approval.js +22 -2
- package/dist/interpreter.d.ts +8 -1
- package/dist/interpreter.js +8 -1
- package/dist/recommendations.js +13 -0
- package/dist/token-usage.d.ts +6 -0
- package/dist/token-usage.js +32 -5
- package/dist/workflow.js +166 -50
- package/docs/telemetry.md +3 -1
- package/package.json +1 -1
- package/resources/package.yaml +1 -0
- package/resources/skills/ticket.md +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Ticket-driven task entry point: `aiw task start --ticket=TEAM-123 [--title=text]` starts an SDD session that records the ticket, and `aiw task status` reports the ticket, stage statuses, and the next pending step with ticket-aware guidance. `aiw spec --ticket=TEAM-123` links the specification scaffold to the ticket. A new `ticket` skill (recommended as the `ticket-driven-development` capability) drives the flow in the host agent, keeping the brainstorming skip an explicit human decision.
|
|
12
|
+
- `aiw telemetry show` summarizes buffered events by command and outcome (fill level against the retention cap, per-command success/failure counts); `--verbose` adds each event verbatim. `telemetry status` keeps reporting configuration only.
|
|
13
|
+
- Per-model token usage: `usage.yml` records carry model attribution (`stage|budget|used|model`; legacy three-column rows still parse) and `aiw token-usage` summarizes total consumption by model. The OpenAI-compatible provider reports the response usage block, so interpreter usage is measured (marked `(measured)`) when the endpoint provides it and estimated otherwise; `scan.embedding`, `scan.interpreter`, and `recommend.decision` all record their model.
|
|
14
|
+
|
|
7
15
|
## [0.6.1] - 2026-10-13
|
|
8
16
|
|
|
9
17
|
### Added
|
package/README.md
CHANGED
|
@@ -204,6 +204,13 @@ npx @multileaf/ai-workflow recommend --select=typescript-quality,verification
|
|
|
204
204
|
|
|
205
205
|
# Track a feature from its first artifact through final human approval.
|
|
206
206
|
npx @multileaf/ai-workflow workflow start
|
|
207
|
+
|
|
208
|
+
# Or start directly from a ticket / already-clear requirement (no brainstorm needed).
|
|
209
|
+
npx @multileaf/ai-workflow task start --ticket=PROJ-142 --title="Add export endpoint"
|
|
210
|
+
npx @multileaf/ai-workflow task status
|
|
211
|
+
# Record an accepted brainstorm skip, then link the specification to the ticket:
|
|
212
|
+
npx @multileaf/ai-workflow skip brainstorming --reason="Requirement provided by ticket PROJ-142"
|
|
213
|
+
npx @multileaf/ai-workflow spec --ticket=PROJ-142 --title="Add export endpoint"
|
|
207
214
|
npx @multileaf/ai-workflow brainstorm --title="Feature name"
|
|
208
215
|
npx @multileaf/ai-workflow gate brainstorming
|
|
209
216
|
# Present evidence and wait for a human reply before each approve command.
|
|
@@ -343,7 +350,8 @@ The typed SDK is available from `@multileaf/ai-workflow/plugin-sdk`. It exports
|
|
|
343
350
|
Telemetry is disabled by default and requires explicit consent — offered once during interactive installation with an explicit default of No. Each event contains only the command name and its outcome; never arguments, file paths, or file contents. When telemetry is enabled but no host transport is configured, events accumulate in a rotated local buffer (`.aiw/telemetry-buffer.jsonl`, 200-event cap) that `aiw telemetry status --verbose` can inspect and `aiw telemetry disable` purges.
|
|
344
351
|
|
|
345
352
|
```bash
|
|
346
|
-
npx @multileaf/ai-workflow telemetry
|
|
353
|
+
npx @multileaf/ai-workflow telemetry show # summarized by command and outcome
|
|
354
|
+
npx @multileaf/ai-workflow telemetry show --verbose # events verbatim
|
|
347
355
|
npx @multileaf/ai-workflow telemetry enable
|
|
348
356
|
npx @multileaf/ai-workflow telemetry disable
|
|
349
357
|
```
|
package/dist/human-approval.d.ts
CHANGED
|
@@ -20,7 +20,10 @@ export type WorkflowSession = {
|
|
|
20
20
|
status: "active" | "complete";
|
|
21
21
|
startedAt: string;
|
|
22
22
|
updatedAt: string;
|
|
23
|
+
ticket?: string;
|
|
24
|
+
title?: string;
|
|
23
25
|
};
|
|
26
|
+
export declare function validateTaskTicket(ticket: string): void;
|
|
24
27
|
export type ApprovalStageDefinition = {
|
|
25
28
|
optional: boolean;
|
|
26
29
|
requiresQualityGate: boolean;
|
|
@@ -49,3 +52,4 @@ export declare function skipOptionalStage(ledger: ApprovalLedger, stage: HumanAp
|
|
|
49
52
|
export declare function invalidateStageAndDescendants(ledger: ApprovalLedger, stage: HumanApprovalStage, updatedAt: string): ApprovalLedger;
|
|
50
53
|
export declare function serializeApprovalLedger(ledger: ApprovalLedger): string;
|
|
51
54
|
export declare function parseApprovalLedger(content: string): ApprovalLedger;
|
|
55
|
+
export declare function assertWorkflowId(value: string): void;
|
package/dist/human-approval.js
CHANGED
|
@@ -2,6 +2,10 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { parseDocument, stringify } from "yaml";
|
|
3
3
|
import { HUMAN_APPROVAL_STAGES } from "./types.js";
|
|
4
4
|
export const APPROVAL_STAGES = HUMAN_APPROVAL_STAGES;
|
|
5
|
+
export function validateTaskTicket(ticket) {
|
|
6
|
+
if (!/^[A-Z][A-Z0-9_]*-\d+$/.test(ticket))
|
|
7
|
+
throw new Error("Task ticket must match TEAM-123, for example PROJ-142.");
|
|
8
|
+
}
|
|
5
9
|
const STAGE_DEFINITIONS = {
|
|
6
10
|
brainstorming: { optional: true, requiresQualityGate: true },
|
|
7
11
|
specification: { optional: false, requiresQualityGate: true },
|
|
@@ -22,7 +26,15 @@ const APPROVAL_RECORD_KEYS = new Set([
|
|
|
22
26
|
"updatedAt",
|
|
23
27
|
]);
|
|
24
28
|
const LEDGER_KEYS = new Set(["schema", "workflowId", "stages"]);
|
|
25
|
-
const SESSION_KEYS = new Set([
|
|
29
|
+
const SESSION_KEYS = new Set([
|
|
30
|
+
"schema",
|
|
31
|
+
"workflowId",
|
|
32
|
+
"status",
|
|
33
|
+
"startedAt",
|
|
34
|
+
"updatedAt",
|
|
35
|
+
"ticket",
|
|
36
|
+
"title",
|
|
37
|
+
]);
|
|
26
38
|
export function createApprovalLedger(workflowId) {
|
|
27
39
|
assertWorkflowId(workflowId);
|
|
28
40
|
return { schema: 1, workflowId, stages: {} };
|
|
@@ -47,12 +59,20 @@ export function parseWorkflowSession(content) {
|
|
|
47
59
|
throw new Error("Workflow session status must be active or complete.");
|
|
48
60
|
if (typeof value.startedAt !== "string" || typeof value.updatedAt !== "string")
|
|
49
61
|
throw new Error("Workflow session requires start and update timestamps.");
|
|
62
|
+
if (value.ticket !== undefined && typeof value.ticket !== "string")
|
|
63
|
+
throw new Error("Workflow session ticket must be a string.");
|
|
64
|
+
if (value.title !== undefined && typeof value.title !== "string")
|
|
65
|
+
throw new Error("Workflow session title must be a string.");
|
|
66
|
+
if (typeof value.ticket === "string")
|
|
67
|
+
validateTaskTicket(value.ticket);
|
|
50
68
|
const session = {
|
|
51
69
|
schema: 1,
|
|
52
70
|
workflowId: value.workflowId,
|
|
53
71
|
status: value.status,
|
|
54
72
|
startedAt: value.startedAt,
|
|
55
73
|
updatedAt: value.updatedAt,
|
|
74
|
+
...(value.ticket === undefined ? {} : { ticket: value.ticket }),
|
|
75
|
+
...(value.title === undefined ? {} : { title: value.title }),
|
|
56
76
|
};
|
|
57
77
|
validateWorkflowSession(session);
|
|
58
78
|
return session;
|
|
@@ -189,7 +209,7 @@ function assertTimestamp(value) {
|
|
|
189
209
|
!Number.isFinite(Date.parse(value)))
|
|
190
210
|
throw new Error("Approval timestamp must be a valid date-time.");
|
|
191
211
|
}
|
|
192
|
-
function assertWorkflowId(value) {
|
|
212
|
+
export function assertWorkflowId(value) {
|
|
193
213
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value))
|
|
194
214
|
throw new Error("Workflow ID must be 1–64 characters using letters, numbers, dot, underscore, or hyphen.");
|
|
195
215
|
}
|
package/dist/interpreter.d.ts
CHANGED
|
@@ -10,11 +10,16 @@ export type InterpretationRequest = {
|
|
|
10
10
|
export type Interpreter = {
|
|
11
11
|
interpret(request: InterpretationRequest): Promise<EvidenceFact[]>;
|
|
12
12
|
};
|
|
13
|
+
export type AiProviderUsage = {
|
|
14
|
+
promptTokens: number;
|
|
15
|
+
completionTokens: number;
|
|
16
|
+
};
|
|
13
17
|
export type AiProviderRequest = {
|
|
14
18
|
system: string;
|
|
15
19
|
prompt: string;
|
|
16
20
|
maxTokens: number;
|
|
17
21
|
responseFormat: "json";
|
|
22
|
+
onUsage?: (usage: AiProviderUsage) => void;
|
|
18
23
|
};
|
|
19
24
|
export type AiProvider = {
|
|
20
25
|
generate(request: AiProviderRequest): Promise<string>;
|
|
@@ -35,5 +40,7 @@ export declare function buildInterpreterQuery(profile: ProjectProfile): string;
|
|
|
35
40
|
type AiFetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
36
41
|
export declare function loadAiProviderConfig(fs: FileSystem, aiwPath: string): AiProviderConfig | undefined;
|
|
37
42
|
export declare function createOpenAiCompatibleProvider(config: AiProviderConfig, fetchImpl?: AiFetchLike, env?: Readonly<Record<string, string | undefined>>): AiProvider;
|
|
38
|
-
export declare function createProjectInterpreter(root: string, provider: AiProvider, contextReader?: ContextReader
|
|
43
|
+
export declare function createProjectInterpreter(root: string, provider: AiProvider, contextReader?: ContextReader, options?: {
|
|
44
|
+
onUsage?: (usage: AiProviderUsage) => void;
|
|
45
|
+
}): Interpreter;
|
|
39
46
|
export {};
|
package/dist/interpreter.js
CHANGED
|
@@ -212,6 +212,12 @@ export function createOpenAiCompatibleProvider(config, fetchImpl = (url, init) =
|
|
|
212
212
|
if (!response.ok)
|
|
213
213
|
throw new Error(`AI provider request failed for model ${config.model}: ${await response.text()}`);
|
|
214
214
|
const body = (await response.json());
|
|
215
|
+
if (typeof body.usage?.prompt_tokens === "number" &&
|
|
216
|
+
typeof body.usage?.completion_tokens === "number")
|
|
217
|
+
request.onUsage?.({
|
|
218
|
+
promptTokens: body.usage.prompt_tokens,
|
|
219
|
+
completionTokens: body.usage.completion_tokens,
|
|
220
|
+
});
|
|
215
221
|
const content = body.choices?.[0]?.message?.content;
|
|
216
222
|
if (typeof content !== "string")
|
|
217
223
|
throw new Error(`AI provider ${config.model} returned no message content`);
|
|
@@ -219,7 +225,7 @@ export function createOpenAiCompatibleProvider(config, fetchImpl = (url, init) =
|
|
|
219
225
|
},
|
|
220
226
|
};
|
|
221
227
|
}
|
|
222
|
-
export function createProjectInterpreter(root, provider, contextReader = async (path) => new TextDecoder("utf-8", { fatal: true }).decode(await readFile(path))) {
|
|
228
|
+
export function createProjectInterpreter(root, provider, contextReader = async (path) => new TextDecoder("utf-8", { fatal: true }).decode(await readFile(path)), options = {}) {
|
|
223
229
|
const projectRoot = resolve(root);
|
|
224
230
|
return {
|
|
225
231
|
async interpret(request) {
|
|
@@ -261,6 +267,7 @@ export function createProjectInterpreter(root, provider, contextReader = async (
|
|
|
261
267
|
prompt: `Detected profile:\n${safeProfile}\n\nScoped project context:\n${sections.join("\n\n")}`,
|
|
262
268
|
maxTokens: scoped.maxTokens,
|
|
263
269
|
responseFormat: "json",
|
|
270
|
+
...(options.onUsage ? { onUsage: options.onUsage } : {}),
|
|
264
271
|
});
|
|
265
272
|
return parseFacts(output, new Set(scoped.files));
|
|
266
273
|
},
|
package/dist/recommendations.js
CHANGED
|
@@ -14,6 +14,19 @@ const CATALOG = [
|
|
|
14
14
|
? { evidence: projectEvidence(profile), confidence: 0.92 }
|
|
15
15
|
: undefined,
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
id: "ticket-driven-development",
|
|
19
|
+
provider: "multileaf",
|
|
20
|
+
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
|
+
permissions: [],
|
|
22
|
+
resources: local([
|
|
23
|
+
{ type: "skills", id: "ticket" },
|
|
24
|
+
{ type: "templates", id: "task" },
|
|
25
|
+
]),
|
|
26
|
+
matches: (profile) => hasProjectSignals(profile)
|
|
27
|
+
? { evidence: projectEvidence(profile), confidence: 0.9 }
|
|
28
|
+
: undefined,
|
|
29
|
+
},
|
|
17
30
|
{
|
|
18
31
|
id: "requirements-specification",
|
|
19
32
|
provider: "multileaf",
|
package/dist/token-usage.d.ts
CHANGED
|
@@ -2,8 +2,14 @@ export type TokenUsage = {
|
|
|
2
2
|
stage: string;
|
|
3
3
|
budget: number;
|
|
4
4
|
used: number;
|
|
5
|
+
model?: string;
|
|
5
6
|
};
|
|
6
7
|
export declare function validateTokenUsage(usage: TokenUsage): TokenUsage;
|
|
7
8
|
export declare function parseTokenUsage(content: string): TokenUsage[];
|
|
8
9
|
export declare function serializeTokenUsage(entries: TokenUsage[]): string;
|
|
9
10
|
export declare function recordTokenUsage(entries: TokenUsage[], usage: TokenUsage): TokenUsage[];
|
|
11
|
+
export declare function summarizeUsageByModel(entries: TokenUsage[]): {
|
|
12
|
+
model: string;
|
|
13
|
+
tokens: number;
|
|
14
|
+
stages: string[];
|
|
15
|
+
}[];
|
package/dist/token-usage.js
CHANGED
|
@@ -10,6 +10,8 @@ export function validateTokenUsage(usage) {
|
|
|
10
10
|
}
|
|
11
11
|
if (usage.used > usage.budget)
|
|
12
12
|
throw new Error(`Token budget exceeded for stage: ${usage.stage}`);
|
|
13
|
+
if (usage.model !== undefined && (typeof usage.model !== "string" || !usage.model.trim()))
|
|
14
|
+
throw new Error("Token usage model must be a non-empty string when present.");
|
|
13
15
|
return usage;
|
|
14
16
|
}
|
|
15
17
|
export function parseTokenUsage(content) {
|
|
@@ -20,19 +22,44 @@ export function parseTokenUsage(content) {
|
|
|
20
22
|
.slice(1)
|
|
21
23
|
.filter(Boolean)
|
|
22
24
|
.map((line) => {
|
|
23
|
-
const [stage, budget, used] = line.split("|");
|
|
24
|
-
return validateTokenUsage({
|
|
25
|
+
const [stage, budget, used, model] = line.split("|");
|
|
26
|
+
return validateTokenUsage({
|
|
27
|
+
stage,
|
|
28
|
+
budget: Number(budget),
|
|
29
|
+
used: Number(used),
|
|
30
|
+
...(model ? { model } : {}),
|
|
31
|
+
});
|
|
25
32
|
});
|
|
26
33
|
}
|
|
27
34
|
export function serializeTokenUsage(entries) {
|
|
28
35
|
return ([
|
|
29
36
|
"schema: 1",
|
|
30
37
|
...entries
|
|
31
|
-
.sort((a, b) => a.stage
|
|
32
|
-
|
|
38
|
+
.sort((a, b) => a.stage < b.stage
|
|
39
|
+
? -1
|
|
40
|
+
: a.stage > b.stage
|
|
41
|
+
? 1
|
|
42
|
+
: (a.model ?? "").localeCompare(b.model ?? ""))
|
|
43
|
+
.map((entry) => `${entry.stage}|${entry.budget}|${entry.used}${entry.model ? `|${entry.model}` : ""}`),
|
|
33
44
|
].join("\n") + "\n");
|
|
34
45
|
}
|
|
35
46
|
export function recordTokenUsage(entries, usage) {
|
|
36
47
|
validateTokenUsage(usage);
|
|
37
|
-
return [
|
|
48
|
+
return [
|
|
49
|
+
...entries.filter((entry) => entry.stage !== usage.stage || entry.model !== usage.model),
|
|
50
|
+
usage,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
export function summarizeUsageByModel(entries) {
|
|
54
|
+
const byModel = new Map();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const model = entry.model ?? "unattributed";
|
|
57
|
+
const bucket = byModel.get(model) ?? { tokens: 0, stages: new Set() };
|
|
58
|
+
bucket.tokens += entry.used;
|
|
59
|
+
bucket.stages.add(entry.stage);
|
|
60
|
+
byModel.set(model, bucket);
|
|
61
|
+
}
|
|
62
|
+
return [...byModel]
|
|
63
|
+
.map(([model, { tokens, stages }]) => ({ model, tokens, stages: [...stages].sort() }))
|
|
64
|
+
.sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
38
65
|
}
|
package/dist/workflow.js
CHANGED
|
@@ -34,8 +34,8 @@ import { enforceOrganizationPolicy, parseOrganizationPolicy, serializeOrganizati
|
|
|
34
34
|
import { composeContext, CONTEXT_LAYERS, readContextStore, writeContextLayer, } from "./context-store.js";
|
|
35
35
|
import { loadContextDelta, retrieveTaskContext } from "./context-retrieval.js";
|
|
36
36
|
import { createSummaryCache, isSummaryFresh, parseSummaryCache, serializeSummaryCache, } from "./context-cache.js";
|
|
37
|
-
import { parseTokenUsage, recordTokenUsage, serializeTokenUsage } from "./token-usage.js";
|
|
38
|
-
import { APPROVAL_STAGES, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, } from "./human-approval.js";
|
|
37
|
+
import { parseTokenUsage, recordTokenUsage, serializeTokenUsage, summarizeUsageByModel, } from "./token-usage.js";
|
|
38
|
+
import { APPROVAL_STAGES, assertWorkflowId, validateTaskTicket, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, } from "./human-approval.js";
|
|
39
39
|
import { measureContextQuality, serializeContextQuality } from "./context-quality.js";
|
|
40
40
|
import { serializeAdapterCapabilities } from "./adapter-contract.js";
|
|
41
41
|
import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceability.js";
|
|
@@ -46,61 +46,122 @@ import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
|
46
46
|
import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
|
|
47
47
|
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
48
48
|
import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
49
|
+
function beginWorkflowSession(fs, aiw, options) {
|
|
50
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
51
|
+
const ledgerPath = join(aiw, "approvals.yml");
|
|
52
|
+
if (fs.exists(sessionPath)) {
|
|
53
|
+
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
54
|
+
if (previous.status === "active")
|
|
55
|
+
return {
|
|
56
|
+
exitCode: 1,
|
|
57
|
+
error: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
58
|
+
};
|
|
59
|
+
if (!fs.exists(ledgerPath))
|
|
60
|
+
return { exitCode: 1, error: "Completed workflow is missing its approval ledger." };
|
|
61
|
+
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
62
|
+
if (fs.exists(archive))
|
|
63
|
+
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
64
|
+
fs.mkdir(join(aiw, "checkpoints"));
|
|
65
|
+
fs.write(archive, fs.read(ledgerPath));
|
|
66
|
+
}
|
|
67
|
+
else if (fs.exists(ledgerPath)) {
|
|
68
|
+
return {
|
|
69
|
+
exitCode: 1,
|
|
70
|
+
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const workflowId = options.id ?? randomUUID();
|
|
74
|
+
const priorRun = join(aiw, `checkpoints/workflow-${workflowId}-approvals.yml`);
|
|
75
|
+
if (fs.exists(priorRun))
|
|
76
|
+
return {
|
|
77
|
+
exitCode: 1,
|
|
78
|
+
error: `Workflow ID has already been used: ${workflowId}. Choose a new ID.`,
|
|
79
|
+
};
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const session = {
|
|
82
|
+
schema: 1,
|
|
83
|
+
workflowId,
|
|
84
|
+
status: "active",
|
|
85
|
+
startedAt: now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
...(options.ticket ? { ticket: options.ticket } : {}),
|
|
88
|
+
...(options.title ? { title: options.title } : {}),
|
|
89
|
+
};
|
|
90
|
+
const ledger = createApprovalLedger(workflowId);
|
|
91
|
+
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
92
|
+
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
93
|
+
return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
|
|
94
|
+
}
|
|
49
95
|
export async function runCommand(args, root, fs, services = {}) {
|
|
50
96
|
const aiw = join(root, WORKFLOW_DIRECTORY);
|
|
51
97
|
try {
|
|
52
98
|
fs = withSafeProjectWrites(fs, root);
|
|
53
99
|
const command = args[0] ?? "help";
|
|
100
|
+
if (command === "task") {
|
|
101
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
102
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
103
|
+
const usage = "Usage: aiw task start --ticket=TEAM-123 [--title=text] [--id=workflow-id] | aiw task status";
|
|
104
|
+
const action = args[1];
|
|
105
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
106
|
+
if (action === "start") {
|
|
107
|
+
const options = parseNamedOptions(args.slice(2), ["ticket", "title", "id"]);
|
|
108
|
+
if (!options?.ticket)
|
|
109
|
+
return { exitCode: 1, error: usage };
|
|
110
|
+
validateTaskTicket(options.ticket);
|
|
111
|
+
if (options.id !== undefined) {
|
|
112
|
+
try {
|
|
113
|
+
assertWorkflowId(options.id);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { exitCode: 1, error: "Workflow ID must be a UUID." };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const result = beginWorkflowSession(fs, aiw, {
|
|
120
|
+
...(options.id ? { id: options.id } : {}),
|
|
121
|
+
ticket: options.ticket,
|
|
122
|
+
...(options.title ? { title: options.title } : {}),
|
|
123
|
+
});
|
|
124
|
+
if (result.exitCode !== 0)
|
|
125
|
+
return result;
|
|
126
|
+
const title = options.title ? ` titled "${options.title}"` : "";
|
|
127
|
+
return {
|
|
128
|
+
exitCode: 0,
|
|
129
|
+
output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
|
|
130
|
+
`Next steps: ask the user whether the requirement is clear enough to skip brainstorming (record an accepted skip with \`aiw skip brainstorming --reason="Requirement provided by ticket ${options.ticket}"\`), then create the specification with \`aiw spec --ticket=${options.ticket} --title=...\`.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (action === "status") {
|
|
134
|
+
if (!fs.exists(sessionPath))
|
|
135
|
+
return {
|
|
136
|
+
exitCode: 0,
|
|
137
|
+
output: "No SDD workflow is active. Start one with `aiw task start --ticket=TEAM-123`.",
|
|
138
|
+
};
|
|
139
|
+
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
140
|
+
const lines = [
|
|
141
|
+
...(session.ticket
|
|
142
|
+
? [`Ticket: ${session.ticket}${session.title ? ` — ${session.title}` : ""}`]
|
|
143
|
+
: []),
|
|
144
|
+
`Workflow: ${session.workflowId} (${session.status})`,
|
|
145
|
+
"Stages:",
|
|
146
|
+
...APPROVAL_STAGES.map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
|
|
147
|
+
];
|
|
148
|
+
const next = APPROVAL_STAGES.find((stage) => (ledger.stages[stage]?.status ?? "not-started") === "not-started");
|
|
149
|
+
if (session.status === "active" && next)
|
|
150
|
+
lines.push(`Next: ${next === "brainstorming" && session.ticket ? `decide brainstorm or record an accepted skip (ticket ${session.ticket} already provides the requirement)` : `advance ${next} (gate, approval)`}.`);
|
|
151
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
152
|
+
}
|
|
153
|
+
return { exitCode: 1, error: usage };
|
|
154
|
+
}
|
|
54
155
|
if (command === "workflow") {
|
|
55
156
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
56
157
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
57
158
|
const action = args[1];
|
|
58
159
|
const sessionPath = join(aiw, "workflow.yml");
|
|
59
|
-
const ledgerPath = join(aiw, "approvals.yml");
|
|
60
160
|
if (action === "start") {
|
|
61
161
|
const options = parseNamedOptions(args.slice(2), ["id"]);
|
|
62
162
|
if (!options)
|
|
63
163
|
return { exitCode: 1, error: "Usage: aiw workflow start [--id=workflow-id]" };
|
|
64
|
-
|
|
65
|
-
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
66
|
-
if (previous.status === "active")
|
|
67
|
-
return {
|
|
68
|
-
exitCode: 1,
|
|
69
|
-
error: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
70
|
-
};
|
|
71
|
-
if (!fs.exists(ledgerPath))
|
|
72
|
-
return { exitCode: 1, error: "Completed workflow is missing its approval ledger." };
|
|
73
|
-
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
74
|
-
if (fs.exists(archive))
|
|
75
|
-
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
76
|
-
fs.mkdir(join(aiw, "checkpoints"));
|
|
77
|
-
fs.write(archive, fs.read(ledgerPath));
|
|
78
|
-
}
|
|
79
|
-
else if (fs.exists(ledgerPath)) {
|
|
80
|
-
return {
|
|
81
|
-
exitCode: 1,
|
|
82
|
-
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
const workflowId = options.id ?? randomUUID();
|
|
86
|
-
const priorRun = join(aiw, `checkpoints/workflow-${workflowId}-approvals.yml`);
|
|
87
|
-
if (fs.exists(priorRun))
|
|
88
|
-
return {
|
|
89
|
-
exitCode: 1,
|
|
90
|
-
error: `Workflow ID has already been used: ${workflowId}. Choose a new ID.`,
|
|
91
|
-
};
|
|
92
|
-
const now = new Date().toISOString();
|
|
93
|
-
const session = {
|
|
94
|
-
schema: 1,
|
|
95
|
-
workflowId,
|
|
96
|
-
status: "active",
|
|
97
|
-
startedAt: now,
|
|
98
|
-
updatedAt: now,
|
|
99
|
-
};
|
|
100
|
-
const ledger = createApprovalLedger(workflowId);
|
|
101
|
-
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
102
|
-
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
103
|
-
return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
|
|
164
|
+
return beginWorkflowSession(fs, aiw, { id: options.id });
|
|
104
165
|
}
|
|
105
166
|
if (action === "status") {
|
|
106
167
|
if (!fs.exists(sessionPath))
|
|
@@ -359,13 +420,44 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
359
420
|
if (command === "telemetry") {
|
|
360
421
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
361
422
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
362
|
-
const usage = "Usage: aiw telemetry <status|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
423
|
+
const usage = "Usage: aiw telemetry <status|show|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
363
424
|
const action = args[1];
|
|
364
425
|
if (!action)
|
|
365
426
|
return { exitCode: 1, error: usage };
|
|
366
427
|
const telemetryConfig = loadTelemetryConfig(fs, aiw);
|
|
367
428
|
const actionArgs = args.slice(2).filter((arg) => arg !== "--verbose");
|
|
368
429
|
const verbose = args.includes("--verbose");
|
|
430
|
+
if (action === "show") {
|
|
431
|
+
if (actionArgs.length !== 0)
|
|
432
|
+
return { exitCode: 1, error: usage };
|
|
433
|
+
const buffered = readTelemetryBuffer(fs, aiw);
|
|
434
|
+
const byCommand = new Map();
|
|
435
|
+
for (const event of buffered) {
|
|
436
|
+
const key = event.command ?? "unknown";
|
|
437
|
+
const entry = byCommand.get(key) ?? { success: 0, failure: 0 };
|
|
438
|
+
if (event.outcome === "failure")
|
|
439
|
+
entry.failure += 1;
|
|
440
|
+
else
|
|
441
|
+
entry.success += 1;
|
|
442
|
+
byCommand.set(key, entry);
|
|
443
|
+
}
|
|
444
|
+
const lines = [
|
|
445
|
+
`Telemetry is ${telemetryConfig.enabled ? "enabled" : "disabled"}.`,
|
|
446
|
+
`Buffered events: ${buffered.length}/${TELEMETRY_BUFFER_CAP} (.aiw/${TELEMETRY_BUFFER_FILE}).`,
|
|
447
|
+
];
|
|
448
|
+
if (buffered.length > 0) {
|
|
449
|
+
lines.push("By command:");
|
|
450
|
+
for (const [command, { success, failure }] of [...byCommand].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
451
|
+
lines.push(`- ${command}: ${success} success, ${failure} failure`);
|
|
452
|
+
}
|
|
453
|
+
if (verbose && buffered.length > 0) {
|
|
454
|
+
lines.push("Events:");
|
|
455
|
+
lines.push(...buffered.map((event) => `- ${JSON.stringify(event)}`));
|
|
456
|
+
}
|
|
457
|
+
if (!telemetryConfig.enabled)
|
|
458
|
+
lines.push("Note: telemetry is disabled, so no new events are recorded; the buffer keeps previously collected events until purged.");
|
|
459
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
460
|
+
}
|
|
369
461
|
if (action === "status") {
|
|
370
462
|
if (actionArgs.length !== 0)
|
|
371
463
|
return { exitCode: 1, error: usage };
|
|
@@ -444,10 +536,15 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
444
536
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
445
537
|
let interpreter = services.interpreter;
|
|
446
538
|
let interpreterModel;
|
|
539
|
+
let interpreterUsage;
|
|
447
540
|
if (!interpreter) {
|
|
448
541
|
const aiConfig = loadAiProviderConfig(fs, aiw);
|
|
449
542
|
if (aiConfig?.enabled) {
|
|
450
|
-
interpreter = createProjectInterpreter(root, services.aiProvider ?? createOpenAiCompatibleProvider(aiConfig)
|
|
543
|
+
interpreter = createProjectInterpreter(root, services.aiProvider ?? createOpenAiCompatibleProvider(aiConfig), undefined, {
|
|
544
|
+
onUsage: (usage) => {
|
|
545
|
+
interpreterUsage = usage;
|
|
546
|
+
},
|
|
547
|
+
});
|
|
451
548
|
interpreterModel = aiConfig.model;
|
|
452
549
|
}
|
|
453
550
|
}
|
|
@@ -477,6 +574,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
477
574
|
label: "embedding",
|
|
478
575
|
tokens: Math.ceil(buildInterpreterQuery(profile).length / 4),
|
|
479
576
|
detail: `${contextConfig.model} — scoped interpreter context to ${matches.length} semantic matches`,
|
|
577
|
+
model: contextConfig.model,
|
|
480
578
|
});
|
|
481
579
|
}
|
|
482
580
|
}
|
|
@@ -488,10 +586,14 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
488
586
|
const interpretationRequest = createInterpretationRequest(profile, scopedFiles, 4000, priorityFiles);
|
|
489
587
|
const inferred = await interpreter.interpret(interpretationRequest);
|
|
490
588
|
profile.facts = mergeFacts(profile.facts, inferred);
|
|
589
|
+
const actualTokens = interpreterUsage
|
|
590
|
+
? interpreterUsage.promptTokens + interpreterUsage.completionTokens
|
|
591
|
+
: undefined;
|
|
491
592
|
modelUsage.push({
|
|
492
593
|
label: "interpreter",
|
|
493
|
-
tokens: interpretationRequest.estimatedTokens,
|
|
494
|
-
detail: `${interpreterModel} — ${inferred.length} facts from ${interpretationRequest.files.length} files`,
|
|
594
|
+
tokens: actualTokens ?? interpretationRequest.estimatedTokens,
|
|
595
|
+
detail: `${interpreterModel} — ${inferred.length} facts from ${interpretationRequest.files.length} files${actualTokens !== undefined ? " (measured)" : ""}`,
|
|
596
|
+
model: interpreterModel ?? "unknown",
|
|
495
597
|
});
|
|
496
598
|
}
|
|
497
599
|
const overrides = readFactOverrides(fs, aiw);
|
|
@@ -530,6 +632,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
530
632
|
label: "embedding",
|
|
531
633
|
tokens: Math.ceil(indexedChars / 4),
|
|
532
634
|
detail: `${semanticConfig.model} — indexed ${index.chunks.length} chunks in ${Math.ceil(index.chunks.length / 64)} batch requests`,
|
|
635
|
+
model: semanticConfig.model,
|
|
533
636
|
});
|
|
534
637
|
}
|
|
535
638
|
if (modelUsage.length > 0) {
|
|
@@ -541,6 +644,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
541
644
|
stage: `scan.${usage.label}`,
|
|
542
645
|
budget: usage.tokens,
|
|
543
646
|
used: usage.tokens,
|
|
647
|
+
model: usage.model,
|
|
544
648
|
});
|
|
545
649
|
fs.write(usagePath, serializeTokenUsage(next));
|
|
546
650
|
}
|
|
@@ -1076,6 +1180,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1076
1180
|
stage: "recommend.decision",
|
|
1077
1181
|
budget: decisionTokens,
|
|
1078
1182
|
used: decisionTokens,
|
|
1183
|
+
model: rankingConfig.model,
|
|
1079
1184
|
})));
|
|
1080
1185
|
ordered = ranked.items;
|
|
1081
1186
|
}
|
|
@@ -1409,8 +1514,11 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1409
1514
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1410
1515
|
assertStageReady(aiw, fs, "specification");
|
|
1411
1516
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
1517
|
+
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
1518
|
+
if (ticket !== undefined)
|
|
1519
|
+
validateTaskTicket(ticket);
|
|
1412
1520
|
const path = join(aiw, "generated/specs/specification.md");
|
|
1413
|
-
fs.write(path, `# ${title}\n\n## Context\n\n## Requirements\n\n### REQ-001: Requirement title\n\n- **Description:**\n- **Acceptance criteria:**\n - Given\n - When\n - Then\n\n## Non-functional Requirements\n\n## Constraints\n\n## Open Questions\n\n`);
|
|
1521
|
+
fs.write(path, `# ${title}\n${ticket ? `\n> Ticket: ${ticket}\n` : ""}\n## Context\n\n## Requirements\n\n### REQ-001: Requirement title\n\n- **Description:**\n- **Acceptance criteria:**\n - Given\n - When\n - Then\n\n## Non-functional Requirements\n\n## Constraints\n\n## Open Questions\n\n`);
|
|
1414
1522
|
return { exitCode: 0, output: `Specification artifact created: ${path}` };
|
|
1415
1523
|
}
|
|
1416
1524
|
if (command === "adr") {
|
|
@@ -1834,7 +1942,15 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1834
1942
|
fs.write(usagePath, serializeTokenUsage(next));
|
|
1835
1943
|
return { exitCode: 0, output: `Token usage recorded: ${stage} (${used}/${budget})` };
|
|
1836
1944
|
}
|
|
1837
|
-
|
|
1945
|
+
if (entries.length === 0)
|
|
1946
|
+
return { exitCode: 0, output: "No token usage recorded yet." };
|
|
1947
|
+
const summary = summarizeUsageByModel(entries);
|
|
1948
|
+
return {
|
|
1949
|
+
exitCode: 0,
|
|
1950
|
+
output: `Token usage by model:\n${summary
|
|
1951
|
+
.map((entry) => `- ${entry.model}: ${entry.tokens.toLocaleString("en-US")} tokens (${entry.stages.join(", ")})`)
|
|
1952
|
+
.join("\n")}\n\nRaw records:\n${serializeTokenUsage(entries)}`,
|
|
1953
|
+
};
|
|
1838
1954
|
}
|
|
1839
1955
|
if (command === "context-quality") {
|
|
1840
1956
|
const metrics = measureContextQuality(readContextStore(root, fs));
|
|
@@ -1880,7 +1996,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1880
1996
|
}
|
|
1881
1997
|
return {
|
|
1882
1998
|
exitCode: 0,
|
|
1883
|
-
output: "aiw install [--target target] | workflow <start|status|complete> [--id=id] | approve <stage> | reject <stage> --reason=text | skip <optional-stage> --reason=text | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | semantic <status|query> | decision <status|triage> | self-validate --ticket=TYPE-000 | organization-policy --file=path [--replace] | policy-check --package=path [--package=path] | preset --file=path [--replace] | context | context-summary | context-quality | capabilities [--target=target] | token-usage [--stage=name --budget=number --used=number] | registry [--search=query] [--private=name] | registry configure --file=path [--replace] | skills <search|inspect|install|check|update> [options] | audit-package --package=path | verify-package --package=path --checksum=sha256 | brainstorm [--title=title] | spec [--title=title] | adr [--id=id --title=title] | plan [--title=title] | verify | trace | gate <stage> | recommend [--select=id,id] | status | doctor | repair | uninstall [--dry-run] | target <target> | rollback | confirm | resolve --package=path | update --package=path --target=target | validate [--package=path]",
|
|
1999
|
+
output: "aiw install [--target target] | workflow <start|status|complete> [--id=id] | task <start|status> --ticket=TEAM-123 [--title=text] | approve <stage> | reject <stage> --reason=text | skip <optional-stage> --reason=text | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|show|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | semantic <status|query> | decision <status|triage> | self-validate --ticket=TYPE-000 | organization-policy --file=path [--replace] | policy-check --package=path [--package=path] | preset --file=path [--replace] | context | context-summary | context-quality | capabilities [--target=target] | token-usage [--stage=name --budget=number --used=number] | registry [--search=query] [--private=name] | registry configure --file=path [--replace] | skills <search|inspect|install|check|update> [options] | audit-package --package=path | verify-package --package=path --checksum=sha256 | brainstorm [--title=title] | spec [--title=title] | adr [--id=id --title=title] | plan [--title=title] | verify | trace | gate <stage> | recommend [--select=id,id] | status | doctor | repair | uninstall [--dry-run] | target <target> | rollback | confirm | resolve --package=path | update --package=path --target=target | validate [--package=path]",
|
|
1884
2000
|
};
|
|
1885
2001
|
}
|
|
1886
2002
|
catch (error) {
|
package/docs/telemetry.md
CHANGED
|
@@ -9,7 +9,9 @@ During an interactive `aiw install`, after the optional model questions, the ins
|
|
|
9
9
|
## Controls
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
aiw telemetry
|
|
12
|
+
aiw telemetry show # buffered events summarized by command and outcome
|
|
13
|
+
aiw telemetry show --verbose # ...plus each event verbatim
|
|
14
|
+
aiw telemetry status # configuration only
|
|
13
15
|
aiw telemetry enable --commands=include --outcomes=include
|
|
14
16
|
aiw telemetry enable --commands=exclude --outcomes=include
|
|
15
17
|
aiw telemetry disable
|
package/package.json
CHANGED
package/resources/package.yaml
CHANGED
|
@@ -31,6 +31,7 @@ resources:
|
|
|
31
31
|
- { id: nestjs-development, version: 0.1.0, path: skills/nestjs-development.md }
|
|
32
32
|
- { id: prisma-data-access, version: 0.1.0, path: skills/prisma-data-access.md }
|
|
33
33
|
- { id: vite-frontend, version: 0.1.0, path: skills/vite-frontend.md }
|
|
34
|
+
- { id: ticket, version: 0.1.0, path: skills/ticket.md }
|
|
34
35
|
rules:
|
|
35
36
|
- { id: language-policy, version: 0.1.0, path: rules/language-policy.md }
|
|
36
37
|
- { id: tdd-policy, version: 0.1.0, path: rules/tdd-policy.md }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ticket
|
|
3
|
+
description: Start a feature directly from a ticket or specific requirement without a brainstorming phase.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ticket-Driven Task
|
|
7
|
+
|
|
8
|
+
For every approval or optional skip, use the host's native blocking question widget, not a prose-only prompt when the widget is available. Leave choices unselected and offer "Accept skip" / "Do not skip" (or "Approve and continue" / "Request changes" / "Reject/stop" at gates). Wait for an actual submission before running any `aiw` approval or skip command. The CLI records the decision but cannot verify that a widget was used.
|
|
9
|
+
|
|
10
|
+
Use this skill when the user provides a ticket ID (for example `PROJ-142`) or an already-clear requirement and wants to start work immediately.
|
|
11
|
+
|
|
12
|
+
1. Extract the ticket ID and a short title from the user's request. Start the task with `npx --yes --package=@multileaf/ai-workflow -- aiw task start --ticket=<TICKET> --title="<short title>"` (add `--id=<uuid>` only if the user provides one). If a session is already active, report it with `aiw task status` and stop.
|
|
13
|
+
2. Brainstorming decision: the ticket already provides the requirement, but skipping is still a human decision. Ask with the blocking widget whether to skip brainstorming ("Accept skip" / "Do not skip"). Only after an explicit acceptance, record it with `npx --yes --package=@multileaf/ai-workflow -- aiw skip brainstorming --reason="Requirement provided by ticket <TICKET>"`. If the user prefers to brainstorm after all, run the brainstorming skill instead.
|
|
14
|
+
3. Create the specification linked to the ticket with `npx --yes --package=@multileaf/ai-workflow -- aiw spec --ticket=<TICKET> --title="<title>"`, fill in requirements with stable IDs and Given/When/Then acceptance criteria derived from the ticket, then pass `aiw gate specification`, present the artifact, wait, and record the human reply with `aiw approve specification` (or `aiw reject specification --reason=<feedback>` and revise).
|
|
15
|
+
4. Continue the standard SDD sequence: technical design when the change has architecture impact (`aiw adr`, gate, approval), implementation plan (`aiw plan`, gate, approval), TDD implementation, verification (`aiw verify`, gate, approval), code review, and traceability (`aiw trace`, gate, approval). Check progress at any point with `npx --yes --package=@multileaf/ai-workflow -- aiw task status`, which reports the ticket and the next pending stage.
|
|
16
|
+
5. Complete with `npx --yes --package=@multileaf/ai-workflow -- aiw workflow complete` only after all stages resolve; the ticket reference stays in the archived session and the specification.
|
|
17
|
+
|
|
18
|
+
Never invent a ticket ID: if the user did not provide one, ask for it (or offer the plain `aiw workflow start` flow). Never skip brainstorming without an explicit human acceptance recorded through `aiw skip brainstorming`. Use `npx --yes --package=@multileaf/ai-workflow -- aiw <command>` when no global CLI is available.
|