@codedology/code-forge 0.2.2 → 0.3.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 +22 -0
- package/README.md +2 -0
- package/bin/code-forge.mjs +57 -9
- package/docs/README.md +1 -0
- package/docs/reference/blocks.json +26 -1
- package/package.json +1 -1
- package/skill/scripts/forge +1 -1
- package/src/cli/keys.mjs +2 -1
- package/src/cli/logs.mjs +899 -0
- package/src/install/wizard/run.mjs +7 -2
- package/src/keys/onepassword.mjs +14 -4
- package/src/session/scrub.mjs +266 -0
- package/src/util/error-hint.mjs +92 -0
- package/src/util/error-kind.mjs +32 -0
- package/src/util/error-log.mjs +525 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ waits on a second real consumer (plan §10.4, Q17).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] — 2026-10-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `code-forge logs`: every failed verb is logged (scrubbed, flag names only) to `~/.code-forge/logs/errors.jsonl`;
|
|
14
|
+
`logs`, `logs summary`, `logs clear`, `logs path`, and `logs report` to share selected errors as a public GitHub issue
|
|
15
|
+
after you see the full text and say yes (`gh` or a prefilled link; a last secret check stops it). Opt out with
|
|
16
|
+
`CODE_FORGE_NO_ERROR_LOG=1`.
|
|
17
|
+
- Better error reports (`code-forge logs report`): a second, AI cleaning pass (a closed-book L1 session that only lists
|
|
18
|
+
names, hosts, URLs, accounts, paths or secrets to hide; code-forge replaces them, and you see counts only),
|
|
19
|
+
fingerprints that group the same error in `logs`/`logs summary` and find an earlier issue (add a "happened again"
|
|
20
|
+
comment instead of a duplicate), a version check before reporting, Send · Edit in my editor · Cancel, a GitHub issue
|
|
21
|
+
form, and a one-line hint after a failure in a terminal; see docs/privacy.md. `--no-ai` skips the AI pass with an
|
|
22
|
+
extra yes.
|
|
23
|
+
|
|
24
|
+
## [0.2.3] — 2026-10-01
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- 1Password: code-forge prints a line before each call that may make 1Password ask for approval, and waits 60 s instead
|
|
29
|
+
of 20 s, so a Touch ID or password prompt no longer times out while you look for it.
|
|
30
|
+
|
|
9
31
|
## [0.2.2] — 2026-10-01
|
|
10
32
|
|
|
11
33
|
### Added
|
package/README.md
CHANGED
|
@@ -108,6 +108,8 @@ usage line.
|
|
|
108
108
|
| `list` | `code-forge list` |
|
|
109
109
|
| `remove` | `code-forge remove [<harness>] [--scope project\|global]` |
|
|
110
110
|
| `upgrade` | `code-forge upgrade [--source <path>]` |
|
|
111
|
+
| `logs` | `code-forge logs [--last N] [--json]` · `logs summary [--days N] [--json]` · `logs clear [--yes]` · `logs path` — the local error log (`~/.code-forge/logs/errors.jsonl`) |
|
|
112
|
+
| `logs report` | `code-forge logs report [--last N] [--kind K] [--verb V] [--note "text"] [--dry-run] [--yes]` — share selected errors as a public GitHub issue; you see the full text first and say yes once |
|
|
111
113
|
| `help` / `version` | `code-forge --help` (or no verb) · `code-forge --version` |
|
|
112
114
|
|
|
113
115
|
`run`, `block`, `worker`, `spawn`, `s2`, `author`, `facts` and `review-file` are what the Agent
|
package/bin/code-forge.mjs
CHANGED
|
@@ -12,6 +12,9 @@ import { realpathSync } from 'node:fs';
|
|
|
12
12
|
import { readdir } from 'node:fs/promises';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
+
import { clearErrorKind, reportErrorKind, takeErrorKind } from '../src/util/error-kind.mjs';
|
|
16
|
+
import { maybeHint } from '../src/util/error-hint.mjs';
|
|
17
|
+
import { captureStderr, writeVerbFailure } from '../src/util/error-log.mjs';
|
|
15
18
|
import { redact } from '../src/util/redact.mjs';
|
|
16
19
|
|
|
17
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -95,26 +98,71 @@ export async function run(argv) {
|
|
|
95
98
|
|
|
96
99
|
const modulePath = path.join(CLI_DIR, `${verb}.mjs`);
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
// B27: a verb that exits non-zero or throws is logged to `~/.code-forge/logs/errors.jsonl`
|
|
102
|
+
// (scrubbed; never throws; never changes the exit code). The kind a verb reports is cleared
|
|
103
|
+
// first, and stderr is recorded only while the verb runs (the message is its last text).
|
|
104
|
+
// Every logging step is guarded on its own: none of them can change the verb's outcome.
|
|
105
|
+
/** @type {{stop: () => string|null} | null} */
|
|
106
|
+
let capture = null;
|
|
107
|
+
try {
|
|
108
|
+
clearErrorKind();
|
|
109
|
+
capture = captureStderr(process.stderr);
|
|
110
|
+
} catch {
|
|
111
|
+
// no recording: the entry has no message
|
|
112
|
+
}
|
|
113
|
+
/** @type {number} */
|
|
114
|
+
let code;
|
|
115
|
+
/** @type {unknown} */
|
|
116
|
+
let thrown;
|
|
117
|
+
let threw = false;
|
|
118
|
+
/** @type {string|null} */
|
|
119
|
+
let stderrText = null;
|
|
99
120
|
try {
|
|
100
121
|
const mod = await import(pathToFileURL(modulePath).href);
|
|
101
122
|
const handler = mod.default;
|
|
102
123
|
if (typeof handler !== 'function') {
|
|
103
124
|
process.stderr.write(redact(`code-forge: verb "${verb}" has no default export function\n`));
|
|
104
|
-
|
|
125
|
+
code = 1;
|
|
126
|
+
} else {
|
|
127
|
+
const exitCode = await handler(rest, { verbs, verb, reportErrorKind });
|
|
128
|
+
code = exitCode === undefined || exitCode === null ? 0 : isValidExitCode(exitCode) ? exitCode : 1;
|
|
105
129
|
}
|
|
106
|
-
exitCode = await handler(rest, { verbs, verb });
|
|
107
130
|
} catch (err) {
|
|
131
|
+
thrown = err;
|
|
132
|
+
threw = true;
|
|
133
|
+
code = 1;
|
|
108
134
|
// A verb's error can carry an argv/env token (a fake key in a test, a real one in
|
|
109
135
|
// production); every stderr path here is redacted, this one included.
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
try {
|
|
137
|
+
process.stderr.write(redact(`code-forge: verb "${verb}" failed: ${/** @type {any} */ (err)?.stack ?? String(err)}\n`));
|
|
138
|
+
} catch {
|
|
139
|
+
// a broken stderr never changes the exit code
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
try {
|
|
143
|
+
stderrText = capture ? capture.stop() : null;
|
|
144
|
+
} catch {
|
|
145
|
+
stderrText = null;
|
|
146
|
+
}
|
|
112
147
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
148
|
+
/** @type {string|null} */
|
|
149
|
+
let kind = null;
|
|
150
|
+
try {
|
|
151
|
+
kind = takeErrorKind();
|
|
152
|
+
} catch {
|
|
153
|
+
kind = null;
|
|
154
|
+
}
|
|
155
|
+
if (code !== 0) {
|
|
156
|
+
try {
|
|
157
|
+
const entry = await writeVerbFailure({ verb, args: rest, exit: code, kind, stderrText, threw, thrown });
|
|
158
|
+
// B28: one line on a terminal saying how to send it (not for usage errors, agents, or the
|
|
159
|
+
// same error twice within 24 h); maybeHint never throws either
|
|
160
|
+
await maybeHint({ entry, exit: code, isTTY: process.stderr.isTTY === true });
|
|
161
|
+
} catch {
|
|
162
|
+
// logging never changes the exit code (writeVerbFailure does not throw; belt and braces)
|
|
163
|
+
}
|
|
116
164
|
}
|
|
117
|
-
return
|
|
165
|
+
return code;
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
/**
|
package/docs/README.md
CHANGED
|
@@ -27,6 +27,7 @@ then 5a.
|
|
|
27
27
|
| [reference/config.md](reference/config.md) | every `.code-forge.yml` key, its type and default. Generated from `schema/code-forge.schema.json` |
|
|
28
28
|
| [reference/blocks.json](reference/blocks.json) | the build blocks of this package and what each one owns |
|
|
29
29
|
| [marketing/code-forge-story.md](marketing/code-forge-story.md) | the marketing content bank (copy, lessons, what not to claim) |
|
|
30
|
+
| [privacy.md](privacy.md) | what the local error log holds, every cleaning rule, the AI check, and how to turn the log off or delete it |
|
|
30
31
|
| [releasing.md](releasing.md) | for maintainers: record changes (`npm run changelog`), cut a release with one command (`npm run release`), GitHub Releases, what stays manual |
|
|
31
32
|
| [`skill/SKILL.md`](../skill/SKILL.md) and [`skill/references/`](../skill/references/) | the Agent Skill the orchestrator follows. The deepest and most exact description of the rules |
|
|
32
33
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"plans/code-forge-plan-v1.3.md §10.4",
|
|
5
5
|
"plans/NIGHT-LOG.md"
|
|
6
6
|
],
|
|
7
|
-
"block_count":
|
|
7
|
+
"block_count": 30,
|
|
8
8
|
"baseline_ids": ["B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"],
|
|
9
9
|
"baseline_note": "Wave 0-1 blocks, landed before v1.3; depends_on may name them, but they have no entry under blocks.",
|
|
10
10
|
"blocks": {
|
|
@@ -346,6 +346,31 @@
|
|
|
346
346
|
],
|
|
347
347
|
"depends_on": ["B13a"],
|
|
348
348
|
"note": "root-added 2026-10-01; not in the v1.3 §10.4 table"
|
|
349
|
+
},
|
|
350
|
+
"B27": {
|
|
351
|
+
"title": "local error log for every verb (~/.code-forge/logs/errors.jsonl, scrubbed), code-forge logs (list, summary, clear, path) and logs report: share selected errors as a GitHub issue after one yes",
|
|
352
|
+
"wave": 6,
|
|
353
|
+
"level": "L2",
|
|
354
|
+
"status": "landed",
|
|
355
|
+
"owned": [
|
|
356
|
+
"bin/code-forge.mjs", "src/util/error-log.mjs", "src/util/error-kind.mjs", "src/cli/logs.mjs", "src/keys/onepassword.mjs",
|
|
357
|
+
"test/cli/logs.test.mjs", "test/util/error-log.test.mjs"
|
|
358
|
+
],
|
|
359
|
+
"depends_on": ["B0.1"],
|
|
360
|
+
"note": "root-added 2026-10-01; not in the v1.3 §10.4 table"
|
|
361
|
+
},
|
|
362
|
+
"B28": {
|
|
363
|
+
"title": "better error reports: an AI cleaning pass (closed-book L1 session that only lists what to hide), fingerprints and no duplicate issues, a hint after a failure, a version check, edit before sending, and a GitHub issue form",
|
|
364
|
+
"wave": 6,
|
|
365
|
+
"level": "L2",
|
|
366
|
+
"status": "landed",
|
|
367
|
+
"owned": [
|
|
368
|
+
"bin/code-forge.mjs", "src/util/error-log.mjs", "src/util/error-hint.mjs", "src/cli/logs.mjs", "src/session/scrub.mjs",
|
|
369
|
+
".github/ISSUE_TEMPLATE/error-report.yml", ".github/ISSUE_TEMPLATE/config.yml", "docs/privacy.md",
|
|
370
|
+
"test/cli/logs.test.mjs", "test/util/error-log.test.mjs"
|
|
371
|
+
],
|
|
372
|
+
"depends_on": ["B27"],
|
|
373
|
+
"note": "root-added 2026-10-01; not in the v1.3 §10.4 table"
|
|
349
374
|
}
|
|
350
375
|
}
|
|
351
376
|
}
|
package/package.json
CHANGED
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.
|
|
13
|
+
PINNED_VERSION="0.3.0"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|
package/src/cli/keys.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { writeSafe } from '../util/redact.mjs';
|
|
19
|
-
import { describeOpItem, isOpRef, OP_MESSAGES, opItemIdFromInput, toOpRef } from '../keys/onepassword.mjs';
|
|
19
|
+
import { describeOpItem, isOpRef, OP_APPROVE_NOTE, OP_MESSAGES, opItemIdFromInput, toOpRef } from '../keys/onepassword.mjs';
|
|
20
20
|
import { assertKeyName, createDefaultKeyStore, parseRef, readOpAndCache, resolveKey } from '../keys/store.mjs';
|
|
21
21
|
|
|
22
22
|
const USAGE = 'usage: code-forge keys list | set <name> [--op <item-id|link|op://ref>] | test <name> [--ref <ref>] | remove <name>\n';
|
|
@@ -109,6 +109,7 @@ export async function runKeys(args, { store, stdout = process.stdout, stderr = p
|
|
|
109
109
|
err(`${name}: --op must be a 1Password item ID, item link or op://vault/item/field reference\n`);
|
|
110
110
|
return 2;
|
|
111
111
|
}
|
|
112
|
+
if (flagArg !== undefined && (itemId !== null || fullRef)) err(`${OP_APPROVE_NOTE}\n`);
|
|
112
113
|
// A 1Password item ID or link becomes a full op:// reference before any store call (B25).
|
|
113
114
|
if (flagArg !== undefined && itemId !== null) {
|
|
114
115
|
/** @type {Awaited<ReturnType<typeof toOpRef>>} */
|