phasegate 0.160.3 → 0.160.5
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/package.json +1 -1
- package/scripts/harness/ci-governance/application/dto/scaffold-design-input.ts +2 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-design-output.ts +2 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-design-usecase.ts +17 -0
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-design-handler.ts +25 -3
- package/scripts/harness/integrations/pre-commit.ts +7 -1
- package/scripts/harness/main.ts +61 -26
- package/scripts/harness/setup/skill-deployer.ts +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.160.5] - 2026-05-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-189 — umbrella CLI UX cleanup** — aligns `validate --format json` with the global JSON contract, makes `scaffold-design` default to dry-run with explicit `--apply`, fixes bypass audit empty-range wording, and synchronizes public help for scaffold, quick-mode, and delegate commands.
|
|
15
|
+
|
|
16
|
+
## [0.160.4] - 2026-05-14
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **WI-184 — skill catalog CLI** — fixes `phasegate skills list` so guidance-category skills no longer crash an undefined accumulator, shares the `SKILL.md` catalog path with `skills info`, and covers empty skill catalogs.
|
|
21
|
+
|
|
10
22
|
## [0.160.3] - 2026-05-14
|
|
11
23
|
|
|
12
24
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @unit ci-governance
|
|
2
2
|
// @layer application
|
|
3
|
+
// @work-item-id WI-189
|
|
3
4
|
|
|
4
5
|
export interface ScaffoldDesignOutput {
|
|
5
6
|
readonly targetPath: string;
|
|
6
7
|
readonly templatePath: string;
|
|
7
8
|
readonly unit: string;
|
|
8
9
|
readonly phase: string;
|
|
10
|
+
readonly dryRun: boolean;
|
|
9
11
|
readonly written: boolean;
|
|
10
12
|
readonly alreadyExists: boolean;
|
|
11
13
|
readonly overwritten: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @unit ci-governance
|
|
2
2
|
// @layer application
|
|
3
|
+
// @work-item-id WI-189
|
|
3
4
|
|
|
4
5
|
import { DesignPhase } from '../../domain/value-objects/design-phase.js';
|
|
5
6
|
import type { TemplateRepositoryPort } from '../../domain/ports/template-repository-port.js';
|
|
@@ -21,18 +22,33 @@ export class ScaffoldDesignUseCase {
|
|
|
21
22
|
}
|
|
22
23
|
const phase = DesignPhase.create(input.phase);
|
|
23
24
|
const unit = input.unit.trim();
|
|
25
|
+
const dryRun = input.dryRun === true;
|
|
24
26
|
const force = input.force === true;
|
|
25
27
|
|
|
26
28
|
const templatePath = this.templates.resolvePath(phase);
|
|
27
29
|
const targetPath = this.writer.resolvePath(unit, phase);
|
|
28
30
|
const alreadyExists = await this.writer.exists(unit, phase);
|
|
29
31
|
|
|
32
|
+
if (dryRun) {
|
|
33
|
+
return {
|
|
34
|
+
targetPath,
|
|
35
|
+
templatePath,
|
|
36
|
+
unit,
|
|
37
|
+
phase: phase.value,
|
|
38
|
+
dryRun: true,
|
|
39
|
+
written: false,
|
|
40
|
+
alreadyExists,
|
|
41
|
+
overwritten: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
if (alreadyExists && !force) {
|
|
31
46
|
return {
|
|
32
47
|
targetPath,
|
|
33
48
|
templatePath,
|
|
34
49
|
unit,
|
|
35
50
|
phase: phase.value,
|
|
51
|
+
dryRun: false,
|
|
36
52
|
written: false,
|
|
37
53
|
alreadyExists: true,
|
|
38
54
|
overwritten: false,
|
|
@@ -48,6 +64,7 @@ export class ScaffoldDesignUseCase {
|
|
|
48
64
|
templatePath,
|
|
49
65
|
unit,
|
|
50
66
|
phase: phase.value,
|
|
67
|
+
dryRun: false,
|
|
51
68
|
written: true,
|
|
52
69
|
alreadyExists,
|
|
53
70
|
overwritten: alreadyExists && force,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @unit ci-governance
|
|
2
2
|
// @layer presentation
|
|
3
|
+
// @work-item-id WI-189
|
|
3
4
|
|
|
4
5
|
import { DesignPhase } from '../../domain/value-objects/design-phase.js';
|
|
5
6
|
import type { ScaffoldDesignUseCase } from '../../application/usecases/scaffold-design-usecase.js';
|
|
@@ -7,6 +8,8 @@ import type { ScaffoldDesignUseCase } from '../../application/usecases/scaffold-
|
|
|
7
8
|
export interface ScaffoldDesignHandlerArgs {
|
|
8
9
|
readonly unit?: string;
|
|
9
10
|
readonly phase?: string;
|
|
11
|
+
readonly dryRun?: boolean;
|
|
12
|
+
readonly apply?: boolean;
|
|
10
13
|
readonly force?: boolean;
|
|
11
14
|
readonly format?: 'human' | 'json';
|
|
12
15
|
}
|
|
@@ -21,6 +24,12 @@ export class ScaffoldDesignHandler {
|
|
|
21
24
|
|
|
22
25
|
async handle(args: ScaffoldDesignHandlerArgs): Promise<ScaffoldDesignHandlerResult> {
|
|
23
26
|
const format = args.format ?? 'human';
|
|
27
|
+
const apply = args.apply === true;
|
|
28
|
+
const dryRun = args.dryRun === true || !apply;
|
|
29
|
+
|
|
30
|
+
if (args.dryRun === true && apply) {
|
|
31
|
+
return this.fail(format, '--dry-run と --apply は同時に指定できません', 2);
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
if (!args.unit || args.unit.trim().length === 0) {
|
|
26
35
|
return this.fail(format, '--unit <unit-id> は必須です', 2);
|
|
@@ -45,6 +54,7 @@ export class ScaffoldDesignHandler {
|
|
|
45
54
|
result = await this.useCase.execute({
|
|
46
55
|
unit: args.unit,
|
|
47
56
|
phase: args.phase,
|
|
57
|
+
dryRun,
|
|
48
58
|
force: args.force,
|
|
49
59
|
});
|
|
50
60
|
} catch (err) {
|
|
@@ -61,10 +71,22 @@ export class ScaffoldDesignHandler {
|
|
|
61
71
|
|
|
62
72
|
if (result.alreadyExists && !result.written) {
|
|
63
73
|
return {
|
|
64
|
-
exitCode: 2,
|
|
74
|
+
exitCode: result.dryRun ? 0 : 2,
|
|
75
|
+
output: [
|
|
76
|
+
result.dryRun ? `dry-run: 既に存在します: ${result.targetPath}` : `既に存在します: ${result.targetPath}`,
|
|
77
|
+
result.dryRun ? '書き込むには --apply を指定してください。' : '上書きするには --force を指定してください。',
|
|
78
|
+
].join('\n'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (result.dryRun) {
|
|
83
|
+
return {
|
|
84
|
+
exitCode: 0,
|
|
65
85
|
output: [
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
`dry-run: 設計文書を生成予定: ${result.targetPath}`,
|
|
87
|
+
`テンプレ: ${result.templatePath}`,
|
|
88
|
+
`Unit: ${result.unit} / phase: ${result.phase}`,
|
|
89
|
+
'書き込むには --apply を指定してください。',
|
|
68
90
|
].join('\n'),
|
|
69
91
|
};
|
|
70
92
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @layer presentation
|
|
4
4
|
* @work-item-id WI-141
|
|
5
5
|
* @work-item-id WI-109
|
|
6
|
+
* @work-item-id WI-189
|
|
6
7
|
*
|
|
7
8
|
* Pre-commit CLI entry.
|
|
8
9
|
* Runs L2 validators against staged TypeScript files AND design-document
|
|
@@ -533,6 +534,11 @@ function hasCompleteBypassTrailerSet(results: readonly BypassTrailerValidationRe
|
|
|
533
534
|
return results.some((result) => result.hasAnyBypassTrailer && result.complete);
|
|
534
535
|
}
|
|
535
536
|
|
|
537
|
+
function toBypassAuditStdout(stdout: string, changedFiles: readonly string[]): string {
|
|
538
|
+
if (changedFiles.length > 0) return stdout;
|
|
539
|
+
return stdout.replace("No staged files to check. Skipping.", "No changed files in range to check. Skipping.");
|
|
540
|
+
}
|
|
541
|
+
|
|
536
542
|
export async function runBypassAudit(
|
|
537
543
|
deps: PreCommitDeps,
|
|
538
544
|
options: BypassAuditOptions = {},
|
|
@@ -554,7 +560,7 @@ export async function runBypassAudit(
|
|
|
554
560
|
|
|
555
561
|
const sections = [
|
|
556
562
|
`${BOLD}[phasegate]${RESET} Bypass audit (${baseRef}..${headRef})`,
|
|
557
|
-
result.stdout,
|
|
563
|
+
toBypassAuditStdout(result.stdout, changedFiles),
|
|
558
564
|
];
|
|
559
565
|
let exitCode = result.exitCode;
|
|
560
566
|
if (result.exitCode !== 0 && !hasCompleteBypassTrailerSet(bypassResults)) {
|
package/scripts/harness/main.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* @work-item-id WI-171 / WI-172 / WI-173
|
|
7
7
|
* @work-item-id WI-175
|
|
8
8
|
* @work-item-id WI-176
|
|
9
|
+
* @work-item-id WI-184
|
|
10
|
+
* @work-item-id WI-189
|
|
9
11
|
*
|
|
10
12
|
* Phasegate CLI エントリポイント。
|
|
11
13
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -64,7 +66,9 @@ import {
|
|
|
64
66
|
deploySkills,
|
|
65
67
|
getCategoryForSkill,
|
|
66
68
|
getHarnessVersion,
|
|
69
|
+
getSkillMarkdownPath,
|
|
67
70
|
initHarnessConfig,
|
|
71
|
+
listAvailableSkillNames,
|
|
68
72
|
} from "./setup/skill-deployer.js";
|
|
69
73
|
import { createSkillQualityHandlers } from "./skill-quality/composition-root.js";
|
|
70
74
|
import { createTraceabilityModelModule } from "./traceability-model/composition-root.js";
|
|
@@ -153,7 +157,8 @@ Setup:
|
|
|
153
157
|
--with-husky, --with-ci, --yes)
|
|
154
158
|
update-skills Alias for reconcile (kept for compatibility)
|
|
155
159
|
doctor Diagnose silent installation failures (--json, --strict, --agent <claude|codex|both>, --report-out <path>)
|
|
156
|
-
scaffold-wi <unit> <
|
|
160
|
+
scaffold-wi <unit|_cross> <story|issue|chore>
|
|
161
|
+
Create docs/inception/{unit}/WI-XXX/description.md
|
|
157
162
|
emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
|
|
158
163
|
install Install phasegate managed files (--dry-run|--apply, --force)
|
|
159
164
|
uninstall Uninstall phasegate managed files (--dry-run|--apply, --force)
|
|
@@ -180,8 +185,9 @@ Commands:
|
|
|
180
185
|
|
|
181
186
|
lint Run lint checks (--json, --target <path>)
|
|
182
187
|
|
|
183
|
-
validate Run validators (--layer L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci)
|
|
188
|
+
validate Run validators (--layer L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci|json, --json)
|
|
184
189
|
ci-check CI check (--quick for quick mode, --fail-on-reject, --dry-run, --files)
|
|
190
|
+
check-change-category Classify changed paths for quick mode (--paths <csv>, --format human|json)
|
|
185
191
|
|
|
186
192
|
phasegate:check-ready Check ready status (--json)
|
|
187
193
|
phasegate:check-phase Check phase gate (--unit <unitId>, --json)
|
|
@@ -208,7 +214,7 @@ Gate semantics:
|
|
|
208
214
|
config:plan Plan safe config changes (--intent <l4-strict|codex-hooks|ci-fail-on-warning|baseline-reset|quick-mode-strict>, --dry-run, --json)
|
|
209
215
|
ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
|
|
210
216
|
baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
|
|
211
|
-
scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --force, --json)
|
|
217
|
+
scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --dry-run|--apply, --force, --json)
|
|
212
218
|
|
|
213
219
|
skill:execute-tdd-cycle Execute TDD cycle (--unit, --story, --desc, --phase RED|GREEN|REFACTOR, --passed)
|
|
214
220
|
skill:check-coverage Check coverage (--story <storyId>, --json)
|
|
@@ -406,7 +412,8 @@ function parseValidateFormat(args: readonly string[]): "human" | "agent" | "ci"
|
|
|
406
412
|
const raw = parseFlag(args, "--format");
|
|
407
413
|
if (raw === undefined) return undefined;
|
|
408
414
|
if (raw === "human" || raw === "agent" || raw === "ci") return raw;
|
|
409
|
-
|
|
415
|
+
if (raw === "json") return "ci";
|
|
416
|
+
throw new Error(`Invalid --format value for validate: '${raw}'. Supported values: human, agent, ci, json.`);
|
|
410
417
|
}
|
|
411
418
|
|
|
412
419
|
function levenshtein(a: string, b: string): number {
|
|
@@ -543,7 +550,8 @@ Run validators against the project. Without --layer, runs all enabled validator
|
|
|
543
550
|
|
|
544
551
|
Options:
|
|
545
552
|
--layer <L0|L2|L3|L4> Run only the specified validator layer; L0 prints runtime hook info
|
|
546
|
-
--json
|
|
553
|
+
--format <human|agent|ci|json> Output format; json is an alias for ci JSON
|
|
554
|
+
--json Output machine-readable JSON when --format is omitted
|
|
547
555
|
--help, -h Show this help`,
|
|
548
556
|
lint: `Usage: phasegate lint [options]
|
|
549
557
|
|
|
@@ -623,6 +631,28 @@ Options:
|
|
|
623
631
|
Examples:
|
|
624
632
|
phasegate check-change-category --paths src/foo.ts,src/bar.ts
|
|
625
633
|
phasegate check-change-category --paths src/foo.ts --format json`,
|
|
634
|
+
"scaffold-wi": `Usage: phasegate scaffold-wi <unit|_cross> <story|issue|chore>
|
|
635
|
+
|
|
636
|
+
Create docs/inception/{unit}/WI-XXX/description.md.
|
|
637
|
+
|
|
638
|
+
Arguments:
|
|
639
|
+
<unit|_cross> Unit id or _cross for cross-cutting work items.
|
|
640
|
+
<story|issue|chore> Work item type.
|
|
641
|
+
|
|
642
|
+
Options:
|
|
643
|
+
--help, -h Show this help`,
|
|
644
|
+
"scaffold-design": `Usage: phasegate scaffold-design --unit <id> --phase <phase> [options]
|
|
645
|
+
|
|
646
|
+
Scaffold a product construction design document.
|
|
647
|
+
|
|
648
|
+
Options:
|
|
649
|
+
--unit <id> Unit id under docs/product/construction.
|
|
650
|
+
--phase <phase> logical, domain, uiux, unit-test, or it-test.
|
|
651
|
+
--dry-run Preview target and template without writing (default).
|
|
652
|
+
--apply Write the scaffold.
|
|
653
|
+
--force Overwrite an existing target when applying.
|
|
654
|
+
--json Output machine-readable JSON.
|
|
655
|
+
--help, -h Show this help`,
|
|
626
656
|
"ci:generate-template": `Usage: phasegate ci:generate-template [options]
|
|
627
657
|
|
|
628
658
|
Generates a CI template configuration.
|
|
@@ -640,6 +670,15 @@ Options:
|
|
|
640
670
|
Examples:
|
|
641
671
|
phasegate ci:generate-template --type aidlc-gate
|
|
642
672
|
phasegate ci:generate-template --preset strict --type pre-commit --render`,
|
|
673
|
+
"delegate-sonnet": `Usage: phasegate delegate-sonnet [...args]
|
|
674
|
+
|
|
675
|
+
Delegate a task to Sonnet 4.6 by forwarding all args to scripts/delegate-sonnet.sh.
|
|
676
|
+
|
|
677
|
+
Arguments:
|
|
678
|
+
[...args] Task text and options consumed by the delegate script.
|
|
679
|
+
|
|
680
|
+
Options:
|
|
681
|
+
--help, -h Show this help`,
|
|
643
682
|
};
|
|
644
683
|
|
|
645
684
|
function printSubcommandHelp(command: string): void {
|
|
@@ -2188,7 +2227,7 @@ async function main(): Promise<void> {
|
|
|
2188
2227
|
const layer = parseFlag(args, "--layer") as "L0" | "L2" | "L3" | "L4" | "all" | undefined;
|
|
2189
2228
|
const unit = parseFlag(args, "--unit");
|
|
2190
2229
|
const phase = parseFlag(args, "--phase");
|
|
2191
|
-
const format = parseValidateFormat(args);
|
|
2230
|
+
const format = parseValidateFormat(args) ?? (json ? "ci" : undefined);
|
|
2192
2231
|
// WI-094 / ADR-017: --fail-on-warning / --no-fail-on-warning / 未指定→config値
|
|
2193
2232
|
const failOnWarning = parseTriStateFlag(args, "--fail-on-warning", "--no-fail-on-warning");
|
|
2194
2233
|
const noL4 = hasFlag(args, "--no-l4");
|
|
@@ -2203,7 +2242,7 @@ async function main(): Promise<void> {
|
|
|
2203
2242
|
targetPaths,
|
|
2204
2243
|
});
|
|
2205
2244
|
console.log(result.output);
|
|
2206
|
-
if (layer === "L2" || layer === "all") {
|
|
2245
|
+
if ((layer === "L2" || layer === "all") && format !== "ci") {
|
|
2207
2246
|
await printStoryReflectionValidationSummary(rootDir, unit);
|
|
2208
2247
|
}
|
|
2209
2248
|
process.exit(result.exitCode);
|
|
@@ -2489,11 +2528,15 @@ Examples:
|
|
|
2489
2528
|
const mod = buildCiGovernance(rootDir, harnessRoot);
|
|
2490
2529
|
const unit = parseFlag(args, "--unit") ?? "";
|
|
2491
2530
|
const phase = parseFlag(args, "--phase") ?? "";
|
|
2531
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
2532
|
+
const apply = hasFlag(args, "--apply");
|
|
2492
2533
|
const force = hasFlag(args, "--force");
|
|
2493
2534
|
const format = json ? "json" : "human";
|
|
2494
2535
|
const result = await mod.scaffoldDesignHandler.handle({
|
|
2495
2536
|
unit,
|
|
2496
2537
|
phase,
|
|
2538
|
+
dryRun,
|
|
2539
|
+
apply,
|
|
2497
2540
|
force,
|
|
2498
2541
|
format,
|
|
2499
2542
|
});
|
|
@@ -2753,25 +2796,16 @@ Examples:
|
|
|
2753
2796
|
// ── skills ──
|
|
2754
2797
|
case "skills": {
|
|
2755
2798
|
const subCommand = args[1];
|
|
2756
|
-
const skillsRoot = join(harnessRoot, "skills");
|
|
2757
2799
|
|
|
2758
2800
|
if (subCommand === "list") {
|
|
2759
|
-
const
|
|
2760
|
-
const
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
} catch {
|
|
2768
|
-
// skip directories without SKILL.md
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
skills.sort();
|
|
2773
|
-
|
|
2774
|
-
const grouped: Record<string, string[]> = { core: [], aidlc: [], utility: [], unknown: [] };
|
|
2801
|
+
const skills = await listAvailableSkillNames(harnessRoot);
|
|
2802
|
+
const grouped: Record<"core" | "aidlc" | "utility" | "guidance" | "unknown", string[]> = {
|
|
2803
|
+
core: [],
|
|
2804
|
+
aidlc: [],
|
|
2805
|
+
utility: [],
|
|
2806
|
+
guidance: [],
|
|
2807
|
+
unknown: [],
|
|
2808
|
+
};
|
|
2775
2809
|
for (const name of skills) {
|
|
2776
2810
|
const cat = getCategoryForSkill(name) ?? "unknown";
|
|
2777
2811
|
grouped[cat].push(name);
|
|
@@ -2783,8 +2817,9 @@ Examples:
|
|
|
2783
2817
|
core: "Core — Quality Defense",
|
|
2784
2818
|
aidlc: "AIDLC — Development Workflow",
|
|
2785
2819
|
utility: "Utility",
|
|
2820
|
+
guidance: "Guidance",
|
|
2786
2821
|
};
|
|
2787
|
-
for (const cat of ["core", "aidlc", "utility", "unknown"] as const) {
|
|
2822
|
+
for (const cat of ["core", "aidlc", "utility", "guidance", "unknown"] as const) {
|
|
2788
2823
|
if (grouped[cat].length === 0) continue;
|
|
2789
2824
|
const label = labels[cat] ?? "Other";
|
|
2790
2825
|
console.log(` [${label}] (${grouped[cat].length})`);
|
|
@@ -2803,7 +2838,7 @@ Examples:
|
|
|
2803
2838
|
process.exit(2);
|
|
2804
2839
|
}
|
|
2805
2840
|
const { promises: fs } = await import("node:fs");
|
|
2806
|
-
const skillMdPath =
|
|
2841
|
+
const skillMdPath = getSkillMarkdownPath(harnessRoot, skillName);
|
|
2807
2842
|
try {
|
|
2808
2843
|
const content = await fs.readFile(skillMdPath, "utf-8");
|
|
2809
2844
|
console.log(content);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @layer infrastructure
|
|
3
3
|
// @work-item-id WI-086 / WI-087
|
|
4
4
|
// @work-item-id WI-127
|
|
5
|
+
// @work-item-id WI-184
|
|
5
6
|
// Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
|
|
6
7
|
|
|
7
8
|
import { promises as fs } from "node:fs";
|
|
@@ -78,6 +79,33 @@ export function getCategoryForSkill(skillName: string): SkillCategory | null {
|
|
|
78
79
|
return null;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
export function getSkillMarkdownPath(harnessRoot: string, skillName: string): string {
|
|
83
|
+
return join(harnessRoot, SKILLS_SOURCE_DIR, skillName, "SKILL.md");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function listAvailableSkillNames(harnessRoot: string): Promise<string[]> {
|
|
87
|
+
const skillsRoot = join(harnessRoot, SKILLS_SOURCE_DIR);
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const skills: string[] = [];
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!entry.isDirectory()) continue;
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(getSkillMarkdownPath(harnessRoot, entry.name));
|
|
100
|
+
skills.push(entry.name);
|
|
101
|
+
} catch {
|
|
102
|
+
// Directories without SKILL.md are not catalog entries.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return skills.sort();
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
export interface DeployResult {
|
|
82
110
|
deployedSkills: string[];
|
|
83
111
|
targetDir: string;
|