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.
- package/CHANGELOG.md +15 -0
- package/README.ja.md +52 -15
- package/README.md +39 -11
- package/docs/ADR/030-injection-threat-model-and-trust-root.md +145 -0
- package/docs/guide/hooks-integration.md +101 -1
- package/docs/guide/installation.md +1 -1
- package/docs/guide/quick-vs-full-mode.md +1 -1
- package/docs/guide/skills-overview.md +7 -8
- package/package.json +1 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +131 -63
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +31 -4
- package/scripts/harness/agent-integration/presentation/spotlight.ts +65 -0
- package/scripts/harness/biome-ast-engine/application/mappers/build-harness-error-payload-output-mapper.ts +3 -3
- package/scripts/harness/biome-ast-engine/infrastructure/mappers/rule-violation-code-mapper.ts +3 -3
- package/scripts/harness/biome-ast-engine/presentation/cli/harness-lint-command-handler.ts +6 -4
- package/scripts/harness/ci-governance/application/dto/pin-integrity-input.ts +8 -0
- package/scripts/harness/ci-governance/application/dto/pin-integrity-output.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/verify-integrity-input.ts +7 -0
- package/scripts/harness/ci-governance/application/dto/verify-integrity-output.ts +10 -0
- package/scripts/harness/ci-governance/application/usecases/pin-integrity-usecase.ts +60 -0
- package/scripts/harness/ci-governance/application/usecases/verify-integrity-usecase.ts +46 -0
- package/scripts/harness/ci-governance/composition-root.ts +67 -64
- package/scripts/harness/ci-governance/domain/ports/integrity-manifest-repository-port.ts +14 -0
- package/scripts/harness/ci-governance/domain/ports/sha256-hasher-port.ts +10 -0
- package/scripts/harness/ci-governance/domain/services/integrity-checker.ts +42 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-drift.ts +16 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-manifest.ts +49 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +40 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha256-hasher-adapter.ts +17 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +7 -73
- package/scripts/harness/ci-governance/infrastructure/adapters/integrity-manifest-json-repository-adapter.ts +83 -0
- package/scripts/harness/ci-governance/presentation/handlers/integrity-handler.ts +76 -0
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +60 -28
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +33 -8
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +90 -0
- package/scripts/harness/harness-error/infrastructure/registry/l1-error-definitions.ts +9 -9
- package/scripts/harness/installation/application/bundled-skill-selection.ts +2 -5
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +69 -1
- package/scripts/harness/main.ts +257 -105
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +65 -3
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +44 -40
- package/scripts/harness/setup/skill-deployer.ts +2 -4
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +82 -49
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +133 -53
- package/scripts/harness/validator-system/composition-root.ts +153 -99
- package/scripts/harness/validator-system/domain/ports/coverage-attestation-gating-policy-port.ts +14 -0
- package/scripts/harness/validator-system/domain/ports/coverage-attestation-verification-policy-port.ts +30 -0
- package/scripts/harness/validator-system/domain/ports/injection-scan-policy-port.ts +14 -0
- package/scripts/harness/validator-system/domain/services/coverage-attestation-gating-service.ts +56 -0
- package/scripts/harness/validator-system/domain/services/coverage-attestation-verification-service.ts +45 -0
- package/scripts/harness/validator-system/domain/services/injection-pattern-scan-service.ts +118 -0
- package/scripts/harness/validator-system/domain/value-objects/attestation-verification-report.ts +59 -0
- package/scripts/harness/validator-system/domain/value-objects/coverage-gating-report.ts +67 -0
- package/scripts/harness/validator-system/domain/value-objects/injection-scan-report.ts +55 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +37 -31
- package/scripts/harness/validator-system/infrastructure/adapters/adr-foundation-reference-adapter.ts +13 -7
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-gating-adapter.ts +87 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-verification-adapter.ts +165 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-injection-scan-adapter.ts +82 -0
- package/skills/README.md +1 -1
- package/skills/codebase-mapper/SKILL.md +1 -1
- 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
- package/skills/doc-health-checker/SKILL.md +148 -0
- package/skills/release-publisher/SKILL.md +101 -0
- package/skills/skill-creator/SKILL.md +74 -332
- package/skills/story-implementor/SKILL.md +52 -0
- package/skills/story-mapper/SKILL.md +4 -0
- package/skills/story-writer/SKILL.md +9 -0
- package/skills/uiux-designer/references/uiux-design-template.md +4 -4
- package/skills/unit-designer/SKILL.md +3 -1
- package/templates/.claude/scripts/deny-check.sh +260 -0
- package/skills/doc-freshness-checker/SKILL.md +0 -140
- package/skills/implementation-planner/SKILL.md +0 -169
- package/skills/implementation-planner/references/document-structure.md +0 -116
- package/skills/implementation-planner/references/plan-template.md +0 -177
- package/skills/implementation-planner/references/workflow.md +0 -164
- 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
|
+
}
|
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -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
|
|
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(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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 ?? [
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
74
|
-
|
|
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
|
|
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(
|
|
56
|
+
throw new Error("EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)");
|
|
33
57
|
}
|
|
34
|
-
// INV-6: allPassed === validatorResults.every(
|
|
35
|
-
const
|
|
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(
|
|
71
|
+
throw new Error("EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)");
|
|
47
72
|
}
|
|
48
|
-
const allPassed = validatorResults.every((r) => r
|
|
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:
|
|
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: '
|
|
78
|
+
title: '参照されないファイルが残っている',
|
|
79
|
+
category: 'architecture',
|
|
79
80
|
ownerValidatorId: 'architecture',
|
|
80
|
-
defaultFixExample: 'const actual = "
|
|
81
|
+
defaultFixExample: 'export const actual = "shared";',
|
|
81
82
|
}),
|
|
82
83
|
createDefinition({
|
|
83
84
|
code: 'L1-008',
|
|
84
|
-
title: '
|
|
85
|
-
category: '
|
|
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-
|
|
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,
|