phasegate 0.191.0 → 0.222.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ja.md +52 -15
  3. package/README.md +39 -11
  4. package/docs/ADR/030-injection-threat-model-and-trust-root.md +145 -0
  5. package/docs/guide/hooks-integration.md +101 -1
  6. package/docs/guide/installation.md +1 -1
  7. package/docs/guide/quick-vs-full-mode.md +1 -1
  8. package/docs/guide/skills-overview.md +7 -8
  9. package/package.json +1 -1
  10. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +131 -63
  11. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +31 -4
  12. package/scripts/harness/agent-integration/presentation/spotlight.ts +65 -0
  13. package/scripts/harness/biome-ast-engine/application/mappers/build-harness-error-payload-output-mapper.ts +3 -3
  14. package/scripts/harness/biome-ast-engine/infrastructure/mappers/rule-violation-code-mapper.ts +3 -3
  15. package/scripts/harness/biome-ast-engine/presentation/cli/harness-lint-command-handler.ts +6 -4
  16. package/scripts/harness/ci-governance/application/dto/pin-integrity-input.ts +8 -0
  17. package/scripts/harness/ci-governance/application/dto/pin-integrity-output.ts +9 -0
  18. package/scripts/harness/ci-governance/application/dto/verify-integrity-input.ts +7 -0
  19. package/scripts/harness/ci-governance/application/dto/verify-integrity-output.ts +10 -0
  20. package/scripts/harness/ci-governance/application/usecases/pin-integrity-usecase.ts +60 -0
  21. package/scripts/harness/ci-governance/application/usecases/verify-integrity-usecase.ts +46 -0
  22. package/scripts/harness/ci-governance/composition-root.ts +67 -64
  23. package/scripts/harness/ci-governance/domain/ports/integrity-manifest-repository-port.ts +14 -0
  24. package/scripts/harness/ci-governance/domain/ports/sha256-hasher-port.ts +10 -0
  25. package/scripts/harness/ci-governance/domain/services/integrity-checker.ts +42 -0
  26. package/scripts/harness/ci-governance/domain/value-objects/integrity-drift.ts +16 -0
  27. package/scripts/harness/ci-governance/domain/value-objects/integrity-manifest.ts +49 -0
  28. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +40 -0
  29. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha256-hasher-adapter.ts +17 -0
  30. package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +7 -73
  31. package/scripts/harness/ci-governance/infrastructure/adapters/integrity-manifest-json-repository-adapter.ts +83 -0
  32. package/scripts/harness/ci-governance/presentation/handlers/integrity-handler.ts +76 -0
  33. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +60 -28
  34. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +33 -8
  35. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +90 -0
  36. package/scripts/harness/harness-error/infrastructure/registry/l1-error-definitions.ts +9 -9
  37. package/scripts/harness/installation/application/bundled-skill-selection.ts +2 -5
  38. package/scripts/harness/installation/application/usecases/run-reconcile.ts +69 -1
  39. package/scripts/harness/main.ts +257 -105
  40. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +65 -3
  41. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +44 -40
  42. package/scripts/harness/setup/skill-deployer.ts +2 -4
  43. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +82 -49
  44. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +133 -53
  45. package/scripts/harness/validator-system/composition-root.ts +153 -99
  46. package/scripts/harness/validator-system/domain/ports/coverage-attestation-gating-policy-port.ts +14 -0
  47. package/scripts/harness/validator-system/domain/ports/coverage-attestation-verification-policy-port.ts +30 -0
  48. package/scripts/harness/validator-system/domain/ports/injection-scan-policy-port.ts +14 -0
  49. package/scripts/harness/validator-system/domain/services/coverage-attestation-gating-service.ts +56 -0
  50. package/scripts/harness/validator-system/domain/services/coverage-attestation-verification-service.ts +45 -0
  51. package/scripts/harness/validator-system/domain/services/injection-pattern-scan-service.ts +118 -0
  52. package/scripts/harness/validator-system/domain/value-objects/attestation-verification-report.ts +59 -0
  53. package/scripts/harness/validator-system/domain/value-objects/coverage-gating-report.ts +67 -0
  54. package/scripts/harness/validator-system/domain/value-objects/injection-scan-report.ts +55 -0
  55. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +37 -31
  56. package/scripts/harness/validator-system/infrastructure/adapters/adr-foundation-reference-adapter.ts +13 -7
  57. package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-gating-adapter.ts +87 -0
  58. package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-verification-adapter.ts +165 -0
  59. package/scripts/harness/validator-system/infrastructure/adapters/file-system-injection-scan-adapter.ts +82 -0
  60. package/skills/README.md +1 -1
  61. package/skills/codebase-mapper/SKILL.md +1 -1
  62. package/skills/consistency-checker/references//343/203/201/343/202/247/343/203/203/343/202/257/343/203/252/343/202/271/343/203/210.md +1 -1
  63. package/skills/doc-health-checker/SKILL.md +148 -0
  64. package/skills/release-publisher/SKILL.md +101 -0
  65. package/skills/skill-creator/SKILL.md +74 -332
  66. package/skills/story-implementor/SKILL.md +52 -0
  67. package/skills/story-mapper/SKILL.md +4 -0
  68. package/skills/story-writer/SKILL.md +9 -0
  69. package/skills/uiux-designer/references/uiux-design-template.md +4 -4
  70. package/skills/unit-designer/SKILL.md +3 -1
  71. package/templates/.claude/scripts/deny-check.sh +260 -0
  72. package/skills/doc-freshness-checker/SKILL.md +0 -140
  73. package/skills/implementation-planner/SKILL.md +0 -169
  74. package/skills/implementation-planner/references/document-structure.md +0 -116
  75. package/skills/implementation-planner/references/plan-template.md +0 -177
  76. package/skills/implementation-planner/references/workflow.md +0 -164
  77. package/skills/pointer-validator/SKILL.md +0 -105
