iterate-plugin 2.9.1 → 2.9.2

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.
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
2
3
  import { join, resolve, sep } from 'node:path';
3
4
  import yaml from 'js-yaml';
4
5
  /**
@@ -168,9 +169,46 @@ export function validateConfig(config) {
168
169
  */
169
170
  export function resolveProjectRoot(input) {
170
171
  const raw = (input ?? '').trim();
171
- const root = raw ? resolve(raw) : resolve(process.cwd());
172
+ const root = raw ? resolve(raw) : resolve(effectiveCwd());
172
173
  if (!root || root === sep) {
173
174
  return { ok: false, reason: 'Refusing filesystem root as project root.' };
174
175
  }
175
176
  return { ok: true, root };
176
177
  }
178
+ /**
179
+ * Resolve the default working directory for tools invoked without an explicit
180
+ * `path`. Prefers the process cwd, but a daemon-managed web server can start
181
+ * with cwd = `/` (e.g. launchd), which is not a usable project root. In that
182
+ * case fall back to the session workspace encoded in `DSH_SESSION_JSONL`
183
+ * (`…/sessions/<encoded-workspace>/<session-id>/session.jsonl.zstd`), where
184
+ * the workspace directory is `--`-wrapped with `/` → `-` and percent-encoded
185
+ * bytes spelled as `~<hex>` (e.g. `/Volumes/Eng-Dev/iterate-skill` →
186
+ * `--Volumes-Eng-Dev-iterate-skill--`).
187
+ */
188
+ function effectiveCwd() {
189
+ let cwd = '';
190
+ try {
191
+ cwd = process.cwd();
192
+ }
193
+ catch {
194
+ // cwd may be unreadable (deleted dir) — fall through to session workspace
195
+ }
196
+ if (cwd && cwd !== sep && cwd !== homedir())
197
+ return cwd;
198
+ const session = process.env.DSH_SESSION_JSONL;
199
+ if (session) {
200
+ const m = session.match(/\/sessions\/([^/]+)\//);
201
+ const encoded = m ? m[1] : undefined;
202
+ if (encoded && encoded.startsWith('--') && encoded.endsWith('--')) {
203
+ try {
204
+ const decoded = decodeURIComponent(encoded.slice(2, -2).replace(/~/g, '%'));
205
+ if (decoded && decoded.startsWith(sep))
206
+ return decoded;
207
+ }
208
+ catch {
209
+ // malformed encoding — fall through to cwd
210
+ }
211
+ }
212
+ }
213
+ return cwd || sep;
214
+ }
@@ -149,7 +149,7 @@ export function registerCheckpointTool(ctx) {
149
149
  const projectRoot = resolved.root;
150
150
  if (args.operation === 'load') {
151
151
  const checkpoint = readCheckpoint(projectRoot);
152
- return { operation: 'load', ok: true, checkpoint: checkpoint ?? undefined };
152
+ return { operation: 'load', ok: true, checkpoint: checkpoint };
153
153
  }
154
154
  if (args.operation === 'clear') {
155
155
  const existed = existsSync(checkpointPath(projectRoot));
@@ -217,7 +217,7 @@ export function registerStatusTool(ctx) {
217
217
  additionalProperties: false,
218
218
  properties: {
219
219
  ok: { type: 'boolean', required: true },
220
- mode: { type: 'string' },
220
+ mode: { oneOf: [{ type: 'string' }, { type: 'null' }] },
221
221
  currentRound: { type: 'integer' },
222
222
  totalRounds: { type: 'integer' },
223
223
  fixedCount: { type: 'integer' },
@@ -227,7 +227,7 @@ export function registerStatusTool(ctx) {
227
227
  hasCheckpoint: { type: 'boolean' },
228
228
  interrupted: { type: 'boolean', description: 'True when a checkpoint exists, meaning the previous run was interrupted before finishing.' },
229
229
  resumeCount: { type: 'integer', description: 'How many times the current checkpoint has already been resumed.' },
230
- lastUpdated: { type: 'string' },
230
+ lastUpdated: { oneOf: [{ type: 'string' }, { type: 'null' }] },
231
231
  error: { type: 'string' },
232
232
  },
233
233
  },
@@ -258,7 +258,7 @@ export function registerStatusTool(ctx) {
258
258
  });
259
259
  return {
260
260
  ok: true,
261
- mode: status.mode ?? undefined,
261
+ mode: status.mode ?? null,
262
262
  currentRound: status.currentRound,
263
263
  totalRounds: status.totalRounds,
264
264
  fixedCount: status.fixedCount,
@@ -268,7 +268,7 @@ export function registerStatusTool(ctx) {
268
268
  hasCheckpoint: status.hasCheckpoint,
269
269
  interrupted: status.interrupted,
270
270
  resumeCount: status.resumeCount,
271
- lastUpdated: status.lastUpdated ?? undefined,
271
+ lastUpdated: status.lastUpdated ?? null,
272
272
  };
273
273
  },
274
274
  }));
