phasegate 0.283.0 → 0.315.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 +16 -0
- package/docs/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +28 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
- package/scripts/harness/ci-governance/composition-root.ts +2 -2
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +57 -9
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- package/skills/quick-implementor/SKILL.md +19 -0
|
@@ -1,17 +1,59 @@
|
|
|
1
1
|
// @unit agent-integration
|
|
2
2
|
// @layer infrastructure
|
|
3
3
|
// @work-item-id WI-206
|
|
4
|
+
// @work-item-id WI-348
|
|
5
|
+
// @work-item-id WI-350
|
|
4
6
|
|
|
5
|
-
import * as fs from
|
|
6
|
-
import * as path from
|
|
7
|
-
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { ConfigQueryPort } from "../../domain/ports/config-query-port.js";
|
|
8
10
|
import type {
|
|
9
11
|
FullModeSessionQueryInput,
|
|
10
12
|
FullModeSessionQueryPort,
|
|
11
13
|
FullModeSessionQueryResult,
|
|
12
|
-
} from
|
|
13
|
-
import
|
|
14
|
-
|
|
14
|
+
} from "../../domain/ports/full-mode-session-query-port.js";
|
|
15
|
+
import { WriteTargetScope } from "../../domain/value-objects/write-target-scope.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* quick-mode の `ChangeCategoryValue` と同一の語彙。
|
|
19
|
+
* 照合相手は quick-mode 分類結果(`dominantCategory`)なので、この語彙以外は
|
|
20
|
+
* session.json 側の書式誤りとして扱う。
|
|
21
|
+
* `session begin` 側との同期は
|
|
22
|
+
* `__tests__/integration/harness-api/session-begin-allowed-categories.integration.test.ts`
|
|
23
|
+
* が検証する(WI-348)。
|
|
24
|
+
*/
|
|
25
|
+
const KNOWN_CHANGE_CATEGORIES: ReadonlySet<string> = new Set([
|
|
26
|
+
"bugfix",
|
|
27
|
+
"docs",
|
|
28
|
+
"test",
|
|
29
|
+
"config",
|
|
30
|
+
"feature",
|
|
31
|
+
"domain",
|
|
32
|
+
"api",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* WI-348: v0.301.0 以前の `phasegate session begin` は allowedCategories に
|
|
37
|
+
* レイヤー名(domain / application / infrastructure / presentation / config)を
|
|
38
|
+
* 書き出していた。この語彙は ChangeCategory と domain / config でしか交差せず、
|
|
39
|
+
* 旧形式 session.json をそのまま照合すると feature / api / bugfix などが
|
|
40
|
+
* 恒常的に拒否され session が事実上無力化する。
|
|
41
|
+
*
|
|
42
|
+
* `session begin` はカテゴリ指定フラグを持たず常に定数を書き出すため、
|
|
43
|
+
* 「ChangeCategory 語彙に無い値が混ざっている」= 旧形式生成物と判断できる。
|
|
44
|
+
* その場合のみ全カテゴリ許可へ正規化する。
|
|
45
|
+
* 全要素が既知カテゴリなら手編集による意図的な絞り込みとみなし原文を尊重する。
|
|
46
|
+
* unit / 期限 / target path のスコープ判定は正規化後も従来どおり効く。
|
|
47
|
+
*/
|
|
48
|
+
function normalizeAllowedCategories(rawCategories: readonly string[]): readonly string[] {
|
|
49
|
+
if (rawCategories.length === 0) {
|
|
50
|
+
return rawCategories;
|
|
51
|
+
}
|
|
52
|
+
if (rawCategories.every((value) => KNOWN_CHANGE_CATEGORIES.has(value))) {
|
|
53
|
+
return rawCategories;
|
|
54
|
+
}
|
|
55
|
+
return [...KNOWN_CHANGE_CATEGORIES];
|
|
56
|
+
}
|
|
15
57
|
|
|
16
58
|
interface FullModeSessionDocument {
|
|
17
59
|
readonly mode?: unknown;
|
|
@@ -35,52 +77,62 @@ export class FileSystemFullModeSessionQueryAdapter implements FullModeSessionQue
|
|
|
35
77
|
async check(input: FullModeSessionQueryInput): Promise<FullModeSessionQueryResult> {
|
|
36
78
|
let document: FullModeSessionDocument;
|
|
37
79
|
try {
|
|
38
|
-
const raw = await fs.readFile(path.join(this.options.rootDir,
|
|
80
|
+
const raw = await fs.readFile(path.join(this.options.rootDir, ".phasegate", "session.json"), "utf8");
|
|
39
81
|
document = JSON.parse(raw) as FullModeSessionDocument;
|
|
40
82
|
} catch {
|
|
41
|
-
return { active: false, allowed: false, reason:
|
|
83
|
+
return { active: false, allowed: false, reason: "session marker not found or unreadable" };
|
|
42
84
|
}
|
|
43
85
|
|
|
44
|
-
if (document.mode !==
|
|
45
|
-
return { active: false, allowed: false, reason:
|
|
86
|
+
if (document.mode !== "full") {
|
|
87
|
+
return { active: false, allowed: false, reason: "session mode is not full" };
|
|
46
88
|
}
|
|
47
|
-
if (typeof document.unit !==
|
|
48
|
-
return { active: true, allowed: false, reason:
|
|
89
|
+
if (typeof document.unit !== "string" || document.unit === "") {
|
|
90
|
+
return { active: true, allowed: false, reason: "session unit is missing" };
|
|
49
91
|
}
|
|
50
|
-
if (typeof document.workItemId !==
|
|
51
|
-
return { active: true, allowed: false, reason:
|
|
92
|
+
if (typeof document.workItemId !== "string" || !/^WI-\d+$/.test(document.workItemId)) {
|
|
93
|
+
return { active: true, allowed: false, reason: "session work item is invalid" };
|
|
52
94
|
}
|
|
53
|
-
if (typeof document.expiresAt !==
|
|
54
|
-
return { active: true, allowed: false, reason:
|
|
95
|
+
if (typeof document.expiresAt !== "string" || Number.isNaN(Date.parse(document.expiresAt))) {
|
|
96
|
+
return { active: true, allowed: false, reason: "session expiry is invalid" };
|
|
55
97
|
}
|
|
56
98
|
if ((this.options.now?.() ?? new Date()).getTime() >= Date.parse(document.expiresAt)) {
|
|
57
99
|
return {
|
|
58
100
|
active: true,
|
|
59
101
|
allowed: false,
|
|
60
|
-
reason:
|
|
102
|
+
reason: "session expired",
|
|
61
103
|
workItemId: document.workItemId,
|
|
62
104
|
unit: document.unit,
|
|
63
105
|
expiresAt: document.expiresAt,
|
|
64
106
|
};
|
|
65
107
|
}
|
|
66
|
-
if (
|
|
67
|
-
|
|
108
|
+
if (
|
|
109
|
+
!Array.isArray(document.allowedCategories) ||
|
|
110
|
+
document.allowedCategories.some((value) => typeof value !== "string")
|
|
111
|
+
) {
|
|
112
|
+
return { active: true, allowed: false, reason: "session allowedCategories is invalid" };
|
|
68
113
|
}
|
|
69
|
-
|
|
114
|
+
const allowedCategories = normalizeAllowedCategories(document.allowedCategories as readonly string[]);
|
|
115
|
+
if (input.dominantCategory === undefined || !allowedCategories.includes(input.dominantCategory)) {
|
|
70
116
|
return {
|
|
71
117
|
active: true,
|
|
72
118
|
allowed: false,
|
|
73
|
-
reason: `category ${input.dominantCategory ??
|
|
119
|
+
reason: `category ${input.dominantCategory ?? "<unknown>"} is not allowed by session`,
|
|
74
120
|
workItemId: document.workItemId,
|
|
75
121
|
unit: document.unit,
|
|
76
122
|
expiresAt: document.expiresAt,
|
|
77
123
|
};
|
|
78
124
|
}
|
|
79
|
-
|
|
125
|
+
// WI-350: unit を持たないパス(プロジェクト直下のファイル・__tests__ 配下など)は
|
|
126
|
+
// 集約 unitId が undefined になる。旧実装はこれを即拒否していたが、
|
|
127
|
+
// 直下の allTargetPathsBelongToUnit は unitless パスを許容しており内部矛盾していた。
|
|
128
|
+
// 集約チェックは「unitId が定義済みかつ session unit と不一致」の場合のみ拒否し、
|
|
129
|
+
// unitless は per-path チェックへ委ねる。unit 付きパスが 1 つでも混ざれば
|
|
130
|
+
// per-path 側が従来どおり拒否するため、unit 境界は緩まない。
|
|
131
|
+
if (input.unitId !== undefined && input.unitId !== document.unit) {
|
|
80
132
|
return {
|
|
81
133
|
active: true,
|
|
82
134
|
allowed: false,
|
|
83
|
-
reason: `target unit ${input.unitId
|
|
135
|
+
reason: `target unit ${input.unitId} does not match session unit ${document.unit}`,
|
|
84
136
|
workItemId: document.workItemId,
|
|
85
137
|
unit: document.unit,
|
|
86
138
|
expiresAt: document.expiresAt,
|
|
@@ -83,9 +83,24 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
83
83
|
// GitHub #40: config が JSON として壊れている場合に hook プロセス全体を throw で
|
|
84
84
|
// 落とすと、エージェントの全ツール呼び出しが遮断され config の修復自体が不能になる。
|
|
85
85
|
// main.ts の ConfigPersistenceError と同じ意味論(警告 + 既定値で続行)に揃える。
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
86
|
+
// ADR-038 §4 G1 / WI-333: config **不在**(ENOENT)も同じデッドロックを起こすため、
|
|
87
|
+
// invalid-json と同じ「警告 + 既定値で続行」の fail-open とする。config を作成する
|
|
88
|
+
// Write 自体が遮断されない(自己修復経路が残る)ことがこの adapter の契約。
|
|
89
|
+
// それ以外の fs エラー(EACCES 等の真の異常)は従来どおり throw する。
|
|
90
|
+
// gated スコープへの書き込みは phase-gate 側が既定設定で fail-closed を維持する。
|
|
91
|
+
let raw: string;
|
|
92
|
+
try {
|
|
93
|
+
raw = fs.readFileSync(this.configPath, "utf8");
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
`Warning: phasegate.config.json not found at ${this.configPath}; continuing with default hook settings so self-repair stays possible (run \`phasegate init\` to generate one).\n`,
|
|
98
|
+
);
|
|
99
|
+
this.cachedConfig = {};
|
|
100
|
+
return this.cachedConfig;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
89
104
|
let doc: HarnessConfigDocument;
|
|
90
105
|
try {
|
|
91
106
|
doc = JSON.parse(raw) as HarnessConfigDocument;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @work-item-id WI-202 / WI-204
|
|
5
5
|
* @work-item-id WI-206
|
|
6
6
|
* @work-item-id WI-208
|
|
7
|
+
* @work-item-id WI-345
|
|
8
|
+
* @work-item-id WI-347
|
|
7
9
|
*
|
|
8
10
|
* PreToolUse Hook Adapter
|
|
9
11
|
* Claude Code の PreToolUse Hook エントリポイント
|
|
@@ -80,8 +82,10 @@ function projectRootForConfig(configPath: string): string {
|
|
|
80
82
|
return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
+
function isProjectExternalPath(filePath: string, cwd: string, projectRoot: string): boolean {
|
|
86
|
+
const resolvedPath = path.resolve(cwd, filePath);
|
|
87
|
+
const relativePath = path.relative(projectRoot, resolvedPath);
|
|
88
|
+
return relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
async function main(): Promise<void> {
|
|
@@ -141,15 +145,29 @@ async function main(): Promise<void> {
|
|
|
141
145
|
const bashTargets = extractor.extract(input.tool_input.command);
|
|
142
146
|
if (bashTargets.length > 0) {
|
|
143
147
|
targetFilePaths.push(...bashTargets.map(toRelative));
|
|
148
|
+
const recordedTargetPaths = new Set(targetChanges.map((change) => change.filePath));
|
|
149
|
+
const bashTargetChanges = await Promise.all(
|
|
150
|
+
bashTargets.map((bashTarget) => buildBashTargetChange(cwd, bashTarget, toRelative)),
|
|
151
|
+
);
|
|
152
|
+
for (const change of bashTargetChanges) {
|
|
153
|
+
if (!recordedTargetPaths.has(change.filePath)) {
|
|
154
|
+
targetChanges.push(change);
|
|
155
|
+
recordedTargetPaths.add(change.filePath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
144
158
|
effectiveToolName = 'Write';
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
try {
|
|
149
163
|
const configPath = await findConfigPath(cwd);
|
|
150
|
-
const projectTargetFilePaths = targetFilePaths.filter((filePath) => !isProjectExternalAbsolutePath(filePath));
|
|
151
|
-
const projectTargetChanges = targetChanges.filter((change) => !isProjectExternalAbsolutePath(change.filePath));
|
|
152
164
|
const projectRoot = projectRootForConfig(configPath);
|
|
165
|
+
const projectTargetFilePaths = targetFilePaths.filter(
|
|
166
|
+
(filePath) => !isProjectExternalPath(filePath, cwd, projectRoot),
|
|
167
|
+
);
|
|
168
|
+
const projectTargetChanges = targetChanges.filter(
|
|
169
|
+
(change) => !isProjectExternalPath(change.filePath, cwd, projectRoot),
|
|
170
|
+
);
|
|
153
171
|
const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
|
|
154
172
|
const phaseGateQueryPort = new PhaseGateQueryAdapter();
|
|
155
173
|
const storyReflectionQueryPort = new FileSystemStoryReflectionQueryAdapter({
|
|
@@ -157,7 +175,8 @@ async function main(): Promise<void> {
|
|
|
157
175
|
configPath,
|
|
158
176
|
});
|
|
159
177
|
const fullModeRequirementQueryPort = new QuickModeFullModeRequirementAdapter({
|
|
160
|
-
classifyUseCaseFactory: () =>
|
|
178
|
+
classifyUseCaseFactory: () =>
|
|
179
|
+
createQuickModeCompositionRoot({ configPath, rootDir: projectRoot }).classifyUseCase,
|
|
161
180
|
});
|
|
162
181
|
const baselineGrandfatherQueryPort = new CiGovernanceBaselineGrandfatherAdapter({
|
|
163
182
|
baseDir: projectRoot,
|
|
@@ -222,7 +241,7 @@ async function buildTargetChanges(
|
|
|
222
241
|
toRelative: (p: string) => string,
|
|
223
242
|
): Promise<TargetChange[]> {
|
|
224
243
|
const toolInput = input.tool_input;
|
|
225
|
-
if (toolInput
|
|
244
|
+
if (toolInput == null || typeof toolInput !== 'object') {
|
|
226
245
|
return [];
|
|
227
246
|
}
|
|
228
247
|
|
|
@@ -258,6 +277,32 @@ async function readExistingContent(cwd: string, filePath: string): Promise<strin
|
|
|
258
277
|
}
|
|
259
278
|
}
|
|
260
279
|
|
|
280
|
+
async function buildBashTargetChange(
|
|
281
|
+
cwd: string,
|
|
282
|
+
rawPath: string,
|
|
283
|
+
toRelative: (p: string) => string,
|
|
284
|
+
): Promise<TargetChange> {
|
|
285
|
+
const filePath = toRelative(rawPath);
|
|
286
|
+
if (rawPath.includes('$') || rawPath.includes('`') || rawPath.startsWith('~')) {
|
|
287
|
+
// hook ではシェル展開後の実パスを解決できないため、存在チェックを避けて MODIFY 既定にする。
|
|
288
|
+
return { filePath };
|
|
289
|
+
}
|
|
290
|
+
const absolutePath = path.isAbsolute(rawPath) ? rawPath : path.join(cwd, rawPath);
|
|
291
|
+
try {
|
|
292
|
+
await fs.stat(absolutePath);
|
|
293
|
+
return { filePath };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
296
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
297
|
+
// Bash では afterContent を取得できないため、空文字を CREATE 判定用の
|
|
298
|
+
// 「変更後内容あり」sentinel として渡す。実ファイルへの書き込みは行わない。
|
|
299
|
+
return { filePath, beforeContent: null, afterContent: '' };
|
|
300
|
+
}
|
|
301
|
+
// 権限エラー等で存在を確認できない場合は従来どおり MODIFY 既定(安全側)。
|
|
302
|
+
return { filePath };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
261
306
|
main().catch((error) => {
|
|
262
307
|
process.stderr.write(`予期しないエラー: ${String(error)}\n`);
|
|
263
308
|
process.exit(2);
|
|
@@ -126,11 +126,11 @@ export function buildCiGovernance(baseDir: string, harnessRoot: string = baseDir
|
|
|
126
126
|
|
|
127
127
|
// Handlers
|
|
128
128
|
const generateCiTemplateHandler = new GenerateCiTemplateHandler(generateCiTemplateUseCase, renderCiTemplateUseCase);
|
|
129
|
-
const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase);
|
|
129
|
+
const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase, validatePointersUseCase);
|
|
130
130
|
const refreshAgentContextHandler = new RefreshAgentContextHandler(refreshAgentContextUseCase);
|
|
131
131
|
const refreshClaudeMdHandler = new RefreshClaudeMdHandler(refreshClaudeMdUseCase);
|
|
132
132
|
const checkAgentContextHandler = new CheckAgentContextHandler(checkAgentContextUseCase);
|
|
133
|
-
const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase);
|
|
133
|
+
const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase, resetRepetitionUseCase);
|
|
134
134
|
const createBaselineHandler = new CreateBaselineHandler(createBaselineUseCase);
|
|
135
135
|
const scaffoldDesignHandler = new ScaffoldDesignHandler(scaffoldDesignUseCase);
|
|
136
136
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit ci-governance
|
|
4
|
+
* @work-item-id WI-331
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
export interface ClaudeMdTemplateValues {
|
|
@@ -9,20 +10,28 @@ export interface ClaudeMdTemplateValues {
|
|
|
9
10
|
readonly presets: readonly string[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const USER_SECTION_START =
|
|
13
|
-
const USER_SECTION_END =
|
|
14
|
-
const DEFAULT_USER_SECTION =
|
|
13
|
+
const USER_SECTION_START = "<!-- phasegate:user-section:start -->";
|
|
14
|
+
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
15
|
+
const DEFAULT_USER_SECTION = "Project-specific agent instructions go here.";
|
|
15
16
|
|
|
16
17
|
export class ClaudeMdComposer {
|
|
18
|
+
// Rebuilds CLAUDE.md from the current template, carrying over the existing
|
|
19
|
+
// user-section body. Because the whole file is re-rendered, a legacy file
|
|
20
|
+
// whose user-section still sits inside the managed block (pre-WI-331
|
|
21
|
+
// template shape) is migrated to the current outside-of-block structure.
|
|
17
22
|
compose(template: string, existing: string | null, values: ClaudeMdTemplateValues): string {
|
|
18
23
|
const userSection = this.extractUserSection(existing) ?? DEFAULT_USER_SECTION;
|
|
19
|
-
|
|
20
|
-
.replace(
|
|
21
|
-
.replace(
|
|
22
|
-
.replace(
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
.
|
|
24
|
+
const rendered = template
|
|
25
|
+
.replace("{{PHASEGATE_COMMANDS}}", this.toList(values.commands))
|
|
26
|
+
.replace("{{PHASEGATE_SKILLS}}", this.toList(values.skills))
|
|
27
|
+
.replace("{{PHASEGATE_PRESETS}}", this.toList(values.presets))
|
|
28
|
+
// Collapse blank-line runs before injecting the user body so
|
|
29
|
+
// user-authored spacing is preserved byte-for-byte.
|
|
30
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
31
|
+
// Replacer function keeps `$`-sequences in the user body from being
|
|
32
|
+
// interpreted as String.replace substitution patterns.
|
|
33
|
+
.replace("{{PHASEGATE_USER_SECTION}}", () => userSection);
|
|
34
|
+
return `${rendered.trimEnd()}\n`;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
private extractUserSection(existing: string | null): string | null {
|
|
@@ -34,6 +43,6 @@ export class ClaudeMdComposer {
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
private toList(values: readonly string[]): string {
|
|
37
|
-
return values.map((value) => `- \`${value}\``).join(
|
|
46
|
+
return values.map((value) => `- \`${value}\``).join("\n");
|
|
38
47
|
}
|
|
39
48
|
}
|
|
@@ -29,6 +29,13 @@ export class CheckRepetitionHandler {
|
|
|
29
29
|
async handle(args: CheckRepetitionHandlerArgs): Promise<CheckRepetitionHandlerResult> {
|
|
30
30
|
const { errorCode, reset = false, format = 'human' } = args;
|
|
31
31
|
|
|
32
|
+
if (errorCode.trim().length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
output: 'Error: --code <errorCode> is required. Usage: phasegate ci:check-repetition --code <errorCode> [--reset] [--json]',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
if (reset && this.resetUseCase) {
|
|
33
40
|
const result = await this.resetUseCase.execute({ errorCode, confirmedResolution: true });
|
|
34
41
|
|
|
@@ -49,9 +56,10 @@ export class CheckRepetitionHandler {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
const result = await this.checkUseCase.execute({ errorCode });
|
|
59
|
+
const exitCode = result.escalated === true ? 1 : 0;
|
|
52
60
|
|
|
53
61
|
if (format === 'json') {
|
|
54
|
-
return { exitCode
|
|
62
|
+
return { exitCode, output: JSON.stringify(result, null, 2) };
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
const lines: string[] = [];
|
|
@@ -63,6 +71,6 @@ export class CheckRepetitionHandler {
|
|
|
63
71
|
lines.push(` Escalated: ${result.escalated}`);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
return { exitCode
|
|
74
|
+
return { exitCode, output: lines.join('\n') };
|
|
67
75
|
}
|
|
68
76
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @layer domain
|
|
2
2
|
// @unit harness-api
|
|
3
|
-
// @work-item-id WI-108 / WI-114, WI-186, WI-318, WI-321, WI-328
|
|
3
|
+
// @work-item-id WI-108 / WI-114, WI-186, WI-318, WI-321, WI-328, WI-339
|
|
4
4
|
// command-dispatch-service.ts — CommandDispatchService Domain Service
|
|
5
5
|
|
|
6
6
|
import type { ArtifactScannerPort } from "../ports/artifact-scanner-port.js";
|
|
@@ -10,7 +10,7 @@ import type { ImpactAnalysisPort } from "../ports/impact-analysis-port.js";
|
|
|
10
10
|
import type { PhaseGateQueryPort } from "../ports/phase-gate-query-port.js";
|
|
11
11
|
import type { ValidatorExecutionPort } from "../ports/validator-execution-port.js";
|
|
12
12
|
import { CheckReadyResult } from "../value-objects/check-ready-result.js";
|
|
13
|
-
import { CiCheckResult } from "../value-objects/ci-check-result.js";
|
|
13
|
+
import { CiCheckResult, type ValidatorCheckItem } from "../value-objects/ci-check-result.js";
|
|
14
14
|
import { DriftReportSummary } from "../value-objects/drift-report-summary.js";
|
|
15
15
|
import { type ExitCode, HarnessApiResponse, type HarnessError } from "../value-objects/harness-api-response.js";
|
|
16
16
|
import type {
|
|
@@ -76,6 +76,15 @@ function summarizeLayerResults(
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* WI-339: status の layer 集計へ渡す前に、ci-check / complete-check と同じ
|
|
81
|
+
* CiCheckResult 投影で raw passed を実効 passed に正規化する。
|
|
82
|
+
* failOnWarning は既存 harness-api 経路と同じ既定値 false を使用する。
|
|
83
|
+
*/
|
|
84
|
+
function toEffectiveValidatorResults(items: readonly ValidatorCheckItem[]): readonly ValidatorCheckItem[] {
|
|
85
|
+
return items.length === 0 ? items : CiCheckResult.fromResults(items).validatorResults;
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
function hasEnabledLiveFailure(
|
|
80
89
|
enabledLayers: readonly LayerId[],
|
|
81
90
|
liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>>,
|
|
@@ -248,7 +257,7 @@ export class CommandDispatchService {
|
|
|
248
257
|
this.ports.validatorExecutionPort.runAllValidators(),
|
|
249
258
|
]);
|
|
250
259
|
liveValidationByLayer.L1 = lintResult.passed ? "pass" : "fail";
|
|
251
|
-
Object.assign(liveValidationByLayer, summarizeLayerResults(validatorResults));
|
|
260
|
+
Object.assign(liveValidationByLayer, summarizeLayerResults(toEffectiveValidatorResults(validatorResults)));
|
|
252
261
|
} catch {
|
|
253
262
|
liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? "error";
|
|
254
263
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// @layer domain
|
|
2
2
|
// @unit harness-api
|
|
3
3
|
// @work-item-id WI-307
|
|
4
|
+
// @work-item-id WI-332
|
|
4
5
|
// ci-check-result.ts — CiCheckResult Value Object
|
|
5
6
|
|
|
7
|
+
import { isEffectivelyPassed } from "../../../validator-system/domain/services/effective-severity-policy.js";
|
|
6
8
|
import type { HarnessError } from "./harness-api-response.js";
|
|
7
9
|
|
|
8
10
|
export interface ValidatorCheckItem {
|
|
@@ -19,28 +21,6 @@ export interface CiCheckResultProps {
|
|
|
19
21
|
failOnWarning?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
* WI-260 / ADR-017: validator 1件が「実質 pass」か判定する severity-aware ルール。
|
|
24
|
-
* `validate` 経路の AggregateValidationResultsUseCase と同一の判定式を harness-api の
|
|
25
|
-
* domain VO 側に持たせ、ci-check と validate の集約挙動を一致させる(レイヤー越境回避のため
|
|
26
|
-
* usecase を直接 import せず判定ロジックを共有)。
|
|
27
|
-
*
|
|
28
|
-
* - skipped: 実質 pass
|
|
29
|
-
* - passed=true: 実質 pass
|
|
30
|
-
* - passed=false かつ error severity(!= warning)を含む: fail
|
|
31
|
-
* - passed=false かつ errors=[](severity 判定不能): 安全側に倒して fail
|
|
32
|
-
* - passed=false かつ warning のみ: failOnWarning=false(既定)で実質 pass、true で fail
|
|
33
|
-
*/
|
|
34
|
-
function isEffectivelyPassed(item: ValidatorCheckItem, failOnWarning: boolean): boolean {
|
|
35
|
-
if (item.skipped || item.passed) return true;
|
|
36
|
-
const errors = item.errors ?? [];
|
|
37
|
-
const hasNonWarningError = errors.some((e) => e.severity !== "warning");
|
|
38
|
-
const hasWarnings = errors.some((e) => e.severity === "warning");
|
|
39
|
-
const isEmptyFail = errors.length === 0;
|
|
40
|
-
const hasFail = isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings);
|
|
41
|
-
return !hasFail;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
24
|
/**
|
|
45
25
|
* WI-307: public ci-check projection の `passed` を aggregate policy と一致させる。
|
|
46
26
|
* skipped は独立した green state のため raw passed を維持し、warning-only failure だけを
|
|
@@ -8,11 +8,13 @@ export interface CreateHarnessErrorInput {
|
|
|
8
8
|
readonly code: string;
|
|
9
9
|
readonly message: string;
|
|
10
10
|
readonly suggestion: string;
|
|
11
|
-
readonly severity?:
|
|
11
|
+
readonly severity?: "error" | "warning";
|
|
12
12
|
readonly adrRef?: string;
|
|
13
13
|
readonly fixExample?: string;
|
|
14
14
|
readonly validatorId: string;
|
|
15
15
|
readonly suggestedSkill?: string;
|
|
16
16
|
readonly scaffoldCommand?: string;
|
|
17
17
|
readonly templatePath?: string;
|
|
18
|
+
/** WI-335: 修復方式分類。省略時は ErrorDefinition の既定(未定義なら manual 扱い)。 */
|
|
19
|
+
readonly remediationType?: "mechanical" | "ai-assisted" | "manual";
|
|
18
20
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
export interface HarnessErrorContract {
|
|
5
5
|
readonly code: string;
|
|
6
|
-
readonly severity:
|
|
6
|
+
readonly severity: "error" | "warning";
|
|
7
7
|
readonly message: string;
|
|
8
8
|
readonly suggestion: string;
|
|
9
9
|
readonly adr_ref?: string;
|
|
@@ -11,4 +11,6 @@ export interface HarnessErrorContract {
|
|
|
11
11
|
readonly suggested_skill?: string;
|
|
12
12
|
readonly scaffold_command?: string;
|
|
13
13
|
readonly template_path?: string;
|
|
14
|
+
/** WI-335: 修復方式分類(mechanical / ai-assisted / manual)。未設定は manual 扱い。 */
|
|
15
|
+
readonly remediation_type?: "mechanical" | "ai-assisted" | "manual";
|
|
14
16
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* HarnessErrorContract への投影Mapper
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
import type { HarnessError } from
|
|
7
|
+
|
|
8
|
+
import type { HarnessError } from "../../domain/value-objects/harness-error.js";
|
|
9
|
+
import type { HarnessErrorContract } from "../dto/harness-error-contract.js";
|
|
9
10
|
|
|
10
11
|
export class HarnessErrorContractMapper {
|
|
11
12
|
toReadonlyContract(harnessError: HarnessError): Readonly<HarnessErrorContract> {
|
|
@@ -14,21 +15,12 @@ export class HarnessErrorContractMapper {
|
|
|
14
15
|
severity: harnessError.severity.toString(),
|
|
15
16
|
message: harnessError.message,
|
|
16
17
|
suggestion: harnessError.suggestion,
|
|
17
|
-
...(harnessError.adrRef !== null
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
...(harnessError.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
...(harnessError.suggestedSkill !== null
|
|
24
|
-
? { suggested_skill: harnessError.suggestedSkill }
|
|
25
|
-
: {}),
|
|
26
|
-
...(harnessError.scaffoldCommand !== null
|
|
27
|
-
? { scaffold_command: harnessError.scaffoldCommand }
|
|
28
|
-
: {}),
|
|
29
|
-
...(harnessError.templatePath !== null
|
|
30
|
-
? { template_path: harnessError.templatePath }
|
|
31
|
-
: {}),
|
|
18
|
+
...(harnessError.adrRef !== null ? { adr_ref: harnessError.adrRef.toString() } : {}),
|
|
19
|
+
...(harnessError.fixExample !== null ? { fix_example: harnessError.fixExample.toString() } : {}),
|
|
20
|
+
...(harnessError.suggestedSkill !== null ? { suggested_skill: harnessError.suggestedSkill } : {}),
|
|
21
|
+
...(harnessError.scaffoldCommand !== null ? { scaffold_command: harnessError.scaffoldCommand } : {}),
|
|
22
|
+
...(harnessError.templatePath !== null ? { template_path: harnessError.templatePath } : {}),
|
|
23
|
+
...(harnessError.remediationType !== null ? { remediation_type: harnessError.remediationType } : {}),
|
|
32
24
|
};
|
|
33
25
|
|
|
34
26
|
return Object.freeze(contract);
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 単一draftを HarnessErrorContract へ変換するUseCase
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
7
|
+
|
|
8
|
+
import type { HarnessErrorFactory } from "../../domain/services/harness-error-factory.js";
|
|
9
|
+
import type { CreateHarnessErrorInput } from "../dto/create-harness-error-input.js";
|
|
10
|
+
import type { HarnessErrorContract } from "../dto/harness-error-contract.js";
|
|
11
|
+
import type { HarnessErrorContractMapper } from "../mappers/harness-error-contract-mapper.js";
|
|
11
12
|
|
|
12
13
|
export interface CreateHarnessErrorUseCaseDeps {
|
|
13
14
|
readonly harnessErrorFactory: HarnessErrorFactory;
|
|
@@ -23,9 +24,7 @@ export class CreateHarnessErrorUseCase {
|
|
|
23
24
|
this.contractMapper = deps.contractMapper;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
async execute(
|
|
27
|
-
input: CreateHarnessErrorInput
|
|
28
|
-
): Promise<Readonly<HarnessErrorContract>> {
|
|
27
|
+
async execute(input: CreateHarnessErrorInput): Promise<Readonly<HarnessErrorContract>> {
|
|
29
28
|
const harnessError = await this.harnessErrorFactory.create({
|
|
30
29
|
code: input.code,
|
|
31
30
|
message: input.message,
|
|
@@ -37,6 +36,7 @@ export class CreateHarnessErrorUseCase {
|
|
|
37
36
|
suggestedSkill: input.suggestedSkill,
|
|
38
37
|
scaffoldCommand: input.scaffoldCommand,
|
|
39
38
|
templatePath: input.templatePath,
|
|
39
|
+
remediationType: input.remediationType,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
return this.contractMapper.toReadonlyContract(harnessError);
|