@codedology/code-forge 0.5.1 → 0.6.1
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 +59 -0
- package/README.md +39 -2
- package/docs/README.md +5 -4
- package/docs/reference/blocks.json +127 -1
- package/docs/reference/config.md +2 -0
- package/package.json +1 -1
- package/schema/code-forge.schema.json +7 -0
- package/skill/SKILL.md +2 -0
- package/skill/references/adapters/claude-code.md +14 -0
- package/skill/references/autopilot.md +48 -0
- package/skill/references/code.md +1 -1
- package/skill/references/decisions.md +3 -1
- package/skill/references/harden.md +1 -1
- package/skill/references/ledger.md +2 -0
- package/skill/references/plan.md +6 -4
- package/skill/references/review.md +2 -2
- package/skill/references/rules-core.md +4 -1
- package/skill/scripts/forge +1 -1
- package/skill/templates/plan.md +4 -3
- package/src/autopilot/actions.mjs +491 -0
- package/src/autopilot/binnacle.mjs +494 -0
- package/src/autopilot/delegate.mjs +422 -0
- package/src/autopilot/grant.mjs +354 -0
- package/src/autopilot/limits.mjs +848 -0
- package/src/autopilot/scopes.mjs +95 -0
- package/src/cli/author.mjs +7 -5
- package/src/cli/autopilot.mjs +500 -0
- package/src/cli/block.mjs +40 -8
- package/src/cli/facts.mjs +7 -6
- package/src/cli/jev.mjs +10 -6
- package/src/cli/logs.mjs +4 -142
- package/src/cli/plan.mjs +18 -7
- package/src/cli/run.mjs +20 -61
- package/src/cli/s2.mjs +5 -4
- package/src/cli/spawn.mjs +5 -4
- package/src/config/closed-book.mjs +20 -3
- package/src/config/load.mjs +113 -1
- package/src/doctor/probes.mjs +34 -11
- package/src/engines/builders/claude.mjs +3 -3
- package/src/engines/builders/codex.mjs +6 -5
- package/src/engines/builders/grok.mjs +3 -3
- package/src/engines/builders/validate-params.mjs +3 -3
- package/src/ledger/report.mjs +2 -0
- package/src/review/fixloop.mjs +69 -5
- package/src/review/gate-check.mjs +156 -6
- package/src/session/facts.mjs +291 -73
- package/src/session/plan-check.mjs +75 -6
- package/src/session/spawn.mjs +68 -8
- package/src/state/run.mjs +8 -1
- package/src/util/breadcrumbs.mjs +1 -0
- package/src/util/forbidden.mjs +10 -0
- package/src/util/gh.mjs +175 -0
- package/src/worker/engine.mjs +8 -4
- package/src/worker/loop.mjs +42 -0
package/src/cli/block.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `code-forge block open|attempt|rebase|claim|close|stop` (plan §4.9). The skill calls
|
|
3
3
|
* `block open` before every spawn in every engine; it prints the brief pointer (≤ 200 bytes).
|
|
4
4
|
*
|
|
5
|
-
* block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file>
|
|
5
|
+
* block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file>
|
|
6
6
|
* [--brief <file>] [--attempt <n>] [--base <sha>] [--lines <forecast>] [--kind code|docs|contract]
|
|
7
7
|
* block attempt <id> --run <r>
|
|
8
8
|
* block rebase <id> --run <r> [--head <ref>]
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
* FAILED: no review-file ticket id for <files>` (skipped under `--no-require-reviews`, the human's
|
|
34
34
|
* way out). It runs after the review rows and reuses their file set (listed once).
|
|
35
35
|
*
|
|
36
|
+
* B47: `block open` without `--level` takes the level `autopilot level` chose for the block (the
|
|
37
|
+
* latest `autopilot.level` row of the run for it whose MAC verifies, `autopilotLevel`); with
|
|
38
|
+
* neither it is a usage error (`no --level given and no autopilot level recorded for <id>`). An
|
|
39
|
+
* explicit `--level` always wins; a repeated or valueless `--level` is a usage error as before.
|
|
40
|
+
*
|
|
36
41
|
* `--acceptance` is a YAML/JSON list of `{clause, tests: [test ids…]}`. `block close` runs B8's
|
|
37
42
|
* rows of the gate (worker pin, MACs, orphans); the gate rows owned by later blocks join it
|
|
38
43
|
* through `closeBlock`'s `extraChecks`.
|
|
@@ -41,13 +46,14 @@
|
|
|
41
46
|
import { readFile, realpath } from 'node:fs/promises';
|
|
42
47
|
import path from 'node:path';
|
|
43
48
|
import { parse as parseYAML } from 'yaml';
|
|
49
|
+
import { approvalCheckText, expiryChecks } from '../autopilot/limits.mjs';
|
|
44
50
|
import { appendRow, readAllRows } from '../ledger/write.mjs';
|
|
45
51
|
import { attemptBlock, claimPath, closeBlock, openBlock, rebaseBlock, stopBlock } from '../state/block.mjs';
|
|
46
52
|
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
47
53
|
import { StateError } from '../state/paths.mjs';
|
|
48
54
|
import { findOverlap } from '../state/registry.mjs';
|
|
49
55
|
import { readRun, writeSigned } from '../state/run.mjs';
|
|
50
|
-
import { loadKey } from '../state/signer.mjs';
|
|
56
|
+
import { loadKey, verifyRow } from '../state/signer.mjs';
|
|
51
57
|
import { blockFileSet, reviewGateCheck, waiveFinding } from '../review/gate-check.mjs';
|
|
52
58
|
import { writeSafe } from '../util/redact.mjs';
|
|
53
59
|
import { DEFAULT_CONFIG_FILENAME, loadProjectConfig } from '../config/load.mjs';
|
|
@@ -58,7 +64,7 @@ import { gitChildEnv } from '../worker/ticket.mjs';
|
|
|
58
64
|
import { missingReviewTickets } from '../engines/sentinel.mjs';
|
|
59
65
|
|
|
60
66
|
const USAGE =
|
|
61
|
-
'usage: code-forge block open <id> --run <r> --level L<n> --owned <paths…> --acceptance <file> [--brief <file>] [--attempt <n>] [--base <sha>] [--lines <n>] [--kind code|docs|contract]\n' +
|
|
67
|
+
'usage: code-forge block open <id> --run <r> [--level L<n>] --owned <paths…> --acceptance <file> [--brief <file>] [--attempt <n>] [--base <sha>] [--lines <n>] [--kind code|docs|contract]\n' +
|
|
62
68
|
' code-forge block attempt|rebase <id> --run <r> · block close <id> --run <r> [--transcript <file>] [--report <file>] [--no-require-reviews] · block claim <id> <path> --run <r> · block stop <id> --run <r> --reason <text>\n' +
|
|
63
69
|
' code-forge block waive <id> <finding-id> --run <r> --file <path> --reason <text>\n';
|
|
64
70
|
|
|
@@ -67,7 +73,8 @@ const FLAG_VALUES = ['run', 'level', 'acceptance', 'brief', 'attempt', 'base', '
|
|
|
67
73
|
|
|
68
74
|
/**
|
|
69
75
|
* @param {string[]} args
|
|
70
|
-
* @param {{stdout?: {write: (s: string) => unknown}, stderr?: {write: (s: string) => unknown}, probe?: import('../state/run.mjs').StartTimeProbe}} [deps]
|
|
76
|
+
* @param {{stdout?: {write: (s: string) => unknown}, stderr?: {write: (s: string) => unknown}, probe?: import('../state/run.mjs').StartTimeProbe, now?: () => Date}} [deps] -
|
|
77
|
+
* `now` (B48): the clock of the autopilot expiry checks.
|
|
71
78
|
* @returns {Promise<number>}
|
|
72
79
|
*/
|
|
73
80
|
export async function runBlock(args, deps = {}) {
|
|
@@ -84,7 +91,7 @@ export async function runBlock(args, deps = {}) {
|
|
|
84
91
|
const [id, extra] = positionals;
|
|
85
92
|
const runId = typeof flags.run === 'string' ? flags.run : undefined;
|
|
86
93
|
if (!id || !runId) throw new StateError('usage', 'a block id and --run are required');
|
|
87
|
-
if (sub === 'open' && (typeof flags.acceptance !== 'string' || !Array.isArray(flags.owned) || typeof flags.level !== 'string')) {
|
|
94
|
+
if (sub === 'open' && (typeof flags.acceptance !== 'string' || !Array.isArray(flags.owned) || (flags.level !== undefined && typeof flags.level !== 'string'))) {
|
|
88
95
|
throw new StateError('usage', 'block open needs --level, --owned and --acceptance');
|
|
89
96
|
}
|
|
90
97
|
if (sub === 'claim' && (!extra || positionals.length !== 2)) throw new StateError('usage', 'block claim needs exactly one path');
|
|
@@ -93,8 +100,18 @@ export async function runBlock(args, deps = {}) {
|
|
|
93
100
|
if (sub === 'waive' && typeof flags.file !== 'string') throw new StateError('usage', 'block waive needs --file <path> (a finding id is scoped to one file)');
|
|
94
101
|
const record = await readRun(runId);
|
|
95
102
|
const writeRow = (/** @type {Record<string, any>} */ row) => appendRow(row, { slug: record.project });
|
|
103
|
+
// B48: every block command runs the lazy autopilot checks (grant expiry, expired approvals);
|
|
104
|
+
// a failed check is a warning, never a reason to stop the block command
|
|
105
|
+
try {
|
|
106
|
+
err(approvalCheckText(await expiryChecks(runId, (deps.now ?? (() => new Date()))(), { writeRow })));
|
|
107
|
+
} catch (thrown) {
|
|
108
|
+
err(`autopilot: WARN the expiry check failed (${thrown?.message ?? String(thrown)})\n`);
|
|
109
|
+
}
|
|
96
110
|
|
|
97
111
|
if (sub === 'open') {
|
|
112
|
+
// B47: no --level ⇒ the level autopilot chose for the block (a signed autopilot.level row)
|
|
113
|
+
const level = typeof flags.level === 'string' ? flags.level : await autopilotLevel(runId, id, record);
|
|
114
|
+
if (level === null) throw new StateError('usage', `no --level given and no autopilot level recorded for ${id}`);
|
|
98
115
|
const acceptance = parseYAML(await readFile(/** @type {string} */ (flags.acceptance), 'utf8'), { prettyErrors: false });
|
|
99
116
|
let brief;
|
|
100
117
|
if (typeof flags.brief === 'string') {
|
|
@@ -106,7 +123,7 @@ export async function runBlock(args, deps = {}) {
|
|
|
106
123
|
const { block, pointer } = await openBlock({
|
|
107
124
|
runId,
|
|
108
125
|
id,
|
|
109
|
-
level
|
|
126
|
+
level,
|
|
110
127
|
owned: /** @type {string[]} */ (flags.owned),
|
|
111
128
|
acceptance,
|
|
112
129
|
attempt: intFlag(flags.attempt, 'attempt'),
|
|
@@ -117,8 +134,9 @@ export async function runBlock(args, deps = {}) {
|
|
|
117
134
|
cfg: await openConfig(record.workspace),
|
|
118
135
|
writeRow,
|
|
119
136
|
});
|
|
120
|
-
const raised = block.level !==
|
|
121
|
-
|
|
137
|
+
const raised = block.level !== level ? ` (${block.kind} floor, asked ${level})` : '';
|
|
138
|
+
const chosen = typeof flags.level === 'string' ? '' : ' (autopilot level)';
|
|
139
|
+
out(`block ${id} open · ${block.level}${raised}${chosen} · ${block.kind} · attempt ${block.attempt} · base ${block.base_sha.slice(0, 12)}\n`);
|
|
122
140
|
out(`${pointer ?? 'brief: none given (--brief <file>)'}\n`);
|
|
123
141
|
return 0;
|
|
124
142
|
}
|
|
@@ -204,6 +222,20 @@ export async function runBlock(args, deps = {}) {
|
|
|
204
222
|
return 2;
|
|
205
223
|
}
|
|
206
224
|
|
|
225
|
+
/**
|
|
226
|
+
* B47: the level `autopilot level` chose for the block — the latest `autopilot.level` row of this
|
|
227
|
+
* run for it whose MAC verifies — or null when there is none.
|
|
228
|
+
* @param {string} runId @param {string} id @param {Record<string, any>} record
|
|
229
|
+
* @returns {Promise<string | null>}
|
|
230
|
+
*/
|
|
231
|
+
async function autopilotLevel(runId, id, record) {
|
|
232
|
+
const rows = (await readAllRows(record.project)).filter((r) => r?.event === 'autopilot.level' && r.run === runId && r.block === id && typeof r.level === 'string');
|
|
233
|
+
if (rows.length === 0) return null;
|
|
234
|
+
const key = await loadKey(runId);
|
|
235
|
+
const valid = rows.filter((r) => verifyRow(r, key).ok);
|
|
236
|
+
return valid.length === 0 ? null : valid[valid.length - 1].level;
|
|
237
|
+
}
|
|
238
|
+
|
|
207
239
|
/**
|
|
208
240
|
* The project config for `block open`'s coder floor (B34): the loaded `.code-forge.yml`, or
|
|
209
241
|
* undefined when there is none or it does not load — the floor then takes its default (L1), the
|
package/src/cli/facts.mjs
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
import { accessSync, constants, statSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { loadProjectConfig } from '../config/load.mjs';
|
|
14
|
+
import { loadProjectConfig, projectRootFor, slugFor } from '../config/load.mjs';
|
|
15
15
|
import { buildFacts, FactsError } from '../session/facts.mjs';
|
|
16
16
|
import { runRootFor, SessionError } from '../session/spawn.mjs';
|
|
17
17
|
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
18
18
|
import { StateError } from '../state/paths.mjs';
|
|
19
19
|
import { redact, writeSafe } from '../util/redact.mjs';
|
|
20
|
-
import { slugFor } from './run.mjs';
|
|
21
20
|
import { exitCodeFor } from './spawn.mjs';
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -64,14 +63,16 @@ export async function runFactsVerb(args, deps = {}) {
|
|
|
64
63
|
writeSafe(stderr, USAGE);
|
|
65
64
|
return 2;
|
|
66
65
|
}
|
|
67
|
-
|
|
66
|
+
// B50: the project root's config, slug, plans dir and snapshot, also from a subfolder (flag paths stay cwd-relative)
|
|
67
|
+
const root = projectRootFor(cwd);
|
|
68
|
+
const loaded = await loadProjectConfig(root);
|
|
68
69
|
if (!loaded.ok) {
|
|
69
70
|
writeSafe(stderr, `facts: ${loaded.message}\n`);
|
|
70
71
|
return 2;
|
|
71
72
|
}
|
|
72
73
|
const cfg = /** @type {Record<string, any>} */ (loaded.config);
|
|
73
74
|
const briefPath = path.resolve(cwd, flags.brief);
|
|
74
|
-
const plansDir = path.resolve(
|
|
75
|
+
const plansDir = path.resolve(root, cfg.project?.plans_dir ?? 'plans');
|
|
75
76
|
const outPath = typeof flags.out === 'string' ? path.resolve(cwd, flags.out) : path.join(plansDir, `${path.basename(briefPath, path.extname(briefPath))}.facts.md`);
|
|
76
77
|
const sources = Array.isArray(flags.sources) ? flags.sources.map((s) => path.resolve(cwd, s)) : [];
|
|
77
78
|
// --brief must be a regular file; a --sources path may be a file or a directory
|
|
@@ -89,11 +90,11 @@ export async function runFactsVerb(args, deps = {}) {
|
|
|
89
90
|
const run = typeof flags.run === 'string' ? flags.run : undefined;
|
|
90
91
|
// pre-run argument checks: a bad --run id or an unusable project state is usage (exit 2)
|
|
91
92
|
const runRoot = run ? runRootFor(run, cfg?.tmp?.root) : undefined;
|
|
92
|
-
const slug = slugFor(cfg,
|
|
93
|
+
const slug = slugFor(cfg, root);
|
|
93
94
|
let result;
|
|
94
95
|
try {
|
|
95
96
|
result = await buildFacts(
|
|
96
|
-
{ cfg, briefPath, sources, outPath, run, runRoot, slug, projectDir:
|
|
97
|
+
{ cfg, briefPath, sources, outPath, run, runRoot, slug, projectDir: root, timeoutMs: timeout === undefined ? undefined : timeout * 1000 },
|
|
97
98
|
{ ...deps, stderr },
|
|
98
99
|
);
|
|
99
100
|
} catch (err) {
|
package/src/cli/jev.mjs
CHANGED
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
* or mistyped one is a usage error naming it, with nothing recorded. `system1.disable` still
|
|
17
17
|
* applies. No key is resolved, no request is sent, and the row says `source: 'rules'`.
|
|
18
18
|
* The ledger slug is `--slug`, else `project.slug`, else the directory-name slug `run` and
|
|
19
|
-
* `author` use (`slugFor`; B36 — it used to fall back to `code-forge`).
|
|
20
|
-
*
|
|
19
|
+
* `author` use (`slugFor`; B36 — it used to fall back to `code-forge`). B50: the config and the
|
|
20
|
+
* directory name are the PROJECT ROOT's (`projectRootFor`): inside git, the nearest regular-file
|
|
21
|
+
* `.code-forge.yml` from the cwd up to the git top level, else the git top level; outside git, the
|
|
22
|
+
* nearest one up to the home directory when the cwd is under it, else the cwd alone. A run from a
|
|
23
|
+
* subfolder thus writes to the project's ledger. An unknown flag is a usage error.
|
|
21
24
|
*
|
|
22
25
|
* Resolves the Jev key through B2's chain (`resolveKey`, earlier batch — the seam rule allows a
|
|
23
26
|
* Wave 1 module to import a block in an earlier batch statically), asks Jev, runs the answer
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
|
|
32
35
|
import { randomUUID } from 'node:crypto';
|
|
33
36
|
import { readFile } from 'node:fs/promises';
|
|
34
|
-
import { loadConfigFile, DEFAULT_CONFIG_FILENAME } from '../config/load.mjs';
|
|
37
|
+
import { loadConfigFile, DEFAULT_CONFIG_FILENAME, projectRootFor, slugFor } from '../config/load.mjs';
|
|
35
38
|
import path from 'node:path';
|
|
36
39
|
import { buildQuestionPayload, isKnownQuestion, isQuestionDisabled } from '../decide/questions.mjs';
|
|
37
40
|
import { askJev as realAskJev } from '../decide/jev-client.mjs';
|
|
@@ -40,7 +43,6 @@ import { resolveDispatchQuestionsByRules } from '../decide/fallback-rules.mjs';
|
|
|
40
43
|
import { createDefaultKeyStore, resolveKey } from '../keys/store.mjs';
|
|
41
44
|
import { appendRow as realAppendRow } from '../ledger/write.mjs';
|
|
42
45
|
import { writeSafe } from '../util/redact.mjs';
|
|
43
|
-
import { slugFor } from './run.mjs';
|
|
44
46
|
|
|
45
47
|
const USAGE = 'usage: code-forge jev ask <question-id> --state <file> [--cwd <dir>] [--slug <slug>] [--key-ref <ref>] [--block <id>] [--plan <file>] [--rules]\n';
|
|
46
48
|
|
|
@@ -180,7 +182,9 @@ export async function runJev(args, deps = {}) {
|
|
|
180
182
|
return 1;
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
// B50: the project's config and ledger, also from a subfolder of the project
|
|
186
|
+
const root = projectRootFor(cwd);
|
|
187
|
+
const configPath = path.join(root, DEFAULT_CONFIG_FILENAME);
|
|
184
188
|
const loaded = await loadConfig(configPath);
|
|
185
189
|
let cfg = {};
|
|
186
190
|
if (loaded.ok) {
|
|
@@ -191,7 +195,7 @@ export async function runJev(args, deps = {}) {
|
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
// the same ledger as the rest of the tool: --slug, else project.slug, else the directory name
|
|
194
|
-
const slug = slugFlag.value ?? slugFor(cfg,
|
|
198
|
+
const slug = slugFlag.value ?? slugFor(cfg, root);
|
|
195
199
|
|
|
196
200
|
if (isQuestionDisabled(id, cfg)) {
|
|
197
201
|
err(`jev ask: question "${id}" is disabled by system1.disable\n`);
|
package/src/cli/logs.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { spawn } from 'node:child_process';
|
|
36
|
-
import {
|
|
36
|
+
import { readFileSync } from 'node:fs';
|
|
37
37
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
38
38
|
import os from 'node:os';
|
|
39
39
|
import path from 'node:path';
|
|
@@ -43,7 +43,7 @@ import { NO_ERROR_LOG_ENV, capBytes, createScrubber, describeCounts, errorLogPat
|
|
|
43
43
|
import { compareVersions, fixStatus, loadKnownFixes } from '../util/known-fixes.mjs';
|
|
44
44
|
import { redact, writeSafe } from '../util/redact.mjs';
|
|
45
45
|
import { currentRunRoot } from '../util/tmp.mjs';
|
|
46
|
-
import {
|
|
46
|
+
import { GH_TIMEOUT_MS, findReported, findSecret, ghWithBodyFile, pathLookup } from '../util/gh.mjs';
|
|
47
47
|
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
48
48
|
import { AI_REPORT_MAX_BYTES, applyItems, describeAiCounts, runAiScrub } from '../session/scrub.mjs';
|
|
49
49
|
|
|
@@ -83,7 +83,8 @@ export const DISCLOSURE = disclosure();
|
|
|
83
83
|
|
|
84
84
|
export const CONFIRM_MESSAGE = 'This will be public on GitHub. Send it?';
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
// the secret check and the gh helpers live in `util/gh.mjs` (shared with B47's waiver issues)
|
|
87
|
+
export { REPORT_SECRET_RULES, entropy, findSecret } from '../util/gh.mjs';
|
|
87
88
|
const NPM_TIMEOUT_MS = 10_000;
|
|
88
89
|
/** `code-forge doctor --json` for `--with-doctor` gets at most this long. */
|
|
89
90
|
export const DOCTOR_TIMEOUT_MS = 60_000;
|
|
@@ -117,64 +118,6 @@ export const PACKAGE_NAME = '@codedology/code-forge';
|
|
|
117
118
|
* the editor on the terminal (default: argv only, stdio inherited, never a shell).
|
|
118
119
|
*/
|
|
119
120
|
|
|
120
|
-
/**
|
|
121
|
-
* Secret shapes the final report is checked for, after scrubbing. A hit stops the report.
|
|
122
|
-
* @type {ReadonlyArray<{name: string, re: RegExp}>}
|
|
123
|
-
*/
|
|
124
|
-
export const REPORT_SECRET_RULES = Object.freeze([
|
|
125
|
-
{ name: 'API key (sk-)', re: /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{10,}/ },
|
|
126
|
-
{ name: 'GitHub token', re: /(?<![A-Za-z0-9_])(?:gh[opsur]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/ },
|
|
127
|
-
{ name: 'Slack token', re: /(?<![A-Za-z0-9_-])xox[abprs]-[A-Za-z0-9-]{10,}/ },
|
|
128
|
-
{ name: 'AWS access key', re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![0-9A-Z])/ },
|
|
129
|
-
{ name: 'private key', re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },
|
|
130
|
-
{ name: 'JWT', re: /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/ },
|
|
131
|
-
// copied without g/y: `.test()` must carry no `lastIndex` state between checks
|
|
132
|
-
...SECRET_LOOKING_PATTERNS.map((re) => ({ name: 'secret-shaped token', re: new RegExp(re.source, re.flags.replace(/[gy]/g, '')) })),
|
|
133
|
-
]);
|
|
134
|
-
|
|
135
|
-
/** @param {string} s @returns {number} Shannon entropy in bits per character */
|
|
136
|
-
export function entropy(s) {
|
|
137
|
-
/** @type {Map<string, number>} */
|
|
138
|
-
const freq = new Map();
|
|
139
|
-
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
140
|
-
let bits = 0;
|
|
141
|
-
for (const n of freq.values()) {
|
|
142
|
-
const p = n / s.length;
|
|
143
|
-
bits -= p * Math.log2(p);
|
|
144
|
-
}
|
|
145
|
-
return bits;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* A 32+ character run of `[A-Za-z0-9+/_=-]` with at least two of upper/lower/digit and entropy of
|
|
150
|
-
* at least 4 bits per character (a hex hash tops out at 4 and is not flagged; a random token is).
|
|
151
|
-
* @param {string} line
|
|
152
|
-
* @returns {boolean}
|
|
153
|
-
*/
|
|
154
|
-
function hasHighEntropyRun(line) {
|
|
155
|
-
for (const m of line.matchAll(/[A-Za-z0-9+/_=-]{32,}/g)) {
|
|
156
|
-
const run = m[0];
|
|
157
|
-
const classes = [/[A-Z]/, /[a-z]/, /[0-9]/].filter((re) => re.test(run)).length;
|
|
158
|
-
if (classes >= 2 && entropy(run) >= 4) return true;
|
|
159
|
-
}
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The first secret-shaped text in `text` (rule name and 1-based line), or null. The matched text
|
|
165
|
-
* itself is never returned.
|
|
166
|
-
* @param {string} text
|
|
167
|
-
* @returns {{rule: string, line: number}|null}
|
|
168
|
-
*/
|
|
169
|
-
export function findSecret(text) {
|
|
170
|
-
const lines = text.split('\n');
|
|
171
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
172
|
-
for (const r of REPORT_SECRET_RULES) if (r.re.test(lines[i])) return { rule: r.name, line: i + 1 };
|
|
173
|
-
if (hasHighEntropyRun(lines[i])) return { rule: 'high-entropy string', line: i + 1 };
|
|
174
|
-
}
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
121
|
/**
|
|
179
122
|
* `owner/repo` from package.json `bugs.url`, else `repository` (string or `{url}`).
|
|
180
123
|
* @param {any} [pkg]
|
|
@@ -197,21 +140,6 @@ export function issueRepo(pkg) {
|
|
|
197
140
|
return null;
|
|
198
141
|
}
|
|
199
142
|
|
|
200
|
-
/** @param {string} pathEnv @returns {(cmd: string) => boolean} */
|
|
201
|
-
function pathLookup(pathEnv) {
|
|
202
|
-
return (cmd) =>
|
|
203
|
-
pathEnv.split(path.delimiter).some((dir) => {
|
|
204
|
-
if (!dir) return false;
|
|
205
|
-
try {
|
|
206
|
-
const f = path.join(dir, cmd);
|
|
207
|
-
accessSync(f, fsConstants.X_OK);
|
|
208
|
-
return statSync(f).isFile();
|
|
209
|
-
} catch {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
143
|
/** @param {ErrorEntry} e */
|
|
216
144
|
function command(e) {
|
|
217
145
|
return e.sub ? `${e.verb} ${e.sub}` : e.verb;
|
|
@@ -895,72 +823,6 @@ async function editBody(body, env, deps, out) {
|
|
|
895
823
|
}
|
|
896
824
|
}
|
|
897
825
|
|
|
898
|
-
/**
|
|
899
|
-
* An earlier issue holding `fp` in its body (open or closed): `{hit}` (null when there is none),
|
|
900
|
-
* or `{why}` when the search failed (`gh exit n`; never gh's output).
|
|
901
|
-
* @param {typeof realExec} exec @param {NodeJS.ProcessEnv} env @param {string} repo @param {string} fp
|
|
902
|
-
* @returns {Promise<{hit: {number: number, url: string, state: string}|null} | {why: string}>}
|
|
903
|
-
*/
|
|
904
|
-
async function findReported(exec, env, repo, fp) {
|
|
905
|
-
const res = await ghRun(exec, ['gh', 'issue', 'list', '--repo', repo, '--state', 'all', '--search', `${fp} in:body`, '--json', 'number,url,state', '--limit', '5'], env);
|
|
906
|
-
if (res.result !== 'ok') return { why: res.code !== null ? `gh exit ${res.code}` : res.timedOut ? 'gh timed out' : 'gh did not finish' };
|
|
907
|
-
let list;
|
|
908
|
-
try {
|
|
909
|
-
list = JSON.parse(res.stdout);
|
|
910
|
-
} catch {
|
|
911
|
-
return { why: 'gh gave no list' };
|
|
912
|
-
}
|
|
913
|
-
if (!Array.isArray(list)) return { why: 'gh gave no list' };
|
|
914
|
-
const hit = list.find((i) => Number.isSafeInteger(i?.number) && typeof i?.url === 'string' && /^https:\/\//.test(i.url));
|
|
915
|
-
return { hit: hit ? { number: hit.number, url: hit.url, state: typeof hit.state === 'string' ? hit.state.toLowerCase() : 'unknown' } : null };
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Write `text` to a private temp file and run the first argv of `argvsFor(file)`; when it fails
|
|
920
|
-
* over a label, run the next one (once).
|
|
921
|
-
* @param {typeof realExec} exec @param {NodeJS.ProcessEnv} env @param {string} text
|
|
922
|
-
* @param {(file: string) => string[][]} argvsFor
|
|
923
|
-
* @param {(stdout: string) => void} [onOk]
|
|
924
|
-
* @returns {Promise<string|null>} null when gh succeeded; else why (`exit n`, never its output).
|
|
925
|
-
*/
|
|
926
|
-
async function ghWithBodyFile(exec, env, text, argvsFor, onOk) {
|
|
927
|
-
/** @type {string|null} */
|
|
928
|
-
let dir = null;
|
|
929
|
-
try {
|
|
930
|
-
let res;
|
|
931
|
-
try {
|
|
932
|
-
const root = currentRunRoot();
|
|
933
|
-
await mkdir(root, { recursive: true });
|
|
934
|
-
dir = await mkdtemp(path.join(root, 'report-'));
|
|
935
|
-
const bodyFile = path.join(dir, 'body.md');
|
|
936
|
-
await writeFile(bodyFile, text, { encoding: 'utf8', mode: 0o600 });
|
|
937
|
-
const [first, retry] = argvsFor(bodyFile);
|
|
938
|
-
res = await ghRun(exec, first, env);
|
|
939
|
-
// gh's stderr is matched to decide the retry, never printed
|
|
940
|
-
if (res.result !== 'ok' && retry && /label/i.test(res.stderr)) res = await ghRun(exec, retry, env);
|
|
941
|
-
} catch {
|
|
942
|
-
return 'could not write the report file';
|
|
943
|
-
}
|
|
944
|
-
if (res.result !== 'ok') return res.code !== null ? `exit ${res.code}` : res.timedOut ? 'timed out' : 'did not finish';
|
|
945
|
-
onOk?.(res.stdout);
|
|
946
|
-
return null;
|
|
947
|
-
} finally {
|
|
948
|
-
if (dir !== null) await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
* @param {typeof realExec} exec @param {string[]} argv @param {NodeJS.ProcessEnv} env
|
|
954
|
-
* @returns {Promise<{result: string, code: number|null, timedOut: boolean, stdout: string, stderr: string}>}
|
|
955
|
-
*/
|
|
956
|
-
async function ghRun(exec, argv, env) {
|
|
957
|
-
try {
|
|
958
|
-
return await exec(argv, { env, timeoutMs: GH_TIMEOUT_MS });
|
|
959
|
-
} catch {
|
|
960
|
-
return { result: 'failed', code: null, timedOut: false, stdout: '', stderr: '' };
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
826
|
/**
|
|
965
827
|
* The prefilled issue-form link: the form's `details` field holds the whole body (GitHub fills an
|
|
966
828
|
* issue form's fields from query parameters named by field id).
|
package/src/cli/plan.mjs
CHANGED
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
* WARN lines (a required section found by its position under a near-miss heading) go to stderr and
|
|
6
6
|
* never fail the check. Either way stdout ends with the lane recorded per block (B36):
|
|
7
7
|
* `lanes: B1 L1 (jev) · B2 none` — read from the project's ledger (`--slug`, else `project.slug`,
|
|
8
|
-
* else the directory-name slug `run`, `author` and `jev ask` use).
|
|
9
|
-
* Limits come from `.code-forge.yml` when there is one (`caps.coders`, `budget.block_cases`,
|
|
8
|
+
* else the directory-name slug `run`, `author` and `jev ask` use — the project root's, B50).
|
|
9
|
+
* Limits come from the project root's `.code-forge.yml` when there is one (`caps.coders`, `budget.block_cases`,
|
|
10
10
|
* `budget.block_lines`), else the plan defaults.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { loadProjectConfig } from '../config/load.mjs';
|
|
15
|
+
import { loadProjectConfig, projectRootFor, slugFor } from '../config/load.mjs';
|
|
16
16
|
import { ledgerPath } from '../ledger/paths.mjs';
|
|
17
17
|
import { readAllRows } from '../ledger/write.mjs';
|
|
18
|
+
import { FactsError, readProjectBins } from '../session/facts.mjs';
|
|
18
19
|
import { checkPlan } from '../session/plan-check.mjs';
|
|
19
20
|
import { parseFlags } from '../state/cli-args.mjs';
|
|
20
21
|
import { StateError } from '../state/paths.mjs';
|
|
21
22
|
import { writeSafe } from '../util/redact.mjs';
|
|
22
|
-
import { slugFor } from './run.mjs';
|
|
23
23
|
|
|
24
24
|
const USAGE = 'usage: code-forge plan check <plan-file> [--facts <sheet>] [--slug <slug>]\n';
|
|
25
25
|
|
|
@@ -50,14 +50,16 @@ export async function runPlanVerb(args, deps = {}) {
|
|
|
50
50
|
};
|
|
51
51
|
const text = read(planPath, 'the plan file');
|
|
52
52
|
const factsText = factsPath ? read(factsPath, '--facts') : undefined;
|
|
53
|
-
|
|
53
|
+
// B50: the project's config and ledger, also from a subfolder of the project
|
|
54
|
+
const root = projectRootFor(cwd);
|
|
55
|
+
const loaded = await loadProjectConfig(root);
|
|
54
56
|
if (!loaded.ok && loaded.error !== 'not-found') {
|
|
55
57
|
writeSafe(stderr, `plan: ${loaded.message}\n`);
|
|
56
58
|
return 2;
|
|
57
59
|
}
|
|
58
60
|
const cfg = loaded.ok ? loaded.config : undefined;
|
|
59
61
|
// the same ledger as the rest of the tool (and `jev ask`): --slug, else project.slug, else the directory name
|
|
60
|
-
const slug = typeof flags.slug === 'string' ? flags.slug : slugFor(cfg,
|
|
62
|
+
const slug = typeof flags.slug === 'string' ? flags.slug : slugFor(cfg, root);
|
|
61
63
|
try {
|
|
62
64
|
ledgerPath(slug);
|
|
63
65
|
} catch {
|
|
@@ -65,7 +67,16 @@ export async function runPlanVerb(args, deps = {}) {
|
|
|
65
67
|
}
|
|
66
68
|
// a missing ledger is no rows (every block `none`); any other read error propagates as itself
|
|
67
69
|
const decisions = await readAllRows(slug);
|
|
68
|
-
|
|
70
|
+
// package.json is read lazily: only a command claim that needs the bin prefix reads it, so a bad
|
|
71
|
+
// package.json fails only the plans that depend on it
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = checkPlan(text, { planPath, factsPath, factsText, cfg, decisions, projectBins: () => readProjectBins(root) });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (!(err instanceof FactsError)) throw err;
|
|
77
|
+
writeSafe(stderr, `plan: ${err.message}\n`);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
69
80
|
for (const line of result.warnings) writeSafe(stderr, `${line}\n`);
|
|
70
81
|
for (const line of result.errors) writeSafe(stderr, `${line}\n`);
|
|
71
82
|
if (result.ok) writeSafe(stdout, `plan check: ok (${result.blocks} blocks)\n`);
|
package/src/cli/run.mjs
CHANGED
|
@@ -22,37 +22,18 @@
|
|
|
22
22
|
* run record per ticket — no restart).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { readdirSync } from 'node:fs';
|
|
26
25
|
import path from 'node:path';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { validateConfig } from '../config/validate.mjs';
|
|
26
|
+
import { reloadFailureText, reloadWorkspace } from '../autopilot/limits.mjs';
|
|
27
|
+
import { DEFAULT_CONFIG_FILENAME, loadProjectConfig, projectRootFor, slugFor } from '../config/load.mjs';
|
|
30
28
|
import { budgetUsdOf, runSpend } from '../ledger/spend.mjs';
|
|
31
29
|
import { appendRow, readAllRows } from '../ledger/write.mjs';
|
|
32
30
|
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
33
31
|
import { StateError } from '../state/paths.mjs';
|
|
34
|
-
import { endRun, processStartTime, readRun, reattachWorker,
|
|
32
|
+
import { endRun, processStartTime, readRun, reattachWorker, startRun, stopPinnedWorker } from '../state/run.mjs';
|
|
35
33
|
import { writeSafe } from '../util/redact.mjs';
|
|
36
34
|
import { sweepRoots, tmpBase } from '../util/tmp.mjs';
|
|
37
35
|
import { launchWorker, repinWorker } from '../worker/loop.mjs';
|
|
38
|
-
import {
|
|
39
|
-
import { repoRootOf, WorkerError } from '../worker/ticket.mjs';
|
|
40
|
-
import { hasCliOnPath } from './validate.mjs';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `project.slug` from config, else the workspace directory name as a ledger slug. The configured
|
|
44
|
-
* value is NOT trusted here: `startRun` refuses any slug outside `/^[a-z0-9][a-z0-9-]*$/`
|
|
45
|
-
* (`bad-project`) before anything is written, so `../x` never reaches the ledger path. Run ids
|
|
46
|
-
* are checked the same way by the state layer (`assertRunId`, `bad-run-id`).
|
|
47
|
-
* @param {Record<string, any>} cfg @param {string} workspace
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
export function slugFor(cfg, workspace) {
|
|
51
|
-
const configured = cfg?.project?.slug;
|
|
52
|
-
if (typeof configured === 'string' && configured.length > 0) return configured;
|
|
53
|
-
const derived = path.basename(workspace).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
54
|
-
return derived || 'project';
|
|
55
|
-
}
|
|
36
|
+
import { repoRootOf } from '../worker/ticket.mjs';
|
|
56
37
|
|
|
57
38
|
/**
|
|
58
39
|
* B33: the run's spend so far (estimated USD from its ledger rows, open blocks included) and its
|
|
@@ -137,7 +118,8 @@ export async function runRun(args, deps = {}) {
|
|
|
137
118
|
return 0;
|
|
138
119
|
}
|
|
139
120
|
if (sub === 'start') {
|
|
140
|
-
|
|
121
|
+
// B50: the run's workspace is the project root (its config and ledger slug), also from a subfolder
|
|
122
|
+
const workspace = projectRootFor(path.resolve(typeof flags.cwd === 'string' ? flags.cwd : process.cwd()));
|
|
141
123
|
const loaded = await loadProjectConfig(workspace);
|
|
142
124
|
if (!loaded.ok) {
|
|
143
125
|
err(`run start: ${loaded.message}\n`);
|
|
@@ -190,52 +172,29 @@ export async function runRun(args, deps = {}) {
|
|
|
190
172
|
|
|
191
173
|
/**
|
|
192
174
|
* `run reload --run <id>`: load + validate the workspace config, pin the queue's pending tickets
|
|
193
|
-
* to the old snapshot and store the new one (`reloadRun`). Exit 0 on a reload
|
|
194
|
-
* config that does not load or validate, or on a refused key.
|
|
175
|
+
* to the old snapshot and store the new one (`reloadWorkspace` → `reloadRun`). Exit 0 on a reload
|
|
176
|
+
* or no change, 1 on a config that does not load or validate, or on a refused key. B48: while an
|
|
177
|
+
* autopilot grant is active, a change to a limit key is refused (`autopilot-limit`): only the
|
|
178
|
+
* owner changes one, with `code-forge autopilot approve` (the verb never passes its internal allow).
|
|
195
179
|
* @param {string} runId
|
|
196
180
|
* @param {{out: (s: string) => void, err: (s: string) => void}} io
|
|
197
181
|
* @returns {Promise<number>}
|
|
198
182
|
*/
|
|
199
183
|
async function reloadVerb(runId, { out, err }) {
|
|
200
|
-
const { project
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
184
|
+
const { project } = await readRun(runId);
|
|
185
|
+
const reloaded = await reloadWorkspace({ runId, writeRow: ledgerWriter(project) });
|
|
186
|
+
if (!reloaded.ok) {
|
|
187
|
+
if (reloaded.error === 'not-found') err(`run reload: no ${DEFAULT_CONFIG_FILENAME} in the workspace — nothing changed\n`);
|
|
188
|
+
else if (reloaded.error === 'parse-error') err(`run reload: the config could not be parsed (${DEFAULT_CONFIG_FILENAME}${reloaded.line ? `:${reloaded.line}` : ''}) — nothing changed\n`);
|
|
189
|
+
else if (reloaded.error !== 'invalid') err(`run reload: ${reloadFailureText(reloaded)} — nothing changed\n`);
|
|
206
190
|
else {
|
|
207
|
-
|
|
208
|
-
|
|
191
|
+
// rule ids and key paths only: a validator message may quote a value
|
|
192
|
+
const errors = reloaded.errors ?? [];
|
|
193
|
+
const lines = errors.map((e) => ` [${e.rule}]${e.path ? ` ${e.path}` : ''}\n`);
|
|
194
|
+
err(`run reload: ${DEFAULT_CONFIG_FILENAME} is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}) — nothing changed; run code-forge validate\n${lines.join('')}`);
|
|
209
195
|
}
|
|
210
196
|
return 1;
|
|
211
197
|
}
|
|
212
|
-
const result = validateConfig(loaded.config, { seenInCache: await loadSeenInCache(), hasCliOnPath });
|
|
213
|
-
if (!result.valid) {
|
|
214
|
-
// rule ids and key paths only: a validator message may quote a value
|
|
215
|
-
const lines = result.errors.map((e) => ` [${e.rule}]${typeof e.path === 'string' && e.path.length > 0 ? ` ${e.path}` : ''}\n`);
|
|
216
|
-
err(`run reload: ${DEFAULT_CONFIG_FILENAME} is invalid (${result.errors.length} error${result.errors.length === 1 ? '' : 's'}) — nothing changed; run code-forge validate\n${lines.join('')}`);
|
|
217
|
-
return 1;
|
|
218
|
-
}
|
|
219
|
-
/** @type {string | null} */
|
|
220
|
-
let repoRoot = null;
|
|
221
|
-
try {
|
|
222
|
-
repoRoot = await repoRootOf(workspace);
|
|
223
|
-
} catch (thrown) {
|
|
224
|
-
if (!(thrown instanceof WorkerError && thrown.code === 'no-repo')) throw thrown;
|
|
225
|
-
// no repository: there is no review queue to pin
|
|
226
|
-
}
|
|
227
|
-
const queueRoot = repoRoot;
|
|
228
|
-
const readPending = () => {
|
|
229
|
-
if (queueRoot === null) return [];
|
|
230
|
-
try {
|
|
231
|
-
readdirSync(queueDir(queueRoot));
|
|
232
|
-
} catch (thrown) {
|
|
233
|
-
if (thrown?.code === 'ENOENT') return [];
|
|
234
|
-
throw new StateError('queue-unreadable', 'cannot read the review queue; nothing changed');
|
|
235
|
-
}
|
|
236
|
-
return pendingTickets(queueRoot);
|
|
237
|
-
};
|
|
238
|
-
const reloaded = await reloadRun({ runId, config: loaded.config, readPending, effectiveSlug: slugFor(loaded.config, workspace), writeRow: ledgerWriter(project) });
|
|
239
198
|
if (reloaded.changed !== null && reloaded.changed.length === 0) {
|
|
240
199
|
out(`run ${runId}: no config change\n`);
|
|
241
200
|
return 0;
|
package/src/cli/s2.mjs
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { loadProjectConfig } from '../config/load.mjs';
|
|
11
|
+
import { loadProjectConfig, projectRootFor, slugFor } from '../config/load.mjs';
|
|
12
12
|
import { runS2 } from '../session/s2.mjs';
|
|
13
13
|
import { runRootFor, SessionError } from '../session/spawn.mjs';
|
|
14
14
|
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
15
15
|
import { StateError } from '../state/paths.mjs';
|
|
16
16
|
import { writeSafe } from '../util/redact.mjs';
|
|
17
|
-
import { slugFor } from './run.mjs';
|
|
18
17
|
import { exitCodeFor } from './spawn.mjs';
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -52,7 +51,9 @@ export async function runS2Verb(args, deps = {}) {
|
|
|
52
51
|
writeSafe(stderr, USAGE);
|
|
53
52
|
return 2;
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
// B50: the project root's config and ledger slug, also from a subfolder
|
|
55
|
+
const root = projectRootFor(process.cwd());
|
|
56
|
+
const loaded = await loadProjectConfig(root);
|
|
56
57
|
if (!loaded.ok) {
|
|
57
58
|
writeSafe(stderr, `s2: ${loaded.message}\n`);
|
|
58
59
|
return 2;
|
|
@@ -67,7 +68,7 @@ export async function runS2Verb(args, deps = {}) {
|
|
|
67
68
|
run: typeof flags.run === 'string' ? flags.run : undefined,
|
|
68
69
|
block: typeof flags.block === 'string' ? flags.block : undefined,
|
|
69
70
|
runRoot: typeof flags.run === 'string' ? runRootFor(flags.run, cfg?.tmp?.root) : undefined,
|
|
70
|
-
slug: slugFor(cfg,
|
|
71
|
+
slug: slugFor(cfg, root),
|
|
71
72
|
timeoutMs: timeout === undefined ? undefined : timeout * 1000,
|
|
72
73
|
},
|
|
73
74
|
{ ...deps, stderr },
|