pumuki 6.3.267 → 6.3.269

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.
Files changed (29) hide show
  1. package/VERSION +1 -1
  2. package/core/gate/evaluateGate.test.ts +13 -5
  3. package/core/gate/evaluateGate.ts +4 -4
  4. package/docs/governance/CONTRIBUTING.md +1 -1
  5. package/integrations/config/coreSkillsLock.ts +2 -1
  6. package/integrations/config/skillsDetectorRegistry.ts +18 -0
  7. package/integrations/config/skillsRuleClassification.ts +147 -0
  8. package/integrations/git/astIntelligenceDualValidation.ts +1 -0
  9. package/integrations/git/runPlatformGate.ts +125 -33
  10. package/integrations/git/stageRunners.ts +4 -10
  11. package/integrations/lifecycle/cli.ts +94 -4
  12. package/integrations/lifecycle/doctor.ts +33 -0
  13. package/integrations/lifecycle/install.ts +26 -4
  14. package/integrations/lifecycle/notificationStatus.ts +54 -0
  15. package/integrations/lifecycle/preWriteAutomation.ts +35 -5
  16. package/integrations/lifecycle/preWriteLease.ts +271 -0
  17. package/integrations/lifecycle/status.ts +40 -2
  18. package/integrations/lifecycle/watch.ts +2 -1
  19. package/integrations/mcp/evidenceStdioServer.cli.ts +2 -2
  20. package/integrations/policy/skillsEnforcement.ts +3 -14
  21. package/integrations/policy/tddBddEnforcement.ts +3 -2
  22. package/package.json +2 -1
  23. package/scripts/classify-skills-rules.ts +32 -0
  24. package/scripts/framework-menu-system-notifications-cause.ts +11 -1
  25. package/scripts/package-install-smoke-consumer-git-payload-lib.ts +2 -2
  26. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -1
  27. package/scripts/package-install-smoke-execution-lib.ts +3 -1
  28. package/scripts/package-install-smoke-gate-lib.ts +3 -0
  29. package/scripts/package-install-smoke-lifecycle-lib.ts +29 -0
@@ -76,6 +76,11 @@ import {
76
76
  import { runPolicyReconcile } from './policyReconcile';
77
77
  import { runLifecycleAudit, type LifecycleAuditStage } from './audit';
78
78
  import { resolvePreWriteEnforcement, type PreWriteEnforcementResolution } from '../policy/preWriteEnforcement';
79
+ import {
80
+ readLocalContextStatus,
81
+ repairLocalContext,
82
+ writeLocalContext,
83
+ } from '../context/contextGate';
79
84
 
80
85
  type LifecycleCommand =
81
86
  | 'bootstrap'
@@ -92,6 +97,7 @@ type LifecycleCommand =
92
97
  | 'adapter'
93
98
  | 'analytics'
94
99
  | 'policy'
100
+ | 'context'
95
101
  | 'audit';
96
102
 
97
103
  type SddCommand =
@@ -107,6 +113,7 @@ type LoopCommand = 'run' | 'status' | 'stop' | 'resume' | 'list' | 'export';
107
113
  type AnalyticsCommand = 'hotspots';
108
114
  type AnalyticsHotspotsCommand = 'report' | 'diagnose';
109
115
  type PolicyCommand = 'reconcile';
116
+ type ContextCommand = 'init' | 'status' | 'repair';
110
117
 
111
118
  type SddSessionAction = 'open' | 'refresh' | 'close';
112
119
 
@@ -178,6 +185,7 @@ export type ParsedArgs = {
178
185
  policyCommand?: PolicyCommand;
179
186
  policyStrict?: boolean;
180
187
  policyApply?: boolean;
188
+ contextCommand?: ContextCommand;
181
189
  auditStage?: LifecycleAuditStage;
182
190
  auditEngine?: boolean;
183
191
  };
@@ -204,6 +212,9 @@ Pumuki lifecycle commands:
204
212
  pumuki analytics hotspots report [--top=<n>] [--since-days=<n>] [--json] [--output-json=<path>] [--output-markdown=<path>]
205
213
  pumuki analytics hotspots diagnose [--json]
206
214
  pumuki policy reconcile [--strict] [--apply] [--json]
215
+ pumuki context init [--json]
216
+ pumuki context status [--json]
217
+ pumuki context repair [--json]
207
218
  pumuki sdd status [--json]
208
219
  pumuki sdd validate [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--json]
209
220
  pumuki sdd session --open --change=<change-id|auto> [--ttl-minutes=<n>] [--json]
@@ -239,6 +250,7 @@ const isLifecycleCommand = (value: string): value is LifecycleCommand =>
239
250
  value === 'adapter' ||
240
251
  value === 'analytics' ||
241
252
  value === 'policy' ||
253
+ value === 'context' ||
242
254
  value === 'audit';
243
255
 
244
256
  const parseAdapterAgent = (value?: string): AdapterAgent => {
@@ -651,6 +663,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
651
663
  let analyticsSinceDays: ParsedArgs['analyticsSinceDays'];
652
664
  let analyticsJsonOutputPath: ParsedArgs['analyticsJsonOutputPath'];
653
665
  let analyticsMarkdownOutputPath: ParsedArgs['analyticsMarkdownOutputPath'];
666
+ let contextCommand: ParsedArgs['contextCommand'];
654
667
  let auditStage: LifecycleAuditStage | undefined;
655
668
  let auditEngine = false;
656
669
 
@@ -863,6 +876,31 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
863
876
  };
864
877
  }
