pumuki 6.3.266 → 6.3.268

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
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.3.267] - 2026-05-14
4
+
5
+ - iOS: `skills.ios.no-dispatchsemaphore` now emits actionable AST-style evidence for `DispatchSemaphore` usage, including exact lines, primary/related nodes and remediation toward `TaskGroup`, `AsyncStream` or explicit async boundaries.
6
+
3
7
  ## [6.3.266] - 2026-05-14
4
8
 
5
9
  - iOS: `skills.ios.no-dispatchgroup` now emits actionable AST-style evidence for `DispatchGroup` usage, including exact lines, primary/related nodes and remediation toward `TaskGroup` or other Swift concurrency boundaries.
@@ -16,6 +16,7 @@ import {
16
16
  collectSwiftAnyViewLines,
17
17
  collectSwiftCallbackStyleSignatureLines,
18
18
  collectSwiftDispatchGroupLines,
19
+ collectSwiftDispatchSemaphoreLines,
19
20
  hasSwiftAnyViewUsage,
20
21
  hasSwiftAsyncWithoutAwaitUsage,
21
22
  hasSwiftCallbackStyleSignature,
@@ -510,6 +511,7 @@ OperationQueue()
510
511
  assert.equal(hasSwiftOperationQueueUsage(source), true);
511
512
  assert.deepEqual(collectSwiftDispatchQueueLines(source), [2]);
512
513
  assert.deepEqual(collectSwiftDispatchGroupLines(source), [3]);
514
+ assert.deepEqual(collectSwiftDispatchSemaphoreLines(source), [4]);
513
515
  });
514
516
 
515
517
  test('hasSwiftTaskDetachedUsage detecta Task.detached y evita Task normal', () => {
@@ -485,6 +485,10 @@ export const hasSwiftDispatchSemaphoreUsage = (source: string): boolean => {
485
485
  });
486
486
  };
487
487
 
488
+ export const collectSwiftDispatchSemaphoreLines = (source: string): readonly number[] => {
489
+ return sortedUniqueLines(collectSwiftRegexLines(source, /\bDispatchSemaphore\b/));
490
+ };
491
+
488
492
  export const hasSwiftOperationQueueUsage = (source: string): boolean => {
489
493
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
490
494
  if (current !== 'O') {
@@ -649,7 +649,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
649
649
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath, isApprovedIOSBridgePath], detect: TextIOS.hasSwiftCallbackStyleSignature, locateLines: TextIOS.collectSwiftCallbackStyleSignatureLines, primaryNode: (lines) => ({ kind: 'call', name: 'escaping callback-style API signature', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: async/await API or explicit bridge adapter', lines }], why: 'Callback-style completion APIs outside bridge layers bypass Swift structured concurrency and make cancellation, isolation and error flow implicit.', impact: 'Consumers must reason about escaping lifetime, actor hops and callback ordering manually, which increases race, leak and flaky-test risk in production iOS flows.', expected_fix: 'Expose async/await or AsyncSequence APIs in production boundaries. Keep callbacks only inside approved bridge/adapters that wrap legacy SDKs and document the conversion point explicitly.', ruleId: 'heuristics.ios.callback-style.ast', code: 'HEURISTICS_IOS_CALLBACK_STYLE_AST', message: 'AST heuristic detected callback-style API signature outside bridge layers.' },
650
650
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchQueueUsage, locateLines: TextIOS.collectSwiftDispatchQueueLines, primaryNode: (lines) => ({ kind: 'call', name: 'GCD DispatchQueue call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: structured concurrency Task/actor/MainActor boundary', lines }], why: 'DispatchQueue introduces unstructured GCD scheduling in production Swift code instead of preserving Swift concurrency cancellation, priority and actor isolation semantics.', impact: 'Manual queue hops make ordering, cancellation and main-actor safety harder to reason about, increasing race and flaky UI update risk.', expected_fix: 'Use async/await, Task, TaskGroup, actors, MainActor.run or isolated async APIs. Keep GCD only inside explicitly approved legacy bridge layers with documented ownership.', ruleId: 'heuristics.ios.dispatchqueue.ast', code: 'HEURISTICS_IOS_DISPATCHQUEUE_AST', message: 'AST heuristic detected DispatchQueue usage.' },
651
651
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchGroupUsage, locateLines: TextIOS.collectSwiftDispatchGroupLines, primaryNode: (lines) => ({ kind: 'call', name: 'GCD DispatchGroup call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: structured concurrency TaskGroup or async aggregation boundary', lines }], why: 'DispatchGroup is an unstructured coordination primitive that makes asynchronous control flow and cancellation implicit instead of modeled by Swift concurrency.', impact: 'Group coordination is harder to reason about and can hide waiting or deadlock risks in production code paths that should be expressed through TaskGroup or async aggregation.', expected_fix: 'Use TaskGroup, async let, await aggregation, actors or explicit async APIs. Keep DispatchGroup only inside approved legacy bridge layers with documented ownership and migration scope.', ruleId: 'heuristics.ios.dispatchgroup.ast', code: 'HEURISTICS_IOS_DISPATCHGROUP_AST', message: 'AST heuristic detected DispatchGroup usage.' },
652
- { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchSemaphoreUsage, ruleId: 'heuristics.ios.dispatchsemaphore.ast', code: 'HEURISTICS_IOS_DISPATCHSEMAPHORE_AST', message: 'AST heuristic detected DispatchSemaphore usage.' },
652
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchSemaphoreUsage, locateLines: TextIOS.collectSwiftDispatchSemaphoreLines, primaryNode: (lines) => ({ kind: 'call', name: 'GCD DispatchSemaphore call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: structured concurrency TaskGroup, AsyncStream or explicit async boundary', lines }], why: 'DispatchSemaphore is a blocking synchronization primitive that hides ordering and backpressure behind manual waits instead of Swift concurrency boundaries.', impact: 'Semaphore waits can stall threads, obscure cancellation and create deadlock-prone coordination in production code paths that should remain async.', expected_fix: 'Use TaskGroup, AsyncStream, async/await or explicit async boundaries. Keep DispatchSemaphore only inside approved legacy bridge layers with documented ownership and bounded waiting.', ruleId: 'heuristics.ios.dispatchsemaphore.ast', code: 'HEURISTICS_IOS_DISPATCHSEMAPHORE_AST', message: 'AST heuristic detected DispatchSemaphore usage.' },
653
653
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOperationQueueUsage, ruleId: 'heuristics.ios.operation-queue.ast', code: 'HEURISTICS_IOS_OPERATION_QUEUE_AST', message: 'AST heuristic detected OperationQueue usage.' },
654
654
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTaskDetachedUsage, ruleId: 'heuristics.ios.task-detached.ast', code: 'HEURISTICS_IOS_TASK_DETACHED_AST', message: 'AST heuristic detected Task.detached usage.' },
655
655
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAsyncWithoutAwaitUsage, ruleId: 'heuristics.ios.concurrency.async-without-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST', message: 'AST heuristic detected a private async function without await; remove async unless a protocol/override boundary requires it.' },
@@ -25,12 +25,14 @@ test('evaluateGate devuelve WARN cuando hay warnings sin bloqueantes', () => {
25
25
  severity: 'WARN',
26
26
  code: 'RULE_WARN',
27
27
  message: 'Warn finding',
28
+ blocking: false,
28
29
  },
29
30
  {
30
31
  ruleId: 'rule.info',
31
32
  severity: 'INFO',
32
33
  code: 'RULE_INFO',
33
34
  message: 'Info finding',
35
+ blocking: false,
34
36
  },
35
37
  ];
