phasegate 0.160.7 → 0.160.9

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,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.160.9] - 2026-05-15
11
+
12
+ ### Fixed
13
+
14
+ - **WI-201 — config plan apply discoverability** — documents `config:plan --apply` in subcommand help, top-level help, and CLI reference so agents can discover the managed config apply path from `--help`.
15
+
16
+ ## [0.160.8] - 2026-05-15
17
+
18
+ ### Fixed
19
+
20
+ - **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.
21
+
10
22
  ## [0.160.6] - 2026-05-14
11
23
 
12
24
  ### Fixed
@@ -28,7 +28,7 @@ Command names in this document are split into three surfaces:
28
28
  | `uninstall --dry-run` / `--apply` | Remove PhaseGate-managed files and managed blocks using `.phasegate/manifest.json`; `--force` handles managed conflict cases. |
29
29
  | `reconcile --dry-run` / `--apply` | Update PhaseGate-managed files to current package templates and refresh manifest hashes; `--force` allows managed-file replacement with backup. |
30
30
  | `setup:agent` | Agent-readable setup planner and optional apply path. Options: `--intent <minimal\|recommended\|strict\|ci-only\|agent-hooks\|retrofit>`, `--agent <claude\|codex\|both>`, `--workflow <standard\|strict>`, `--with-husky`, `--with-ci`, `--dry-run`, `--apply`, `--json`. <!-- @work-item-id WI-172 --> |
31
- | `config:plan` | Agent-readable configuration change planner. Options: `--intent <l4-strict\|codex-hooks\|ci-fail-on-warning\|baseline-reset\|quick-mode-strict>`, `--dry-run`, `--json`. <!-- @work-item-id WI-173 --> |
31
+ | `config:plan` | Agent-readable configuration change planner. Options: `--intent <l4-strict\|codex-hooks\|ci-fail-on-warning\|baseline-reset\|quick-mode-strict\|retrofit-bootstrap\|planning-mode-relax>`, `--dry-run`, `--apply`, `--json`. <!-- @work-item-id WI-173 --> <!-- @work-item-id WI-201 --> |
32
32
  | `update-skills` | Compatibility alias for `reconcile`; use `reconcile` for new automation. |
33
33
  | `scaffold-wi <unit> <type>` | Create `docs/inception/{unit}/WI-XXX/description.md` using the next free WI number. |
34
34
  | `emit-agent-rules` | Print the AGENTS.md / CLAUDE.md WI workflow rules block. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.7",
3
+ "version": "0.160.9",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -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) =>
@@ -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";
@@ -217,7 +219,7 @@ Gate semantics:
217
219
  refresh-claude-md Refresh CLAUDE.md standard sections (--dry-run, --apply, --json)
218
220
  p2:check-agent-context Check AGENTS.md / CLAUDE.md freshness (--threshold-days <n>, --json)
219
221
  setup:agent Plan agent-driven setup (--intent <minimal|recommended|strict|ci-only|agent-hooks|retrofit>, --agent <claude|codex|both>, --dry-run|--apply, --json)
220
- config:plan Plan safe config changes (--intent <l4-strict|codex-hooks|ci-fail-on-warning|baseline-reset|quick-mode-strict|retrofit-bootstrap|planning-mode-relax>, --dry-run, --json)
222
+ config:plan Plan safe config changes (--intent <l4-strict|codex-hooks|ci-fail-on-warning|baseline-reset|quick-mode-strict|retrofit-bootstrap|planning-mode-relax>, --dry-run|--apply, --json)
221
223
  ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
222
224
  baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
223
225
  scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --dry-run|--apply, --force, --json)
@@ -538,6 +540,7 @@ Intents:
538
540
 
539
541
  Options:
540
542
  --dry-run
543
+ --apply
541
544
  --json
542
545
  --help, -h Show this help`,
543
546
  reconcile: `Usage: phasegate reconcile [options]
@@ -811,6 +814,12 @@ interface ConfigPatchPreview {
811
814
  readonly operations: readonly ConfigPatchOperation[];
812
815
  }
813
816
 
