forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6

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 (196) hide show
  1. package/AGENTS.md +18 -7
  2. package/CHANGELOG.md +79 -1
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/README.md +6 -2
  6. package/bin/forge-cmd.js +20 -0
  7. package/bin/forge.js +28 -375
  8. package/docs/INDEX.md +1 -1
  9. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  10. package/docs/guides/MIGRATION.md +4 -4
  11. package/docs/guides/SETUP.md +16 -16
  12. package/docs/reference/COMMANDS.md +8 -5
  13. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  14. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  15. package/docs/reference/INSTALL.md +4 -0
  16. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  17. package/docs/reference/RELEASE.md +5 -3
  18. package/docs/reference/TOOLCHAIN.md +8 -0
  19. package/docs/reference/github-accounts.md +134 -0
  20. package/docs/reference/protected-state-surfaces.md +4 -4
  21. package/docs/reference/shepherd.md +114 -35
  22. package/lefthook.yml +12 -0
  23. package/lib/activation/ensure-forge-home.js +33 -15
  24. package/lib/adapters/pr-state-adapter.js +359 -144
  25. package/lib/audit-evidence.js +71 -110
  26. package/lib/base-remote.js +138 -0
  27. package/lib/beta5-compatibility-evidence.js +1093 -0
  28. package/lib/bun-lockfile-proof.js +413 -0
  29. package/lib/bun-workflow-pins.js +461 -0
  30. package/lib/capabilities/index.js +9 -0
  31. package/lib/capabilities/model.js +141 -0
  32. package/lib/capabilities/probes.js +347 -0
  33. package/lib/capped-jsonl-log.js +236 -0
  34. package/lib/codex-skills.js +2 -2
  35. package/lib/commands/_manifest.js +1 -0
  36. package/lib/commands/_registry.js +50 -20
  37. package/lib/commands/clean.js +252 -32
  38. package/lib/commands/dev.js +4 -33
  39. package/lib/commands/doctor.js +37 -6
  40. package/lib/commands/gate.js +197 -27
  41. package/lib/commands/github.js +215 -0
  42. package/lib/commands/hooks.js +276 -30
  43. package/lib/commands/insights.js +8 -3
  44. package/lib/commands/memory.js +66 -2
  45. package/lib/commands/merge.js +1265 -58
  46. package/lib/commands/plan.js +33 -2
  47. package/lib/commands/pr.js +3 -1
  48. package/lib/commands/preflight.js +21 -4
  49. package/lib/commands/prime.js +21 -8
  50. package/lib/commands/push.js +146 -54
  51. package/lib/commands/recall.js +127 -49
  52. package/lib/commands/recap.js +6 -1
  53. package/lib/commands/release.js +39 -3
  54. package/lib/commands/remember.js +28 -4
  55. package/lib/commands/serve.js +26 -9
  56. package/lib/commands/setup.js +323 -98
  57. package/lib/commands/shepherd.js +591 -73
  58. package/lib/commands/ship.js +36 -91
  59. package/lib/commands/skill.js +127 -11
  60. package/lib/commands/status.js +17 -1
  61. package/lib/commands/team.js +47 -8
  62. package/lib/commands/test.js +187 -38
  63. package/lib/commands/validate.js +65 -21
  64. package/lib/commands/worktree.js +359 -45
  65. package/lib/core/runtime-graph.js +1 -1
  66. package/lib/doc-assertions.js +297 -0
  67. package/lib/existing-tdd-gate.js +253 -0
  68. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  69. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  70. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  71. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  72. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  73. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  74. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  75. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  76. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  77. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  78. package/lib/forge-context.js +1 -4
  79. package/lib/forge-issues.js +134 -32
  80. package/lib/gate-events.js +98 -10
  81. package/lib/git-defaults.js +56 -0
  82. package/lib/github-context.js +308 -0
  83. package/lib/global-flags.js +1 -0
  84. package/lib/harness-capability-matrix.js +3 -3
  85. package/lib/hook-renderer.js +122 -5
  86. package/lib/insights.js +96 -80
  87. package/lib/issue-render.js +19 -0
  88. package/lib/kernel/backing-issue.js +14 -2
  89. package/lib/kernel/broker.js +739 -31
  90. package/lib/kernel/claim-reconciler.js +238 -0
  91. package/lib/kernel/cli-broker-factory.js +12 -1
  92. package/lib/kernel/close-on-merge.js +154 -0
  93. package/lib/kernel/fs-class.js +42 -25
  94. package/lib/kernel/lease-enforcer.js +9 -4
  95. package/lib/kernel/legacy-claim-repair.js +442 -0
  96. package/lib/kernel/live-claim-projection.js +26 -0
  97. package/lib/kernel/migrations.js +118 -3
  98. package/lib/kernel/readiness-model.js +184 -12
  99. package/lib/kernel/schema.js +49 -1
  100. package/lib/kernel/sqlite-driver.js +3435 -172
  101. package/lib/kernel/taxonomy-validator.js +4 -1
  102. package/lib/kernel/windows-private-acl.js +239 -0
  103. package/lib/lefthook-wiring.js +21 -1
  104. package/lib/memory/hygiene.js +191 -0
  105. package/lib/memory/router.js +110 -28
  106. package/lib/memory/usage-evidence.js +4 -0
  107. package/lib/memory-digest.js +106 -15
  108. package/lib/memory-recall-events.js +145 -0
  109. package/lib/memory-recall.js +71 -10
  110. package/lib/merge-rules.js +143 -21
  111. package/lib/npm-publish-workflow.js +465 -0
  112. package/lib/orientation.js +68 -43
  113. package/lib/package-root.js +2 -0
  114. package/lib/plugin-catalog.js +14 -4
  115. package/lib/pr-bundle.js +5 -6
  116. package/lib/pr-monitor/auto-actions.js +169 -28
  117. package/lib/pr-monitor/differ.js +110 -4
  118. package/lib/pr-monitor/events.js +0 -0
  119. package/lib/pr-monitor/flow-monitor.js +1424 -0
  120. package/lib/pr-monitor/gather.js +251 -44
  121. package/lib/pr-monitor/journal.js +18 -39
  122. package/lib/pr-monitor/monitor.js +117 -10
  123. package/lib/pr-monitor/process-identity.js +117 -0
  124. package/lib/pr-monitor/reconcile-executor.js +1129 -470
  125. package/lib/pr-monitor/reconcile.js +0 -0
  126. package/lib/pr-monitor/render-summary.js +293 -0
  127. package/lib/pr-monitor/review-preflight.js +269 -0
  128. package/lib/pr-monitor/shepherd-lease.js +38 -20
  129. package/lib/pr-monitor/verdict.js +438 -0
  130. package/lib/pr-monitor/watch-lifecycle.js +145 -27
  131. package/lib/pr-monitor/watch-owner.js +1414 -0
  132. package/lib/pr-monitor/watch.js +129 -58
  133. package/lib/pr-pull.js +33 -14
  134. package/lib/pr-shepherd.js +51 -11
  135. package/lib/preflight/gates.js +65 -18
  136. package/lib/preflight/runner.js +5 -0
  137. package/lib/project-memory.js +178 -4
  138. package/lib/protected-state-authority.js +1100 -0
  139. package/lib/protected-state-surfaces.js +243 -45
  140. package/lib/release-readiness.js +53 -7
  141. package/lib/review-adapter.js +65 -0
  142. package/lib/shell-utils.js +1 -1
  143. package/lib/skills-sync.js +71 -35
  144. package/lib/smart-merge.js +28 -4
  145. package/lib/symlink-utils.js +74 -26
  146. package/lib/upgrade-safety.js +39 -0
  147. package/lib/using-forge.js +19 -6
  148. package/lib/validation/risk-manifest.js +339 -0
  149. package/lib/workflow/enforce-stage.js +44 -0
  150. package/lib/workflow/plan-authority.js +225 -0
  151. package/package.json +12 -9
  152. package/scripts/commitlint.js +13 -15
  153. package/scripts/doc-asserting-tests.js +158 -0
  154. package/scripts/generate-risk-manifest.js +91 -0
  155. package/scripts/github-context-bridge.sh +10 -0
  156. package/scripts/legacy-claim-repair.js +145 -0
  157. package/scripts/lib/behavioral-eval-runner.js +310 -0
  158. package/scripts/lib/behavioral-eval-runtime.js +457 -0
  159. package/scripts/lib/eval-evidence.js +328 -0
  160. package/scripts/lib/eval-runner.js +81 -41
  161. package/scripts/lib/immutable-eval-corpus.js +309 -0
  162. package/scripts/lib/promotion-evidence-loader.js +94 -0
  163. package/scripts/lib/promotion-scorecard.js +314 -0
  164. package/scripts/npm-release-receipt.js +134 -0
  165. package/scripts/process-tree.js +773 -0
  166. package/scripts/protected-state-check.js +479 -31
  167. package/scripts/run-command-eval.js +29 -1
  168. package/scripts/sync-agent-skills.js +333 -34
  169. package/scripts/sync-d20-audit.js +172 -0
  170. package/scripts/test-full-suite.js +935 -37
  171. package/scripts/test-profile.js +13 -3
  172. package/scripts/test.js +271 -57
  173. package/skills/coverage.json +1 -0
  174. package/skills/review/SKILL.md +6 -11
  175. package/skills/review/evals/scorecard.json +4 -4
  176. package/skills/rollback/SKILL.md +4 -11
  177. package/skills/rollback/evals/scorecard.json +3 -3
  178. package/skills/setup/SKILL.md +18 -0
  179. package/skills/setup/evals/scorecard.json +3 -3
  180. package/skills/shepherd/SKILL.md +39 -16
  181. package/skills/shepherd/evals/scorecard.json +4 -4
  182. package/skills/ship/SKILL.md +4 -12
  183. package/skills/ship/evals/scorecard.json +3 -3
  184. package/skills/validate/SKILL.md +3 -0
  185. package/skills/validate/evals/scorecard.json +1 -1
  186. package/skills/worktree/SKILL.md +6 -1
  187. package/skills/worktree/evals/scorecard.json +2 -2
  188. package/lib/beads-setup.js +0 -538
  189. package/lib/beads-sync-scaffold.js +0 -189
  190. package/lib/pat-setup.js +0 -207
  191. package/lib/pr-monitor/render-sticky.js +0 -206
  192. package/lib/pr-monitor/upsert-sticky.js +0 -169
  193. package/scripts/beads-context.sh +0 -577
  194. package/scripts/beads-migrate-to-dolt.sh +0 -7
  195. package/scripts/beads-upgrade-smoke.sh +0 -284
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -19,6 +19,7 @@
19
19
  * @typedef {Object} GateOutcome
