phasegate 0.160.7 → 0.160.8
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
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.160.8] - 2026-05-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-201 — managed config plan apply path** — adds `config:plan --apply` for applicable config patch plans, records rollback backups before mutating `phasegate.config.json`, advertises managed apply commands in config-plan guidance, and points config edit blocks back to the reviewed dry-run/apply workflow.
|
|
15
|
+
|
|
10
16
|
## [0.160.6] - 2026-05-14
|
|
11
17
|
|
|
12
18
|
### Fixed
|
package/package.json
CHANGED
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit agent-integration
|
|
4
4
|
* @story H11-02
|
|
5
|
+
* @work-item-id WI-201
|
|
5
6
|
*
|
|
6
7
|
* HandlePreToolUseUseCase
|
|
7
8
|
* PreToolUse Hook処理のオーケストレーション
|
|
@@ -237,6 +238,31 @@ export class HandlePreToolUseUseCase {
|
|
|
237
238
|
unitId: string | undefined,
|
|
238
239
|
): HandlePreToolUseOutput {
|
|
239
240
|
const fp = blockedFilePath ?? "不明なファイル";
|
|
241
|
+
if (result.dominantCategory === "config" && /(?:^|\/)phasegate\.config\.json$/.test(fp)) {
|
|
242
|
+
const dryRunCommand = "phasegate config:plan --intent retrofit-bootstrap --dry-run --json";
|
|
243
|
+
const applyCommand = "phasegate config:plan --intent retrofit-bootstrap --apply --json";
|
|
244
|
+
const lines = [
|
|
245
|
+
`Full mode 必須変更が検出されました: ${fp}`,
|
|
246
|
+
"カテゴリ: config",
|
|
247
|
+
];
|
|
248
|
+
if (result.rejectionRule) {
|
|
249
|
+
lines.push(`判定ルール: ${result.rejectionRule}`);
|
|
250
|
+
}
|
|
251
|
+
if (result.rejectionReason) {
|
|
252
|
+
lines.push(`理由: ${result.rejectionReason}`);
|
|
253
|
+
}
|
|
254
|
+
lines.push(`次のアクション: ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
shouldBlock: true,
|
|
258
|
+
blockedFilePath,
|
|
259
|
+
blockReason: "FULL_MODE_REQUIRED",
|
|
260
|
+
error: { message: lines.join("\n") },
|
|
261
|
+
fullModeRejectionRule: result.rejectionRule,
|
|
262
|
+
fullModeDominantCategory: result.dominantCategory,
|
|
263
|
+
nextAction: `${dryRunCommand} && ${applyCommand}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
240
266
|
const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
|
|
241
267
|
if (result.dominantCategory) {
|
|
242
268
|
lines.push(`カテゴリ: ${result.dominantCategory}`);
|
|
@@ -373,6 +399,11 @@ export class HandlePreToolUseUseCase {
|
|
|
373
399
|
message: (fp) =>
|
|
374
400
|
`保護ファイルへの書き込みがブロックされました: ${fp}\nバージョン変更を含む package.json の更新は /quick-implementor スキルを使用してください。`,
|
|
375
401
|
},
|
|
402
|
+
{
|
|
403
|
+
pattern: /(?:^|\/)phasegate\.config\.json$/,
|
|
404
|
+
message: (fp) =>
|
|
405
|
+
`保護ファイルへの書き込みがブロックされました: ${fp}\n設定変更は CLI 経由で計画・適用してください: phasegate config:plan --intent retrofit-bootstrap --dry-run --json / phasegate config:plan --intent retrofit-bootstrap --apply --json`,
|
|
406
|
+
},
|
|
376
407
|
{
|
|
377
408
|
pattern: /(?:^|\/)harness\.config\.json$/,
|
|
378
409
|
message: (fp) =>
|
package/scripts/harness/main.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @work-item-id WI-196
|
|
14
14
|
* @work-item-id WI-197
|
|
15
15
|
* @work-item-id WI-200
|
|
16
|
+
* @work-item-id WI-201
|
|
16
17
|
*
|
|
17
18
|
* Phasegate CLI エントリポイント。
|
|
18
19
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
readFile as fsReadFile,
|
|
26
27
|
readdir as fsReaddir,
|
|
27
28
|
readlink as fsReadlink,
|
|
29
|
+
rename as fsRename,
|
|
28
30
|
writeFile as fsWriteFile,
|
|
29
31
|
} from "node:fs/promises";
|
|
30
32
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -811,6 +813,12 @@ interface ConfigPatchPreview {
|
|
|
811
813
|
readonly operations: readonly ConfigPatchOperation[];
|
|
812
814
|
}
|
|
813
815
|
|
|
816
|
+
interface ConfigApplyResult {
|
|
817
|
+
readonly changed: boolean;
|
|
818
|
+
readonly backupPath: string;
|
|
819
|
+
readonly appliedOperations: readonly ConfigPatchOperation[];
|
|
820
|
+
}
|
|
821
|
+
|
|
814
822
|
function parseInitPhasePreset(value: string | undefined): InitPhasePreset | undefined {
|
|
815
823
|
if (value === undefined) return undefined;
|
|
816
824
|
if (value === "full" || value === "standard" || value === "minimal" || value === "custom") {
|
|
@@ -1267,6 +1275,40 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
|
|
|
1267
1275
|
};
|
|
1268
1276
|
}
|
|
1269
1277
|
|
|
1278
|
+
function configPlanBackupPath(rootDir: string, now: Date): string {
|
|
1279
|
+
const stamp = now.toISOString().replaceAll(":", "-");
|
|
1280
|
+
return join(rootDir, ".phasegate", "backups", `phasegate.config.${stamp}.json`);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async function applyConfigPlan(rootDir: string, plan: Awaited<ReturnType<typeof buildConfigChangePlan>>): Promise<ConfigApplyResult> {
|
|
1284
|
+
const patch = plan.configPatch;
|
|
1285
|
+
if (patch.applicability !== "applicable") {
|
|
1286
|
+
throw new Error(`config plan is not applicable: ${patch.blockedReason ?? patch.applicability}`);
|
|
1287
|
+
}
|
|
1288
|
+
if (patch.operations.length === 0) {
|
|
1289
|
+
throw new Error("config plan has no operations to apply.");
|
|
1290
|
+
}
|
|
1291
|
+
if (!isPlainRecord(patch.after)) {
|
|
1292
|
+
throw new Error("config plan after-state must be a JSON object.");
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const configPath = join(rootDir, patch.path);
|
|
1296
|
+
const backupPath = configPlanBackupPath(rootDir, new Date());
|
|
1297
|
+
await fsMkdir(dirname(backupPath), { recursive: true });
|
|
1298
|
+
const beforeText = patch.before === null ? "" : `${JSON.stringify(patch.before, null, 2)}\n`;
|
|
1299
|
+
await fsWriteFile(backupPath, beforeText, "utf8");
|
|
1300
|
+
|
|
1301
|
+
const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1302
|
+
await fsWriteFile(tempPath, `${JSON.stringify(patch.after, null, 2)}\n`, "utf8");
|
|
1303
|
+
await fsRename(tempPath, configPath);
|
|
1304
|
+
|
|
1305
|
+
return {
|
|
1306
|
+
changed: true,
|
|
1307
|
+
backupPath: backupPath.slice(rootDir.length + 1),
|
|
1308
|
+
appliedOperations: patch.operations,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1270
1312
|
function hasStructuredInstallError(value: unknown): boolean {
|
|
1271
1313
|
return isPlainRecord(value) && isPlainRecord(value.error);
|
|
1272
1314
|
}
|
|
@@ -1284,7 +1326,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1284
1326
|
targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: layers.L4.failOnWarning"],
|
|
1285
1327
|
managedTargets: ["phasegate.config.json"],
|
|
1286
1328
|
externalActions: [],
|
|
1287
|
-
commands: ["phasegate validate --layer L4 --fail-on-warning --format human"],
|
|
1329
|
+
commands: ["phasegate config:plan --intent l4-strict --apply --json", "phasegate validate --layer L4 --fail-on-warning --format human"],
|
|
1288
1330
|
validations: ["phasegate phasegate:detect-drift --json", "phasegate phasegate:check-ready"],
|
|
1289
1331
|
risks: ["L4 findings may be advisory today but become blocking when fail-on-warning is enabled."],
|
|
1290
1332
|
},
|
|
@@ -1300,7 +1342,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1300
1342
|
targets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
|
|
1301
1343
|
managedTargets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
|
|
1302
1344
|
externalActions: [{ id: "github-actions-first-run", label: "Trigger or inspect the first GitHub Actions PhaseGate run.", command: null, blocking: false }],
|
|
1303
|
-
commands: ["phasegate install --with-ci --apply", "phasegate validate --layer L4 --fail-on-warning"],
|
|
1345
|
+
commands: ["phasegate install --with-ci --apply", "phasegate config:plan --intent ci-fail-on-warning --apply --json", "phasegate validate --layer L4 --fail-on-warning"],
|
|
1304
1346
|
validations: ["phasegate doctor", "phasegate ci:generate-template --type aidlc-gate --render"],
|
|
1305
1347
|
risks: ["Existing warning-only projects may start failing CI after rollout."],
|
|
1306
1348
|
},
|
|
@@ -1316,7 +1358,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1316
1358
|
targets: ["phasegate.config.json: quickMode"],
|
|
1317
1359
|
managedTargets: ["phasegate.config.json"],
|
|
1318
1360
|
externalActions: [],
|
|
1319
|
-
commands: ["phasegate check-change-category --paths <changed-files> --format json"],
|
|
1361
|
+
commands: ["phasegate config:plan --intent quick-mode-strict --apply --json", "phasegate check-change-category --paths <changed-files> --format json"],
|
|
1320
1362
|
validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
|
|
1321
1363
|
risks: ["More changes will require Full Mode validation before commit."],
|
|
1322
1364
|
},
|
|
@@ -1324,7 +1366,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1324
1366
|
targets: ["phasegate.config.json: planningMode.default", "phasegate.config.json: phaseDependencies.override", "phasegate.config.json: quickMode.relaxedGates"],
|
|
1325
1367
|
managedTargets: ["phasegate.config.json"],
|
|
1326
1368
|
externalActions: [],
|
|
1327
|
-
commands: ["phasegate baseline --dry-run", "phasegate config:plan --intent retrofit-bootstrap --json"],
|
|
1369
|
+
commands: ["phasegate baseline --dry-run", "phasegate config:plan --intent retrofit-bootstrap --json", "phasegate config:plan --intent retrofit-bootstrap --apply --json"],
|
|
1328
1370
|
validations: ["phasegate validate-metadata docs/inception/_shared/*.md", "phasegate check-phase-gate --level 2"],
|
|
1329
1371
|
risks: ["Manual planning mode accepts existing retrofit planning evidence; review the patch before applying it to avoid weakening greenfield projects."],
|
|
1330
1372
|
},
|
|
@@ -1332,7 +1374,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1332
1374
|
targets: ["phasegate.config.json: planningMode.default"],
|
|
1333
1375
|
managedTargets: ["phasegate.config.json"],
|
|
1334
1376
|
externalActions: [],
|
|
1335
|
-
commands: ["phasegate config:plan --intent planning-mode-relax --json"],
|
|
1377
|
+
commands: ["phasegate config:plan --intent planning-mode-relax --json", "phasegate config:plan --intent planning-mode-relax --apply --json"],
|
|
1336
1378
|
validations: ["phasegate check-phase-gate --level 2", "phasegate phasegate:check-ready"],
|
|
1337
1379
|
risks: ["Manual planning mode reduces PhaseGate's QA enforcement for plan documents until strict planning is restored."],
|
|
1338
1380
|
},
|
|
@@ -1975,13 +2017,42 @@ async function main(): Promise<void> {
|
|
|
1975
2017
|
}
|
|
1976
2018
|
|
|
1977
2019
|
case "config:plan": {
|
|
1978
|
-
const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--json"];
|
|
2020
|
+
const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--apply", "--json"];
|
|
1979
2021
|
const flagError = validateKnownFlags(args, KNOWN_CONFIG_PLAN_FLAGS);
|
|
1980
2022
|
if (flagError) {
|
|
1981
2023
|
console.error(flagError);
|
|
1982
2024
|
process.exit(2);
|
|
1983
2025
|
}
|
|
2026
|
+
const apply = hasFlag(args, "--apply");
|
|
2027
|
+
if (apply && hasFlag(args, "--dry-run")) {
|
|
2028
|
+
console.error("Error: --apply and --dry-run cannot be used together.");
|
|
2029
|
+
process.exit(2);
|
|
2030
|
+
}
|
|
1984
2031
|
const plan = await buildConfigChangePlan(rootDir, parseConfigChangeIntent(parseFlag(args, "--intent")));
|
|
2032
|
+
if (apply) {
|
|
2033
|
+
try {
|
|
2034
|
+
const applyResult = await applyConfigPlan(rootDir, plan);
|
|
2035
|
+
const output = { ...plan, applyResult };
|
|
2036
|
+
if (json) {
|
|
2037
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2038
|
+
} else {
|
|
2039
|
+
console.log(`phasegate config:plan apply (${plan.intent})`);
|
|
2040
|
+
console.log(`changed: ${applyResult.changed}`);
|
|
2041
|
+
console.log(`backup: ${applyResult.backupPath}`);
|
|
2042
|
+
console.log("Applied operations:");
|
|
2043
|
+
for (const operation of applyResult.appliedOperations) console.log(`- ${operation.op} ${operation.pointer}`);
|
|
2044
|
+
}
|
|
2045
|
+
process.exit(0);
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2048
|
+
if (json) {
|
|
2049
|
+
console.log(JSON.stringify({ intent: plan.intent, refused: true, error: message, configPatch: plan.configPatch }, null, 2));
|
|
2050
|
+
} else {
|
|
2051
|
+
console.error(`Error: ${message}`);
|
|
2052
|
+
}
|
|
2053
|
+
process.exit(1);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1985
2056
|
if (json) {
|
|
1986
2057
|
console.log(JSON.stringify(plan, null, 2));
|
|
1987
2058
|
} else {
|