@@ -0,0 +1,83 @@
1
+ // @unit ci-governance
2
+ // @layer infrastructure
3
+
4
+ import * as fs from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ import type { IntegrityManifestRepositoryPort } from "../../domain/ports/integrity-manifest-repository-port.js";
7
+ import { IntegrityManifest } from "../../domain/value-objects/integrity-manifest.js";
8
+
9
+ interface IntegrityJsonV1 {
10
+ readonly version: 1;
11
+ readonly algorithm: "sha256";
12
+ readonly files: Readonly<Record<string, string>>;
13
+ }
14
+
15
+ /**
16
+ * phasegate.integrity.json(ルート)の読み書きアダプタ。
17
+ * files は path 昇順キーで決定的に書き出す。
18
+ */
19
+ export class IntegrityManifestJsonRepositoryAdapter implements IntegrityManifestRepositoryPort {
20
+ private readonly filePath: string;
21
+
22
+ constructor(baseDir: string, relativePath = "phasegate.integrity.json") {
23
+ this.filePath = path.isAbsolute(relativePath) ? relativePath : path.join(baseDir, relativePath);
24
+ }
25
+
26
+ getPath(): string {
27
+ return this.filePath;
28
+ }
29
+
30
+ async exists(): Promise<boolean> {
31
+ try {
32
+ await fs.access(this.filePath);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ async save(manifest: IntegrityManifest): Promise<string> {
40
+ const files: Record<string, string> = {};
41
+ for (const [p, digest] of manifest.sortedEntries()) {
42
+ files[p] = digest;
43
+ }
44
+ const data: IntegrityJsonV1 = {
45
+ version: 1,
46
+ algorithm: "sha256",
47
+ files,
48
+ };
49
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
50
+ await fs.writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
51
+ return this.filePath;
52
+ }
53
+
54
+ async load(): Promise<IntegrityManifest | null> {
55
+ let content: string;
56
+ try {
57
+ content = await fs.readFile(this.filePath, "utf-8");
58
+ } catch (err) {
59
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
60
+ throw err;
61
+ }
62
+
63
+ let parsed: IntegrityJsonV1;
64
+ try {
65
+ parsed = JSON.parse(content) as IntegrityJsonV1;
66
+ } catch (err) {
67
+ throw new Error(`Invalid integrity JSON at ${this.filePath}: ${String(err)}`);
68
+ }
69
+
70
+ if (
71
+ !parsed ||
72
+ parsed.version !== 1 ||
73
+ parsed.algorithm !== "sha256" ||
74
+ typeof parsed.files !== "object" ||
75
+ parsed.files === null
76
+ ) {
77
+ throw new Error(`Invalid integrity JSON schema at ${this.filePath}`);
78
+ }
79
+
80
+ const files = new Map<string, string>(Object.entries(parsed.files));
81
+ return IntegrityManifest.create({ files });
82
+ }
83
+ }
@@ -0,0 +1,76 @@
1
+ // @unit ci-governance
2
+ // @layer presentation
3
+
4
+ import type { PinIntegrityUseCase } from "../../application/usecases/pin-integrity-usecase.js";
5
+ import type { VerifyIntegrityUseCase } from "../../application/usecases/verify-integrity-usecase.js";
6
+ import type { IntegrityDriftKind } from "../../domain/value-objects/integrity-drift.js";
7
+
8
+ export interface IntegrityPinArgs {
9
+ readonly dryRun?: boolean;
10
+ readonly format?: "human" | "json";
11
+ }
12
+
13
+ export interface IntegrityVerifyArgs {
14
+ readonly format?: "human" | "json";
15
+ }
16
+
17
+ export interface IntegrityHandlerResult {
18
+ readonly exitCode: number;
19
+ readonly output: string;
20
+ }
21
+
22
+ const DRIFT_LABEL: Record<IntegrityDriftKind, string> = {
23
+ mismatch: "MISMATCH (digest 不一致)",
24
+ added: "ADDED (manifest 未登録)",
25
+ missing: "MISSING (実ファイル欠落)",
26
+ "manifest-absent": "MANIFEST-ABSENT (manifest 欠落)",
27
+ };
28
+
29
+ export class IntegrityHandler {
30
+ constructor(
31
+ private readonly pinUseCase: PinIntegrityUseCase,
32
+ private readonly verifyUseCase: VerifyIntegrityUseCase,
33
+ ) {}
34
+
35
+ async pin(args: IntegrityPinArgs): Promise<IntegrityHandlerResult> {
36
+ const format = args.format ?? "human";
37
+ const result = await this.pinUseCase.execute({ dryRun: args.dryRun });
38
+
39
+ if (format === "json") {
40
+ return { exitCode: 0, output: JSON.stringify(result, null, 2) };
41
+ }
42
+
43
+ const header = result.dryRun
44
+ ? `[dry run] integrity manifest を生成しました(保存先想定: ${result.savedPath})`
45
+ : `integrity manifest を保存しました: ${result.savedPath}`;
46
+ return {
47
+ exitCode: 0,
48
+ output: [header, `エントリ数: ${result.entryCount}`].join("\n"),
49
+ };
50
+ }
51
+
52
+ async verify(args: IntegrityVerifyArgs): Promise<IntegrityHandlerResult> {
53
+ const format = args.format ?? "human";
54
+ const result = await this.verifyUseCase.execute();
55
+ const exitCode = result.ok ? 0 : 2;
56
+
57
+ if (format === "json") {
58
+ return { exitCode, output: JSON.stringify(result, null, 2) };
59
+ }
60
+
61
+ if (result.ok) {
62
+ return {
63
+ exitCode: 0,
64
+ output: `integrity 照合 OK: drift はありません(${result.manifestPath})`,
65
+ };
66
+ }
67
+
68
+ const lines = [
69
+ `integrity drift を ${result.drifts.length} 件検出しました(${result.manifestPath}):`,
70
+ ...result.drifts.map((d) => `- [${DRIFT_LABEL[d.kind]}] ${d.path}`),
71
+ "",
72
+ "意図的な変更であれば `phasegate integrity:pin` で manifest を更新してください。",
73
+ ];
74
+ return { exitCode: 2, output: lines.join("\n") };
75
+ }
76
+ }
@@ -4,50 +4,77 @@
4
4
  * @work-item-id WI-133 / WI-156
5
5
  * @work-item-id WI-217
6
6
  * @work-item-id WI-212
7
+ * @work-item-id WI-258
8
+ * @work-item-id WI-259
9
+ * @work-item-id WI-268
7
10
  */
8
- import type { HarnessConfigV2 } from '../../domain/harness-config.js';
11
+ import type { HarnessConfigV2 } from "../../domain/harness-config.js";
9
12
 
10
13
  export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefined): object | undefined {
11
14
  if (!resolvedConfig) return undefined;
12
15
 
13
- const l3Validators = normalizeValidators(resolvedConfig.layers.L3.validators, {
14
- security: 'L3-001',
15
- performance: 'L3-002',
16
- coverage: 'L3-003',
17
- nyquist: 'L3-004',
18
- 'ac-bound-coverage': 'L3-005',
19
- }, /^L3-\d{3}$/);
20
- const l4Validators = normalizeValidators(resolvedConfig.layers.L4.validators, {
21
- 'drift-detect': 'L4-001',
22
- 'drift-detector': 'L4-001',
23
- 'consistency-check': 'L4-002',
24
- 'consistency-checker': 'L4-002',
25
- 'dead-code': 'L4-003',
26
- 'dead-code-detector': 'L4-003',
27
- 'doc-freshness': 'L4-004',
28
- 'doc-freshness-checker': 'L4-004',
29
- 'pointer-validation': 'L4-005',
30
- 'pointer-validator': 'L4-005',
31
- 'skill-catalog-drift': 'L4-006',
32
- }, /^L4-\d{3}$/);
16
+ const l3Validators = normalizeValidators(
17
+ resolvedConfig.layers.L3.validators,
18
+ {
19
+ security: "L3-001",
20
+ performance: "L3-002",
21
+ coverage: "L3-003",
22
+ nyquist: "L3-004",
23
+ "ac-bound-coverage": "L3-005",
24
+ "injection-scan": "L3-006",
25
+ "coverage-attestation-verification": "L3-007",
26
+ },
27
+ /^L3-\d{3}$/,
28
+ );
29
+ const l4Validators = normalizeValidators(
30
+ resolvedConfig.layers.L4.validators,
31
+ {
32
+ "drift-detect": "L4-001",
33
+ "drift-detector": "L4-001",
34
+ "consistency-check": "L4-002",
35
+ "consistency-checker": "L4-002",
36
+ "dead-code": "L4-003",
37
+ "dead-code-detector": "L4-003",
38
+ "doc-freshness": "L4-004",
39
+ "doc-freshness-checker": "L4-004",
40
+ "pointer-validation": "L4-005",
41
+ "pointer-validator": "L4-005",
42
+ "skill-catalog-drift": "L4-006",
43
+ },
44
+ /^L4-\d{3}$/,
45
+ );
33
46
  const effectiveL4Validators = usesCustomDocumentRoots(resolvedConfig)
34
- ? includeValidator(l4Validators, 'L4-002')
47
+ ? includeValidator(l4Validators, "L4-002")
35
48
  : l4Validators;
36
49
 
37
50
  return {
38
51
  project: {
39
52
  preset: resolvedConfig.project.preset,
40
- languages: resolvedConfig.project.languages ?? ['typescript'],
53
+ languages: resolvedConfig.project.languages ?? ["typescript"],
41
54
  },
42
55
  paths: {
43
56
  designDocs: resolvedConfig.paths.designDocs,
44
57
  inceptionDocs: resolvedConfig.paths.inceptionDocs,
45
58
  },
46
59
  layers: {
47
- L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
60
+ // WI-258 / ADR-030 §Decision.3.②: L2-016 (coverage-attestation-gating) default-ON でリストに含める。
61
+ L2: {
62
+ enabled: resolvedConfig.layers.L2.enabled,
63
+ validators: ["L2-001", "L2-002", "L2-003", "L2-013", "L2-014", "L2-015", "L2-016"],
64
+ },
48
65
  L3: {
49
66
  enabled: resolvedConfig.layers.L3.enabled,
50
- validators: l3Validators.length > 0 ? l3Validators : ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
67
+ // WI-259 / ADR-030 §Decision.3.④: L3-006 (injection-scan) advisory default-ON。
68
+ // WI-268 / ADR-030 §Decision.1・§Decision.3.② 第2段: L3-007 (coverage-attestation-verification)
69
+ // は fail-closed default-ON。いずれも fallback 判定の後に force-include し、normalize 結果でも
70
+ // fallback でも常に含める(L3-007 は fail-closed だが現 corpus は実参照 0 件ゆえ緑)。
71
+ validators: includeValidator(
72
+ includeValidator(
73
+ l3Validators.length > 0 ? l3Validators : ["L3-001", "L3-002", "L3-003", "L3-004"],
74
+ "L3-006",
75
+ ),
76
+ "L3-007",
77
+ ),
51
78
  coverageThreshold: resolvedConfig.layers.L3.coverageThreshold,
52
79
  requirementMatrixPath: resolvedConfig.layers.L3.requirementMatrixPath,
53
80
  // WI-227 / H16-03: L3-005 のスコープ(additive-safe。未設定は [])
@@ -55,7 +82,10 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
55
82
  },
56
83
  L4: {
57
84
  enabled: resolvedConfig.layers.L4.enabled,
58
- validators: effectiveL4Validators.length > 0 ? effectiveL4Validators : ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'],
85
+ validators:
86
+ effectiveL4Validators.length > 0
87
+ ? effectiveL4Validators
88
+ : ["L4-001", "L4-002", "L4-003", "L4-004", "L4-005", "L4-006"],
59
89
  },
60
90
  },
61
91
  harnesses: {
@@ -70,8 +100,10 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
70
100
  }
71
101
 
72
102
  function usesCustomDocumentRoots(resolvedConfig: HarnessConfigV2): boolean {
73
- return resolvedConfig.paths.designDocs !== 'docs/product/construction'
74
- || resolvedConfig.paths.inceptionDocs !== 'docs/inception';
103
+ return (
104
+ resolvedConfig.paths.designDocs !== "docs/product/construction" ||
105
+ resolvedConfig.paths.inceptionDocs !== "docs/inception"
106
+ );
75
107
  }
76
108
 
77
109
  function includeValidator(validators: readonly string[], validatorId: string): readonly string[] {
@@ -2,7 +2,7 @@
2
2
  // @unit harness-api
3
3
  // ci-check-result.ts — CiCheckResult Value Object
4
4
 
5
- import type { HarnessError } from './harness-api-response.js';
5
+ import type { HarnessError } from "./harness-api-response.js";
6
6
 
7
7
  export interface ValidatorCheckItem {
8
8
  validatorId: string;
@@ -14,6 +14,30 @@ export interface ValidatorCheckItem {
14
14
  export interface CiCheckResultProps {
15
15
  validatorResults: readonly ValidatorCheckItem[];
16
16
  allPassed: boolean;
17
+ /** WI-260 / ADR-017: warning-only failure を fail 扱いにする opt-in(既定 false)。 */
18
+ failOnWarning?: boolean;
19
+ }
20
+
21
+ /**
22
+ * WI-260 / ADR-017: validator 1件が「実質 pass」か判定する severity-aware ルール。
23
+ * `validate` 経路の AggregateValidationResultsUseCase と同一の判定式を harness-api の
24
+ * domain VO 側に持たせ、ci-check と validate の集約挙動を一致させる(レイヤー越境回避のため
25
+ * usecase を直接 import せず判定ロジックを共有)。
26
+ *
27
+ * - skipped: 実質 pass
28
+ * - passed=true: 実質 pass
29
+ * - passed=false かつ error severity(!= warning)を含む: fail
30
+ * - passed=false かつ errors=[](severity 判定不能): 安全側に倒して fail
31
+ * - passed=false かつ warning のみ: failOnWarning=false(既定)で実質 pass、true で fail
32
+ */
33
+ function isEffectivelyPassed(item: ValidatorCheckItem, failOnWarning: boolean): boolean {
34
+ if (item.skipped || item.passed) return true;
35
+ const errors = item.errors ?? [];
36
+ const hasNonWarningError = errors.some((e) => e.severity !== "warning");
37
+ const hasWarnings = errors.some((e) => e.severity === "warning");
38
+ const isEmptyFail = errors.length === 0;
39
+ const hasFail = isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings);
40
+ return !hasFail;
17
41
  }
18
42
 
19
43
  export class CiCheckResult {
@@ -29,23 +53,24 @@ export class CiCheckResult {
29
53
  static create(props: CiCheckResultProps): CiCheckResult {
30
54
  // INV-5: validatorResults は 1件以上
31
55
  if (!props.validatorResults || props.validatorResults.length === 0) {
32
- throw new Error('EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)');
56
+ throw new Error("EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)");
33
57
  }
34
- // INV-6: allPassed === validatorResults.every(r => r.passed || r.skipped)
35
- const computedAllPassed = props.validatorResults.every((r) => r.passed || r.skipped);
58
+ // INV-6 (WI-260/ADR-017 更新): allPassed === validatorResults.every(実質pass)
59
+ const failOnWarning = props.failOnWarning ?? false;
60
+ const computedAllPassed = props.validatorResults.every((r) => isEffectivelyPassed(r, failOnWarning));
36
61
  if (props.allPassed !== computedAllPassed) {
37
62
  throw new Error(
38
- `HarnessApiDomainError: allPassed=${props.allPassed} does not match validatorResults state (computed: ${computedAllPassed})`
63
+ `HarnessApiDomainError: allPassed=${props.allPassed} does not match validatorResults state (computed: ${computedAllPassed})`,
39
64
  );
40
65
  }
41
66
  return new CiCheckResult(props.validatorResults, props.allPassed);
42
67
  }
43
68
 
44
- static fromResults(validatorResults: readonly ValidatorCheckItem[]): CiCheckResult {
69
+ static fromResults(validatorResults: readonly ValidatorCheckItem[], failOnWarning = false): CiCheckResult {
45
70
  if (!validatorResults || validatorResults.length === 0) {
46
- throw new Error('EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)');
71
+ throw new Error("EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)");
47
72
  }
48
- const allPassed = validatorResults.every((r) => r.passed || r.skipped);
73
+ const allPassed = validatorResults.every((r) => isEffectivelyPassed(r, failOnWarning));
49
74
  return new CiCheckResult(validatorResults, allPassed);
50
75
  }
51
76
 
@@ -0,0 +1,90 @@
1
+ // @unit harness-api
2
+ // @layer domain
3
+ // @work-item-id WI-250
4
+ // known-harness-commands.ts — CLI 実サーフェスの canonical 既知コマンド一覧
5
+ //
6
+ // main.ts の CLI dispatch(`switch (command)`)が受理する全トップレベルコマンド名の
7
+ // 単一ソース。`phasegate:` prefix 必須の CommandRegistry では表現できない
8
+ // 非 prefix コマンド(lint / init / baseline 等)を含む完全なサーフェスを表す。
9
+ //
10
+ // 不変条件(conformance テストで強制):
11
+ // - main.ts の `case "..."` ラベル集合と集合一致すること
12
+ // (__tests__/integration/harness-api/known-harness-commands-conformance.test.ts)
13
+ // - エントリは重複なくソート済みであること
14
+ //
15
+ // main.ts にコマンドを追加/削除した場合は、必ずこのリストも同期更新すること。
16
+
17
+ export const KNOWN_HARNESS_COMMANDS: readonly string[] = Object.freeze([
18
+ "baseline",
19
+ "bypass:audit",
20
+ "check-change-category",
21
+ "check-phase-gate",
22
+ "ci-check",
23
+ "ci:auto-refresh-agent-context",
24
+ "ci:check-repetition",
25
+ "ci:generate-template",
26
+ "ci:migrate-agents-md",
27
+ "commit-msg",
28
+ "complete-check",
29
+ "config:plan",
30
+ "delegate-sonnet",
31
+ "disable-feature",
32
+ "doctor",
33
+ "emit-agent-rules",
34
+ "enable-feature",
35
+ "hook",
36
+ "init",
37
+ "install",
38
+ "integrity:pin",
39
+ "integrity:verify",
40
+ "lint",
41
+ "list-adrs",
42
+ "list-errors",
43
+ "list-features",
44
+ "migrate",
45
+ "p2:check-agent-context",
46
+ "p2:check-freshness",
47
+ "p2:check-initial-creation",
48
+ "p2:generate-e2e-template",
49
+ "p2:validate-pointers",
50
+ "phasegate:attest",
51
+ "phasegate:check-phase",
52
+ "phasegate:check-ready",
53
+ "phasegate:ci-check",
54
+ "phasegate:complete-check",
55
+ "phasegate:detect-drift",
56
+ "phasegate:generate-matrix",
57
+ "phasegate:impact-analysis",
58
+ "phasegate:lint",
59
+ "phasegate:status",
60
+ "phasegate:verify-attestation",
61
+ "pre-commit",
62
+ "reconcile",
63
+ "refresh-claude-md",
64
+ "regression:analyze-migration",
65
+ "regression:configure-ci-gate",
66
+ "regression:migrate-v0-tests",
67
+ "regression:run-agent-guard",
68
+ "regression:run-gng-gate",
69
+ "regression:run-k-requirements",
70
+ "regression:run-k14-k15",
71
+ "render-errors",
72
+ "scaffold-design",
73
+ "scaffold-wi",
74
+ "session",
75
+ "setup:agent",
76
+ "skill:apply-cascade-update",
77
+ "skill:check-coverage",
78
+ "skill:collect-lessons",
79
+ "skill:execute-tdd-cycle",
80
+ "skill:validate-structure",
81
+ "skills",
82
+ "status",
83
+ "uninstall",
84
+ "update-skills",
85
+ "validate",
86
+ "validate-adr",
87
+ "validate-fix",
88
+ "validate-metadata",
89
+ "work-items:status",
90
+ ]);
@@ -67,24 +67,24 @@ export const L1_ERROR_DEFINITIONS = Object.freeze([
67
67
  }),
68
68
  createDefinition({
69
69
  code: 'L1-006',
70
- title: '参照されないファイルが残っている',
70
+ title: '重複コードが検出された',
71
71
  category: 'architecture',
72
72
  ownerValidatorId: 'architecture',
73
- defaultFixExample: 'export const actual = "shared";',
73
+ defaultFixExample:
74
+ 'function buildValue(): string { return "shared"; }\nconst actual = buildValue();',
74
75
  }),
75
76
  createDefinition({
76
77
  code: 'L1-007',
77
- title: 'コメント量が過剰である',
78
- category: 'quality',
78
+ title: '参照されないファイルが残っている',
79
+ category: 'architecture',
79
80
  ownerValidatorId: 'architecture',
80
- defaultFixExample: 'const actual = "keep comments concise";',
81
+ defaultFixExample: 'export const actual = "shared";',
81
82
  }),
82
83
  createDefinition({
83
84
  code: 'L1-008',
84
- title: '重複コードが検出された',
85
- category: 'architecture',
85
+ title: 'コメント量が過剰である',
86
+ category: 'quality',
86
87
  ownerValidatorId: 'architecture',
87
- defaultFixExample:
88
- 'function buildValue(): string { return "shared"; }\nconst actual = buildValue();',
88
+ defaultFixExample: 'const actual = "keep comments concise";',
89
89
  }),
90
90
  ]);
@@ -8,17 +8,15 @@ const CORE_SKILLS = [
8
8
  "cascade-updater",
9
9
  "codebase-mapper",
10
10
  "consistency-checker",
11
- "doc-freshness-checker",
11
+ "doc-health-checker",
12
12
  "engineering-perspective",
13
13
  "implementation-readiness-checker",
14
- "pointer-validator",
15
14
  "test-coverage-checker",
16
15
  ] as const;
17
16
 
18
17
  const AIDLC_SKILLS = [
19
18
  "domain-designer",
20
19
  "environment-designer",
21
- "implementation-planner",
22
20
  "it-test-designer",
23
21
  "it-test-logic-designer",
24
22
  "logical-designer",
@@ -37,10 +35,9 @@ const AIDLC_SKILLS = [
37
35
  ] as const;
38
36
 
39
37
  const UTILITY_SKILLS = ["codex-delegator", "skill-creator"] as const;
40
- const GUIDANCE_SKILLS = ["phasegate-toolkit-guide", "phasegate-config-doctor"] as const;
38
+ const GUIDANCE_SKILLS = ["phasegate-toolkit-guide", "phasegate-config-doctor", "release-publisher"] as const;
41
39
 
42
40
  export function getBundledSkillsForSet(skillSet: SkillSet): string[] {
43
41
  if (skillSet === "core") return [...CORE_SKILLS];
44
42
  return [...CORE_SKILLS, ...AIDLC_SKILLS, ...UTILITY_SKILLS, ...GUIDANCE_SKILLS];
45
43
  }
46
-
@@ -6,6 +6,7 @@
6
6
  // @work-item-id WI-210
7
7
  // @work-item-id WI-216
8
8
  // @work-item-id WI-219
9
+ // @work-item-id WI-264
9
10
 
10
11
  import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, readdir, rm, symlink, writeFile } from "node:fs/promises";
11
12
  import { dirname, join, relative, resolve } from "node:path";
@@ -18,7 +19,7 @@ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
18
19
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
19
20
  import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
20
21
 
21
- type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse";
22
+ type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse" | "prune";
22
23
  type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "copy-dir" | "symlink" | "unknown";
23
24
 
24
25
  export interface ReconcilePlanItem {
@@ -62,6 +63,8 @@ const SHELL_END = "# === phasegate managed (END) ===";
62
63
  const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
63
64
  const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
64
65
  const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
66
+ const HARNESS_VERSION_BASENAME = ".harness-version";
67
+ const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills"] as const;
65
68
 
66
69
  function isRecord(value: unknown): value is Record<string, unknown> {
67
70
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -302,6 +305,7 @@ export class RunReconcileUseCase {
302
305
  const outcomes: Array<{
303
306
  readonly item: ReconcilePlanItem;
304
307
  readonly needsBackup: boolean;
308
+ readonly prune?: boolean;
305
309
  readonly apply: () => Promise<string | null>;
306
310
  }> = [];
307
311
 
@@ -346,12 +350,23 @@ export class RunReconcileUseCase {
346
350
  }
347
351
  }
348
352
 
353
+ for (const outcome of this.planOrphanSkills(input, manifest)) {
354
+ outcomes.push(outcome);
355
+ plan.push(outcome.item);
356
+ }
357
+
349
358
  if (!input.apply || refused.length > 0) {
350
359
  return { plan, refused, changed, backupDir: null };
351
360
  }
352
361
 
353
362
  for (const outcome of outcomes) {
354
363
  if (!outcome.item.changed) continue;
364
+ if (outcome.prune === true) {
365
+ await outcome.apply();
366
+ nextManifest = nextManifest.removeEntry(outcome.item.path);
367
+ changed.push(outcome.item);
368
+ continue;
369
+ }
355
370
  if (outcome.needsBackup) {
356
371
  backupDir ??= join(input.projectRoot, ".phasegate", "backups", backupStamp);
357
372
  await this.backup(input.projectRoot, outcome.item.path, backupDir);
@@ -616,6 +631,59 @@ export class RunReconcileUseCase {
616
631
  };
617
632
  }
618
633
 
634
+ private planOrphanSkills(input: RunReconcileInput, manifest: DeploymentManifest): Array<{
635
+ readonly item: ReconcilePlanItem;
636
+ readonly needsBackup: boolean;
637
+ readonly prune: boolean;
638
+ readonly apply: () => Promise<string | null>;
639
+ }> {
640
+ const allowed = new Set(getBundledSkillsForSet("all"));
641
+ const outcomes: Array<{
642
+ readonly item: ReconcilePlanItem;
643
+ readonly needsBackup: boolean;
644
+ readonly prune: boolean;
645
+ readonly apply: () => Promise<string | null>;
646
+ }> = [];
647
+ for (const entry of manifest.entries) {
648
+ if (entry.mode !== "created") continue;
649
+ const skillName = this.orphanSkillName(entry.path, allowed);
650
+ if (skillName === null) continue;
651
+ const absolutePath = this.resolveProjectPath(input.projectRoot, entry.path);
652
+ outcomes.push({
653
+ item: this.item(
654
+ entry.path,
655
+ "prune",
656
+ "mechanical",
657
+ "copy-dir",
658
+ true,
659
+ `${entry.path}: prune orphan skill (not in current bundle)`,
660
+ "- skill directory",
661
+ null,
662
+ ),
663
+ needsBackup: false,
664
+ prune: true,
665
+ apply: async () => {
666
+ await rm(absolutePath, { recursive: true, force: true });
667
+ return null;
668
+ },
669
+ });
670
+ }
671
+ return outcomes;
672
+ }
673
+
674
+ private orphanSkillName(entryPath: string, allowed: ReadonlySet<string>): string | null {
675
+ for (const prefix of SKILL_ROOT_PREFIXES) {
676
+ const marker = `${prefix}/`;
677
+ if (!entryPath.startsWith(marker)) continue;
678
+ const remainder = entryPath.slice(marker.length);
679
+ if (remainder.length === 0 || remainder.includes("/")) return null;
680
+ if (remainder === HARNESS_VERSION_BASENAME) return null;
681
+ if (allowed.has(remainder)) return null;
682
+ return remainder;
683
+ }
684
+ return null;
685
+ }
686
+
619
687
  private createdEntry(path: string, hashInput: string): DeploymentEntry {
620
688
  return DeploymentEntry.create({
621
689
  path,