pi-cicd 0.3.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +34 -40
- package/docs/API.md +61 -0
- package/docs/COMMANDS.md +138 -0
- package/docs/CONFIG.md +123 -0
- package/docs/GUIDE.md +171 -0
- package/docs/PATTERNS.md +49 -0
- package/docs/QUICKSTART.md +99 -0
- package/{dist/index.d.ts → index.ts} +26 -4
- package/install.mjs +34 -0
- package/package.json +21 -21
- package/skills/intelligent-deploy/SKILL.md +229 -0
- package/src/ci/pipeline.ts +130 -0
- package/src/ci/pr-creator.ts +74 -0
- package/src/ci/report.ts +65 -0
- package/src/ci/test-runner.ts +129 -0
- package/src/config.ts +99 -0
- package/src/deploy/canary-deploy.ts +211 -0
- package/src/deploy/landing-queue.ts +222 -0
- package/src/headless/answer-injector.ts +99 -0
- package/src/headless/exit-codes.ts +32 -0
- package/src/headless/idle-detector.ts +76 -0
- package/src/headless/jsonl-stream.ts +90 -0
- package/src/headless/orchestrator.ts +207 -0
- package/{dist/index.js → src/index.ts} +30 -9
- package/src/tools/ci_status.ts +137 -0
- package/src/types.ts +149 -0
- package/src/workflow/deployment-workflow.ts +153 -0
- package/dist/ci/pipeline.d.ts +0 -43
- package/dist/ci/pipeline.d.ts.map +0 -1
- package/dist/ci/pipeline.js +0 -107
- package/dist/ci/pipeline.js.map +0 -1
- package/dist/ci/pr-creator.d.ts +0 -17
- package/dist/ci/pr-creator.d.ts.map +0 -1
- package/dist/ci/pr-creator.js +0 -67
- package/dist/ci/pr-creator.js.map +0 -1
- package/dist/ci/report.d.ts +0 -14
- package/dist/ci/report.d.ts.map +0 -1
- package/dist/ci/report.js +0 -51
- package/dist/ci/report.js.map +0 -1
- package/dist/ci/test-runner.d.ts +0 -10
- package/dist/ci/test-runner.d.ts.map +0 -1
- package/dist/ci/test-runner.js +0 -111
- package/dist/ci/test-runner.js.map +0 -1
- package/dist/config.d.ts +0 -33
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -67
- package/dist/config.js.map +0 -1
- package/dist/deploy/canary-deploy.d.ts +0 -80
- package/dist/deploy/canary-deploy.d.ts.map +0 -1
- package/dist/deploy/canary-deploy.js +0 -145
- package/dist/deploy/canary-deploy.js.map +0 -1
- package/dist/deploy/landing-queue.d.ts +0 -83
- package/dist/deploy/landing-queue.d.ts.map +0 -1
- package/dist/deploy/landing-queue.js +0 -172
- package/dist/deploy/landing-queue.js.map +0 -1
- package/dist/headless/answer-injector.d.ts +0 -27
- package/dist/headless/answer-injector.d.ts.map +0 -1
- package/dist/headless/answer-injector.js +0 -80
- package/dist/headless/answer-injector.js.map +0 -1
- package/dist/headless/exit-codes.d.ts +0 -13
- package/dist/headless/exit-codes.d.ts.map +0 -1
- package/dist/headless/exit-codes.js +0 -29
- package/dist/headless/exit-codes.js.map +0 -1
- package/dist/headless/idle-detector.d.ts +0 -32
- package/dist/headless/idle-detector.d.ts.map +0 -1
- package/dist/headless/idle-detector.js +0 -62
- package/dist/headless/idle-detector.js.map +0 -1
- package/dist/headless/jsonl-stream.d.ts +0 -28
- package/dist/headless/jsonl-stream.d.ts.map +0 -1
- package/dist/headless/jsonl-stream.js +0 -65
- package/dist/headless/jsonl-stream.js.map +0 -1
- package/dist/headless/orchestrator.d.ts +0 -63
- package/dist/headless/orchestrator.d.ts.map +0 -1
- package/dist/headless/orchestrator.js +0 -156
- package/dist/headless/orchestrator.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tools/ci_status.d.ts +0 -40
- package/dist/tools/ci_status.d.ts.map +0 -1
- package/dist/tools/ci_status.js +0 -110
- package/dist/tools/ci_status.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -17
- package/dist/types.js.map +0 -1
- package/dist/workflow/deployment-workflow.d.ts +0 -56
- package/dist/workflow/deployment-workflow.d.ts.map +0 -1
- package/dist/workflow/deployment-workflow.js +0 -95
- package/dist/workflow/deployment-workflow.js.map +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ci — Headless orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Ties together exit codes, answer injection, idle detection, and JSONL
|
|
5
|
+
* streaming into a single execution loop.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExitCode, CIEvent, CIOptions } from "../types.ts";
|
|
9
|
+
import { EXIT_CODES } from "../types.ts";
|
|
10
|
+
import { resolveExitCode } from "./exit-codes.ts";
|
|
11
|
+
import { matchAnswer } from "./answer-injector.ts";
|
|
12
|
+
import type { AnswerEntry } from "../types.ts";
|
|
13
|
+
import { IdleDetector } from "./idle-detector.ts";
|
|
14
|
+
import { CIEventCollector, writeCIEvent } from "./jsonl-stream.ts";
|
|
15
|
+
import type { Writable } from "node:stream";
|
|
16
|
+
|
|
17
|
+
export interface OrchestratorResult {
|
|
18
|
+
exitCode: ExitCode;
|
|
19
|
+
events: CIEvent[];
|
|
20
|
+
durationMs: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OrchestratorHooks {
|
|
24
|
+
/** Called for each step of execution. Return a status string to signal completion. */
|
|
25
|
+
executeStep: (
|
|
26
|
+
prompt: string,
|
|
27
|
+
injectAnswer: (question: string) => string | undefined,
|
|
28
|
+
) => Promise<StepResult>;
|
|
29
|
+
/** Optional writable for JSONL streaming. */
|
|
30
|
+
outputStream?: Writable;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StepResult {
|
|
34
|
+
status: string;
|
|
35
|
+
edits?: { file: string; lines_added: number; lines_removed: number }[];
|
|
36
|
+
tests?: { command: string; passed: number; failed: number }[];
|
|
37
|
+
cost?: { tokens: { input: number; output: number }; cost_usd: number };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RESTART_CONFIG = {
|
|
41
|
+
baseDelayMs: 5_000,
|
|
42
|
+
maxDelayMs: 30_000,
|
|
43
|
+
backoffMultiplier: 2,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class HeadlessOrchestrator {
|
|
47
|
+
private readonly collector = new CIEventCollector();
|
|
48
|
+
private readonly answers: AnswerEntry[];
|
|
49
|
+
private readonly idleTimeoutMs: number;
|
|
50
|
+
private readonly maxRetries: number;
|
|
51
|
+
private readonly hooks: OrchestratorHooks;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
answers: AnswerEntry[],
|
|
55
|
+
options: CIOptions,
|
|
56
|
+
hooks: OrchestratorHooks,
|
|
57
|
+
) {
|
|
58
|
+
this.answers = answers;
|
|
59
|
+
this.idleTimeoutMs = options.idleTimeoutMs ?? 15_000;
|
|
60
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
61
|
+
this.hooks = hooks;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run the orchestrator loop.
|
|
66
|
+
*
|
|
67
|
+
* 1. Emit ci_start
|
|
68
|
+
* 2. Execute steps via the hook, checking for answer injection
|
|
69
|
+
* 3. On idle timeout → retry with exponential backoff
|
|
70
|
+
* 4. Emit ci_end with the resolved exit code
|
|
71
|
+
*/
|
|
72
|
+
async run(prompt: string, mode: "single" | "plan"): Promise<OrchestratorResult> {
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
|
|
75
|
+
const startEvent: CIEvent = {
|
|
76
|
+
type: "ci_start",
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
task: prompt,
|
|
79
|
+
mode,
|
|
80
|
+
};
|
|
81
|
+
this.emit(startEvent);
|
|
82
|
+
|
|
83
|
+
let lastExitCode: ExitCode = EXIT_CODES.ERROR;
|
|
84
|
+
let retries = 0;
|
|
85
|
+
|
|
86
|
+
while (retries <= this.maxRetries) {
|
|
87
|
+
const result = await this.runAttempt(prompt);
|
|
88
|
+
lastExitCode = result;
|
|
89
|
+
|
|
90
|
+
if (lastExitCode === EXIT_CODES.SUCCESS) break;
|
|
91
|
+
if (lastExitCode === EXIT_CODES.BLOCKED || lastExitCode === EXIT_CODES.CANCELLED) break;
|
|
92
|
+
|
|
93
|
+
// Retry on error/timeout
|
|
94
|
+
retries++;
|
|
95
|
+
if (retries <= this.maxRetries) {
|
|
96
|
+
const delay = Math.min(
|
|
97
|
+
RESTART_CONFIG.baseDelayMs *
|
|
98
|
+
Math.pow(RESTART_CONFIG.backoffMultiplier, retries - 1),
|
|
99
|
+
RESTART_CONFIG.maxDelayMs,
|
|
100
|
+
);
|
|
101
|
+
await sleep(delay);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const durationMs = Date.now() - startTime;
|
|
106
|
+
const endEvent: CIEvent = {
|
|
107
|
+
type: "ci_end",
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
exit_code: lastExitCode,
|
|
110
|
+
duration_ms: durationMs,
|
|
111
|
+
};
|
|
112
|
+
this.emit(endEvent);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
exitCode: lastExitCode,
|
|
116
|
+
events: this.collector.all(),
|
|
117
|
+
durationMs,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Single attempt: run with idle detection.
|
|
123
|
+
*/
|
|
124
|
+
private async runAttempt(prompt: string): Promise<ExitCode> {
|
|
125
|
+
return new Promise<ExitCode>((resolve) => {
|
|
126
|
+
let settled = false;
|
|
127
|
+
|
|
128
|
+
const idle = new IdleDetector({
|
|
129
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
130
|
+
onTimeout: () => {
|
|
131
|
+
if (!settled) {
|
|
132
|
+
settled = true;
|
|
133
|
+
resolve(EXIT_CODES.TIMEOUT);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const injectAnswer = (question: string): string | undefined => {
|
|
139
|
+
idle.reset();
|
|
140
|
+
return matchAnswer(this.answers, question);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
idle.start();
|
|
144
|
+
|
|
145
|
+
this.hooks
|
|
146
|
+
.executeStep(prompt, injectAnswer)
|
|
147
|
+
.then((stepResult) => {
|
|
148
|
+
if (!settled) {
|
|
149
|
+
settled = true;
|
|
150
|
+
idle.stop();
|
|
151
|
+
|
|
152
|
+
// Emit detail events
|
|
153
|
+
if (stepResult.edits) {
|
|
154
|
+
for (const edit of stepResult.edits) {
|
|
155
|
+
this.emit({
|
|
156
|
+
type: "ci_edit",
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
file: edit.file,
|
|
159
|
+
lines_added: edit.lines_added,
|
|
160
|
+
lines_removed: edit.lines_removed,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (stepResult.tests) {
|
|
165
|
+
for (const t of stepResult.tests) {
|
|
166
|
+
this.emit({
|
|
167
|
+
type: "ci_test",
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
command: t.command,
|
|
170
|
+
passed: t.passed,
|
|
171
|
+
failed: t.failed,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (stepResult.cost) {
|
|
176
|
+
this.emit({
|
|
177
|
+
type: "ci_cost",
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
tokens: stepResult.cost.tokens,
|
|
180
|
+
cost_usd: stepResult.cost.cost_usd,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resolve(resolveExitCode(stepResult.status));
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.catch(() => {
|
|
188
|
+
if (!settled) {
|
|
189
|
+
settled = true;
|
|
190
|
+
idle.stop();
|
|
191
|
+
resolve(EXIT_CODES.ERROR);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private emit(event: CIEvent): void {
|
|
198
|
+
this.collector.emit(event);
|
|
199
|
+
if (this.hooks.outputStream) {
|
|
200
|
+
writeCIEvent(this.hooks.outputStream, event);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sleep(ms: number): Promise<void> {
|
|
206
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
207
|
+
}
|
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers the /ci status command and CI lifecycle hooks.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
import type { CIEvent, CIOptions, ExitCode } from "./src/types.ts";
|
|
8
|
+
import { EXIT_CODES } from "./src/types.ts";
|
|
9
|
+
import {
|
|
10
|
+
ciStatusHandler,
|
|
11
|
+
createRunTracker,
|
|
12
|
+
clearRuns,
|
|
13
|
+
registerRun,
|
|
14
|
+
type CIRunRecord,
|
|
15
|
+
} from "./src/tools/ci_status.ts";
|
|
16
|
+
import { CIPipeline, type PipelineResult } from "./src/ci/pipeline.ts";
|
|
17
|
+
import { generateReport } from "./src/ci/report.ts";
|
|
18
|
+
|
|
7
19
|
// Re-export for consumers
|
|
8
20
|
export { EXIT_CODES } from "./src/types.ts";
|
|
9
21
|
export { resolveExitCode } from "./src/headless/exit-codes.ts";
|
|
@@ -17,15 +29,24 @@ export { parseTestResults } from "./src/ci/test-runner.ts";
|
|
|
17
29
|
export { generateReport } from "./src/ci/report.ts";
|
|
18
30
|
export { ciStatusHandler, registerRun, clearRuns, createRunTracker } from "./src/tools/ci_status.ts";
|
|
19
31
|
export { loadCiConfig, DEFAULT_CONFIG } from "./src/config.ts";
|
|
32
|
+
export type { PiCiConfig } from "./src/config.ts";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extension API type (minimal — avoids hard dep on pi-coding-agent types).
|
|
36
|
+
*/
|
|
37
|
+
interface ExtensionAPI {
|
|
38
|
+
registerCommand?: (name: string, handler: (args: unknown) => string | Promise<string>) => void;
|
|
39
|
+
on?: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
/**
|
|
21
43
|
* Default export — Pi extension registration.
|
|
22
44
|
*/
|
|
23
|
-
export default function piCiExtension(pi) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
export default function piCiExtension(pi: ExtensionAPI): void {
|
|
46
|
+
// Register /ci status command
|
|
47
|
+
if (pi.registerCommand) {
|
|
48
|
+
pi.registerCommand("ci", (args: unknown) => {
|
|
49
|
+
return ciStatusHandler(args);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
30
52
|
}
|
|
31
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ci — /ci status command handler.
|
|
3
|
+
*
|
|
4
|
+
* Shows the status of the current or last CI run.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CIEventCollector } from "../headless/jsonl-stream.ts";
|
|
8
|
+
import { generateReport } from "../ci/report.ts";
|
|
9
|
+
import type { CIEvent, CIEndEvent, ExitCode } from "../types.ts";
|
|
10
|
+
import { isCIEndEvent } from "../headless/jsonl-stream.ts";
|
|
11
|
+
|
|
12
|
+
export interface CIRunRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
startTime: string;
|
|
15
|
+
events: CIEvent[];
|
|
16
|
+
exitCode?: ExitCode;
|
|
17
|
+
durationMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Simple in-memory registry of CI runs (for the status command).
|
|
22
|
+
*/
|
|
23
|
+
const runRegistry = new Map<string, CIRunRecord>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register a CI run for status lookups.
|
|
27
|
+
*/
|
|
28
|
+
export function registerRun(record: CIRunRecord): void {
|
|
29
|
+
runRegistry.set(record.id, record);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clear all registered runs (useful for testing).
|
|
34
|
+
*/
|
|
35
|
+
export function clearRuns(): void {
|
|
36
|
+
runRegistry.clear();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a specific run by ID (prefix match supported).
|
|
41
|
+
*/
|
|
42
|
+
export function getRun(id: string): CIRunRecord | undefined {
|
|
43
|
+
// Exact match first
|
|
44
|
+
if (runRegistry.has(id)) {
|
|
45
|
+
return runRegistry.get(id);
|
|
46
|
+
}
|
|
47
|
+
// Prefix match
|
|
48
|
+
for (const [key, value] of runRegistry) {
|
|
49
|
+
if (key.startsWith(id)) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Handle the /ci status command.
|
|
58
|
+
*
|
|
59
|
+
* Returns a human-readable status string.
|
|
60
|
+
*/
|
|
61
|
+
export function ciStatusHandler(args: unknown): string {
|
|
62
|
+
const runId = typeof args === "string" ? args : undefined;
|
|
63
|
+
|
|
64
|
+
if (runId) {
|
|
65
|
+
const run = getRun(runId);
|
|
66
|
+
if (!run) {
|
|
67
|
+
return `No CI run found matching: ${runId}`;
|
|
68
|
+
}
|
|
69
|
+
return formatRunStatus(run);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Show all runs
|
|
73
|
+
if (runRegistry.size === 0) {
|
|
74
|
+
return "No CI runs found.";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
for (const run of runRegistry.values()) {
|
|
79
|
+
lines.push(formatRunStatus(run));
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
return lines.join("\n").trimEnd();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatRunStatus(run: CIRunRecord): string {
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
lines.push(`Run: ${run.id}`);
|
|
88
|
+
lines.push(`Started: ${run.startTime}`);
|
|
89
|
+
|
|
90
|
+
const endEvent = run.events.find((e): e is CIEndEvent => isCIEndEvent(e));
|
|
91
|
+
if (endEvent) {
|
|
92
|
+
lines.push(`Exit Code: ${endEvent.exit_code}`);
|
|
93
|
+
lines.push(`Duration: ${(endEvent.duration_ms / 1000).toFixed(1)}s`);
|
|
94
|
+
|
|
95
|
+
const status =
|
|
96
|
+
endEvent.exit_code === 0
|
|
97
|
+
? "SUCCESS"
|
|
98
|
+
: endEvent.exit_code === 10
|
|
99
|
+
? "BLOCKED"
|
|
100
|
+
: endEvent.exit_code === 11
|
|
101
|
+
? "CANCELLED"
|
|
102
|
+
: "ERROR";
|
|
103
|
+
lines.push(`Status: ${status}`);
|
|
104
|
+
} else {
|
|
105
|
+
lines.push("Status: RUNNING");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a CI run tracker that collects events and registers the run.
|
|
113
|
+
*/
|
|
114
|
+
export function createRunTracker(runId: string): {
|
|
115
|
+
collector: CIEventCollector;
|
|
116
|
+
finalize: () => CIRunRecord;
|
|
117
|
+
} {
|
|
118
|
+
const collector = new CIEventCollector();
|
|
119
|
+
const startTime = new Date().toISOString();
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
collector,
|
|
123
|
+
finalize() {
|
|
124
|
+
const events = collector.all();
|
|
125
|
+
const endEvent = events.find((e): e is CIEndEvent => isCIEndEvent(e));
|
|
126
|
+
const record: CIRunRecord = {
|
|
127
|
+
id: runId,
|
|
128
|
+
startTime,
|
|
129
|
+
events,
|
|
130
|
+
exitCode: endEvent?.exit_code,
|
|
131
|
+
durationMs: endEvent?.duration_ms,
|
|
132
|
+
};
|
|
133
|
+
registerRun(record);
|
|
134
|
+
return record;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ci — Shared types for headless CI mode.
|
|
3
|
+
*
|
|
4
|
+
* Exit code contract and CI event types per SPEC.md §2.1 and §5.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Exit codes
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export const EXIT_CODES = {
|
|
12
|
+
SUCCESS: 0,
|
|
13
|
+
ERROR: 1,
|
|
14
|
+
TIMEOUT: 1,
|
|
15
|
+
BLOCKED: 10,
|
|
16
|
+
CANCELLED: 11,
|
|
17
|
+
NEEDS_INPUT: 12,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// CI events (JSONL stream)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type CIEventType =
|
|
27
|
+
| "ci_start"
|
|
28
|
+
| "ci_progress"
|
|
29
|
+
| "ci_edit"
|
|
30
|
+
| "ci_test"
|
|
31
|
+
| "ci_cost"
|
|
32
|
+
| "ci_end";
|
|
33
|
+
|
|
34
|
+
export interface CIEventBase {
|
|
35
|
+
type: CIEventType;
|
|
36
|
+
timestamp: string; // ISO 8601
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CIStartEvent extends CIEventBase {
|
|
40
|
+
type: "ci_start";
|
|
41
|
+
task: string;
|
|
42
|
+
mode: "single" | "plan";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type CIRunPhase =
|
|
46
|
+
| "exploring"
|
|
47
|
+
| "planning"
|
|
48
|
+
| "implementing"
|
|
49
|
+
| "verifying"
|
|
50
|
+
| "reviewing";
|
|
51
|
+
|
|
52
|
+
export interface CIProgressEvent extends CIEventBase {
|
|
53
|
+
type: "ci_progress";
|
|
54
|
+
phase: CIRunPhase;
|
|
55
|
+
files?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CIEditEvent extends CIEventBase {
|
|
59
|
+
type: "ci_edit";
|
|
60
|
+
file: string;
|
|
61
|
+
lines_added: number;
|
|
62
|
+
lines_removed: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CITestEvent extends CIEventBase {
|
|
66
|
+
type: "ci_test";
|
|
67
|
+
command: string;
|
|
68
|
+
passed: number;
|
|
69
|
+
failed: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CICostEvent extends CIEventBase {
|
|
73
|
+
type: "ci_cost";
|
|
74
|
+
tokens: { input: number; output: number };
|
|
75
|
+
cost_usd: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CIEndEvent extends CIEventBase {
|
|
79
|
+
type: "ci_end";
|
|
80
|
+
exit_code: ExitCode;
|
|
81
|
+
duration_ms: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type CIEvent =
|
|
85
|
+
| CIStartEvent
|
|
86
|
+
| CIProgressEvent
|
|
87
|
+
| CIEditEvent
|
|
88
|
+
| CITestEvent
|
|
89
|
+
| CICostEvent
|
|
90
|
+
| CIEndEvent;
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Answer injection
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
export interface AnswerEntry {
|
|
97
|
+
match: string;
|
|
98
|
+
answer: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AnswerFile {
|
|
102
|
+
answers: AnswerEntry[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Test results
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export interface TestSummary {
|
|
110
|
+
passed: number;
|
|
111
|
+
failed: number;
|
|
112
|
+
total: number;
|
|
113
|
+
duration_ms: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// PR creation
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export interface PROptions {
|
|
121
|
+
title: string;
|
|
122
|
+
body?: string;
|
|
123
|
+
base?: string;
|
|
124
|
+
head?: string;
|
|
125
|
+
draft?: boolean;
|
|
126
|
+
labels?: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PRResult {
|
|
130
|
+
url: string;
|
|
131
|
+
number: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Pipeline
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
export type CIPipelineMode = "single" | "plan" | "review" | "supervised";
|
|
139
|
+
|
|
140
|
+
export interface CIOptions {
|
|
141
|
+
prompt: string;
|
|
142
|
+
mode: CIPipelineMode;
|
|
143
|
+
answersFile?: string;
|
|
144
|
+
planFile?: string;
|
|
145
|
+
prNumber?: number;
|
|
146
|
+
resume?: string;
|
|
147
|
+
idleTimeoutMs?: number;
|
|
148
|
+
maxRetries?: number;
|
|
149
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Workflow - Pattern from pi-crew workflow-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Declarative deployment workflow definition.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface DeploymentStep {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
action: "validate" | "build" | "test" | "deploy" | "verify" | "rollback";
|
|
11
|
+
environment: "staging" | "production" | "preview";
|
|
12
|
+
dependsOn?: string[];
|
|
13
|
+
parallelGroup?: string;
|
|
14
|
+
timeout?: number; // ms
|
|
15
|
+
retry?: {
|
|
16
|
+
maxAttempts: number;
|
|
17
|
+
backoffMs: number;
|
|
18
|
+
};
|
|
19
|
+
conditions?: {
|
|
20
|
+
onlyIf?: string;
|
|
21
|
+
skipIf?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DeploymentWorkflow {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
version: string;
|
|
29
|
+
steps: DeploymentStep[];
|
|
30
|
+
maxConcurrency?: number;
|
|
31
|
+
rollbackOnFailure?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DeploymentState {
|
|
35
|
+
workflowId: string;
|
|
36
|
+
runId: string;
|
|
37
|
+
status: "pending" | "running" | "completed" | "failed" | "rolled_back";
|
|
38
|
+
currentStep?: string;
|
|
39
|
+
completedSteps: string[];
|
|
40
|
+
failedSteps: string[];
|
|
41
|
+
startedAt: string;
|
|
42
|
+
finishedAt?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a standard deployment workflow
|
|
47
|
+
*/
|
|
48
|
+
export function createDeploymentWorkflow(
|
|
49
|
+
options: {
|
|
50
|
+
name: string;
|
|
51
|
+
environments?: string[];
|
|
52
|
+
includeStaging?: boolean;
|
|
53
|
+
}
|
|
54
|
+
): DeploymentWorkflow {
|
|
55
|
+
const envs = options.environments ?? ["staging", "production"];
|
|
56
|
+
|
|
57
|
+
const steps: DeploymentStep[] = [
|
|
58
|
+
{ id: "validate", name: "Validate", action: "validate", environment: "staging" as const },
|
|
59
|
+
{ id: "build", name: "Build", action: "build", environment: "staging" as const, dependsOn: ["validate"] },
|
|
60
|
+
{ id: "test", name: "Test", action: "test", environment: "staging" as const, dependsOn: ["build"] },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (options.includeStaging !== false) {
|
|
64
|
+
steps.push({
|
|
65
|
+
id: "deploy-staging",
|
|
66
|
+
name: "Deploy to Staging",
|
|
67
|
+
action: "deploy",
|
|
68
|
+
environment: "staging",
|
|
69
|
+
dependsOn: ["test"],
|
|
70
|
+
retry: { maxAttempts: 3, backoffMs: 5000 },
|
|
71
|
+
});
|
|
72
|
+
steps.push({
|
|
73
|
+
id: "verify-staging",
|
|
74
|
+
name: "Verify Staging",
|
|
75
|
+
action: "verify",
|
|
76
|
+
environment: "staging",
|
|
77
|
+
dependsOn: ["deploy-staging"],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (envs.includes("production")) {
|
|
82
|
+
steps.push({
|
|
83
|
+
id: "deploy-production",
|
|
84
|
+
name: "Deploy to Production",
|
|
85
|
+
action: "deploy",
|
|
86
|
+
environment: "production",
|
|
87
|
+
dependsOn: options.includeStaging !== false ? ["verify-staging"] : ["test"],
|
|
88
|
+
retry: { maxAttempts: 3, backoffMs: 10000 },
|
|
89
|
+
conditions: {
|
|
90
|
+
onlyIf: "BRANCH=main",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
name: options.name,
|
|
97
|
+
description: `Deployment workflow for ${options.name}`,
|
|
98
|
+
version: "1.0.0",
|
|
99
|
+
steps,
|
|
100
|
+
maxConcurrency: 1,
|
|
101
|
+
rollbackOnFailure: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Execute a deployment workflow
|
|
107
|
+
*/
|
|
108
|
+
export async function* executeDeploymentWorkflow(
|
|
109
|
+
workflow: DeploymentWorkflow,
|
|
110
|
+
executor: (step: DeploymentStep) => Promise<{ success: boolean; error?: string }>
|
|
111
|
+
): AsyncGenerator<DeploymentState> {
|
|
112
|
+
const state: DeploymentState = {
|
|
113
|
+
workflowId: workflow.name,
|
|
114
|
+
runId: `deploy-${Date.now()}`,
|
|
115
|
+
status: "pending",
|
|
116
|
+
completedSteps: [],
|
|
117
|
+
failedSteps: [],
|
|
118
|
+
startedAt: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
yield state;
|
|
122
|
+
|
|
123
|
+
const stepMap = new Map(workflow.steps.map(s => [s.id, s]));
|
|
124
|
+
const completed = new Set<string>();
|
|
125
|
+
|
|
126
|
+
for (const step of workflow.steps) {
|
|
127
|
+
// Check dependencies
|
|
128
|
+
if (step.dependsOn?.some(dep => !completed.has(dep))) {
|
|
129
|
+
continue; // Dependencies not met
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
state.status = "running";
|
|
133
|
+
state.currentStep = step.id;
|
|
134
|
+
yield state;
|
|
135
|
+
|
|
136
|
+
const result = await executor(step);
|
|
137
|
+
|
|
138
|
+
if (result.success) {
|
|
139
|
+
completed.add(step.id);
|
|
140
|
+
state.completedSteps.push(step.id);
|
|
141
|
+
} else {
|
|
142
|
+
state.failedSteps.push(step.id);
|
|
143
|
+
state.status = "failed";
|
|
144
|
+
state.finishedAt = new Date().toISOString();
|
|
145
|
+
yield state;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
state.status = "completed";
|
|
151
|
+
state.finishedAt = new Date().toISOString();
|
|
152
|
+
yield state;
|
|
153
|
+
}
|