865
878
 
879
+ if (commandRaw === 'context') {
880
+ const subcommandRaw = argv[1] ?? 'status';
881
+ if (
882
+ subcommandRaw !== 'init' &&
883
+ subcommandRaw !== 'status' &&
884
+ subcommandRaw !== 'repair'
885
+ ) {
886
+ throw new Error(`Unsupported context subcommand "${subcommandRaw}".\n\n${HELP_TEXT}`);
887
+ }
888
+ contextCommand = subcommandRaw;
889
+ for (const arg of argv.slice(2)) {
890
+ if (arg === '--json') {
891
+ json = true;
892
+ continue;
893
+ }
894
+ throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
895
+ }
896
+ return {
897
+ command: commandRaw,
898
+ purgeArtifacts: false,
899
+ json,
900
+ contextCommand,
901
+ };
902
+ }
903
+
866
904
  if (commandRaw === 'loop') {
867
905
  const subcommandRaw = argv[1] ?? '';
868
906
  if (
@@ -1582,6 +1620,16 @@ const printDoctorReport = (
1582
1620
  `PRE_PUSH=${report.policyValidation.stages.PRE_PUSH.validationCode ?? 'n/a'} strict=${report.policyValidation.stages.PRE_PUSH.strict ? 'yes' : 'no'} ` +
1583
1621
  `CI=${report.policyValidation.stages.CI.validationCode ?? 'n/a'} strict=${report.policyValidation.stages.CI.strict ? 'yes' : 'no'}`
1584
1622
  );
1623
+ writeInfo(
1624
+ `[pumuki] notifications: enabled=${report.notifications.enabled ? 'yes' : 'no'} ` +
1625
+ `deliverable=${report.notifications.deliverable ? 'yes' : 'no'} ` +
1626
+ `channel=${report.notifications.channel} ` +
1627
+ `blocked_dialog=${report.notifications.blockedDialogEnabled ? 'yes' : 'no'} ` +
1628
+ `reason=${report.notifications.disabledReason ?? 'none'}`
1629
+ );
1630
+ if (report.notifications.remediation) {
1631
+ writeInfo(`[pumuki] notifications remediation: ${report.notifications.remediation}`);
1632
+ }
1585
1633
 
1586
1634
  for (const issue of report.issues) {
1587
1635
  writeInfo(`[pumuki] ${issue.severity.toUpperCase()}: ${issue.message}`);
@@ -2377,6 +2425,28 @@ export const runLifecycleCli = async (
2377
2425
  }
2378
2426
  return result.gate_exit_code;
2379
2427
  }
2428
+ case 'context': {
2429
+ const repoRoot = process.cwd();
2430
+ const result =
2431
+ parsed.contextCommand === 'init'
2432
+ ? writeLocalContext(repoRoot)
2433
+ : parsed.contextCommand === 'repair'
2434
+ ? repairLocalContext(repoRoot)
2435
+ : readLocalContextStatus(repoRoot);
2436
+ if (parsed.json) {
2437
+ writeInfo(JSON.stringify(result, null, 2));
2438
+ } else {
2439
+ writeInfo(`[pumuki][context] status=${result.status}`);
2440
+ writeInfo(`[pumuki][context] path=${result.path}`);
2441
+ writeInfo(`[pumuki][context] ${result.message}`);
2442
+ if (result.document) {
2443
+ writeInfo(`[pumuki][context] repo=${result.document.repo_root}`);
2444
+ writeInfo(`[pumuki][context] agent=${result.document.contract.agent_instruction}`);
2445
+ writeInfo(`[pumuki][context] user=${result.document.contract.user_remediation}`);
2446
+ }
2447
+ }
2448
+ return result.status === 'ok' ? 0 : 1;
2449
+ }
2380
2450
  case 'doctor': {
2381
2451
  const report = runLifecycleDoctor({
2382
2452
  deep: parsed.doctorDeep === true,
@@ -2467,6 +2537,16 @@ export const runLifecycleCli = async (
2467
2537
  `PRE_PUSH=${status.policyValidation.stages.PRE_PUSH.validationCode ?? 'n/a'} strict=${status.policyValidation.stages.PRE_PUSH.strict ? 'yes' : 'no'} ` +
2468
2538
  `CI=${status.policyValidation.stages.CI.validationCode ?? 'n/a'} strict=${status.policyValidation.stages.CI.strict ? 'yes' : 'no'}`
2469
2539
  );
2540
+ writeInfo(
2541
+ `[pumuki] notifications: enabled=${status.notifications.enabled ? 'yes' : 'no'} ` +
2542
+ `deliverable=${status.notifications.deliverable ? 'yes' : 'no'} ` +
2543
+ `channel=${status.notifications.channel} ` +
2544
+ `blocked_dialog=${status.notifications.blockedDialogEnabled ? 'yes' : 'no'} ` +
2545
+ `reason=${status.notifications.disabledReason ?? 'none'}`
2546
+ );
2547
+ if (status.notifications.remediation) {
2548
+ writeInfo(`[pumuki] notifications remediation: ${status.notifications.remediation}`);
2549
+ }
2470
2550
  writeInfo(
2471
2551
  `[pumuki] experimental: ANALYTICS=${status.experimentalFeatures.features.analytics.mode} source=${status.experimentalFeatures.features.analytics.source} layer=${status.experimentalFeatures.features.analytics.layer} blocking=${status.experimentalFeatures.features.analytics.blocking ? 'yes' : 'no'} env=${status.experimentalFeatures.features.analytics.activationVariable}`
2472
2552
  );
@@ -2849,8 +2929,18 @@ export const runLifecycleCli = async (
2849
2929
  }
2850
2930
  };
2851
2931
 
2852
- if (require.main === module) {
2853
- void runLifecycleCli(process.argv.slice(2)).then((code) => {
2854
- process.exitCode = code;
2855
- });
2932
+ if (
2933
+ require.main === module &&
2934
+ process.env.PUMUKI_RUNTIME_EXECUTION_SOURCE === 'source-bin'
2935
+ ) {
2936
+ void runLifecycleCli(process.argv.slice(2)).then(
2937
+ (code) => {
2938
+ process.exit(code);
2939
+ },
2940
+ (error) => {
2941
+ const message = error instanceof Error ? error.message : 'Unexpected lifecycle CLI error.';
2942
+ writeError(message);
2943
+ process.exit(1);
2944
+ }
2945
+ );
2856
2946
  }
@@ -20,6 +20,11 @@ import {
20
20
  readLifecycleDependencyInventory,
21
21
  type LifecycleDependencyInventory,
22
22
  } from './dependencyInventory';
23
+ import { readPreWriteLeaseStatus } from './preWriteLease';
24
+ import {
25
+ readLifecycleNotificationStatus,
26
+ type LifecycleNotificationStatus,
27
+ } from './notificationStatus';
23
28
 
24
29
  export type DoctorIssueSeverity = 'warning' | 'error';
25
30
 
@@ -101,6 +106,7 @@ export type LifecycleDoctorReport = {
101
106
  hooksDirectory: string;
102
107
  hooksDirectoryResolution: 'git-rev-parse' | 'git-config' | 'default';
103
108
  policyValidation: LifecyclePolicyValidationSnapshot;
109
+ notifications: LifecycleNotificationStatus;
104
110
  policy_signature_remediation?: string;
105
111
  issues: ReadonlyArray<DoctorIssue>;
106
112
  deep?: DoctorDeepReport;
@@ -116,7 +122,32 @@ const buildDoctorIssues = (params: {
116
122
  lifecycleState: LifecycleState;
117
123
  }): ReadonlyArray<DoctorIssue> => {
118
124
  const issues: DoctorIssue[] = [];
125
+ const notificationStatus = readLifecycleNotificationStatus({
126
+ repoRoot: params.repoRoot,
127
+ });
119
128
  const evidenceResult = readEvidenceResult(params.repoRoot);
129
+ const preWriteLeaseStatus = readPreWriteLeaseStatus({
130
+ repoRoot: params.repoRoot,
131
+ git: new LifecycleGitService(),
132
+ });
133
+
134
+ if (!preWriteLeaseStatus.valid && preWriteLeaseStatus.changedCodePaths.length > 0) {
135
+ issues.push({
136
+ severity: 'error',
137
+ message:
138
+ `ENFORCEMENT_GAP: PRE_WRITE hard-stop lease is not valid (${preWriteLeaseStatus.code}) ` +
139
+ `for ${preWriteLeaseStatus.changedCodePaths.length} changed code file(s).`,
140
+ });
141
+ }
142
+
143
+ if (!notificationStatus.deliverable) {
144
+ issues.push({
145
+ severity: 'warning',
146
+ message:
147
+ `System notifications are not deliverable (${notificationStatus.disabledReason ?? 'unknown'}). ` +
148
+ `${notificationStatus.remediation ?? 'Review notification configuration.'}`,
149
+ });
150
+ }
120
151
 
121
152
  if (params.trackedNodeModulesPaths.length > 0) {
122
153
  issues.push({
@@ -886,6 +917,7 @@ export const runLifecycleDoctor = (params?: {
886
917
 
887
918
  const policyValidation = readLifecyclePolicyValidationSnapshot(repoRoot);
888
919
  const policySignatureRemediation = buildPolicySignatureRemediation(policyValidation);
920
+ const notifications = readLifecycleNotificationStatus({ repoRoot });
889
921
 
890
922
  return {
891
923
  repoRoot,
@@ -898,6 +930,7 @@ export const runLifecycleDoctor = (params?: {
898
930
  hooksDirectory: hooksDirectory.path,
899
931
  hooksDirectoryResolution: hooksDirectory.source,
900
932
  policyValidation,
933
+ notifications,
901
934
  ...(policySignatureRemediation
902
935
  ? { policy_signature_remediation: policySignatureRemediation }
903
936
  : {}),
@@ -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';
@@ -15,6 +15,11 @@ import { ensureRuntimeArtifactsIgnored } from './artifacts';
15
15
  import { runLifecycleAdapterInstall } from './adapter';
16
16
  import { runPolicyReconcile } from './policyReconcile';
17
17
  import { emitGateBlockedNotification } from '../notifications/emitAuditSummaryNotification';
18
+ import { persistSystemNotificationsConfig } from '../../scripts/framework-menu-system-notifications-config';
19
+ import {
20
+ readLifecycleNotificationStatus,
21
+ type LifecycleNotificationStatus,
22
+ } from './notificationStatus';
18
23
 
19
24
  export type LifecycleInstallResult = {
20
25
  repoRoot: string;
@@ -22,6 +27,7 @@ export type LifecycleInstallResult = {
22
27
  changedHooks: ReadonlyArray<string>;
23
28
  openSpecBootstrap?: OpenSpecBootstrapResult;
24
29
  degradedDoctorBypass?: boolean;
30
+ notifications: LifecycleNotificationStatus;
25
31
  };
26
32
 
27
33
  const shouldBootstrapEvidence = (repoRoot: string): boolean =>
@@ -99,6 +105,14 @@ const materializeStrictPolicyAsCode = (repoRoot: string): void => {
99
105
  }
100
106
  };
101
107
 
108
+ const isPreWriteEnforcementGapIssue = (message: string): boolean =>
109
+ message.startsWith('ENFORCEMENT_GAP: PRE_WRITE hard-stop lease is not valid');
110
+
111
+ const ensureInstallNotificationsEnabled = (repoRoot: string): LifecycleNotificationStatus => {
112
+ persistSystemNotificationsConfig(repoRoot, true);
113
+ return readLifecycleNotificationStatus({ repoRoot });
114
+ };
115
+
102
116
  export const runLifecycleInstall = (params?: {
103
117
  cwd?: string;
104
118
  git?: ILifecycleGitService;
@@ -114,8 +128,14 @@ export const runLifecycleInstall = (params?: {
114
128
  cwd: params?.cwd,
115
129
  git,
116
130
  });
131
+ const notifications = ensureInstallNotificationsEnabled(report.repoRoot);
132
+ const installBlockingIssues = report.issues.filter(
133
+ (issue) => issue.severity === 'error' && !isPreWriteEnforcementGapIssue(issue.message)
134
+ );
135
+ const hasInstallBlockingIssues =
136
+ installBlockingIssues.length > 0 || report.deep?.blocking === true;
117
137
 
118
- if (doctorHasBlockingIssues(report)) {
138
+ if (hasInstallBlockingIssues) {
119
139
  if (bestEffortAfterDoctorBlock) {
120
140
  const version = getCurrentPumukiVersion();
121
141
  const priorArtifacts = readOpenSpecManagedArtifacts(git, report.repoRoot);
@@ -133,10 +153,11 @@ export const runLifecycleInstall = (params?: {
133
153
  changedHooks,
134
154
  openSpecBootstrap: undefined,
135
155
  degradedDoctorBypass: true,
156
+ notifications,
136
157
  };
137
158
  }
138
- const renderedIssues = report.issues.map((issue) => `- [${issue.severity}] ${issue.message}`).join('\n');
139
- const firstIssue = report.issues[0];
159
+ const renderedIssues = installBlockingIssues.map((issue) => `- [${issue.severity}] ${issue.message}`).join('\n');
160
+ const firstIssue = installBlockingIssues[0];
140
161
  const notificationResult = (params?.notifyGateBlocked ?? emitGateBlockedNotification)({
141
162
  repoRoot: report.repoRoot,
142
163
  stage: 'PRE_COMMIT',
@@ -185,5 +206,6 @@ export const runLifecycleInstall = (params?: {
185
206
  version,
186
207
  changedHooks,
187
208
  openSpecBootstrap,
209
+ notifications,
188
210
  };
189
211
  };
@@ -0,0 +1,54 @@
1
+ import { resolveEffectiveSystemNotificationsConfig } from '../../scripts/framework-menu-system-notifications-effective-config';
2
+ import { resolveSystemNotificationGate } from '../../scripts/framework-menu-system-notifications-gate';
3
+
4
+ export type LifecycleNotificationStatus = {
5
+ enabled: boolean;
6
+ blockedDialogEnabled: boolean;
7
+ channel: string;
8
+ muteUntil: string | null;
9
+ deliverable: boolean;
10
+ disabledReason: string | null;
11
+ remediation: string | null;
12
+ };
13
+
14
+ const remediationForReason = (reason: string | null): string | null => {
15
+ if (reason === null) {
16
+ return null;
17
+ }
18
+ if (reason === 'disabled') {
19
+ return 'Activa notificaciones con la configuracion repo-local de Pumuki y elimina env vars PUMUKI_*NOTIFICATIONS que las deshabiliten.';
20
+ }
21
+ if (reason === 'muted') {
22
+ return 'Quita el silencio temporal desde la notificacion o elimina muteUntil en .pumuki/system-notifications.json.';
23
+ }
24
+ if (reason === 'unsupported-platform') {
25
+ return 'Usa el fallback stderr o configura un canal compatible para este sistema.';
26
+ }
27
+ return 'Revisa la configuracion de notificaciones y reejecuta pumuki status/doctor.';
28
+ };
29
+
30
+ export const readLifecycleNotificationStatus = (params: {
31
+ repoRoot: string;
32
+ env?: NodeJS.ProcessEnv;
33
+ now?: () => number;
34
+ }): LifecycleNotificationStatus => {
35
+ const env = params.env ?? process.env;
36
+ const config = resolveEffectiveSystemNotificationsConfig({
37
+ repoRoot: params.repoRoot,
38
+ });
39
+ const gate = resolveSystemNotificationGate({
40
+ config,
41
+ nowMs: (params.now ?? Date.now)(),
42
+ env,
43
+ });
44
+ const disabledReason = gate?.reason ?? null;
45
+ return {
46
+ enabled: config.enabled,
47
+ blockedDialogEnabled: config.blockedDialogEnabled === true,
48
+ channel: config.channel,
49
+ muteUntil: config.muteUntil ?? null,
50
+ deliverable: gate === null,
51
+ disabledReason,
52
+ remediation: remediationForReason(disabledReason),
53
+ };
54
+ };
@@ -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,