forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
package/lib/commands/plan.js
CHANGED
|
@@ -13,6 +13,10 @@ const path = require('node:path');
|
|
|
13
13
|
const { execFileSync } = require('node:child_process');
|
|
14
14
|
const { resolveIssueBackend } = require('../issue-backend');
|
|
15
15
|
const { runIssueOperation } = require('../forge-issues');
|
|
16
|
+
const {
|
|
17
|
+
findPlanWorkFolder,
|
|
18
|
+
reconcilePlanAuthority,
|
|
19
|
+
} = require('../workflow/plan-authority');
|
|
16
20
|
|
|
17
21
|
// Constants for security
|
|
18
22
|
// Note: cwd is resolved at call-time via getExecOptions() to avoid stale require-time snapshots
|
|
@@ -483,7 +487,7 @@ async function registerBranchIssueLinkage(options, branch, issueId) {
|
|
|
483
487
|
branch,
|
|
484
488
|
actor: null,
|
|
485
489
|
issue_id: issueId,
|
|
486
|
-
work_folder: null,
|
|
490
|
+
work_folder: options.workFolder || null,
|
|
487
491
|
registered_at: new Date().toISOString(),
|
|
488
492
|
state: 'active',
|
|
489
493
|
});
|
|
@@ -976,11 +980,38 @@ async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
|
976
980
|
};
|
|
977
981
|
}
|
|
978
982
|
|
|
983
|
+
const workFolder = options.workFolder || findPlanWorkFolder(options.projectRoot || process.cwd(), featureSlug);
|
|
984
|
+
|
|
979
985
|
// F1: persist the branch->issue linkage so a plan-created branch resolves
|
|
980
986
|
// to its issue for kernel-authoritative stage state (dev/validate/ship).
|
|
981
987
|
// Kernel-only, best-effort.
|
|
982
988
|
if (issueBackend === 'kernel') {
|
|
983
|
-
await registerBranchIssueLinkage({ ...options, issueBackend }, branch.branchName, issue.issueId);
|
|
989
|
+
await registerBranchIssueLinkage({ ...options, issueBackend, workFolder }, branch.branchName, issue.issueId);
|
|
990
|
+
await withPlanDriver(options, driver => {
|
|
991
|
+
if (!driver || typeof driver.loadPlanSnapshot !== 'function') return null;
|
|
992
|
+
let existing;
|
|
993
|
+
try {
|
|
994
|
+
existing = driver.loadPlanSnapshot({ issue_id: issue.issueId }, {});
|
|
995
|
+
} catch (error) {
|
|
996
|
+
// Injectable issue runners used by embedding callers may not persist into
|
|
997
|
+
// this driver's store. With no repository artifacts there is nothing to
|
|
998
|
+
// reconcile; preserve that legacy embedding contract.
|
|
999
|
+
if (/^Issue .* not found in the kernel$/i.test(error.message)) {
|
|
1000
|
+
if (!workFolder) return null;
|
|
1001
|
+
throw new Error(`Issue ${issue.issueId} is not available for plan authority persistence`);
|
|
1002
|
+
}
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
if (!existing && !workFolder) return null;
|
|
1006
|
+
return reconcilePlanAuthority({
|
|
1007
|
+
driver,
|
|
1008
|
+
issueId: issue.issueId,
|
|
1009
|
+
projectRoot: options.projectRoot || process.cwd(),
|
|
1010
|
+
workFolder,
|
|
1011
|
+
mode: 'plan',
|
|
1012
|
+
repairCommand: `forge plan ${JSON.stringify(featureName)} --issue ${issue.issueId}`,
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
984
1015
|
}
|
|
985
1016
|
|
|
986
1017
|
// Build result summary
|
package/lib/commands/pr.js
CHANGED
|
@@ -81,6 +81,8 @@ async function handler(args, flags, projectRoot, opts) {
|
|
|
81
81
|
|
|
82
82
|
module.exports = {
|
|
83
83
|
name: 'pr',
|
|
84
|
+
githubAuth: (args = []) => ['ship', 'merge', 'shepherd']
|
|
85
|
+
.includes(stripGlobalFlags(args).find(arg => !arg.startsWith('-'))),
|
|
84
86
|
description:
|
|
85
87
|
'Unified pull-request surface: forge pr ship|preflight|shepherd|merge (wraps ship/preflight/shepherd/merge)',
|
|
86
88
|
usage,
|
|
@@ -88,11 +88,19 @@ function resolveBaseRef(exec) {
|
|
|
88
88
|
* @param {{ runAll?: boolean }} [opts]
|
|
89
89
|
* @returns {{ resolved: boolean, changedFiles: string[], reason?: string, baseRef?: string }}
|
|
90
90
|
*/
|
|
91
|
-
function resolveChangeSet(exec = execFileSync, { runAll = false } = {}) {
|
|
91
|
+
function resolveChangeSet(exec = execFileSync, { runAll = false, baseRef: explicitBaseRef = null } = {}) {
|
|
92
92
|
if (runAll) {
|
|
93
93
|
return { resolved: true, changedFiles: [], reason: 'whole-tree scope (--all)' };
|
|
94
94
|
}
|
|
95
|
-
const baseRef = resolveBaseRef(exec);
|
|
95
|
+
const baseRef = explicitBaseRef || resolveBaseRef(exec);
|
|
96
|
+
if (explicitBaseRef && !gitVerifyRef(exec, explicitBaseRef)) {
|
|
97
|
+
return {
|
|
98
|
+
resolved: false,
|
|
99
|
+
baseRef: explicitBaseRef,
|
|
100
|
+
changedFiles: [],
|
|
101
|
+
reason: `explicit base branch ${explicitBaseRef} is unavailable`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
96
104
|
if (!baseRef) {
|
|
97
105
|
return {
|
|
98
106
|
resolved: false,
|
package/lib/commands/push.js
CHANGED
|
@@ -4,8 +4,9 @@ const { execFileSync, spawnSync } = require('node:child_process');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const forgeToken = require('../../scripts/check-forge-token');
|
|
7
|
-
const { QUICK_LANE_ENV_VAR, QUICK_LANE_VALUE } = require('../../scripts/test');
|
|
7
|
+
const { QUICK_LANE_ENV_VAR, QUICK_LANE_VALUE, resolveFullSuiteTimeoutMs } = require('../../scripts/test');
|
|
8
8
|
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
9
|
+
const { verifyValidationReceipt } = require('../validation-receipt');
|
|
9
10
|
|
|
10
11
|
const isWindows = process.platform === 'win32';
|
|
11
12
|
|
|
@@ -127,8 +128,8 @@ function maybeTriggerShepherdAfterPush({
|
|
|
127
128
|
try {
|
|
128
129
|
trigger({ projectRoot });
|
|
129
130
|
return { armed: true };
|
|
130
|
-
} catch
|
|
131
|
-
return { armed: false, reason:
|
|
131
|
+
} catch {
|
|
132
|
+
return { armed: false, reason: 'shepherd-launch-failed' };
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -191,6 +192,94 @@ function runLint(spawnFn, pkgManager, projectRoot) {
|
|
|
191
192
|
return lintResult.status === 0;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Reports the kill signal a timed-out test run is terminated with. Shared by
|
|
197
|
+
* the spawn options and the timeout detector so the two cannot drift.
|
|
198
|
+
*/
|
|
199
|
+
const TEST_RUN_KILL_SIGNAL = 'SIGKILL';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Reads the terminating signal under either field name.
|
|
203
|
+
*
|
|
204
|
+
* `node:child_process.spawnSync` reports `signal`; native `Bun.spawnSync`
|
|
205
|
+
* reports `signalCode`. Read both rather than assuming one.
|
|
206
|
+
*
|
|
207
|
+
* @param {Object} testResult - spawnSync result object
|
|
208
|
+
* @returns {string|null} Signal name, or null when none was reported
|
|
209
|
+
*/
|
|
210
|
+
function terminationSignalOf(testResult) {
|
|
211
|
+
return testResult.signal || testResult.signalCode || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reports whether a test run was killed by its own wall-clock budget.
|
|
216
|
+
*
|
|
217
|
+
* Requires AFFIRMATIVE evidence. Verified against pinned Bun 1.3.12 on Windows:
|
|
218
|
+
* a `node:child_process` spawnSync timeout returns `status: null`,
|
|
219
|
+
* `signal: <killSignal>`, AND an error whose `code` is `ETIMEDOUT`. Native
|
|
220
|
+
* `Bun.spawnSync` instead sets `exitedDueToTimeout: true` with `signalCode`.
|
|
221
|
+
* Those two markers are the only proof of a timeout, and they are checked
|
|
222
|
+
* BEFORE the generic error branch — testing `error` first swallowed the timeout
|
|
223
|
+
* diagnostic in exactly the case it was written for.
|
|
224
|
+
*
|
|
225
|
+
* A bare kill signal with no marker is deliberately NOT treated as a timeout:
|
|
226
|
+
* an OOM kill or an operator `kill -9` produces the identical status/signal
|
|
227
|
+
* shape, and since a genuine budget kill always carries a marker, inferring
|
|
228
|
+
* from the signal alone buys nothing and costs a misdiagnosis (it would claim
|
|
229
|
+
* the budget elapsed and send the user to raise FORGE_TEST_TIMEOUT_MS).
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} testResult - spawnSync result object
|
|
232
|
+
* @returns {boolean} True when the run hit its budget
|
|
233
|
+
*/
|
|
234
|
+
function isTimeoutTermination(testResult) {
|
|
235
|
+
if (testResult.exitedDueToTimeout === true) return true;
|
|
236
|
+
return Boolean(testResult.error) && testResult.error.code === 'ETIMEDOUT';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Explain a test run that never produced an exit status.
|
|
241
|
+
*
|
|
242
|
+
* `spawnSync` reports `status: null` when the child was killed by a signal
|
|
243
|
+
* (timeout kill included) or never started at all. Without this the push
|
|
244
|
+
* aborted with no summary at all, which is indistinguishable from a genuine
|
|
245
|
+
* test failure.
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} testResult - spawnSync result object
|
|
248
|
+
* @param {number} timeoutMs - Wall-clock budget applied to the run
|
|
249
|
+
* @param {function} log - Logger function
|
|
250
|
+
*/
|
|
251
|
+
function reportTestRunTermination(testResult, timeoutMs, log) {
|
|
252
|
+
const signal = terminationSignalOf(testResult);
|
|
253
|
+
|
|
254
|
+
if (isTimeoutTermination(testResult)) {
|
|
255
|
+
log(
|
|
256
|
+
`Test run timed out after ${Math.round(timeoutMs / 1000)}s `
|
|
257
|
+
+ `(killed by ${signal || 'timeout'}) — push aborted.`,
|
|
258
|
+
);
|
|
259
|
+
log('Raise the budget with FORGE_TEST_TIMEOUT_MS if this machine is slower than the default.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (testResult.error) {
|
|
264
|
+
log(`Test run could not complete: ${testResult.error.message}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (signal) {
|
|
269
|
+
// Signalled without timeout evidence: report the fact and the likely
|
|
270
|
+
// causes. Do NOT blame the budget — that sends the user after the wrong
|
|
271
|
+
// lever and hides the real failure.
|
|
272
|
+
log(
|
|
273
|
+
`Test run was terminated by ${signal} before finishing — push aborted. `
|
|
274
|
+
+ `The run had a ${Math.round(timeoutMs / 1000)}s budget but reported no timeout, `
|
|
275
|
+
+ 'so this is most likely an external kill or an out-of-memory kill.',
|
|
276
|
+
);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
log('Test run ended without an exit status — push aborted.');
|
|
281
|
+
}
|
|
282
|
+
|
|
194
283
|
/**
|
|
195
284
|
* Run tests unless in quick mode, logging warnings for first push.
|
|
196
285
|
* @param {function} spawnFn - spawnSync or mock
|
|
@@ -199,9 +288,10 @@ function runLint(spawnFn, pkgManager, projectRoot) {
|
|
|
199
288
|
* @param {string} projectRoot - Absolute path to project root
|
|
200
289
|
* @param {boolean} quickMode - Whether to skip tests
|
|
201
290
|
* @param {function} log - Logger function
|
|
291
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment used to resolve the test budget
|
|
202
292
|
* @returns {boolean|undefined} True if tests passed, undefined if skipped
|
|
203
293
|
*/
|
|
204
|
-
function runTests(spawnFn, execFn, pkgManager, projectRoot, quickMode, log) {
|
|
294
|
+
function runTests(spawnFn, execFn, pkgManager, projectRoot, quickMode, log, env = process.env) {
|
|
205
295
|
if (quickMode) {
|
|
206
296
|
log('Tests skipped (--quick) — CI will run full suite on GitHub');
|
|
207
297
|
const branch = getCurrentBranch(execFn);
|
|
@@ -211,12 +301,25 @@ function runTests(spawnFn, execFn, pkgManager, projectRoot, quickMode, log) {
|
|
|
211
301
|
return undefined;
|
|
212
302
|
}
|
|
213
303
|
|
|
304
|
+
// `forge push` runs the package-level `test` script, i.e. the whole suite.
|
|
305
|
+
// Budget it from the shared full-suite budget in scripts/test.js — which is
|
|
306
|
+
// sized as ~2x the measured healthy full-suite runtime and overridable with
|
|
307
|
+
// FORGE_TEST_TIMEOUT_MS — instead of an arbitrary local cap: the old fixed
|
|
308
|
+
// 120s killed a healthy full run mid-suite and pushed nothing. Never
|
|
309
|
+
// reintroduce a local constant here; change the shared budget with a fresh
|
|
310
|
+
// measurement instead.
|
|
311
|
+
const timeoutMs = resolveFullSuiteTimeoutMs(env);
|
|
214
312
|
const testResult = spawnFn(pkgManager, ['run', 'test'], {
|
|
215
313
|
stdio: 'inherit',
|
|
216
314
|
shell: isWindows,
|
|
217
315
|
cwd: projectRoot,
|
|
218
|
-
timeout:
|
|
316
|
+
timeout: timeoutMs,
|
|
317
|
+
killSignal: TEST_RUN_KILL_SIGNAL,
|
|
219
318
|
});
|
|
319
|
+
if (testResult.status === null || testResult.status === undefined) {
|
|
320
|
+
reportTestRunTermination(testResult, timeoutMs, log);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
220
323
|
return testResult.status === 0;
|
|
221
324
|
}
|
|
222
325
|
|
|
@@ -244,6 +347,7 @@ module.exports = {
|
|
|
244
347
|
const existsFn = deps?.existsSync || fs.existsSync;
|
|
245
348
|
const log = deps?.log || console.log;
|
|
246
349
|
const tokenWrite = deps?.writeForgeToken || forgeToken.write;
|
|
350
|
+
const verifyReceipt = deps?.verifyValidationReceipt || verifyValidationReceipt;
|
|
247
351
|
|
|
248
352
|
const quickMode = !!(flags && (flags['--quick'] || flags.quick));
|
|
249
353
|
const pkgManager = detectPackageManager(projectRoot, existsFn);
|
|
@@ -266,9 +370,16 @@ module.exports = {
|
|
|
266
370
|
}
|
|
267
371
|
|
|
268
372
|
// Step 3: Tests (skip in quick mode)
|
|
269
|
-
const
|
|
373
|
+
const receipt = quickMode
|
|
374
|
+
? { valid: false }
|
|
375
|
+
: verifyReceipt(resolveGitToplevel(execFn, projectRoot));
|
|
376
|
+
const validationReused = receipt.valid === true;
|
|
377
|
+
if (validationReused) log(`Tests reused from full validation at ${receipt.head || 'current HEAD'}`);
|
|
378
|
+
const testsPassed = validationReused
|
|
379
|
+
? true
|
|
380
|
+
: runTests(spawnFn, execFn, pkgManager, projectRoot, quickMode, log, deps?.env || process.env);
|
|
270
381
|
if (!quickMode && !testsPassed) {
|
|
271
|
-
return { success: false, quickMode, lintPassed: true, testsPassed: false, pushed: false };
|
|
382
|
+
return { success: false, quickMode, lintPassed: true, testsPassed: false, validationReused, pushed: false };
|
|
272
383
|
}
|
|
273
384
|
|
|
274
385
|
// Step 4: Write nonce token so lefthook skips pre-push hooks.
|
|
@@ -290,7 +401,7 @@ module.exports = {
|
|
|
290
401
|
success: false,
|
|
291
402
|
quickMode,
|
|
292
403
|
lintPassed: true,
|
|
293
|
-
...(quickMode ? {} : { testsPassed: true }),
|
|
404
|
+
...(quickMode ? {} : { testsPassed: true, validationReused }),
|
|
294
405
|
pushed: false,
|
|
295
406
|
};
|
|
296
407
|
}
|
|
@@ -305,7 +416,7 @@ module.exports = {
|
|
|
305
416
|
success: true,
|
|
306
417
|
quickMode,
|
|
307
418
|
lintPassed: true,
|
|
308
|
-
...(quickMode ? {} : { testsPassed: true }),
|
|
419
|
+
...(quickMode ? {} : { testsPassed: true, validationReused }),
|
|
309
420
|
pushed: true,
|
|
310
421
|
};
|
|
311
422
|
},
|
package/lib/commands/recall.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const memoryRouter = require('../memory/router');
|
|
4
|
+
const projectMemory = require('../project-memory');
|
|
5
|
+
const { types: { isProxy } } = require('node:util');
|
|
4
6
|
const { stripGlobalFlags } = require('../global-flags');
|
|
5
7
|
const { fenceUntrusted } = require('../untrusted-content');
|
|
6
8
|
const { applyBudget, buildSection, estimateTokens } = require('../orientation');
|
|
@@ -15,10 +17,8 @@ const RECALL_CONTENT_BUDGET = 1100;
|
|
|
15
17
|
// `--type`): `--type` is a reserved GLOBAL flag hard-validated to workflow classifications.
|
|
16
18
|
const TYPE_TAG_PREFIX = 'type:';
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
// not reimplemented), so scan a generous window of recent notes before filtering rather than
|
|
20
|
+
// Kind filtering is pushed into the Kernel read so limits and capped metadata stay truthful.
|
|
20
21
|
// the small default page — otherwise the type match could fall outside the default limit.
|
|
21
|
-
const TYPE_FILTER_SCAN = 1000;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Separate the optional positional query from `--kind <type>`, `--limit N`, and `--json`.
|
|
@@ -80,6 +80,13 @@ function withType(entry) {
|
|
|
80
80
|
return type ? { ...entry, type } : entry;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function privateCommandOption(options, field) {
|
|
84
|
+
if (!options || typeof options !== 'object' || isProxy(options)) return undefined;
|
|
85
|
+
let descriptor;
|
|
86
|
+
try { descriptor = Object.getOwnPropertyDescriptor(options, field); } catch { return undefined; }
|
|
87
|
+
return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
function formatEntry(entry) {
|
|
84
91
|
const date = entry.timestamp ? entry.timestamp.slice(0, 10) : '';
|
|
85
92
|
const trust = memoryTrustStatus({
|
|
@@ -106,48 +113,32 @@ function formatEntry(entry) {
|
|
|
106
113
|
return `- ${label}${marker}${typeMarker}${note}${tagSuffix}`;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} else {
|
|
130
|
-
capped = false;
|
|
116
|
+
function createUsageRecorder(projectRoot, recallStore, commandOpts) {
|
|
117
|
+
const invocationId = privateCommandOption(commandOpts, 'invocationId');
|
|
118
|
+
const usageStore = privateCommandOption(commandOpts, 'usageStore');
|
|
119
|
+
const onUsageEvidence = privateCommandOption(commandOpts, 'onUsageEvidence');
|
|
120
|
+
const invocationStartedAt = privateCommandOption(commandOpts, 'invocationStartedAt')
|
|
121
|
+
|| privateCommandOption(commandOpts, 'now')
|
|
122
|
+
|| new Date().toISOString();
|
|
123
|
+
const evidenceCallback = typeof onUsageEvidence === 'function' && !isProxy(onUsageEvidence)
|
|
124
|
+
? onUsageEvidence
|
|
125
|
+
: undefined;
|
|
126
|
+
return selected => {
|
|
127
|
+
const observation = projectMemory.recordRecallUsage(projectRoot, selected, {
|
|
128
|
+
invocationId,
|
|
129
|
+
usageStore: usageStore || recallStore,
|
|
130
|
+
invocationStartedAt,
|
|
131
|
+
});
|
|
132
|
+
if (evidenceCallback) {
|
|
133
|
+
// Observability is deliberately aggregate-only: callers never receive query, note,
|
|
134
|
+
// path, secret, or raw durable identifiers.
|
|
135
|
+
try { evidenceCallback(observation); } catch { /* advisory test seam */ }
|
|
131
136
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (json) {
|
|
135
|
-
// Object (not a bare array) so programmatic consumers see the total and whether the
|
|
136
|
-
// result was truncated (raise --limit to page further).
|
|
137
|
-
return {
|
|
138
|
-
success: true,
|
|
139
|
-
output: `${JSON.stringify({ notes, total, capped, scope }, null, 2)}\n`,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (notes.length === 0) {
|
|
144
|
-
const reason = query
|
|
145
|
-
? `No notes match "${query}".`
|
|
146
|
-
: 'No notes remembered yet. Use "forge remember <note>" to add one.';
|
|
147
|
-
return { success: true, output: reason };
|
|
148
|
-
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
149
139
|
|
|
150
|
-
|
|
140
|
+
function buildRecallSections(notes) {
|
|
141
|
+
return notes
|
|
151
142
|
.map((entry, index) => {
|
|
152
143
|
const content = formatEntry(entry);
|
|
153
144
|
if (estimateTokens(content) > RECALL_CONTENT_BUDGET) return null;
|
|
@@ -162,29 +153,35 @@ async function handler(args, flags, projectRoot) {
|
|
|
162
153
|
content,
|
|
163
154
|
priority: index,
|
|
164
155
|
preserve: false,
|
|
165
|
-
data: { trust },
|
|
156
|
+
data: { trust, memoryId: entry.id },
|
|
166
157
|
});
|
|
167
158
|
})
|
|
168
159
|
.filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function humanRecallHeader(query, capped, rendered, noteCount, total, scope) {
|
|
163
|
+
const noun = scope === 'all' && !query ? 'stored memory record(s)' : 'remembered note(s)';
|
|
164
|
+
if (query) {
|
|
165
|
+
return capped || rendered < noteCount
|
|
166
|
+
? `Top ${rendered} note(s) matching "${query}" (raise --limit for more):`
|
|
167
|
+
: `${rendered} note(s) matching "${query}":`;
|
|
168
|
+
}
|
|
169
|
+
if (capped || rendered < noteCount) {
|
|
170
|
+
return `Showing ${rendered} of ${total} ${noun} (newest first):`;
|
|
171
|
+
}
|
|
172
|
+
return `${rendered} ${noun}:`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderHumanRecall(notes, result, recordUsage) {
|
|
176
|
+
const sections = buildRecallSections(notes);
|
|
169
177
|
const budgeted = applyBudget(sections, RECALL_CONTENT_BUDGET).sections
|
|
170
178
|
.filter(section => section.content);
|
|
171
179
|
for (const section of budgeted) {
|
|
172
180
|
section.content = fenceUntrusted(section.content, { source: 'memory' });
|
|
173
181
|
}
|
|
182
|
+
recordUsage(budgeted.map(section => ({ id: section.data.memoryId })));
|
|
174
183
|
const rendered = budgeted.length;
|
|
175
|
-
const
|
|
176
|
-
let header;
|
|
177
|
-
if (query) {
|
|
178
|
-
// BM25 returns at most `limit`; when full, signal it is the TOP-N, not the whole set.
|
|
179
|
-
header = capped || rendered < notes.length
|
|
180
|
-
? `Top ${rendered} note(s) matching "${query}" (raise --limit for more):`
|
|
181
|
-
: `${rendered} note(s) matching "${query}":`;
|
|
182
|
-
} else if (capped || rendered < notes.length) {
|
|
183
|
-
// Never a bare full dump: show the rendered count and the true total.
|
|
184
|
-
header = `Showing ${rendered} of ${total} ${noun} (newest first):`;
|
|
185
|
-
} else {
|
|
186
|
-
header = `${rendered} ${noun}:`;
|
|
187
|
-
}
|
|
184
|
+
const header = humanRecallHeader(result.query, result.capped, rendered, notes.length, result.total, result.scope);
|
|
188
185
|
const confirmed = budgeted.filter(section => section.data.trust === 'confirmed');
|
|
189
186
|
const suggested = budgeted.filter(section => section.data.trust === 'suggested');
|
|
190
187
|
const groups = [
|
|
@@ -195,10 +192,47 @@ async function handler(args, flags, projectRoot) {
|
|
|
195
192
|
title,
|
|
196
193
|
...entries.map(entry => entry.content),
|
|
197
194
|
]);
|
|
198
|
-
return {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
return { success: true, output: [header, ...body].join('\n') };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function emptyRecall(query) {
|
|
199
|
+
const reason = query
|
|
200
|
+
? `No notes match "${query}".`
|
|
201
|
+
: 'No notes remembered yet. Use "forge remember <note>" to add one.';
|
|
202
|
+
return { success: true, output: reason };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handler(args, flags, projectRoot, commandOpts = {}) {
|
|
206
|
+
const { query, limit, type, json } = parseArgs(args);
|
|
207
|
+
// `--all` is a GLOBAL boolean flag: in production bin/forge.js strips it from
|
|
208
|
+
// args and sets flags.all; on a direct handler call it may still be in args.
|
|
209
|
+
const all = Boolean(flags && flags.all) || args.includes('--all');
|
|
210
|
+
|
|
211
|
+
// A `--type` filter scans a generous recent window, then keeps only matching notes — the
|
|
212
|
+
// read stays entirely in the existing store (no schema change). `--limit` is re-applied
|
|
213
|
+
// AFTER filtering so it caps the typed result set, not the pre-filter scan.
|
|
214
|
+
// Resolve once for the complete recall: selection, count, and advisory evidence share
|
|
215
|
+
// one cached driver/connection rather than each opening or migrating independently.
|
|
216
|
+
const recallStore = projectMemory.resolveStore(projectRoot);
|
|
217
|
+
const result = memoryRouter.recall(projectRoot, { query, limit, all, kind: type }, { store: recallStore });
|
|
218
|
+
const notes = result.notes.map(withType);
|
|
219
|
+
const recordUsage = createUsageRecorder(projectRoot, recallStore, commandOpts);
|
|
220
|
+
|
|
221
|
+
if (json) {
|
|
222
|
+
// Object (not a bare array) so programmatic consumers see the total and whether the
|
|
223
|
+
// result was truncated (raise --limit to page further).
|
|
224
|
+
recordUsage(notes);
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
output: `${JSON.stringify({ notes, total: result.total, capped: result.capped, scope: result.scope }, null, 2)}\n`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (notes.length === 0) {
|
|
232
|
+
return emptyRecall(query);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return renderHumanRecall(notes, result, recordUsage);
|
|
202
236
|
}
|
|
203
237
|
|
|
204
238
|
module.exports = {
|
package/lib/commands/release.js
CHANGED
|
@@ -9,12 +9,13 @@ const {
|
|
|
9
9
|
const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
|
|
10
10
|
const { normalizeArgs, normalizeIssueResult, withResolvedIssueBackend } = require('./_issue');
|
|
11
11
|
const { generateNpmPublishWorkflow } = require('../npm-publish-workflow');
|
|
12
|
+
const { updateBunWorkflowPins } = require('../bun-workflow-pins');
|
|
12
13
|
|
|
13
14
|
// `forge release <id>` releases a claimed issue; `forge release check` runs the
|
|
14
15
|
// release-readiness gate. The two share the top-level verb, so this command
|
|
15
16
|
// dispatches `check` to the gate and routes everything else through the shared
|
|
16
17
|
// issue dispatch (resolve backend → runIssueOperation('release') → normalize).
|
|
17
|
-
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit | forge release generate-npm-workflow';
|
|
18
|
+
const usage = 'Usage: forge release <id> | forge release check --target 0.1.0 [--json] | forge release regen-audit | forge release generate-npm-workflow --expect-head <full-sha> | forge release update-bun-pins --expect-head <full-sha>';
|
|
18
19
|
|
|
19
20
|
async function runReleaseIssue(args, projectRoot, opts = {}) {
|
|
20
21
|
const resolved = withResolvedIssueBackend(projectRoot, opts);
|
|
@@ -45,23 +46,42 @@ function parseReleaseArgs(args = []) {
|
|
|
45
46
|
return false;
|
|
46
47
|
}
|
|
47
48
|
const previous = args[index - 1];
|
|
48
|
-
return previous !== '--target';
|
|
49
|
+
return previous !== '--target' && previous !== '--expect-head';
|
|
49
50
|
});
|
|
50
51
|
|
|
51
52
|
return {
|
|
52
53
|
subcommand,
|
|
53
54
|
target: readOption(args, '--target', SUPPORTED_TARGET),
|
|
55
|
+
expectedHead: readOption(args, '--expect-head', null),
|
|
54
56
|
json: args.includes('--json'),
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
async function handler(args, _flags, projectRoot, opts = {}) {
|
|
59
61
|
const parsed = parseReleaseArgs(args);
|
|
62
|
+
if (parsed.subcommand === 'update-bun-pins') {
|
|
63
|
+
const updater = opts.updateBunWorkflowPins || updateBunWorkflowPins;
|
|
64
|
+
const updated = await updater(projectRoot, {
|
|
65
|
+
env: opts.env,
|
|
66
|
+
kernelDeps: opts.kernelDeps,
|
|
67
|
+
expectedHead: parsed.expectedHead,
|
|
68
|
+
resolveHead: opts.resolveHead,
|
|
69
|
+
});
|
|
70
|
+
return updated.success
|
|
71
|
+
? {
|
|
72
|
+
success: true,
|
|
73
|
+
updated,
|
|
74
|
+
output: `Pinned ${updated.paths.length} workflows to Bun ${updated.version}.\n`,
|
|
75
|
+
}
|
|
76
|
+
: updated;
|
|
77
|
+
}
|
|
60
78
|
|
|
61
79
|
if (parsed.subcommand === 'generate-npm-workflow') {
|
|
62
80
|
const generated = await generateNpmPublishWorkflow(projectRoot, {
|
|
63
81
|
env: opts.env,
|
|
64
82
|
kernelDeps: opts.kernelDeps,
|
|
83
|
+
expectedHead: parsed.expectedHead,
|
|
84
|
+
resolveHead: opts.resolveHead,
|
|
65
85
|
});
|
|
66
86
|
return generated.success
|
|
67
87
|
? {
|
|
@@ -111,6 +131,7 @@ module.exports = {
|
|
|
111
131
|
usage,
|
|
112
132
|
flags: {
|
|
113
133
|
'--target <version>': 'Release target to check',
|
|
134
|
+
'--expect-head <full-sha>': 'Required exact HEAD for protected workflow generation',
|
|
114
135
|
'--json': 'Emit the readiness report as JSON',
|
|
115
136
|
},
|
|
116
137
|
handler,
|
package/lib/commands/remember.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const memoryRouter = require('../memory/router');
|
|
4
5
|
const { stripGlobalFlags } = require('../global-flags');
|
|
5
6
|
|
|
@@ -22,6 +23,7 @@ const STRUCTURED_FIELDS = [
|
|
|
22
23
|
// `--kind` (NOT `--type`): `--type` is a reserved GLOBAL flag that bin/forge.js hard-validates
|
|
23
24
|
// against workflow classifications (critical|standard|…), so it can never carry a note type.
|
|
24
25
|
const TYPE_TAG_PREFIX = 'type:';
|
|
26
|
+
const CONTENT_HASH_TAG_PREFIX = 'content-hash:';
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Split positional note words from `--kind`, `--tag <label>`, the structured field flags,
|
|
@@ -105,15 +107,37 @@ async function handler(args, _flags, projectRoot) {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
// The type rides as a reserved tag so it is stored and filterable without a schema change.
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
+
const contentHash = type === 'session-summary'
|
|
111
|
+
? crypto.createHash('sha256').update(JSON.stringify({
|
|
112
|
+
body,
|
|
113
|
+
tags: [...tags].sort((left, right) => left.localeCompare(right)),
|
|
114
|
+
type,
|
|
115
|
+
}), 'utf8').digest('hex')
|
|
116
|
+
: null;
|
|
117
|
+
const metadata = contentHash ? { kind: 'session-summary', content_hash: contentHash } : undefined;
|
|
118
|
+
const hashTag = contentHash ? `${CONTENT_HASH_TAG_PREFIX}${contentHash}` : null;
|
|
119
|
+
const allTags = [
|
|
120
|
+
...tags,
|
|
121
|
+
...(type ? [`${TYPE_TAG_PREFIX}${type}`] : []),
|
|
122
|
+
...(hashTag ? [hashTag] : []),
|
|
123
|
+
];
|
|
124
|
+
let entry;
|
|
125
|
+
if (hashTag) {
|
|
126
|
+
const matches = memoryRouter.recall(projectRoot, {
|
|
127
|
+
query: contentHash,
|
|
128
|
+
limit: 2,
|
|
129
|
+
all: true,
|
|
130
|
+
}).notes;
|
|
131
|
+
entry = matches.find(note => Array.isArray(note.tags) && note.tags.includes(hashTag));
|
|
132
|
+
}
|
|
133
|
+
if (!entry) entry = memoryRouter.append(projectRoot, body, { tags: allTags });
|
|
110
134
|
|
|
111
135
|
if (json) {
|
|
112
|
-
const payload = type ? { ...entry, type } : entry;
|
|
136
|
+
const payload = type ? { ...entry, type, metadata } : entry;
|
|
113
137
|
return { success: true, output: `${JSON.stringify(payload, null, 2)}\n` };
|
|
114
138
|
}
|
|
115
139
|
|
|
116
|
-
const userTags = entry.tags.filter(tag => !tag.startsWith(TYPE_TAG_PREFIX));
|
|
140
|
+
const userTags = entry.tags.filter(tag => !tag.startsWith(TYPE_TAG_PREFIX) && !tag.startsWith(CONTENT_HASH_TAG_PREFIX));
|
|
117
141
|
const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
|
|
118
142
|
const typePrefix = type ? `(${type}) ` : '';
|
|
119
143
|
return {
|