@dzhechkov/harness-core 0.3.149 → 0.4.1

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 (107) hide show
  1. package/.dz-manifest.json +399 -55
  2. package/README.md +80 -3
  3. package/dist/agentdb-index.d.ts.map +1 -1
  4. package/dist/agentdb-index.js +10 -2
  5. package/dist/agentdb-index.js.map +1 -1
  6. package/dist/backlog-embed.d.ts +94 -0
  7. package/dist/backlog-embed.d.ts.map +1 -0
  8. package/dist/backlog-embed.js +138 -0
  9. package/dist/backlog-embed.js.map +1 -0
  10. package/dist/backlog.d.ts +180 -7
  11. package/dist/backlog.d.ts.map +1 -1
  12. package/dist/backlog.js +429 -26
  13. package/dist/backlog.js.map +1 -1
  14. package/dist/challenge-panel.d.ts +3 -0
  15. package/dist/challenge-panel.d.ts.map +1 -1
  16. package/dist/challenge-panel.js +3 -0
  17. package/dist/challenge-panel.js.map +1 -1
  18. package/dist/export-holdout.d.ts +149 -0
  19. package/dist/export-holdout.d.ts.map +1 -0
  20. package/dist/export-holdout.js +198 -0
  21. package/dist/export-holdout.js.map +1 -0
  22. package/dist/feature-adr-checkpoints.d.ts +82 -0
  23. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  24. package/dist/feature-adr-checkpoints.js +138 -1
  25. package/dist/feature-adr-checkpoints.js.map +1 -1
  26. package/dist/feature-adr-routing.d.ts +3 -0
  27. package/dist/feature-adr-routing.d.ts.map +1 -1
  28. package/dist/feature-adr-routing.js +3 -0
  29. package/dist/feature-adr-routing.js.map +1 -1
  30. package/dist/guard.d.ts +42 -0
  31. package/dist/guard.d.ts.map +1 -1
  32. package/dist/guard.js +73 -1
  33. package/dist/guard.js.map +1 -1
  34. package/dist/index.d.ts +17 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +28 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/loop-blobs.generated.d.ts +33 -0
  39. package/dist/loop-blobs.generated.d.ts.map +1 -0
  40. package/dist/loop-blobs.generated.js +101 -0
  41. package/dist/loop-blobs.generated.js.map +1 -0
  42. package/dist/loop-lint.d.ts +63 -0
  43. package/dist/loop-lint.d.ts.map +1 -0
  44. package/dist/loop-lint.js +606 -0
  45. package/dist/loop-lint.js.map +1 -0
  46. package/dist/loop-plan.d.ts +416 -0
  47. package/dist/loop-plan.d.ts.map +1 -0
  48. package/dist/loop-plan.js +1151 -0
  49. package/dist/loop-plan.js.map +1 -0
  50. package/dist/loop-render.d.ts +104 -0
  51. package/dist/loop-render.d.ts.map +1 -0
  52. package/dist/loop-render.js +989 -0
  53. package/dist/loop-render.js.map +1 -0
  54. package/dist/loop-trace.d.ts +204 -0
  55. package/dist/loop-trace.d.ts.map +1 -0
  56. package/dist/loop-trace.js +550 -0
  57. package/dist/loop-trace.js.map +1 -0
  58. package/dist/mutation-gate.d.ts +247 -0
  59. package/dist/mutation-gate.d.ts.map +1 -0
  60. package/dist/mutation-gate.js +535 -0
  61. package/dist/mutation-gate.js.map +1 -0
  62. package/dist/no-stubs.d.ts +53 -0
  63. package/dist/no-stubs.d.ts.map +1 -0
  64. package/dist/no-stubs.js +190 -0
  65. package/dist/no-stubs.js.map +1 -0
  66. package/dist/package-skill-layouts.d.ts +67 -0
  67. package/dist/package-skill-layouts.d.ts.map +1 -0
  68. package/dist/package-skill-layouts.js +81 -0
  69. package/dist/package-skill-layouts.js.map +1 -0
  70. package/dist/patterns.d.ts.map +1 -1
  71. package/dist/patterns.js +156 -75
  72. package/dist/patterns.js.map +1 -1
  73. package/dist/recall-domain-boost.d.ts +96 -0
  74. package/dist/recall-domain-boost.d.ts.map +1 -0
  75. package/dist/recall-domain-boost.js +158 -0
  76. package/dist/recall-domain-boost.js.map +1 -0
  77. package/dist/store-lock.d.ts +108 -0
  78. package/dist/store-lock.d.ts.map +1 -0
  79. package/dist/store-lock.js +231 -0
  80. package/dist/store-lock.js.map +1 -0
  81. package/dist/workflows.d.ts +16 -22
  82. package/dist/workflows.d.ts.map +1 -1
  83. package/dist/workflows.js +17 -98
  84. package/dist/workflows.js.map +1 -1
  85. package/package.json +7 -5
  86. package/sbom.json +1062 -202
  87. package/src/agentdb-index.ts +10 -1
  88. package/src/backlog-embed.ts +156 -0
  89. package/src/backlog.ts +536 -28
  90. package/src/challenge-panel.ts +4 -0
  91. package/src/export-holdout.ts +235 -0
  92. package/src/feature-adr-checkpoints.ts +192 -1
  93. package/src/feature-adr-routing.ts +4 -0
  94. package/src/guard.ts +106 -1
  95. package/src/index.ts +72 -1
  96. package/src/loop-blobs.generated.ts +114 -0
  97. package/src/loop-lint.ts +643 -0
  98. package/src/loop-plan.ts +1419 -0
  99. package/src/loop-render.ts +1050 -0
  100. package/src/loop-trace.ts +650 -0
  101. package/src/mutation-gate.ts +701 -0
  102. package/src/no-stubs.ts +204 -0
  103. package/src/package-skill-layouts.ts +107 -0
  104. package/src/patterns.ts +135 -60
  105. package/src/recall-domain-boost.ts +178 -0
  106. package/src/store-lock.ts +258 -0
  107. package/src/workflows.ts +18 -117
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Domain-aware recall re-ranking — the pure half of `dz recall --domain <name>`.
3
+ *
4
+ * WHY THIS EXISTS. `dz teach --domain <name>` has always recorded a domain, and
5
+ * `dz recall --all --stats` prints the per-domain histogram — but the RANKING path
6
+ * (`recallHybrid`: lexical FTS5 + optional vector leg merged by RRF) never looked at
7
+ * the field. So a store shared by different KINDS of work returns them interleaved:
8
+ * medical-research lessons dilute a coding recall and vice versa, and the dilution
9
+ * gets worse as either side grows.
10
+ *
11
+ * WHY A BOOST AND NOT A FILTER. Cross-pollination here is real, not theoretical:
12
+ * "a reviewer's evidence needs the same execute-don't-describe discipline as your
13
+ * own claims" was learned reviewing code and applies verbatim to medical sources;
14
+ * the medical report's core insight ("was the source ever opened?") is the same
15
+ * principle as our claim-check. A hard filter would cut exactly the transfers that
16
+ * make one store worth more than two. So: matching-domain hits move UP, foreign
17
+ * ones stay in the list.
18
+ *
19
+ * THE PROMISE, NARROWLY: this reorders. It NEVER drops a hit, never invents one,
20
+ * and never changes how many are returned. If the boost is wrong, the cost is
21
+ * ordering; it can't hide a lesson from you.
22
+ */
23
+ /** How many places a same-domain lesson may climb. STATED AS POSITIONS, not as a
24
+ * score multiplier: "moves up at most two places" is a sentence a reader can check
25
+ * against the output, while "index × 0.55" is an opaque constant whose behaviour
26
+ * changes with position (it could not lift index 2 past index 1 at all, but did
27
+ * lift index 3 past index 2 — an accident, not a design). A bounded lift keeps
28
+ * lexical relevance dominant: a domain tag is a hint about relevance, not evidence
29
+ * of it, so it breaks near-ties instead of overruling the ranking. */
30
+ export const DOMAIN_LIFT_EXACT = 2;
31
+ /** A prefix/suffix relative (`health-research` against `health`) climbs less. */
32
+ export const DOMAIN_LIFT_RELATED = 1;
33
+ /** Normalize a domain tag for comparison: case- and separator-insensitive, so
34
+ * `Health-Research`, `health_research` and `health research` are one domain. */
35
+ export function normalizeDomain(domain) {
36
+ return String(domain ?? '')
37
+ .toLowerCase()
38
+ .replace(/[\s_]+/g, '-')
39
+ // COLLAPSE hyphen runs. `Health - Research` became `health---research`, which the
40
+ // doc comment above calls one domain with `health-research` and the code then
41
+ // treated as unrelated. Only ordering is at stake here — the export hold-out has its
42
+ // own stricter key — but a comment that describes behaviour the code lacks is the
43
+ // same defect wherever it sits.
44
+ .replace(/-{2,}/g, '-')
45
+ .replace(/^-+|-+$/g, '');
46
+ }
47
+ /** How a hit's domain relates to the requested one. `related` covers the common
48
+ * hierarchy shapes (`health` ↔ `health-research`) without a taxonomy: a taxonomy
49
+ * nobody maintains drifts, and a wrong taxonomy is worse than none. */
50
+ export function domainMatch(hitDomain, wanted) {
51
+ const a = normalizeDomain(hitDomain);
52
+ const b = normalizeDomain(wanted);
53
+ if (a === '' || b === '')
54
+ return 'none';
55
+ if (a === b)
56
+ return 'exact';
57
+ if (a.startsWith(`${b}-`) || b.startsWith(`${a}-`))
58
+ return 'related';
59
+ return 'none';
60
+ }
61
+ /**
62
+ * Re-rank hits so same-domain lessons surface first, keeping every hit.
63
+ *
64
+ * Effective position = `index - lift`. The original relevance order dominates; the
65
+ * domain only breaks near-ties, and the lift is BOUNDED so a tail match can never
66
+ * teleport to the top. Ties keep the incoming order (stable sort), so the function
67
+ * is deterministic — the same input always yields the same output, which is what
68
+ * makes it testable at all.
69
+ */
70
+ export function applyDomainBoost(hits, wanted) {
71
+ const target = normalizeDomain(wanted);
72
+ if (target === '' || hits.length === 0) {
73
+ return { hits, exact: 0, related: 0, moved: 0, noMatches: true };
74
+ }
75
+ let exact = 0;
76
+ let related = 0;
77
+ const scored = hits.map((hit, index) => {
78
+ const match = domainMatch(hit.pattern.domain, target);
79
+ if (match === 'exact')
80
+ exact += 1;
81
+ else if (match === 'related')
82
+ related += 1;
83
+ const lift = match === 'exact' ? DOMAIN_LIFT_EXACT : match === 'related' ? DOMAIN_LIFT_RELATED : 0;
84
+ return { hit, index, lift, effective: index - lift };
85
+ });
86
+ // Tie-break: at equal effective position the LIFTED hit goes first, then the
87
+ // original order. Without this the foreign hit it landed level with won the tie
88
+ // by having the lower original index, so a lift of N moved the hit only N-1
89
+ // places — the code quietly delivered less than the constant promised. A test
90
+ // asserting the documented bound caught it; the fix is here, not in the test.
91
+ scored.sort((a, b) => (a.effective - b.effective) || (b.lift - a.lift) || (a.index - b.index));
92
+ const reordered = scored.map((s) => s.hit);
93
+ let moved = 0;
94
+ scored.forEach((s, newIndex) => { if (s.index !== newIndex)
95
+ moved += 1; });
96
+ return { hits: reordered, exact, related, moved, noMatches: exact === 0 && related === 0 };
97
+ }
98
+ /**
99
+ * How many hits the CUT hid that an unboosted recall would have shown.
100
+ *
101
+ * WHY THIS EXISTS. `applyDomainBoost` never drops a hit — but the CLI still cuts the
102
+ * list at `--limit`, and a promotion INTO the top N necessarily pushes something out
103
+ * of it. Cross-model review found the resulting sentence to be a lie by omission: the
104
+ * note said "foreign-domain lessons kept" while `foreign-B`, visible without
105
+ * `--domain`, had vanished from the printed output. Both statements were true of
106
+ * different lists, which is exactly how an honest tool ends up misleading its reader.
107
+ *
108
+ * The fix is not to stop promoting — that would be the filter we refused to build. It
109
+ * is to COUNT what fell past the cut and say so, because the reader can act on that
110
+ * (raise `--limit`) only if they know it happened.
111
+ *
112
+ * Compares by identity: both lists hold the same hit objects.
113
+ */
114
+ export function countDisplacedByCut(original, boosted, limit) {
115
+ if (!Number.isFinite(limit) || limit <= 0)
116
+ return 0;
117
+ const shownAfter = new Set(boosted.slice(0, limit));
118
+ let displaced = 0;
119
+ for (const hit of original.slice(0, limit)) {
120
+ if (!shownAfter.has(hit))
121
+ displaced += 1;
122
+ }
123
+ return displaced;
124
+ }
125
+ /** The line that reports the cut. Empty when nothing was displaced, so the common
126
+ * case stays quiet. */
127
+ export function renderDomainCutNote(displaced, limit) {
128
+ if (displaced <= 0)
129
+ return '';
130
+ const plural = displaced === 1 ? 'lesson' : 'lessons';
131
+ return ` ${displaced} lower-ranked ${plural} fell past the --limit ${limit} cut to make room — raise --limit to see them (the boost promotes; the cut is what hides)`;
132
+ }
133
+ /** One honest line about what the boost did — including the case where it did
134
+ * nothing, which a silent reorder would hide.
135
+ *
136
+ * The two tail phrases below are a WIRE CONTRACT: `learning_bridge.py` in the
137
+ * `goap-research-ed25519` skill detects whether the installed `dz` supports
138
+ * `--domain` by looking for them, because an older CLI ignores the flag and exits 0
139
+ * (so an exit code cannot tell the versions apart). A test pins both strings — if you
140
+ * reword them, update the bridge in the same change or you switch that loop into
141
+ * permanent degraded mode without a single test turning red. */
142
+ export function renderDomainBoostNote(result, wanted) {
143
+ if (result.noMatches) {
144
+ return ` domain "${wanted}": no lesson in this result carries it — order unchanged, nothing was hidden`;
145
+ }
146
+ const parts = [`${result.exact} exact`];
147
+ if (result.related > 0)
148
+ parts.push(`${result.related} related`);
149
+ const effect = result.moved === 0
150
+ ? 'the order was already correct — nothing moved'
151
+ : `${result.moved} changed position`;
152
+ // The counts describe the CANDIDATES the boost ranked, which is a longer list than
153
+ // the one printed (the caller over-fetches, then cuts at --limit). Saying "3 changed
154
+ // position" above two printed lines reads as an arithmetic error unless the note
155
+ // names the list it is talking about — so it does.
156
+ return ` domain "${wanted}": among ${result.hits.length} candidate(s) — ${parts.join(', ')} match(es), ${effect}; foreign-domain lessons kept (a boost, not a filter)`;
157
+ }
158
+ //# sourceMappingURL=recall-domain-boost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall-domain-boost.js","sourceRoot":"","sources":["../src/recall-domain-boost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Cross-process exclusive lock for the JSON pattern store — built on `proper-lockfile`.
3
+ *
4
+ * WHY THIS EXISTS. The JSON backend writes ATOMICALLY (temp file + rename), so no reader
5
+ * ever sees a torn file. That is a different property from the one needed here. A teach is
6
+ * a read-modify-write: load every record, add one, write them all back. Two teaches that
7
+ * overlap both load the same N records, and each writes back N+1 — the second rename wins
8
+ * and the first lesson is gone. BOTH processes report success, so a lost lesson is silent.
9
+ * Atomicity prevents corruption; only mutual exclusion prevents a lost update.
10
+ *
11
+ * The SQLite backend has its own transaction locking and does not go through here.
12
+ *
13
+ * WHY `proper-lockfile` AND NOT THE PREVIOUS HAND-ROLLED LOCK. An independent review
14
+ * (Codex gpt-5.6-sol) graded the hand-rolled `wx`-file lock F. Its findings, and how this
15
+ * implementation answers them:
16
+ *
17
+ * 1. *Stale-break deleted a successor's live lock* — the old code `rmSync`'d a lock file
18
+ * by PATHNAME after deciding it was stale, so two waiters breaking the same stale lock
19
+ * could delete each other's freshly created locks in a cascade. `proper-lockfile`
20
+ * acquires with an atomic `mkdir`, and a waiter that breaks a stale lock retries with
21
+ * staleness DISABLED, so per acquisition attempt it can remove at most the one lock it
22
+ * observed as stale — never a successor's fresh lock in a loop. The residual
23
+ * microsecond window (stat-stale → holder replaced → rmdir) is closed by DETECTION:
24
+ * every holder heartbeats its lock's mtime and treats a foreign mtime as ECOMPROMISED,
25
+ * which {@link withStoreLock} converts into a loud error instead of a silent lost
26
+ * update. (This is also the model the RVF spec uses for `.rvf` writer locks:
27
+ * single-writer advisory lock file + ownership verification before unlink —
28
+ * `ruvector/docs/research/rvf/spec/09-concurrency-versioning.md`.)
29
+ *
30
+ * 2. *"Unparseable means dead" deleted live locks* — there is no lock BODY to parse any
31
+ * more. The lock is a directory; its existence is the lock, so the empty-file /
32
+ * half-written-file misclassification class is structurally gone.
33
+ *
34
+ * 3. *Age was mistaken for liveness* — staleness is now filesystem mtime plus a HEARTBEAT:
35
+ * the holder refreshes the lock's mtime on a timer (every `staleMs / 2`, ≥ 1s), so a
36
+ * holder slower than `staleMs` is never broken while alive, and a crashed holder's lock
37
+ * stops being refreshed and is reclaimed after `staleMs`. Nothing trusts a
38
+ * self-reported timestamp, so a forged far-future `ts` can no longer block forever.
39
+ * NOTE: the heartbeat runs on the event loop, so a SYNCHRONOUS critical section longer
40
+ * than `staleMs` could still be observed as stale — keep {@link withStoreLockSync}
41
+ * bodies short (they are: single-file JSON rewrites) or raise `staleMs`.
42
+ *
43
+ * 4. *The timeout was not an acquisition deadline* — the retry loops below compute a real
44
+ * deadline (`now + timeoutMs`), re-check it after EVERY sleep, clamp the last sleep to
45
+ * the remaining budget, and never acquire after the deadline has passed.
46
+ *
47
+ * 5. *The lock was not store-wide* — every JSON-tier writer in `patterns.ts` now runs
48
+ * under this lock (see the writer inventory in that module).
49
+ *
50
+ * On timeout this THROWS ({@link StoreLockTimeoutError}) rather than proceeding unlocked.
51
+ * Proceeding would restore exactly the silent lost-update this exists to stop, and a teach
52
+ * that failed loudly can be retried — one that vanished cannot.
53
+ *
54
+ * MIGRATION: the pre-`proper-lockfile` implementation left a regular FILE at
55
+ * `.dz/store.lock`; the library's lock is a DIRECTORY at the same path. A leftover file
56
+ * from a crashed old process is removed only once it is older than `staleMs` — a fresh
57
+ * one is honoured as held, same as any other lock.
58
+ */
59
+ /** A lock whose HEARTBEAT stopped this long ago is presumed abandoned (crashed holder). */
60
+ export declare const STALE_LOCK_MS = 30000;
61
+ /** Give up waiting after this long and say so, rather than blocking a CLI forever. */
62
+ export declare const LOCK_TIMEOUT_MS = 10000;
63
+ /** Path of the lock guarding a project's pattern store (a DIRECTORY when held). */
64
+ export declare function storeLockPath(projectRoot: string): string;
65
+ /** Options accepted by {@link withStoreLock} / {@link withStoreLockSync}. */
66
+ export interface StoreLockOptions {
67
+ /** Heartbeat-stopped threshold before a lock is presumed abandoned (min 2000, lib-enforced). */
68
+ readonly staleMs?: number;
69
+ /** Acquisition deadline. Also overridable via `DZ_STORE_LOCK_TIMEOUT_MS` (tests/ops). */
70
+ readonly timeoutMs?: number;
71
+ /** Delay between acquisition attempts while the lock is held by someone else. */
72
+ readonly pollMs?: number;
73
+ }
74
+ /** Acquisition gave up at the deadline. Nothing was written; the operation can be retried. */
75
+ export declare class StoreLockTimeoutError extends Error {
76
+ readonly code = "ELOCKTIMEOUT";
77
+ constructor(lockPath: string, waitedMs: number);
78
+ }
79
+ /**
80
+ * The holder detected that its lock was broken out from under it (ECOMPROMISED) — e.g. a
81
+ * waiter reclaimed the lock after this process was suspended past `staleMs`. The critical
82
+ * section DID run, but it may have raced another writer, so the operation must be treated
83
+ * as failed and retried (store writes are idempotent by deterministic record id).
84
+ */
85
+ export declare class StoreLockCompromisedError extends Error {
86
+ readonly code = "ECOMPROMISED";
87
+ constructor(lockPath: string, cause: Error);
88
+ }
89
+ /**
90
+ * Run `fn` while holding the store lock, releasing it however `fn` ends.
91
+ *
92
+ * Throws {@link StoreLockTimeoutError} when the lock cannot be acquired by the deadline
93
+ * (nothing has run), and {@link StoreLockCompromisedError} when the lock was broken while
94
+ * `fn` ran (it DID run, but may have raced — retry; store writes are idempotent).
95
+ */
96
+ export declare function withStoreLock<T>(projectRoot: string, fn: () => Promise<T>, opts?: StoreLockOptions): Promise<T>;
97
+ /**
98
+ * Synchronous {@link withStoreLock}, for the store's synchronous writers (prune / remove).
99
+ * Same deadline and failure semantics. The heartbeat cannot fire while `fn` blocks the
100
+ * event loop, so keep bodies well under `staleMs` (they are: single-file JSON rewrites).
101
+ *
102
+ * CAVEAT: do not contend with an ASYNC `withStoreLock` holder in the SAME process — the
103
+ * sync wait blocks the event loop that would release that holder, so the wait can only
104
+ * end in the loud {@link StoreLockTimeoutError} (never a lost update). Cross-process
105
+ * contention, the case this lock exists for, is unaffected.
106
+ */
107
+ export declare function withStoreLockSync<T>(projectRoot: string, fn: () => T, opts?: StoreLockOptions): T;
108
+ //# sourceMappingURL=store-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-lock.d.ts","sourceRoot":"","sources":["../src/store-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAMH,2FAA2F;AAC3F,eAAO,MAAM,aAAa,QAAS,CAAC;AACpC,sFAAsF;AACtF,eAAO,MAAM,eAAe,QAAS,CAAC;AAKtC,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,8FAA8F;AAC9F,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,kBAAkB;gBACnB,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAO/C;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,IAAI,kBAAkB;gBACnB,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAI3C;AA0ED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,CAAC,EACX,IAAI,GAAE,gBAAqB,GAC1B,CAAC,CAuBH"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Cross-process exclusive lock for the JSON pattern store — built on `proper-lockfile`.
3
+ *
4
+ * WHY THIS EXISTS. The JSON backend writes ATOMICALLY (temp file + rename), so no reader
5
+ * ever sees a torn file. That is a different property from the one needed here. A teach is
6
+ * a read-modify-write: load every record, add one, write them all back. Two teaches that
7
+ * overlap both load the same N records, and each writes back N+1 — the second rename wins
8
+ * and the first lesson is gone. BOTH processes report success, so a lost lesson is silent.
9
+ * Atomicity prevents corruption; only mutual exclusion prevents a lost update.
10
+ *
11
+ * The SQLite backend has its own transaction locking and does not go through here.
12
+ *
13
+ * WHY `proper-lockfile` AND NOT THE PREVIOUS HAND-ROLLED LOCK. An independent review
14
+ * (Codex gpt-5.6-sol) graded the hand-rolled `wx`-file lock F. Its findings, and how this
15
+ * implementation answers them:
16
+ *
17
+ * 1. *Stale-break deleted a successor's live lock* — the old code `rmSync`'d a lock file
18
+ * by PATHNAME after deciding it was stale, so two waiters breaking the same stale lock
19
+ * could delete each other's freshly created locks in a cascade. `proper-lockfile`
20
+ * acquires with an atomic `mkdir`, and a waiter that breaks a stale lock retries with
21
+ * staleness DISABLED, so per acquisition attempt it can remove at most the one lock it
22
+ * observed as stale — never a successor's fresh lock in a loop. The residual
23
+ * microsecond window (stat-stale → holder replaced → rmdir) is closed by DETECTION:
24
+ * every holder heartbeats its lock's mtime and treats a foreign mtime as ECOMPROMISED,
25
+ * which {@link withStoreLock} converts into a loud error instead of a silent lost
26
+ * update. (This is also the model the RVF spec uses for `.rvf` writer locks:
27
+ * single-writer advisory lock file + ownership verification before unlink —
28
+ * `ruvector/docs/research/rvf/spec/09-concurrency-versioning.md`.)
29
+ *
30
+ * 2. *"Unparseable means dead" deleted live locks* — there is no lock BODY to parse any
31
+ * more. The lock is a directory; its existence is the lock, so the empty-file /
32
+ * half-written-file misclassification class is structurally gone.
33
+ *
34
+ * 3. *Age was mistaken for liveness* — staleness is now filesystem mtime plus a HEARTBEAT:
35
+ * the holder refreshes the lock's mtime on a timer (every `staleMs / 2`, ≥ 1s), so a
36
+ * holder slower than `staleMs` is never broken while alive, and a crashed holder's lock
37
+ * stops being refreshed and is reclaimed after `staleMs`. Nothing trusts a
38
+ * self-reported timestamp, so a forged far-future `ts` can no longer block forever.
39
+ * NOTE: the heartbeat runs on the event loop, so a SYNCHRONOUS critical section longer
40
+ * than `staleMs` could still be observed as stale — keep {@link withStoreLockSync}
41
+ * bodies short (they are: single-file JSON rewrites) or raise `staleMs`.
42
+ *
43
+ * 4. *The timeout was not an acquisition deadline* — the retry loops below compute a real
44
+ * deadline (`now + timeoutMs`), re-check it after EVERY sleep, clamp the last sleep to
45
+ * the remaining budget, and never acquire after the deadline has passed.
46
+ *
47
+ * 5. *The lock was not store-wide* — every JSON-tier writer in `patterns.ts` now runs
48
+ * under this lock (see the writer inventory in that module).
49
+ *
50
+ * On timeout this THROWS ({@link StoreLockTimeoutError}) rather than proceeding unlocked.
51
+ * Proceeding would restore exactly the silent lost-update this exists to stop, and a teach
52
+ * that failed loudly can be retried — one that vanished cannot.
53
+ *
54
+ * MIGRATION: the pre-`proper-lockfile` implementation left a regular FILE at
55
+ * `.dz/store.lock`; the library's lock is a DIRECTORY at the same path. A leftover file
56
+ * from a crashed old process is removed only once it is older than `staleMs` — a fresh
57
+ * one is honoured as held, same as any other lock.
58
+ */
59
+ import { mkdirSync, rmSync, statSync } from 'node:fs';
60
+ import { join, resolve } from 'node:path';
61
+ import { lockSync } from 'proper-lockfile';
62
+ /** A lock whose HEARTBEAT stopped this long ago is presumed abandoned (crashed holder). */
63
+ export const STALE_LOCK_MS = 30_000;
64
+ /** Give up waiting after this long and say so, rather than blocking a CLI forever. */
65
+ export const LOCK_TIMEOUT_MS = 10_000;
66
+ /** `proper-lockfile` silently clamps `stale` up to this minimum. */
67
+ const MIN_STALE_MS = 2_000;
68
+ /** Path of the lock guarding a project's pattern store (a DIRECTORY when held). */
69
+ export function storeLockPath(projectRoot) {
70
+ return join(projectRoot, '.dz', 'store.lock');
71
+ }
72
+ /** Acquisition gave up at the deadline. Nothing was written; the operation can be retried. */
73
+ export class StoreLockTimeoutError extends Error {
74
+ code = 'ELOCKTIMEOUT';
75
+ constructor(lockPath, waitedMs) {
76
+ super(`the pattern store at ${lockPath} stayed locked for ${waitedMs}ms — ` +
77
+ 'another dz process is still writing. Nothing was written; retry.');
78
+ this.name = 'StoreLockTimeoutError';
79
+ }
80
+ }
81
+ /**
82
+ * The holder detected that its lock was broken out from under it (ECOMPROMISED) — e.g. a
83
+ * waiter reclaimed the lock after this process was suspended past `staleMs`. The critical
84
+ * section DID run, but it may have raced another writer, so the operation must be treated
85
+ * as failed and retried (store writes are idempotent by deterministic record id).
86
+ */
87
+ export class StoreLockCompromisedError extends Error {
88
+ code = 'ECOMPROMISED';
89
+ constructor(lockPath, cause) {
90
+ super(`the pattern store lock at ${lockPath} was compromised while held (${cause.message}) — retry the operation.`);
91
+ this.name = 'StoreLockCompromisedError';
92
+ }
93
+ }
94
+ const defaultTimeoutMs = () => {
95
+ const env = Number(process.env['DZ_STORE_LOCK_TIMEOUT_MS']);
96
+ return Number.isFinite(env) && env > 0 ? env : LOCK_TIMEOUT_MS;
97
+ };
98
+ const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
99
+ /** Synchronous sleep without spinning (Atomics.wait on a throwaway buffer). */
100
+ const sleepSync = (ms) => {
101
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
102
+ };
103
+ /**
104
+ * Remove a REGULAR-FILE lock left by the pre-`proper-lockfile` implementation, but only
105
+ * once it is stale — a fresh legacy lock belongs to a live old-version process and is
106
+ * honoured. (Never deletes a directory: that is a live library lock.)
107
+ */
108
+ function clearStaleLegacyLockFile(lockPath, staleMs) {
109
+ try {
110
+ const st = statSync(lockPath);
111
+ if (st.isFile() && Date.now() - st.mtimeMs >= staleMs)
112
+ rmSync(lockPath, { force: true });
113
+ }
114
+ catch { /* gone already — fine */ }
115
+ }
116
+ /**
117
+ * One acquisition attempt. Returns `'held'` when someone else holds the lock (retry later);
118
+ * throws on real I/O errors. `mkdir`-based, so creation is atomic; staleness is heartbeat
119
+ * mtime, and the library's own stale-break retries with staleness disabled (see header §1).
120
+ */
121
+ function tryAcquire(projectRoot, staleMs, onCompromised) {
122
+ const lockPath = storeLockPath(projectRoot);
123
+ try {
124
+ const release = lockSync(resolve(projectRoot, '.dz', 'store'), {
125
+ lockfilePath: lockPath,
126
+ stale: staleMs,
127
+ realpath: false, // the guarded resource is a concept, not an existing file
128
+ onCompromised,
129
+ });
130
+ return {
131
+ release: () => {
132
+ try {
133
+ release();
134
+ }
135
+ catch { /* ERELEASED after a compromise — the lock is no longer ours to remove */ }
136
+ },
137
+ compromise: () => undefined,
138
+ };
139
+ }
140
+ catch (err) {
141
+ const code = err.code;
142
+ if (code === 'ELOCKED')
143
+ return 'held';
144
+ if (code === 'ENOTDIR') {
145
+ // A stale legacy FILE lock: the library can only rmdir directories. Clear it (only
146
+ // if actually stale) and report 'held' so the caller's loop retries.
147
+ clearStaleLegacyLockFile(lockPath, staleMs);
148
+ return 'held';
149
+ }
150
+ throw err;
151
+ }
152
+ }
153
+ function resolveOpts(opts) {
154
+ return {
155
+ staleMs: Math.max(opts.staleMs ?? STALE_LOCK_MS, MIN_STALE_MS),
156
+ timeoutMs: opts.timeoutMs ?? defaultTimeoutMs(),
157
+ pollMs: opts.pollMs ?? 25,
158
+ };
159
+ }
160
+ /**
161
+ * Run `fn` while holding the store lock, releasing it however `fn` ends.
162
+ *
163
+ * Throws {@link StoreLockTimeoutError} when the lock cannot be acquired by the deadline
164
+ * (nothing has run), and {@link StoreLockCompromisedError} when the lock was broken while
165
+ * `fn` ran (it DID run, but may have raced — retry; store writes are idempotent).
166
+ */
167
+ export async function withStoreLock(projectRoot, fn, opts = {}) {
168
+ const { staleMs, timeoutMs, pollMs } = resolveOpts(opts);
169
+ mkdirSync(join(projectRoot, '.dz'), { recursive: true });
170
+ const started = Date.now();
171
+ const deadline = started + timeoutMs;
172
+ let compromised;
173
+ const onCompromised = (e) => { compromised = e; };
174
+ for (;;) {
175
+ const got = tryAcquire(projectRoot, staleMs, onCompromised);
176
+ if (got !== 'held') {
177
+ let result;
178
+ try {
179
+ result = await fn();
180
+ }
181
+ finally {
182
+ got.release();
183
+ }
184
+ if (compromised !== undefined)
185
+ throw new StoreLockCompromisedError(storeLockPath(projectRoot), compromised);
186
+ return result;
187
+ }
188
+ const now = Date.now();
189
+ if (now >= deadline)
190
+ throw new StoreLockTimeoutError(storeLockPath(projectRoot), now - started);
191
+ await sleep(Math.min(pollMs, deadline - now));
192
+ }
193
+ }
194
+ /**
195
+ * Synchronous {@link withStoreLock}, for the store's synchronous writers (prune / remove).
196
+ * Same deadline and failure semantics. The heartbeat cannot fire while `fn` blocks the
197
+ * event loop, so keep bodies well under `staleMs` (they are: single-file JSON rewrites).
198
+ *
199
+ * CAVEAT: do not contend with an ASYNC `withStoreLock` holder in the SAME process — the
200
+ * sync wait blocks the event loop that would release that holder, so the wait can only
201
+ * end in the loud {@link StoreLockTimeoutError} (never a lost update). Cross-process
202
+ * contention, the case this lock exists for, is unaffected.
203
+ */
204
+ export function withStoreLockSync(projectRoot, fn, opts = {}) {
205
+ const { staleMs, timeoutMs, pollMs } = resolveOpts(opts);
206
+ mkdirSync(join(projectRoot, '.dz'), { recursive: true });
207
+ const started = Date.now();
208
+ const deadline = started + timeoutMs;
209
+ let compromised;
210
+ const onCompromised = (e) => { compromised = e; };
211
+ for (;;) {
212
+ const got = tryAcquire(projectRoot, staleMs, onCompromised);
213
+ if (got !== 'held') {
214
+ let result;
215
+ try {
216
+ result = fn();
217
+ }
218
+ finally {
219
+ got.release();
220
+ }
221
+ if (compromised !== undefined)
222
+ throw new StoreLockCompromisedError(storeLockPath(projectRoot), compromised);
223
+ return result;
224
+ }
225
+ const now = Date.now();
226
+ if (now >= deadline)
227
+ throw new StoreLockTimeoutError(storeLockPath(projectRoot), now - started);
228
+ sleepSync(Math.min(pollMs, deadline - now));
229
+ }
230
+ }
231
+ //# sourceMappingURL=store-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-lock.js","sourceRoot":"","sources":["../src/store-lock.ts"],"names":[],"mappings":"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"}
@@ -1,28 +1,22 @@
1
1
  /**
2
- * Dynamic workflow templates for Opus 4.8+ orchestration.
2
+ * RETIRED — the ADR-005 workflow templates are gone (feature loop-designer, AM-6 / architecture
3
+ * §8.2). They emitted a pre-`meta`/`phases` format (`export default {tasks, maxConcurrency}`) the
4
+ * current Workflow runtime cannot execute — a broken command, not a feature.
3
5
  *
4
- * Per ADR-005: workflows live in the orchestration layer only.
5
- * They generate JS scripts that Claude Code's dynamic workflow engine executes.
6
- * Core schema, adapters, and skill content remain model-agnostic.
6
+ * Replacement: `dz workflow init` (scaffold a `loop-plan/1` plan), `dz workflow validate`,
7
+ * `dz workflow render` (plan executable region-delimited loop script), plus the sibling gates
8
+ * `dz workflow-lint` and `dz workflow-trace`. See `loop-plan.ts` / `loop-render.ts`.
7
9
  *
8
- * @packageDocumentation
10
+ * BREAKING (deliberate, channeled): removing `WorkflowTemplate`/`WORKFLOWS`/`getWorkflow` is a
11
+ * public API change for harness-core. harness-core is 0.x, where MINOR is this repo's breaking
12
+ * channel; the removal is named in the CHANGELOG and README — never silent. The only internal
13
+ * importer was `harness-cli`'s `cmdWorkflow` (MEASURED at plan time), rewritten in the same change.
14
+ *
15
+ * This module stays as a deprecation shim so stale imports fail LOUDLY with a pointer, not with a
16
+ * confusing "module not found".
9
17
  */