@@ -47,14 +47,14 @@ export function registerConfigTool(ctx) {
47
47
  properties: {
48
48
  found: { type: 'boolean', required: true },
49
49
  valid: { type: 'boolean' },
50
- errors: { type: 'array', items: { type: 'string' } },
50
+ errors: { oneOf: [{ type: 'array', items: { type: 'string' } }, { type: 'null' }] },
51
51
  section: { type: 'string' },
52
52
  data: { type: 'json' },
53
53
  config: { type: 'json' },
54
54
  availableSections: { type: 'array', items: { type: 'string' } },
55
55
  operation: { type: 'string' },
56
56
  ok: { type: 'boolean' },
57
- backupPath: { type: 'string' },
57
+ backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
58
58
  error: { type: 'string' },
59
59
  },
60
60
  },
@@ -100,7 +100,7 @@ export function registerConfigTool(ctx) {
100
100
  operation: 'write',
101
101
  ok: true,
102
102
  found: true,
103
- backupPath: result.backupPath ?? undefined,
103
+ backupPath: result.backupPath ?? null,
104
104
  config: config,
105
105
  };
106
106
  }
@@ -112,7 +112,7 @@ export function registerConfigTool(ctx) {
112
112
  return {
113
113
  found: hasOverride,
114
114
  valid: errors.length === 0,
115
- errors: errors.length > 0 ? errors : undefined,
115
+ errors: errors.length > 0 ? errors : null,
116
116
  section: 'validation_report',
117
117
  };
118
118
  }
@@ -182,7 +182,7 @@ export function registerReviewTool(ctx) {
182
182
  operation: 'aggregate',
183
183
  mode,
184
184
  report: report,
185
- schemaValidation: schemaValidation,
185
+ schemaValidation: (schemaValidation ?? null),
186
186
  };
187
187
  }
188
188
  if (args.operation === 'meta-review') {
@@ -262,7 +262,7 @@ export function registerTriageTool(ctx) {
262
262
  skipped: { type: 'integer' },
263
263
  count: { type: 'integer' },
264
264
  path: { type: 'string' },
265
- backupPath: { type: 'string' },
265
+ backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
266
266
  entries: { type: 'json' },
267
267
  errors: { type: 'array', items: { type: 'string' } },
268
268
  error: { type: 'string' },
@@ -321,7 +321,7 @@ export function registerTriageTool(ctx) {
321
321
  skipped: result.skipped,
322
322
  count: result.count,
323
323
  path: result.configPath,
324
- backupPath: result.backupPath ?? undefined,
324
+ backupPath: result.backupPath ?? null,
325
325
  };
326
326
  }
327
327
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus a dry-run pure-review mode with multi-round convergence and a meta-review that audits the report and emits a final review report.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
2
3
  import { join, resolve, sep } from 'node:path'
3
4
  import yaml from 'js-yaml'
4
5
  import type { IterateConfig } from './types.ts'
@@ -185,9 +186,43 @@ export type ProjectRootResult = { ok: true; root: string } | { ok: false; reason
185
186
  */
186
187
  export function resolveProjectRoot(input?: string): ProjectRootResult {
187
188
  const raw = (input ?? '').trim()
188
- const root = raw ? resolve(raw) : resolve(process.cwd())
189
+ const root = raw ? resolve(raw) : resolve(effectiveCwd())
189
190
  if (!root || root === sep) {
190
191
  return { ok: false, reason: 'Refusing filesystem root as project root.' }
191
192
  }
192
193
  return { ok: true, root }
194
+ }
195
+
196
+ /**
197
+ * Resolve the default working directory for tools invoked without an explicit
198
+ * `path`. Prefers the process cwd, but a daemon-managed web server can start
199
+ * with cwd = `/` (e.g. launchd), which is not a usable project root. In that
200
+ * case fall back to the session workspace encoded in `DSH_SESSION_JSONL`
201
+ * (`…/sessions/<encoded-workspace>/<session-id>/session.jsonl.zstd`), where
202
+ * the workspace directory is `--`-wrapped with `/` → `-` and percent-encoded
203
+ * bytes spelled as `~<hex>` (e.g. `/Volumes/Eng-Dev/iterate-skill` →
204
+ * `--Volumes-Eng-Dev-iterate-skill--`).
205
+ */
206
+ function effectiveCwd(): string {
207
+ let cwd = ''
208
+ try {
209
+ cwd = process.cwd()
210
+ } catch {
211
+ // cwd may be unreadable (deleted dir) — fall through to session workspace
212
+ }
213
+ if (cwd && cwd !== sep && cwd !== homedir()) return cwd
214
+ const session = process.env.DSH_SESSION_JSONL
215
+ if (session) {
216
+ const m = session.match(/\/sessions\/([^/]+)\//)
217
+ const encoded = m ? m[1] : undefined
218
+ if (encoded && encoded.startsWith('--') && encoded.endsWith('--')) {
219
+ try {
220
+ const decoded = decodeURIComponent(encoded.slice(2, -2).replace(/~/g, '%'))
221
+ if (decoded && decoded.startsWith(sep)) return decoded
222
+ } catch {
223
+ // malformed encoding — fall through to cwd
224
+ }
225
+ }
226
+ }
227
+ return cwd || sep
193
228
  }
@@ -173,7 +173,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
173
173
 
174
174
  if (args.operation === 'load') {
175
175
  const checkpoint = readCheckpoint(projectRoot)
176
- return { operation: 'load', ok: true, checkpoint: (checkpoint as unknown as JsonValue | null) ?? undefined }
176
+ return { operation: 'load', ok: true, checkpoint: checkpoint as unknown as JsonValue | null }
177
177
  }
178
178
 
179
179
  if (args.operation === 'clear') {
@@ -245,7 +245,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
245
245
  additionalProperties: false,
246
246
  properties: {
247
247
  ok: { type: 'boolean', required: true },
248
- mode: { type: 'string' },
248
+ mode: { oneOf: [{ type: 'string' }, { type: 'null' }] },
249
249
  currentRound: { type: 'integer' },
250
250
  totalRounds: { type: 'integer' },
251
251
  fixedCount: { type: 'integer' },
@@ -255,7 +255,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
255
255
  hasCheckpoint: { type: 'boolean' },
256
256
  interrupted: { type: 'boolean', description: 'True when a checkpoint exists, meaning the previous run was interrupted before finishing.' },
257
257
  resumeCount: { type: 'integer', description: 'How many times the current checkpoint has already been resumed.' },
258
- lastUpdated: { type: 'string' },
258
+ lastUpdated: { oneOf: [{ type: 'string' }, { type: 'null' }] },
259
259
  error: { type: 'string' },
260
260
  },
261
261
  },
@@ -285,7 +285,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
285
285
  })
286
286
  return {
287
287
  ok: true,
288
- mode: status.mode ?? undefined,
288
+ mode: status.mode ?? null,
289
289
  currentRound: status.currentRound,
290
290
  totalRounds: status.totalRounds,
291
291
  fixedCount: status.fixedCount,
@@ -295,7 +295,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
295
295
  hasCheckpoint: status.hasCheckpoint,
296
296
  interrupted: status.interrupted,
297
297
  resumeCount: status.resumeCount,
298
- lastUpdated: status.lastUpdated ?? undefined,
298
+ lastUpdated: status.lastUpdated ?? null,
299
299
  }
300
300
  },
