forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5
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 +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +21 -1
- package/bin/forge.js +16 -369
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +9 -4
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +117 -17
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +397 -100
- package/lib/agents-config.js +5 -0
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +358 -13
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/plan.js +23 -115
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +23 -3
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +18 -4
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +191 -95
- package/lib/commands/shepherd.js +49 -4
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +383 -0
- package/lib/commands/status.js +54 -33
- package/lib/commands/test.js +56 -34
- package/lib/commands/worktree.js +247 -43
- package/lib/core/runtime-graph.js +89 -15
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +64 -491
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +5 -5
- package/lib/hook-renderer.js +147 -16
- package/lib/insights.js +96 -80
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +44 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +292 -18
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +212 -0
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +371 -49
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +9 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +842 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +252 -0
- package/lib/pr-monitor/watch-lifecycle.js +14 -2
- package/lib/pr-pull.js +98 -24
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +40 -0
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/shell-utils.js +1 -1
- package/lib/skill-eval.js +750 -0
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +47 -9
- package/lib/using-forge.js +328 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +6 -7
- package/rules/using-forge.md +24 -0
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +184 -44
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +10 -11
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +5 -11
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +84 -38
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +10 -12
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +92 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/beads-nudge.js +0 -91
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/commands/board.js +0 -64
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -192
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/lib/agents-config.js
CHANGED
|
@@ -93,6 +93,10 @@ Forge currently supports Claude Code, Codex, and Cursor. Hermes support is plann
|
|
|
93
93
|
- **Codex** - OpenAI's CLI agent, skills-based workflow
|
|
94
94
|
- **Cursor** - IDE-integrated, native Plan/Ask/Debug modes
|
|
95
95
|
|
|
96
|
+
## Skill Dispatch (auto-trigger)
|
|
97
|
+
|
|
98
|
+
Before ANY response — including clarifying questions or exploring the codebase — if there is even a 1% chance a Forge skill applies, invoke it, then announce \`Using [skill] to [purpose]\`. Invoke the \`using-forge\` dispatch skill (auto-discovered from your agent's own skills — Forge setup installs it into each harness's skills dir; it carries the 1%-rule and routing table), or run \`forge skill for "<situation>"\` for the deterministic best-fit skill. This is agent-agnostic — never branch on harness identity.
|
|
99
|
+
|
|
96
100
|
## Quick Start
|
|
97
101
|
|
|
98
102
|
\`\`\`bash
|
|
@@ -962,6 +966,7 @@ Choose based on server documentation.
|
|
|
962
966
|
module.exports = {
|
|
963
967
|
detectProjectMetadata,
|
|
964
968
|
generateAgentsMd,
|
|
969
|
+
generateAgentsMdContent,
|
|
965
970
|
generateCursorConfig,
|
|
966
971
|
generateArchitectureDoc,
|
|
967
972
|
generateConfigurationDoc,
|
package/lib/audit-evidence.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
const { execFileSync } = require('node:child_process');
|
|
2
|
-
const fs = require('node:fs');
|
|
3
1
|
const path = require('node:path');
|
|
2
|
+
const { randomUUID } = require('node:crypto');
|
|
3
|
+
const { appendCappedJsonlRecord } = require('./capped-jsonl-log');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subagent evidence lands in the local append-only log D25 specifies, now that
|
|
7
|
+
* the Beads audit CLI is retired along with the rest of the Beads runtime. The
|
|
8
|
+
* log is the only copy of an entry, so a record carries the prompt and response
|
|
9
|
+
* the Beads entry used to hold; it is capped like the protected-state log so a
|
|
10
|
+
* long dev run cannot grow it unbounded.
|
|
11
|
+
*/
|
|
12
|
+
const AUDIT_EVIDENCE_LOG = '.forge/log.jsonl';
|
|
13
|
+
const AUDIT_EVIDENCE_MAX_RECORDS = 500;
|
|
4
14
|
|
|
5
15
|
const VERDICT_LABELS = {
|
|
6
16
|
PASS: 'good',
|
|
@@ -40,15 +50,6 @@ const SECRET_TEXT_PATTERNS = [
|
|
|
40
50
|
/\bsk-[A-Za-z0-9_-]{8,}\b/g,
|
|
41
51
|
];
|
|
42
52
|
|
|
43
|
-
function defaultRunCommand(command, args, options = {}) {
|
|
44
|
-
return execFileSync(command, args, {
|
|
45
|
-
cwd: options.cwd || process.cwd(),
|
|
46
|
-
encoding: 'utf8',
|
|
47
|
-
input: options.input,
|
|
48
|
-
timeout: options.timeout || 120000,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
53
|
function redactString(value) {
|
|
53
54
|
return SECRET_TEXT_PATTERNS.reduce(
|
|
54
55
|
(current, entry) => {
|
|
@@ -132,96 +133,62 @@ function buildSubagentAuditPayload(event) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Best-effort: a failed append is reported back to the caller, never thrown, so
|
|
138
|
+
* losing the evidence can never fail the command that produced it.
|
|
139
|
+
*/
|
|
140
|
+
function appendAuditRecord(record, options) {
|
|
141
|
+
const logPath = path.resolve(options.cwd || process.cwd(), AUDIT_EVIDENCE_LOG);
|
|
142
|
+
const appendRecord = options.appendRecord || appendCappedJsonlRecord;
|
|
143
|
+
|
|
137
144
|
try {
|
|
138
|
-
|
|
139
|
-
return
|
|
140
|
-
} catch (
|
|
141
|
-
|
|
142
|
-
const match = /\bint-[A-Za-z0-9_-]+\b/.exec(String(output));
|
|
143
|
-
return match ? match[0] : null;
|
|
145
|
+
appendRecord(logPath, record, options.maxRecords || AUDIT_EVIDENCE_MAX_RECORDS);
|
|
146
|
+
return { success: true, logPath };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { success: false, logPath, error: error.message };
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
return String(output).includes('--meta-json');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function writeFallbackMetadata(payload, entryId, options) {
|
|
153
|
-
if (!entryId || !payload.metadata || Object.keys(payload.metadata).length === 0) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const cwd = options.cwd || process.cwd();
|
|
158
|
-
const fsImpl = options.fs || fs;
|
|
159
|
-
const forgeDir = path.join(cwd, '.forge').replace(/\\/g, '/');
|
|
160
|
-
const logPath = path.join(forgeDir, 'log.jsonl').replace(/\\/g, '/');
|
|
161
|
-
const line = {
|
|
152
|
+
function buildAuditEvidenceRecord(payload, entryId, recordedAt) {
|
|
153
|
+
return {
|
|
162
154
|
kind: 'forge.auditEvidence',
|
|
163
|
-
sourceOfTruth: '
|
|
164
|
-
|
|
155
|
+
sourceOfTruth: 'forge_log',
|
|
156
|
+
entryId,
|
|
157
|
+
recordedAt,
|
|
165
158
|
command: redact(payload.command),
|
|
159
|
+
issueId: redact(payload.issueId),
|
|
166
160
|
role: redact(payload.role),
|
|
167
161
|
phase: redact(payload.phase),
|
|
168
162
|
taskId: redact(payload.taskId),
|
|
169
163
|
taskTitle: redact(payload.taskTitle),
|
|
170
|
-
|
|
164
|
+
model: redact(payload.model),
|
|
165
|
+
verdict: payload.verdict,
|
|
166
|
+
// Already redacted by buildSubagentAuditPayload.
|
|
167
|
+
prompt: payload.prompt,
|
|
168
|
+
response: payload.response,
|
|
169
|
+
metadata: payload.metadata,
|
|
171
170
|
};
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
fsImpl.mkdirSync(forgeDir, { recursive: true });
|
|
175
|
-
fsImpl.appendFileSync(logPath, `${JSON.stringify(line)}\n`);
|
|
176
|
-
return { path: logPath, line };
|
|
177
|
-
} catch (error) {
|
|
178
|
-
return { path: logPath, line, skipped: true, error: error.message };
|
|
179
|
-
}
|
|
180
171
|
}
|
|
181
172
|
|
|
182
173
|
function recordSubagentAuditEvent(event, options = {}) {
|
|
183
174
|
const payload = buildSubagentAuditPayload(event);
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const args = [
|
|
190
|
-
'audit',
|
|
191
|
-
'record',
|
|
192
|
-
'--json',
|
|
193
|
-
'--kind',
|
|
194
|
-
'llm_call',
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
if (payload.issueId) {
|
|
198
|
-
args.push('--issue-id', payload.issueId);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
args.push(
|
|
202
|
-
'--model',
|
|
203
|
-
payload.model,
|
|
204
|
-
'--prompt',
|
|
205
|
-
payload.prompt,
|
|
206
|
-
'--response',
|
|
207
|
-
payload.response,
|
|
175
|
+
const entryId = (options.newId || randomUUID)();
|
|
176
|
+
const record = buildAuditEvidenceRecord(
|
|
177
|
+
payload,
|
|
178
|
+
entryId,
|
|
179
|
+
options.now || new Date().toISOString(),
|
|
208
180
|
);
|
|
181
|
+
const written = appendAuditRecord(record, options);
|
|
209
182
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const output = runCommand('bd', args, { cwd: options.cwd || process.cwd() });
|
|
215
|
-
const entryId = parseRecordId(output, { requireJson: true });
|
|
216
|
-
const fallback = metaJsonSupported ? null : writeFallbackMetadata(payload, entryId, options);
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
success: Boolean(entryId),
|
|
220
|
-
entryId,
|
|
221
|
-
output,
|
|
183
|
+
const result = {
|
|
184
|
+
success: written.success,
|
|
185
|
+
entryId: written.success ? entryId : null,
|
|
186
|
+
record,
|
|
222
187
|
payload,
|
|
223
|
-
|
|
188
|
+
logPath: written.logPath,
|
|
224
189
|
};
|
|
190
|
+
if (!written.success) result.error = written.error;
|
|
191
|
+
return result;
|
|
225
192
|
}
|
|
226
193
|
|
|
227
194
|
function labelSubagentAuditEvent(entryId, event, options = {}) {
|
|
@@ -233,36 +200,29 @@ function labelSubagentAuditEvent(entryId, event, options = {}) {
|
|
|
233
200
|
return { skipped: true };
|
|
234
201
|
}
|
|
235
202
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
], { cwd: options.cwd || process.cwd() });
|
|
250
|
-
} catch (error) {
|
|
251
|
-
return {
|
|
252
|
-
success: false,
|
|
253
|
-
label,
|
|
254
|
-
error: error.message,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const labeledEntryId = parseRecordId(output, { requireJson: true });
|
|
203
|
+
// A label is its own record rather than a rewrite of the recorded entry: the
|
|
204
|
+
// log is append-only, so a verdict is read by joining on entryId.
|
|
205
|
+
const record = {
|
|
206
|
+
kind: 'forge.auditEvidenceLabel',
|
|
207
|
+
sourceOfTruth: 'forge_log',
|
|
208
|
+
entryId,
|
|
209
|
+
recordedAt: options.now || new Date().toISOString(),
|
|
210
|
+
role,
|
|
211
|
+
verdict,
|
|
212
|
+
label,
|
|
213
|
+
reason: `${role} verdict: ${verdict}`,
|
|
214
|
+
};
|
|
215
|
+
const written = appendAuditRecord(record, options);
|
|
259
216
|
|
|
260
|
-
|
|
261
|
-
success:
|
|
217
|
+
const result = {
|
|
218
|
+
success: written.success,
|
|
262
219
|
label,
|
|
263
|
-
entryId:
|
|
264
|
-
|
|
220
|
+
entryId: written.success ? entryId : null,
|
|
221
|
+
record,
|
|
222
|
+
logPath: written.logPath,
|
|
265
223
|
};
|
|
224
|
+
if (!written.success) result.error = written.error;
|
|
225
|
+
return result;
|
|
266
226
|
}
|
|
267
227
|
|
|
268
228
|
function recordAndLabelSubagentAuditEvent(event, options = {}) {
|
|
@@ -272,11 +232,12 @@ function recordAndLabelSubagentAuditEvent(event, options = {}) {
|
|
|
272
232
|
}
|
|
273
233
|
|
|
274
234
|
module.exports = {
|
|
235
|
+
AUDIT_EVIDENCE_LOG,
|
|
236
|
+
AUDIT_EVIDENCE_MAX_RECORDS,
|
|
275
237
|
VERDICT_LABELS,
|
|
276
238
|
buildSubagentAuditPayload,
|
|
277
239
|
recordSubagentAuditEvent,
|
|
278
240
|
labelSubagentAuditEvent,
|
|
279
241
|
recordAndLabelSubagentAuditEvent,
|
|
280
|
-
hasAuditMetaJsonSupport,
|
|
281
242
|
redact,
|
|
282
243
|
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The local append-only JSONL sink shared by every Forge log that must survive
|
|
6
|
+
* without a database handle: the protected-state audit log and the subagent
|
|
7
|
+
* audit-evidence log. Kept in its own module so both writers get the same
|
|
8
|
+
* concurrency and cap behaviour instead of a second copy drifting from the first.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const TRIM_LOCK_ATTEMPTS = 5;
|
|
12
|
+
const TRIM_LOCK_RETRY_MS = 20;
|
|
13
|
+
/**
|
|
14
|
+
* How many times the trim re-reads the log for records appended since its
|
|
15
|
+
* snapshot. Each pass copies less than the last, so a small bound is enough to
|
|
16
|
+
* drain a burst without letting a busy writer keep the rename waiting forever.
|
|
17
|
+
*/
|
|
18
|
+
const TRIM_DELTA_PASSES = 3;
|
|
19
|
+
/**
|
|
20
|
+
* How many times the trim rewrites the log to get it under the cap. A rewrite
|
|
21
|
+
* that drained concurrent appends can still sit above it, and one more quiet
|
|
22
|
+
* pass settles that; the bound stops a relentless writer from pinning the lock.
|
|
23
|
+
*/
|
|
24
|
+
const NEWLINE_BYTE = 0x0a;
|
|
25
|
+
/** The trim rewrite takes milliseconds, so an older lock outlived its process. */
|
|
26
|
+
const TRIM_LOCK_STALE_MS = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* Another process is using the file. POSIX reports only EEXIST/EBUSY, but
|
|
29
|
+
* Windows answers EPERM/EACCES both for a lock whose delete is still pending and
|
|
30
|
+
* for a replace of a log some other writer holds open.
|
|
31
|
+
*/
|
|
32
|
+
const FILE_CONTENDED_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
33
|
+
|
|
34
|
+
function sleepSync(ms) {
|
|
35
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function lockHeldSinceMs(lockPath) {
|
|
39
|
+
try {
|
|
40
|
+
return fs.lstatSync(lockPath).mtimeMs;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code === 'ENOENT') return null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function acquireTrimLock(lockPath) {
|
|
48
|
+
for (let attempt = 0; attempt < TRIM_LOCK_ATTEMPTS; attempt += 1) {
|
|
49
|
+
try {
|
|
50
|
+
return fs.openSync(lockPath, 'wx');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (!FILE_CONTENDED_CODES.has(error.code)) throw error;
|
|
53
|
+
|
|
54
|
+
const heldSince = lockHeldSinceMs(lockPath);
|
|
55
|
+
if (heldSince !== null && Date.now() - heldSince > TRIM_LOCK_STALE_MS) {
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(lockPath);
|
|
58
|
+
} catch {
|
|
59
|
+
// Another writer reclaimed it first; retry against theirs.
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
sleepSync(TRIM_LOCK_RETRY_MS);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function releaseTrimLock(handle, lockPath) {
|
|
72
|
+
try {
|
|
73
|
+
fs.closeSync(handle);
|
|
74
|
+
} catch {
|
|
75
|
+
// Already closed.
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
fs.unlinkSync(lockPath);
|
|
79
|
+
} catch {
|
|
80
|
+
// Already reclaimed as stale.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reads the log as whole records plus the byte offset they end at, so a later
|
|
86
|
+
* read can resume exactly where this one stopped. A trailing partial line is
|
|
87
|
+
* excluded from both: it belongs to an append still in flight, and cutting the
|
|
88
|
+
* offset at the last newline keeps the resume point on a record boundary.
|
|
89
|
+
*/
|
|
90
|
+
function snapshotJsonl(logPath) {
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = fs.readFileSync(logPath);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code !== 'ENOENT') throw error;
|
|
96
|
+
return { lines: [], size: 0 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const size = raw.lastIndexOf(NEWLINE_BYTE) + 1;
|
|
100
|
+
return { lines: raw.subarray(0, size).toString('utf8').split('\n').filter(Boolean), size };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The whole records appended past `offset`, or null when there are none yet. */
|
|
104
|
+
function readAppendsSince(logPath, offset) {
|
|
105
|
+
let size;
|
|
106
|
+
try {
|
|
107
|
+
// Almost every trim runs with nobody appending, so answer from a stat alone
|
|
108
|
+
// and keep the extra work off the lock in the common case.
|
|
109
|
+
size = fs.statSync(logPath).size;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error.code === 'ENOENT') return null;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
if (size <= offset) return null;
|
|
115
|
+
|
|
116
|
+
let handle;
|
|
117
|
+
try {
|
|
118
|
+
handle = fs.openSync(logPath, 'r');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code === 'ENOENT') return null;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const buffer = Buffer.alloc(size - offset);
|
|
126
|
+
const read = fs.readSync(handle, buffer, 0, buffer.length, offset);
|
|
127
|
+
const end = buffer.subarray(0, read).lastIndexOf(NEWLINE_BYTE) + 1;
|
|
128
|
+
if (end === 0) return null;
|
|
129
|
+
|
|
130
|
+
const bytes = buffer.subarray(0, end);
|
|
131
|
+
return { bytes, records: bytes.toString('utf8').split('\n').filter(Boolean).length };
|
|
132
|
+
} finally {
|
|
133
|
+
fs.closeSync(handle);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One rewrite of the log down to its cap, returning the record count it left
|
|
139
|
+
* behind — or null when a contended rename made it give up. Records drained from
|
|
140
|
+
* concurrent appenders ride above the cap, so the caller has to check.
|
|
141
|
+
* Must be called with the trim lock held.
|
|
142
|
+
*/
|
|
143
|
+
function rewriteToCap(logPath, maxRecords, pendingLine = null) {
|
|
144
|
+
const snapshot = snapshotJsonl(logPath);
|
|
145
|
+
if (snapshot.lines.length < maxRecords && pendingLine !== null) {
|
|
146
|
+
fs.appendFileSync(logPath, `${pendingLine}\n`, 'utf8');
|
|
147
|
+
return snapshot.lines.length + 1;
|
|
148
|
+
}
|
|
149
|
+
if (snapshot.lines.length <= maxRecords && pendingLine === null) return snapshot.lines.length;
|
|
150
|
+
|
|
151
|
+
const candidates = pendingLine === null ? snapshot.lines : [...snapshot.lines, pendingLine];
|
|
152
|
+
const kept = candidates.slice(-maxRecords);
|
|
153
|
+
// Write-then-rename so a crash mid-trim cannot leave a torn log behind.
|
|
154
|
+
const tempPath = `${logPath}.${process.pid}.tmp`;
|
|
155
|
+
fs.writeFileSync(tempPath, `${kept.join('\n')}\n`, 'utf8');
|
|
156
|
+
|
|
157
|
+
// Appenders hold no lock, so records keep landing in the log this rename is
|
|
158
|
+
// about to replace. Copy them onto the temp file first — repeatedly, since
|
|
159
|
+
// the copy itself takes time a writer can append into. Nothing heavier goes
|
|
160
|
+
// between the last copy and the rename, which is what keeps the window small.
|
|
161
|
+
let offset = snapshot.size;
|
|
162
|
+
let drained = 0;
|
|
163
|
+
for (let pass = 0; pass < TRIM_DELTA_PASSES; pass += 1) {
|
|
164
|
+
const appended = readAppendsSince(logPath, offset);
|
|
165
|
+
if (appended === null) break;
|
|
166
|
+
fs.appendFileSync(tempPath, appended.bytes);
|
|
167
|
+
offset += appended.bytes.length;
|
|
168
|
+
drained += appended.records;
|
|
169
|
+
}
|
|
170
|
+
if (pendingLine !== null && drained > 0) {
|
|
171
|
+
const prepared = snapshotJsonl(tempPath).lines;
|
|
172
|
+
fs.writeFileSync(tempPath, `${prepared.slice(-maxRecords).join('\n')}\n`, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
fs.renameSync(tempPath, logPath);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// Another writer still has the log open for its append. Same answer as
|
|
179
|
+
// losing the lock race: drop this trim, the next writer will do it.
|
|
180
|
+
if (!FILE_CONTENDED_CODES.has(error.code)) throw error;
|
|
181
|
+
fs.rmSync(tempPath, { force: true });
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return pendingLine === null ? kept.length + drained : Math.min(kept.length + drained, maxRecords);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function appendAtCap(logPath, recordLine, maxRecords) {
|
|
188
|
+
const snapshot = snapshotJsonl(logPath);
|
|
189
|
+
|
|
190
|
+
const lockPath = `${logPath}.lock`;
|
|
191
|
+
const lock = acquireTrimLock(lockPath);
|
|
192
|
+
// A held lock means another writer is already trimming. These logs are
|
|
193
|
+
// best-effort sinks on the hot path of a hook or a command, so skip and let
|
|
194
|
+
// the next writer past the cap trim instead of blocking the caller.
|
|
195
|
+
if (lock === null) {
|
|
196
|
+
fs.appendFileSync(logPath, `${recordLine}\n`, 'utf8');
|
|
197
|
+
return snapshot.lines.length + 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const rewritten = rewriteToCap(logPath, maxRecords, recordLine);
|
|
202
|
+
if (rewritten === null) {
|
|
203
|
+
fs.appendFileSync(logPath, `${recordLine}\n`, 'utf8');
|
|
204
|
+
return snapshotJsonl(logPath).lines.length;
|
|
205
|
+
}
|
|
206
|
+
return rewritten;
|
|
207
|
+
} finally {
|
|
208
|
+
releaseTrimLock(lock, lockPath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Below the cap, O_APPEND keeps each small record atomic. At the cap, the new
|
|
214
|
+
* record is prepared inside the locked replacement so failure cannot be
|
|
215
|
+
* reported after the authorization has already landed in the live log.
|
|
216
|
+
*
|
|
217
|
+
* That leaves one window, and the trim narrows rather than closes it: records
|
|
218
|
+
* appended after the trim's snapshot are drained onto the replacement file right
|
|
219
|
+
* before the rename, so only a record landing between the final drain and the
|
|
220
|
+
* rename syscall itself can still be lost. Closing it completely would mean
|
|
221
|
+
* locking every append, which these hot-path sinks do not pay for.
|
|
222
|
+
*/
|
|
223
|
+
function appendCappedJsonlRecord(logPath, record, maxRecords) {
|
|
224
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
225
|
+
const recordLine = JSON.stringify(record);
|
|
226
|
+
const snapshot = snapshotJsonl(logPath);
|
|
227
|
+
if (snapshot.lines.length < maxRecords) {
|
|
228
|
+
fs.appendFileSync(logPath, `${recordLine}\n`, 'utf8');
|
|
229
|
+
return snapshot.lines.length + 1;
|
|
230
|
+
}
|
|
231
|
+
return appendAtCap(logPath, recordLine, maxRecords);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
appendCappedJsonlRecord,
|
|
236
|
+
};
|
package/lib/commands/_issue.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
|
|
4
4
|
const { resolveIssueBackend, hasExplicitBackendSignal, shouldUseKernelBroker } = require('../issue-backend');
|
|
5
|
-
const { maybeWarnUnmigratedBeads } = require('../beads-nudge');
|
|
6
5
|
const {
|
|
7
6
|
ISSUE_COMMAND_SCHEMA_VERSION,
|
|
8
7
|
ISSUE_COMMAND_ERROR_SCHEMA_VERSION,
|
|
@@ -17,11 +16,10 @@ const { checkReadFirst } = require('../grounding/read-first');
|
|
|
17
16
|
const { recordContextLoaded } = require('../grounding/context-events');
|
|
18
17
|
|
|
19
18
|
// The Forge issue command surface. Each subcommand routes through the shared
|
|
20
|
-
// runIssueOperation, which
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// issue
|
|
24
|
-
// abstraction (lib/forge-issues.js + the issue adapters).
|
|
19
|
+
// runIssueOperation, which dispatches to the Forge Kernel — the only issue backend.
|
|
20
|
+
// This module therefore carries NO direct issue-tracker invocation or argv
|
|
21
|
+
// translation — those live in the backend abstraction (lib/forge-issues.js + the
|
|
22
|
+
// kernel issue adapter).
|
|
25
23
|
const SUBCOMMANDS = {
|
|
26
24
|
create: {
|
|
27
25
|
description: 'Create an issue via Forge',
|
|
@@ -67,10 +65,8 @@ const SUBCOMMANDS = {
|
|
|
67
65
|
description: 'Show issue statistics via Forge',
|
|
68
66
|
usage: 'forge issue stats [flags]',
|
|
69
67
|
},
|
|
70
|
-
// KAP-7: derived read queries. The
|
|
71
|
-
//
|
|
72
|
-
// backend maps each to its passthrough subcommand). They are READS, so they are
|
|
73
|
-
// intentionally NOT in WRITE_SUBCOMMANDS.
|
|
68
|
+
// KAP-7: derived read queries. The Kernel passes the operation name through
|
|
69
|
+
// unchanged. They are READS, so they are intentionally NOT in WRITE_SUBCOMMANDS.
|
|
74
70
|
blocked: {
|
|
75
71
|
description: 'Show blocked issues via Forge',
|
|
76
72
|
usage: 'forge issue blocked [flags]',
|
|
@@ -106,9 +102,7 @@ const SUBCOMMANDS = {
|
|
|
106
102
|
usage: 'forge issue owns <id> [--json]',
|
|
107
103
|
},
|
|
108
104
|
// Active-lease listing (kernel issue 7dc229d4). A bare passthrough to the Kernel
|
|
109
|
-
// lease table (kernel_claims)
|
|
110
|
-
// forge-issues.js rejects the Beads path explicitly. A READ, so intentionally NOT
|
|
111
|
-
// in WRITE_SUBCOMMANDS.
|
|
105
|
+
// lease table (kernel_claims). A READ, so intentionally NOT in WRITE_SUBCOMMANDS.
|
|
112
106
|
claims: {
|
|
113
107
|
description: 'Show active issue leases (claims) via Forge',
|
|
114
108
|
usage: 'forge issue claims [--json]',
|
|
@@ -389,8 +383,7 @@ function formatIssueHelp() {
|
|
|
389
383
|
}
|
|
390
384
|
|
|
391
385
|
// Map a CLI subcommand to the backend operation name. `dep` fans out to
|
|
392
|
-
// `dep.<action>`; every other subcommand uses its own name
|
|
393
|
-
// any tracker-specific translation, e.g. Beads claim -> `update <id> --claim`).
|
|
386
|
+
// `dep.<action>`; every other subcommand uses its own name.
|
|
394
387
|
function resolveIssueOperation(subcommand, args) {
|
|
395
388
|
if (subcommand === 'dep') {
|
|
396
389
|
return `dep.${normalizeArgs(args)[0]}`;
|
|
@@ -400,10 +393,8 @@ function resolveIssueOperation(subcommand, args) {
|
|
|
400
393
|
|
|
401
394
|
// The Kernel create payload (buildCreatePayload) reads only the --title flag, so a
|
|
402
395
|
// bare leading positional (`forge create "title"`) would be ignored and the title
|
|
403
|
-
// would default to the minted UUID.
|
|
404
|
-
//
|
|
405
|
-
// --title/--title= is present. The Beads backend keeps its native positional
|
|
406
|
-
// handling (this never runs for the Beads path).
|
|
396
|
+
// would default to the minted UUID. Translate a single leading bare positional into
|
|
397
|
+
// `--title <value>` when no explicit --title/--title= is present.
|
|
407
398
|
function withKernelCreateTitle(args) {
|
|
408
399
|
const hasTitle = args.some(
|
|
409
400
|
arg => arg === '--title' || (typeof arg === 'string' && arg.startsWith('--title=')),
|
|
@@ -454,10 +445,12 @@ function validateDepArgs(args) {
|
|
|
454
445
|
return null;
|
|
455
446
|
}
|
|
456
447
|
|
|
457
|
-
//
|
|
458
|
-
// shared runIssueOperation deps see it. OPT-IN ONLY: opts is left
|
|
459
|
-
// when no explicit signal is present (env/config/explicit),
|
|
460
|
-
//
|
|
448
|
+
// Normalize any explicit issue-backend signal and thread the resolved value into
|
|
449
|
+
// opts so the shared runIssueOperation deps see it. OPT-IN ONLY: opts is left
|
|
450
|
+
// byte-identical when no explicit signal is present (env/config/explicit), so the
|
|
451
|
+
// no-signal path stays untouched. A copy is returned — the caller's opts object is
|
|
452
|
+
// never mutated. A retired value (`beads`) warns here with the migrate pointer and
|
|
453
|
+
// resolves to the kernel.
|
|
461
454
|
function withResolvedIssueBackend(projectRoot, opts = {}) {
|
|
462
455
|
const env = opts.env || process.env;
|
|
463
456
|
const signalContext = { deps: opts, env, projectRoot };
|
|
@@ -479,13 +472,13 @@ function withResolvedIssueBackend(projectRoot, opts = {}) {
|
|
|
479
472
|
|
|
480
473
|
// The Kernel broker returns the issue-command contract shape
|
|
481
474
|
// ({ ok, schema_version, command, data, next_commands } or { ok:false, error })
|
|
482
|
-
// rather than the
|
|
475
|
+
// rather than the legacy { success, output }. The bin/forge.js result printer
|
|
483
476
|
// keys on `success`/`output`, so a raw kernel contract would render as
|
|
484
477
|
// "Command failed". Normalize ONLY the contract shape (ok defined, success
|
|
485
478
|
// undefined) into { success, output } here, at the command boundary — the kernel
|
|
486
479
|
// contract itself stays untouched. Every other result passes through byte-identical.
|
|
487
480
|
//
|
|
488
|
-
// Response-contract
|
|
481
|
+
// Response-contract guarantees:
|
|
489
482
|
// * SUCCESS → the printed envelope carries `ok:true` (consumers gate on it).
|
|
490
483
|
// * FAILURE → the contract `exit_code` is surfaced as `result.exitCode` so the bin
|
|
491
484
|
// printer exits with the error class's code (not always 1); and on
|
|
@@ -609,8 +602,7 @@ function splitLeadingIds(args = []) {
|
|
|
609
602
|
// which broke envelope parity for multi-id close. `ok` is true only when every id
|
|
610
603
|
// closed; per-id outcomes live in `data.results` and the contract `exit_code` of the
|
|
611
604
|
// first failure is surfaced as `exitCode` so the bin printer exits with the error
|
|
612
|
-
// class's code.
|
|
613
|
-
// `close id1 id2 ...` invocation.
|
|
605
|
+
// class's code.
|
|
614
606
|
async function runKernelBatchClose(runner, operation, ids, flags, projectRoot, opts, verifyEnabled = false) {
|
|
615
607
|
const results = [];
|
|
616
608
|
let allSucceeded = true;
|
|
@@ -700,13 +692,11 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
|
|
|
700
692
|
return { success: false, error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
|
|
701
693
|
}
|
|
702
694
|
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
// SILENTLY minted a junk issue (and could queue a GitHub projection). Help must
|
|
709
|
-
// never touch a backend.
|
|
695
|
+
// --help short-circuit: print the subcommand's usage and return BEFORE resolving
|
|
696
|
+
// the backend or dispatching any operation. Without this, a help request is
|
|
697
|
+
// forwarded to the Kernel as an operation arg: the plural path fails with a bare
|
|
698
|
+
// "Command failed" and the singular path SILENTLY mints a junk issue (and could
|
|
699
|
+
// queue a GitHub projection). Help must never touch a backend.
|
|
710
700
|
if (normalizeArgs(args).some(arg => arg === '--help' || arg === '-h')) {
|
|
711
701
|
return { success: true, output: `${spec.usage}\n\n${spec.description}` };
|
|
712
702
|
}
|
|
@@ -749,17 +739,16 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
|
|
|
749
739
|
}
|
|
750
740
|
}
|
|
751
741
|
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
// a runtime no-op (undefined under Beads) that documents the Kernel-capable surface.
|
|
742
|
+
// The Kernel is reached through the runIssueOperation seam. Naming the injected
|
|
743
|
+
// local `runIssueOperation` keeps the dispatch a literal call to a binding named
|
|
744
|
+
// `runIssueOperation` (the kernel-evidence gate is syntactic) while still honoring
|
|
745
|
+
// an injected runner.
|
|
757
746
|
const runIssueOperation = opts.runIssueOperation || defaultRunIssueOperation;
|
|
758
747
|
const operation = resolveIssueOperation(subcommand, args);
|
|
759
748
|
const operationArgs = resolveOperationArgs(subcommand, args, opts);
|
|
760
749
|
|
|
761
|
-
// Check-after-write (gate.issue_verify): resolved ONCE per invocation
|
|
762
|
-
//
|
|
750
|
+
// Check-after-write (gate.issue_verify): resolved ONCE per invocation. Reads never
|
|
751
|
+
// trigger a read-back.
|
|
763
752
|
const verifyEnabled = VERIFIED_SUBCOMMANDS.has(subcommand)
|
|
764
753
|
&& shouldUseKernelBroker(opts)
|
|
765
754
|
&& isIssueVerifyEnabled(projectRoot, opts);
|
|
@@ -779,7 +768,7 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
|
|
|
779
768
|
projectRoot,
|
|
780
769
|
{ ...opts, kernelBroker: opts.kernelBroker },
|
|
781
770
|
);
|
|
782
|
-
// Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a
|
|
771
|
+
// Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a legacy
|
|
783
772
|
// {success,output} shape). Warn-only: attaches verified/mismatches, never
|
|
784
773
|
// changes the result's success or exit code.
|
|
785
774
|
if (verifyEnabled && result && typeof result === 'object' && result.ok === true && result.success === undefined) {
|
|
@@ -787,10 +776,6 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
|
|
|
787
776
|
}
|
|
788
777
|
// Best-effort, non-blocking: mirror a stage-transition comment into stage_runs.
|
|
789
778
|
recordStageTransitionFromComment(subcommand, operationArgs, result, opts);
|
|
790
|
-
// Best-effort, non-blocking: nudge a returning 0.0.10 user whose empty Kernel
|
|
791
|
-
// read hides an unmigrated legacy issue store (kernel issue a5399f3d). The hint
|
|
792
|
-
// text lives in lib/beads-nudge.js so this hot path stays token-free.
|
|
793
|
-
maybeWarnUnmigratedBeads(subcommand, result, projectRoot, rawOpts);
|
|
794
779
|
// Grounding (gate.read_first): a successful `forge show <id>` counts as reading
|
|
795
780
|
// the issue, so append a `context.loaded` event. Best-effort and awaited (a
|
|
796
781
|
// fire-and-forget append could lose the event when the CLI process exits); a
|