iterate-plugin 3.5.6 → 3.6.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.
Files changed (75) hide show
  1. package/README.md +64 -20
  2. package/README.zh-CN.md +53 -17
  3. package/dist/approval-gate.js +143 -23
  4. package/dist/atomic-fs.js +148 -17
  5. package/dist/config-loader.js +509 -34
  6. package/dist/config-write.js +83 -22
  7. package/dist/evidence.js +141 -87
  8. package/dist/file-lock.js +165 -0
  9. package/dist/index.js +54 -28
  10. package/dist/jobs.js +56 -11
  11. package/dist/live.js +52 -13
  12. package/dist/meta-review.js +132 -28
  13. package/dist/method-scope.js +31 -8
  14. package/dist/review-scope.js +32 -9
  15. package/dist/review.js +96 -17
  16. package/dist/session-hooks.js +55 -13
  17. package/dist/skill-prompt.js +417 -67
  18. package/dist/tools/checkpoint.js +85 -19
  19. package/dist/tools/config.js +44 -2
  20. package/dist/tools/context.js +22 -5
  21. package/dist/tools/decision-log.js +122 -27
  22. package/dist/tools/defense-events.js +90 -31
  23. package/dist/tools/defense-store.js +28 -3
  24. package/dist/tools/experience-bank.js +108 -43
  25. package/dist/tools/experience-store.js +54 -4
  26. package/dist/tools/fix.js +347 -119
  27. package/dist/tools/history.js +68 -14
  28. package/dist/tools/present.js +80 -0
  29. package/dist/tools/prune.js +156 -53
  30. package/dist/tools/quality-gate.js +52 -2
  31. package/dist/tools/quality-store.js +17 -1
  32. package/dist/tools/review.js +158 -7
  33. package/dist/tools/transcript.js +173 -36
  34. package/dist/tools/triage.js +67 -12
  35. package/dist/tools/validate.js +115 -24
  36. package/dist/transcript.js +267 -4
  37. package/lib/client.js +945 -181
  38. package/lib/parse.js +760 -57
  39. package/package.json +15 -12
  40. package/src/approval-gate.ts +160 -23
  41. package/src/atomic-fs.ts +152 -17
  42. package/src/client/index.ts +600 -129
  43. package/src/config-loader.ts +542 -35
  44. package/src/config-write.ts +108 -26
  45. package/src/evidence.ts +194 -92
  46. package/src/file-lock.ts +184 -0
  47. package/src/index.ts +53 -29
  48. package/src/jobs.ts +63 -12
  49. package/src/live.ts +48 -11
  50. package/src/meta-review.ts +151 -34
  51. package/src/method-scope.ts +31 -8
  52. package/src/review-scope.ts +33 -10
  53. package/src/review.ts +96 -17
  54. package/src/session-hooks.ts +54 -12
  55. package/src/skill-prompt.ts +417 -67
  56. package/src/tools/checkpoint.ts +81 -19
  57. package/src/tools/config.ts +44 -2
  58. package/src/tools/context.ts +25 -5
  59. package/src/tools/decision-log.ts +127 -22
  60. package/src/tools/defense-events.ts +87 -31
  61. package/src/tools/defense-store.ts +30 -3
  62. package/src/tools/experience-bank.ts +112 -44
  63. package/src/tools/experience-store.ts +54 -5
  64. package/src/tools/fix.ts +258 -20
  65. package/src/tools/history.ts +74 -15
  66. package/src/tools/present.ts +89 -0
  67. package/src/tools/prune.ts +160 -51
  68. package/src/tools/quality-gate.ts +57 -2
  69. package/src/tools/quality-store.ts +18 -1
  70. package/src/tools/review.ts +155 -6
  71. package/src/tools/transcript.ts +178 -36
  72. package/src/tools/triage.ts +70 -13
  73. package/src/tools/validate.ts +138 -32
  74. package/src/transcript.ts +279 -4
  75. package/src/types.ts +29 -1
