popeye-cli 2.2.0 → 2.7.0
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/dist/adapters/gemini.d.ts +14 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +41 -6
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +14 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +42 -6
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +10 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +44 -5
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +324 -20
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +3 -2
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +21 -6
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +55 -4
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/templates/fullstack.js +1 -1
- package/dist/generators/templates/website-components.js +1 -1
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +4 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +17 -11
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-conversion.js +1 -1
- package/dist/generators/templates/website-conversion.js.map +1 -1
- package/dist/generators/templates/website-landing.js +1 -1
- package/dist/generators/templates/website-landing.js.map +1 -1
- package/dist/generators/templates/website-layout.d.ts +36 -4
- package/dist/generators/templates/website-layout.d.ts.map +1 -1
- package/dist/generators/templates/website-layout.js +466 -23
- package/dist/generators/templates/website-layout.js.map +1 -1
- package/dist/generators/templates/website-pricing.js +1 -1
- package/dist/generators/templates/website-pricing.js.map +1 -1
- package/dist/generators/templates/website-sections.js +1 -1
- package/dist/generators/templates/website-sections.js.map +1 -1
- package/dist/generators/templates/website-seo.d.ts.map +1 -1
- package/dist/generators/templates/website-seo.js +4 -1
- package/dist/generators/templates/website-seo.js.map +1 -1
- package/dist/generators/templates/website.d.ts +1 -1
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +1 -1
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-ai.d.ts +52 -0
- package/dist/generators/website-content-ai.d.ts.map +1 -0
- package/dist/generators/website-content-ai.js +141 -0
- package/dist/generators/website-content-ai.js.map +1 -0
- package/dist/generators/website-content-scanner.d.ts +1 -1
- package/dist/generators/website-content-scanner.d.ts.map +1 -1
- package/dist/generators/website-content-scanner.js +98 -1
- package/dist/generators/website-content-scanner.js.map +1 -1
- package/dist/generators/website-context.d.ts +34 -1
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +131 -9
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +12 -0
- package/dist/generators/website-debug.d.ts.map +1 -1
- package/dist/generators/website-debug.js +16 -0
- package/dist/generators/website-debug.js.map +1 -1
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +26 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/pipeline/auto-recovery.d.ts +56 -0
- package/dist/pipeline/auto-recovery.d.ts.map +1 -0
- package/dist/pipeline/auto-recovery.js +185 -0
- package/dist/pipeline/auto-recovery.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +39 -0
- package/dist/pipeline/change-request.d.ts.map +1 -1
- package/dist/pipeline/change-request.js +40 -1
- package/dist/pipeline/change-request.js.map +1 -1
- package/dist/pipeline/check-runner.d.ts +30 -1
- package/dist/pipeline/check-runner.d.ts.map +1 -1
- package/dist/pipeline/check-runner.js +122 -1
- package/dist/pipeline/check-runner.js.map +1 -1
- package/dist/pipeline/command-resolver.d.ts.map +1 -1
- package/dist/pipeline/command-resolver.js +33 -2
- package/dist/pipeline/command-resolver.js.map +1 -1
- package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
- package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
- package/dist/pipeline/consensus/arbitrator-query.js +70 -0
- package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
- package/dist/pipeline/consensus/consensus-runner.js +809 -35
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
- package/dist/pipeline/cr-lifecycle.d.ts +42 -0
- package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
- package/dist/pipeline/cr-lifecycle.js +89 -0
- package/dist/pipeline/cr-lifecycle.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +1 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -1
- package/dist/pipeline/gate-engine.js +26 -7
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +306 -16
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
- package/dist/pipeline/phases/architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/architecture.js +5 -3
- package/dist/pipeline/phases/architecture.js.map +1 -1
- package/dist/pipeline/phases/audit.d.ts.map +1 -1
- package/dist/pipeline/phases/audit.js +5 -3
- package/dist/pipeline/phases/audit.js.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.js +10 -1
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.js +10 -3
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.js +10 -1
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
- package/dist/pipeline/phases/done.d.ts.map +1 -1
- package/dist/pipeline/phases/done.js +9 -4
- package/dist/pipeline/phases/done.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +7 -3
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/phase-context.d.ts +2 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
- package/dist/pipeline/phases/phase-context.js +3 -1
- package/dist/pipeline/phases/phase-context.js.map +1 -1
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
- package/dist/pipeline/phases/production-gate.js +28 -3
- package/dist/pipeline/phases/production-gate.js.map +1 -1
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
- package/dist/pipeline/phases/qa-validation.js +38 -5
- package/dist/pipeline/phases/qa-validation.js.map +1 -1
- package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
- package/dist/pipeline/phases/recovery-loop.js +200 -6
- package/dist/pipeline/phases/recovery-loop.js.map +1 -1
- package/dist/pipeline/phases/review.d.ts.map +1 -1
- package/dist/pipeline/phases/review.js +58 -28
- package/dist/pipeline/phases/review.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +18 -2
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/phases/stuck.d.ts.map +1 -1
- package/dist/pipeline/phases/stuck.js +10 -0
- package/dist/pipeline/phases/stuck.js.map +1 -1
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
- package/dist/pipeline/repo-snapshot.js +3 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -1
- package/dist/pipeline/role-execution-adapter.d.ts +2 -1
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
- package/dist/pipeline/role-execution-adapter.js +22 -7
- package/dist/pipeline/role-execution-adapter.js.map +1 -1
- package/dist/pipeline/skill-loader.d.ts +19 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -1
- package/dist/pipeline/skill-loader.js +22 -0
- package/dist/pipeline/skill-loader.js.map +1 -1
- package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
- package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
- package/dist/pipeline/skills/coverage-gate.js +143 -0
- package/dist/pipeline/skills/coverage-gate.js.map +1 -0
- package/dist/pipeline/skills/usage-registry.d.ts +48 -0
- package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
- package/dist/pipeline/skills/usage-registry.js +55 -0
- package/dist/pipeline/skills/usage-registry.js.map +1 -0
- package/dist/pipeline/strategy-context.d.ts +20 -0
- package/dist/pipeline/strategy-context.d.ts.map +1 -0
- package/dist/pipeline/strategy-context.js +55 -0
- package/dist/pipeline/strategy-context.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +25 -5
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +4 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +25 -13
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +18 -8
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.js +4 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +104 -18
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.js +17 -1
- package/dist/pipeline/type-defs/packets.js.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +160 -16
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.js +26 -1
- package/dist/pipeline/type-defs/state.js.map +1 -1
- package/dist/shared/text-utils.d.ts +23 -0
- package/dist/shared/text-utils.d.ts.map +1 -0
- package/dist/shared/text-utils.js +66 -0
- package/dist/shared/text-utils.js.map +1 -0
- package/dist/shared/website-strategy-format.d.ts +18 -0
- package/dist/shared/website-strategy-format.d.ts.map +1 -0
- package/dist/shared/website-strategy-format.js +47 -0
- package/dist/shared/website-strategy-format.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +57 -8
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +1 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +1 -1
- package/dist/types/workflow.d.ts +447 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +3 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +6 -3
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +1 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +2 -29
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +3 -2
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +51 -6
- package/src/adapters/grok.ts +51 -6
- package/src/adapters/openai.ts +53 -5
- package/src/cli/commands/create.ts +1 -1
- package/src/cli/interactive.ts +333 -19
- package/src/generators/all.ts +3 -2
- package/src/generators/doc-parser.ts +75 -15
- package/src/generators/templates/fullstack.ts +1 -1
- package/src/generators/templates/website-components.ts +1 -1
- package/src/generators/templates/website-config.ts +23 -11
- package/src/generators/templates/website-conversion.ts +1 -1
- package/src/generators/templates/website-landing.ts +1 -1
- package/src/generators/templates/website-layout.ts +491 -23
- package/src/generators/templates/website-pricing.ts +1 -1
- package/src/generators/templates/website-sections.ts +1 -1
- package/src/generators/templates/website-seo.ts +4 -1
- package/src/generators/templates/website.ts +3 -0
- package/src/generators/website-content-ai.ts +186 -0
- package/src/generators/website-content-scanner.ts +113 -1
- package/src/generators/website-context.ts +151 -12
- package/src/generators/website-debug.ts +26 -0
- package/src/generators/website.ts +28 -3
- package/src/pipeline/auto-recovery.ts +283 -0
- package/src/pipeline/change-request.ts +63 -1
- package/src/pipeline/check-runner.ts +141 -2
- package/src/pipeline/command-resolver.ts +34 -2
- package/src/pipeline/consensus/arbitrator-query.ts +101 -0
- package/src/pipeline/consensus/consensus-runner.ts +1099 -42
- package/src/pipeline/cr-lifecycle.ts +103 -0
- package/src/pipeline/gate-engine.ts +35 -7
- package/src/pipeline/orchestrator.ts +361 -16
- package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
- package/src/pipeline/phases/architecture.ts +6 -3
- package/src/pipeline/phases/audit.ts +6 -3
- package/src/pipeline/phases/consensus-architecture.ts +10 -1
- package/src/pipeline/phases/consensus-master-plan.ts +10 -3
- package/src/pipeline/phases/consensus-role-plans.ts +10 -1
- package/src/pipeline/phases/done.ts +15 -4
- package/src/pipeline/phases/intake.ts +7 -3
- package/src/pipeline/phases/phase-context.ts +6 -1
- package/src/pipeline/phases/production-gate.ts +41 -3
- package/src/pipeline/phases/qa-validation.ts +51 -5
- package/src/pipeline/phases/recovery-loop.ts +229 -7
- package/src/pipeline/phases/review.ts +73 -30
- package/src/pipeline/phases/role-planning.ts +21 -2
- package/src/pipeline/phases/stuck.ts +10 -0
- package/src/pipeline/repo-snapshot.ts +3 -0
- package/src/pipeline/role-execution-adapter.ts +30 -4
- package/src/pipeline/skill-loader.ts +33 -0
- package/src/pipeline/skills/coverage-gate.ts +199 -0
- package/src/pipeline/skills/usage-registry.ts +87 -0
- package/src/pipeline/strategy-context.ts +60 -0
- package/src/pipeline/type-defs/artifacts.ts +4 -0
- package/src/pipeline/type-defs/checks.ts +4 -0
- package/src/pipeline/type-defs/packets.ts +18 -1
- package/src/pipeline/type-defs/state.ts +26 -1
- package/src/shared/text-utils.ts +70 -0
- package/src/shared/website-strategy-format.ts +56 -0
- package/src/state/index.ts +60 -8
- package/src/types/consensus.ts +1 -0
- package/src/types/workflow.ts +6 -0
- package/src/upgrade/handlers.ts +9 -3
- package/src/workflow/consensus.ts +1 -0
- package/src/workflow/website-strategy.ts +2 -36
- package/src/workflow/website-updater.ts +4 -2
- package/tests/adapters/gemini.test.ts +165 -0
- package/tests/adapters/grok.test.ts +137 -0
- package/tests/adapters/openai.test.ts +128 -0
- package/tests/generators/doc-parser.test.ts +88 -9
- package/tests/generators/quality-gate.test.ts +19 -3
- package/tests/generators/website-components.test.ts +34 -0
- package/tests/generators/website-content-ai.test.ts +308 -0
- package/tests/generators/website-content-scanner.test.ts +86 -0
- package/tests/generators/website-context.test.ts +3 -2
- package/tests/integration/smokestack-scaffold.test.ts +385 -0
- package/tests/pipeline/auto-recovery.test.ts +337 -0
- package/tests/pipeline/change-request.test.ts +70 -0
- package/tests/pipeline/command-resolver.test.ts +42 -0
- package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
- package/tests/pipeline/consensus-runner.test.ts +1333 -10
- package/tests/pipeline/consensus-scoring.test.ts +602 -18
- package/tests/pipeline/gate-engine.test.ts +34 -0
- package/tests/pipeline/install-check.test.ts +261 -0
- package/tests/pipeline/orchestrator.test.ts +1506 -15
- package/tests/pipeline/packets/builders.test.ts +29 -6
- package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
- package/tests/pipeline/pipeline-persistence.test.ts +230 -0
- package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
- package/tests/pipeline/role-execution-adapter.test.ts +88 -0
- package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
- package/tests/pipeline/skills/usage-registry.test.ts +114 -0
- package/tests/pipeline/strategy-context.test.ts +148 -0
- package/tests/shared/text-utils.test.ts +155 -0
- package/tests/state/progress-analysis.test.ts +375 -0
- package/tests/upgrade/handlers.test.ts +33 -2
- package/tests/workflow/consensus.test.ts +6 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-recovery via arbitrator strategic guidance (v2.6.0).
|
|
3
|
+
*
|
|
4
|
+
* When the pipeline exhausts its recovery budget, this module consults
|
|
5
|
+
* the arbitrator for a strategic perspective before entering STUCK.
|
|
6
|
+
* The arbitrator sees the full failure history and identifies patterns,
|
|
7
|
+
* not individual failures.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import logging from 'node:console';
|
|
13
|
+
|
|
14
|
+
import type { PipelineState, ArtifactEntry } from './types.js';
|
|
15
|
+
import type { ArtifactManager } from './artifact-manager.js';
|
|
16
|
+
import type { ConsensusConfig } from '../types/consensus.js';
|
|
17
|
+
import { queryProvider } from './consensus/arbitrator-query.js';
|
|
18
|
+
import { getModelForProvider } from './consensus/consensus-runner.js';
|
|
19
|
+
|
|
20
|
+
const logger = logging;
|
|
21
|
+
|
|
22
|
+
// ─── Types ───────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
interface FailureContext {
|
|
25
|
+
failedPhase: string;
|
|
26
|
+
recoveryCount: number;
|
|
27
|
+
maxIterations: number;
|
|
28
|
+
blockers: string[];
|
|
29
|
+
checkFailures: string[];
|
|
30
|
+
transitionSequence: string[];
|
|
31
|
+
rcaSummaries: string[];
|
|
32
|
+
recoveryPlanSummaries: string[];
|
|
33
|
+
sessionGuidance: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AutoRecoveryResult {
|
|
37
|
+
success: boolean;
|
|
38
|
+
guidance: string | null;
|
|
39
|
+
artifact: ArtifactEntry | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface AutoRecoveryOptions {
|
|
43
|
+
pipeline: PipelineState;
|
|
44
|
+
projectDir: string;
|
|
45
|
+
artifactManager: ArtifactManager;
|
|
46
|
+
consensusConfig?: Partial<ConsensusConfig>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── AUTO-RECOVERY GUIDANCE marker ───────────────────────
|
|
50
|
+
|
|
51
|
+
const AUTO_RECOVERY_MARKER = '--- AUTO-RECOVERY GUIDANCE ---';
|
|
52
|
+
|
|
53
|
+
// ─── Failure Context Builder ─────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract structured failure evidence from pipeline state.
|
|
57
|
+
* All file reads are wrapped in try/catch with placeholder text.
|
|
58
|
+
*/
|
|
59
|
+
export function buildFailureContext(
|
|
60
|
+
pipeline: PipelineState,
|
|
61
|
+
projectDir: string,
|
|
62
|
+
): FailureContext {
|
|
63
|
+
const failedPhase = pipeline.failedPhase ?? 'unknown';
|
|
64
|
+
const recoveryCount = pipeline.recoveryCount;
|
|
65
|
+
const maxIterations = pipeline.maxRecoveryIterations;
|
|
66
|
+
|
|
67
|
+
// Gate blockers
|
|
68
|
+
const gateResult = pipeline.failedPhase
|
|
69
|
+
? pipeline.gateResults[pipeline.failedPhase]
|
|
70
|
+
: undefined;
|
|
71
|
+
const blockers = gateResult?.blockers ?? [];
|
|
72
|
+
|
|
73
|
+
// Check failures (command + stderr excerpt)
|
|
74
|
+
const checkResults = pipeline.failedPhase
|
|
75
|
+
? pipeline.gateChecks[pipeline.failedPhase] ?? []
|
|
76
|
+
: [];
|
|
77
|
+
const checkFailures = checkResults
|
|
78
|
+
.filter((c) => c.status === 'fail')
|
|
79
|
+
.map((c) => `${c.check_type}: cmd="${c.command}" stderr="${(c.stderr_summary ?? '').slice(0, 200)}"`)
|
|
80
|
+
.slice(0, 5);
|
|
81
|
+
|
|
82
|
+
// Phase transition sequence — last 10 entries from gateResults
|
|
83
|
+
const transitionSequence = Object.entries(pipeline.gateResults)
|
|
84
|
+
.map(([phase, result]) => {
|
|
85
|
+
const topBlocker = result.blockers?.[0] ?? 'none';
|
|
86
|
+
return `${phase}: ${result.pass ? 'PASS' : 'FAIL'} (${topBlocker})`;
|
|
87
|
+
})
|
|
88
|
+
.slice(-10);
|
|
89
|
+
|
|
90
|
+
// Last 3 RCA report summaries (read text artifacts from disk)
|
|
91
|
+
const rcaArtifacts = pipeline.artifacts
|
|
92
|
+
.filter((a) => a.type === 'rca_report' && a.content_type === 'markdown')
|
|
93
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
94
|
+
.slice(0, 3);
|
|
95
|
+
const rcaSummaries = rcaArtifacts.map((a) => readArtifactSummary(projectDir, a.path, 500));
|
|
96
|
+
|
|
97
|
+
// Last 2 recovery fix plan summaries
|
|
98
|
+
const fixPlanArtifacts = pipeline.artifacts
|
|
99
|
+
.filter((a) => a.type === 'recovery_fix_plan')
|
|
100
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
101
|
+
.slice(0, 2);
|
|
102
|
+
const recoveryPlanSummaries = fixPlanArtifacts.map((a) => readArtifactSummary(projectDir, a.path, 500));
|
|
103
|
+
|
|
104
|
+
const sessionGuidance = pipeline.sessionGuidance ?? '';
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
failedPhase,
|
|
108
|
+
recoveryCount,
|
|
109
|
+
maxIterations,
|
|
110
|
+
blockers,
|
|
111
|
+
checkFailures,
|
|
112
|
+
transitionSequence,
|
|
113
|
+
rcaSummaries,
|
|
114
|
+
recoveryPlanSummaries,
|
|
115
|
+
sessionGuidance,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Prompt Builder ──────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build the strategic recovery prompt for the arbitrator.
|
|
123
|
+
* Focuses on pattern analysis, not individual fixes.
|
|
124
|
+
*/
|
|
125
|
+
export function buildAutoRecoveryPrompt(ctx: FailureContext): string {
|
|
126
|
+
const lines: string[] = [
|
|
127
|
+
'# Strategic Recovery Analysis',
|
|
128
|
+
'',
|
|
129
|
+
`The pipeline failed at ${ctx.failedPhase} after ${ctx.recoveryCount}/${ctx.maxIterations} recovery attempts.`,
|
|
130
|
+
'All tactical fixes have been tried. You need to identify the PATTERN, not the individual failure.',
|
|
131
|
+
'',
|
|
132
|
+
'## Failure Timeline',
|
|
133
|
+
...ctx.transitionSequence.map((s) => `- ${s}`),
|
|
134
|
+
'',
|
|
135
|
+
'## Current Blockers',
|
|
136
|
+
...ctx.blockers.map((b) => `- ${b}`),
|
|
137
|
+
...(ctx.checkFailures.length > 0
|
|
138
|
+
? ['', '## Check Failures', ...ctx.checkFailures.map((f) => `- ${f}`)]
|
|
139
|
+
: []),
|
|
140
|
+
'',
|
|
141
|
+
'## What Was Already Tried',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
if (ctx.rcaSummaries.length > 0) {
|
|
145
|
+
lines.push('### RCA Reports');
|
|
146
|
+
ctx.rcaSummaries.forEach((s, i) => {
|
|
147
|
+
lines.push(`#### RCA ${i + 1}`, s, '');
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (ctx.recoveryPlanSummaries.length > 0) {
|
|
152
|
+
lines.push('### Recovery Fix Plans');
|
|
153
|
+
ctx.recoveryPlanSummaries.forEach((s, i) => {
|
|
154
|
+
lines.push(`#### Fix Plan ${i + 1}`, s, '');
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (ctx.sessionGuidance) {
|
|
159
|
+
lines.push('## Current Guidance', ctx.sessionGuidance.slice(0, 1500), '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines.push(
|
|
163
|
+
'## Your Task',
|
|
164
|
+
'',
|
|
165
|
+
`1. **Root Pattern**: What recurring pattern caused all ${ctx.recoveryCount} attempts to fail?`,
|
|
166
|
+
' Pick the SINGLE most likely root pattern.',
|
|
167
|
+
'',
|
|
168
|
+
'2. **Primary Strategy**: One concrete strategic change that breaks the pattern.',
|
|
169
|
+
' (e.g., "reduce scope to X", "replace Y approach with Z", "remove module W entirely")',
|
|
170
|
+
'',
|
|
171
|
+
'3. **Fallback Strategy**: One alternative if the primary doesn\'t work.',
|
|
172
|
+
'',
|
|
173
|
+
'4. **Stop Doing** (list 3 things): Based on the history, what should the pipeline',
|
|
174
|
+
' STOP trying? These are approaches that have been attempted and failed.',
|
|
175
|
+
'',
|
|
176
|
+
'5. **Concrete Next Steps**: 3-5 high-level steps (not code-level fixes).',
|
|
177
|
+
'',
|
|
178
|
+
'Be decisive. Pick one direction. Do NOT produce another tactical dump.',
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Main Entry Point ────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Attempt auto-recovery by consulting the arbitrator for strategic guidance.
|
|
188
|
+
*
|
|
189
|
+
* @returns Result with success flag, guidance text, and stored artifact
|
|
190
|
+
*/
|
|
191
|
+
export async function attemptAutoRecovery(
|
|
192
|
+
opts: AutoRecoveryOptions,
|
|
193
|
+
): Promise<AutoRecoveryResult> {
|
|
194
|
+
const { pipeline, projectDir, artifactManager, consensusConfig } = opts;
|
|
195
|
+
|
|
196
|
+
// Build failure context and strategic prompt
|
|
197
|
+
const ctx = buildFailureContext(pipeline, projectDir);
|
|
198
|
+
const prompt = buildAutoRecoveryPrompt(ctx);
|
|
199
|
+
|
|
200
|
+
// Resolve arbitrator provider config
|
|
201
|
+
const arbitratorName = consensusConfig?.arbitrator ?? 'gemini';
|
|
202
|
+
const model = getModelForProvider(consensusConfig, arbitratorName);
|
|
203
|
+
const providerConfig = {
|
|
204
|
+
provider: arbitratorName,
|
|
205
|
+
model,
|
|
206
|
+
temperature: 0.3,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
logger.log(
|
|
210
|
+
`[auto-recovery] Querying ${providerConfig.provider}/${providerConfig.model} for strategic guidance`,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Query with 90s timeout
|
|
214
|
+
const raw = await queryProvider(prompt, providerConfig, 90_000);
|
|
215
|
+
|
|
216
|
+
if (!raw) {
|
|
217
|
+
logger.warn('[auto-recovery] Provider returned no response (timeout or error)');
|
|
218
|
+
return { success: false, guidance: null, artifact: null };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Validate minimum response length
|
|
222
|
+
if (raw.trim().length < 50) {
|
|
223
|
+
logger.warn(`[auto-recovery] Response too short (${raw.trim().length} chars), discarding`);
|
|
224
|
+
return { success: false, guidance: null, artifact: null };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Store as artifact
|
|
228
|
+
const artifact = artifactManager.createAndStoreText(
|
|
229
|
+
'auto_recovery_guidance',
|
|
230
|
+
`# Auto-Recovery Strategic Guidance\n\n${raw}`,
|
|
231
|
+
'RECOVERY_LOOP',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Inject into sessionGuidance (idempotent — replaces prior auto-recovery block)
|
|
235
|
+
injectAutoRecoveryGuidance(pipeline, raw);
|
|
236
|
+
|
|
237
|
+
logger.log(`[auto-recovery] Guidance received and injected (${raw.length} chars)`);
|
|
238
|
+
|
|
239
|
+
return { success: true, guidance: raw, artifact };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Guidance Injection ──────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Inject auto-recovery guidance into pipeline.sessionGuidance.
|
|
246
|
+
* Replaces any prior auto-recovery block while preserving base guidance.
|
|
247
|
+
*/
|
|
248
|
+
export function injectAutoRecoveryGuidance(
|
|
249
|
+
pipeline: PipelineState,
|
|
250
|
+
guidance: string,
|
|
251
|
+
): void {
|
|
252
|
+
const existing = pipeline.sessionGuidance ?? '';
|
|
253
|
+
|
|
254
|
+
// Strip prior auto-recovery block if present
|
|
255
|
+
const base = existing.includes(AUTO_RECOVERY_MARKER)
|
|
256
|
+
? existing.slice(0, existing.indexOf(AUTO_RECOVERY_MARKER)).trim()
|
|
257
|
+
: existing;
|
|
258
|
+
|
|
259
|
+
pipeline.sessionGuidance = [
|
|
260
|
+
base,
|
|
261
|
+
'',
|
|
262
|
+
AUTO_RECOVERY_MARKER,
|
|
263
|
+
guidance.slice(0, 3000),
|
|
264
|
+
].join('\n').trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Read artifact content from disk, returning first N chars or placeholder.
|
|
271
|
+
*/
|
|
272
|
+
function readArtifactSummary(projectDir: string, artifactPath: string, maxChars: number): string {
|
|
273
|
+
try {
|
|
274
|
+
const fullPath = join(projectDir, artifactPath);
|
|
275
|
+
if (!existsSync(fullPath)) {
|
|
276
|
+
return `[Could not read: ${artifactPath} — file not found]`;
|
|
277
|
+
}
|
|
278
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
279
|
+
return content.slice(0, maxChars);
|
|
280
|
+
} catch {
|
|
281
|
+
return `[Could not read: ${artifactPath}]`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Each CR routes to the appropriate consensus phase for approval.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
10
|
PipelinePhase,
|
|
@@ -24,6 +24,8 @@ export interface BuildChangeRequestArgs {
|
|
|
24
24
|
affectedArtifacts: ArtifactRef[];
|
|
25
25
|
affectedPhases: PipelinePhase[];
|
|
26
26
|
riskLevel: 'low' | 'medium' | 'high';
|
|
27
|
+
/** Deterministic drift fingerprint for CR deduplication (v2.4.9) */
|
|
28
|
+
driftKey?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -50,6 +52,7 @@ export function buildChangeRequest(args: BuildChangeRequestArgs): ChangeRequest
|
|
|
50
52
|
risk_level: args.riskLevel,
|
|
51
53
|
},
|
|
52
54
|
status: 'proposed',
|
|
55
|
+
drift_key: args.driftKey,
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -117,3 +120,62 @@ export function formatChangeRequest(cr: ChangeRequest): string {
|
|
|
117
120
|
|
|
118
121
|
return lines.join('\n');
|
|
119
122
|
}
|
|
123
|
+
|
|
124
|
+
// ─── Drift Key Dedup (v2.4.9) ────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compute a deterministic drift key for CR deduplication.
|
|
128
|
+
* Same drift (same change type, baseline, changed configs, content hashes)
|
|
129
|
+
* always produces the same key, regardless of input order.
|
|
130
|
+
*
|
|
131
|
+
* Args:
|
|
132
|
+
* changeType: The CR change type (config, scope, etc.).
|
|
133
|
+
* baselineSnapshotId: The artifact_id of the baseline snapshot.
|
|
134
|
+
* changedConfigs: List of changed config file paths.
|
|
135
|
+
* configHashPairs: Array of "path:beforeHash->afterHash" strings.
|
|
136
|
+
*
|
|
137
|
+
* Returns:
|
|
138
|
+
* A 32-char hex string (SHA-256 prefix).
|
|
139
|
+
*/
|
|
140
|
+
export function computeDriftKey(
|
|
141
|
+
changeType: string,
|
|
142
|
+
baselineSnapshotId: string,
|
|
143
|
+
changedConfigs: string[],
|
|
144
|
+
configHashPairs: string[],
|
|
145
|
+
): string {
|
|
146
|
+
const sortedConfigs = [...changedConfigs].sort().join(',');
|
|
147
|
+
const sortedPairs = [...configHashPairs].sort().join(',');
|
|
148
|
+
const input = `${changeType}|${baselineSnapshotId}|${sortedConfigs}|${sortedPairs}`;
|
|
149
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 32);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Pending CR shape from PipelineState */
|
|
153
|
+
interface PendingCR {
|
|
154
|
+
cr_id: string;
|
|
155
|
+
change_type: string;
|
|
156
|
+
target_phase: string;
|
|
157
|
+
status: string;
|
|
158
|
+
drift_key?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check whether a pending CR with the same drift_key already exists.
|
|
163
|
+
* Returns true if any non-rejected CR has the same drift_key (proposed,
|
|
164
|
+
* approved, or resolved CRs all count as "already tracked").
|
|
165
|
+
*
|
|
166
|
+
* Args:
|
|
167
|
+
* pendingCRs: The current pending change requests array (may be undefined).
|
|
168
|
+
* driftKey: The drift key to check.
|
|
169
|
+
*
|
|
170
|
+
* Returns:
|
|
171
|
+
* true if a non-rejected duplicate exists.
|
|
172
|
+
*/
|
|
173
|
+
export function isDuplicateCR(
|
|
174
|
+
pendingCRs: PendingCR[] | undefined,
|
|
175
|
+
driftKey: string,
|
|
176
|
+
): boolean {
|
|
177
|
+
if (!pendingCRs) return false;
|
|
178
|
+
return pendingCRs.some(
|
|
179
|
+
(cr) => cr.drift_key === driftKey && cr.status !== 'rejected',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { exec } from 'node:child_process';
|
|
10
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
11
11
|
import { join, extname } from 'node:path';
|
|
12
12
|
|
|
13
13
|
import type {
|
|
@@ -17,13 +17,16 @@ import type {
|
|
|
17
17
|
RepoSnapshot,
|
|
18
18
|
ArtifactEntry,
|
|
19
19
|
PipelinePhase,
|
|
20
|
+
PipelineState,
|
|
20
21
|
} from './types.js';
|
|
21
22
|
import { ArtifactManager } from './artifact-manager.js';
|
|
23
|
+
import { assertSkillCoverage } from './skills/coverage-gate.js';
|
|
22
24
|
|
|
23
25
|
// ─── Constants ───────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
/** Default timeout per check type in milliseconds */
|
|
26
28
|
const DEFAULT_TIMEOUTS: Record<string, number> = {
|
|
29
|
+
install: 15 * 60 * 1000, // 15 minutes
|
|
27
30
|
build: 20 * 60 * 1000, // 20 minutes
|
|
28
31
|
test: 10 * 60 * 1000, // 10 minutes
|
|
29
32
|
lint: 5 * 60 * 1000, // 5 minutes
|
|
@@ -58,6 +61,7 @@ const PLACEHOLDER_PATTERNS = [
|
|
|
58
61
|
/\btemp\b(?!late)/i, // 'temp' but not 'template'
|
|
59
62
|
/lorem ipsum/i,
|
|
60
63
|
/example\.com/i,
|
|
64
|
+
/coming soon/i,
|
|
61
65
|
];
|
|
62
66
|
|
|
63
67
|
// ─── Command Sanitization ────────────────────────────────
|
|
@@ -198,13 +202,14 @@ export function storeCheckResults(
|
|
|
198
202
|
|
|
199
203
|
function mapCheckTypeToArtifactType(
|
|
200
204
|
checkType: GateCheckType,
|
|
201
|
-
): 'build_check' | 'test_check' | 'lint_check' | 'typecheck_check' | 'placeholder_scan' {
|
|
205
|
+
): 'build_check' | 'test_check' | 'lint_check' | 'typecheck_check' | 'placeholder_scan' | 'install_check' {
|
|
202
206
|
switch (checkType) {
|
|
203
207
|
case 'build': return 'build_check';
|
|
204
208
|
case 'test': return 'test_check';
|
|
205
209
|
case 'lint': return 'lint_check';
|
|
206
210
|
case 'typecheck': return 'typecheck_check';
|
|
207
211
|
case 'placeholder_scan': return 'placeholder_scan';
|
|
212
|
+
case 'install': return 'install_check';
|
|
208
213
|
default: return 'build_check';
|
|
209
214
|
}
|
|
210
215
|
}
|
|
@@ -391,6 +396,41 @@ export async function runStartCheck(
|
|
|
391
396
|
});
|
|
392
397
|
}
|
|
393
398
|
|
|
399
|
+
// ─── Skill Coverage Check (v2.2.1) ──────────────────────
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Run deterministic skill coverage check against pipeline state.
|
|
403
|
+
* No subprocess needed — pure in-memory assertion.
|
|
404
|
+
*
|
|
405
|
+
* Args:
|
|
406
|
+
* pipeline: Current pipeline state with activeRoles and skillUsageEvents.
|
|
407
|
+
* currentPhase: Current pipeline phase for phase-aware deferral (v2.4.5).
|
|
408
|
+
* Omit for strict mode (checks all roles).
|
|
409
|
+
*
|
|
410
|
+
* Returns:
|
|
411
|
+
* GateCheckResult with pass/fail status.
|
|
412
|
+
*/
|
|
413
|
+
export function runSkillCoverageCheck(
|
|
414
|
+
pipeline: PipelineState,
|
|
415
|
+
currentPhase?: PipelinePhase,
|
|
416
|
+
): GateCheckResult {
|
|
417
|
+
const events = pipeline.skillUsageEvents ?? [];
|
|
418
|
+
const result = assertSkillCoverage(pipeline.activeRoles, events, pipeline, currentPhase);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
check_type: 'skill_coverage',
|
|
422
|
+
status: result.pass ? 'pass' : 'fail',
|
|
423
|
+
command: 'skill-coverage-check',
|
|
424
|
+
exit_code: result.pass ? 0 : 1,
|
|
425
|
+
stderr_summary: result.pass
|
|
426
|
+
? `All ${result.covered.length} active roles have skill usage` +
|
|
427
|
+
(result.deferred.length > 0 ? ` (${result.deferred.length} deferred)` : '')
|
|
428
|
+
: `Missing skill usage: ${result.missing.map((m) => `${m.role} (${m.reason})`).join(', ')}`,
|
|
429
|
+
duration_ms: 0,
|
|
430
|
+
timestamp: new Date().toISOString(),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
394
434
|
// ─── Env Check (v1.1 Gap #5) ────────────────────────────
|
|
395
435
|
|
|
396
436
|
/**
|
|
@@ -502,3 +542,102 @@ function parseEnvVarValues(content: string): Record<string, string> {
|
|
|
502
542
|
}
|
|
503
543
|
return vars;
|
|
504
544
|
}
|
|
545
|
+
|
|
546
|
+
// ─── Install Skip Heuristic ─────────────────────────────
|
|
547
|
+
|
|
548
|
+
/** Marker file for install skip heuristic */
|
|
549
|
+
interface InstallMarker {
|
|
550
|
+
lockfileHash: string;
|
|
551
|
+
timestamp: string;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Canonical lockfile names by ecosystem */
|
|
555
|
+
const LOCKFILE_MAP: Record<string, string> = {
|
|
556
|
+
npm: 'package-lock.json',
|
|
557
|
+
pnpm: 'pnpm-lock.yaml',
|
|
558
|
+
yarn: 'yarn.lock',
|
|
559
|
+
bun: 'bun.lockb',
|
|
560
|
+
poetry: 'poetry.lock',
|
|
561
|
+
pip: 'requirements.txt',
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Determine whether install can be skipped based on lockfile hash.
|
|
566
|
+
* Returns true only if the marker matches the current lockfile hash
|
|
567
|
+
* AND the install target directory exists (node_modules or .venv).
|
|
568
|
+
*/
|
|
569
|
+
export function shouldSkipInstall(projectDir: string, snapshot: RepoSnapshot): boolean {
|
|
570
|
+
const markerPath = join(projectDir, '.popeye', 'install-marker.json');
|
|
571
|
+
if (!existsSync(markerPath)) return false;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const marker: InstallMarker = JSON.parse(readFileSync(markerPath, 'utf-8'));
|
|
575
|
+
const currentHash = getCanonicalLockfileHash(snapshot);
|
|
576
|
+
if (!currentHash || marker.lockfileHash !== currentHash) return false;
|
|
577
|
+
|
|
578
|
+
// Verify install target directory exists
|
|
579
|
+
const pm = snapshot.package_manager ?? 'npm';
|
|
580
|
+
const isPython = pm === 'pip' || pm === 'poetry';
|
|
581
|
+
const targetDir = isPython
|
|
582
|
+
? join(projectDir, '.venv')
|
|
583
|
+
: join(projectDir, 'node_modules');
|
|
584
|
+
return existsSync(targetDir);
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Write install marker after successful install.
|
|
592
|
+
* Stores the current lockfile hash for future skip checks.
|
|
593
|
+
*/
|
|
594
|
+
export function writeInstallMarker(projectDir: string, snapshot: RepoSnapshot): void {
|
|
595
|
+
const markerDir = join(projectDir, '.popeye');
|
|
596
|
+
const markerPath = join(markerDir, 'install-marker.json');
|
|
597
|
+
|
|
598
|
+
const lockfileHash = getCanonicalLockfileHash(snapshot);
|
|
599
|
+
if (!lockfileHash) return;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
if (!existsSync(markerDir)) {
|
|
603
|
+
mkdirSync(markerDir, { recursive: true });
|
|
604
|
+
}
|
|
605
|
+
const marker: InstallMarker = {
|
|
606
|
+
lockfileHash,
|
|
607
|
+
timestamp: new Date().toISOString(),
|
|
608
|
+
};
|
|
609
|
+
writeFileSync(markerPath, JSON.stringify(marker, null, 2));
|
|
610
|
+
} catch {
|
|
611
|
+
// Non-fatal — worst case we re-run install next time
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Invalidate install marker so the next check re-runs install.
|
|
617
|
+
* Called on missing-module errors or dependency changes.
|
|
618
|
+
*/
|
|
619
|
+
export function invalidateInstallMarker(projectDir: string): void {
|
|
620
|
+
const markerPath = join(projectDir, '.popeye', 'install-marker.json');
|
|
621
|
+
try {
|
|
622
|
+
if (existsSync(markerPath)) unlinkSync(markerPath);
|
|
623
|
+
} catch {
|
|
624
|
+
// Non-fatal
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Get content_hash of the canonical lockfile from the snapshot */
|
|
629
|
+
function getCanonicalLockfileHash(snapshot: RepoSnapshot): string | undefined {
|
|
630
|
+
const pm = snapshot.package_manager ?? 'npm';
|
|
631
|
+
const lockfileName = LOCKFILE_MAP[pm];
|
|
632
|
+
|
|
633
|
+
if (lockfileName) {
|
|
634
|
+
const entry = snapshot.config_files.find((c) => c.type === lockfileName);
|
|
635
|
+
if (entry) return entry.content_hash;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Fallback: use package.json hash if no lockfile found
|
|
639
|
+
const pkgEntry = snapshot.config_files.find((c) => c.type === 'package.json');
|
|
640
|
+
if (pkgEntry) return pkgEntry.content_hash;
|
|
641
|
+
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
@@ -40,14 +40,21 @@ export function resolveCommands(
|
|
|
40
40
|
case 'python':
|
|
41
41
|
resolved = resolvePythonCommands(snapshot);
|
|
42
42
|
break;
|
|
43
|
-
case 'mixed':
|
|
43
|
+
case 'mixed': {
|
|
44
44
|
// Prefer Node commands, augment with Python where Node is missing
|
|
45
45
|
resolved = resolveNodeCommands(pm, scripts, snapshot);
|
|
46
|
+
const pyResolved = resolvePythonCommands(snapshot);
|
|
46
47
|
if (!resolved.test) {
|
|
47
|
-
const pyResolved = resolvePythonCommands(snapshot);
|
|
48
48
|
resolved.test = pyResolved.test;
|
|
49
49
|
}
|
|
50
|
+
// Chain both install commands for mixed projects
|
|
51
|
+
if (resolved.install && pyResolved.install) {
|
|
52
|
+
resolved.install = `${resolved.install} && ${pyResolved.install}`;
|
|
53
|
+
} else if (!resolved.install) {
|
|
54
|
+
resolved.install = pyResolved.install;
|
|
55
|
+
}
|
|
50
56
|
break;
|
|
57
|
+
}
|
|
51
58
|
default:
|
|
52
59
|
resolved = { resolved_from: 'none' };
|
|
53
60
|
}
|
|
@@ -60,6 +67,8 @@ export function resolveCommands(
|
|
|
60
67
|
if (overrides.typecheck) resolved.typecheck = overrides.typecheck;
|
|
61
68
|
if (overrides.migrations) resolved.migrations = overrides.migrations;
|
|
62
69
|
if (overrides.start) resolved.start = overrides.start;
|
|
70
|
+
if (overrides.install) resolved.install = overrides.install;
|
|
71
|
+
if (overrides.install_cwd) resolved.install_cwd = overrides.install_cwd;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
return resolved;
|
|
@@ -120,6 +129,19 @@ function resolveNodeCommands(
|
|
|
120
129
|
resolved.start = `${run} dev`;
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
// Install — always resolve based on package manager
|
|
133
|
+
resolved.install = pm === 'yarn' ? 'yarn install' : `${pm} install`;
|
|
134
|
+
|
|
135
|
+
// Workspace detection: install must run at the workspace root
|
|
136
|
+
const rootPkg = snapshot.config_files.find((c) => c.type === 'package.json');
|
|
137
|
+
const hasWorkspaces = rootPkg?.key_fields?.workspaces !== undefined;
|
|
138
|
+
const hasPnpmWorkspace = snapshot.config_files.some(
|
|
139
|
+
(c) => c.type === 'pnpm-workspace.yaml',
|
|
140
|
+
);
|
|
141
|
+
if (hasWorkspaces || hasPnpmWorkspace) {
|
|
142
|
+
resolved.install_cwd = '.';
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
return resolved;
|
|
124
146
|
}
|
|
125
147
|
|
|
@@ -164,5 +186,15 @@ function resolvePythonCommands(snapshot: RepoSnapshot): ResolvedCommands {
|
|
|
164
186
|
// Start
|
|
165
187
|
resolved.start = 'uvicorn main:app --host 0.0.0.0 --port 8000';
|
|
166
188
|
|
|
189
|
+
// Install — conservative: only well-known safe patterns
|
|
190
|
+
const hasPoetryLock = snapshot.config_files.some((c) => c.type === 'poetry.lock');
|
|
191
|
+
const hasReqs = snapshot.config_files.some((c) => c.type === 'requirements.txt');
|
|
192
|
+
if (hasPoetryLock) {
|
|
193
|
+
resolved.install = 'poetry install';
|
|
194
|
+
} else if (hasReqs) {
|
|
195
|
+
resolved.install = 'pip install -r requirements.txt';
|
|
196
|
+
}
|
|
197
|
+
// No install for pyproject.toml-only (may need build backends, system deps)
|
|
198
|
+
|
|
167
199
|
return resolved;
|
|
168
200
|
}
|