phasegate 0.160.5 → 0.160.6

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.6] - 2026-05-14
11
+
12
+ ### Fixed
13
+
14
+ - **WI-190..WI-196 — post-0.160.5 dogfood regressions** — aligns agent context refresh with reconcile rendering, adds retrofit planning-mode config plans, makes cascade update dry-runs non-mutating and explicit, fixes doctor `_shared` drift counts, removes pnpm-only scheduled CI templates, exposes `migrate work-items` help, and accepts positional `delegate-sonnet` prompts.
15
+
10
16
  ## [0.160.5] - 2026-05-14
11
17
 
12
18
  ### Fixed
@@ -1,4 +1,5 @@
1
1
  # Phasegate — Agent context refresh workflow
2
+ # @work-item-id WI-194
2
3
  #
3
4
  # 使い方:
4
5
  # このファイルを .github/workflows/agent-context-refresh.yml にコピーして使用する。
@@ -32,18 +33,21 @@ jobs:
32
33
  uses: actions/setup-node@v4
33
34
  with:
34
35
  node-version: '20'
35
- cache: 'pnpm'
36
-
37
- - name: Install pnpm
38
- uses: pnpm/action-setup@v4
39
- with:
40
- version: 9
41
36
 
42
37
  - name: Install dependencies
43
- run: pnpm install --frozen-lockfile
38
+ run: |
39
+ if [ -f pnpm-lock.yaml ]; then
40
+ pnpm install --frozen-lockfile
41
+ elif [ -f yarn.lock ]; then
42
+ yarn install --immutable || yarn install --frozen-lockfile
43
+ elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
44
+ npm ci
45
+ else
46
+ npm install
47
+ fi
44
48
 
45
49
  - name: Refresh agent context
46
- run: pnpm run harness ci:auto-refresh-agent-context --apply
50
+ run: npx phasegate ci:auto-refresh-agent-context --apply
47
51
 
48
52
  - name: Create pull request
49
53
  uses: peter-evans/create-pull-request@v6
@@ -1,4 +1,5 @@
1
1
  # Phasegate — 週次整合性チェックワークフロー
2
+ # @work-item-id WI-194
2
3
  #
3
4
  # 使い方:
4
5
  # このファイルを .github/workflows/consistency-check.yml にコピーして使用する。
@@ -31,15 +32,18 @@ jobs:
31
32
  uses: actions/setup-node@v4
32
33
  with:
33
34
  node-version: '20'
34
- cache: 'pnpm'
35
-
36
- - name: Install pnpm
37
- uses: pnpm/action-setup@v4
38
- with:
39
- version: 9
40
35
 
41
36
  - name: Install dependencies
42
- run: pnpm install --frozen-lockfile
37
+ run: |
38
+ if [ -f pnpm-lock.yaml ]; then
39
+ pnpm install --frozen-lockfile
40
+ elif [ -f yarn.lock ]; then
41
+ yarn install --immutable || yarn install --frozen-lockfile
42
+ elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
43
+ npm ci
44
+ else
45
+ npm install
46
+ fi
43
47
 
44
48
  # L4-001: Drift Detection(設計⇔コード双方向乖離)
45
49
  - name: L4 Drift Detection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.5",
3
+ "version": "0.160.6",
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",
@@ -4,11 +4,12 @@
4
4
  # Usage:
5
5
  # bash scripts/delegate-sonnet.sh --prompt "プロンプト" --output "出力パス"
6
6
  # bash scripts/delegate-sonnet.sh --prompt-file /tmp/prompt.md --output "出力パス"
7
+ # bash scripts/delegate-sonnet.sh "プロンプト" --output "出力パス"
7
8
  #
8
9
  # Options:
9
10
  # --prompt 委任プロンプト(直接指定、2000文字以下推奨)
10
11
  # --prompt-file 委任プロンプトファイル(長文の場合)
11
- # --output 出力ファイルパス
12
+ # --output 出力ファイルパス(省略時: .phasegate/delegate-sonnet-output.md)
12
13
  # --max-turns 最大ターン数(デフォルト: 30)