36
38
 
@@ -42,7 +44,7 @@ test('evaluateGate devuelve WARN cuando hay warnings sin bloqueantes', () => {
42
44
  assert.equal(result.warnings[0]?.ruleId, 'rule.warn');
43
45
  });
44
46
 
45
- test('evaluateGate devuelve BLOCK cuando existe al menos un finding bloqueante', () => {
47
+ test('evaluateGate devuelve BLOCK para cualquier finding runtime salvo blocking=false', () => {
46
48
  const findings: Finding[] = [
47
49
  {
48
50
  ruleId: 'rule.error',
@@ -62,18 +64,24 @@ test('evaluateGate devuelve BLOCK cuando existe al menos un finding bloqueante',
62
64
  code: 'RULE_WARN',
63
65
  message: 'Warn finding',
64
66
  },
67
+ {
68
+ ruleId: 'rule.info.advisory',
69
+ severity: 'INFO',
70
+ code: 'RULE_INFO_ADVISORY',
71
+ message: 'Info advisory finding',
72
+ blocking: false,
73
+ },
65
74
  ];
66
75
 
67
76
  const result = evaluateGate(findings, defaultPolicy);
68
77
 
69
78
  assert.equal(result.outcome, 'BLOCK');
70
- assert.equal(result.blocking.length, 2);
79
+ assert.equal(result.blocking.length, 3);
71
80
  assert.deepEqual(
72
81
  result.blocking.map((finding) => finding.ruleId),
73
- ['rule.error', 'rule.critical']
82
+ ['rule.error', 'rule.critical', 'rule.warn']
74
83
  );
75
- assert.equal(result.warnings.length, 1);
76
- assert.equal(result.warnings[0]?.ruleId, 'rule.warn');
84
+ assert.deepEqual(result.warnings, []);
77
85
  });
78
86
 
79
87
  test('evaluateGate bloquea cualquier severidad cuando la policy zero-violations usa INFO', () => {
@@ -3,16 +3,16 @@ import type { GateOutcome } from './GateOutcome';
3
3
  import type { GatePolicy } from './GatePolicy';
4
4
  import { isSeverityAtLeast } from '../rules/Severity';
5
5
 
6
+ const isBlockingFinding = (finding: Finding): boolean => finding.blocking !== false;
7
+
6
8
  export function evaluateGate(
7
9
  findings: Finding[],
8
10
  policy: GatePolicy
9
11
  ): { outcome: GateOutcome; blocking: Finding[]; warnings: Finding[] } {
10
- const blocking = findings.filter((finding) =>
11
- isSeverityAtLeast(finding.severity, policy.blockOnOrAbove)
12
- );
12
+ const blocking = findings.filter(isBlockingFinding);
13
13
  const warnings = findings.filter(
14
14
  (finding) =>
15
- !isSeverityAtLeast(finding.severity, policy.blockOnOrAbove) &&
15
+ !isBlockingFinding(finding) &&
16
16
  isSeverityAtLeast(finding.severity, policy.warnOnOrAbove)
17
17
  );
18
18
 
@@ -6,6 +6,10 @@ This file keeps only the operational highlights and rollout notes that matter wh
6
6
 
7
7
  ## 2026-04 (CLI stability and macOS notifications)
8
8
 
9
+ ### 2026-05-14 (v6.3.267)
10
+
11
+ - Published `pumuki@6.3.267` with AST-style line/node evidence for `skills.ios.no-dispatchsemaphore`, making `DispatchSemaphore` usage remediable through `TaskGroup`, `AsyncStream` or explicit async boundaries.
12
+
9
13
  ### 2026-05-14 (v6.3.266)
10
14
 
11
15
  - Published `pumuki@6.3.266` with AST-style line/node evidence for `skills.ios.no-dispatchgroup`, making GCD `DispatchGroup` usage remediable through `TaskGroup` or structured async aggregation boundaries.
@@ -188,6 +188,7 @@ const toDualValidationFinding = (params: {
188
188
  return {
189
189
  ruleId: 'governance.ast-intelligence.dual-validation.shadow',
190
190
  severity: 'INFO',
191
+ blocking: false,
191
192
  code: 'AST_INTELLIGENCE_DUAL_VALIDATION_SHADOW',
192
193
  message:
193
194
  `AST Intelligence dual validation shadow at ${params.stage}: ` +
@@ -39,6 +39,10 @@ import {
39
39
  filterFactsByPathPrefixes,
40
40
  resolveGateScopePathPrefixesFromEnv,
41
41
  } from './filterFactsByPathPrefixes';
42
+ import {
43
+ readPreWriteLeaseStatus,
44
+ toPreWriteEnforcementGapFinding,
45
+ } from '../lifecycle/preWriteLease';
42
46
 
43
47
  export type OperationalMemoryShadowRecommendation = {
44
48
  recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK';
@@ -801,7 +805,7 @@ const shouldBlockFromFinding = (finding: Finding | undefined): boolean => {
801
805
  if (!finding) {
802
806
  return false;
803
807
  }
804
- return finding.severity === 'ERROR' || finding.severity === 'CRITICAL';
808
+ return finding.blocking !== false;
805
809
  };
806
810
 
807
811
  const applySkillsFindingEnforcement = (
@@ -817,6 +821,7 @@ const applySkillsFindingEnforcement = (
817
821
  return {
818
822
  ...finding,
819
823
  severity: 'WARN',
824
+ blocking: false,
820
825
  };
821
826
  };
822
827
 
@@ -834,6 +839,7 @@ const toSoftPreCommitSkillsFinding = (params: {
834
839
  return {
835
840
  ...params.finding,
836
841
  severity: 'WARN',
842
+ blocking: false,
837
843
  code: `${params.finding.code}_SOFT_PRECOMMIT`,
838
844
  message:
839
845
  `${params.finding.message} ` +
@@ -939,6 +945,18 @@ export async function runPlatformGate(params: {
939
945
  : facts;
940
946
  const filesScanned = countScannedFilesFromFacts(factsForPlatformEvaluation);
941
947
  const observedCodePaths = collectObservedCodePathsFromFacts(facts);
948
+ const preWriteLeaseFinding =
949
+ params.policy.stage === 'PRE_COMMIT' ||
950
+ params.policy.stage === 'PRE_PUSH' ||
951
+ params.policy.stage === 'CI'
952
+ ? toPreWriteEnforcementGapFinding({
953
+ stage: params.policy.stage,
954
+ status: readPreWriteLeaseStatus({
955
+ repoRoot,
956
+ git,
957
+ }),
958
+ })
959
+ : undefined;
942
960
 
943
961
  const platformEvaluation = dependencies.evaluatePlatformGateFindings({
944
962
  facts: factsForPlatformEvaluation,
@@ -1184,12 +1202,12 @@ export async function runPlatformGate(params: {
1184
1202
  ? tddBddEvaluation.snapshot
1185
1203
  : undefined;
1186
1204
  const hasTddBddBlockingFinding = tddBddEvaluation.findings.some(
1187
- (finding) => finding.severity === 'ERROR' || finding.severity === 'CRITICAL'
1205
+ (finding) => shouldBlockFromFinding(finding)
1188
1206
  );
1189
1207
  const hasNativeBlockingFinding = findings.some(
1190
- (finding) => finding.severity === 'ERROR' || finding.severity === 'CRITICAL'
1208
+ (finding) => shouldBlockFromFinding(finding)
1191
1209
  );
1192
- const preCommitSoftSkillsEnabled = process.env.PUMUKI_PRE_COMMIT_SOFT_SKILLS !== '0';
1210
+ const preCommitSoftSkillsEnabled = process.env.PUMUKI_PRE_COMMIT_SOFT_SKILLS === '1';
1193
1211
  const lowRiskPreCommitWindow = observedCodePaths.length > 0 && observedCodePaths.length <= 3;
1194
1212
  const shouldSoftEnforceSkillsFindings =
1195
1213
  params.policy.stage === 'PRE_COMMIT'
@@ -1228,6 +1246,7 @@ export async function runPlatformGate(params: {
1228
1246
  ? [
1229
1247
  sddBlockingFinding,
1230
1248
  ...(degradedModeFinding ? [degradedModeFinding] : []),
1249
+ ...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []),
1231
1250
  ...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
1232
1251
  ...(effectiveUnsupportedSkillsMappingFinding ? [effectiveUnsupportedSkillsMappingFinding] : []),
1233
1252
  ...(effectivePlatformSkillsCoverageFinding ? [effectivePlatformSkillsCoverageFinding] : []),
@@ -1245,6 +1264,7 @@ export async function runPlatformGate(params: {
1245
1264
  || effectivePlatformSkillsCoverageFinding
1246
1265
  || effectiveCrossPlatformCriticalFinding
1247
1266
  || effectiveSkillsScopeComplianceFinding
1267
+ || preWriteLeaseFinding
1248
1268
  || activeRulesEmptyForCodeChangesFinding
1249
1269
  || effectiveIosTestsQualityFinding
1250
1270
  || astIntelligenceDualFinding
@@ -1255,6 +1275,7 @@ export async function runPlatformGate(params: {
1255
1275
  || tddBddEvaluation.findings.length > 0
1256
1276
  ? [
1257
1277
  ...(degradedModeFinding ? [degradedModeFinding] : []),
1278
+ ...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []),
1258
1279
  ...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
1259
1280
  ...(effectiveUnsupportedSkillsMappingFinding ? [effectiveUnsupportedSkillsMappingFinding] : []),
1260
1281
  ...(effectivePlatformSkillsCoverageFinding ? [effectivePlatformSkillsCoverageFinding] : []),
@@ -1284,6 +1305,7 @@ export async function runPlatformGate(params: {
1284
1305
  const baseGateOutcome =
1285
1306
  sddBlockingFinding ||
1286
1307
  degradedModeBlocks ||
1308
+ shouldBlockFromFinding(preWriteLeaseFinding) ||
1287
1309
  shouldBlockFromFinding(policyAsCodeBlockingFinding) ||
1288
1310
  shouldBlockFromFinding(effectiveUnsupportedSkillsMappingFinding) ||
1289
1311
  shouldBlockFromFinding(effectivePlatformSkillsCoverageFinding) ||
@@ -818,7 +818,7 @@ export async function runPreCommitStage(
818
818
  repoRoot,
819
819
  stage: 'PRE_COMMIT',
820
820
  scope: {
821
- kind: 'staged',
821
+ kind: 'workingTree',
822
822
  },
823
823
  });
824
824
  if (
@@ -898,9 +898,7 @@ export async function runPrePushStage(
898
898
  repoRoot,
899
899
  stage: 'PRE_PUSH',
900
900
  scope: {
901
- kind: 'range',
902
- fromRef: bootstrapBaseRef,
903
- toRef: 'HEAD',
901
+ kind: 'repoAndStaged',
904
902
  },
905
903
  });
906
904
  if (
@@ -1006,9 +1004,7 @@ export async function runPrePushStage(
1006
1004
  repoRoot,
1007
1005
  stage: 'PRE_PUSH',
1008
1006
  scope: {
1009
- kind: 'range',
1010
- fromRef: prePushFromRef,
1011
- toRef: prePushToRef,
1007
+ kind: 'repoAndStaged',
1012
1008
  },
1013
1009
  sddDecisionOverride: historicalPrePushSddOverride,
1014
1010
  });
@@ -1064,9 +1060,7 @@ export async function runCiStage(
1064
1060
  policy: resolved.policy,
1065
1061
  policyTrace: resolved.trace,
1066
1062
  scope: {
1067
- kind: 'range',
1068
- fromRef: ciBaseRef,
1069
- toRef: 'HEAD',
1063
+ kind: 'repo',
1070
1064
  },
1071
1065
  });
1072
1066
  if (exitCode !== 0) {
@@ -2849,8 +2849,18 @@ export const runLifecycleCli = async (
2849
2849
  }
2850
2850
  };
2851
2851
 
2852
- if (require.main === module) {
2853
- void runLifecycleCli(process.argv.slice(2)).then((code) => {
2854
- process.exitCode = code;
2855
- });
2852
+ if (
2853
+ require.main === module &&
2854
+ process.env.PUMUKI_RUNTIME_EXECUTION_SOURCE === 'source-bin'
2855
+ ) {
2856
+ void runLifecycleCli(process.argv.slice(2)).then(
2857
+ (code) => {
2858
+ process.exit(code);
2859
+ },
2860
+ (error) => {
2861
+ const message = error instanceof Error ? error.message : 'Unexpected lifecycle CLI error.';
2862
+ writeError(message);
2863
+ process.exit(1);
2864
+ }
2865
+ );
2856
2866
  }
@@ -20,6 +20,7 @@ import {
20
20
  readLifecycleDependencyInventory,
21
21
  type LifecycleDependencyInventory,
22
22
  } from './dependencyInventory';
23
+ import { readPreWriteLeaseStatus } from './preWriteLease';
23
24
 
24
25
  export type DoctorIssueSeverity = 'warning' | 'error';
25
26
 
@@ -117,6 +118,19 @@ const buildDoctorIssues = (params: {
117
118
  }): ReadonlyArray<DoctorIssue> => {
118
119
  const issues: DoctorIssue[] = [];
119
120
  const evidenceResult = readEvidenceResult(params.repoRoot);
121
+ const preWriteLeaseStatus = readPreWriteLeaseStatus({
122
+ repoRoot: params.repoRoot,
123
+ git: new LifecycleGitService(),
124
+ });
125
+
126
+ if (!preWriteLeaseStatus.valid && preWriteLeaseStatus.changedCodePaths.length > 0) {
127
+ issues.push({
128
+ severity: 'error',
129
+ message:
130
+ `ENFORCEMENT_GAP: PRE_WRITE hard-stop lease is not valid (${preWriteLeaseStatus.code}) ` +
131
+ `for ${preWriteLeaseStatus.changedCodePaths.length} changed code file(s).`,
132
+ });
133
+ }
120
134
 
121
135
  if (params.trackedNodeModulesPaths.length > 0) {
122
136
  issues.push({
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { installPumukiHooks } from './hookManager';
4
4
  import { LifecycleGitService, type ILifecycleGitService } from './gitService';
5
- import { doctorHasBlockingIssues, runLifecycleDoctor } from './doctor';
5
+ import { runLifecycleDoctor } from './doctor';
6
6
  import { runOpenSpecBootstrap, type OpenSpecBootstrapResult } from './openSpecBootstrap';
7
7
  import { LifecycleNpmService, type ILifecycleNpmService } from './npmService';
8
8
  import { getCurrentPumukiVersion } from './packageInfo';
@@ -99,6 +99,9 @@ const materializeStrictPolicyAsCode = (repoRoot: string): void => {
99
99
  }
100
100
  };
101
101
 
102
+ const isPreWriteEnforcementGapIssue = (message: string): boolean =>
103
+ message.startsWith('ENFORCEMENT_GAP: PRE_WRITE hard-stop lease is not valid');
104
+
102
105
  export const runLifecycleInstall = (params?: {
103
106
  cwd?: string;
104
107
  git?: ILifecycleGitService;
@@ -114,8 +117,13 @@ export const runLifecycleInstall = (params?: {
114
117
  cwd: params?.cwd,
115
118
  git,
116
119
  });
120
+ const installBlockingIssues = report.issues.filter(
121
+ (issue) => issue.severity === 'error' && !isPreWriteEnforcementGapIssue(issue.message)
122
+ );
123
+ const hasInstallBlockingIssues =
124
+ installBlockingIssues.length > 0 || report.deep?.blocking === true;
117
125
 
118
- if (doctorHasBlockingIssues(report)) {
126
+ if (hasInstallBlockingIssues) {
119
127
  if (bestEffortAfterDoctorBlock) {
120
128
  const version = getCurrentPumukiVersion();
121
129
  const priorArtifacts = readOpenSpecManagedArtifacts(git, report.repoRoot);
@@ -135,8 +143,8 @@ export const runLifecycleInstall = (params?: {
135
143
  degradedDoctorBypass: true,
136
144
  };
137
145
  }
138
- const renderedIssues = report.issues.map((issue) => `- [${issue.severity}] ${issue.message}`).join('\n');
139
- const firstIssue = report.issues[0];
146
+ const renderedIssues = installBlockingIssues.map((issue) => `- [${issue.severity}] ${issue.message}`).join('\n');
147
+ const firstIssue = installBlockingIssues[0];
140
148
  const notificationResult = (params?.notifyGateBlocked ?? emitGateBlockedNotification)({
141
149
  repoRoot: report.repoRoot,
142
150
  stage: 'PRE_COMMIT',
@@ -1,8 +1,11 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { evaluateAiGate } from '../gate/evaluateAiGate';
3
+ import { GitService } from '../git/GitService';
2
4
  import { runPlatformGate } from '../git/runPlatformGate';
3
5
  import { runEnterpriseAiGateCheck } from '../mcp/aiGateCheck';
4
6
  import { writeMcpAiGateReceipt } from '../mcp/aiGateReceipt';
5
7
  import { evaluateSddPolicy } from '../sdd';
8
+ import { writePreWriteLease } from './preWriteLease';
6
9
 
7
10
  const PRE_WRITE_AUTOMATION_RETRY_BACKOFF_MS = 200;
8
11
 
@@ -33,7 +36,7 @@ const PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES = new Set<string>([
33
36
  ]);
34
37
 
35
38
  export type PreWriteAutomationAction = {
36
- action: 'refresh_evidence' | 'refresh_mcp_receipt' | 'retry_backoff';
39
+ action: 'refresh_evidence' | 'refresh_mcp_receipt' | 'retry_backoff' | 'write_prewrite_lease';
37
40
  status: 'OK' | 'FAILED';
38
41
  details: string;
39
42
  };
@@ -47,6 +50,7 @@ type PreWriteAutomationDependencies = {
47
50
  evaluateAiGate: typeof evaluateAiGate;
48
51
  runEnterpriseAiGateCheck: typeof runEnterpriseAiGateCheck;
49
52
  writeMcpAiGateReceipt: typeof writeMcpAiGateReceipt;
53
+ writePreWriteLease: typeof writePreWriteLease;
50
54
  sleep: (ms: number) => Promise<void>;
51
55
  retryBackoffMs: number;
52
56
  };
@@ -55,6 +59,7 @@ const defaultDependencies: PreWriteAutomationDependencies = {
55
59
  evaluateAiGate,
56
60
  runEnterpriseAiGateCheck,
57
61
  writeMcpAiGateReceipt,
62
+ writePreWriteLease,
58
63
  sleep: (ms: number) =>
59
64
  new Promise((resolve) => {
60
65
  setTimeout(resolve, ms);
@@ -98,7 +103,7 @@ export const buildPreWriteAutomationTrace = async (params: {
98
103
  attempted: false,
99
104
  actions: [],
100
105
  };
101
- if (params.sdd.stage !== 'PRE_WRITE' || params.aiGate.allowed) {
106
+ if (params.sdd.stage !== 'PRE_WRITE') {
102
107
  return {
103
108
  aiGate: params.aiGate,
104
109
  trace,
@@ -106,14 +111,17 @@ export const buildPreWriteAutomationTrace = async (params: {
106
111
  }
107
112
 
108
113
  let aiGate = params.aiGate;
109
- if (hasAutoFixableEvidenceViolation(aiGate)) {
114
+ if (
115
+ process.env.PUMUKI_PRE_WRITE_REFRESH_GATE !== '0' ||
116
+ hasAutoFixableEvidenceViolation(aiGate)
117
+ ) {
110
118
  trace.attempted = true;
111
119
  try {
112
120
  const gateExitCode = await params.runPlatformGate({
113
121
  policy: {
114
122
  stage: 'PRE_COMMIT',
115
- blockOnOrAbove: 'ERROR',
116
- warnOnOrAbove: 'WARN',
123
+ blockOnOrAbove: 'INFO',
124
+ warnOnOrAbove: 'INFO',
117
125
  },
118
126
  scope: {
119
127
  kind: 'workingTree',
@@ -235,6 +243,28 @@ export const buildPreWriteAutomationTrace = async (params: {
235
243
  }
236
244
  }
237
245
 
246
+ if (params.sdd.decision.allowed && aiGate.allowed && existsSync(params.repoRoot)) {
247
+ trace.attempted = true;
248
+ try {
249
+ const git = new GitService();
250
+ const leaseResult = activeDependencies.writePreWriteLease({
251
+ repoRoot: params.repoRoot,
252
+ git,
253
+ });
254
+ trace.actions.push({
255
+ action: 'write_prewrite_lease',
256
+ status: leaseResult.valid ? 'OK' : 'FAILED',
257
+ details: `${leaseResult.code} path=${leaseResult.path}`,
258
+ });
259
+ } catch (error) {
260
+ trace.actions.push({
261
+ action: 'write_prewrite_lease',
262
+ status: 'FAILED',
263
+ details: error instanceof Error ? error.message : 'Unknown PRE_WRITE lease write error',
264
+ });
265
+ }
266
+ }
267
+
238
268
  return {
239
269
  aiGate,
240
270
  trace,
@@ -0,0 +1,271 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import type { Finding } from '../../core/gate/Finding';
4
+ import type { IGitService } from '../git/GitService';
5
+ import { hasAllowedExtension } from '../git/gitDiffUtils';
6
+ import { DEFAULT_FACT_FILE_EXTENSIONS } from '../git/runPlatformGateFacts';
7
+
8
+ export type PreWriteLease = {
9
+ version: '1';
10
+ kind: 'pumuki-pre-write-lease';
11
+ repo_root: string;
12
+ head: string;
13
+ branch: string | null;
14
+ issued_at: string;
15
+ expires_at: string;
16
+ pre_change_code_changes_count: number;
17
+ pre_change_code_paths: string[];
18
+ };
19
+
20
+ export type PreWriteLeaseStatus =
21
+ | {
22
+ valid: true;
23
+ code: 'PRE_WRITE_LEASE_VALID';
24
+ path: string;
25
+ lease: PreWriteLease;
26
+ changedCodePaths: string[];
27
+ }
28
+ | {
29
+ valid: false;
30
+ code:
31
+ | 'PRE_WRITE_LEASE_MISSING'
32
+ | 'PRE_WRITE_LEASE_INVALID'
33
+ | 'PRE_WRITE_LEASE_EXPIRED'
34
+ | 'PRE_WRITE_LEASE_HEAD_MISMATCH'
35
+ | 'PRE_WRITE_LEASE_DIRTY_AT_ISSUE';
36
+ path: string;
37
+ message: string;
38
+ changedCodePaths: string[];
39
+ lease?: PreWriteLease;
40
+ };
41
+
42
+ export type PreWriteLeaseWriteResult = {
43
+ path: string;
44
+ written: boolean;
45
+ valid: boolean;
46
+ code: 'PRE_WRITE_LEASE_WRITTEN' | 'PRE_WRITE_LEASE_NOT_WRITTEN_DIRTY_CODE';
47
+ message: string;
48
+ lease?: PreWriteLease;
49
+ changedCodePaths: string[];
50
+ };
51
+
52
+ const PRE_WRITE_LEASE_TTL_MS = 4 * 60 * 60 * 1000;
53
+
54
+ export const resolvePreWriteLeasePath = (repoRoot: string): string =>
55
+ join(repoRoot, '.pumuki', 'prewrite-lease.json');
56
+
57
+ const runGitOrEmpty = (
58
+ git: Pick<IGitService, 'runGit'>,
59
+ repoRoot: string,
60
+ args: ReadonlyArray<string>
61
+ ): string => {
62
+ try {
63
+ return git.runGit([...args], repoRoot);
64
+ } catch {
65
+ return '';
66
+ }
67
+ };
68
+
69
+ const collectCodePathsFromOutput = (output: string): string[] =>
70
+ output
71
+ .split('\n')
72
+ .map((line) => line.trim())
73
+ .filter((line) => line.length > 0)
74
+ .filter((line) => hasAllowedExtension(line, DEFAULT_FACT_FILE_EXTENSIONS));
75
+
76
+ export const collectPreWriteCodeChangePaths = (params: {
77
+ repoRoot: string;
78
+ git: Pick<IGitService, 'runGit'>;
79
+ }): string[] => {
80
+ const paths = new Set<string>();
81
+ for (const output of [
82
+ runGitOrEmpty(params.git, params.repoRoot, ['diff', '--name-only']),
83
+ runGitOrEmpty(params.git, params.repoRoot, ['diff', '--cached', '--name-only']),
84
+ runGitOrEmpty(params.git, params.repoRoot, ['ls-files', '--others', '--exclude-standard']),
85
+ ]) {
86
+ for (const path of collectCodePathsFromOutput(output)) {
87
+ paths.add(path);
88
+ }
89
+ }
90
+ return [...paths].sort((left, right) => left.localeCompare(right));
91
+ };
92
+
93
+ const resolveHead = (params: {
94
+ repoRoot: string;
95
+ git: Pick<IGitService, 'runGit'>;
96
+ }): string => runGitOrEmpty(params.git, params.repoRoot, ['rev-parse', 'HEAD']).trim();
97
+
98
+ const resolveBranch = (params: {
99
+ repoRoot: string;
100
+ git: Pick<IGitService, 'runGit'>;
101
+ }): string | null => {
102
+ const branch = runGitOrEmpty(params.git, params.repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
103
+ return branch.length === 0 || branch === 'HEAD' ? null : branch;
104
+ };
105
+
106
+ const parseLease = (raw: string): PreWriteLease | undefined => {
107
+ try {
108
+ const value = JSON.parse(raw) as Partial<PreWriteLease>;
109
+ if (
110
+ value.version === '1' &&
111
+ value.kind === 'pumuki-pre-write-lease' &&
112
+ typeof value.repo_root === 'string' &&
113
+ typeof value.head === 'string' &&
114
+ typeof value.issued_at === 'string' &&
115
+ typeof value.expires_at === 'string' &&
116
+ typeof value.pre_change_code_changes_count === 'number' &&
117
+ Array.isArray(value.pre_change_code_paths)
118
+ ) {
119
+ return {
120
+ version: '1',
121
+ kind: 'pumuki-pre-write-lease',
122
+ repo_root: value.repo_root,
123
+ head: value.head,
124
+ branch: typeof value.branch === 'string' ? value.branch : null,
125
+ issued_at: value.issued_at,
126
+ expires_at: value.expires_at,
127
+ pre_change_code_changes_count: value.pre_change_code_changes_count,
128
+ pre_change_code_paths: value.pre_change_code_paths.filter(
129
+ (item): item is string => typeof item === 'string'
130
+ ),
131
+ };
132
+ }
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ return undefined;
137
+ };
138
+
139
+ export const readPreWriteLeaseStatus = (params: {
140
+ repoRoot: string;
141
+ git: Pick<IGitService, 'runGit'>;
142
+ now?: Date;
143
+ }): PreWriteLeaseStatus => {
144
+ const path = resolvePreWriteLeasePath(params.repoRoot);
145
+ const changedCodePaths = collectPreWriteCodeChangePaths(params);
146
+ if (!existsSync(path)) {
147
+ return {
148
+ valid: false,
149
+ code: 'PRE_WRITE_LEASE_MISSING',
150
+ path,
151
+ changedCodePaths,
152
+ message: 'No valid PRE_WRITE lease exists for this code diff.',
153
+ };
154
+ }
155
+
156
+ const lease = parseLease(readFileSync(path, 'utf8'));
157
+ if (!lease) {
158
+ return {
159
+ valid: false,
160
+ code: 'PRE_WRITE_LEASE_INVALID',
161
+ path,
162
+ changedCodePaths,
163
+ message: 'PRE_WRITE lease is invalid or unreadable.',
164
+ };
165
+ }
166
+
167
+ if (lease.head !== resolveHead(params)) {
168
+ return {
169
+ valid: false,
170
+ code: 'PRE_WRITE_LEASE_HEAD_MISMATCH',
171
+ path,
172
+ lease,
173
+ changedCodePaths,
174
+ message: 'PRE_WRITE lease was issued for a different HEAD.',
175
+ };
176
+ }
177
+
178
+ if (Date.parse(lease.expires_at) <= (params.now ?? new Date()).getTime()) {
179
+ return {
180
+ valid: false,
181
+ code: 'PRE_WRITE_LEASE_EXPIRED',
182
+ path,
183
+ lease,
184
+ changedCodePaths,
185
+ message: 'PRE_WRITE lease has expired.',
186
+ };
187
+ }
188
+
189
+ if (lease.pre_change_code_changes_count !== 0 || lease.pre_change_code_paths.length !== 0) {
190
+ return {
191
+ valid: false,
192
+ code: 'PRE_WRITE_LEASE_DIRTY_AT_ISSUE',
193
+ path,
194
+ lease,
195
+ changedCodePaths,
196
+ message: 'PRE_WRITE lease was issued after code changes already existed.',
197
+ };
198
+ }
199
+
200
+ return {
201
+ valid: true,
202
+ code: 'PRE_WRITE_LEASE_VALID',
203
+ path,
204
+ lease,
205
+ changedCodePaths,
206
+ };
207
+ };
208
+
209
+ export const writePreWriteLease = (params: {
210
+ repoRoot: string;
211
+ git: Pick<IGitService, 'runGit'>;
212
+ now?: Date;
213
+ }): PreWriteLeaseWriteResult => {
214
+ const path = resolvePreWriteLeasePath(params.repoRoot);
215
+ const changedCodePaths = collectPreWriteCodeChangePaths(params);
216
+ if (changedCodePaths.length > 0) {
217
+ return {
218
+ path,
219
+ written: false,
220
+ valid: false,
221
+ code: 'PRE_WRITE_LEASE_NOT_WRITTEN_DIRTY_CODE',
222
+ changedCodePaths,
223
+ message:
224
+ `PRE_WRITE lease not written because code changes already exist: ${changedCodePaths.join(', ')}`,
225
+ };
226
+ }
227
+
228
+ const issuedAt = params.now ?? new Date();
229
+ const lease: PreWriteLease = {
230
+ version: '1',
231
+ kind: 'pumuki-pre-write-lease',
232
+ repo_root: params.repoRoot,
233
+ head: resolveHead(params),
234
+ branch: resolveBranch(params),
235
+ issued_at: issuedAt.toISOString(),
236
+ expires_at: new Date(issuedAt.getTime() + PRE_WRITE_LEASE_TTL_MS).toISOString(),
237
+ pre_change_code_changes_count: 0,
238
+ pre_change_code_paths: [],
239
+ };
240
+ mkdirSync(dirname(path), { recursive: true });
241
+ writeFileSync(path, `${JSON.stringify(lease, null, 2)}\n`, 'utf8');
242
+ return {
243
+ path,
244
+ written: true,
245
+ valid: true,
246
+ code: 'PRE_WRITE_LEASE_WRITTEN',
247
+ changedCodePaths,
248
+ message: 'PRE_WRITE lease written for clean code state.',
249
+ lease,
250
+ };
251
+ };
252
+
253
+ export const toPreWriteEnforcementGapFinding = (params: {
254
+ stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
255
+ status: PreWriteLeaseStatus;
256
+ }): Finding | undefined => {
257
+ if (params.status.valid || params.status.changedCodePaths.length === 0) {
258
+ return undefined;
259
+ }
260
+ return {
261
+ ruleId: 'governance.prewrite.enforcement-gap',
262
+ severity: 'ERROR',
263
+ code: 'ENFORCEMENT_GAP_PRE_WRITE_LEASE_MISSING',
264
+ message:
265
+ `PRE_WRITE hard-stop lease is not valid at ${params.stage}: ${params.status.code}. ` +
266
+ 'Run PRE_WRITE before editing code; hooks/audit cannot accept code diffs without a prior clean PRE_WRITE lease.',
267
+ filePath: '.pumuki/prewrite-lease.json',
268
+ matchedBy: 'PreWriteLeaseGuard',
269
+ source: 'prewrite-lease',
270
+ };
271
+ };
@@ -17,6 +17,7 @@ import {
17
17
  readLifecycleDependencyInventory,
18
18
  type LifecycleDependencyInventory,
19
19
  } from './dependencyInventory';
20
+ import { readPreWriteLeaseStatus } from './preWriteLease';
20
21
 
21
22
  export type LifecycleStatus = {
22
23
  repoRoot: string;
@@ -35,8 +36,23 @@ export type LifecycleStatus = {
35
36
 
36
37
  const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
37
38
  const evidenceResult = readEvidenceResult(repoRoot);
39
+ const preWriteLeaseStatus = readPreWriteLeaseStatus({
40
+ repoRoot,
41
+ git: new LifecycleGitService(),
42
+ });
43
+ const leaseIssues: DoctorIssue[] =
44
+ !preWriteLeaseStatus.valid && preWriteLeaseStatus.changedCodePaths.length > 0
45
+ ? [
46
+ {
47
+ severity: 'error',
48
+ message:
49
+ `ENFORCEMENT_GAP: PRE_WRITE hard-stop lease is not valid (${preWriteLeaseStatus.code}) ` +
50
+ `for ${preWriteLeaseStatus.changedCodePaths.length} changed code file(s).`,
51
+ },
52
+ ]
53
+ : [];
38
54
  if (evidenceResult.kind !== 'valid') {
39
- return [];
55
+ return leaseIssues;
40
56
  }
41
57
 
42
58
  const evidence = evidenceResult.evidence;
@@ -47,11 +63,12 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
47
63
 
48
64
  if (!blocked) {
49
65
  if (evidence.snapshot.outcome !== 'WARN') {
50
- return [];
66
+ return leaseIssues;
51
67
  }
52
68
 
53
69
  const warnStage = evidence?.snapshot?.stage ?? 'PRE_WRITE';
54
70
  return [
71
+ ...leaseIssues,
55
72
  {
56
73
  severity: 'warning',
57
74
  message: appendTrackingActionableContext({
@@ -68,6 +85,7 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
68
85
  message: `Governance is blocked (${blockedStage}).`,
69
86
  });
70
87
  return [
88
+ ...leaseIssues,
71
89
  {
72
90
  severity: 'error',
73
91
  message,
@@ -48,8 +48,8 @@ export const resolveSkillsEnforcement = (): SkillsEnforcementResolution => {
48
48
  };
49
49
  }
50
50
  return {
51
- mode: 'advisory',
51
+ mode: 'strict',
52
52
  source: 'default',
53
- blocking: false,
53
+ blocking: true,
54
54
  };
55
55
  };
@@ -52,9 +52,9 @@ export const resolveTddBddEnforcement = (): TddBddEnforcementResolution => {
52
52
  };
53
53
  }
54
54
  return {
55
- mode: 'advisory',
55
+ mode: 'strict',
56
56
  source: 'default',
57
- blocking: false,
57
+ blocking: true,
58
58
  };
59
59
  };
60
60
 
@@ -71,6 +71,7 @@ const applyTddBddFindingEnforcement = (
71
71
  return {
72
72
  ...finding,
73
73
  severity: 'WARN',
74
+ blocking: false,
74
75
  };
75
76
  };
76
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.266",
3
+ "version": "6.3.268",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -4,6 +4,7 @@ import {
4
4
  assertLifecycleStatusMatchesSnapshot,
5
5
  captureLifecycleStatusSnapshot,
6
6
  runLifecycleInstallStep,
7
+ runLifecyclePreWriteStep,
7
8
  runLifecycleUninstallStep,
8
9
  } from './package-install-smoke-lifecycle-lib';
9
10
  import {
@@ -37,10 +38,11 @@ export const runPackageInstallSmoke = (mode: SmokeMode): void => {
37
38
  });
38
39
 
39
40
  setupConsumerRepository(workspace, mode);
41
+ runLifecycleInstallStep(workspace);
42
+ runLifecyclePreWriteStep(workspace);
40
43
  writeStagedPayload(workspace, mode);
41
44
 
42
45
  const lifecycleStatusSnapshot = captureLifecycleStatusSnapshot(workspace);
43
- runLifecycleInstallStep(workspace);
44
46
 
45
47
  const results = runDefaultSmokeGateSteps({
46
48
  workspace,
@@ -25,6 +25,9 @@ export const runGateStep = (
25
25
  expectation: SmokeExpectation
26
26
  ): { outcome: string; exitCode: number } => {
27
27
  const gateEnv: NodeJS.ProcessEnv = {
28
+ PUMUKI_DISABLE_SYSTEM_NOTIFICATIONS: '1',
29
+ PUMUKI_SYSTEM_NOTIFICATIONS: '0',
30
+ PUMUKI_NOTIFICATIONS: '0',
28
31
  PUMUKI_SDD_BYPASS: '1',
29
32
  ...(step.label === 'ci' ? { GITHUB_BASE_REF: 'main' } : {}),
30
33
  };
@@ -38,6 +38,9 @@ export const runLifecycleInstallStep = (workspace: SmokeWorkspace): void => {
38
38
  executable: command.executable,
39
39
  args: command.args,
40
40
  env: {
41
+ PUMUKI_DISABLE_SYSTEM_NOTIFICATIONS: '1',
42
+ PUMUKI_SYSTEM_NOTIFICATIONS: '0',
43
+ PUMUKI_NOTIFICATIONS: '0',
41
44
  PUMUKI_SKIP_OPENSPEC_BOOTSTRAP: '1',
42
45
  },
43
46
  });
@@ -46,6 +49,27 @@ export const runLifecycleInstallStep = (workspace: SmokeWorkspace): void => {
46
49
  assertSuccess(result, 'pumuki lifecycle install');
47
50
  };
48
51
 
52
+ export const runLifecyclePreWriteStep = (workspace: SmokeWorkspace): void => {
53
+ const command = resolveConsumerPumukiCommand({
54
+ consumerRepo: workspace.consumerRepo,
55
+ binary: 'pumuki-pre-write',
56
+ });
57
+ const result = runCommand({
58
+ cwd: workspace.consumerRepo,
59
+ executable: command.executable,
60
+ args: command.args,
61
+ env: {
62
+ PUMUKI_DISABLE_SYSTEM_NOTIFICATIONS: '1',
63
+ PUMUKI_SYSTEM_NOTIFICATIONS: '0',
64
+ PUMUKI_NOTIFICATIONS: '0',
65
+ PUMUKI_SDD_BYPASS: '1',
66
+ },
67
+ });
68
+ pushCommandLog(workspace.commandLog, result);
69
+ assertNoFatalOutput(result, 'pumuki lifecycle pre-write');
70
+ assertSuccess(result, 'pumuki lifecycle pre-write');
71
+ };
72
+
49
73
  export const runLifecycleUninstallStep = (workspace: SmokeWorkspace): void => {
50
74
  const command = resolveConsumerPumukiCommand({
51
75
  consumerRepo: workspace.consumerRepo,
@@ -56,6 +80,11 @@ export const runLifecycleUninstallStep = (workspace: SmokeWorkspace): void => {
56
80
  cwd: workspace.consumerRepo,
57
81
  executable: command.executable,
58
82
  args: command.args,
83
+ env: {
84
+ PUMUKI_DISABLE_SYSTEM_NOTIFICATIONS: '1',
85
+ PUMUKI_SYSTEM_NOTIFICATIONS: '0',
86
+ PUMUKI_NOTIFICATIONS: '0',
87
+ },
59
88
  });
60
89
  pushCommandLog(workspace.commandLog, result);
61
90
  assertNoFatalOutput(result, 'pumuki lifecycle uninstall');