refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
console.log('sample brownfield source file');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Architecture
|
||||
|
||||
Single-file CLI entry point, no layering.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Concerns
|
||||
|
||||
None recorded.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Conventions
|
||||
|
||||
No conventions established yet.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Integrations
|
||||
|
||||
None.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Stack
|
||||
|
||||
Node.js, plain CommonJS, no framework.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Structure
|
||||
|
||||
`src/` holds the single entry point.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Testing
|
||||
|
||||
No test suite exists yet.
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: "04"
|
||||
plan: "04-01"
|
||||
created: 2026-01-01
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Plan 04-01: Post-Rollover Feature
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: "02"
|
||||
plan: "02-01"
|
||||
created: 2026-01-01
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Plan 02-01: Printable Renderer
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: "01"
|
||||
plan: "01-01"
|
||||
created: 2026-01-01
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Plan 01-01: Tokenizer
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: "01"
|
||||
plan: "01-01"
|
||||
created: 2026-01-01
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Plan 01-01: Tokenizer
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Loop QA Walk Sample
|
||||
|
||||
## What This Is
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- Used by scenarios/milestone-rollover.json: stands in for the roadmapper's re-plan after
|
||||
`milestone complete 1.0` — a real ROADMAP.md carries the next milestone's version marker,
|
||||
which is what lets a boundary-aware oracle actually SEE the milestone_version change
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
# Roadmap: Loop QA Walk Sample
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
executor-01-01-a1b2c3d4
|
||||
|
||||
@@ -5,7 +5,7 @@ current_plan: "01-01"
|
||||
total_plans_in_phase: "1"
|
||||
status: "In Progress"
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# STATE
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: "01"
|
||||
plan: "01-01"
|
||||
status: complete
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Plan 01-01 Summary
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: "01"
|
||||
plan: "01-01"
|
||||
status: in-progress
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Plan 01-01 Summary (PARTIAL — agent abandoned mid-execute)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ name: "Parser"
|
||||
created: 2026-01-01
|
||||
status: failed
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Phase 1: Parser — User Acceptance Testing
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ name: "Parser"
|
||||
created: 2026-01-01
|
||||
status: passed
|
||||
---
|
||||
<!-- provenance: template-derived (gsd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
<!-- provenance: template-derived (msd-core/templates/). NOT real-user-sourced. See #2371 — adequate for happy-path/sequence scenarios, NOT sufficient as a negative fixture asserting the engine correctly rejects input. -->
|
||||
|
||||
# Phase 1: Parser — User Acceptance Testing
|
||||
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
/**
|
||||
* loop-walk.cjs — a small stateful harness around `tests/helpers.cjs` for the
|
||||
* loop QA walk. This module owns NONE of the subprocess mechanics itself
|
||||
* (spawn, retry-on-kill, quote-splitting) — those live in `runGsdTools` /
|
||||
* (spawn, retry-on-kill, quote-splitting) — those live in `runMsdTools` /
|
||||
* `createTempGitProject` and are reused verbatim. What this module adds is
|
||||
* the walk-specific concerns: turning a raw helper result into the typed
|
||||
* `RunResult` from `./result.cjs`, guaranteeing the child never inherits an
|
||||
* ambient `GSD_*` variable that would silently redirect the engine, pinning
|
||||
* ambient `MSD_*` variable that would silently redirect the engine, pinning
|
||||
* the clock so output is reproducible run-to-run, and giving callers a
|
||||
* content-free way to observe what the SUT wrote to disk.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { runGsdTools, cleanup } = require('../helpers.cjs');
|
||||
const { runMsdTools, cleanup } = require('../helpers.cjs');
|
||||
const { classify } = require('./result.cjs');
|
||||
const { createFixture } = require('../fixtures/index.cjs');
|
||||
const { LOOP_HOST_CONTRACT } = require('../../gsd-core/bin/lib/loop-host-contract.cjs');
|
||||
const { LOOP_HOST_CONTRACT } = require('../../msd-core/bin/lib/loop-host-contract.cjs');
|
||||
const { resolveWithin } = require('./paths.cjs');
|
||||
|
||||
/**
|
||||
@@ -48,19 +48,19 @@ const DEFAULT_NOW_MS = 1767225600000;
|
||||
* only passes `git: true` silently also gets `.planning/PROJECT.md` seeded.
|
||||
* A "greenfield" walk built that way would report `project_exists: true`
|
||||
* from its very first step, which makes the greenfield trajectory
|
||||
* meaningless: it can never exercise the create path (`/gsd-new-project`)
|
||||
* meaningless: it can never exercise the create path (`/msd-new-project`)
|
||||
* because a project already exists before the walk begins. Every entry
|
||||
* below therefore states `projectDoc` explicitly rather than relying on
|
||||
* that default, and `greenfield` is the harness default precisely because
|
||||
* an empty git repo with no `.planning/` at all is what a real user's
|
||||
* working tree looks like before running `/gsd-new-project` for the first
|
||||
* working tree looks like before running `/msd-new-project` for the first
|
||||
* time.
|
||||
*
|
||||
* @type {Record<string, (prefix: string) => string>}
|
||||
*/
|
||||
const FIXTURE_BUILDERS = {
|
||||
// A git repo with NO `.planning/` at all — what a user has before
|
||||
// `/gsd-new-project`.
|
||||
// `/msd-new-project`.
|
||||
greenfield: (prefix) => createFixture({ prefix, git: true, planning: false, projectDoc: false }),
|
||||
// `.planning/phases/` exists, but no PROJECT.md yet.
|
||||
planning: (prefix) => createFixture({ prefix, git: true, planning: true, projectDoc: false }),
|
||||
@@ -117,7 +117,7 @@ function collectStatSnapshot(root) {
|
||||
class LoopWalk {
|
||||
/**
|
||||
* @param {string} dir - absolute project root (an already-created temp git project).
|
||||
* @param {number} nowMs - pinned epoch ms passed to every `run()` as `GSD_NOW_MS`.
|
||||
* @param {number} nowMs - pinned epoch ms passed to every `run()` as `MSD_NOW_MS`.
|
||||
*/
|
||||
constructor(dir, nowMs) {
|
||||
this.dir = dir;
|
||||
@@ -150,7 +150,7 @@ class LoopWalk {
|
||||
* @returns {LoopWalk}
|
||||
*/
|
||||
static create(opts = {}) {
|
||||
const { prefix = 'gsd-loop-walk-', nowMs = DEFAULT_NOW_MS, fixture = 'greenfield' } = opts;
|
||||
const { prefix = 'msd-loop-walk-', nowMs = DEFAULT_NOW_MS, fixture = 'greenfield' } = opts;
|
||||
const build = FIXTURE_BUILDERS[fixture];
|
||||
if (!build) {
|
||||
throw new Error(
|
||||
@@ -162,7 +162,7 @@ class LoopWalk {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a `gsd-tools` invocation inside this walk's project and return the
|
||||
* Run a `msd-tools` invocation inside this walk's project and return the
|
||||
* typed `RunResult` from `classify()`.
|
||||
*
|
||||
* SIGNATURE: `run(...argvTokens)` where the LAST argument, if it is a
|
||||
@@ -176,7 +176,7 @@ class LoopWalk {
|
||||
*
|
||||
* WHY `jsonErrors` defaults to `true`: `--json-errors` (`docs/json-errors.md`)
|
||||
* is a real CLI flag, but it is the TOOLING/TEST surface — a human or a real
|
||||
* workflow invokes `gsd_run <cmd>` directly, WITHOUT it. Defaulting to
|
||||
* workflow invokes `msd_run <cmd>` directly, WITHOUT it. Defaulting to
|
||||
* `true` keeps every pre-existing call site's behavior byte-for-byte
|
||||
* unchanged (they all exercised `--json-errors` before this option
|
||||
* existed), while `{ jsonErrors: false }` lets a scenario step opt into
|
||||
@@ -184,16 +184,16 @@ class LoopWalk {
|
||||
* prove the tooling surface works and could never catch a regression a
|
||||
* real user would actually hit.
|
||||
*
|
||||
* WHY the env is built the way it is (ambient `GSD_*` sanitization):
|
||||
* `runGsdTools` composes the child env as
|
||||
* `{ ...process.env, ...TEST_ENV_BASE, ...env }` — so whatever `GSD_*`
|
||||
* WHY the env is built the way it is (ambient `MSD_*` sanitization):
|
||||
* `runMsdTools` composes the child env as
|
||||
* `{ ...process.env, ...TEST_ENV_BASE, ...env }` — so whatever `MSD_*`
|
||||
* variables happen to be set in the *parent* shell (e.g. a developer or CI
|
||||
* runner with `GSD_WORKSTREAM` / `GSD_PROJECT` exported for an unrelated
|
||||
* runner with `MSD_WORKSTREAM` / `MSD_PROJECT` exported for an unrelated
|
||||
* reason) flow straight through into the child and can silently redirect
|
||||
* the engine at a different workstream or project root than the one this
|
||||
* walk created — an invisible, non-deterministic test-pollution vector.
|
||||
* `runGsdTools`'s own merge order means the last object spread wins, so
|
||||
* this method builds an `env` override that sets EVERY ambient `GSD_*` key
|
||||
* `runMsdTools`'s own merge order means the last object spread wins, so
|
||||
* this method builds an `env` override that sets EVERY ambient `MSD_*` key
|
||||
* (scanned live from `process.env`, not a hardcoded list — a new leaking
|
||||
* var needs no code change here to be caught) to `undefined`, then layers
|
||||
* the two intentionally-pinned vars on top.
|
||||
@@ -203,29 +203,29 @@ class LoopWalk {
|
||||
* exercised here via `execFileSync`) iterates `Object.keys(env)` and
|
||||
* OMITS any key whose value is `undefined` from the actual `KEY=VALUE`
|
||||
* pairs handed to the OS — it does not stringify it to the literal text
|
||||
* `"undefined"`. That means `{ GSD_WORKSTREAM: undefined }` in the `env`
|
||||
* option makes the child process behave exactly as if `GSD_WORKSTREAM`
|
||||
* were never exported at all, even though `process.env.GSD_WORKSTREAM` is
|
||||
* `"undefined"`. That means `{ MSD_WORKSTREAM: undefined }` in the `env`
|
||||
* option makes the child process behave exactly as if `MSD_WORKSTREAM`
|
||||
* were never exported at all, even though `process.env.MSD_WORKSTREAM` is
|
||||
* still set and non-empty in the parent. This was verified empirically
|
||||
* (not assumed) — see the module verification transcript — because
|
||||
* `delete`-based approaches were not available here (the merge is inside
|
||||
* `runGsdTools`, not under this method's control) and a stringified
|
||||
* `runMsdTools`, not under this method's control) and a stringified
|
||||
* `"undefined"` would have been a silent correctness bug indistinguishable
|
||||
* from a passing run until an actual leak test caught it.
|
||||
*
|
||||
* ⚠️ KNOWN LIMIT — SUCCESS-PATH STDERR IS NOT OBSERVABLE THROUGH THIS
|
||||
* SUBSTRATE, SO `result.warnings` IS ERROR-PATH-ONLY TODAY: this method
|
||||
* builds `raw.stderr` as `result.success ? '' : (result.error ?? '')`
|
||||
* (below), and `runGsdTools` (`tests/helpers.cjs`) invokes the child via
|
||||
* (below), and `runMsdTools` (`tests/helpers.cjs`) invokes the child via
|
||||
* `execFileSync`, which discards the child's stderr stream entirely on a
|
||||
* clean (non-throwing) exit — Node never captures it, so there is no text
|
||||
* to forward even if this method wanted to. The practical effect: for any
|
||||
* exit-0 invocation, `classify()` always receives `stderr: ''`, so
|
||||
* `result.warnings` can never be non-empty on the success path, no matter
|
||||
* what the real `gsd-tools` process actually wrote to stderr. `warnings`
|
||||
* what the real `msd-tools` process actually wrote to stderr. `warnings`
|
||||
* only ever populates on the exit-1 (error) path, where `result.error`
|
||||
* (helpers.cjs's captured stderr-on-failure text) is threaded through.
|
||||
* Capturing success-path stderr would require changing `runGsdTools` /
|
||||
* Capturing success-path stderr would require changing `runMsdTools` /
|
||||
* `tests/helpers.cjs` (e.g. to `spawnSync`) — out of scope here because
|
||||
* that helper is shared by ~131 test files. DO NOT build an oracle that
|
||||
* assumes `.warnings` reflects success-path stderr; it structurally cannot
|
||||
@@ -246,17 +246,17 @@ class LoopWalk {
|
||||
/** @type {Record<string, string|undefined>} */
|
||||
const sanitize = {};
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (key.startsWith('GSD_')) sanitize[key] = undefined;
|
||||
if (key.startsWith('MSD_')) sanitize[key] = undefined;
|
||||
}
|
||||
const env = {
|
||||
...sanitize,
|
||||
GSD_TEST_MODE: '1',
|
||||
GSD_NOW_MS: String(this.nowMs),
|
||||
MSD_TEST_MODE: '1',
|
||||
MSD_NOW_MS: String(this.nowMs),
|
||||
};
|
||||
|
||||
let raw;
|
||||
try {
|
||||
const result = runGsdTools(argv, this.dir, env);
|
||||
const result = runMsdTools(argv, this.dir, env);
|
||||
raw = {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.output,
|
||||
@@ -265,7 +265,7 @@ class LoopWalk {
|
||||
argv,
|
||||
};
|
||||
} catch {
|
||||
// `runGsdTools` throws only after a retried, persistent subprocess
|
||||
// `runMsdTools` throws only after a retried, persistent subprocess
|
||||
// kill (host OOM / scheduler contention — see helpers.cjs
|
||||
// `throwResourceStarvation`). That is a statement about the HOST, not
|
||||
// the engine under test, so a walk must degrade to a TIMEOUT result
|
||||
@@ -308,7 +308,7 @@ class LoopWalk {
|
||||
* Remove this walk's temp project. Safe to call multiple times.
|
||||
*
|
||||
* `opts.keep` (default `false`) skips the removal entirely — the caller
|
||||
* (a QA-report run, typically via `--keep` / `GSD_QA_KEEP=1`) wants the
|
||||
* (a QA-report run, typically via `--keep` / `MSD_QA_KEEP=1`) wants the
|
||||
* failing/inspected tree left on disk for a human to `cd` into. When kept,
|
||||
* this returns `this.dir` so the caller can record it (e.g. as
|
||||
* `preservedDir` on a scenario report); when actually cleaned up, it
|
||||
|
||||
@@ -93,7 +93,7 @@ const SENTINEL_STRINGS = new Set(['undefined', 'null', 'NaN', '[object Object]']
|
||||
* `checkAgentsInstalled`, surfaced on every `init` subcommand's response via
|
||||
* `init.cts`'s `withProjectRoot`. Points at the INSTALL tree (the runtime's
|
||||
* global config dir, or — for the `claude` runtime — the `agents/` directory
|
||||
* bundled as a sibling of `gsd-core/`), never at the project: agents are
|
||||
* bundled as a sibling of `msd-core/`), never at the project: agents are
|
||||
* installed once, not per-project.
|
||||
*/
|
||||
const EXTERNAL_PATH_ALLOWED_KEYS = Object.freeze(new Set(['agents_dir']));
|
||||
@@ -334,11 +334,11 @@ const ORACLES = Object.freeze([
|
||||
'(VIOLATION). When ctx.projectDir is supplied, an absolute-path string outside it is reported as a SMELL — ' +
|
||||
'never a violation — UNLESS its leaf key name is in EXTERNAL_PATH_ALLOWED_KEYS (a field whose contract is to ' +
|
||||
'point outside the project, e.g. agents_dir) or the string\'s FIRST WHITESPACE-DELIMITED WORD is an EXACT ' +
|
||||
'member of getLiveCommandTokens() (e.g. `/gsd:progress`, or `/gsd-plan-phase 2` where the token carries ' +
|
||||
'member of getLiveCommandTokens() (e.g. `/msd:progress`, or `/msd-plan-phase 2` where the token carries ' +
|
||||
'trailing arguments — see isLiveCommandToken in tests/helpers/live-command-registry.cjs, the single predicate ' +
|
||||
'shared with routing-validity), either of which is skipped entirely: not a smell, not a violation. Exact ' +
|
||||
'membership, never a `startsWith` prefix test with an unconstrained remainder — a string merely SHARING a ' +
|
||||
'prefix with a real token (e.g. `/gsd-x/../../../etc/passwd`) is not exempted and still surfaces as a SMELL. ' +
|
||||
'prefix with a real token (e.g. `/msd-x/../../../etc/passwd`) is not exempted and still surfaces as a SMELL. ' +
|
||||
'Without ctx.projectDir the path check is skipped rather than guessed.',
|
||||
check(ctx) {
|
||||
try {
|
||||
@@ -555,14 +555,14 @@ const ORACLES = Object.freeze([
|
||||
'Every command token actually carried by the payload must name a command present in ctx.liveCommands. ' +
|
||||
'Rather than enumerating fixed field paths (`actions[].command` / `next.command`), this walks the ENTIRE ' +
|
||||
'payload for any string whose first whitespace-delimited word starts with one of LIVE_COMMAND_TOKEN_PREFIXES ' +
|
||||
'(`/gsd-`, `/gsd:`, `$gsd-`) — catching the token wherever it is actually carried: `recommended_command`, a ' +
|
||||
'(`/msd-`, `/msd:`, `$msd-`) — catching the token wherever it is actually carried: `recommended_command`, a ' +
|
||||
'bare-string `next`, `next` as an array of `{command}`, `actions` as an object map, `steps[].command`, ' +
|
||||
'`actions[].next.command`, etc (#3913 P9 SEC-2 — the fixed-path version missed all of these). A candidate ' +
|
||||
'passes only when its first word is an EXACT member of ctx.liveCommands (via the shared ' +
|
||||
'`isLiveCommandToken` predicate, tests/helpers/live-command-registry.cjs — the SAME predicate ' +
|
||||
'value-hygiene\'s command-token exemption uses, so the two can never drift on what a live token is); a ' +
|
||||
'pleasing consequence is that value-hygiene therefore exempts exactly the strings routing-validity vouches ' +
|
||||
'for. A bare `recommended` id (no `/gsd-`-style prefix — e.g. `discuss-phase`, by design ' +
|
||||
'for. A bare `recommended` id (no `/msd-`-style prefix — e.g. `discuss-phase`, by design ' +
|
||||
'`src/smart-entry.cts` `actions.find(a => a.recommended)?.id`, an action id, not a command token) is never a ' +
|
||||
'candidate. Engages only when at least one prefix-shaped string is present; a payload with no such string is ' +
|
||||
'not engaged (ok).',
|
||||
|
||||
@@ -83,7 +83,7 @@ function realpathNearestAncestor(candidate, realpathFn = defaultRealpath) {
|
||||
* MACOS SYMLINK CASE (do not "simplify" this back to a lexical compare): on macOS,
|
||||
* `os.tmpdir()` / `TMPDIR` resolves to `/var/folders/...`, which is itself a symlink to
|
||||
* `/private/var/folders/...`. `fs.mkdtempSync` returns the unresolved `/var/...` form, but
|
||||
* `gsd-tools` output paths are realpath-resolved by the OS/child process to the
|
||||
* `msd-tools` output paths are realpath-resolved by the OS/child process to the
|
||||
* `/private/var/...` form. Comparing those two strings *lexically* makes the very same
|
||||
* directory compare as "outside the project" and fires a spurious `value-hygiene` smell on
|
||||
* every macOS run — and would make a legitimate `resolveWithin` call throw a false containment
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* different documents, which defeats diffing/reviewing a report in CI.
|
||||
*
|
||||
* REPRO LINES ARE ALWAYS STRINGS: `step.repro` is either a real,
|
||||
* copy-pasteable `cd <dir> && node <absolute path to gsd-tools.cjs> ...`
|
||||
* copy-pasteable `cd <dir> && node <absolute path to msd-tools.cjs> ...`
|
||||
* command (plus a trailing `#` shell-comment note about the pinned clock and
|
||||
* cleared env — see `buildRepro`), or a string clearly prefixed
|
||||
* `NOT RUNNABLE: ...` explaining why (the tree was not preserved, or the
|
||||
@@ -42,7 +42,7 @@ const REPORT_VERSION = 1;
|
||||
*
|
||||
* The emitted `node` path is ABSOLUTE (resolved from this file's own
|
||||
* location via `__dirname`), never the repo-relative
|
||||
* `gsd-core/bin/gsd-tools.cjs` — `preservedDir` is a temp project directory
|
||||
* `msd-core/bin/msd-tools.cjs` — `preservedDir` is a temp project directory
|
||||
* unrelated to the repo checkout, so `cd`ing into it first and then
|
||||
* resolving a repo-relative path throws `MODULE_NOT_FOUND` (#3597).
|
||||
*
|
||||
@@ -54,16 +54,16 @@ function buildRepro({ preservedDir, argv }) {
|
||||
return 'NOT RUNNABLE: this step declared no CLI invocation (no "run" array) — there is nothing to reproduce.';
|
||||
}
|
||||
if (!preservedDir) {
|
||||
return 'NOT RUNNABLE: the scenario tree was not preserved for this run — re-run with `--keep` (or `GSD_QA_KEEP=1`) to get a reproducible command.';
|
||||
return 'NOT RUNNABLE: the scenario tree was not preserved for this run — re-run with `--keep` (or `MSD_QA_KEEP=1`) to get a reproducible command.';
|
||||
}
|
||||
const gsdToolsPath = path.resolve(__dirname, '..', '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
const msdToolsPath = path.resolve(__dirname, '..', '..', 'msd-core', 'bin', 'msd-tools.cjs');
|
||||
// The trailing `# ...` is a shell comment, not part of the command: pasting
|
||||
// the whole line (including the note) into a shell still runs correctly,
|
||||
// since everything from `#` to end-of-line is ignored. This keeps the
|
||||
// string a single copy-pasteable line while telling a human reader the
|
||||
// walk also pins a clock and clears ambient env vars this repro cannot.
|
||||
return `cd ${preservedDir} && node ${gsdToolsPath} --json-errors ${argv.join(' ')}`
|
||||
+ ' # note: the walk also pins GSD_NOW_MS and clears ambient GSD_* vars, so results may differ slightly';
|
||||
return `cd ${preservedDir} && node ${msdToolsPath} --json-errors ${argv.join(' ')}`
|
||||
+ ' # note: the walk also pins MSD_NOW_MS and clears ambient MSD_* vars, so results may differ slightly';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
* oracle is how a QA tool reports green on a broken engine.
|
||||
*
|
||||
* Every KIND below corresponds to a shape the real CLI actually produces; each
|
||||
* was observed against `gsd-tools`, not imagined. See
|
||||
* `.gsd/phase/test-2966-loop-qa-walk/40-design.md` for the behavior table.
|
||||
* was observed against `msd-tools`, not imagined. See
|
||||
* `.msd/phase/test-2966-loop-qa-walk/40-design.md` for the behavior table.
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
/**
|
||||
* Frozen classification of a single `gsd-tools` invocation.
|
||||
* Frozen classification of a single `msd-tools` invocation.
|
||||
*
|
||||
* Tests assert on these constants, never on their string values, so renaming a
|
||||
* value is a one-line change here rather than a sweep through the suite.
|
||||
@@ -74,9 +74,9 @@ function tryParseJson(text) {
|
||||
/**
|
||||
* Return the last non-empty line of a stream.
|
||||
*
|
||||
* Load-bearing: `gsd-tools` writes operator warnings to stderr *before* the
|
||||
* Load-bearing: `msd-tools` writes operator warnings to stderr *before* the
|
||||
* structured error envelope — e.g.
|
||||
* `gsd-tools: warning: unknown config key(s) in .planning/config.json: …`
|
||||
* `msd-tools: warning: unknown config key(s) in .planning/config.json: …`
|
||||
* followed by `{"ok":false,…}`. Parsing the whole stderr blob therefore fails
|
||||
* on a command that is in fact conforming. During research this exact shape
|
||||
* produced a false positive against `review-lane`, which is why this function
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
|
||||
/**
|
||||
* run-report.cjs — developer/CI entry point that discovers every QA-walk
|
||||
* scenario, runs it for real against `gsd-tools`, and writes a single
|
||||
* scenario, runs it for real against `msd-tools`, and writes a single
|
||||
* `qa-report.json` document (see `report.cjs`).
|
||||
*
|
||||
* This is a TOOL, not a test file — it is invoked directly with `node`,
|
||||
* never through `gsd-test` / `node --test`, and is deliberately NOT named
|
||||
* never through `msd-test` / `node --test`, and is deliberately NOT named
|
||||
* `*.test.cjs` so it is never picked up by the test runner's glob.
|
||||
*
|
||||
* Usage:
|
||||
* node tests/qa/run-report.cjs [--out <path>] [--keep]
|
||||
*
|
||||
* `--out <path>` defaults to `qa-report.json` at the repo root.
|
||||
* `--keep` (or `GSD_QA_KEEP=1`) preserves every scenario's temp project
|
||||
* `--keep` (or `MSD_QA_KEEP=1`) preserves every scenario's temp project
|
||||
* directory instead of deleting it, and threads real repro commands for it
|
||||
* into the report — see `report.cjs`'s `buildRepro`.
|
||||
*/
|
||||
@@ -88,7 +88,7 @@ function discoverScenarioFiles() {
|
||||
*
|
||||
* @param {{keep?: boolean, liveCommands?: string[]}} [opts] `keep` (default
|
||||
* `false`) is forwarded to `runScenario` — see its own header for the
|
||||
* `GSD_QA_KEEP=1` interaction. `liveCommands` defaults to a fresh call to
|
||||
* `MSD_QA_KEEP=1` interaction. `liveCommands` defaults to a fresh call to
|
||||
* `getLiveCommandTokens()` when omitted.
|
||||
* @returns {Array<ReturnType<typeof runScenario> & {fixture: string}>}
|
||||
* @throws {Error} when no scenario files are discovered.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* WHY THIS FILE EXISTS
|
||||
* ────────────────────
|
||||
* A scenario is a small JSON document describing a sequence of loop-host
|
||||
* points, artifacts an "agent" would write at each point, and `gsd-tools`
|
||||
* points, artifacts an "agent" would write at each point, and `msd-tools`
|
||||
* invocations to run there. `loadScenario` validates the JSON shape so a
|
||||
* malformed scenario fails fast and loud (never a silent no-op walk);
|
||||
* `runScenario` drives a `LoopWalk` through the steps, evaluating both
|
||||
@@ -19,7 +19,7 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { resolveRef } = require('./fixtures/index.cjs');
|
||||
const { LOOP_HOST_CONTRACT } = require('../../gsd-core/bin/lib/loop-host-contract.cjs');
|
||||
const { LOOP_HOST_CONTRACT } = require('../../msd-core/bin/lib/loop-host-contract.cjs');
|
||||
const { MUTATIONS, apply, NOOP } = require('./mutations.cjs');
|
||||
const { resolveWithin, isAbsoluteLike, hasTraversalSegment } = require('./paths.cjs');
|
||||
|
||||
@@ -309,7 +309,7 @@ function evaluateExpectations(expectations, result) {
|
||||
* keep?: boolean,
|
||||
* }} opts `keep` (default `false`) preserves the walk's temp project instead
|
||||
* of removing it in `finally` — see `LoopWalk#cleanup`. Also honored via
|
||||
* the `GSD_QA_KEEP=1` environment variable (an `||`, not an override: either
|
||||
* the `MSD_QA_KEEP=1` environment variable (an `||`, not an override: either
|
||||
* one being truthy keeps the tree), since a CI operator invoking this
|
||||
* through a shell cannot pass a JS option.
|
||||
* @returns {{
|
||||
@@ -328,7 +328,7 @@ function runScenario(scenario, opts) {
|
||||
if (typeof runOracles !== 'function') {
|
||||
throw new Error('runScenario: opts.runOracles (function) is required');
|
||||
}
|
||||
const shouldKeep = keep || process.env.GSD_QA_KEEP === '1';
|
||||
const shouldKeep = keep || process.env.MSD_QA_KEEP === '1';
|
||||
|
||||
const walk = LoopWalk.create({ fixture: scenario.fixture });
|
||||
/** @type {object[]} */
|
||||
@@ -405,7 +405,7 @@ function runScenario(scenario, opts) {
|
||||
const readOnly = !!step.readOnly;
|
||||
const runs = Array.isArray(step.run) ? step.run : [];
|
||||
// `step.jsonErrors === false` opts a step into the human-invocation
|
||||
// path (`gsd_run <cmd>` without `--json-errors`); any other value
|
||||
// path (`msd_run <cmd>` without `--json-errors`); any other value
|
||||
// (including undefined) keeps `LoopWalk#run`'s own default of true.
|
||||
const jsonErrorMode = step.jsonErrors !== false;
|
||||
const runOptions = { jsonErrors: jsonErrorMode };
|
||||
|
||||
@@ -8,24 +8,24 @@
|
||||
* epic #4272).
|
||||
*
|
||||
* WHAT'S EXECUTABLE VS WHAT ISN'T (see
|
||||
* `.gsd/phase/chore-4298-tdd-walk-qa-harness/40-design.md`): the two TDD
|
||||
* `.msd/phase/chore-4298-tdd-walk-qa-harness/40-design.md`): the two TDD
|
||||
* dispatch backends' `${TDD_APPLICABLE ? "..." : ""}` embed-list markers are
|
||||
* LLM-orchestrator pseudo-syntax composed by the agent reading the workflow
|
||||
* markdown at dispatch time — they are NOT real bash and cannot be executed
|
||||
* by a shell. What IS real, executable bash is the resolution block that
|
||||
* computes the `TDD_APPLICABLE` variable itself:
|
||||
* - `gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md`
|
||||
* - `msd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md`
|
||||
* ("harness" backend) — one self-contained fenced block: its own
|
||||
* `gsd_run` shim-discovery preamble, then the `TDD_APPLICABLE_RAW=...` /
|
||||
* `msd_run` shim-discovery preamble, then the `TDD_APPLICABLE_RAW=...` /
|
||||
* `TDD_APPLICABLE_RC=$?` / fail-closed check / `TDD_APPLICABLE=` assignment.
|
||||
* - `gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md`
|
||||
* ("worktree" backend) — the `gsd_run` shim-discovery preamble lives in
|
||||
* - `msd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md`
|
||||
* ("worktree" backend) — the `msd_run` shim-discovery preamble lives in
|
||||
* the file's FIRST fenced block, which is not just the shim: it is the
|
||||
* whole ISOLATION resolution (its own independent fail-closed branches),
|
||||
* ahead of a LATER fenced block that contains the `_TDD_APPLICABLE_RAW=...`
|
||||
* resolution followed by the `EXECUTOR_PROMPT=` composition this harness
|
||||
* does not need. A standalone execution must concatenate the first block
|
||||
* (for its `gsd_run` definition) with just the TDD resolution lines
|
||||
* (for its `msd_run` definition) with just the TDD resolution lines
|
||||
* sliced out of the second.
|
||||
*
|
||||
* This module extracts each backend's real bash via `indexOf`/`slice` on the
|
||||
@@ -40,16 +40,16 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { createTempProject, cleanup, runGsdTools, readFileNormalized, TEST_ENV_BASE } = require('../helpers.cjs');
|
||||
const { createTempProject, cleanup, runMsdTools, readFileNormalized, TEST_ENV_BASE } = require('../helpers.cjs');
|
||||
|
||||
/** Absolute real repo root — NOT the temp fixture's own root. */
|
||||
const REPO_ROOT = path.join(__dirname, '..', '..');
|
||||
|
||||
const RESOLUTION_STEP_PATH = path.join(
|
||||
REPO_ROOT, 'gsd-core', 'workflows', 'execute-phase', 'steps', 'tdd-applicability-resolution.md',
|
||||
REPO_ROOT, 'msd-core', 'workflows', 'execute-phase', 'steps', 'tdd-applicability-resolution.md',
|
||||
);
|
||||
const DISPATCH_STEP_PATH = path.join(
|
||||
REPO_ROOT, 'gsd-core', 'workflows', 'execute-phase', 'steps', 'executor-isolation-dispatch.md',
|
||||
REPO_ROOT, 'msd-core', 'workflows', 'execute-phase', 'steps', 'executor-isolation-dispatch.md',
|
||||
);
|
||||
|
||||
const FENCE_OPEN = '```bash';
|
||||
@@ -192,8 +192,8 @@ function parseResultSentinel(stdout) {
|
||||
|
||||
// Bounds `bash -c` execution (below) of a REAL fenced bash block extracted
|
||||
// from a workflow markdown file's resolution/dispatch step — a bash-hosted
|
||||
// script that may itself shell out further (e.g. via a `gsd_run` shim), not
|
||||
// a bare `gsd-tools.cjs` CLI subcommand invocation. Numerically coincides
|
||||
// script that may itself shell out further (e.g. via a `msd_run` shim), not
|
||||
// a bare `msd-tools.cjs` CLI subcommand invocation. Numerically coincides
|
||||
// with tests/helpers/timeouts.cjs's `BUILD_TIMEOUT_MS` (hooks-bundle build
|
||||
// via scripts/build-hooks.js, an unrelated operation) and with this batch's
|
||||
// other file-local `PLAN_PRE_HOOK_CLI_TIMEOUT_MS` (a different file, a
|
||||
@@ -216,19 +216,19 @@ const TDD_BACKEND_SCRIPT_TIMEOUT_MS = 30000;
|
||||
*/
|
||||
function executeBackendScript(script, cwd) {
|
||||
try {
|
||||
// #4298 Standards+Spec review: `runGsdTools` (tests/helpers.cjs) scrubs
|
||||
// #4298 Standards+Spec review: `runMsdTools` (tests/helpers.cjs) scrubs
|
||||
// SESSION_IDENTITY_ENV_KEYS + config-location env vars before spawning,
|
||||
// because an ambient developer/CI value (e.g. a real ~/.gsd/config.json
|
||||
// location override) can silently change what a gsd-tools child resolves
|
||||
// — documented there as #2665. This harness spawns `gsd_run` the exact
|
||||
// because an ambient developer/CI value (e.g. a real ~/.msd/config.json
|
||||
// location override) can silently change what a msd-tools child resolves
|
||||
// — documented there as #2665. This harness spawns `msd_run` the exact
|
||||
// same way (indirectly, via the extracted resolution script), so it needs
|
||||
// the same hermeticity: spread the exported `TEST_ENV_BASE` (every
|
||||
// scrub-listed key set to '') AFTER `...process.env` but BEFORE the two
|
||||
// intentional overrides below, so ambient state is blanked and only
|
||||
// RUNTIME_DIR/GSD_TEST_MODE are deliberately set.
|
||||
// RUNTIME_DIR/MSD_TEST_MODE are deliberately set.
|
||||
const stdout = execFileSync('bash', ['-c', script], {
|
||||
cwd,
|
||||
env: { ...process.env, ...TEST_ENV_BASE, RUNTIME_DIR: REPO_ROOT, GSD_TEST_MODE: '1' },
|
||||
env: { ...process.env, ...TEST_ENV_BASE, RUNTIME_DIR: REPO_ROOT, MSD_TEST_MODE: '1' },
|
||||
encoding: 'utf8',
|
||||
timeout: TDD_BACKEND_SCRIPT_TIMEOUT_MS,
|
||||
});
|
||||
@@ -271,7 +271,7 @@ class TddWalk {
|
||||
* @returns {TddWalk}
|
||||
*/
|
||||
static create(opts = {}) {
|
||||
const { prefix = 'gsd-tdd-walk-' } = opts;
|
||||
const { prefix = 'msd-tdd-walk-' } = opts;
|
||||
const dir = createTempProject(prefix);
|
||||
return new TddWalk(dir);
|
||||
}
|
||||
@@ -288,7 +288,7 @@ class TddWalk {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve TDD-applicability via the real CLI verb (`gsd_run query
|
||||
* Resolve TDD-applicability via the real CLI verb (`msd_run query
|
||||
* phase.tdd-applicable`), reusing Phase 1's proven invocation
|
||||
* (`tests/phase-tdd-applicable.test.cjs`).
|
||||
*
|
||||
@@ -300,7 +300,7 @@ class TddWalk {
|
||||
const { cliFlag = false } = opts;
|
||||
const argv = ['query', 'phase.tdd-applicable', this.planPath];
|
||||
if (cliFlag) argv.push('--cli-flag');
|
||||
const result = runGsdTools(argv, this.dir);
|
||||
const result = runMsdTools(argv, this.dir);
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error, exitCode: result.exitCode ?? null };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user