817
+ interface ConfigApplyResult {
818
+ readonly changed: boolean;
819
+ readonly backupPath: string;
820
+ readonly appliedOperations: readonly ConfigPatchOperation[];
821
+ }
822
+
814
823
  function parseInitPhasePreset(value: string | undefined): InitPhasePreset | undefined {
815
824
  if (value === undefined) return undefined;
816
825
  if (value === "full" || value === "standard" || value === "minimal" || value === "custom") {
@@ -1267,6 +1276,40 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
1267
1276
  };
1268
1277
  }
1269
1278
 
1279
+ function configPlanBackupPath(rootDir: string, now: Date): string {
1280
+ const stamp = now.toISOString().replaceAll(":", "-");
1281
+ return join(rootDir, ".phasegate", "backups", `phasegate.config.${stamp}.json`);
1282
+ }
1283
+
1284
+ async function applyConfigPlan(rootDir: string, plan: Awaited<ReturnType<typeof buildConfigChangePlan>>): Promise<ConfigApplyResult> {
1285
+ const patch = plan.configPatch;
1286
+ if (patch.applicability !== "applicable") {
1287
+ throw new Error(`config plan is not applicable: ${patch.blockedReason ?? patch.applicability}`);
1288
+ }
1289
+ if (patch.operations.length === 0) {
1290
+ throw new Error("config plan has no operations to apply.");
1291
+ }
1292
+ if (!isPlainRecord(patch.after)) {
1293
+ throw new Error("config plan after-state must be a JSON object.");
1294
+ }
1295
+
1296
+ const configPath = join(rootDir, patch.path);
1297
+ const backupPath = configPlanBackupPath(rootDir, new Date());
1298
+ await fsMkdir(dirname(backupPath), { recursive: true });
1299
+ const beforeText = patch.before === null ? "" : `${JSON.stringify(patch.before, null, 2)}\n`;
1300
+ await fsWriteFile(backupPath, beforeText, "utf8");
1301
+
1302
+ const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
1303
+ await fsWriteFile(tempPath, `${JSON.stringify(patch.after, null, 2)}\n`, "utf8");
1304
+ await fsRename(tempPath, configPath);
1305
+
1306
+ return {
1307
+ changed: true,
1308
+ backupPath: backupPath.slice(rootDir.length + 1),
1309
+ appliedOperations: patch.operations,
1310
+ };
1311
+ }
1312
+
1270
1313
  function hasStructuredInstallError(value: unknown): boolean {
1271
1314
  return isPlainRecord(value) && isPlainRecord(value.error);
1272
1315
  }
@@ -1284,7 +1327,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1284
1327
  targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: layers.L4.failOnWarning"],
1285
1328
  managedTargets: ["phasegate.config.json"],
1286
1329
  externalActions: [],
1287
- commands: ["phasegate validate --layer L4 --fail-on-warning --format human"],
1330
+ commands: ["phasegate config:plan --intent l4-strict --apply --json", "phasegate validate --layer L4 --fail-on-warning --format human"],
1288
1331
  validations: ["phasegate phasegate:detect-drift --json", "phasegate phasegate:check-ready"],
1289
1332
  risks: ["L4 findings may be advisory today but become blocking when fail-on-warning is enabled."],
1290
1333
  },
@@ -1300,7 +1343,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1300
1343
  targets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
1301
1344
  managedTargets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
1302
1345
  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"],
1346
+ commands: ["phasegate install --with-ci --apply", "phasegate config:plan --intent ci-fail-on-warning --apply --json", "phasegate validate --layer L4 --fail-on-warning"],
1304
1347
  validations: ["phasegate doctor", "phasegate ci:generate-template --type aidlc-gate --render"],
1305
1348
  risks: ["Existing warning-only projects may start failing CI after rollout."],
1306
1349
  },
@@ -1316,7 +1359,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1316
1359
  targets: ["phasegate.config.json: quickMode"],
1317
1360
  managedTargets: ["phasegate.config.json"],
1318
1361
  externalActions: [],
