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.
- package/VERSION +1 -1
- package/core/gate/evaluateGate.test.ts +13 -5
- package/core/gate/evaluateGate.ts +4 -4
- package/docs/governance/CONTRIBUTING.md +1 -1
- package/integrations/config/coreSkillsLock.ts +2 -1
- package/integrations/config/skillsDetectorRegistry.ts +18 -0
- package/integrations/config/skillsRuleClassification.ts +147 -0
- package/integrations/git/astIntelligenceDualValidation.ts +1 -0
- package/integrations/git/runPlatformGate.ts +125 -33
- package/integrations/git/stageRunners.ts +4 -10
- package/integrations/lifecycle/cli.ts +94 -4
- package/integrations/lifecycle/doctor.ts +33 -0
- package/integrations/lifecycle/install.ts +26 -4
- package/integrations/lifecycle/notificationStatus.ts +54 -0
- package/integrations/lifecycle/preWriteAutomation.ts +35 -5
- package/integrations/lifecycle/preWriteLease.ts +271 -0
- package/integrations/lifecycle/status.ts +40 -2
- package/integrations/lifecycle/watch.ts +2 -1
- package/integrations/mcp/evidenceStdioServer.cli.ts +2 -2
- package/integrations/policy/skillsEnforcement.ts +3 -14
- package/integrations/policy/tddBddEnforcement.ts +3 -2
- package/package.json +2 -1
- package/scripts/classify-skills-rules.ts +32 -0
- package/scripts/framework-menu-system-notifications-cause.ts +11 -1
- package/scripts/package-install-smoke-consumer-git-payload-lib.ts +2 -2
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -1
- package/scripts/package-install-smoke-execution-lib.ts +3 -1
- package/scripts/package-install-smoke-gate-lib.ts +3 -0
- 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 (
|
|
2853
|
-
|
|
2854
|
-
|
|
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 {
|
|
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 (
|
|
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 =
|
|
139
|
-
const firstIssue =
|
|
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'
|
|
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 (
|
|
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: '
|
|
116
|
-
warnOnOrAbove: '
|
|
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,
|