@multileaf/ai-workflow 0.6.0 → 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 +25 -0
- package/README.md +10 -2
- package/dist/generator.js +1 -0
- 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/migration.d.ts +1 -1
- package/dist/migration.js +5 -4
- package/dist/ownership.d.ts +1 -1
- package/dist/ownership.js +1 -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 +199 -52
- 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,29 @@ 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
|
+
|
|
15
|
+
## [0.6.1] - 2026-10-13
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Root context rendering: syncing a non-empty resource selection now writes the generated project context to the active target's root file (`AGENTS.md` for Codex/Cursor, `CLAUDE.md` for Claude Code, `GEMINI.md` for Gemini CLI), and `aiw target` migrates it between targets. The file is owned by AI Workflow, listed in the sync preview, and an existing hand-written root context is reported as a conflict instead of being overwritten.
|
|
20
|
+
- Decision-model usage in `aiw recommend` is now recorded in `.aiw/usage.yml` (`recommend.decision`) alongside the scan stages.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Release workflows require a single protected-environment approval: the `github-release` job no longer declares the `npm` environment it did not need.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Root context rendering: syncing a non-empty resource selection now writes the generated project context to the active target's root file (`AGENTS.md` for Codex/Cursor, `CLAUDE.md` for Claude Code, `GEMINI.md` for Gemini CLI), and `aiw target` migrates it between targets. The file is owned by AI Workflow, listed in the sync preview, and an existing hand-written root context is reported as a conflict instead of being overwritten.
|
|
29
|
+
|
|
7
30
|
## [0.6.0] - 2026-10-13
|
|
8
31
|
|
|
9
32
|
### Added
|
|
@@ -17,6 +40,7 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
17
40
|
|
|
18
41
|
- Interactive onboarding messages shortened to single-line prompts.
|
|
19
42
|
- Embedding-model classification during onboarding now requires positive evidence (model name contains "embed" or Ollama family "embedding"); the previous heuristic misclassified `gpt-oss:20b` as an embedding model.
|
|
43
|
+
|
|
20
44
|
## [0.5.0] - 2026-10-08
|
|
21
45
|
|
|
22
46
|
### Added
|
|
@@ -33,6 +57,7 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
33
57
|
### Changed
|
|
34
58
|
|
|
35
59
|
- GitHub Actions now use Node 24-native `actions/checkout@v5` and `actions/setup-node@v5`, and CI runs on Node 24.
|
|
60
|
+
|
|
36
61
|
## [0.4.1] - 2026-10-08
|
|
37
62
|
|
|
38
63
|
### Fixed
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ For a Codex installation, the base layout is:
|
|
|
63
63
|
|
|
64
64
|
`.aiw/` stores the provider-neutral manifest, profile, lockfile, checkpoints, bootstrap skill, selected resources, and generated working artifacts. ADRs are stored under `.context/adrs/`. The installer intentionally adds only `ai-init` and the base project state.
|
|
65
65
|
|
|
66
|
-
Run `/ai-init` in the selected coding agent to scan the project and review evidence-based recommendations for skills, rules, agents, hooks, and templates. AI Workflow installs only the resources the user selects, then generates project-specific quality guidance from the detected stack and tools. Existing user files are preserved; conflicts are reported instead of overwritten. Hook resources are rendered according to each adapter's native or compatibility support.
|
|
66
|
+
Run `/ai-init` in the selected coding agent to scan the project and review evidence-based recommendations for skills, rules, agents, hooks, and templates. AI Workflow installs only the resources the user selects, then generates project-specific quality guidance from the detected stack and tools. Syncing a non-empty selection also renders a root context file for the active target — `AGENTS.md` (Codex/Cursor), `CLAUDE.md` (Claude Code), `GEMINI.md` (Gemini CLI) — with the confirmed profile, quality commands, and pointers to `.aiw/` state; `aiw target` migrates it between targets. An existing hand-written root context file is reported as a conflict and never overwritten. Existing user files are preserved; conflicts are reported instead of overwritten. Hook resources are rendered according to each adapter's native or compatibility support.
|
|
67
67
|
|
|
68
68
|
The installer does not modify `.gitignore`. Add `.aiw/generated/` yourself if generated specifications, plans, and artifacts should remain local. Versioned files under `.aiw/checkpoints/` are designed to remain reviewable project history.
|
|
69
69
|
|
|
@@ -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/generator.js
CHANGED
|
@@ -23,6 +23,7 @@ export function generateProjectResources(profile, selected, write) {
|
|
|
23
23
|
write(resolveGeneratedPath("agents/project-context.md"), `# Project Context\n\nThis project uses ${profile.frameworks.map((framework) => (framework === "nextjs" ? "Next.js" : framework)).join(", ")}. Follow the detected project structure and existing framework conventions.\n`);
|
|
24
24
|
if (selected.length > 0) {
|
|
25
25
|
write(resolveGeneratedPath("context/project-profile.md"), `# Confirmed Project Profile\n\n- Languages: ${profile.runtime.languages.join(", ") || "none detected"}\n- Frameworks: ${profile.frameworks.join(", ") || "none detected"}\n- Package manager: ${profile.packageManager}\n- All generated content is in English.\n`);
|
|
26
|
+
write(resolveGeneratedPath("context/root-context.md"), `# AI Workflow Project Context\n\nThis repository is managed by AI Workflow (\`.aiw/\` holds the neutral project state).\n\n- Languages: ${profile.runtime.languages.join(", ") || "none detected"}\n- Frameworks: ${profile.frameworks.join(", ") || "none detected"}\n- Package manager: ${profile.packageManager}${linterCommand ? `\n- Lint: \`${linterCommand}\`` : ""}${formatterCommand ? `\n- Format: \`${formatterCommand}\`` : ""}${uniqueTestCommands.map(({ scope, command }) => `\n- Tests (${scope}): \`${command}\``).join("")}\n\nBefore completing changes, run the project quality commands listed above. Follow installed AI Workflow skills, rules, and agents; the confirmed profile lives in \`.aiw/profile.yml\` and workflow evidence in \`.aiw/checkpoints/\`.\n`);
|
|
26
27
|
write(resolveGeneratedPath("hooks/quality-check.md"), `# Quality Check Hook\n\nRun the available project quality commands before completion.${linterCommand ? `\n\n- Linter: \`${linterCommand}\`` : ""}${formatterCommand ? `\n- Formatter: \`${formatterCommand}\`` : ""}${uniqueTestCommands.map(({ scope, command }) => `\n- ${scope} tests: \`${command}\``).join("")}\n`);
|
|
27
28
|
}
|
|
28
29
|
}
|
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/migration.d.ts
CHANGED
package/dist/migration.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseDocument } from "yaml";
|
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { renderResourcePath } from "./adapter.js";
|
|
4
4
|
import { RESOURCE_TYPES } from "./package-contract.js";
|
|
5
|
+
const MIGRATABLE_TYPES = [...RESOURCE_TYPES, "context"];
|
|
5
6
|
export function planNeutralResourceMigration(neutralRoot, relativeFiles, target) {
|
|
6
7
|
const migrations = [];
|
|
7
8
|
const diagnostics = [];
|
|
@@ -14,9 +15,7 @@ export function planNeutralResourceMigration(neutralRoot, relativeFiles, target)
|
|
|
14
15
|
diagnostics.push({ path: relative, reason: "path is not normalized and project-relative" });
|
|
15
16
|
continue;
|
|
16
17
|
}
|
|
17
|
-
if (extra ||
|
|
18
|
-
parts.length !== 3 ||
|
|
19
|
-
!RESOURCE_TYPES.includes(candidateType)) {
|
|
18
|
+
if (extra || parts.length !== 3 || !MIGRATABLE_TYPES.includes(candidateType)) {
|
|
20
19
|
diagnostics.push({ path: relative, reason: "unsupported neutral resource path" });
|
|
21
20
|
continue;
|
|
22
21
|
}
|
|
@@ -39,7 +38,9 @@ export function planNeutralResourceMigration(neutralRoot, relativeFiles, target)
|
|
|
39
38
|
}
|
|
40
39
|
migrations.push({
|
|
41
40
|
source: join(neutralRoot, relative),
|
|
42
|
-
destination:
|
|
41
|
+
destination: type === "context"
|
|
42
|
+
? renderResourcePath(target, type, id)
|
|
43
|
+
: renderResourcePath(target, type, id),
|
|
43
44
|
type,
|
|
44
45
|
id,
|
|
45
46
|
neutralSource: target === "universal",
|
package/dist/ownership.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type OwnedFile = {
|
|
|
5
5
|
checksum: string;
|
|
6
6
|
kind?: "neutral-resource" | "target-resource" | "ai-init" | "supporting-file";
|
|
7
7
|
target?: Target;
|
|
8
|
-
resourceType?: ResourceType;
|
|
8
|
+
resourceType?: ResourceType | "context";
|
|
9
9
|
resourceId?: string;
|
|
10
10
|
};
|
|
11
11
|
export type OwnershipMetadata = Omit<OwnedFile, "path" | "checksum">;
|
package/dist/ownership.js
CHANGED
|
@@ -49,7 +49,7 @@ export function parseOwnership(content) {
|
|
|
49
49
|
if (file.target !== undefined && !TARGETS.includes(file.target))
|
|
50
50
|
throw new Error(`Ownership inventory target is invalid: ${file.path}`);
|
|
51
51
|
if (file.resourceType !== undefined &&
|
|
52
|
-
!RESOURCE_TYPES.includes(String(file.resourceType)))
|
|
52
|
+
![...RESOURCE_TYPES, "context"].includes(String(file.resourceType)))
|
|
53
53
|
throw new Error(`Ownership inventory resource type is invalid: ${file.path}`);
|
|
54
54
|
if (file.resourceId !== undefined &&
|
|
55
55
|
(typeof file.resourceId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(file.resourceId)))
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { BASE_DIRECTORIES, WORKFLOW_DIRECTORY } from "./constants.js";
|
|
4
|
-
import { detectTarget, isTarget, renderAiInit, renderAiInitPath, renderResourcePath, } from "./adapter.js";
|
|
4
|
+
import { detectTarget, isTarget, renderAiInit, describeResource, renderAiInitPath, renderResourcePath, } from "./adapter.js";
|
|
5
5
|
import { loadBundledResources } from "./bundled-resources.js";
|
|
6
6
|
import { assertSafeProjectPath, withSafeProjectWrites, writeProjectFilesAtomically, } from "./files.js";
|
|
7
7
|
import { parseProjectProfile, profileProject } from "./profile.js";
|
|
@@ -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
|
}
|
|
@@ -1050,8 +1154,13 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1050
1154
|
exitCode: 1,
|
|
1051
1155
|
error: `Decision model ${rankingConfig.model} is not available at ${rankingConfig.baseUrl}.`,
|
|
1052
1156
|
};
|
|
1157
|
+
let decisionTokens = 0;
|
|
1053
1158
|
ranked = await rankRecommendations(recommendations, profile, async (scoped, recs) => {
|
|
1054
|
-
const
|
|
1159
|
+
const request = buildRecommendationRankingRequest(profileSummaryForRanking(scoped), recs);
|
|
1160
|
+
decisionTokens += Math.ceil((request.state.length +
|
|
1161
|
+
Object.values(request.questions).reduce((total, question) => total + question.instructions.length, 0)) /
|
|
1162
|
+
4);
|
|
1163
|
+
const response = await decisionProvider.decide(request);
|
|
1055
1164
|
const answers = {};
|
|
1056
1165
|
for (const [id, answer] of Object.entries(response.answers))
|
|
1057
1166
|
answers[id] =
|
|
@@ -1065,6 +1174,14 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1065
1174
|
: {};
|
|
1066
1175
|
return answers;
|
|
1067
1176
|
});
|
|
1177
|
+
const usagePath = join(aiw, "usage.yml");
|
|
1178
|
+
const entries = fs.exists(usagePath) ? parseTokenUsage(fs.read(usagePath)) : [];
|
|
1179
|
+
fs.write(usagePath, serializeTokenUsage(recordTokenUsage(entries, {
|
|
1180
|
+
stage: "recommend.decision",
|
|
1181
|
+
budget: decisionTokens,
|
|
1182
|
+
used: decisionTokens,
|
|
1183
|
+
model: rankingConfig.model,
|
|
1184
|
+
})));
|
|
1068
1185
|
ordered = ranked.items;
|
|
1069
1186
|
}
|
|
1070
1187
|
const selectedOption = args.find((arg) => arg.startsWith("--select="));
|
|
@@ -1397,8 +1514,11 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1397
1514
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1398
1515
|
assertStageReady(aiw, fs, "specification");
|
|
1399
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);
|
|
1400
1520
|
const path = join(aiw, "generated/specs/specification.md");
|
|
1401
|
-
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`);
|
|
1402
1522
|
return { exitCode: 0, output: `Specification artifact created: ${path}` };
|
|
1403
1523
|
}
|
|
1404
1524
|
if (command === "adr") {
|
|
@@ -1822,7 +1942,15 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1822
1942
|
fs.write(usagePath, serializeTokenUsage(next));
|
|
1823
1943
|
return { exitCode: 0, output: `Token usage recorded: ${stage} (${used}/${budget})` };
|
|
1824
1944
|
}
|
|
1825
|
-
|
|
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
|
+
};
|
|
1826
1954
|
}
|
|
1827
1955
|
if (command === "context-quality") {
|
|
1828
1956
|
const metrics = measureContextQuality(readContextStore(root, fs));
|
|
@@ -1868,7 +1996,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1868
1996
|
}
|
|
1869
1997
|
return {
|
|
1870
1998
|
exitCode: 0,
|
|
1871
|
-
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]",
|
|
1872
2000
|
};
|
|
1873
2001
|
}
|
|
1874
2002
|
catch (error) {
|
|
@@ -2134,6 +2262,25 @@ function syncSelectedResources(fs, root, aiw, profile, selected, apply = false,
|
|
|
2134
2262
|
const rule = path.match(/^generated\/rules\/(project-quality|tdd-project-policy)\.md$/);
|
|
2135
2263
|
if (rule)
|
|
2136
2264
|
addResource("rules", rule[1], content);
|
|
2265
|
+
const rootContext = path === "generated/context/root-context.md";
|
|
2266
|
+
if (rootContext) {
|
|
2267
|
+
const neutralContextPath = join(root, describeResource("universal", "context", "project").path);
|
|
2268
|
+
addOwned(neutralContextPath, content, {
|
|
2269
|
+
kind: "neutral-resource",
|
|
2270
|
+
target: "universal",
|
|
2271
|
+
resourceType: "context",
|
|
2272
|
+
resourceId: "project",
|
|
2273
|
+
});
|
|
2274
|
+
const targetContextPath = describeResource(target, "context", "project").path;
|
|
2275
|
+
if (targetContextPath !== renderAiInitPath(target)) {
|
|
2276
|
+
addOwned(join(root, targetContextPath), content, {
|
|
2277
|
+
kind: "target-resource",
|
|
2278
|
+
target,
|
|
2279
|
+
resourceType: "context",
|
|
2280
|
+
resourceId: "project",
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2137
2284
|
});
|
|
2138
2285
|
const ownershipPath = join(aiw, "ownership.yml");
|
|
2139
2286
|
if (fs.pathType?.(ownershipPath) !== "file")
|
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.
|