301
301
  }),
@@ -60,14 +60,14 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
60
60
  properties: {
61
61
  found: { type: 'boolean', required: true },
62
62
  valid: { type: 'boolean' },
63
- errors: { type: 'array', items: { type: 'string' } },
63
+ errors: { oneOf: [{ type: 'array', items: { type: 'string' } }, { type: 'null' }] },
64
64
  section: { type: 'string' },
65
65
  data: { type: 'json' },
66
66
  config: { type: 'json' },
67
67
  availableSections: { type: 'array', items: { type: 'string' } },
68
68
  operation: { type: 'string' },
69
69
  ok: { type: 'boolean' },
70
- backupPath: { type: 'string' },
70
+ backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
71
71
  error: { type: 'string' },
72
72
  },
73
73
  },
@@ -113,7 +113,7 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
113
113
  operation: 'write',
114
114
  ok: true,
115
115
  found: true,
116
- backupPath: result.backupPath ?? undefined,
116
+ backupPath: result.backupPath ?? null,
117
117
  config: config as unknown as JsonValue,
118
118
  }
119
119
  }
@@ -127,7 +127,7 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
127
127
  return {
128
128
  found: hasOverride,
129
129
  valid: errors.length === 0,
130
- errors: errors.length > 0 ? errors : undefined,
130
+ errors: errors.length > 0 ? errors : null,
131
131
  section: 'validation_report',
132
132
  }
133
133
  }
@@ -213,7 +213,7 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
213
213
  operation: 'aggregate',
214
214
  mode,
215
215
  report: report as unknown as JsonValue,
216
- schemaValidation: schemaValidation as unknown as JsonValue | undefined,
216
+ schemaValidation: (schemaValidation ?? null) as unknown as JsonValue | null,
217
217
  }
218
218
  }
219
219
 
@@ -302,7 +302,7 @@ export function registerTriageTool(ctx: { tools: { register: (def: ReturnType<ty
302
302
  skipped: { type: 'integer' },
303
303
  count: { type: 'integer' },
304
304
  path: { type: 'string' },
305
- backupPath: { type: 'string' },
305
+ backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
306
306
  entries: { type: 'json' },
307
307
  errors: { type: 'array', items: { type: 'string' } },
308
308
  error: { type: 'string' },
@@ -363,7 +363,7 @@ export function registerTriageTool(ctx: { tools: { register: (def: ReturnType<ty
363
363
  skipped: result.skipped,
364
364
  count: result.count,
365
365
  path: result.configPath,
366
- backupPath: result.backupPath ?? undefined,
366
+ backupPath: result.backupPath ?? null,
367
367
  }
368
368
  }
369
369