pumuki 6.3.129 → 6.3.130
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/AGENTS.md +16 -1
- package/CHANGELOG.md +9 -0
- package/README.md +14 -10
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +2583 -24
- package/core/facts/detectors/text/android.ts +4633 -34
- package/core/facts/detectors/typescript/index.test.ts +3639 -73
- package/core/facts/detectors/typescript/index.ts +4819 -270
- package/core/facts/extractHeuristicFacts.ts +271 -6
- package/core/rules/presets/heuristics/android.test.ts +314 -1
- package/core/rules/presets/heuristics/android.ts +1220 -50
- package/core/rules/presets/heuristics/typescript.test.ts +158 -2
- package/core/rules/presets/heuristics/typescript.ts +508 -0
- package/docs/README.md +3 -3
- package/docs/operations/RELEASE_NOTES.md +7 -1
- package/docs/operations/framework-menu-consumer-walkthrough.md +18 -15
- package/docs/product/API_REFERENCE.md +1 -1
- package/docs/product/USAGE.md +1 -1
- package/docs/validation/README.md +3 -1
- package/integrations/config/skillsCompilerTemplates.test.ts +131 -0
- package/integrations/config/skillsCompilerTemplates.ts +953 -7
- package/integrations/config/skillsDetectorRegistry.ts +451 -8
- package/integrations/config/skillsMarkdownRules.ts +884 -2
- package/integrations/evidence/buildEvidence.ts +29 -1
- package/integrations/evidence/platformSummary.test.ts +73 -9
- package/integrations/evidence/platformSummary.ts +165 -7
- package/integrations/evidence/repoState.ts +3 -0
- package/integrations/evidence/schema.ts +18 -0
- package/integrations/evidence/trackingContract.ts +17 -0
- package/integrations/evidence/writeEvidence.test.ts +3 -0
- package/integrations/evidence/writeEvidence.ts +29 -1
- package/integrations/gate/evaluateAiGate.ts +251 -8
- package/integrations/gate/governanceActionCatalog.ts +275 -0
- package/integrations/gate/remediationCatalog.ts +8 -0
- package/integrations/git/runPlatformGate.ts +9 -1
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cliGovernanceConsole.ts +69 -0
- package/integrations/lifecycle/governanceNextAction.ts +171 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +369 -0
- package/integrations/lifecycle/packageInfo.ts +118 -1
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/lifecycle/trackingState.ts +403 -0
- package/integrations/lifecycle/watch.ts +1 -1
- package/integrations/mcp/aiGateCheck.ts +194 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/enterpriseServer.ts +19 -3
- package/integrations/mcp/preFlightCheck.ts +66 -3
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/package.json +1 -1
- package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
- package/scripts/check-tracking-single-active.sh +1 -1
- package/scripts/framework-menu-advanced-view-lib.ts +49 -0
- package/scripts/framework-menu-consumer-actions-lib.ts +32 -32
- package/scripts/framework-menu-consumer-preflight-render.ts +10 -0
- package/scripts/framework-menu-consumer-preflight-run.ts +23 -0
- package/scripts/framework-menu-consumer-preflight-types.ts +12 -0
- package/scripts/framework-menu-consumer-runtime-actions.ts +11 -5
- package/scripts/framework-menu-consumer-runtime-audit.ts +0 -28
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +118 -42
- package/scripts/framework-menu-consumer-runtime-lib.ts +38 -0
- package/scripts/framework-menu-consumer-runtime-menu.ts +55 -15
- package/scripts/framework-menu-consumer-runtime-types.ts +4 -0
- package/scripts/framework-menu-evidence-summary-read.ts +17 -1
- package/scripts/framework-menu-evidence-summary-types.ts +3 -0
- package/scripts/framework-menu-layout-data.ts +3 -23
- package/scripts/framework-menu-system-notifications-macos-applescript-dialog.ts +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +14 -2
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +1 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +128 -4
- package/scripts/framework-menu-system-notifications-payloads.ts +8 -1
- package/scripts/framework-menu-system-notifications-text.ts +7 -1
- package/scripts/framework-menu.ts +37 -2
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +10 -1
- package/scripts/package-install-smoke-consumer-npm-lib.ts +46 -9
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
- package/skills.lock.json +613 -698
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readEvidenceResult } from '../evidence/readEvidence';
|
|
4
|
+
import { readRepoTrackingState } from '../evidence/trackingContract';
|
|
5
|
+
import type { RepoTrackingState } from '../evidence/schema';
|
|
6
|
+
import { readSddStatus } from '../sdd';
|
|
7
|
+
import type { SddStatusPayload } from '../sdd/types';
|
|
8
|
+
import type { LifecycleExperimentalFeaturesSnapshot } from './experimentalFeaturesSnapshot';
|
|
9
|
+
import type { ILifecycleGitService } from './gitService';
|
|
10
|
+
import { LifecycleGitService } from './gitService';
|
|
11
|
+
import type { LifecyclePolicyValidationSnapshot } from './policyValidationSnapshot';
|
|
12
|
+
import { writeInfo } from './cliOutputs';
|
|
13
|
+
import { formatTrackingActionableContext } from './trackingState';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
|
|
16
|
+
|
|
17
|
+
export type GovernanceEvidenceSummary = {
|
|
18
|
+
path: string;
|
|
19
|
+
readable: 'missing' | 'invalid' | 'valid';
|
|
20
|
+
snapshot_stage?: string;
|
|
21
|
+
snapshot_outcome?: 'PASS' | 'WARN' | 'BLOCK';
|
|
22
|
+
matched_warn_count?: number;
|
|
23
|
+
matched_blocking_count?: number;
|
|
24
|
+
findings_count?: number;
|
|
25
|
+
ai_gate_status?: 'ALLOWED' | 'BLOCKED';
|
|
26
|
+
human_summary_preview: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type GovernanceContractSurface = {
|
|
30
|
+
agents_md: boolean;
|
|
31
|
+
skills_lock_json: boolean;
|
|
32
|
+
skills_sources_json: boolean;
|
|
33
|
+
vendor_skills_dir: boolean;
|
|
34
|
+
pumuki_adapter_json: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type GovernanceObservationSnapshot = {
|
|
38
|
+
schema_version: '1';
|
|
39
|
+
platform_bundles_effective?: ReadonlyArray<string>;
|
|
40
|
+
pre_write_effective?: {
|
|
41
|
+
mode: 'off' | 'advisory' | 'strict';
|
|
42
|
+
source: string;
|
|
43
|
+
blocking: boolean;
|
|
44
|
+
strict_policy: boolean;
|
|
45
|
+
};
|
|
46
|
+
sdd: {
|
|
47
|
+
experimental_raw: string | null;
|
|
48
|
+
effective_mode: 'off' | 'advisory' | 'strict';
|
|
49
|
+
experimental_source: string;
|
|
50
|
+
};
|
|
51
|
+
sdd_session: {
|
|
52
|
+
active: boolean;
|
|
53
|
+
valid: boolean;
|
|
54
|
+
change_id: string | null;
|
|
55
|
+
remaining_seconds: number | null;
|
|
56
|
+
};
|
|
57
|
+
policy_strict: {
|
|
58
|
+
pre_write: boolean;
|
|
59
|
+
pre_commit: boolean;
|
|
60
|
+
pre_push: boolean;
|
|
61
|
+
ci: boolean;
|
|
62
|
+
};
|
|
63
|
+
enterprise_warn_as_block_env: boolean;
|
|
64
|
+
evidence: GovernanceEvidenceSummary;
|
|
65
|
+
git: {
|
|
66
|
+
current_branch: string | null;
|
|
67
|
+
on_protected_branch_hint: boolean;
|
|
68
|
+
};
|
|
69
|
+
contract_surface: GovernanceContractSurface;
|
|
70
|
+
tracking: RepoTrackingState;
|
|
71
|
+
attention_codes: ReadonlyArray<string>;
|
|
72
|
+
governance_effective: 'green' | 'attention' | 'blocked';
|
|
73
|
+
agent_bootstrap_hints: ReadonlyArray<string>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const truthyEnv = (value: string | undefined): boolean => {
|
|
77
|
+
if (typeof value !== 'string') {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const normalized = value.trim().toLowerCase();
|
|
81
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'strict';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const readCurrentBranch = (git: ILifecycleGitService, repoRoot: string): string | null => {
|
|
85
|
+
try {
|
|
86
|
+
const branch = git.runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).trim();
|
|
87
|
+
return branch.length > 0 ? branch : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const readSddStatusSafe = (repoRoot: string, git: ILifecycleGitService): SddStatusPayload => {
|
|
94
|
+
try {
|
|
95
|
+
return readSddStatus(repoRoot, git);
|
|
96
|
+
} catch {
|
|
97
|
+
return {
|
|
98
|
+
repoRoot,
|
|
99
|
+
openspec: {
|
|
100
|
+
installed: false,
|
|
101
|
+
projectInitialized: false,
|
|
102
|
+
minimumVersion: '0.0.0',
|
|
103
|
+
recommendedVersion: '0.0.0',
|
|
104
|
+
compatible: false,
|
|
105
|
+
},
|
|
106
|
+
session: {
|
|
107
|
+
repoRoot,
|
|
108
|
+
active: false,
|
|
109
|
+
valid: false,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const buildContractSurface = (repoRoot: string): GovernanceContractSurface => ({
|
|
116
|
+
agents_md: existsSync(join(repoRoot, 'AGENTS.md')),
|
|
117
|
+
skills_lock_json: existsSync(join(repoRoot, 'skills.lock.json')),
|
|
118
|
+
skills_sources_json: existsSync(join(repoRoot, 'skills.sources.json')),
|
|
119
|
+
vendor_skills_dir: existsSync(join(repoRoot, 'vendor', 'skills')),
|
|
120
|
+
pumuki_adapter_json: existsSync(join(repoRoot, '.pumuki', 'adapter.json')),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const PLATFORM_BUNDLE_ORDER = [
|
|
124
|
+
'android-enterprise-rules',
|
|
125
|
+
'backend-enterprise-rules',
|
|
126
|
+
'frontend-enterprise-rules',
|
|
127
|
+
'ios-enterprise-rules',
|
|
128
|
+
] as const;
|
|
129
|
+
|
|
130
|
+
const PLATFORM_BUNDLE_LABELS: Record<(typeof PLATFORM_BUNDLE_ORDER)[number], string> = {
|
|
131
|
+
'android-enterprise-rules': 'android',
|
|
132
|
+
'backend-enterprise-rules': 'backend',
|
|
133
|
+
'frontend-enterprise-rules': 'frontend',
|
|
134
|
+
'ios-enterprise-rules': 'ios',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const resolvePlatformBundlesEffective = (repoRoot: string): ReadonlyArray<string> => {
|
|
138
|
+
const vendorSkillsPath = join(repoRoot, 'vendor', 'skills');
|
|
139
|
+
if (!existsSync(vendorSkillsPath)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const present = new Set(
|
|
143
|
+
readdirSync(vendorSkillsPath, { withFileTypes: true })
|
|
144
|
+
.filter((entry) => entry.isDirectory())
|
|
145
|
+
.map((entry) => entry.name)
|
|
146
|
+
);
|
|
147
|
+
return PLATFORM_BUNDLE_ORDER.filter((bundle) => present.has(bundle)).map(
|
|
148
|
+
(bundle) => PLATFORM_BUNDLE_LABELS[bundle]
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const summarizeEvidence = (repoRoot: string): GovernanceEvidenceSummary => {
|
|
153
|
+
const evidenceResult = readEvidenceResult(repoRoot);
|
|
154
|
+
const path = evidenceResult.source_descriptor.path;
|
|
155
|
+
if (evidenceResult.kind === 'missing') {
|
|
156
|
+
return { path, readable: 'missing', human_summary_preview: [] };
|
|
157
|
+
}
|
|
158
|
+
if (evidenceResult.kind === 'invalid') {
|
|
159
|
+
return {
|
|
160
|
+
path,
|
|
161
|
+
readable: 'invalid',
|
|
162
|
+
human_summary_preview: [evidenceResult.detail ?? evidenceResult.reason],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const snapshot = evidenceResult.evidence.snapshot;
|
|
167
|
+
const hints = evidenceResult.evidence.operational_hints?.human_summary_lines ?? [];
|
|
168
|
+
const breakdown = evidenceResult.evidence.operational_hints?.rule_execution_breakdown;
|
|
169
|
+
return {
|
|
170
|
+
path,
|
|
171
|
+
readable: 'valid',
|
|
172
|
+
snapshot_stage: snapshot.stage,
|
|
173
|
+
snapshot_outcome: snapshot.outcome,
|
|
174
|
+
matched_warn_count: breakdown?.matched_warn_count,
|
|
175
|
+
matched_blocking_count: breakdown?.matched_blocking_count,
|
|
176
|
+
findings_count: Array.isArray(snapshot.findings) ? snapshot.findings.length : 0,
|
|
177
|
+
ai_gate_status: evidenceResult.evidence.ai_gate.status,
|
|
178
|
+
human_summary_preview: hints.slice(0, 5),
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const buildHints = (
|
|
183
|
+
surface: GovernanceContractSurface,
|
|
184
|
+
branch: string | null,
|
|
185
|
+
protectedBranchHint: boolean,
|
|
186
|
+
tracking: RepoTrackingState
|
|
187
|
+
): string[] => {
|
|
188
|
+
const hints: string[] = [];
|
|
189
|
+
if (surface.agents_md) {
|
|
190
|
+
hints.push('AGENTS.md presente: aplica el contrato del repo antes de dar governance en verde.');
|
|
191
|
+
}
|
|
192
|
+
if (!surface.skills_lock_json) {
|
|
193
|
+
hints.push('Falta skills.lock.json: genera lock canónico de skills antes de cerrar la gobernanza.');
|
|
194
|
+
}
|
|
195
|
+
if (!surface.pumuki_adapter_json) {
|
|
196
|
+
hints.push('Falta .pumuki/adapter.json: instala el adaptador si quieres wiring IDE/MCP explícito.');
|
|
197
|
+
}
|
|
198
|
+
if (protectedBranchHint && branch) {
|
|
199
|
+
hints.push(`La rama "${branch}" cae en el set protegido por defecto: usa feature/* o refactor/*.`);
|
|
200
|
+
}
|
|
201
|
+
if (tracking.conflict) {
|
|
202
|
+
hints.push('Tracking canónico en conflicto: AGENTS.md y los README del repo no apuntan al mismo MD.');
|
|
203
|
+
}
|
|
204
|
+
if (tracking.enforced && !tracking.canonical_present) {
|
|
205
|
+
hints.push(`Falta el tracking canónico declarado (${tracking.canonical_path ?? 'sin resolver'}).`);
|
|
206
|
+
}
|
|
207
|
+
if (tracking.enforced && tracking.single_in_progress_valid === false) {
|
|
208
|
+
const actionableContext = formatTrackingActionableContext(tracking);
|
|
209
|
+
hints.push(
|
|
210
|
+
`El tracking canónico debe dejar exactamente una 🚧 (actual=${tracking.in_progress_count ?? 'n/a'}${actionableContext ? `, ${actionableContext}` : ''}).`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
hints.push('SDD/OpenSpec: usa PUMUKI_EXPERIMENTAL_SDD=advisory|strict cuando el loop SDD esté activo.');
|
|
214
|
+
hints.push('WARN-as-BLOCK: activa PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK=1 si el repo exige promoción dura.');
|
|
215
|
+
return hints;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const readGovernanceObservationSnapshot = (params: {
|
|
219
|
+
repoRoot: string;
|
|
220
|
+
experimentalFeatures: LifecycleExperimentalFeaturesSnapshot;
|
|
221
|
+
policyValidation: LifecyclePolicyValidationSnapshot;
|
|
222
|
+
git?: ILifecycleGitService;
|
|
223
|
+
}): GovernanceObservationSnapshot => {
|
|
224
|
+
const git = params.git ?? new LifecycleGitService();
|
|
225
|
+
const { repoRoot, experimentalFeatures, policyValidation } = params;
|
|
226
|
+
const rawSdd = process.env.PUMUKI_EXPERIMENTAL_SDD?.trim();
|
|
227
|
+
const sddStatus = readSddStatusSafe(repoRoot, git);
|
|
228
|
+
const evidence = summarizeEvidence(repoRoot);
|
|
229
|
+
const branch = readCurrentBranch(git, repoRoot);
|
|
230
|
+
const onProtected = typeof branch === 'string' && DEFAULT_PROTECTED_BRANCHES.has(branch.trim().toLowerCase());
|
|
231
|
+
const surface = buildContractSurface(repoRoot);
|
|
232
|
+
const tracking = readRepoTrackingState(repoRoot);
|
|
233
|
+
const warnAsBlock = truthyEnv(process.env.PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK);
|
|
234
|
+
|
|
235
|
+
const attention: string[] = [];
|
|
236
|
+
if (evidence.readable === 'invalid') {
|
|
237
|
+
attention.push('EVIDENCE_INVALID_OR_CHAIN');
|
|
238
|
+
}
|
|
239
|
+
if (evidence.readable === 'valid' && evidence.ai_gate_status === 'BLOCKED') {
|
|
240
|
+
attention.push('AI_GATE_BLOCKED');
|
|
241
|
+
}
|
|
242
|
+
if (evidence.readable === 'valid' && evidence.snapshot_outcome === 'WARN') {
|
|
243
|
+
attention.push('EVIDENCE_SNAPSHOT_WARN');
|
|
244
|
+
}
|
|
245
|
+
if (evidence.readable === 'valid' && evidence.snapshot_outcome === 'BLOCK') {
|
|
246
|
+
attention.push('EVIDENCE_SNAPSHOT_BLOCK');
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
sddStatus.session.valid !== true &&
|
|
250
|
+
(sddStatus.session.active === true || !!sddStatus.session.changeId || sddStatus.session.remainingSeconds === 0)
|
|
251
|
+
) {
|
|
252
|
+
attention.push('SDD_SESSION_INVALID_OR_EXPIRED');
|
|
253
|
+
}
|
|
254
|
+
if (!policyValidation.stages.PRE_WRITE.strict) {
|
|
255
|
+
attention.push('POLICY_PRE_WRITE_NOT_STRICT');
|
|
256
|
+
}
|
|
257
|
+
if (!policyValidation.stages.PRE_COMMIT.strict) {
|
|
258
|
+
attention.push('POLICY_PRE_COMMIT_NOT_STRICT');
|
|
259
|
+
}
|
|
260
|
+
if (!policyValidation.stages.PRE_PUSH.strict) {
|
|
261
|
+
attention.push('POLICY_PRE_PUSH_NOT_STRICT');
|
|
262
|
+
}
|
|
263
|
+
if (!policyValidation.stages.CI.strict) {
|
|
264
|
+
attention.push('POLICY_CI_NOT_STRICT');
|
|
265
|
+
}
|
|
266
|
+
if (onProtected) {
|
|
267
|
+
attention.push('GITFLOW_PROTECTED_BRANCH_CONTEXT');
|
|
268
|
+
}
|
|
269
|
+
if (tracking.conflict) {
|
|
270
|
+
attention.push('TRACKING_CANONICAL_SOURCE_CONFLICT');
|
|
271
|
+
}
|
|
272
|
+
if (tracking.enforced && !tracking.canonical_present) {
|
|
273
|
+
attention.push('TRACKING_CANONICAL_FILE_MISSING');
|
|
274
|
+
}
|
|
275
|
+
if (tracking.enforced && tracking.single_in_progress_valid === false) {
|
|
276
|
+
attention.push('TRACKING_CANONICAL_IN_PROGRESS_INVALID');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let governanceEffective: GovernanceObservationSnapshot['governance_effective'] = 'green';
|
|
280
|
+
if (
|
|
281
|
+
evidence.readable === 'invalid'
|
|
282
|
+
|| (evidence.readable === 'valid' && evidence.ai_gate_status === 'BLOCKED')
|
|
283
|
+
|| (evidence.readable === 'valid' && evidence.snapshot_outcome === 'BLOCK')
|
|
284
|
+
) {
|
|
285
|
+
governanceEffective = 'blocked';
|
|
286
|
+
} else if (attention.length > 0) {
|
|
287
|
+
governanceEffective = 'attention';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
schema_version: '1',
|
|
292
|
+
platform_bundles_effective: resolvePlatformBundlesEffective(repoRoot),
|
|
293
|
+
pre_write_effective: {
|
|
294
|
+
mode: experimentalFeatures.features.pre_write.mode,
|
|
295
|
+
source: experimentalFeatures.features.pre_write.source,
|
|
296
|
+
blocking: experimentalFeatures.features.pre_write.blocking,
|
|
297
|
+
strict_policy: policyValidation.stages.PRE_WRITE.strict,
|
|
298
|
+
},
|
|
299
|
+
sdd: {
|
|
300
|
+
experimental_raw: rawSdd && rawSdd.length > 0 ? rawSdd : null,
|
|
301
|
+
effective_mode: experimentalFeatures.features.sdd.mode,
|
|
302
|
+
experimental_source: experimentalFeatures.features.sdd.source,
|
|
303
|
+
},
|
|
304
|
+
sdd_session: {
|
|
305
|
+
active: sddStatus.session.active,
|
|
306
|
+
valid: sddStatus.session.valid,
|
|
307
|
+
change_id: sddStatus.session.changeId ?? null,
|
|
308
|
+
remaining_seconds:
|
|
309
|
+
typeof sddStatus.session.remainingSeconds === 'number' ? sddStatus.session.remainingSeconds : null,
|
|
310
|
+
},
|
|
311
|
+
policy_strict: {
|
|
312
|
+
pre_write: policyValidation.stages.PRE_WRITE.strict,
|
|
313
|
+
pre_commit: policyValidation.stages.PRE_COMMIT.strict,
|
|
314
|
+
pre_push: policyValidation.stages.PRE_PUSH.strict,
|
|
315
|
+
ci: policyValidation.stages.CI.strict,
|
|
316
|
+
},
|
|
317
|
+
enterprise_warn_as_block_env: warnAsBlock,
|
|
318
|
+
evidence,
|
|
319
|
+
git: {
|
|
320
|
+
current_branch: branch,
|
|
321
|
+
on_protected_branch_hint: onProtected,
|
|
322
|
+
},
|
|
323
|
+
contract_surface: surface,
|
|
324
|
+
tracking,
|
|
325
|
+
attention_codes: attention,
|
|
326
|
+
governance_effective: governanceEffective,
|
|
327
|
+
agent_bootstrap_hints: buildHints(surface, branch, onProtected, tracking),
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export const buildGovernanceObservationSummaryLines = (
|
|
332
|
+
snapshot: GovernanceObservationSnapshot
|
|
333
|
+
): string[] => {
|
|
334
|
+
const lines = [
|
|
335
|
+
`Contract: AGENTS=${snapshot.contract_surface.agents_md ? 'yes' : 'no'} skills.lock=${snapshot.contract_surface.skills_lock_json ? 'yes' : 'no'} skills.sources=${snapshot.contract_surface.skills_sources_json ? 'yes' : 'no'} vendor/skills=${snapshot.contract_surface.vendor_skills_dir ? 'yes' : 'no'} adapter=${snapshot.contract_surface.pumuki_adapter_json ? 'yes' : 'no'}`,
|
|
336
|
+
`Governance: ${snapshot.governance_effective.toUpperCase()}`,
|
|
337
|
+
`Platforms: ${snapshot.platform_bundles_effective?.join(', ') || 'none-detected'}`,
|
|
338
|
+
`GitFlow: branch=${snapshot.git.current_branch ?? 'unknown'} protected_hint=${snapshot.git.on_protected_branch_hint ? 'yes' : 'no'}`,
|
|
339
|
+
`SDD: env=${snapshot.sdd.experimental_raw ?? '(unset)'} effective=${snapshot.sdd.effective_mode} session_active=${snapshot.sdd_session.active} session_valid=${snapshot.sdd_session.valid} change=${snapshot.sdd_session.change_id ?? 'none'}`,
|
|
340
|
+
`Evidence: readable=${snapshot.evidence.readable} stage=${snapshot.evidence.snapshot_stage ?? 'n/a'} outcome=${snapshot.evidence.snapshot_outcome ?? 'n/a'} ai_gate=${snapshot.evidence.ai_gate_status ?? 'n/a'} findings=${snapshot.evidence.findings_count ?? 'n/a'}`,
|
|
341
|
+
`Pre-write: mode=${snapshot.pre_write_effective?.mode ?? 'unknown'} blocking=${snapshot.pre_write_effective?.blocking ? 'yes' : 'no'} strict_policy=${snapshot.pre_write_effective?.strict_policy ? 'yes' : 'no'} source=${snapshot.pre_write_effective?.source ?? 'unknown'}`,
|
|
342
|
+
];
|
|
343
|
+
if (snapshot.attention_codes.length > 0) {
|
|
344
|
+
lines.push(`Attention: ${snapshot.attention_codes.join(', ')}`);
|
|
345
|
+
}
|
|
346
|
+
if (snapshot.tracking.enforced && snapshot.tracking.single_in_progress_valid === false) {
|
|
347
|
+
const actionableContext = formatTrackingActionableContext(snapshot.tracking);
|
|
348
|
+
lines.push(
|
|
349
|
+
`Tracking: canonical=${snapshot.tracking.canonical_path ?? 'unknown'} in_progress_count=${snapshot.tracking.in_progress_count ?? 'n/a'}${actionableContext ? ` ${actionableContext}` : ''}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return lines;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const printGovernanceObservationHuman = (snapshot: GovernanceObservationSnapshot): void => {
|
|
356
|
+
writeInfo('[pumuki] governance truth (S1 / governance console baseline):');
|
|
357
|
+
for (const line of buildGovernanceObservationSummaryLines(snapshot)) {
|
|
358
|
+
writeInfo(`[pumuki] ${line}`);
|
|
359
|
+
}
|
|
360
|
+
for (const hint of snapshot.evidence.human_summary_preview) {
|
|
361
|
+
writeInfo(`[pumuki] evidence hint: ${hint}`);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const doctorGovernanceIsBlocking = (snapshot: GovernanceObservationSnapshot): boolean =>
|
|
366
|
+
snapshot.governance_effective === 'blocked';
|
|
367
|
+
|
|
368
|
+
export const doctorGovernanceNeedsAttention = (snapshot: GovernanceObservationSnapshot): boolean =>
|
|
369
|
+
snapshot.governance_effective !== 'green';
|
|
@@ -49,15 +49,126 @@ const hasPathExecutionHazard = (repoRoot?: string): boolean =>
|
|
|
49
49
|
repoRoot.trim().length > 0 &&
|
|
50
50
|
repoRoot.includes(delimiter);
|
|
51
51
|
|
|
52
|
+
type ConsumerNodeRuntimeSpec = {
|
|
53
|
+
version: string;
|
|
54
|
+
source: 'volta' | '.nvmrc' | 'package.engines';
|
|
55
|
+
commandPrefix: 'volta' | 'nvm';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const normalizeNodeVersionToken = (value: string): string =>
|
|
59
|
+
value.trim().replace(/^node@/i, '').replace(/^v/i, '');
|
|
60
|
+
|
|
61
|
+
const extractNodeVersionToken = (value: string): string | null => {
|
|
62
|
+
const normalized = normalizeNodeVersionToken(value);
|
|
63
|
+
const exactVersion = normalized.match(/\d+\.\d+\.\d+/)?.[0];
|
|
64
|
+
if (exactVersion) {
|
|
65
|
+
return exactVersion;
|
|
66
|
+
}
|
|
67
|
+
const majorOnly = normalized.match(/^\d+$/)?.[0];
|
|
68
|
+
return majorOnly ?? null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
72
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
73
|
+
|
|
74
|
+
const readNestedString = (
|
|
75
|
+
source: Record<string, unknown>,
|
|
76
|
+
path: ReadonlyArray<string>
|
|
77
|
+
): string | undefined => {
|
|
78
|
+
let cursor: unknown = source;
|
|
79
|
+
for (const segment of path) {
|
|
80
|
+
if (!isRecord(cursor)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
cursor = cursor[segment];
|
|
84
|
+
}
|
|
85
|
+
return typeof cursor === 'string' && cursor.trim().length > 0 ? cursor.trim() : undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const readConsumerNodeRuntimeSpec = (repoRoot?: string): ConsumerNodeRuntimeSpec | null => {
|
|
89
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim().length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const packageJsonPath = join(repoRoot, 'package.json');
|
|
94
|
+
if (existsSync(packageJsonPath)) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as unknown;
|
|
97
|
+
if (isRecord(parsed)) {
|
|
98
|
+
const voltaNode = readNestedString(parsed, ['volta', 'node']);
|
|
99
|
+
const voltaVersion = voltaNode ? extractNodeVersionToken(voltaNode) : null;
|
|
100
|
+
if (voltaVersion) {
|
|
101
|
+
return {
|
|
102
|
+
version: voltaVersion,
|
|
103
|
+
source: 'volta',
|
|
104
|
+
commandPrefix: 'volta',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const enginesNode = readNestedString(parsed, ['engines', 'node']);
|
|
109
|
+
const enginesVersion = enginesNode ? extractNodeVersionToken(enginesNode) : null;
|
|
110
|
+
if (enginesVersion) {
|
|
111
|
+
return {
|
|
112
|
+
version: enginesVersion,
|
|
113
|
+
source: 'package.engines',
|
|
114
|
+
commandPrefix: 'nvm',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const nvmrcPath = join(repoRoot, '.nvmrc');
|
|
124
|
+
if (existsSync(nvmrcPath)) {
|
|
125
|
+
try {
|
|
126
|
+
const nvmrcVersion = extractNodeVersionToken(readFileSync(nvmrcPath, 'utf8'));
|
|
127
|
+
if (nvmrcVersion) {
|
|
128
|
+
return {
|
|
129
|
+
version: nvmrcVersion,
|
|
130
|
+
source: '.nvmrc',
|
|
131
|
+
commandPrefix: 'nvm',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const buildConsumerNodeAlignmentCommand = (repoRoot?: string): string | null => {
|
|
143
|
+
const runtimeSpec = readConsumerNodeRuntimeSpec(repoRoot);
|
|
144
|
+
if (!runtimeSpec) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const currentNodeVersion = normalizeNodeVersionToken(process.version);
|
|
149
|
+
if (currentNodeVersion === runtimeSpec.version) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (runtimeSpec.commandPrefix === 'volta') {
|
|
154
|
+
return `volta install node@${runtimeSpec.version} && volta pin node@${runtimeSpec.version}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return `nvm install ${runtimeSpec.version} && nvm use ${runtimeSpec.version}`;
|
|
158
|
+
};
|
|
159
|
+
|
|
52
160
|
export const buildLifecycleAlignmentCommand = (
|
|
53
161
|
runtimeVersion: string,
|
|
54
162
|
repoRoot?: string
|
|
55
163
|
): string => {
|
|
164
|
+
const consumerNodeAlignmentCommand = buildConsumerNodeAlignmentCommand(repoRoot);
|
|
56
165
|
const installStep = `npm install --save-exact pumuki@${runtimeVersion}`;
|
|
57
166
|
const runStep = hasPathExecutionHazard(repoRoot)
|
|
58
167
|
? `${buildLocalPumukiCommand()} install`
|
|
59
168
|
: `npx --yes --package pumuki@${runtimeVersion} pumuki install`;
|
|
60
|
-
return
|
|
169
|
+
return [consumerNodeAlignmentCommand, installStep, runStep]
|
|
170
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
171
|
+
.join(' && ');
|
|
61
172
|
};
|
|
62
173
|
|
|
63
174
|
export const resolvePumukiVersionMetadata = (params?: { repoRoot?: string }): PumukiVersionMetadata => {
|
|
@@ -114,11 +225,17 @@ export const buildLifecycleVersionReport = (params?: {
|
|
|
114
225
|
const driftFromConsumerInstalled =
|
|
115
226
|
metadata.consumerInstalledVersion !== null &&
|
|
116
227
|
metadata.consumerInstalledVersion !== metadata.runtimeVersion;
|
|
228
|
+
const consumerNodeSpec = readConsumerNodeRuntimeSpec(params?.repoRoot);
|
|
229
|
+
const consumerNodeVersion = consumerNodeSpec?.version ?? null;
|
|
230
|
+
const currentNodeVersion = normalizeNodeVersionToken(process.version);
|
|
231
|
+
const driftFromConsumerNode =
|
|
232
|
+
consumerNodeVersion !== null && currentNodeVersion !== consumerNodeVersion;
|
|
117
233
|
const driftFromLifecycleInstalled =
|
|
118
234
|
lifecycleInstalled !== null && metadata.resolvedVersion !== lifecycleInstalled;
|
|
119
235
|
const driftTargets = [
|
|
120
236
|
driftFromRuntime ? `runtime=${metadata.runtimeVersion}` : null,
|
|
121
237
|
driftFromConsumerInstalled ? `consumer=${metadata.consumerInstalledVersion}` : null,
|
|
238
|
+
driftFromConsumerNode ? `node=${consumerNodeVersion}` : null,
|
|
122
239
|
driftFromLifecycleInstalled ? `lifecycle=${lifecycleInstalled}` : null,
|
|
123
240
|
].filter((value): value is string => value !== null);
|
|
124
241
|
const pathExecutionHazard = hasPathExecutionHazard(params?.repoRoot);
|
|
@@ -49,10 +49,17 @@ export const writeLifecycleState = (params: {
|
|
|
49
49
|
openSpecManagedArtifacts?: ReadonlyArray<string>;
|
|
50
50
|
}): void => {
|
|
51
51
|
const { git, repoRoot, version } = params;
|
|
52
|
+
const existingInstalledAt = git.localConfig(repoRoot, PUMUKI_CONFIG_KEYS.installedAt);
|
|
52
53
|
git.applyLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.installed, 'true');
|
|
53
54
|
git.applyLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.version, version);
|
|
54
55
|
git.applyLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.hooks, PUMUKI_MANAGED_HOOKS.join(','));
|
|
55
|
-
git.applyLocalConfig(
|
|
56
|
+
git.applyLocalConfig(
|
|
57
|
+
repoRoot,
|
|
58
|
+
PUMUKI_CONFIG_KEYS.installedAt,
|
|
59
|
+
typeof existingInstalledAt === 'string' && existingInstalledAt.trim().length > 0
|
|
60
|
+
? existingInstalledAt
|
|
61
|
+
: new Date().toISOString()
|
|
62
|
+
);
|
|
56
63
|
if (params.openSpecManagedArtifacts) {
|
|
57
64
|
const serialized = serializeManagedArtifacts(params.openSpecManagedArtifacts);
|
|
58
65
|
if (serialized) {
|