phasegate 0.160.6 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.6",
3
+ "version": "0.160.8",
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) =>
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit ci-governance
4
+ * @work-item-id WI-200
4
5
  */
5
6
 
6
7
  import type { GenerateCiTemplateOutput } from '../../application/dto/generate-ci-template-output.js';
@@ -15,12 +16,13 @@ export class CiTemplateFormatter {
15
16
  lines.push(` [${err.code}] ${err.message}`);
16
17
  }
17
18
  } else {
18
- lines.push('✓ CI Template Generated');
19
+ lines.push('✓ CI Template Plan Ready');
19
20
  lines.push(` Template Type: ${output.templateType}`);
20
21
  lines.push(` Preset: ${output.presetRef}`);
21
22
  lines.push(` Trigger: ${output.triggerCondition}`);
22
23
  lines.push(` Validators: ${output.targetValidatorIds.join(', ')}`);
23
24
  lines.push(` Fail on Warning: ${output.failOnWarning}`);
25
+ lines.push(' Output: no file written; use --render to print template YAML');
24
26
  }
25
27
 
26
28
  return lines.join('\n');
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-146
4
4
  // @work-item-id WI-174
5
+ // @work-item-id WI-198
5
6
  // @work-item-id WI-175
6
7
  // @work-item-id WI-177
7
8
  // @work-item-id WI-182
@@ -190,7 +191,7 @@ function renderAgentContextTemplate(
190
191
  .replaceAll("{{PHASEGATE_HUSKY_STATE}}", options.includeHusky ? "managed" : "not managed by this setup run")
191
192
  .replaceAll("{{PHASEGATE_CI_STATE}}", options.includeCi ? "managed" : "not managed by this setup run")
192
193
  .replaceAll("{{PHASEGATE_COMMANDS}}", commands)
193
- .replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "core" ? "- core skills" : "- all bundled skills")
194
+ .replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "core" ? "- `core skills`" : "- `all bundled skills`")
194
195
  .replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
195
196
  .replaceAll("{{PHASEGATE_USER_SECTION}}", "Project-specific agent instructions go here.");
196
197
  }
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-148
4
4
  // @work-item-id WI-174
5
+ // @work-item-id WI-198
5
6
 
6
7
  import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, symlink, writeFile } from "node:fs/promises";
7
8
  import { dirname, join, relative, resolve } from "node:path";
@@ -156,12 +157,12 @@ function reconcileManagedMarkdown(existing: string | null, incoming: string): st
156
157
 
157
158
  function renderAgentContextTemplate(template: string): string {
158
159
  const commands = [
159
- "- `phasegate doctor`",
160
- "- `phasegate phasegate:check-ready`",
161
- "- `phasegate validate --layer L2 --format human`",
162
- "- `phasegate setup:agent --dry-run`",
163
- "- `phasegate config:plan --intent l4-strict --dry-run`",
164
- ].join("\n");
160
+ "phasegate doctor",
161
+ "phasegate phasegate:check-ready",
162
+ "phasegate validate --layer L2 --format human",
163
+ "phasegate setup:agent --dry-run",
164
+ "phasegate config:plan --intent l4-strict --dry-run",
165
+ ].map((command) => `- \`${command}\``).join("\n");
165
166
  return template
166
167
  .replaceAll("{{PHASEGATE_AGENT}}", "both")
167
168
  .replaceAll("{{PHASEGATE_SKILLS_MODE}}", "all")
@@ -169,7 +170,7 @@ function renderAgentContextTemplate(template: string): string {
169
170
  .replaceAll("{{PHASEGATE_HUSKY_STATE}}", "managed")
170
171
  .replaceAll("{{PHASEGATE_CI_STATE}}", "managed")
171
172
  .replaceAll("{{PHASEGATE_COMMANDS}}", commands)
172
- .replaceAll("{{PHASEGATE_SKILLS}}", "- all bundled skills")
173
+ .replaceAll("{{PHASEGATE_SKILLS}}", "- `all bundled skills`")
173
174
  .replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
174
175
  .replaceAll("{{PHASEGATE_USER_SECTION}}", "Project-specific agent instructions go here.");
175
176
  }
