phasegate 0.340.0 → 0.341.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 +12 -0
- package/docs/folder_management_rules.md +10 -1
- package/docs/guide/cli-reference.md +23 -3
- package/docs/guide/configuration.md +28 -2
- package/docs/guide/hooks-integration.md +7 -1
- package/docs/guide/skills-overview.md +3 -0
- package/package.json +2 -1
- package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +36 -1
- package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
- package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
- package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
- package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +20 -27
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +3 -0
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
- package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
- package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
- package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
- package/scripts/harness/installation/application/usecases/run-install.ts +5 -5
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +62 -21
- package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
- package/scripts/harness/main.ts +128 -90
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
- package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
- package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +4 -0
- package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
- package/scripts/harness/quick-mode/composition-root.ts +5 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
- package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
- package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
- package/scripts/harness/setup/skill-deployer.ts +3 -45
- package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
- package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
- package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
- package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
- package/scripts/harness/skill-quality/composition-root.ts +14 -7
- package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
- package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
- package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
- package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
- package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
- package/skills/README.md +7 -0
- package/skills/cascade-updater/SKILL.md +37 -109
- package/skills/cascade-updater/references//345/261/244/345/210/245/345/210/244/345/256/232/343/202/254/343/202/244/343/203/211.md +5 -3
- package/skills/release-publisher/SKILL.md +3 -1
- package/skills/skill-creator/SKILL.md +8 -9
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* hook 全体をブロックしない(Fail-safe)。
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { readFile } from 'node:fs/promises';
|
|
22
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
23
|
+
import { dirname, join, posix } from 'node:path';
|
|
23
24
|
|
|
24
25
|
import type { StoryReflectionQueryPort } from '../../domain/ports/story-reflection-query-port.js';
|
|
25
26
|
import { StoryReflectionQueryResult } from '../../domain/value-objects/story-reflection-query-result.js';
|
|
@@ -33,6 +34,8 @@ import {
|
|
|
33
34
|
type StoryReflectionSectionInput,
|
|
34
35
|
} from '../../../phase-dependency-model/infrastructure/config/harness-config-phase-config-provider.js';
|
|
35
36
|
import type { StoryReflectionResult } from '../../../phase-dependency-model/domain/values/story-reflection-result.js';
|
|
37
|
+
import { FileSystemWorkItemDependencyCatalog, type WorkItemDependencyCatalogEntry } from '../../../phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.js';
|
|
38
|
+
import { WorkItemReflectionScopeResolver } from '../../../phase-dependency-model/domain/services/work-item-reflection-scope-resolver.js';
|
|
36
39
|
|
|
37
40
|
export interface FileSystemStoryReflectionQueryAdapterDeps {
|
|
38
41
|
readonly rootDir: string;
|
|
@@ -46,8 +49,10 @@ interface RawPhaseDependenciesSection {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
interface RawConfigDocument {
|
|
52
|
+
readonly agentIntegration?: { readonly preToolUse?: { readonly dependencyReflection?: string } };
|
|
49
53
|
readonly phaseDependencies?: RawPhaseDependenciesSection;
|
|
50
54
|
readonly reporting?: { readonly outputDir?: string };
|
|
55
|
+
readonly paths?: { readonly inceptionDocs?: string; readonly designDocs?: string };
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export class FileSystemStoryReflectionQueryAdapter
|
|
@@ -61,6 +66,106 @@ export class FileSystemStoryReflectionQueryAdapter
|
|
|
61
66
|
this.configPath = deps.configPath;
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/** Defaults to advisory; enforcement requires an explicit trusted configuration value. */
|
|
70
|
+
async checkSessionReflection(unitId: string, workItemId: string): Promise<StoryReflectionQueryResult> {
|
|
71
|
+
const unknown = (reason: string) => StoryReflectionQueryResult.skipped([
|
|
72
|
+
`${workItemId}: WI依存反映は未検証です(既存sessionの許可を維持)。${reason}`,
|
|
73
|
+
]);
|
|
74
|
+
try {
|
|
75
|
+
const raw = JSON.parse(await readFile(this.configPath, 'utf8')) as RawConfigDocument;
|
|
76
|
+
const enforce = raw.agentIntegration?.preToolUse?.dependencyReflection === 'enforce';
|
|
77
|
+
const finish = (result: StoryReflectionQueryResult) => enforce ? result.withSessionEnforcement() : result;
|
|
78
|
+
const incomplete = (reason: string) => enforce
|
|
79
|
+
? finish(StoryReflectionQueryResult.block([reason], [])) : unknown(reason);
|
|
80
|
+
const provider = new HarnessConfigPhaseConfigProvider({
|
|
81
|
+
config: {
|
|
82
|
+
customization: { preset: raw.phaseDependencies?.preset, overrideEnabled: raw.phaseDependencies?.override ?? false },
|
|
83
|
+
storyReflection: raw.phaseDependencies?.storyReflection,
|
|
84
|
+
paths: raw.paths,
|
|
85
|
+
},
|
|
86
|
+
defaultOutputDir: raw.reporting?.outputDir ?? '.harness/reports',
|
|
87
|
+
});
|
|
88
|
+
const config = await provider.getStoryReflectionConfig();
|
|
89
|
+
if (!config.enabled) return StoryReflectionQueryResult.skipped();
|
|
90
|
+
// Validate explicit values before provider defaults can hide an empty root.
|
|
91
|
+
for (const value of [raw.paths?.inceptionDocs, raw.paths?.designDocs]) {
|
|
92
|
+
if (value !== undefined) FileSystemStoryReflectionQueryAdapter.normalizeRoot(value);
|
|
93
|
+
}
|
|
94
|
+
const configuredRoots = await provider.getPathRoots();
|
|
95
|
+
const roots = {
|
|
96
|
+
inceptionDocsRoot: FileSystemStoryReflectionQueryAdapter.normalizeRoot(configuredRoots.inceptionDocsRoot),
|
|
97
|
+
designDocsRoot: FileSystemStoryReflectionQueryAdapter.normalizeRoot(configuredRoots.designDocsRoot),
|
|
98
|
+
};
|
|
99
|
+
const catalog = await new FileSystemWorkItemDependencyCatalog({ rootDir: this.rootDir, inceptionRoot: roots.inceptionDocsRoot }).load([workItemId]);
|
|
100
|
+
const scope = new WorkItemReflectionScopeResolver().resolve(unitId, [workItemId], catalog);
|
|
101
|
+
const fs = new FileSystemStoryReflectionAdapter({ rootDir: this.rootDir, inceptionRoot: roots.inceptionDocsRoot, designDocsRoot: roots.designDocsRoot });
|
|
102
|
+
const checker = new StoryReflectionChecker(fs);
|
|
103
|
+
if (scope.status === 'unknown') {
|
|
104
|
+
const diagnostic = catalog.find(item => item.id === scope.workItemId)?.diagnostic;
|
|
105
|
+
const reason = `${scope.code}: ${scope.workItemId ?? workItemId}${diagnostic ? ` (${diagnostic})` : ''}。対象description.mdのID・所属・depends_onを確認してください。`;
|
|
106
|
+
if (!enforce) return unknown(reason);
|
|
107
|
+
const fallback = FileSystemStoryReflectionQueryAdapter.mapResult(await checker.check(unitId, config, roots), true);
|
|
108
|
+
const warnings = [...fallback.warnings, `WI依存反映は未検証です。従来のUnit範囲で検査しました。${reason}`];
|
|
109
|
+
return finish(fallback.passed ? StoryReflectionQueryResult.pass(warnings) : StoryReflectionQueryResult.block([...fallback.blockers], warnings));
|
|
110
|
+
}
|
|
111
|
+
if (config.mappings.length === 0 && catalog.some(item => item.metadata?.type !== 'chore')) {
|
|
112
|
+
return incomplete('非choreの反映先mappingが空です。storyReflection.mappingsを確認してください。');
|
|
113
|
+
}
|
|
114
|
+
for (const item of catalog) {
|
|
115
|
+
if (item.metadata?.type === 'fix') {
|
|
116
|
+
for (const unit of item.unitIds) {
|
|
117
|
+
const candidates = [...new Set(config.mappings.map(mapping => mapping.resolve({ unitId: unit, storyId: item.id }, roots).product))];
|
|
118
|
+
const reflected = await Promise.all(candidates.map(path => fs.fileContainsStoryAnnotation(path, item.id)));
|
|
119
|
+
if (!reflected.some(Boolean)) return incomplete(`${item.id}: 関係productへの反映を確認してください。候補: ${candidates.join(', ')}。関係カテゴリをレビューし内容と@work-item-idを反映してください。`);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const missing = await this.checkSessionArtifacts(item);
|
|
124
|
+
if (missing) return incomplete(missing);
|
|
125
|
+
}
|
|
126
|
+
const result = await checker.checkResolvedScope(scope, config, roots);
|
|
127
|
+
return finish(FileSystemStoryReflectionQueryAdapter.mapResult(result, true));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return unknown(error instanceof Error ? error.message : String(error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static normalizeRoot(value: unknown): string {
|
|
134
|
+
if (typeof value !== 'string' || value.trim() === '') throw new Error('Invalid documentation root: expected non-empty project-relative path');
|
|
135
|
+
const slashPath = value.replaceAll('\\', '/');
|
|
136
|
+
if (slashPath.startsWith('/') || /^[a-zA-Z]:/.test(slashPath) || slashPath.split('/').includes('..')) {
|
|
137
|
+
throw new Error(`Invalid documentation root: ${value}`);
|
|
138
|
+
}
|
|
139
|
+
const normalized = posix.normalize(slashPath).replace(/\/$/, '');
|
|
140
|
+
if (normalized === '.') throw new Error(`Invalid documentation root: ${value}`);
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async isSessionArtifactFile(relativePath: string): Promise<boolean> {
|
|
145
|
+
try {
|
|
146
|
+
return (await stat(join(this.rootDir, relativePath))).isFile();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT' || (error as NodeJS.ErrnoException).code === 'ENOTDIR') return false;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async checkSessionArtifacts(item: WorkItemDependencyCatalogEntry): Promise<string | undefined> {
|
|
154
|
+
const type = item.metadata?.type;
|
|
155
|
+
if (type === 'chore') return undefined;
|
|
156
|
+
const directory = dirname(item.descriptionPath).replaceAll('\\', '/');
|
|
157
|
+
const required = type === 'refactor' ? ['logical_design.md'] : ['logical_design.md', 'domain_model.md'];
|
|
158
|
+
for (const file of required) {
|
|
159
|
+
const path = `${directory}/${file}`;
|
|
160
|
+
if (!(await this.isSessionArtifactFile(path))) return `${item.id}: 必要成果物を確認できません: ${path}`;
|
|
161
|
+
}
|
|
162
|
+
if (type === 'story' || type === 'issue') {
|
|
163
|
+
const tests = await Promise.all(['unit_test_design.md', 'it_test_design.md', 'scenario_test_design.md'].map(file => this.isSessionArtifactFile(`${directory}/${file}`)));
|
|
164
|
+
if (!tests.some(Boolean)) return `${item.id}: ${directory} のtest設計を確認できません。必要なテスト設計を整えてください。`;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
64
169
|
async checkReflection(unitId: string): Promise<StoryReflectionQueryResult> {
|
|
65
170
|
let raw: RawConfigDocument;
|
|
66
171
|
try {
|
|
@@ -102,6 +207,7 @@ export class FileSystemStoryReflectionQueryAdapter
|
|
|
102
207
|
|
|
103
208
|
private static mapResult(
|
|
104
209
|
result: StoryReflectionResult,
|
|
210
|
+
preserveWarnings = false,
|
|
105
211
|
): StoryReflectionQueryResult {
|
|
106
212
|
const warnings = result.warnings.map(
|
|
107
213
|
(w) =>
|
|
@@ -109,7 +215,7 @@ export class FileSystemStoryReflectionQueryAdapter
|
|
|
109
215
|
);
|
|
110
216
|
|
|
111
217
|
if (!result.isBlocked()) {
|
|
112
|
-
return StoryReflectionQueryResult.pass();
|
|
218
|
+
return StoryReflectionQueryResult.pass(preserveWarnings ? warnings : []);
|
|
113
219
|
}
|
|
114
220
|
|
|
115
221
|
const blockers = result.violations.map(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit agent-integration
|
|
4
|
+
* @work-item-id WI-220
|
|
4
5
|
*
|
|
5
6
|
* HarnessConfigConfigQueryAdapter
|
|
6
7
|
* HarnessConfigV2 の harnesses セクションから Hook 設定を読み取る
|
|
@@ -51,6 +52,8 @@ interface AgentIntegrationStopHookSection {
|
|
|
51
52
|
|
|
52
53
|
interface AgentIntegrationSection {
|
|
53
54
|
stopHook?: AgentIntegrationStopHookSection;
|
|
55
|
+
preToolUse?: { enabled?: boolean };
|
|
56
|
+
postToolUse?: { enabled?: boolean };
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
interface HarnessConfigDocument {
|
|
@@ -119,15 +122,14 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
119
122
|
const config = this.loadConfig();
|
|
120
123
|
const harnesses = config.harnesses ?? {};
|
|
121
124
|
|
|
122
|
-
//
|
|
123
|
-
// pre-tool-use → agentLessonCollection
|
|
124
|
-
// post-tool-use → cascadeUpdate
|
|
125
|
-
// stop → デフォルト有効
|
|
125
|
+
// Explicit hook settings take precedence; absent keys preserve legacy behavior.
|
|
126
126
|
if (hookType === "pre-tool-use") {
|
|
127
|
-
|
|
127
|
+
const enabled = config.agentIntegration?.preToolUse?.enabled;
|
|
128
|
+
return typeof enabled === 'boolean' ? enabled : harnesses.agentLessonCollection ?? true;
|
|
128
129
|
}
|
|
129
130
|
if (hookType === "post-tool-use") {
|
|
130
|
-
|
|
131
|
+
const enabled = config.agentIntegration?.postToolUse?.enabled;
|
|
132
|
+
return typeof enabled === 'boolean' ? enabled : harnesses.cascadeUpdate ?? true;
|
|
131
133
|
}
|
|
132
134
|
// stop はデフォルト有効
|
|
133
135
|
return true;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @layer presentation
|
|
2
2
|
// @unit agent-integration
|
|
3
3
|
// @work-item-id WI-123
|
|
4
|
+
// @work-item-id WI-220
|
|
4
5
|
|
|
5
6
|
import * as fs from 'node:fs/promises';
|
|
6
7
|
import * as path from 'node:path';
|
|
@@ -11,6 +12,8 @@ export async function recordHookSkipEvent(input: {
|
|
|
11
12
|
readonly reason: string;
|
|
12
13
|
readonly targetPaths: readonly string[];
|
|
13
14
|
}): Promise<void> {
|
|
15
|
+
// Expected no-op states are not diagnostic events. Preserve existing history.
|
|
16
|
+
if (input.reason === 'HOOK_DISABLED' || input.reason === 'READ_ONLY') return;
|
|
14
17
|
try {
|
|
15
18
|
const phasegateDir = path.join(input.projectRoot, '.phasegate');
|
|
16
19
|
await fs.mkdir(phasegateDir, { recursive: true });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer presentation
|
|
3
|
+
// @work-item-id WI-220
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import type { HandlePostToolUseOutput } from '../application/dto/handle-post-tool-use-dto.js';
|
|
6
|
+
import { ApplyPatchWriteTargetExtractor } from '../domain/services/apply-patch-write-target-extractor.js';
|
|
7
|
+
import { PreToolUsePayloadNormalizer } from './pre-tool-use-payload-normalizer.js';
|
|
8
|
+
|
|
9
|
+
export function normalizePostToolUse(payload: unknown, processCwd: string): { toolName?: string; cwd: string; targets: string[] } {
|
|
10
|
+
const record = payload !== null && typeof payload === 'object' ? payload as Record<string, unknown> : {};
|
|
11
|
+
const cwd = resolve(typeof record.cwd === 'string' ? record.cwd : processCwd);
|
|
12
|
+
const normalized = new PreToolUsePayloadNormalizer().normalize(payload);
|
|
13
|
+
if (!normalized.ok) {
|
|
14
|
+
// An incomplete known invocation still receives full lint, never a partial target scope.
|
|
15
|
+
const named = typeof record.tool_name === 'string' || typeof record.toolName === 'string' || record.toolCall !== undefined;
|
|
16
|
+
return { toolName: named ? 'unknown' : undefined, cwd, targets: [] };
|
|
17
|
+
}
|
|
18
|
+
const { toolName, toolInput, inputTruncated } = normalized.request;
|
|
19
|
+
let targets: readonly string[] = [];
|
|
20
|
+
if (!inputTruncated && ['Write', 'Edit', 'NotebookEdit', 'str_replace_editor'].includes(toolName)) {
|
|
21
|
+
targets = [toolInput.file_path, toolInput.path, ...(toolInput.paths ?? [])].filter((p): p is string => typeof p === 'string' && p.length > 0);
|
|
22
|
+
} else if (!inputTruncated && toolName === 'apply_patch' && toolInput.command?.trimEnd().endsWith('*** End Patch')
|
|
23
|
+
&& (toolInput.command.match(/^\*\*\* Begin Patch\s*$/gm)?.length ?? 0) === (toolInput.command.match(/^\*\*\* End Patch\s*$/gm)?.length ?? 0)) {
|
|
24
|
+
targets = new ApplyPatchWriteTargetExtractor().extract(toolInput.command).map((target) => target.filePath);
|
|
25
|
+
}
|
|
26
|
+
return { toolName, cwd, targets: [...new Set(targets.map((target) => resolve(cwd, target)))] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderPostToolUseFeedback(output: HandlePostToolUseOutput): { exitCode: number; text: string } {
|
|
30
|
+
if (output.skipReason === 'TIMEOUT_EXCEEDED') {
|
|
31
|
+
return { exitCode: 0, text: '検証未完了: TIMEOUT_EXCEEDED。自動再試行はしません。必要なタイミングで phasegate lint を実行してください。\n' };
|
|
32
|
+
}
|
|
33
|
+
if (output.skipReason === 'READ_ONLY' || output.skipReason === 'HOOK_DISABLED') return { exitCode: 0, text: '' };
|
|
34
|
+
if (output.skipReason) return { exitCode: 0, text: `スキップ: ${output.skipReason}\n` };
|
|
35
|
+
if (output.cliResult && output.cliResult.exitCode !== 0) {
|
|
36
|
+
return { exitCode: 0, text: `Lint診断 (exitCode=${output.cliResult.exitCode}):\n${output.cliResult.stdout}\n${output.cliResult.stderr}\n` };
|
|
37
|
+
}
|
|
38
|
+
return { exitCode: 0, text: output.cliResult?.stderr ?? '' };
|
|
39
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer presentation
|
|
3
3
|
* @unit agent-integration
|
|
4
|
+
* @work-item-id WI-220
|
|
4
5
|
* @work-item-id WI-208
|
|
5
6
|
* @work-item-id WI-323
|
|
6
7
|
* @work-item-id WI-384
|
|
@@ -16,11 +17,7 @@ import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child
|
|
|
16
17
|
import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
|
|
17
18
|
import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
|
|
18
19
|
import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
|
|
19
|
-
|
|
20
|
-
interface PostToolUseHookInput {
|
|
21
|
-
tool_name?: string;
|
|
22
|
-
tool_response?: unknown;
|
|
23
|
-
}
|
|
20
|
+
import { normalizePostToolUse, renderPostToolUseFeedback } from './post-tool-use-feedback.js';
|
|
24
21
|
|
|
25
22
|
async function readStdin(): Promise<string> {
|
|
26
23
|
const chunks: Buffer[] = [];
|
|
@@ -30,8 +27,8 @@ async function readStdin(): Promise<string> {
|
|
|
30
27
|
return Buffer.concat(chunks).toString("utf8");
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
async function findConfigPath(): Promise<string> {
|
|
34
|
-
let dir =
|
|
30
|
+
async function findConfigPath(startDir: string): Promise<string> {
|
|
31
|
+
let dir = startDir;
|
|
35
32
|
while (true) {
|
|
36
33
|
const candidates = [
|
|
37
34
|
path.join(dir, "phasegate.config.json"),
|
|
@@ -47,7 +44,7 @@ async function findConfigPath(): Promise<string> {
|
|
|
47
44
|
if (parent === dir) break;
|
|
48
45
|
dir = parent;
|
|
49
46
|
}
|
|
50
|
-
return path.join(
|
|
47
|
+
return path.join(startDir, "phasegate.config.json");
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
function projectRootForConfig(configPath: string): string {
|
|
@@ -64,20 +61,21 @@ async function main(): Promise<void> {
|
|
|
64
61
|
process.exit(2);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
let input:
|
|
64
|
+
let input: unknown;
|
|
68
65
|
try {
|
|
69
|
-
input = JSON.parse(raw) as
|
|
66
|
+
input = JSON.parse(raw) as unknown;
|
|
70
67
|
} catch {
|
|
71
68
|
process.stderr.write(`不正なJSONです: ${raw}\n`);
|
|
72
69
|
process.exit(2);
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
const
|
|
72
|
+
const request = normalizePostToolUse(input, process.cwd());
|
|
73
|
+
const toolName = request.toolName;
|
|
76
74
|
if (!toolName) {
|
|
77
75
|
// WI-323: PostToolUse はツール実行後の lint フィードバックでありゲートではないため、
|
|
78
76
|
// tool_name 欠落は fail-open でスキップする(WI-314 / github#40 方針)。
|
|
79
77
|
// ※ pre-tool-use-hook の同ガードは書き込みゲートなので fail-closed (exit 2) を維持する。
|
|
80
|
-
const configPath = await findConfigPath();
|
|
78
|
+
const configPath = await findConfigPath(request.cwd);
|
|
81
79
|
await recordHookSkipEvent({
|
|
82
80
|
projectRoot: projectRootForConfig(configPath),
|
|
83
81
|
hookType: "post-tool-use",
|
|
@@ -91,10 +89,13 @@ async function main(): Promise<void> {
|
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
try {
|
|
94
|
-
const configPath = await findConfigPath();
|
|
92
|
+
const configPath = await findConfigPath(request.cwd);
|
|
93
|
+
const projectRoot = projectRootForConfig(configPath);
|
|
94
|
+
const affectedFilePaths = request.targets.map((target) => path.relative(projectRoot, target))
|
|
95
|
+
.filter((target) => target !== '..' && !target.startsWith(`..${path.sep}`) && !path.isAbsolute(target));
|
|
95
96
|
const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
|
|
96
97
|
const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
|
|
97
|
-
const cliExecutorPort = new ChildProcessCliExecutorAdapter();
|
|
98
|
+
const cliExecutorPort = new ChildProcessCliExecutorAdapter({ cwd: projectRoot });
|
|
98
99
|
|
|
99
100
|
const useCase = new HandlePostToolUseUseCase({
|
|
100
101
|
configQueryPort,
|
|
@@ -102,27 +103,19 @@ async function main(): Promise<void> {
|
|
|
102
103
|
cliCommandRegistryPort,
|
|
103
104
|
});
|
|
104
105
|
|
|
105
|
-
const output = await useCase.execute({ toolName, affectedFilePaths
|
|
106
|
+
const output = await useCase.execute({ toolName, affectedFilePaths });
|
|
106
107
|
|
|
107
108
|
if (output.skipReason) {
|
|
108
109
|
await recordHookSkipEvent({
|
|
109
110
|
projectRoot: projectRootForConfig(configPath),
|
|
110
111
|
hookType: "post-tool-use",
|
|
111
112
|
reason: output.skipReason,
|
|
112
|
-
targetPaths:
|
|
113
|
+
targetPaths: affectedFilePaths,
|
|
113
114
|
});
|
|
114
|
-
process.stderr.write(`スキップ: ${output.skipReason}\n`);
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (output.executed && output.cliResult) {
|
|
119
|
-
if (output.cliResult.exitCode !== 0) {
|
|
120
|
-
process.stderr.write(`Lint失敗 (exitCode=${output.cliResult.exitCode})\n`);
|
|
121
|
-
}
|
|
122
|
-
process.exit(output.cliResult.exitCode);
|
|
123
115
|
}
|
|
124
|
-
|
|
125
|
-
process.
|
|
116
|
+
const feedback = renderPostToolUseFeedback(output);
|
|
117
|
+
if (feedback.text) process.stderr.write(feedback.text);
|
|
118
|
+
process.exit(feedback.exitCode);
|
|
126
119
|
} catch (error) {
|
|
127
120
|
process.stderr.write(`実行エラー: ${String(error)}\n`);
|
|
128
121
|
process.exit(2);
|
|
@@ -266,6 +266,9 @@ async function main(): Promise<void> {
|
|
|
266
266
|
const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` : "";
|
|
267
267
|
const unit = session.unit !== undefined ? `, unit=${session.unit}` : "";
|
|
268
268
|
process.stderr.write(`phasegate: write allowed (Full Mode session${workItem}${unit})\n`);
|
|
269
|
+
for (const warning of output.storyReflectionWarnings ?? []) {
|
|
270
|
+
process.stderr.write(`phasegate: dependency reflection warning (advisory): ${warning}\n`);
|
|
271
|
+
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
process.exit(0);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @work-item-id WI-203
|
|
5
5
|
* @work-item-id WI-208
|
|
6
6
|
* @work-item-id WI-323
|
|
7
|
+
* @work-item-id WI-220
|
|
7
8
|
*
|
|
8
9
|
* Stop Hook Adapter
|
|
9
10
|
* Claude Code の Stop Hook エントリポイント
|
|
@@ -14,7 +15,6 @@ import * as path from "node:path";
|
|
|
14
15
|
import { HandleStopUseCase } from "../application/usecases/handle-stop-usecase.js";
|
|
15
16
|
import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child-process-cli-executor-adapter.js";
|
|
16
17
|
import { EnvFileReentryGuardStateAdapter } from "../infrastructure/adapters/env-file-reentry-guard-state-adapter.js";
|
|
17
|
-
import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
|
|
18
18
|
import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
|
|
19
19
|
import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
|
|
20
20
|
|
|
@@ -106,14 +106,12 @@ async function main(): Promise<void> {
|
|
|
106
106
|
const configPath = await findConfigPath();
|
|
107
107
|
const reentryGuardStatePort = new EnvFileReentryGuardStateAdapter({ strategy: "env" });
|
|
108
108
|
const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
|
|
109
|
-
const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
|
|
110
109
|
const cliExecutorPort = new ChildProcessCliExecutorAdapter();
|
|
111
110
|
|
|
112
111
|
const useCase = new HandleStopUseCase({
|
|
113
112
|
reentryGuardStatePort,
|
|
114
113
|
cliExecutorPort,
|
|
115
114
|
configQueryPort,
|
|
116
|
-
cliCommandRegistryPort,
|
|
117
115
|
});
|
|
118
116
|
|
|
119
117
|
const output = await useCase.execute({ sessionId });
|
package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
9
|
import type { FilePath } from '../../domain/value-objects/file-path.js';
|
|
8
10
|
import type { BiomeExecutorPort } from '../../domain/ports/biome-executor-port.js';
|
|
9
11
|
|
|
@@ -34,7 +36,25 @@ export class BiomeCliExecutorAdapter implements BiomeExecutorPort {
|
|
|
34
36
|
|
|
35
37
|
constructor(deps: BiomeCliExecutorAdapterDeps) {
|
|
36
38
|
this.cwd = deps.cwd;
|
|
37
|
-
this.biomeBin = deps.biomeBin ?? 'npx';
|
|
39
|
+
this.biomeBin = deps.biomeBin ?? this.findLocalBin(deps.cwd) ?? 'npx';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private findLocalBin(cwd: string): string | undefined {
|
|
43
|
+
// .cmd execution has different quoting/shell requirements; retain its old route.
|
|
44
|
+
if (process.platform === 'win32') return undefined;
|
|
45
|
+
let directory = resolve(cwd);
|
|
46
|
+
while (true) {
|
|
47
|
+
const candidate = join(directory, 'node_modules', '.bin', 'biome');
|
|
48
|
+
try {
|
|
49
|
+
accessSync(candidate, constants.X_OK);
|
|
50
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
51
|
+
} catch {
|
|
52
|
+
// Match local npm-bin lookup from the working directory toward the root.
|
|
53
|
+
}
|
|
54
|
+
const parent = dirname(directory);
|
|
55
|
+
if (parent === directory) return undefined;
|
|
56
|
+
directory = parent;
|
|
57
|
+
}
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
async executeCheck(files: readonly FilePath[]): Promise<void> {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit biome-ast-engine
|
|
4
4
|
* @work-item-id WI-109
|
|
5
|
+
* @work-item-id WI-220
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import * as ts from 'typescript';
|
|
@@ -74,6 +75,10 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
|
|
|
74
75
|
moduleResolution: ts.ModuleResolutionKind.Node16,
|
|
75
76
|
allowJs: false,
|
|
76
77
|
noEmit: true,
|
|
78
|
+
// Snapshots use syntax only. The workspace scanner already supplies every
|
|
79
|
+
// graph node; libraries and transitive imports are not snapshot inputs.
|
|
80
|
+
noLib: true,
|
|
81
|
+
noResolve: true,
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
const snapshots: SourceModuleSnapshot[] = [];
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json
CHANGED
|
@@ -685,6 +685,21 @@
|
|
|
685
685
|
"type": "object",
|
|
686
686
|
"additionalProperties": false,
|
|
687
687
|
"properties": {
|
|
688
|
+
"preToolUse": {
|
|
689
|
+
"type": "object",
|
|
690
|
+
"additionalProperties": false,
|
|
691
|
+
"properties": {
|
|
692
|
+
"enabled": { "type": "boolean" },
|
|
693
|
+
"dependencyReflection": { "type": "string", "enum": ["advisory", "enforce"] }
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
"postToolUse": {
|
|
697
|
+
"type": "object",
|
|
698
|
+
"additionalProperties": false,
|
|
699
|
+
"properties": {
|
|
700
|
+
"enabled": { "type": "boolean" }
|
|
701
|
+
}
|
|
702
|
+
},
|
|
688
703
|
"stopHook": {
|
|
689
704
|
"type": "object",
|
|
690
705
|
"additionalProperties": false,
|
package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts
CHANGED
|
@@ -109,13 +109,16 @@ function toHarnessError(error: ErrorObject): HarnessErrorWithPath {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// Compile only the selected schema, once per module. Failed compilation is not cached.
|
|
113
|
+
let validateSchemaV2: ValidateFunction | undefined;
|
|
114
|
+
let validateSchemaV3: ValidateFunction | undefined;
|
|
114
115
|
|
|
115
116
|
export class AjvConfigSchemaValidator implements ConfigSchemaValidatorPort {
|
|
116
117
|
validate(document: unknown): readonly HarnessError[] {
|
|
117
118
|
const schemaVersion = detectSchemaVersion(document);
|
|
118
|
-
const validateSchema = schemaVersion === 'v3'
|
|
119
|
+
const validateSchema = schemaVersion === 'v3'
|
|
120
|
+
? (validateSchemaV3 ??= createValidateFunction(SCHEMA_V3_PATH))
|
|
121
|
+
: (validateSchemaV2 ??= createValidateFunction(SCHEMA_V2_PATH));
|
|
119
122
|
const valid = validateSchema(document);
|
|
120
123
|
|
|
121
124
|
if (valid) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// @unit harness-api
|
|
1
2
|
// @layer domain
|
|
2
3
|
// biome-lint-port.ts
|
|
3
4
|
|
|
4
5
|
import type { HarnessError } from '../value-objects/harness-api-response.js';
|
|
5
6
|
|
|
6
7
|
export interface BiomeLintPort {
|
|
7
|
-
runLint(): Promise<{
|
|
8
|
+
runLint(reportTargets?: readonly string[]): Promise<{
|
|
8
9
|
passed: boolean;
|
|
9
10
|
errors: HarnessError[];
|
|
10
11
|
warnings: HarnessError[];
|
|
@@ -312,7 +312,18 @@ export class CommandDispatchService {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
case "phasegate:lint": {
|
|
315
|
-
const
|
|
315
|
+
const target = typeof _flags.target === 'string' && _flags.target.length > 0 ? _flags.target : undefined;
|
|
316
|
+
let reportTargets: readonly string[] | undefined = target === undefined ? undefined : [target];
|
|
317
|
+
if (args.reportTargets !== undefined) {
|
|
318
|
+
const parsed: unknown = JSON.parse(args.reportTargets);
|
|
319
|
+
if (!Array.isArray(parsed) || !parsed.every((item): item is string => typeof item === 'string' && item.length > 0)) {
|
|
320
|
+
throw new Error('lint reportTargets must be an array of non-empty paths');
|
|
321
|
+
}
|
|
322
|
+
reportTargets = parsed;
|
|
323
|
+
}
|
|
324
|
+
const lintResult = reportTargets === undefined
|
|
325
|
+
? await this.ports.biomeLintPort.runLint()
|
|
326
|
+
: await this.ports.biomeLintPort.runLint(reportTargets);
|
|
316
327
|
if (lintResult.passed) {
|
|
317
328
|
const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
|
|
318
329
|
return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
|
package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @unit harness-api
|
|
1
2
|
// @layer infrastructure
|
|
2
3
|
// biome-ast-engine-lint-adapter.ts — BiomeAstEngineLintAdapter
|
|
3
4
|
// Wave 2完了後にリアル実装へ差し替え(旧: @stub: wave2-pending)
|
|
@@ -5,6 +6,7 @@
|
|
|
5
6
|
|
|
6
7
|
import type { BiomeLintPort } from '../../domain/ports/biome-lint-port.js';
|
|
7
8
|
import type { HarnessError } from '../../domain/value-objects/harness-api-response.js';
|
|
9
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
8
10
|
|
|
9
11
|
interface RuleViolation {
|
|
10
12
|
filePath: string;
|
|
@@ -37,7 +39,7 @@ function violationToHarnessError(v: RuleViolation): HarnessError {
|
|
|
37
39
|
export class BiomeAstEngineLintAdapter implements BiomeLintPort {
|
|
38
40
|
private readonly stub: IBiomeAstEngineStub;
|
|
39
41
|
|
|
40
|
-
constructor(stub?: IBiomeAstEngineStub, rootDir = process.cwd()) {
|
|
42
|
+
constructor(stub?: IBiomeAstEngineStub, private readonly rootDir = process.cwd()) {
|
|
41
43
|
this.stub = stub ?? BiomeAstEngineLintAdapter.createRealImpl(rootDir);
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -62,12 +64,18 @@ export class BiomeAstEngineLintAdapter implements BiomeLintPort {
|
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
async runLint(): Promise<{ passed: boolean; errors: HarnessError[]; warnings: HarnessError[] }> {
|
|
67
|
+
async runLint(reportTargets?: readonly string[]): Promise<{ passed: boolean; errors: HarnessError[]; warnings: HarnessError[] }> {
|
|
68
|
+
// Always analyze the complete graph; targets restrict reporting, never analysis.
|
|
66
69
|
const result = await this.stub.runLint();
|
|
70
|
+
const targets = reportTargets?.map((target) => resolve(this.rootDir, target)) ?? [];
|
|
67
71
|
const errors: HarnessError[] = [];
|
|
68
72
|
const warnings: HarnessError[] = [];
|
|
69
73
|
|
|
70
74
|
for (const v of result.violations) {
|
|
75
|
+
if (v.filePath && targets.length > 0 && !targets.some((target) => {
|
|
76
|
+
const rel = relative(target, resolve(this.rootDir, v.filePath));
|
|
77
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
78
|
+
})) continue;
|
|
71
79
|
const harnessError = violationToHarnessError(v);
|
|
72
80
|
if (v.severity === 'warning') {
|
|
73
81
|
warnings.push(harnessError);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-216
|
|
4
|
+
// @work-item-id WI-223
|
|
4
5
|
|
|
5
|
-
export type SkillSet = "core" | "all";
|
|
6
|
+
export type SkillSet = "core" | "all" | "consumer";
|
|
7
|
+
export type SkillCategory = "core" | "aidlc" | "utility" | "guidance";
|
|
6
8
|
|
|
7
9
|
const CORE_SKILLS = [
|
|
8
10
|
"cascade-updater",
|
|
@@ -37,7 +39,31 @@ const AIDLC_SKILLS = [
|
|
|
37
39
|
const UTILITY_SKILLS = ["codex-delegator", "skill-creator"] as const;
|
|
38
40
|
const GUIDANCE_SKILLS = ["phasegate-toolkit-guide", "phasegate-config-doctor", "release-publisher"] as const;
|
|
39
41
|
|
|
42
|
+
export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
|
|
43
|
+
core: CORE_SKILLS,
|
|
44
|
+
aidlc: AIDLC_SKILLS,
|
|
45
|
+
utility: UTILITY_SKILLS,
|
|
46
|
+
guidance: GUIDANCE_SKILLS,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function isSkillSet(value: unknown): value is SkillSet {
|
|
50
|
+
return value === "core" || value === "all" || value === "consumer";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveInstalledSkillSet(metadata: string | null): SkillSet {
|
|
54
|
+
try {
|
|
55
|
+
const value: unknown = JSON.parse(metadata ?? "null");
|
|
56
|
+
if (typeof value === "object" && value !== null && "skillSet" in value && isSkillSet(value.skillSet)) {
|
|
57
|
+
return value.skillSet;
|
|
58
|
+
}
|
|
59
|
+
} catch { /* Legacy installations without readable metadata retain the all set. */ }
|
|
60
|
+
return "all";
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
export function getBundledSkillsForSet(skillSet: SkillSet): string[] {
|
|
41
64
|
if (skillSet === "core") return [...CORE_SKILLS];
|
|
42
|
-
|
|
65
|
+
const all = [...CORE_SKILLS, ...AIDLC_SKILLS, ...UTILITY_SKILLS, ...GUIDANCE_SKILLS];
|
|
66
|
+
return skillSet === "consumer"
|
|
67
|
+
? all.filter(name => name !== "release-publisher" && name !== "skill-creator")
|
|
68
|
+
: all;
|
|
43
69
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// @work-item-id WI-216
|
|
7
7
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { getBundledSkillsForSet,
|
|
9
|
+
import { getBundledSkillsForSet, resolveInstalledSkillSet } from "../bundled-skill-selection.js";
|
|
10
10
|
import type { CheckId } from "../../domain/check-id.js";
|
|
11
11
|
import { DiagnosticFinding, type DiagnosticSeverity } from "../../domain/diagnostic-finding.js";
|
|
12
12
|
import type { RepairMode } from "../../domain/repair-mode.js";
|
|
@@ -73,7 +73,7 @@ export function skillDirectoryLooksValid(files: readonly string[]): boolean {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function phasegateSkillDirectoryLooksComplete(files: readonly string[], metadata: string | null): boolean {
|
|
76
|
-
const skillSet
|
|
76
|
+
const skillSet = resolveInstalledSkillSet(metadata);
|
|
77
77
|
const expectedSkills = getBundledSkillsForSet(skillSet);
|
|
78
78
|
return expectedSkills.every((skill) => files.some((file) => {
|
|
79
79
|
const normalized = file.replaceAll("\\", "/");
|