package/dist/atomic-fs.js CHANGED
@@ -4,15 +4,24 @@
4
4
  * Every JSON/YAML artifact the plugin persists (.iterate/*.json,
5
5
  * iterate.config.yaml) is written through here: content goes to a
6
6
  * uniquely-named temp file in the SAME directory as the target (rename is
7
- * only atomic within one filesystem), then is `renameSync`ed over the
8
- * destination. A crash mid-write can therefore leave a stale temp file
9
- * behind, but never a truncated / half-written state file — readers
10
- * (which all tolerate missing/corrupt files) see either the previous or
11
- * the new content, never a mixture.
7
+ * only atomic within one filesystem), is fsynced to plating, then is
8
+ * `renameSync`ed over the destination, followed by a best-effort directory
9
+ * fsync so the rename itself is durable. A crash mid-write can therefore
10
+ * leave a stale temp file behind, but never a truncated / half-written state
11
+ * file — readers (which all tolerate missing/corrupt files) see either the
12
+ * previous or the new content, never a mixture.
13
+ *
14
+ * Two properties travel with the rename (which REPLACES the target inode):
15
+ * - permissions: the temp is given the existing target's mode before the
16
+ * swap, so rewriting a 0755 script stays 0755 and a 0600 secret never
17
+ * widens to the umask default (0644).
18
+ * - symlinks: when the target is a symlink, the atomic replace runs on the
19
+ * link's REAL target instead — a fix written through a linked source file
20
+ * must reach the real file, and the link must survive as a link.
12
21
  */
13
- import { dirname, join } from 'node:path';
14
- import { renameSync, rmSync, writeFileSync } from 'node:fs';
15
- import { rename, rm, writeFile } from 'node:fs/promises';
22
+ import { basename, dirname, join } from 'node:path';
23
+ import { closeSync, fchmodSync, fsyncSync, lstatSync, openSync, realpathSync, renameSync, rmSync, statSync, writeSync, } from 'node:fs';
24
+ import { lstat, open, realpath, rename, rm, stat } from 'node:fs/promises';
16
25
  /** Best-effort unlink of a leftover temp file (never throws). */
17
26
  function cleanupTemp(tmp) {
18
27
  try {
@@ -35,21 +44,128 @@ async function cleanupTempAsync(tmp) {
35
44
  * Unique temp file path for `filePath`, in the same directory, following the
36
45
  * prunable `.<basename>.tmp-<pid>-<random>` convention (see prune.ts
37
46
  * `isPrunableTemp`). The pid+random suffix makes concurrent writers safe.
47
+ * `basename` from node:path handles the platform separator (a hand-rolled
48
+ * `split('/')` breaks on Windows paths).
38
49
  */
39
50
  function tempPathFor(filePath) {
40
- return join(dirname(filePath), `.${filePath.split('/').pop() ?? 'file'}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
51
+ return join(dirname(filePath), `.${basename(filePath)}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
52
+ }
53
+ /**
54
+ * Resolve the path the atomic replace must actually run on. A symlinked
55
+ * target is resolved to its real path: `renameSync(tmp, link)` would swap the
56
+ * LINK for a regular file and the linked-to file would silently keep its old
57
+ * content (a fix that never reaches the real source). When lstat/realpath
58
+ * cannot resolve (missing target, dangling or unreadable link), fall back to
59
+ * writing `filePath` itself — the pre-symlink-aware behavior.
60
+ */
61
+ function resolveAtomicTarget(filePath) {
62
+ try {
63
+ if (lstatSync(filePath).isSymbolicLink())
64
+ return realpathSync(filePath);
65
+ }
66
+ catch {
67
+ // Missing target or unresolvable link — write `filePath` itself.
68
+ }
69
+ return filePath;
70
+ }
71
+ /** Async variant of {@link resolveAtomicTarget}. */
72
+ async function resolveAtomicTargetAsync(filePath) {
73
+ try {
74
+ if ((await lstat(filePath)).isSymbolicLink())
75
+ return await realpath(filePath);
76
+ }
77
+ catch {
78
+ // Missing target or unresolvable link — write `filePath` itself.
79
+ }
80
+ return filePath;
81
+ }
82
+ /**
83
+ * The permission bits to give the temp file: the existing target's mode (so
84
+ * the rename-over swap preserves it), or `null` for a brand-new file — then
85
+ * the default open mode (0o666 & ~umask) applies, exactly like a plain
86
+ * `writeFileSync` of a new file.
87
+ */
88
+ function existingMode(target) {
89
+ try {
90
+ return statSync(target).mode & 0o777;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /** Async variant of {@link existingMode}. */
97
+ async function existingModeAsync(target) {
98
+ try {
99
+ return (await stat(target)).mode & 0o777;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /**
106
+ * Best-effort fsync of the directory holding the final file, so the rename
107
+ * itself survives a crash. Unsupported on some platforms/filesystems — the
108
+ * data is already durable (fsynced on the temp), this only covers the
109
+ * directory entry, so failure is ignored by design.
110
+ */
111
+ function fsyncDirBestEffort(dir) {
112
+ try {
113
+ const fd = openSync(dir, 'r');
114
+ try {
115
+ fsyncSync(fd);
116
+ }
117
+ finally {
118
+ closeSync(fd);
119
+ }
120
+ }
121
+ catch {
122
+ // Directory fsync is unavailable (Windows, some FUSE mounts) — durability
123
+ // of the content does not depend on it.
124
+ }
125
+ }
126
+ /** Async variant of {@link fsyncDirBestEffort}. */
127
+ async function fsyncDirBestEffortAsync(dir) {
128
+ let dh;
129
+ try {
130
+ dh = await open(dir, 'r');
131
+ await dh.sync();
132
+ }
133
+ catch {
134
+ // Directory fsync is unavailable — see fsyncDirBestEffort.
135
+ }
136
+ finally {
137
+ if (dh) {
138
+ await dh.close().catch(() => { });
139
+ }
140
+ }
41
141
  }
42
142
  /**
43
- * Write `data` to `filePath` atomically (temp file + rename, same dir).
143
+ * Write `data` to `filePath` atomically (temp file + fsync + rename, same dir).
44
144
  * Temp files follow the `<name>.tmp-<pid>-<random>` convention also used by
45
- * prune.ts / checkpoint.ts so they are recognizable and prunable.
145
+ * prune.ts / checkpoint.ts so they are recognizable and prunable. The existing
146
+ * target's mode is preserved across the swap and symlinked targets are
147
+ * resolved first (see the header).
46
148
  * Throws on failure (callers own error reporting — they map failures to
47
149
  * structured tool results).
48
150
  */
49
151
  export function writeTextAtomic(filePath, data) {
50
- const tmp = tempPathFor(filePath);
152
+ const target = resolveAtomicTarget(filePath);
153
+ const tmp = tempPathFor(target);
154
+ const mode = existingMode(target);
51
155
  try {
52
- writeFileSync(tmp, data, 'utf-8');
156
+ // fd-based write so the mode can be pinned and the CONTENT fsynced before
157
+ // the rename — a crash after rename but before the data hit the disk would
158
+ // otherwise leave a zero-length / torn state file.
159
+ const fd = openSync(tmp, 'w');
160
+ try {
161
+ writeSync(fd, data, null, 'utf-8');
162
+ if (mode !== null)
163
+ fchmodSync(fd, mode);
164
+ fsyncSync(fd);
165
+ }
166
+ finally {
167
+ closeSync(fd);
168
+ }
53
169
  }
54
170
  catch (err) {
55
171
  // The write itself failed (disk full, permission, …): the temp file may
@@ -59,19 +175,33 @@ export function writeTextAtomic(filePath, data) {
59
175
  throw err;
60
176
  }
61
177
  try {
62
- renameSync(tmp, filePath);
178
+ renameSync(tmp, target);
63
179
  }
64
180
  catch (err) {
65
181
  // Best-effort temp cleanup so a failed rename never litters the directory.
66
182
  cleanupTemp(tmp);
67
183
  throw err;
68
184
  }
185
+ fsyncDirBestEffort(dirname(target));
69
186
  }
70
187
  /** Async variant of {@link writeTextAtomic} (same temp naming convention). */
71
188
  export async function writeTextAtomicAsync(filePath, data) {
72
- const tmp = tempPathFor(filePath);
189
+ const target = await resolveAtomicTargetAsync(filePath);
190
+ const tmp = tempPathFor(target);
191
+ const mode = await existingModeAsync(target);
73
192
  try {
74
- await writeFile(tmp, data, 'utf-8');
193
+ // fd-based write: pin the target's mode and fsync the content before the
194
+ // rename (mirrors the sync variant — see writeTextAtomic).
195
+ const fh = await open(tmp, 'w');
196
+ try {
197
+ await fh.writeFile(data, 'utf-8');
198
+ if (mode !== null)
199
+ await fh.chmod(mode);
200
+ await fh.sync();
201
+ }
202
+ finally {
203
+ await fh.close();
204
+ }
75
205
  }
76
206
  catch (err) {
77
207
  // The write itself failed (disk full, permission, …): the temp file may
@@ -81,13 +211,14 @@ export async function writeTextAtomicAsync(filePath, data) {
81
211
  throw err;
82
212
  }
83
213
  try {
84
- await rename(tmp, filePath);
214
+ await rename(tmp, target);
85
215
  }
86
216
  catch (err) {
87
217
  // Best-effort temp cleanup so a failed rename never litters the directory.
88
218
  await cleanupTempAsync(tmp);
89
219
  throw err;
90
220
  }
221
+ await fsyncDirBestEffortAsync(dirname(target));
91
222
  }
92
223
  /** JSON.stringify (2-space indent) + atomic write in one call. */
93
224
  export function writeJsonAtomic(filePath, value) {