@@ -299,7 +300,7 @@ export class RunReconcileUseCase {
299
300
  const matchesManifest = currentHash.equals(entry.hash);
300
301
  const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
301
302
  const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
302
- const next = entry.mode === "created" && target.strategy !== "package-json"
303
+ const next = entry.mode === "created" && target.strategy !== "package-json" && target.strategy !== "markdown-managed"
303
304
  ? template
304
305
  : this.reconcileContent(target, before, template, input.phasegateVersion);
305
306
  const changed = before !== next;
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-147
4
4
  // @work-item-id WI-174
5
+ // @work-item-id WI-199
5
6
 
6
7
  import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
7
8
  import { dirname, join, relative, resolve } from "node:path";
@@ -19,6 +20,7 @@ export interface UninstallPlanItem {
19
20
  readonly repairMode: RepairMode;
20
21
  readonly strategy: StrategyType;
21
22
  readonly changed: boolean;
23
+ readonly protected: boolean;
22
24
  readonly summary: string;
23
25
  readonly diff: string;
24
26
  readonly skillHint: string | null;
@@ -46,6 +48,7 @@ const SHELL_END = "# === phasegate managed (END) ===";
46
48
  const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
47
49
  const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
48
50
  const PHASEGATE_SCRIPT_PREFIX = "phasegate:";
51
+ const PROTECTED_UNINSTALL_PATHS = new Set(["package.json", "package-lock.json"]);
49
52
 
50
53
  function isRecord(value: unknown): value is Record<string, unknown> {
51
54
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -175,7 +178,7 @@ export class RunUninstallUseCase {
175
178
  const outcome = await this.planEntry(input, entry);
176
179
  outcomes.push({ entry, ...outcome });
177
180
  plan.push(outcome.item);
178
- if (input.apply && outcome.item.changed && (outcome.item.repairMode === "ai-assisted" || outcome.item.repairMode === "manual") && !input.force) {
181
+ if (input.apply && outcome.item.changed && this.requiresForce(outcome.item) && !input.force) {
179
182
  refused.push({ ...outcome.item, action: "refuse" });
180
183
  }
181
184
  }
@@ -372,6 +375,14 @@ export class RunUninstallUseCase {
372
375
  diff: string,
373
376
  skillHint: string | null,
374
377
  ): UninstallPlanItem {
375
- return { path, action, repairMode, strategy, changed, summary, diff, skillHint };
378
+ return { path, action, repairMode, strategy, changed, protected: this.isProtectedPath(path), summary, diff, skillHint };
379
+ }
380
+
381
+ private requiresForce(item: UninstallPlanItem): boolean {
382
+ return item.protected || item.repairMode === "ai-assisted" || item.repairMode === "manual";
383
+ }
384
+
385
+ private isProtectedPath(path: string): boolean {
386
+ return PROTECTED_UNINSTALL_PATHS.has(path);
376
387
  }
377
388
  }
@@ -1,6 +1,7 @@
1
1
  // @unit installation
2
2
  // @layer presentation
3
3
  // @work-item-id WI-147
4
+ // @work-item-id WI-199
4
5
 
5
6
  import type { RunUninstallUseCase } from "../../application/usecases/run-uninstall.js";
6
7
 
@@ -33,14 +34,15 @@ export class UninstallHandler {
33
34
  input.apply ? "phasegate uninstall apply" : "phasegate uninstall dry-run",
34
35
  ...result.plan.map((item) => {
35
36
  const hint = item.skillHint ? `; hint: ${item.skillHint}` : "";
36
- return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}); diff: ${item.diff}${hint}`;
37
+ const protectedMarker = item.protected ? "; protected: true" : "";
38
+ return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}${protectedMarker}); diff: ${item.diff}${hint}`;
37
39
  }),
38
40
  ];
39
41
  if (result.backupDir !== null) lines.push(`backups: ${result.backupDir}`);
40
42
  if (result.archivedManifestPath !== null) lines.push(`archived manifest: ${result.archivedManifestPath}`);
41
43
  if (result.refused.length > 0) {
42
44
  lines.push("");
43
- lines.push("Refused ai-assisted/manual targets. Re-run with --force after reviewing the hint.");
45
+ lines.push("Refused protected, ai-assisted, or manual targets. Re-run with --force after reviewing the plan.");
44
46
  }
45
47
  return {
46
48
  stdout: lines.join("\n"),
@@ -11,6 +11,9 @@
11
11
  * @work-item-id WI-191
12
12
  * @work-item-id WI-195
13
13
  * @work-item-id WI-196
14
+ * @work-item-id WI-197
15
+ * @work-item-id WI-200
16
+ * @work-item-id WI-201
14
17
  *
15
18
  * Phasegate CLI エントリポイント。
16
19
  * 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
@@ -23,6 +26,7 @@ import {
23
26
  readFile as fsReadFile,
24
27
  readdir as fsReaddir,
25
28
  readlink as fsReadlink,
29
+ rename as fsRename,
26
30
  writeFile as fsWriteFile,
27
31
  } from "node:fs/promises";
28
32
  import { dirname, join, resolve } from "node:path";
@@ -809,6 +813,12 @@ interface ConfigPatchPreview {
809
813
  readonly operations: readonly ConfigPatchOperation[];
810
814
  }
811
815
 
816
+ interface ConfigApplyResult {
817
+ readonly changed: boolean;
818
+ readonly backupPath: string;
819
+ readonly appliedOperations: readonly ConfigPatchOperation[];
820
+ }
821
+
812
822
  function parseInitPhasePreset(value: string | undefined): InitPhasePreset | undefined {
813
823
  if (value === undefined) return undefined;
814
824
  if (value === "full" || value === "standard" || value === "minimal" || value === "custom") {
@@ -1265,6 +1275,40 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
1265
1275
  };
1266
1276
  }
1267
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
+
1268
1312
  function hasStructuredInstallError(value: unknown): boolean {
1269
1313
  return isPlainRecord(value) && isPlainRecord(value.error);
1270
1314
  }
@@ -1282,7 +1326,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1282
1326
  targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: layers.L4.failOnWarning"],
1283
1327
  managedTargets: ["phasegate.config.json"],
1284
1328
  externalActions: [],
1285
- 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"],
1286
1330
  validations: ["phasegate phasegate:detect-drift --json", "phasegate phasegate:check-ready"],
1287
1331
  risks: ["L4 findings may be advisory today but become blocking when fail-on-warning is enabled."],
1288
1332
  },
@@ -1298,7 +1342,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1298
1342
  targets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
1299
1343
  managedTargets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
1300
1344
  externalActions: [{ id: "github-actions-first-run", label: "Trigger or inspect the first GitHub Actions PhaseGate run.", command: null, blocking: false }],
1301
- 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"],
1302
1346
  validations: ["phasegate doctor", "phasegate ci:generate-template --type aidlc-gate --render"],
1303
1347
  risks: ["Existing warning-only projects may start failing CI after rollout."],
1304
1348
  },
