valent-pipeline 0.19.15 → 0.19.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.19.15",
3
+ "version": "0.19.16",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -156,6 +156,69 @@ function ensureExcluded(root, worktreeDir) {
156
156
  appendFileSync(excludePath, `${content.endsWith('\n') || content === '' ? '' : '\n'}${line}\n`);
157
157
  }
158
158
 
159
+ // Per-story evidence STATE the harness rewrites every run/retry: the run journal (index.jsonl), the
160
+ // gate-pin ledger (pins.jsonl), the resolved task graph, and the stage-verdict ledger. These are
161
+ // DERIVED records — the board reads them off disk, and the on-disk evidence hash-chain (not git
162
+ // history) is the tamper model — yet commitAll's `git add -A` would TRACK them, and a tracked file
163
+ // that churns on a retry leaves the tree dirty. That dirt refuses the ship merge: both isDirty() AND
164
+ // git's own `merge`/`checkout` see WHOLE-tree dirt — GIT_DIRT_EXCLUDE (evidence.js) only spares the
165
+ // read-side coherence/pin checks, never the merge. Fix = keep them out of git via .git/info/exclude,
166
+ // never a tracked .gitignore edit (which would itself dirty the tree — the same rule ensureExcluded
167
+ // states for the worktree container). One list: a new state-file type is a single line here, not a
168
+ // per-project .gitignore chase. The board still reads them off disk; only their git-history copy goes.
169
+ const STATE_EXCLUDE_PATTERNS = [
170
+ 'stories/*/evidence/index.jsonl',
171
+ 'stories/*/evidence/pins.jsonl',
172
+ 'stories/*/evidence/resolved-graph.json',
173
+ 'stories/*/evidence/stage-results.json',
174
+ ];
175
+
176
+ /**
177
+ * Append the evidence-state ignore patterns to .git/info/exclude (idempotent; shared across linked
178
+ * worktrees via the common git dir). Local-only, so it never dirties the tree the flow keeps clean.
179
+ * Called at the top of commitAll BEFORE `git add -A`, so these files are never newly tracked.
180
+ */
181
+ function ensureStateExcluded(root) {
182
+ let commonDir;
183
+ try {
184
+ commonDir = git(root, ['rev-parse', '--git-common-dir']);
185
+ } catch {
186
+ return; // not a repo / git unavailable — nothing to exclude (commitAll's own callers handle the skip)
187
+ }
188
+ const excludePath = join(isAbsolute(commonDir) ? commonDir : join(root, commonDir), 'info', 'exclude');
189
+ let content = '';
190
+ try {
191
+ content = readFileSync(excludePath, 'utf-8');
192
+ } catch { /* no exclude file yet */ }
193
+ const have = new Set(content.split(/\r?\n/));
194
+ const missing = STATE_EXCLUDE_PATTERNS.filter((p) => !have.has(p));
195
+ if (!missing.length) return;
196
+ mkdirSync(join(excludePath, '..'), { recursive: true });
197
+ appendFileSync(excludePath, `${content.endsWith('\n') || content === '' ? '' : '\n'}${missing.join('\n')}\n`);
198
+ }
199
+
200
+ /**
201
+ * Self-heal projects an older version (or a prior run) already committed these files into: untrack
202
+ * them so they stop dirtying the tree. `git rm --cached` drops them from the index but KEEPS them on
203
+ * disk (the board still reads them). Best-effort over the whole set — a file that can't be untracked
204
+ * (e.g. staged AND modified) is left for the `git add -A` sweep, which still yields a clean tree for
205
+ * the merge. Only call on the commit path (after isDirty), so the staged removals ride the commit
206
+ * that is about to happen rather than producing a surprise commit on an otherwise-clean tree.
207
+ */
208
+ function untrackStateFiles(root) {
209
+ let tracked = [];
210
+ try {
211
+ tracked = git(root, ['ls-files', '--', ...STATE_EXCLUDE_PATTERNS]).split('\n').filter(Boolean);
212
+ } catch {
213
+ return;
214
+ }
215
+ if (!tracked.length) return;
216
+ try {
217
+ git(root, ['rm', '--cached', '--quiet', '--', ...tracked]);
218
+ console.error(`git-flow: untracked ${tracked.length} regenerable evidence-state file(s) (kept on disk) — they churn every run and otherwise dirty the tree, refusing the ship merge.`);
219
+ } catch { /* leave them for the add -A sweep below — a committed churner is still a clean tree */ }
220
+ }
221
+
159
222
  /** Find the registered worktree (path) that has `branch` checked out, or null. */
160
223
  function worktreeForBranch(root, branch) {
161
224
  const out = git(root, ['worktree', 'list', '--porcelain']);
@@ -292,7 +355,9 @@ function commitAll(root, message) {
292
355
  `sweeping now would conclude it and commit unreviewed half-merged code. Resolve or ` +
293
356
  `\`git merge --abort\` first.`);
294
357
  }
358
+ ensureStateExcluded(root); // keep regenerable evidence-state churn out of git BEFORE `add -A` ever sees it
295
359
  if (!isDirty(root)) return { committed: false };
360
+ untrackStateFiles(root); // self-heal any already-tracked churners into this very commit
296
361
  git(root, ['add', '-A']);
297
362
  git(root, ['commit', '-m', message]);
298
363
  return { committed: true, sha: git(root, ['rev-parse', 'HEAD']) };