@codedology/code-forge 0.1.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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/code-forge.mjs +145 -0
- package/docs/README.md +37 -0
- package/docs/reference/blocks.json +288 -0
- package/docs/reference/config.md +162 -0
- package/examples/first-use/README.md +11 -0
- package/examples/first-use/clamp.acceptance.yml +8 -0
- package/examples/first-use/clamp.brief.md +16 -0
- package/examples/first-use/coder/src/math.mjs +31 -0
- package/examples/first-use/coder/test/clamp.test.mjs +26 -0
- package/examples/node-lib/.env.example +3 -0
- package/examples/node-lib/.gitattributes +3 -0
- package/examples/node-lib/.github/workflows/test.yml +11 -0
- package/examples/node-lib/CHANGELOG.md +13 -0
- package/examples/node-lib/README.md +17 -0
- package/examples/node-lib/gitignore +3 -0
- package/examples/node-lib/package.json +19 -0
- package/examples/node-lib/src/list.mjs +23 -0
- package/examples/node-lib/src/math.mjs +21 -0
- package/examples/node-lib/src/strings.mjs +34 -0
- package/examples/node-lib/test/list.test.mjs +12 -0
- package/examples/node-lib/test/math.test.mjs +13 -0
- package/examples/node-lib/test/package.test.mjs +18 -0
- package/examples/node-lib/test/strings.test.mjs +28 -0
- package/package.json +52 -0
- package/schema/code-forge.schema.json +331 -0
- package/schema/facts.schema.json +28 -0
- package/scripts/first-use.mjs +517 -0
- package/scripts/gen-config-doc.mjs +241 -0
- package/skill/SKILL.md +79 -0
- package/skill/references/adapters/claude-code.md +19 -0
- package/skill/references/adapters/solo.md +34 -0
- package/skill/references/adapters/subprocess.md +15 -0
- package/skill/references/code.md +64 -0
- package/skill/references/continuity.md +31 -0
- package/skill/references/decisions.md +50 -0
- package/skill/references/degraded.md +40 -0
- package/skill/references/harden.md +26 -0
- package/skill/references/ledger.md +23 -0
- package/skill/references/plan.md +43 -0
- package/skill/references/proof.md +34 -0
- package/skill/references/review.md +42 -0
- package/skill/references/rules-core.md +72 -0
- package/skill/references/security.md +29 -0
- package/skill/scripts/forge +40 -0
- package/skill/templates/block-record.md +19 -0
- package/skill/templates/facts-sheet.md +22 -0
- package/skill/templates/plan.md +53 -0
- package/src/cli/author.mjs +87 -0
- package/src/cli/block.mjs +262 -0
- package/src/cli/doctor.mjs +74 -0
- package/src/cli/facts.mjs +124 -0
- package/src/cli/gates.mjs +280 -0
- package/src/cli/help.mjs +48 -0
- package/src/cli/init.mjs +14 -0
- package/src/cli/jev.mjs +175 -0
- package/src/cli/keys.mjs +170 -0
- package/src/cli/ledger.mjs +160 -0
- package/src/cli/list.mjs +68 -0
- package/src/cli/models.mjs +131 -0
- package/src/cli/plan.mjs +70 -0
- package/src/cli/proof.mjs +316 -0
- package/src/cli/remove.mjs +76 -0
- package/src/cli/report.mjs +73 -0
- package/src/cli/resolve.mjs +79 -0
- package/src/cli/review-file.mjs +152 -0
- package/src/cli/run.mjs +159 -0
- package/src/cli/s2.mjs +93 -0
- package/src/cli/spawn.mjs +104 -0
- package/src/cli/upgrade.mjs +103 -0
- package/src/cli/validate.mjs +108 -0
- package/src/cli/version.mjs +18 -0
- package/src/cli/worker.mjs +76 -0
- package/src/config/defaults/anthropic.mjs +18 -0
- package/src/config/defaults/index.mjs +24 -0
- package/src/config/defaults/openai.mjs +16 -0
- package/src/config/defaults/xai.mjs +18 -0
- package/src/config/known-ids.mjs +171 -0
- package/src/config/load.mjs +135 -0
- package/src/config/migrate.mjs +46 -0
- package/src/config/refresh.mjs +276 -0
- package/src/config/schema-compile.mjs +211 -0
- package/src/config/schema-paths.mjs +84 -0
- package/src/config/secret-patterns.mjs +85 -0
- package/src/config/validate.mjs +694 -0
- package/src/decide/author-loop.mjs +90 -0
- package/src/decide/escalation.mjs +262 -0
- package/src/decide/fallback-rules.mjs +172 -0
- package/src/decide/jev-client.mjs +216 -0
- package/src/decide/questions.mjs +161 -0
- package/src/decide/thresholds.mjs +199 -0
- package/src/doctor/index.mjs +105 -0
- package/src/doctor/local.mjs +162 -0
- package/src/doctor/probes.mjs +358 -0
- package/src/doctor/rows.mjs +51 -0
- package/src/doctor/worker-probe.mjs +125 -0
- package/src/engines/alias-map.mjs +78 -0
- package/src/engines/builders/claude.mjs +224 -0
- package/src/engines/builders/codex.mjs +242 -0
- package/src/engines/builders/grok.mjs +132 -0
- package/src/engines/builders/index.mjs +35 -0
- package/src/engines/builders/validate-params.mjs +34 -0
- package/src/engines/codex-home.mjs +230 -0
- package/src/engines/isolation.mjs +84 -0
- package/src/engines/probe.mjs +116 -0
- package/src/engines/provider-cli.mjs +24 -0
- package/src/engines/render-rules.mjs +41 -0
- package/src/engines/sentinel.mjs +133 -0
- package/src/engines/usage-parse.mjs +149 -0
- package/src/gates/acceptance.mjs +95 -0
- package/src/gates/detect.mjs +187 -0
- package/src/gates/invariants.mjs +43 -0
- package/src/gates/run.mjs +72 -0
- package/src/gates/safe-edit.mjs +266 -0
- package/src/gates/scope.mjs +154 -0
- package/src/gates/secret-scan.mjs +79 -0
- package/src/gates/transcript-grep.mjs +27 -0
- package/src/install/agent-env.mjs +76 -0
- package/src/install/detect.mjs +121 -0
- package/src/install/harnesses.mjs +131 -0
- package/src/install/link.mjs +463 -0
- package/src/install/wizard/answers.mjs +389 -0
- package/src/install/wizard/flags.mjs +248 -0
- package/src/install/wizard/project-file.mjs +128 -0
- package/src/install/wizard/run.mjs +366 -0
- package/src/install/wizard/steps.mjs +201 -0
- package/src/keys/backends/constants.mjs +7 -0
- package/src/keys/backends/file.mjs +99 -0
- package/src/keys/backends/keychain.mjs +81 -0
- package/src/keys/backends/security-cli.mjs +105 -0
- package/src/keys/onepassword.mjs +46 -0
- package/src/keys/store.mjs +347 -0
- package/src/ledger/calibration.mjs +109 -0
- package/src/ledger/export.mjs +52 -0
- package/src/ledger/outcome.mjs +397 -0
- package/src/ledger/paths.mjs +30 -0
- package/src/ledger/prices.mjs +49 -0
- package/src/ledger/report.mjs +250 -0
- package/src/ledger/write.mjs +279 -0
- package/src/proof/export.mjs +358 -0
- package/src/proof/lock.mjs +138 -0
- package/src/proof/red-green.mjs +519 -0
- package/src/proof/red-parse.mjs +103 -0
- package/src/proof/tiers.mjs +62 -0
- package/src/review/budget.mjs +241 -0
- package/src/review/context.mjs +156 -0
- package/src/review/engine.mjs +264 -0
- package/src/review/finding.schema.json +47 -0
- package/src/review/fixloop.mjs +467 -0
- package/src/review/gate-check.mjs +249 -0
- package/src/review/lenses/A.md +22 -0
- package/src/review/lenses/B.md +23 -0
- package/src/review/lenses/full.md +23 -0
- package/src/review/lenses/judge.md +21 -0
- package/src/review/lenses/quick.md +21 -0
- package/src/review/lenses/recheck.md +23 -0
- package/src/review/packet.mjs +355 -0
- package/src/review/triage.mjs +149 -0
- package/src/review/validate-review.mjs +70 -0
- package/src/session/author.mjs +228 -0
- package/src/session/facts.mjs +855 -0
- package/src/session/plan-check.mjs +294 -0
- package/src/session/s2.mjs +164 -0
- package/src/session/spawn.mjs +610 -0
- package/src/state/block.mjs +289 -0
- package/src/state/cli-args.mjs +67 -0
- package/src/state/paths.mjs +92 -0
- package/src/state/registry.mjs +189 -0
- package/src/state/run.mjs +266 -0
- package/src/state/signer.mjs +132 -0
- package/src/util/exec.mjs +315 -0
- package/src/util/forbidden.mjs +787 -0
- package/src/util/git.mjs +381 -0
- package/src/util/log.mjs +100 -0
- package/src/util/reaper.mjs +300 -0
- package/src/util/redact.mjs +212 -0
- package/src/util/tmp.mjs +409 -0
- package/src/worker/engine.mjs +338 -0
- package/src/worker/loop.mjs +392 -0
- package/src/worker/queue.mjs +186 -0
- package/src/worker/review-state.mjs +145 -0
- package/src/worker/ticket.mjs +195 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness detection (plan §2.1 "Detect" column): `<command>` on PATH OR `~/<homeMarker>/`
|
|
3
|
+
* exists. Deliberately never shells out (`which`/`where`) — this module scans `PATH` directory
|
|
4
|
+
* entries itself, which is both faster and, critically, testable under a FAKE `PATH`/`home`
|
|
5
|
+
* without ever touching the real `$PATH` or the real `~/.claude`, `~/.codex`, `~/.grok`,
|
|
6
|
+
* `~/.gemini`, `~/.agents` (coder-rules.md rule 10 — never inspect the real harness dirs in tests
|
|
7
|
+
* or manual runs).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { access, constants, stat } from 'node:fs/promises';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { HARNESSES } from './harnesses.mjs';
|
|
14
|
+
|
|
15
|
+
/** Windows executable suffixes probed when no extension is already present (PATHEXT convention). */
|
|
16
|
+
const DEFAULT_WIN32_EXTS = ['.EXE', '.CMD', '.BAT', '.COM'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} pathEnv - a `PATH`-shaped string (colon- or semicolon-separated).
|
|
20
|
+
* @param {NodeJS.Platform} platform
|
|
21
|
+
* @returns {string[]} non-empty directory entries, in order, duplicates kept (matches shell PATH
|
|
22
|
+
* resolution: the first match wins, so order must survive).
|
|
23
|
+
*/
|
|
24
|
+
function splitPathEnv(pathEnv, platform) {
|
|
25
|
+
const sep = platform === 'win32' ? ';' : ':';
|
|
26
|
+
return pathEnv.split(sep).filter((entry) => entry.length > 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether `command` resolves to an executable file in `opts.pathEnv`. Never spawns a process.
|
|
31
|
+
* @param {string} command
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {string} [opts.pathEnv] - defaults to `process.env.PATH`; tests pass a fake one.
|
|
34
|
+
* @param {NodeJS.Platform} [opts.platform] - defaults to `process.platform`.
|
|
35
|
+
* @param {string} [opts.pathExt] - Windows only; defaults to `process.env.PATHEXT`.
|
|
36
|
+
* @returns {Promise<boolean>}
|
|
37
|
+
*/
|
|
38
|
+
export async function commandOnPath(command, opts = {}) {
|
|
39
|
+
if (typeof command !== 'string' || command.length === 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const platform = opts.platform ?? process.platform;
|
|
43
|
+
const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
|
|
44
|
+
const dirs = splitPathEnv(pathEnv, platform);
|
|
45
|
+
const exts =
|
|
46
|
+
platform === 'win32'
|
|
47
|
+
? (opts.pathExt ?? process.env.PATHEXT ?? DEFAULT_WIN32_EXTS.join(';')).split(';').filter((e) => e.length > 0)
|
|
48
|
+
: [''];
|
|
49
|
+
|
|
50
|
+
for (const dir of dirs) {
|
|
51
|
+
for (const ext of exts) {
|
|
52
|
+
const candidate = path.join(dir, `${command}${ext}`);
|
|
53
|
+
try {
|
|
54
|
+
// Sequential, not Promise.all: PATH entries are searched in order and the first match
|
|
55
|
+
// must win, matching real shell PATH resolution.
|
|
56
|
+
await access(candidate, constants.X_OK);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
// not this directory — keep looking.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether `<home>/<harness.homeMarker>` exists and is a directory.
|
|
68
|
+
* @param {import('./harnesses.mjs').HarnessRow} harness
|
|
69
|
+
* @param {object} [opts]
|
|
70
|
+
* @param {string} [opts.home] - defaults to `os.homedir()`.
|
|
71
|
+
* @returns {Promise<boolean>}
|
|
72
|
+
*/
|
|
73
|
+
export async function homeMarkerPresent(harness, opts = {}) {
|
|
74
|
+
const home = opts.home ?? os.homedir();
|
|
75
|
+
try {
|
|
76
|
+
const info = await stat(path.join(home, harness.homeMarker));
|
|
77
|
+
return info.isDirectory();
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} DetectionResult
|
|
85
|
+
* @property {string} id
|
|
86
|
+
* @property {boolean} detected - `onPath || homeDirPresent`.
|
|
87
|
+
* @property {boolean} onPath
|
|
88
|
+
* @property {boolean} homeDirPresent
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {import('./harnesses.mjs').HarnessRow} harness
|
|
93
|
+
* @param {object} [opts]
|
|
94
|
+
* @param {string} [opts.home]
|
|
95
|
+
* @param {string} [opts.pathEnv]
|
|
96
|
+
* @param {NodeJS.Platform} [opts.platform]
|
|
97
|
+
* @returns {Promise<DetectionResult>}
|
|
98
|
+
*/
|
|
99
|
+
export async function detectHarness(harness, opts = {}) {
|
|
100
|
+
const [onPath, homeDirPresent] = await Promise.all([
|
|
101
|
+
harness.command ? commandOnPath(harness.command, opts) : Promise.resolve(false),
|
|
102
|
+
homeMarkerPresent(harness, opts),
|
|
103
|
+
]);
|
|
104
|
+
return { id: harness.id, detected: onPath || homeDirPresent, onPath, homeDirPresent };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Detects every row of {@link HARNESSES}, in table order.
|
|
109
|
+
* @param {object} [opts]
|
|
110
|
+
* @param {string} [opts.home]
|
|
111
|
+
* @param {string} [opts.pathEnv]
|
|
112
|
+
* @param {NodeJS.Platform} [opts.platform]
|
|
113
|
+
* @returns {Promise<DetectionResult[]>}
|
|
114
|
+
*/
|
|
115
|
+
export async function detectAll(opts = {}) {
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const harness of HARNESSES) {
|
|
118
|
+
results.push(await detectHarness(harness, opts));
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-harness install path table (plan §2.1, unchanged from v1; research/installer-patterns.md
|
|
3
|
+
* §"Agent Skills standard + `npx skills`"). Pure data — no filesystem access here (that is
|
|
4
|
+
* `./detect.mjs`) and no writes (that is `./link.mjs`).
|
|
5
|
+
*
|
|
6
|
+
* Claude Code and Codex CLI's paths are backed by assumption `[A7]` (both skill directories were
|
|
7
|
+
* observed on this machine, 2026-09-24: `~/.claude/skills` and `~/.codex/skills`, the latter
|
|
8
|
+
* empty). Grok's GLOBAL path is also backed by `[A7]` (`~/.grok/skills` observed with 5 entries,
|
|
9
|
+
* so Grok reads that directory) but its PROJECT path is not — a harness CLI exists to probe it
|
|
10
|
+
* (`doctor`, a later block, asks Grok to list its skills). Gemini is not installed on this machine
|
|
11
|
+
* at all: `[A6]`, its whole row is `verified: false`, matching the plan's "Gemini row UNVERIFIED"
|
|
12
|
+
* (§10.3 acceptance) — the only row that flag applies to. Cursor and Copilot have no CLI and no
|
|
13
|
+
* adapter; detection is directory-presence only, and their paths (shared with Codex/Gemini under
|
|
14
|
+
* `.agents/skills/`) come from research/installer-patterns.md, not from an observation on this
|
|
15
|
+
* machine, but the plan does not ask `doctor` to mark them unverified (there is no CLI to probe).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} HarnessRow
|
|
22
|
+
* @property {string} id - stable identifier, also the config `harnesses[]` entry value.
|
|
23
|
+
* @property {string} label - human-readable name.
|
|
24
|
+
* @property {string|null} command - executable name to look for on PATH, or `null` when the
|
|
25
|
+
* harness has no CLI (Cursor, Copilot) — detection then relies on `homeMarker` alone.
|
|
26
|
+
* @property {string} homeMarker - directory name directly under `$HOME` (e.g. `.claude`) used
|
|
27
|
+
* both as a detection fallback and as the base of the global install path.
|
|
28
|
+
* @property {string} projectSkillsDir - project-relative directory that holds installed skills.
|
|
29
|
+
* @property {boolean} verified - whether this row's paths are confirmed on a real machine (§10.3:
|
|
30
|
+
* exactly the Gemini row is `false`).
|
|
31
|
+
* @property {string} notes - the plan's free-text note for this row, unchanged wording.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Six rows, Claude Code first (plan §2.1 table order). Frozen so a caller cannot mutate the
|
|
36
|
+
* shared table in place.
|
|
37
|
+
* @type {ReadonlyArray<HarnessRow>}
|
|
38
|
+
*/
|
|
39
|
+
export const HARNESSES = Object.freeze(
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
id: 'claude',
|
|
43
|
+
label: 'Claude Code',
|
|
44
|
+
command: 'claude',
|
|
45
|
+
homeMarker: '.claude',
|
|
46
|
+
projectSkillsDir: '.claude/skills',
|
|
47
|
+
verified: true,
|
|
48
|
+
notes: '[A7] global skills dir exists here.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'codex',
|
|
52
|
+
label: 'Codex CLI',
|
|
53
|
+
command: 'codex',
|
|
54
|
+
homeMarker: '.codex',
|
|
55
|
+
projectSkillsDir: '.agents/skills',
|
|
56
|
+
verified: true,
|
|
57
|
+
notes: '[A7] ~/.codex/skills exists (empty). Paths from research/installer-patterns.md.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'grok',
|
|
61
|
+
label: 'Grok CLI',
|
|
62
|
+
command: 'grok',
|
|
63
|
+
homeMarker: '.grok',
|
|
64
|
+
projectSkillsDir: '.agents/skills',
|
|
65
|
+
verified: true,
|
|
66
|
+
notes:
|
|
67
|
+
'[A7] ~/.grok/skills exists with 5 entries, so Grok reads it; the PROJECT path is unverified — doctor asks Grok to list its skills.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'gemini',
|
|
71
|
+
label: 'Gemini CLI',
|
|
72
|
+
command: 'gemini',
|
|
73
|
+
homeMarker: '.gemini',
|
|
74
|
+
projectSkillsDir: '.agents/skills',
|
|
75
|
+
verified: false,
|
|
76
|
+
notes: '[A6] not installed on this machine; path from research, UNVERIFIED.',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'cursor',
|
|
80
|
+
label: 'Cursor',
|
|
81
|
+
command: null,
|
|
82
|
+
homeMarker: '.cursor',
|
|
83
|
+
projectSkillsDir: '.agents/skills',
|
|
84
|
+
verified: true,
|
|
85
|
+
notes: 'detection only; no adapter (no CLI).',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'copilot',
|
|
89
|
+
label: 'Copilot',
|
|
90
|
+
command: null,
|
|
91
|
+
homeMarker: '.copilot',
|
|
92
|
+
projectSkillsDir: '.agents/skills',
|
|
93
|
+
verified: true,
|
|
94
|
+
notes: 'detection only; no adapter (no CLI).',
|
|
95
|
+
},
|
|
96
|
+
].map((row) => Object.freeze(row)),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} id
|
|
101
|
+
* @returns {HarnessRow}
|
|
102
|
+
* @throws {RangeError} when `id` names no row in {@link HARNESSES}.
|
|
103
|
+
*/
|
|
104
|
+
export function getHarness(id) {
|
|
105
|
+
const row = HARNESSES.find((h) => h.id === id);
|
|
106
|
+
if (!row) {
|
|
107
|
+
throw new RangeError(`install: unknown harness "${id}"`);
|
|
108
|
+
}
|
|
109
|
+
return row;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {HarnessRow} harness
|
|
114
|
+
* @param {string} projectRoot - absolute path to the project.
|
|
115
|
+
* @param {string} [skillName]
|
|
116
|
+
* @returns {string} absolute path where the skill would be installed inside the project.
|
|
117
|
+
*/
|
|
118
|
+
export function projectSkillPath(harness, projectRoot, skillName = 'code-forge') {
|
|
119
|
+
return path.join(projectRoot, harness.projectSkillsDir, skillName);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {HarnessRow} harness
|
|
124
|
+
* @param {string} home - absolute path to `$HOME`.
|
|
125
|
+
* @param {string} [skillName]
|
|
126
|
+
* @returns {string} absolute path where the skill would be installed globally, under
|
|
127
|
+
* `<home>/<homeMarker>/skills/<skillName>`.
|
|
128
|
+
*/
|
|
129
|
+
export function globalSkillPath(harness, home, skillName = 'code-forge') {
|
|
130
|
+
return path.join(home, harness.homeMarker, 'skills', skillName);
|
|
131
|
+
}
|