@@ -1314,7 +1358,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1314
1358
  targets: ["phasegate.config.json: quickMode"],
1315
1359
  managedTargets: ["phasegate.config.json"],
1316
1360
  externalActions: [],
1317
- 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"],
1318
1362
  validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
1319
1363
  risks: ["More changes will require Full Mode validation before commit."],
1320
1364
  },
@@ -1322,7 +1366,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1322
1366
  targets: ["phasegate.config.json: planningMode.default", "phasegate.config.json: phaseDependencies.override", "phasegate.config.json: quickMode.relaxedGates"],
1323
1367
  managedTargets: ["phasegate.config.json"],
1324
1368
  externalActions: [],
1325
- 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"],
1326
1370
  validations: ["phasegate validate-metadata docs/inception/_shared/*.md", "phasegate check-phase-gate --level 2"],
1327
1371
  risks: ["Manual planning mode accepts existing retrofit planning evidence; review the patch before applying it to avoid weakening greenfield projects."],
1328
1372
  },
@@ -1330,7 +1374,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1330
1374
  targets: ["phasegate.config.json: planningMode.default"],
1331
1375
  managedTargets: ["phasegate.config.json"],
1332
1376
  externalActions: [],
1333
- 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"],
1334
1378
  validations: ["phasegate check-phase-gate --level 2", "phasegate phasegate:check-ready"],