1319
- commands: ["phasegate check-change-category --paths <changed-files> --format json"],
1362
+ commands: ["phasegate config:plan --intent quick-mode-strict --apply --json", "phasegate check-change-category --paths <changed-files> --format json"],
1320
1363
  validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
1321
1364
  risks: ["More changes will require Full Mode validation before commit."],
1322
1365
  },
@@ -1324,7 +1367,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1324
1367
  targets: ["phasegate.config.json: planningMode.default", "phasegate.config.json: phaseDependencies.override", "phasegate.config.json: quickMode.relaxedGates"],
1325
1368
  managedTargets: ["phasegate.config.json"],
1326
1369
  externalActions: [],
1327
- commands: ["phasegate baseline --dry-run", "phasegate config:plan --intent retrofit-bootstrap --json"],
1370
+ commands: ["phasegate baseline --dry-run", "phasegate config:plan --intent retrofit-bootstrap --json", "phasegate config:plan --intent retrofit-bootstrap --apply --json"],
1328
1371
  validations: ["phasegate validate-metadata docs/inception/_shared/*.md", "phasegate check-phase-gate --level 2"],
1329
1372
  risks: ["Manual planning mode accepts existing retrofit planning evidence; review the patch before applying it to avoid weakening greenfield projects."],
1330
1373
  },
@@ -1332,7 +1375,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1332
1375
  targets: ["phasegate.config.json: planningMode.default"],
1333
1376
  managedTargets: ["phasegate.config.json"],
1334
1377
  externalActions: [],
1335
- commands: ["phasegate config:plan --intent planning-mode-relax --json"],
1378
+ commands: ["phasegate config:plan --intent planning-mode-relax --json", "phasegate config:plan --intent planning-mode-relax --apply --json"],
1336
1379
  validations: ["phasegate check-phase-gate --level 2", "phasegate phasegate:check-ready"],
1337
1380
  risks: ["Manual planning mode reduces PhaseGate's QA enforcement for plan documents until strict planning is restored."],
1338
1381
  },
@@ -1975,13 +2018,42 @@ async function main(): Promise<void> {
1975
2018
  }
1976
2019
 
1977
2020
  case "config:plan": {
1978
- const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--json"];
2021
+ const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--apply", "--json"];
1979
2022
  const flagError = validateKnownFlags(args, KNOWN_CONFIG_PLAN_FLAGS);
1980
2023
  if (flagError) {
1981
2024
  console.error(flagError);
1982
2025
  process.exit(2);
1983
2026
  }
2027
+ const apply = hasFlag(args, "--apply");
2028
+ if (apply && hasFlag(args, "--dry-run")) {
2029
+ console.error("Error: --apply and --dry-run cannot be used together.");
2030
+ process.exit(2);
2031
+ }
1984
2032
  const plan = await buildConfigChangePlan(rootDir, parseConfigChangeIntent(parseFlag(args, "--intent")));
2033
+ if (apply) {
2034
+ try {
2035
+ const applyResult = await applyConfigPlan(rootDir, plan);
2036
+ const output = { ...plan, applyResult };
2037
+ if (json) {
2038
+ console.log(JSON.stringify(output, null, 2));
2039
+ } else {
2040
+ console.log(`phasegate config:plan apply (${plan.intent})`);
2041
+ console.log(`changed: ${applyResult.changed}`);
2042
+ console.log(`backup: ${applyResult.backupPath}`);
2043
+ console.log("Applied operations:");
2044
+ for (const operation of applyResult.appliedOperations) console.log(`- ${operation.op} ${operation.pointer}`);
2045
+ }
2046
+ process.exit(0);
2047
+ } catch (error) {
2048
+ const message = error instanceof Error ? error.message : String(error);
2049
+ if (json) {
2050
+ console.log(JSON.stringify({ intent: plan.intent, refused: true, error: message, configPatch: plan.configPatch }, null, 2));
2051
+ } else {
2052
+ console.error(`Error: ${message}`);
2053
+ }
2054
+ process.exit(1);
2055
+ }
2056
+ }
1985
2057
  if (json) {
1986
2058
  console.log(JSON.stringify(plan, null, 2));
1987
2059
  } else {