20
20
  * @property {boolean} ok - Whether the gate passed.
21
21
  * @property {string} [summary] - Short human-readable result line.
22
+ * @property {string} [inputFingerprint] - Exact input snapshot tested by the gate.
22
23
  *
23
24
  * @typedef {Object} Gate
24
25
  * @property {string} name
@@ -30,6 +31,7 @@
30
31
  * @property {boolean} skipped
31
32
  * @property {string} summary
32
33
  * @property {number} [durationMs]
34
+ * @property {string} [inputFingerprint]
33
35
  */
34
36
 
35
37
  /**
@@ -66,6 +68,9 @@ async function executeGate(gate) {
66
68
  skipped,
67
69
  summary: outcome?.summary || '',
68
70
  durationMs,
71
+ ...(typeof outcome?.inputFingerprint === 'string'
72
+ ? { inputFingerprint: outcome.inputFingerprint }
73
+ : {}),
69
74
  };
70
75
  }
71
76
 
@@ -1,7 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { createHash, randomUUID } = require('node:crypto');
6
+ const { types: { isProxy } } = require('node:util');
3
7
  const { resolveKernelDatabasePath } = require('./kernel/cli-broker-factory');
8
+ const { resolveGitCommonDir } = require('./kernel/broker');
4
9
  const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
10
+ const { normalizeRecallHit } = require('./memory-recall');
11
+ const { createUsageEvidenceStore } = require('../packages/memory');
5
12
 
6
13
  // Project memory is a Forge read model persisted in the kernel store (kernel_memories),
7
14
  // written DIRECTLY rather than through the issue CAS/guarded-event path. The store seam
@@ -30,6 +37,24 @@ function resolveStore(projectRoot, options = {}) {
30
37
  return options.store ?? defaultStore(projectRoot, options);
31
38
  }
32
39
 
40
+ function resolveProjectId(projectRoot, options = {}) {
41
+ const platform = options.platform || process.platform;
42
+ const pathImpl = platform === 'win32' ? path.win32 : path.posix;
43
+ const commonDir = options.gitCommonDir || resolveGitCommonDir(projectRoot, options);
44
+ const absolute = pathImpl.resolve(projectRoot, commonDir);
45
+ const realpath = options.realpath || fs.realpathSync.native;
46
+ let canonical;
47
+ try {
48
+ canonical = realpath(absolute);
49
+ } catch {
50
+ canonical = absolute;
51
+ }
52
+ canonical = canonical.replaceAll('\\', '/');
53
+ return platform === 'win32'
54
+ ? canonical.toLowerCase()
55
+ : canonical;
56
+ }
57
+
33
58
  function assertEntryObject(entry) {
34
59
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
35
60
  throw new TypeError('project memory entry must be an object');
@@ -124,27 +149,169 @@ function list(projectRoot, options = {}) {
124
149
  // driver so recall never loads and re-sorts the whole table. `options.agents` (a
125
150
  // source_agent allow-list) scopes the read, e.g. to human `remember` notes only.
126
151
  function recent(projectRoot, limit, options = {}) {
127
- return resolveStore(projectRoot, options).recentMemories(limit, { agents: options.agents });
152
+ return resolveStore(projectRoot, options).recentMemories(limit, {
153
+ agents: options.agents,
154
+ kind: options.kind,
155
+ });
128
156
  }
129
157
 
130
158
  // Total stored memories (optionally scoped by `options.agents`) — paired with `recent` so
131
159
  // recall can report "showing N of TOTAL".
132
160
  function count(projectRoot, options = {}) {
133
- return resolveStore(projectRoot, options).countMemories({ agents: options.agents });
161
+ return resolveStore(projectRoot, options).countMemories({
162
+ agents: options.agents,
163
+ kind: options.kind,
164
+ });
134
165
  }
135
166
 
136
167
  // BM25 top-N recall over the FTS5 index (token-AND). Unlike `search` (the legacy LIKE
137
168
  // helper) this does not short-circuit an empty query — the driver falls back to recent so
138
169
  // recall stays capped either way.
139
170
  function searchRanked(projectRoot, query, limit, options = {}) {
140
- return resolveStore(projectRoot, options).searchMemoriesRanked(query, limit);
171
+ return resolveStore(projectRoot, options).searchMemoriesRanked(query, limit, {
172
+ kind: options.kind,
173
+ });
141
174
  }
142
175
 
143
176
  // Relevance-only BM25 recall that returns the raw bm25 `score` per entry, so a caller can
144
177
  // apply a relevance floor. A no-match/empty query returns [] (no recency fallback). The
145
178
  // per-turn memory-recall hook uses this to inject nothing unless a note clearly matches.
146
179
  function searchRankedScored(projectRoot, query, limit, options = {}) {
147
- return resolveStore(projectRoot, options).searchMemoriesRankedScored(query, limit);
180
+ const projectId = resolveProjectId(projectRoot, options);
181
+ const searchOptions = {
182
+ projectId,
183
+ excludeKeys: options.excludeKeys || [],
184
+ ...(options.now ? { now: options.now } : {}),
185
+ ...(options.busyTimeoutMs !== undefined ? { busyTimeoutMs: options.busyTimeoutMs } : {}),
186
+ };
187
+ return resolveStore(projectRoot, options)
188
+ .searchMemoriesRankedScored(query, limit, searchOptions)
189
+ .map(hit => normalizeRecallHit(hit, projectId));
190
+ }
191
+
192
+ function opaqueUsageIdentity(kind, value) {
193
+ return createHash('sha256').update(`${kind}\0${value}`).digest('hex');
194
+ }
195
+
196
+ function memoryUsageIdentity(key) {
197
+ return opaqueUsageIdentity('forge.memory.key.v1', key);
198
+ }
199
+
200
+ function ownDataValue(value, field) {
201
+ if (!value || typeof value !== 'object' || isProxy(value)) return undefined;
202
+ let descriptor;
203
+ try { descriptor = Object.getOwnPropertyDescriptor(value, field); } catch { return undefined; }
204
+ return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
205
+ }
206
+
207
+ function usageResolutionOptions(options) {
208
+ const resolutionOptions = {};
209
+ for (const field of ['store', 'gitCommonDir', 'platform', 'realpath', 'databasePath']) {
210
+ const option = ownDataValue(options, field);
211
+ if (option !== undefined) resolutionOptions[field] = option;
212
+ }
213
+ return resolutionOptions;
214
+ }
215
+
216
+ function usageInvocationId(options) {
217
+ const value = ownDataValue(options, 'invocationId');
218
+ return typeof value === 'string' && value ? value : randomUUID();
219
+ }
220
+
221
+ function usageObservedAt(options) {
222
+ const invocationStartedAt = ownDataValue(options, 'invocationStartedAt');
223
+ if (typeof invocationStartedAt === 'string') return invocationStartedAt;
224
+ const now = ownDataValue(options, 'now');
225
+ if (typeof now === 'string') return now;
226
+ return new Date().toISOString();
227
+ }
228
+
229
+ function usageMemoryIdentities(notes) {
230
+ return notes
231
+ .map(note => ownDataValue(note, 'id'))
232
+ .filter(id => typeof id === 'string' && id.length > 0)
233
+ .map(memoryUsageIdentity);
234
+ }
235
+
236
+ function appendRecallUsage(store, identities, invocationId, scope, selectionDigest, observedAt) {
237
+ let appended = 0;
238
+ let failed = 0;
239
+ for (const memoryId of identities) {
240
+ try {
241
+ const result = store.append({
242
+ event_id: opaqueUsageIdentity('forge.memory.used.event.v1', `${invocationId}\0${memoryId}`),
243
+ memory_id: memoryId,
244
+ scope,
245
+ use_kind: 'cli-recall',
246
+ consumer_id: 'cli-recall',
247
+ selection_digest: selectionDigest,
248
+ observed_at: observedAt,
249
+ idempotency_key: opaqueUsageIdentity('forge.memory.used.idempotency.v1', `${invocationId}\0${memoryId}`),
250
+ });
251
+ if (result?.appended) appended += 1;
252
+ } catch {
253
+ // Evidence is advisory: a storage failure must never hide a useful recall.
254
+ failed += 1;
255
+ }
256
+ }
257
+ return { attempted: identities.length, appended, failed };
258
+ }
259
+
260
+ // Recall evidence deliberately accepts only the returned note identifiers. It hashes those
261
+ // identifiers immediately, so neither a user path, query, nor note content can reach storage.
262
+ function recordRecallUsage(projectRoot, selected, options = {}) {
263
+ const notes = Array.isArray(selected) ? selected : [];
264
+ if (notes.length === 0) return { attempted: 0, appended: 0, failed: 0 };
265
+ const usageStore = ownDataValue(options, 'usageStore');
266
+ const resolutionOptions = usageResolutionOptions(options);
267
+ const invocationId = usageInvocationId(options);
268
+ const observedAt = usageObservedAt(options);
269
+ const identities = usageMemoryIdentities(notes);
270
+ if (identities.length === 0) return { attempted: 0, appended: 0, failed: 0 };
271
+ let scope;
272
+ let selectionDigest;
273
+ let store;
274
+ try {
275
+ scope = opaqueUsageIdentity('forge.memory.scope.v1', resolveProjectId(projectRoot, resolutionOptions));
276
+ selectionDigest = opaqueUsageIdentity('forge.memory.selection.v1', identities.join('\0'));
277
+ store = createUsageEvidenceStore(usageStore || resolveStore(projectRoot, resolutionOptions));
278
+ } catch {
279
+ return { attempted: identities.length, appended: 0, failed: identities.length };
280
+ }
281
+ return appendRecallUsage(store, identities, invocationId, scope, selectionDigest, observedAt);
282
+ }
283
+
284
+ function usageProjection(projectRoot, key, options = {}) {
285
+ if (typeof key !== 'string' || !key) return null;
286
+ try {
287
+ const usageStore = ownDataValue(options, 'usageStore');
288
+ return createUsageEvidenceStore(usageStore || resolveStore(projectRoot, options))
289
+ .projection(opaqueUsageIdentity('forge.memory.key.v1', key));
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+
295
+ function usageProjectionStatus(projectRoot, keys, options = {}) {
296
+ if (!Array.isArray(keys) || keys.length === 0) return { available: true, projections: new Map() };
297
+ const rawKeys = [...new Set(keys.filter(key => typeof key === 'string' && key))].slice(0, 200);
298
+ if (rawKeys.length === 0) return { available: true, projections: new Map() };
299
+ const usageStore = ownDataValue(options, 'usageStore');
300
+ const identities = rawKeys.map(memoryUsageIdentity);
301
+ try {
302
+ const rows = createUsageEvidenceStore(usageStore || resolveStore(projectRoot, options)).projections(identities);
303
+ const keyByIdentity = new Map(rawKeys.map((key, index) => [identities[index], key]));
304
+ return {
305
+ available: true,
306
+ projections: new Map(rows.map(row => [keyByIdentity.get(row.memory_id), row]).filter(([key]) => key)),
307
+ };
308
+ } catch {
309
+ return { available: false, projections: new Map() };
310
+ }
311
+ }
312
+
313
+ function usageProjections(projectRoot, keys, options = {}) {
314
+ return usageProjectionStatus(projectRoot, keys, options).projections;
148
315
  }
149
316
 
150
317
  // Close and forget every cached default store. The CLI process is short-lived (the OS
@@ -170,5 +337,12 @@ module.exports = {
170
337
  count,
171
338
  searchRanked,
172
339
  searchRankedScored,
340
+ recordRecallUsage,
341
+ usageProjection,
342
+ usageProjectionStatus,
343
+ usageProjections,
344
+ memoryUsageIdentity,
345
+ resolveStore,
346
+ resolveProjectId,
173
347
  closeAll,
174
348
  };