forgedock 1.5.1 → 1.7.0
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/README.md +13 -2
- package/bin/engine/orchestrate-canary.mjs +15 -0
- package/bin/engine/orchestrate-canary.test.mjs +28 -0
- package/bin/forgedock.mjs +91 -21
- package/bin/hooks/session-start.mjs +22 -1
- package/bin/opencode-adapter.mjs +435 -0
- package/bin/registry.mjs +87 -0
- package/bin/runner.mjs +354 -43
- package/commands/orchestrate/phase-3-dependency.md +29 -4
- package/commands/orchestrate/phase-4-execution.md +197 -26
- package/commands/work-on/build.md +10 -1
- package/commands/work-on.md +10 -1
- package/docs/OPENCODE.md +132 -252
- package/package.json +3 -2
- package/scripts/check-spec-markers.sh +1 -0
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ $ /work-on #1230 "orchestrate: Layer 5 co-change signal is dead code"
|
|
|
56
56
|
|
|
57
57
|
### Try it in 30 seconds — on a throwaway repo, nothing to lose
|
|
58
58
|
|
|
59
|
-
**Requires:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (subscription or `ANTHROPIC_API_KEY`) · [GitHub CLI](https://cli.github.com/) (authenticated) · Node.js ≥ 18.
|
|
59
|
+
**Requires for this demo:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (subscription or `ANTHROPIC_API_KEY`) · [GitHub CLI](https://cli.github.com/) (authenticated) · Node.js ≥ 18.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
npx forgedock demo # spins up a risk-free demo repo and shows you the pipeline end to end
|
|
@@ -224,12 +224,23 @@ More ship today (web-property analytics, browser QA sweeps, self-benchmarking)
|
|
|
224
224
|
|
|
225
225
|
## Install
|
|
226
226
|
|
|
227
|
-
**Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) · [GitHub CLI](https://cli.github.com/) (authenticated) · Node.js ≥ 18.
|
|
227
|
+
**Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [OpenCode](https://opencode.ai/) · [GitHub CLI](https://cli.github.com/) (authenticated) · Node.js ≥ 18.
|
|
228
228
|
|
|
229
229
|
```bash
|
|
230
230
|
npx forgedock # checks your environment, installs commands into ~/.claude/commands/ (available in every Claude Code session on this machine), detects your repo, and hands you a reviewed forge.yaml
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
For OpenCode, install namespaced native commands without changing your provider
|
|
234
|
+
or `opencode.json`:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npx forgedock opencode install
|
|
238
|
+
# restart OpenCode, then: /forge/work-on <issue>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
See [OpenCode support](docs/OPENCODE.md) for architecture, token-loading rules,
|
|
242
|
+
current parity boundaries, and lifecycle commands.
|
|
243
|
+
|
|
233
244
|
**Install is always global**, to `~/.claude/commands/`. `--global` is still accepted on the command line for backward compatibility but has no effect — there's no other install location to opt out of.
|
|
234
245
|
|
|
235
246
|
> **Want engine-mode dispatch?** `npx forgedock` is transient — the `forgedock` binary isn't persisted in PATH after install. `/orchestrate` and `/autopilot` use agent dispatch mode by default, which is fully functional. To enable engine-mode dispatch (`forgedock run-issue`) with its durable phase table and fail-closed review gate, run `npm install -g forgedock` instead.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI contract for the standing-query re-resolution canary in the orchestrator.
|
|
4
|
+
*/
|
|
5
|
+
import { shouldReResolve } from "./resolve.mjs";
|
|
6
|
+
|
|
7
|
+
const [kind, pattern, enabled, maxRounds, rounds] = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
const classified = { kind, pattern, args: [] };
|
|
10
|
+
const config = {
|
|
11
|
+
enabled: enabled === "false" ? false : enabled,
|
|
12
|
+
maxRounds: maxRounds === "unbounded" ? undefined : Number(maxRounds),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
process.stdout.write(`${JSON.stringify(shouldReResolve(classified, config, Number(rounds)))}\n`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const canary = join(here, "orchestrate-canary.mjs");
|
|
9
|
+
|
|
10
|
+
function runCanary(...args) {
|
|
11
|
+
const result = spawnSync(process.execPath, [canary, ...args], { encoding: "utf8" });
|
|
12
|
+
assert.equal(result.status, 0, result.stderr);
|
|
13
|
+
return JSON.parse(result.stdout);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("orchestrate re-resolution canary CLI", () => {
|
|
17
|
+
it("re-resolves a query with the documented argument contract", () => {
|
|
18
|
+
assert.deepEqual(runCanary("query", "priority", "true", "unbounded", "0"), {
|
|
19
|
+
reResolve: true,
|
|
20
|
+
reason: 'query pattern "priority" is a standing predicate — re-resolving',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("preserves literal-set and max-round termination", () => {
|
|
25
|
+
assert.equal(runCanary("literal", "literal-numbers", "true", "3", "0").reResolve, false);
|
|
26
|
+
assert.equal(runCanary("query", "milestone", "true", "3", "3").reResolve, false);
|
|
27
|
+
});
|
|
28
|
+
});
|
package/bin/forgedock.mjs
CHANGED
|
@@ -59,8 +59,8 @@ import {
|
|
|
59
59
|
detectClaudeVersion,
|
|
60
60
|
loadBreakpoints,
|
|
61
61
|
hasFeature,
|
|
62
|
-
getPersistedHomeState,
|
|
63
62
|
setPersistedHomeState,
|
|
63
|
+
getInstalledCommandsDriftStatus,
|
|
64
64
|
} from "./registry.mjs";
|
|
65
65
|
import { renderMark, ember } from "./cinema.mjs";
|
|
66
66
|
import {
|
|
@@ -2740,27 +2740,16 @@ async function doctor(fix = false) {
|
|
|
2740
2740
|
`incomplete (missing: ${missingDirs.join(", ")}). Run: npx forgedock install`,
|
|
2741
2741
|
);
|
|
2742
2742
|
} else {
|
|
2743
|
-
//
|
|
2744
|
-
//
|
|
2745
|
-
//
|
|
2746
|
-
//
|
|
2747
|
-
//
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
const recorded = getPersistedHomeState();
|
|
2751
|
-
let persistedVersion = recorded?.version || "";
|
|
2752
|
-
if (!persistedVersion) {
|
|
2753
|
-
try {
|
|
2754
|
-
persistedVersion = readFileSync(versionPath, "utf-8").trim();
|
|
2755
|
-
} catch {
|
|
2756
|
-
// version file missing/unreadable — reported below as "unknown"
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
const sourceVersion = getVersion();
|
|
2760
|
-
if (persistedVersion && sourceVersion && compareVersions(persistedVersion, sourceVersion) < 0) {
|
|
2743
|
+
// Package-vs-installed-commands drift comparison is factored into
|
|
2744
|
+
// a single shared helper (bin/registry.mjs's
|
|
2745
|
+
// getInstalledCommandsDriftStatus, forge#2719) so doctor, the
|
|
2746
|
+
// `version` command, and the SessionStart hook all agree on one
|
|
2747
|
+
// source of truth instead of re-deriving this logic independently.
|
|
2748
|
+
const { persistedVersion, isStale, unknown } = getInstalledCommandsDriftStatus(FORGE_HOME);
|
|
2749
|
+
if (!unknown && isStale) {
|
|
2761
2750
|
warn(
|
|
2762
2751
|
"Persisted toolset home (~/.forge)",
|
|
2763
|
-
`v${persistedVersion} — source is v${
|
|
2752
|
+
`v${persistedVersion} — source is v${getVersion()}. Run: npx forgedock update`,
|
|
2764
2753
|
);
|
|
2765
2754
|
} else {
|
|
2766
2755
|
pass("Persisted toolset home (~/.forge)", `v${persistedVersion || "unknown"} at ${persistedHome}`);
|
|
@@ -3161,6 +3150,9 @@ function help() {
|
|
|
3161
3150
|
["npx forgedock doctor --fix", "Auto-fix deterministic issues (symlinks, hook, labels, legacy block)"],
|
|
3162
3151
|
["npx forgedock update", "Pull latest & reinstall"],
|
|
3163
3152
|
["npx forgedock uninstall", "Remove commands"],
|
|
3153
|
+
["npx forgedock opencode install", "Install token-efficient native OpenCode commands"],
|
|
3154
|
+
["npx forgedock opencode status", "Check the OpenCode adapter"],
|
|
3155
|
+
["npx forgedock opencode uninstall", "Remove ForgeDock-owned OpenCode files"],
|
|
3164
3156
|
["npx forgedock version", "Print the installed version and check for updates"],
|
|
3165
3157
|
["npx forgedock help", "Show this help"],
|
|
3166
3158
|
];
|
|
@@ -3722,7 +3714,7 @@ async function watch() {
|
|
|
3722
3714
|
const SPLASH_COMMANDS = new Set(["run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
|
|
3723
3715
|
const KNOWN_COMMANDS = new Set([
|
|
3724
3716
|
"install", "init", "enable", "disable", "status", "uninstall", "update",
|
|
3725
|
-
"run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "labels", "config", "help", "--help", "-h",
|
|
3717
|
+
"run", "backend-check", "run-issue", "resume-stalled", "demo", "doctor", "watch", "labels", "config", "help", "--help", "-h",
|
|
3726
3718
|
"version", "--version", "-v",
|
|
3727
3719
|
]);
|
|
3728
3720
|
if (SPLASH_COMMANDS.has(command) || !KNOWN_COMMANDS.has(command)) splash(command);
|
|
@@ -3812,9 +3804,75 @@ switch (command) {
|
|
|
3812
3804
|
case "update":
|
|
3813
3805
|
await update();
|
|
3814
3806
|
break;
|
|
3807
|
+
case "opencode": {
|
|
3808
|
+
const action = restArgs[0] || "install";
|
|
3809
|
+
const {
|
|
3810
|
+
getOpenCodeAdapterStatus,
|
|
3811
|
+
installOpenCodeAdapter,
|
|
3812
|
+
uninstallOpenCodeAdapter,
|
|
3813
|
+
} = await import("./opencode-adapter.mjs");
|
|
3814
|
+
if (action === "install") {
|
|
3815
|
+
const persist = await persistHome({ forgeHome: FORGE_HOME, home: HOME });
|
|
3816
|
+
if (persist.forgeHome === FORGE_HOME && isEphemeralCachePath(FORGE_HOME)) {
|
|
3817
|
+
throw new Error(
|
|
3818
|
+
`Cannot install OpenCode commands from ephemeral package path ${FORGE_HOME}: ` +
|
|
3819
|
+
`${persist.reason || "failed to persist ForgeDock under ~/.forge"}.`,
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
const result = await installOpenCodeAdapter({
|
|
3823
|
+
forgeHome: persist.forgeHome || FORGE_HOME,
|
|
3824
|
+
home: HOME,
|
|
3825
|
+
includeExtras: restArgs.includes("--extras"),
|
|
3826
|
+
});
|
|
3827
|
+
process.stdout.write(
|
|
3828
|
+
`Installed ${result.commandCount} OpenCode commands under ${result.configDir}.\n` +
|
|
3829
|
+
"Restart OpenCode, then run /forge/work-on <issue>.\n",
|
|
3830
|
+
);
|
|
3831
|
+
if (result.migration.removedInstructionsFile || result.migration.removedConfigEntries > 0) {
|
|
3832
|
+
process.stdout.write(
|
|
3833
|
+
`Removed legacy OpenCode adapter artifacts (${result.migration.removedConfigEntries} config entries).\n`,
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
for (const warning of result.migration.warnings) process.stderr.write(`Warning: ${warning}\n`);
|
|
3837
|
+
} else if (action === "status") {
|
|
3838
|
+
const result = await getOpenCodeAdapterStatus({ home: HOME });
|
|
3839
|
+
if (!result.installed) {
|
|
3840
|
+
process.stdout.write(`OpenCode adapter is not installed under ${result.configDir}.\n`);
|
|
3841
|
+
exitCode = 1;
|
|
3842
|
+
} else if (!result.healthy) {
|
|
3843
|
+
process.stdout.write(
|
|
3844
|
+
`OpenCode adapter is incomplete (${result.missing.length} missing file(s)). ` +
|
|
3845
|
+
"Re-run: npx forgedock opencode install\n",
|
|
3846
|
+
);
|
|
3847
|
+
exitCode = 1;
|
|
3848
|
+
} else {
|
|
3849
|
+
process.stdout.write(
|
|
3850
|
+
`OpenCode adapter is healthy (${result.manifest.commandCount} commands) under ${result.configDir}.\n`,
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
} else if (action === "uninstall") {
|
|
3854
|
+
const result = await uninstallOpenCodeAdapter({ home: HOME });
|
|
3855
|
+
process.stdout.write(`Removed ${result.removed} ForgeDock-owned OpenCode file(s).\n`);
|
|
3856
|
+
} else {
|
|
3857
|
+
process.stderr.write(
|
|
3858
|
+
`Unknown OpenCode action: ${action}. Use install, status, or uninstall.\n`,
|
|
3859
|
+
);
|
|
3860
|
+
exitCode = 1;
|
|
3861
|
+
}
|
|
3862
|
+
break;
|
|
3863
|
+
}
|
|
3815
3864
|
case "run":
|
|
3816
3865
|
await run();
|
|
3817
3866
|
break;
|
|
3867
|
+
case "backend-check": {
|
|
3868
|
+
const { checkExecutionBackend } = await import("./runner.mjs");
|
|
3869
|
+
const result = checkExecutionBackend();
|
|
3870
|
+
if (!restArgs.includes("--quiet")) {
|
|
3871
|
+
process.stdout.write(`${result.ready ? "ready" : "unavailable"}: ${result.backend} (${result.reason})\n`);
|
|
3872
|
+
}
|
|
3873
|
+
if (!result.ready) exitCode = 1;
|
|
3874
|
+
break;
|
|
3875
|
+
}
|
|
3818
3876
|
case "run-issue": {
|
|
3819
3877
|
const { runFromCli } = await import("./engine-cli.mjs");
|
|
3820
3878
|
try {
|
|
@@ -3959,6 +4017,18 @@ switch (command) {
|
|
|
3959
4017
|
`Run ${CYAN}npx forgedock update${RESET} to fetch it.`,
|
|
3960
4018
|
);
|
|
3961
4019
|
}
|
|
4020
|
+
|
|
4021
|
+
// forge#2719 — surface package-vs-installed-commands drift (previously
|
|
4022
|
+
// only visible via `doctor` Check 5d). Uses the same shared helper doctor
|
|
4023
|
+
// calls, so both surfaces agree. Silent when versions match or when
|
|
4024
|
+
// either version is unknown (e.g. a git-clone dev install, which never
|
|
4025
|
+
// persists to ~/.forge — see registry.mjs's getInstalledCommandsDriftStatus).
|
|
4026
|
+
const driftStatus = getInstalledCommandsDriftStatus(FORGE_HOME);
|
|
4027
|
+
if (!driftStatus.unknown && driftStatus.isStale) {
|
|
4028
|
+
console.log(
|
|
4029
|
+
` ${YELLOW}Installed commands are stale (v${driftStatus.persistedVersion}) vs package v${driftStatus.sourceVersion}${RESET} — run: ${CYAN}npx forgedock update${RESET}`,
|
|
4030
|
+
);
|
|
4031
|
+
}
|
|
3962
4032
|
break;
|
|
3963
4033
|
}
|
|
3964
4034
|
case "help":
|
|
@@ -51,6 +51,8 @@ const FORGE_HOME = resolve(__dirname, "..", "..");
|
|
|
51
51
|
let resolveState;
|
|
52
52
|
let nudgeSeen;
|
|
53
53
|
let markNudgeSeen;
|
|
54
|
+
// Assigned alongside the other registry.mjs helpers below — see forge#2719.
|
|
55
|
+
let getInstalledCommandsDriftStatus;
|
|
54
56
|
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
56
58
|
// forge-utils helpers — populated inside the try block to honour fail-open
|
|
@@ -88,7 +90,7 @@ try {
|
|
|
88
90
|
// that import() rejects with ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
89
91
|
/** @type {import('../registry.mjs')} */
|
|
90
92
|
(
|
|
91
|
-
{ resolveState, nudgeSeen, markNudgeSeen } = await import(
|
|
93
|
+
{ resolveState, nudgeSeen, markNudgeSeen, getInstalledCommandsDriftStatus } = await import(
|
|
92
94
|
pathToFileURL(join(FORGE_HOME, "bin", "registry.mjs")).href
|
|
93
95
|
)
|
|
94
96
|
);
|
|
@@ -126,6 +128,25 @@ try {
|
|
|
126
128
|
const cwd = process.cwd();
|
|
127
129
|
const state = resolveState(cwd);
|
|
128
130
|
|
|
131
|
+
// Surface package-vs-installed-commands drift as a single non-fatal
|
|
132
|
+
// advisory line (forge#2719). Uses the same shared helper doctor's
|
|
133
|
+
// Check 5d and the `version` command call, so all three surfaces agree.
|
|
134
|
+
// Best-effort: any failure here must never block the session, and
|
|
135
|
+
// managed-optedout directories stay completely silent per this hook's
|
|
136
|
+
// existing contract for that state (see the switch below).
|
|
137
|
+
if (state !== "managed-optedout") {
|
|
138
|
+
try {
|
|
139
|
+
const driftStatus = getInstalledCommandsDriftStatus(FORGE_HOME);
|
|
140
|
+
if (!driftStatus.unknown && driftStatus.isStale) {
|
|
141
|
+
console.log(
|
|
142
|
+
`ForgeDock: installed commands are stale (v${driftStatus.persistedVersion}) vs package v${driftStatus.sourceVersion} — run: npx forgedock update`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Fail open — never let a drift-check error affect session start.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
switch (state) {
|
|
130
151
|
case "managed-active":
|
|
131
152
|
await handleManagedActive(cwd);
|