10
- /** A workflow template generates a JS orchestration script from parameters. */
11
- export interface WorkflowTemplate {
12
- readonly name: string;
13
- readonly description: string;
14
- readonly generate: (options: WorkflowOptions) => string;
15
- }
16
- /** Options passed to workflow generation. */
17
- export interface WorkflowOptions {
18
- readonly projectRoot: string;
19
- readonly dryRun?: boolean;
20
- readonly packages?: readonly string[];
21
- }
22
- /** All registered workflow templates. */
23
- export declare const WORKFLOWS: Record<string, WorkflowTemplate>;
24
- /** Valid workflow names. */
18
+ /** The pinned retirement message the CLI prints (tested by workflow-legacy-shim.test.ts). */
19
+ export declare const WORKFLOW_TEMPLATES_RETIRED_MESSAGE = "dz workflow: the ADR-005 templates are retired (they emitted a pre-meta format the runtime cannot run) \u2014 use dz workflow init/render";
20
+ /** Empty by design: no legacy template names remain. */
25
21
  export declare const WORKFLOW_NAMES: string[];
26
- /** Look up a workflow by name. */
27
- export declare function getWorkflow(name: string): WorkflowTemplate | undefined;
28
22
  //# sourceMappingURL=workflows.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,MAAM,CAAC;CACzD;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAsFD,yCAAyC;AACzC,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAKtD,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,cAAc,UAAyB,CAAC;AAErD,kCAAkC;AAClC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEtE"}
1
+ {"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,kCAAkC,8IACyF,CAAC;AAEzI,wDAAwD;AACxD,eAAO,MAAM,cAAc,EAAE,MAAM,EAAO,CAAC"}