1335
1379
  risks: ["Manual planning mode reduces PhaseGate's QA enforcement for plan documents until strict planning is restored."],
1336
1380
  },
@@ -1973,13 +2017,42 @@ async function main(): Promise<void> {
1973
2017
  }
1974
2018
 
1975
2019
  case "config:plan": {
1976
- const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--json"];
2020
+ const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--apply", "--json"];
1977
2021
  const flagError = validateKnownFlags(args, KNOWN_CONFIG_PLAN_FLAGS);
1978
2022
  if (flagError) {
1979
2023
  console.error(flagError);
1980
2024
  process.exit(2);
1981
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
+ }
1982
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
+ }
1983
2056
  if (json) {
1984
2057
  console.log(JSON.stringify(plan, null, 2));
1985
2058
  } else {
@@ -2402,6 +2475,16 @@ async function main(): Promise<void> {
2402
2475
  break;
2403
2476
  }
2404
2477
 
2478
+ case "status": {
2479
+ console.error("Warning: 'phasegate status' is deprecated; use 'phasegate phasegate:status'.");
2480
+ const mod = createHarnessApiModule();
2481
+ const flags: Record<string, boolean | string> = {};
2482
+ if (json) flags.json = true;
2483
+ await mod.handlers.status.handle({}, flags);
2484
+ if (!json) await printStoryReflectionStatusLine(rootDir);
2485
+ break;
2486
+ }
2487
+
2405
2488
  case "phasegate:lint": {
2406
2489
  const mod = createHarnessApiModule();
2407
2490
  const flags: Record<string, boolean | string> = {};
@@ -2420,6 +2503,15 @@ async function main(): Promise<void> {
2420
2503
  break;
2421
2504
  }
2422
2505
 
2506
+ case "complete-check": {
2507
+ console.error("Warning: 'phasegate complete-check' is deprecated; use 'phasegate phasegate:complete-check'.");
2508
+ const mod = createHarnessApiModule();
2509
+ const flags: Record<string, boolean | string> = {};
2510
+ if (json) flags.json = true;
2511
+ await mod.handlers.completeCheck.handle({}, flags);
2512
+ break;
2513
+ }
2514
+
2423
2515
  case "phasegate:impact-analysis": {
2424
2516
  const mod = createHarnessApiModule();
2425
2517
  const storyId = args[1] && !args[1].startsWith("--") ? args[1] : (parseFlag(args, "--story-id") ?? "");
@@ -2466,6 +2558,11 @@ Examples:
2466
2558
  phasegate ci:generate-template --preset strict --type pre-commit --render`);
2467
2559
  process.exit(0);
2468
2560
  }
2561
+ const flagError = validateKnownFlags(args.slice(1), ["--preset", "--type", "--render", "--json", "--help"]);
2562
+ if (flagError !== null) {
2563
+ console.error(flagError);
2564
+ process.exit(2);
2565
+ }
2469
2566
  const mod = buildCiGovernance(rootDir, harnessRoot);
2470
2567
  const presetId = parseFlag(args, "--preset") ?? "standard";
2471
2568
  const templateType = parseFlag(args, "--type") ?? "aidlc-gate";