@multileaf/ai-workflow 0.6.0 → 0.6.1
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 +17 -0
- package/README.md +1 -1
- package/dist/generator.js +1 -0
- 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/workflow.js +33 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
- Decision-model usage in `aiw recommend` is now recorded in `.aiw/usage.yml` (`recommend.decision`) alongside the scan stages.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Release workflows require a single protected-environment approval: the `github-release` job no longer declares the `npm` environment it did not need.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
7
22
|
## [0.6.0] - 2026-10-13
|
|
8
23
|
|
|
9
24
|
### Added
|
|
@@ -17,6 +32,7 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
17
32
|
|
|
18
33
|
- Interactive onboarding messages shortened to single-line prompts.
|
|
19
34
|
- 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.
|
|
35
|
+
|
|
20
36
|
## [0.5.0] - 2026-10-08
|
|
21
37
|
|
|
22
38
|
### Added
|
|
@@ -33,6 +49,7 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
33
49
|
### Changed
|
|
34
50
|
|
|
35
51
|
- GitHub Actions now use Node 24-native `actions/checkout@v5` and `actions/setup-node@v5`, and CI runs on Node 24.
|
|
52
|
+
|
|
36
53
|
## [0.4.1] - 2026-10-08
|
|
37
54
|
|
|
38
55
|
### 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
|
|
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/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/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";
|
|
@@ -1050,8 +1050,13 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1050
1050
|
exitCode: 1,
|
|
1051
1051
|
error: `Decision model ${rankingConfig.model} is not available at ${rankingConfig.baseUrl}.`,
|
|
1052
1052
|
};
|
|
1053
|
+
let decisionTokens = 0;
|
|
1053
1054
|
ranked = await rankRecommendations(recommendations, profile, async (scoped, recs) => {
|
|
1054
|
-
const
|
|
1055
|
+
const request = buildRecommendationRankingRequest(profileSummaryForRanking(scoped), recs);
|
|
1056
|
+
decisionTokens += Math.ceil((request.state.length +
|
|
1057
|
+
Object.values(request.questions).reduce((total, question) => total + question.instructions.length, 0)) /
|
|
1058
|
+
4);
|
|
1059
|
+
const response = await decisionProvider.decide(request);
|
|
1055
1060
|
const answers = {};
|
|
1056
1061
|
for (const [id, answer] of Object.entries(response.answers))
|
|
1057
1062
|
answers[id] =
|
|
@@ -1065,6 +1070,13 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1065
1070
|
: {};
|
|
1066
1071
|
return answers;
|
|
1067
1072
|
});
|
|
1073
|
+
const usagePath = join(aiw, "usage.yml");
|
|
1074
|
+
const entries = fs.exists(usagePath) ? parseTokenUsage(fs.read(usagePath)) : [];
|
|
1075
|
+
fs.write(usagePath, serializeTokenUsage(recordTokenUsage(entries, {
|
|
1076
|
+
stage: "recommend.decision",
|
|
1077
|
+
budget: decisionTokens,
|
|
1078
|
+
used: decisionTokens,
|
|
1079
|
+
})));
|
|
1068
1080
|
ordered = ranked.items;
|
|
1069
1081
|
}
|
|
1070
1082
|
const selectedOption = args.find((arg) => arg.startsWith("--select="));
|
|
@@ -2134,6 +2146,25 @@ function syncSelectedResources(fs, root, aiw, profile, selected, apply = false,
|
|
|
2134
2146
|
const rule = path.match(/^generated\/rules\/(project-quality|tdd-project-policy)\.md$/);
|
|
2135
2147
|
if (rule)
|
|
2136
2148
|
addResource("rules", rule[1], content);
|
|
2149
|
+
const rootContext = path === "generated/context/root-context.md";
|
|
2150
|
+
if (rootContext) {
|
|
2151
|
+
const neutralContextPath = join(root, describeResource("universal", "context", "project").path);
|
|
2152
|
+
addOwned(neutralContextPath, content, {
|
|
2153
|
+
kind: "neutral-resource",
|
|
2154
|
+
target: "universal",
|
|
2155
|
+
resourceType: "context",
|
|
2156
|
+
resourceId: "project",
|
|
2157
|
+
});
|
|
2158
|
+
const targetContextPath = describeResource(target, "context", "project").path;
|
|
2159
|
+
if (targetContextPath !== renderAiInitPath(target)) {
|
|
2160
|
+
addOwned(join(root, targetContextPath), content, {
|
|
2161
|
+
kind: "target-resource",
|
|
2162
|
+
target,
|
|
2163
|
+
resourceType: "context",
|
|
2164
|
+
resourceId: "project",
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2137
2168
|
});
|
|
2138
2169
|
const ownershipPath = join(aiw, "ownership.yml");
|
|
2139
2170
|
if (fs.pathType?.(ownershipPath) !== "file")
|