@selesai/code 0.3.2 → 0.3.4
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/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -1
- package/dist/config.js.map +1 -1
- package/dist/core/git-command.d.ts +9 -0
- package/dist/core/git-command.d.ts.map +1 -0
- package/dist/core/git-command.js +49 -0
- package/dist/core/git-command.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/defaults/models.json +18 -0
- package/dist/extensions/handoff-new.test.ts +210 -0
- package/dist/extensions/handoff-new.ts +172 -0
- package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/test-resolve-hook-impl.mjs +7 -0
- package/dist/extensions/test-resolve-hook.mjs +7 -0
- package/dist/extensions/workflow/adapter.ts +162 -6
- package/dist/extensions/workflow/modes/prototype.ts +42 -17
- package/dist/extensions/workflow/modes/quick.ts +40 -17
- package/dist/extensions/workflow/state-machine.ts +117 -15
- package/dist/extensions/workflow/validators.ts +42 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +31 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +23 -8
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/skills/improve-codebase/SKILL.md +2 -2
- package/dist/utils/version-check.d.ts +3 -0
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +19 -0
- package/dist/utils/version-check.js.map +1 -1
- package/docs/workflows.md +21 -24
- package/package.json +5 -5
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
// all pi/fs wiring; this module owns the phase graph + gating + reentrancy.
|
|
7
7
|
|
|
8
8
|
export type Phase = string;
|
|
9
|
+
// ponytail: Plan 4 — semantic gate predicate. The adapter reads the file and
|
|
10
|
+
// hands the content here; the SM stays pure (no fs). ok:false carries a human-
|
|
11
|
+
// readable reason the adapter surfaces to the model so a missing marker does
|
|
12
|
+
// not look like a silent hang.
|
|
13
|
+
export type WorkflowArtifactValidator = (
|
|
14
|
+
content: string,
|
|
15
|
+
) => { ok: true } | { ok: false; reason: string };
|
|
9
16
|
// ponytail: open union. KNOWN_PHASES gives autocomplete for the current set;
|
|
10
17
|
// a mode may introduce new phases (deploy, spec, sign-off) without editing
|
|
11
18
|
// this file. The closed union was already a lie — config.phases let modes
|
|
@@ -24,6 +31,9 @@ export type KnownPhase = (typeof KNOWN_PHASES)[number];
|
|
|
24
31
|
export interface PromptContext {
|
|
25
32
|
artifactDir: string;
|
|
26
33
|
userPrompt: string;
|
|
34
|
+
// ponytail: loopMaxIterations is passed through to the loop-phase prompt so
|
|
35
|
+
// the prompt text can state the cap without hardcoding a duplicate number.
|
|
36
|
+
loopMaxIterations?: number;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
export interface SkipRule {
|
|
@@ -59,6 +69,19 @@ export interface WorkflowConfig {
|
|
|
59
69
|
// ponytail: base path for artifact dirs. Adapter injects a real one; tests
|
|
60
70
|
// inject a fake. The state machine computes the path via deps.artifactPathFor.
|
|
61
71
|
artifactsBase?: string;
|
|
72
|
+
// ponytail: Plan 4 — semantic gates. Per-phase artifact validators.
|
|
73
|
+
// Existence is still required (deps.artifactExists), but for a phase listed
|
|
74
|
+
// here the artifact content must also satisfy its validator. Phases absent
|
|
75
|
+
// from this map stay file-existence-only (grilling/research/reuse).
|
|
76
|
+
artifactValidators?: Partial<Record<Phase, WorkflowArtifactValidator>>;
|
|
77
|
+
// ponytail: Plan 4 — close-artifact validators. Map keyed by closeArtifact
|
|
78
|
+
// filename (e.g. "review.md"). Existence required + validator must pass before
|
|
79
|
+
// the terminal phase can close. closeArtifacts without an entry here stay
|
|
80
|
+
// existence-only.
|
|
81
|
+
closeValidators?: Partial<Record<string, WorkflowArtifactValidator>>;
|
|
82
|
+
// ponytail: Plan 3 — engine-owned loop. Max review rounds before the loop
|
|
83
|
+
// stops and asks the user instead of silently passing. Default 3.
|
|
84
|
+
loopMaxIterations?: number;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
export interface WorkflowDeps {
|
|
@@ -70,6 +93,13 @@ export interface WorkflowDeps {
|
|
|
70
93
|
mkdirArtifactDir: (path: string) => Promise<void>;
|
|
71
94
|
// Compute the artifact dir path from a goal. Pure but injected for tests.
|
|
72
95
|
artifactPathFor: (goal: string, base: string) => string;
|
|
96
|
+
// ponytail: Plan 4 — read an artifact for semantic validation. Returns the
|
|
97
|
+
// file content, or undefined if the file does not exist / is unreadable.
|
|
98
|
+
// The adapter wraps node:fs; tests stub it. Only read when a validator is
|
|
99
|
+
// configured for the phase so existence-only phases never pay a read.
|
|
100
|
+
readArtifact: (phase: Phase, artifactDir: string) => Promise<string | undefined>;
|
|
101
|
+
// ponytail: Plan 4 — read a close artifact by absolute path for validation.
|
|
102
|
+
readFile: (path: string) => Promise<string | undefined>;
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
// ── Persisted entry (data the adapter writes via pi.appendEntry) ──
|
|
@@ -96,11 +126,11 @@ export type WorkflowEffect =
|
|
|
96
126
|
| { kind: "started"; phase: Phase; step: number; prompt: string; entry: WorkflowEntry; footer: FooterState }
|
|
97
127
|
| { kind: "alreadyActive"; phase: Phase; step: number }
|
|
98
128
|
| { kind: "advanced"; phase: Phase; step: number; prompt: string; skipped?: Phase; entry: WorkflowEntry; footer: FooterState }
|
|
99
|
-
| { kind: "blocked"; phase: Phase; missing: string }
|
|
100
|
-
| { kind: "terminalNeedsArtifacts"; phase: Phase; missing: string; promptToQueue: string }
|
|
129
|
+
| { kind: "blocked"; phase: Phase; missing: string; reason?: string }
|
|
130
|
+
| { kind: "terminalNeedsArtifacts"; phase: Phase; missing: string; promptToQueue: string; reason?: string }
|
|
101
131
|
| { kind: "terminalReady"; phase: Phase }
|
|
102
132
|
| { kind: "closed"; phase: Phase; artifactDir: string; entry: WorkflowEntry; footer: FooterState }
|
|
103
|
-
| { kind: "endBlocked"; phase: Phase; missing: string }
|
|
133
|
+
| { kind: "endBlocked"; phase: Phase; missing: string; reason?: string }
|
|
104
134
|
| { kind: "idle"; phase: Phase }
|
|
105
135
|
| { kind: "noOp" };
|
|
106
136
|
|
|
@@ -135,6 +165,21 @@ function validateConfig(config: WorkflowConfig): void {
|
|
|
135
165
|
throw new Error(`WorkflowConfig: skipRule references unknown phase "${r.phase}"`);
|
|
136
166
|
}
|
|
137
167
|
}
|
|
168
|
+
// ponytail: Plan 4 — validator keys must reference real phases / real
|
|
169
|
+
// close artifacts, so a typo in a mode config fails fast at construction.
|
|
170
|
+
for (const p of Object.keys(config.artifactValidators ?? {})) {
|
|
171
|
+
if (!seen.has(p)) {
|
|
172
|
+
throw new Error(`WorkflowConfig: artifactValidators references unknown phase "${p}"`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const file of Object.keys(config.closeValidators ?? {})) {
|
|
176
|
+
if (!config.closeArtifacts.includes(file)) {
|
|
177
|
+
throw new Error(`WorkflowConfig: closeValidators references unknown closeArtifact "${file}"`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (config.loopMaxIterations !== undefined && config.loopMaxIterations < 1) {
|
|
181
|
+
throw new Error("WorkflowConfig: loopMaxIterations must be >= 1");
|
|
182
|
+
}
|
|
138
183
|
}
|
|
139
184
|
|
|
140
185
|
export class WorkflowStateMachine {
|
|
@@ -187,7 +232,7 @@ export class WorkflowStateMachine {
|
|
|
187
232
|
|
|
188
233
|
private promptFor(p: Phase): string {
|
|
189
234
|
const fn = this.config.prompts[p];
|
|
190
|
-
return fn ? fn({ artifactDir: this.artifactDir, userPrompt: this.userPrompt }) : "";
|
|
235
|
+
return fn ? fn({ artifactDir: this.artifactDir, userPrompt: this.userPrompt, loopMaxIterations: this.config.loopMaxIterations }) : "";
|
|
191
236
|
}
|
|
192
237
|
|
|
193
238
|
private entryFor(p: Phase, done: boolean): WorkflowEntry {
|
|
@@ -219,10 +264,37 @@ export class WorkflowStateMachine {
|
|
|
219
264
|
return this.config.phases[i + 1];
|
|
220
265
|
}
|
|
221
266
|
|
|
222
|
-
private async
|
|
267
|
+
private async artifactSatisfiedFor(
|
|
268
|
+
p: Phase,
|
|
269
|
+
deps: WorkflowDeps,
|
|
270
|
+
): Promise<{ exists: boolean; reason?: string }> {
|
|
223
271
|
const file = this.config.phaseArtifacts[p];
|
|
224
|
-
if (!file) return true;
|
|
225
|
-
|
|
272
|
+
if (!file) return { exists: true };
|
|
273
|
+
if (!(await deps.artifactExists(p, this.artifactDir))) return { exists: false };
|
|
274
|
+
const validator = this.config.artifactValidators?.[p];
|
|
275
|
+
if (!validator) return { exists: true };
|
|
276
|
+
const content = await deps.readArtifact(p, this.artifactDir);
|
|
277
|
+
if (content === undefined) return { exists: false };
|
|
278
|
+
const res = validator(content);
|
|
279
|
+
if (res.ok) return { exists: true };
|
|
280
|
+
return { exists: true, reason: res.reason };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ponytail: Plan 4 — validate a close artifact (existence + optional
|
|
284
|
+
// semantic gate). Returns {exists, reason}. Used by terminal + end().
|
|
285
|
+
private async closeSatisfiedFor(
|
|
286
|
+
file: string,
|
|
287
|
+
deps: WorkflowDeps,
|
|
288
|
+
): Promise<{ exists: boolean; reason?: string }> {
|
|
289
|
+
const path = `${this.artifactDir}/${file}`;
|
|
290
|
+
if (!(await deps.fileExists(path))) return { exists: false };
|
|
291
|
+
const validator = this.config.closeValidators?.[file];
|
|
292
|
+
if (!validator) return { exists: true };
|
|
293
|
+
const content = await deps.readFile(path);
|
|
294
|
+
if (content === undefined) return { exists: false };
|
|
295
|
+
const res = validator(content);
|
|
296
|
+
if (res.ok) return { exists: true };
|
|
297
|
+
return { exists: true, reason: res.reason };
|
|
226
298
|
}
|
|
227
299
|
|
|
228
300
|
private setPhase(p: Phase): void {
|
|
@@ -234,15 +306,24 @@ export class WorkflowStateMachine {
|
|
|
234
306
|
// Pure given deps. Returns the Effect; does NOT mutate if blocked/idle.
|
|
235
307
|
private async advancePhase(deps: WorkflowDeps, currentPhaseSatisfied = false): Promise<WorkflowEffect> {
|
|
236
308
|
if (!this.active) return { kind: "idle", phase: this.phase };
|
|
237
|
-
if (!currentPhaseSatisfied
|
|
238
|
-
const
|
|
239
|
-
|
|
309
|
+
if (!currentPhaseSatisfied) {
|
|
310
|
+
const sat = await this.artifactSatisfiedFor(this.phase, deps);
|
|
311
|
+
if (!sat.exists) {
|
|
312
|
+
const missing = this.config.phaseArtifacts[this.phase]!;
|
|
313
|
+
return { kind: "blocked", phase: this.phase, missing };
|
|
314
|
+
}
|
|
315
|
+
if (sat.reason) {
|
|
316
|
+
const missing = this.config.phaseArtifacts[this.phase]!;
|
|
317
|
+
return { kind: "blocked", phase: this.phase, missing, reason: sat.reason };
|
|
318
|
+
}
|
|
240
319
|
}
|
|
241
320
|
const next = this.nextPhase(this.phase);
|
|
242
321
|
if (!next) {
|
|
243
|
-
// ponytail: terminal. Need all closeArtifacts
|
|
322
|
+
// ponytail: terminal. Need all closeArtifacts satisfied (existence +
|
|
323
|
+
// optional semantic gate) before end() can close.
|
|
244
324
|
for (const file of this.config.closeArtifacts) {
|
|
245
|
-
|
|
325
|
+
const sat = await this.closeSatisfiedFor(file, deps);
|
|
326
|
+
if (!sat.exists) {
|
|
246
327
|
return {
|
|
247
328
|
kind: "terminalNeedsArtifacts",
|
|
248
329
|
phase: this.phase,
|
|
@@ -250,6 +331,15 @@ export class WorkflowStateMachine {
|
|
|
250
331
|
promptToQueue: `Terminal phase still needs ${this.artifactDir}/${file}. Write it, then the workflow closes automatically.`,
|
|
251
332
|
};
|
|
252
333
|
}
|
|
334
|
+
if (sat.reason) {
|
|
335
|
+
return {
|
|
336
|
+
kind: "terminalNeedsArtifacts",
|
|
337
|
+
phase: this.phase,
|
|
338
|
+
missing: file,
|
|
339
|
+
reason: sat.reason,
|
|
340
|
+
promptToQueue: `Terminal artifact ${this.artifactDir}/${file} is incomplete: ${sat.reason}. Fix it, then the workflow closes automatically.`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
253
343
|
}
|
|
254
344
|
return { kind: "terminalReady", phase: this.phase };
|
|
255
345
|
}
|
|
@@ -315,9 +405,13 @@ export class WorkflowStateMachine {
|
|
|
315
405
|
return { kind: "endBlocked", phase: this.phase, missing: `not at terminal (${last})` };
|
|
316
406
|
}
|
|
317
407
|
for (const file of this.config.closeArtifacts) {
|
|
318
|
-
|
|
408
|
+
const sat = await this.closeSatisfiedFor(file, deps);
|
|
409
|
+
if (!sat.exists) {
|
|
319
410
|
return { kind: "endBlocked", phase: this.phase, missing: file };
|
|
320
411
|
}
|
|
412
|
+
if (sat.reason) {
|
|
413
|
+
return { kind: "endBlocked", phase: this.phase, missing: file, reason: sat.reason };
|
|
414
|
+
}
|
|
321
415
|
}
|
|
322
416
|
this.active = false;
|
|
323
417
|
const entry = this.entryFor(this.phase, true);
|
|
@@ -341,8 +435,16 @@ export class WorkflowStateMachine {
|
|
|
341
435
|
// Acquire synchronously before any await so a concurrent call sees advancing=true.
|
|
342
436
|
this.advancing = true;
|
|
343
437
|
try {
|
|
344
|
-
//
|
|
345
|
-
|
|
438
|
+
// ponytail: Plan 4 — distinguish "not yet written" (keep waiting, noOp)
|
|
439
|
+
// from "written but semantically invalid" (block + surface reason so the
|
|
440
|
+
// model learns which marker is missing instead of stalling silently).
|
|
441
|
+
const sat = await this.artifactSatisfiedFor(this.phase, deps);
|
|
442
|
+
if (!sat.exists) return { kind: "noOp" };
|
|
443
|
+
if (sat.reason) {
|
|
444
|
+
this.autoArmed = false;
|
|
445
|
+
const missing = this.config.phaseArtifacts[this.phase]!;
|
|
446
|
+
return { kind: "blocked", phase: this.phase, missing, reason: sat.reason };
|
|
447
|
+
}
|
|
346
448
|
this.autoArmed = false;
|
|
347
449
|
const out = await this.advancePhase(deps, true);
|
|
348
450
|
if (out.kind === "terminalReady") {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ponytail: Plan 4 — semantic gates. One marker substring check per critical
|
|
2
|
+
// artifact. No JSON, no schema framework — the smallest machine-readable
|
|
3
|
+
// contract that stops an empty/wrong file from silently advancing a phase.
|
|
4
|
+
// Each validator returns {ok:false,reason} so the adapter can surface the
|
|
5
|
+
// exact problem to the model instead of a generic "blocked".
|
|
6
|
+
|
|
7
|
+
export type WorkflowArtifactValidator = (
|
|
8
|
+
content: string,
|
|
9
|
+
) => { ok: true } | { ok: false; reason: string };
|
|
10
|
+
|
|
11
|
+
// ponytail: shared marker factory. First occurrence wins; case-insensitive on
|
|
12
|
+
// the status word; trailing whitespace allowed. Returns ok:false (not throw)
|
|
13
|
+
// when the marker is absent so the workflow blocks cleanly instead of crashing.
|
|
14
|
+
function markerValidator(marker: string, statusWord: string): WorkflowArtifactValidator {
|
|
15
|
+
const re = new RegExp(
|
|
16
|
+
`${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:\\s*${statusWord}\\b`,
|
|
17
|
+
"i",
|
|
18
|
+
);
|
|
19
|
+
return (content) => {
|
|
20
|
+
if (re.test(content)) return { ok: true };
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: `missing required marker \`${marker}: ${statusWord}\` on its own line`,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const planValidator = markerValidator("WORKFLOW_PLAN_STATUS", "ready");
|
|
29
|
+
export const handoffValidator = markerValidator("WORKFLOW_HANDOFF_STATUS", "ready");
|
|
30
|
+
// ponytail: loop-complete.md is engine-written (adapter.ts) with this marker,
|
|
31
|
+
// but a parent/agent-written file must still carry it to advance.
|
|
32
|
+
export const loopCompleteValidator = markerValidator("WORKFLOW_LOOP_STATUS", "clean");
|
|
33
|
+
export const reviewValidator = markerValidator("WORKFLOW_REVIEW_STATUS", "clean");
|
|
34
|
+
|
|
35
|
+
// ponytail: one source of truth for the marker strings so prompts and the
|
|
36
|
+
// engine-written loop-complete.md stay in sync with the validators.
|
|
37
|
+
export const MARKERS = {
|
|
38
|
+
plan: "WORKFLOW_PLAN_STATUS: ready",
|
|
39
|
+
handoff: "WORKFLOW_HANDOFF_STATUS: ready",
|
|
40
|
+
loopComplete: "WORKFLOW_LOOP_STATUS: clean",
|
|
41
|
+
review: "WORKFLOW_REVIEW_STATUS: clean",
|
|
42
|
+
} as const;
|
|
@@ -245,6 +245,7 @@ export declare class InteractiveMode {
|
|
|
245
245
|
private setupKeyHandlers;
|
|
246
246
|
private handleClipboardImagePaste;
|
|
247
247
|
private consumePendingInputImages;
|
|
248
|
+
private handleGitCommand;
|
|
248
249
|
private setupEditorSubmitHandler;
|
|
249
250
|
private subscribeToAgent;
|
|
250
251
|
private handleEvent;
|