@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.
Files changed (37) hide show
  1. package/dist/config.d.ts +1 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +24 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/git-command.d.ts +9 -0
  6. package/dist/core/git-command.d.ts.map +1 -0
  7. package/dist/core/git-command.js +49 -0
  8. package/dist/core/git-command.js.map +1 -0
  9. package/dist/core/slash-commands.d.ts.map +1 -1
  10. package/dist/core/slash-commands.js +1 -0
  11. package/dist/core/slash-commands.js.map +1 -1
  12. package/dist/defaults/models.json +18 -0
  13. package/dist/extensions/handoff-new.test.ts +210 -0
  14. package/dist/extensions/handoff-new.ts +172 -0
  15. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  16. package/dist/extensions/package.json +1 -0
  17. package/dist/extensions/test-resolve-hook-impl.mjs +7 -0
  18. package/dist/extensions/test-resolve-hook.mjs +7 -0
  19. package/dist/extensions/workflow/adapter.ts +162 -6
  20. package/dist/extensions/workflow/modes/prototype.ts +42 -17
  21. package/dist/extensions/workflow/modes/quick.ts +40 -17
  22. package/dist/extensions/workflow/state-machine.ts +117 -15
  23. package/dist/extensions/workflow/validators.ts +42 -0
  24. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  25. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.js +31 -0
  27. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  28. package/dist/package-manager-cli.d.ts.map +1 -1
  29. package/dist/package-manager-cli.js +23 -8
  30. package/dist/package-manager-cli.js.map +1 -1
  31. package/dist/skills/improve-codebase/SKILL.md +2 -2
  32. package/dist/utils/version-check.d.ts +3 -0
  33. package/dist/utils/version-check.d.ts.map +1 -1
  34. package/dist/utils/version-check.js +19 -0
  35. package/dist/utils/version-check.js.map +1 -1
  36. package/docs/workflows.md +21 -24
  37. 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 artifactExistsFor(p: Phase, deps: WorkflowDeps): Promise<boolean> {
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
- return deps.artifactExists(p, this.artifactDir);
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 && !(await this.artifactExistsFor(this.phase, deps))) {
238
- const missing = this.config.phaseArtifacts[this.phase]!;
239
- return { kind: "blocked", phase: this.phase, missing };
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 before end() can close.
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
- if (!(await deps.fileExists(`${this.artifactDir}/${file}`))) {
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
- if (!(await deps.fileExists(`${this.artifactDir}/${file}`))) {
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
- // Artifact must exist at the workflow-owned path. No rescue/redirect magic.
345
- if (!(await this.artifactExistsFor(this.phase, deps))) return { kind: "noOp" };
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;