@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 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
@@ -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
- let exitCode;
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
- return 1;
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
- process.stderr.write(redact(`code-forge: verb "${verb}" failed: ${err?.stack ?? String(err)}\n`));
111
- return 1;
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
- if (exitCode === undefined || exitCode === null) {
115
- return 0;
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 isValidExitCode(exitCode) ? exitCode : 1;
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": 28,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codedology/code-forge",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Plan/harden/code/review orchestration CLI and Agent Skill for cross-model, parallel, evidence-gated feature delivery.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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.2.2"
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>>} */