13
14
  # --dry-run プロンプトを表示するだけで実行しない
14
15
 
@@ -20,6 +21,7 @@ PROMPT_FILE=""
20
21
  OUTPUT_PATH=""
21
22
  MAX_TURNS=30
22
23
  DRY_RUN=false
24
+ POSITIONAL_ARGS=()
23
25
 
24
26
  while [[ $# -gt 0 ]]; do
25
27
  case $1 in
@@ -43,23 +45,35 @@ while [[ $# -gt 0 ]]; do
43
45
  DRY_RUN=true
44
46
  shift
45
47
  ;;
48
+ --)
49
+ shift
50
+ if [[ $# -gt 0 ]]; then
51
+ POSITIONAL_ARGS+=("$@")
52
+ fi
53
+ break
54
+ ;;
46
55
  *)
47
- echo "Unknown option: $1" >&2
48
- exit 1
56
+ if [[ "$1" == --* ]]; then
57
+ echo "Unknown option: $1" >&2
58
+ exit 1
59
+ fi
60
+ POSITIONAL_ARGS+=("$1")
61
+ shift
49
62
  ;;
50
63
  esac
51
64
  done
52
65
 
66
+ if [[ -z "$PROMPT" && ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
67
+ PROMPT="${POSITIONAL_ARGS[*]}"
68
+ fi
69
+
53
70
  # --- バリデーション ---
54
71
  if [[ -z "$PROMPT" && -z "$PROMPT_FILE" ]]; then
55
72
  echo "Error: --prompt or --prompt-file is required" >&2
56
73
  exit 1
57
74
  fi
58
75
 
59
- if [[ -z "$OUTPUT_PATH" ]]; then
60
- echo "Error: --output is required" >&2
61
- exit 1
62
- fi
76
+ OUTPUT_PATH="${OUTPUT_PATH:-.phasegate/delegate-sonnet-output.md}"
63
77
 
64
78
  if [[ -n "$PROMPT_FILE" && ! -f "$PROMPT_FILE" ]]; then
65
79
  echo "Error: Prompt file not found: $PROMPT_FILE" >&2
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit ci-governance
4
+ * @work-item-id WI-190
4
5
  */
5
6
 
6
7
  import type { AgentContextDocumentPort } from '../../domain/ports/agent-context-document-port.js';
@@ -12,12 +13,11 @@ const CLAUDE_MD_PATH = 'CLAUDE.md';
12
13
  const CLAUDE_MD_TEMPLATE_PATH = 'docs/templates/agent-context/CLAUDE.md.template.md';
13
14
 
14
15
  const PHASEGATE_COMMANDS = [
15
- 'phasegate init --with-ci',
16
- 'phasegate ci:auto-refresh-agent-context --dry-run',
17
- 'phasegate ci:auto-refresh-agent-context --apply',
18
- 'phasegate refresh-claude-md --apply',
19
- 'phasegate p2:check-agent-context',
16
+ 'phasegate doctor',
20
17
  'phasegate phasegate:check-ready',
18
+ 'phasegate validate --layer L2 --format human',
19
+ 'phasegate setup:agent --dry-run',
20
+ 'phasegate config:plan --intent l4-strict --dry-run',
21
21
  ];
22
22
 
23
23
  const PHASE_PRESETS = ['minimal', 'standard', 'full', 'custom'];
@@ -37,7 +37,7 @@ export class RefreshClaudeMdUseCase {
37
37
  ]);
38
38
  const nextContent = this.composer.compose(template, existing, {
39
39
  commands: PHASEGATE_COMMANDS,
40
- skills,
40
+ skills: ['all bundled skills'],
41
41
  presets: PHASE_PRESETS,
42
42
  });
43
43
  const changed = existing !== nextContent;
@@ -11,7 +11,7 @@ export interface ClaudeMdTemplateValues {
11
11
 
12
12
  const USER_SECTION_START = '<!-- phasegate:user-section:start -->';
13
13
  const USER_SECTION_END = '<!-- phasegate:user-section:end -->';
14
- const DEFAULT_USER_SECTION = 'プロジェクト固有の指示をここに記載してください。';
14
+ const DEFAULT_USER_SECTION = 'Project-specific agent instructions go here.';
15
15
 
16
16
  export class ClaudeMdComposer {
17
17
  compose(template: string, existing: string | null, values: ClaudeMdTemplateValues): string {
@@ -2,6 +2,7 @@
2
2
  * @layer infrastructure
3
3
  * @unit ci-governance
4
4
  * @work-item-id WI-182 / WI-183
5
+ * @work-item-id WI-194
5
6
  *
6
7
  * TemplateRendererPort実装(YAML書き出し)
7
8
  */
@@ -29,7 +29,7 @@ import type {
29
29
 
30
30
  export type LayerId = 'L1' | 'L2' | 'L3' | 'L4';
31
31
  export type PresetId = 'minimal' | 'standard' | 'strict';
32
- export type PlanningModeValue = 'interactive' | 'embedded-qa';
32
+ export type PlanningModeValue = 'interactive' | 'embedded-qa' | 'manual';
33
33
  export type { PhaseDependenciesPresetId };
34
34
  type DeepPartial<T> = {
35
35
  [K in keyof T]?: T[K] extends Array<infer TItem>
@@ -3,11 +3,11 @@
3
3
  * @unit config-foundation
4
4
  *
5
5
  * Planning Mode設定を表す値オブジェクト
6
- * defaultMode と perPhase の値は列挙値 "interactive" | "embedded-qa" のみ許容
6
+ * defaultMode と perPhase の値は列挙値 "interactive" | "embedded-qa" | "manual" のみ許容
7
7
  */
8
8
  import { ConfigValidationError } from '../errors/config-validation-error.js';
9
9
 
10
- const VALID_MODES = ['interactive', 'embedded-qa'] as const;
10
+ const VALID_MODES = ['interactive', 'embedded-qa', 'manual'] as const;
11
11
  type PlanningMode = (typeof VALID_MODES)[number];
12
12
 
13
13
  export interface PlanningModeConfigProps {
@@ -384,7 +384,8 @@
384
384
  "type": "string",
385
385
  "enum": [
386
386
  "interactive",
387
- "embedded-qa"
387
+ "embedded-qa",
388
+ "manual"
388
389
  ]
389
390
  },
390
391
  "perPhase": {
@@ -393,7 +394,8 @@
393
394
  "type": "string",
394
395
  "enum": [
395
396
  "interactive",
396
- "embedded-qa"
397
+ "embedded-qa",
398
+ "manual"
397
399
  ]
398
400
  }
399
401
  }
@@ -394,7 +394,8 @@
394
394
  "type": "string",
395
395
  "enum": [
396
396
  "interactive",
397
- "embedded-qa"
397
+ "embedded-qa",
398
+ "manual"
398
399
  ]
399
400
  },
400
401
  "perPhase": {
@@ -403,7 +404,8 @@
403
404
  "type": "string",
404
405
  "enum": [
405
406
  "interactive",
406
- "embedded-qa"
407
+ "embedded-qa",
408
+ "manual"
407
409
  ]
408
410
  }
409
411
  }
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-143
4
4
  // @work-item-id WI-187
5
+ // @work-item-id WI-193
5
6
 
6
7
  import { join, relative, sep } from "node:path";
7
8
  import { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
@@ -61,5 +62,6 @@ function isWorkItemDescription(path: string): boolean {
61
62
  function isAdHocPlan(path: string): boolean {
62
63
  if (!path.startsWith("docs/inception/")) return false;
63
64
  if (/\/WI-\d{3}\//.test(path)) return false;
65
+ if (path.startsWith("docs/inception/_shared/") && path.endsWith(".md")) return true;
64
66
  return path.includes("/codding_plan/") || path.endsWith("_plan.md");
65
67
  }
@@ -8,6 +8,9 @@
8
8
  * @work-item-id WI-176
9
9
  * @work-item-id WI-184
10
10
  * @work-item-id WI-189
11
+ * @work-item-id WI-191
12
+ * @work-item-id WI-195
13
+ * @work-item-id WI-196
11
14
  *
12
15
  * Phasegate CLI エントリポイント。
13
16
  * 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
@@ -171,6 +174,7 @@ Commands:
171
174
  disable-feature <name> Disable a harness feature
172
175
  list-features List available features
173
176
  migrate Migrate phasegate.config.json (--schema v3, --config <path>)
177
+ migrate work-items Migrate legacy inception work item directories (--dry-run|--apply)
174
178
  work-items:status Report or apply derived WI frontmatter status (--dry-run|--apply, --id, --fail-on-stale, --json)
175
179
 
176
180
  render-errors Render harness errors (--format human|agent|ci)
@@ -211,7 +215,7 @@ Gate semantics:
211
215
  refresh-claude-md Refresh CLAUDE.md standard sections (--dry-run, --apply, --json)
212
216
  p2:check-agent-context Check AGENTS.md / CLAUDE.md freshness (--threshold-days <n>, --json)
213
217
  setup:agent Plan agent-driven setup (--intent <minimal|recommended|strict|ci-only|agent-hooks|retrofit>, --agent <claude|codex|both>, --dry-run|--apply, --json)
214
- config:plan Plan safe config changes (--intent <l4-strict|codex-hooks|ci-fail-on-warning|baseline-reset|quick-mode-strict>, --dry-run, --json)
218
+ 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)
215
219
  ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
216
220
  baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
217
221
  scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --dry-run|--apply, --force, --json)
@@ -528,7 +532,7 @@ Options:
528
532
  Produce an agent-readable configuration change plan.
529
533
 
530
534
  Intents:
531
- l4-strict, codex-hooks, ci-fail-on-warning, baseline-reset, quick-mode-strict
535
+ l4-strict, codex-hooks, ci-fail-on-warning, baseline-reset, quick-mode-strict, retrofit-bootstrap, planning-mode-relax
532
536
 
533
537
  Options:
534
538
  --dry-run
@@ -562,10 +566,11 @@ Options:
562
566
  --help, -h Show this help`,
563
567
  migrate: `Usage: phasegate migrate [options]
564
568
 
565
- Migrate phasegate.config.json from older schema versions. Backs up the original to phasegate.config.json.bak.
569
+ Migrate phasegate.config.json from older schema versions, or migrate legacy inception work item directories.
566
570
 
567
571
  Options:
568
572
  --dry-run Preview changes without writing
573
+ --apply Apply migration when supported
569
574
  --help, -h Show this help`,
570
575
  "list-errors": `Usage: phasegate list-errors [options]
571
576
 
@@ -769,7 +774,7 @@ function parseCoverageThreshold(raw: string | undefined): number {
769
774
  type InitPhasePreset = "full" | "standard" | "minimal" | "custom";
770
775
  type AgentTarget = "claude" | "codex" | "both";
771
776
  type SetupIntent = "minimal" | "recommended" | "strict" | "ci-only" | "agent-hooks" | "retrofit";
772
- type ConfigChangeIntent = "l4-strict" | "codex-hooks" | "ci-fail-on-warning" | "baseline-reset" | "quick-mode-strict";
777
+ type ConfigChangeIntent = "l4-strict" | "codex-hooks" | "ci-fail-on-warning" | "baseline-reset" | "quick-mode-strict" | "retrofit-bootstrap" | "planning-mode-relax";
773
778
  type SetupCompletenessStatus = "configured" | "planned" | "manual" | "not-applicable" | "unknown";
774
779
 
775
780
  interface SetupCompletenessEntry {
@@ -847,7 +852,9 @@ function parseConfigChangeIntent(value: string | undefined): ConfigChangeIntent
847
852
  value === "codex-hooks" ||
848
853
  value === "ci-fail-on-warning" ||
849
854
  value === "baseline-reset" ||
850
- value === "quick-mode-strict"
855
+ value === "quick-mode-strict" ||
856
+ value === "retrofit-bootstrap" ||
857
+ value === "planning-mode-relax"
851
858
  ) {
852
859
  return value;
853
860
  }
@@ -1216,6 +1223,14 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
1216
1223
  ],
1217
1224
  "codex-hooks": [],
1218
1225
  "baseline-reset": [],
1226
+ "retrofit-bootstrap": [
1227
+ { pointer: "/planningMode/default", path: ["planningMode", "default"], value: "manual" },
1228
+ { pointer: "/phaseDependencies/override", path: ["phaseDependencies", "override"], value: true },
1229
+ { pointer: "/quickMode/relaxedGates", path: ["quickMode", "relaxedGates"], value: ["phase-gate"] },
1230
+ ],
1231
+ "planning-mode-relax": [
1232
+ { pointer: "/planningMode/default", path: ["planningMode", "default"], value: "manual" },
1233
+ ],
1219
1234
  };
1220
1235
  const changes = configIntents[intent];
1221
1236
  if (changes.length === 0) {
@@ -1303,6 +1318,22 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1303
1318
  validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
1304
1319
  risks: ["More changes will require Full Mode validation before commit."],
1305
1320
  },
1321
+ "retrofit-bootstrap": {
1322
+ targets: ["phasegate.config.json: planningMode.default", "phasegate.config.json: phaseDependencies.override", "phasegate.config.json: quickMode.relaxedGates"],
1323
+ managedTargets: ["phasegate.config.json"],
1324
+ externalActions: [],
1325
+ commands: ["phasegate baseline --dry-run", "phasegate config:plan --intent retrofit-bootstrap --json"],
1326
+ validations: ["phasegate validate-metadata docs/inception/_shared/*.md", "phasegate check-phase-gate --level 2"],
1327
+ risks: ["Manual planning mode accepts existing retrofit planning evidence; review the patch before applying it to avoid weakening greenfield projects."],
1328
+ },
1329
+ "planning-mode-relax": {
1330
+ targets: ["phasegate.config.json: planningMode.default"],
1331
+ managedTargets: ["phasegate.config.json"],
1332
+ externalActions: [],
1333
+ commands: ["phasegate config:plan --intent planning-mode-relax --json"],
1334
+ validations: ["phasegate check-phase-gate --level 2", "phasegate phasegate:check-ready"],
1335
+ risks: ["Manual planning mode reduces PhaseGate's QA enforcement for plan documents until strict planning is restored."],
1336
+ },
1306
1337
  };
1307
1338
  const before = await readProjectJson(rootDir, "phasegate.config.json");
1308
1339
  return {
@@ -2587,7 +2618,7 @@ Examples:
2587
2618
  const mod = createSkillQualityHandlers();
2588
2619
  const storyId = parseFlag(args, "--story") ?? "";
2589
2620
  const dryRun = hasFlag(args, "--dry-run");
2590
- const result = await mod.applyCascadeUpdateHandler.handle({ storyId, dryRun });
2621
+ const result = await mod.applyCascadeUpdateHandler.handle({ storyId, dryRun, format: json ? "json" : "human" });
2591
2622
  console.log(result.message);
2592
2623
  process.exit(result.exitCode);
2593
2624
  break;
@@ -2780,6 +2811,10 @@ Examples:
2780
2811
  }
2781
2812
 
2782
2813
  case "delegate-sonnet": {
2814
+ if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
2815
+ printSubcommandHelp("delegate-sonnet");
2816
+ process.exit(0);
2817
+ }
2783
2818
  const { spawn } = await import("node:child_process");
2784
2819
  const scriptPath = join(harnessRoot, "scripts/delegate-sonnet.sh");
2785
2820
  const forwardArgs = args.slice(1);
@@ -125,7 +125,7 @@ export class PhaseInfoResolver {
125
125
  blockers.push(`QAが未完了です: ${node.nodeKey()}`);
126
126
  }
127
127
  if (!actual.planningModeMatch) {
128
- blockers.push(`Planning Mode要件を満たしていません: ${node.nodeKey()}`);
128
+ blockers.push(`Planning Mode evidence requirement is not satisfied: ${node.nodeKey()}`);
129
129
  }
130
130
  }
131
131
  }
@@ -270,7 +270,7 @@ export class PhaseStructure {
270
270
  }
271
271
 
272
272
  if (!planEvidence.planningModeMatch) {
273
- blockers.push(`Planning Mode要件を満たしていません: ${node.nodeKey()}`);
273
+ blockers.push(`Planning Mode evidence requirement is not satisfied: ${node.nodeKey()}`);
274
274
  }
275
275
  }
276
276
 
@@ -3,7 +3,7 @@
3
3
  * @unit phase-dependency-model
4
4
  */
5
5
 
6
- export type PlanningModeValue = 'interactive' | 'embedded-qa';
6
+ export type PlanningModeValue = 'interactive' | 'embedded-qa' | 'manual';
7
7
 
8
8
  export class InvalidPlanningModeError extends Error {
9
9
  constructor(value: string) {
@@ -21,7 +21,7 @@ export class PlanningMode {
21
21
  }
22
22
 
23
23
  static create(value: string): PlanningMode {
24
- if (value !== 'interactive' && value !== 'embedded-qa') {
24
+ if (value !== 'interactive' && value !== 'embedded-qa' && value !== 'manual') {
25
25
  throw new InvalidPlanningModeError(value);
26
26
  }
27
27
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-192
4
5
  */
5
6
  import { CascadeUpdateResult } from '../../domain/value-objects/cascade-update-result.js';
6
7
  import type { CascadeUpdateService } from '../../domain/services/cascade-update-service.js';
@@ -35,7 +36,7 @@ export class ApplyCascadeUpdateUseCase {
35
36
  const updatedContent = content.includes(target.storyIdTag)
36
37
  ? content
37
38
  : `${content}\n${target.storyIdTag}`;
38
- if (!input.dryRun) {
39
+ if (!input.dryRun && updatedContent !== content) {
39
40
  await this.fileSystemPort.write(filePath, updatedContent);
40
41
  }
41
42
  updatedCount++;
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-192
4
5
  */
5
6
  import type { ApplyCascadeUpdateUseCase } from '../../application/usecases/apply-cascade-update-usecase.js';
6
7
 
7
8
  export interface ApplyCascadeUpdateArgs {
8
9
  storyId: string;
9
10
  dryRun?: boolean;
11
+ format?: 'human' | 'json';
10
12
  }
11
13
 
12
14
  export class ApplyCascadeUpdateHandler {
@@ -15,8 +17,15 @@ export class ApplyCascadeUpdateHandler {
15
17
  async handle(args: ApplyCascadeUpdateArgs): Promise<{ exitCode: number; message: string }> {
16
18
  try {
17
19
  const output = await this.useCase.execute({ storyId: args.storyId, dryRun: args.dryRun });
20
+ if (args.format === 'json') {
21
+ return {
22
+ exitCode: output.errors.length > 0 ? 1 : 0,
23
+ message: JSON.stringify({ dryRun: args.dryRun === true, ...output }, null, 2),
24
+ };
25
+ }
18
26
  const tagsLine = output.appliedStoryIds.join(', ');
19
- let msg = `Updated ${output.updatedCount} files with tags: ${tagsLine}`;
27
+ const verb = args.dryRun ? 'Would update' : 'Updated';
28
+ let msg = `${verb} ${output.updatedCount} files with tags: ${tagsLine}`;
20
29
 
21
30
  if (output.errors.length > 0) {
22
31
  const errLines = output.errors.map((e) => ` - ${e}`).join('\n');